/* eslint-disable @next/next/no-img-element */ import { join } from "node:path"; import { ImageResponse } from "next/og"; import sharp from "sharp"; import { rarityGradient } from "@/lib/catalog/rarity"; import { artifactTierCount } from "@/lib/guides/artifacts"; import { getCachedPublicArtifactExport } from "@/lib/cache/public"; import { imageContentDisposition } from "@/lib/guides/export-image"; import { normalizeTargetStat, statPriorityColor, targetStatIconName, targetStatOptions } from "@/lib/guides/stats"; import { normalizeTalentPriorities, talentTypeLabel } from "@/lib/guides/talents"; const anuphanFonts = Promise.all( [ { subset: "latin", weight: 400 as const }, { subset: "thai", weight: 400 as const }, { subset: "latin", weight: 700 as const }, { subset: "thai", weight: 700 as const }, ].map(async ({ subset, weight }) => { const response = await fetch( `https://unpkg.com/@fontsource/anuphan@5.3.0/files/anuphan-${subset}-${weight}-normal.woff`, ); if (!response.ok) throw new Error(`Unable to load Anuphan ${subset} ${weight}`); return { name: "Anuphan", data: await response.arrayBuffer(), weight, style: "normal" as const, }; }), ).catch(() => []); function metric(value: string): string { return `${Number(value).toLocaleString("en-US", { maximumFractionDigits: 2 })}%`; } function statLabel(value: string): string { const labels: Record = { HP: "พลังชีวิต", ATK: "พลังโจมตี", DEF: "พลังป้องกัน", EM: "ความชำนาญธาตุ", ER: "ฟื้นฟูพลังงาน", "CRIT RATE": "อัตราคริ", "CRIT DAMAGE": "ความแรงคริ", "ATK%": "พลังโจมตี%", "HP%": "พลังชีวิต%", "DEF%": "พลังป้องกัน%", "CRIT Rate%/CRIT DMG%": "อัตราคริ/ความแรงคริ", "Pyro DMG%": "ความเสียหายธาตุไฟ%", "Hydro DMG%": "ความเสียหายธาตุน้ำ%", "Cryo DMG%": "ความเสียหายธาตุน้ำแข็ง%", "Electro DMG%": "ความเสียหายธาตุไฟฟ้า%", "Anemo DMG%": "ความเสียหายธาตุลม%", "Geo DMG%": "ความเสียหายธาตุดิน%", "Dendro DMG%": "ความเสียหายธาตุไม้%", "Physical DMG%": "ความเสียหายกายภาพ%", "Healing Bonus%": "โบนัสการรักษา%", }; return labels[value] ?? value; } async function pngDataUrl(url: string): Promise { try { const response = await fetch(url); if (!response.ok) return null; const png = await sharp(Buffer.from(await response.arrayBuffer())) .resize(96, 96, { fit: "contain" }) .png() .toBuffer(); return `data:image/png;base64,${png.toString("base64")}`; } catch { return null; } } async function localPngDataUrl(fileName: string, size: number | null = 96, white = false): Promise { let image = sharp(join(process.cwd(), "public", "icon", fileName)); if (white) image = image.negate({ alpha: false }); const png = await (size === null ? image : image.resize(size, size, { fit: "contain" })) .png() .toBuffer(); return `data:image/png;base64,${png.toString("base64")}`; } export async function GET( _request: Request, context: RouteContext<"/api/guides/[guideId]/artifacts/image">, ) { const { guideId } = await context.params; const guide = await getCachedPublicArtifactExport(guideId); if (!guide) return new Response("ไม่พบไกด์", { status: 404 }); const talents = normalizeTalentPriorities(guide.profile?.talents ?? []); const [fonts, groupAssets, statIconAssets, talentAssets, targetStatAssets] = await Promise.all([ anuphanFonts, Promise.all( guide.groups.map((group) => Promise.all( group.rows.map(async (row) => ({ first: await pngDataUrl(row.first.imageUrl), second: row.second ? await pngDataUrl(row.second.imageUrl) : null, })), ), ), ), Promise.all([ localPngDataUrl("Icon_Sands_of_Eon.webp"), localPngDataUrl("Icon_Goblet_of_Eonothem.webp"), localPngDataUrl("Icon_Circlet_of_Logos.webp"), ]), Promise.all(talents.map((talent) => { const catalogTalent = guide.character.talents.find((item) => item.type === talent.type); return catalogTalent?.iconUrl ? pngDataUrl(catalogTalent.iconUrl) : null; })), Promise.all(targetStatOptions.map((stat) => localPngDataUrl(`stats/${targetStatIconName(stat)}.webp`, 96, true))), ]); const profile = guide.profile; const groupRowHeights = guide.groups.map((group) => group.rows.map((row) => Math.max(76, 76 + (row.note ? Math.ceil(row.note.length / 58) * 22 : 0)))); const leftHeight = guide.groups.reduce((sum, _group, groupIndex) => sum + 68 + groupRowHeights[groupIndex].reduce((groupSum, rowHeight) => groupSum + rowHeight, 0), 0) + Math.max(0, guide.groups.length - 1) * 16; const detailHeight = profile ? 60 + profile.targets.length * 40 + (profile.talents.length ? 82 : 0) + (profile.note ? Math.max(48, Math.ceil(profile.note.length / 58) * 24) : 0) : 0; const rightHeight = profile ? 242 + (profile.substats.length ? 76 + profile.substats.length * 18 : 0) + (detailHeight > 60 ? detailHeight + 16 : 0) + 32 : 0; const height = Math.max(520, 190 + Math.max(leftHeight, rightHeight)); return new ImageResponse(
อาร์ติแฟกต์และสเตตัสที่แนะนำ
อาร์ติแฟกต์และค่าสถานะ
{guide.groups.map((group, groupIndex) => (
{group.name === "Late Game" ? "เลทเกม" : group.name}
{group.rows.map((row, rowIndex) => (
{groupAssets[groupIndex][rowIndex].first ? (
) : null} {row.second && groupAssets[groupIndex][rowIndex].second ? (
) : null}
{artifactTierCount(row.second?.name ?? null)}{" "} {row.first.name}{row.second ? ` + ${row.second.name}` : ""}
{row.note ?
{row.note}
: null}
{metric(row.percent)}
))}
))}
{profile ? (
ออฟหลัก
{[ { label: "นาฬิกา", values: profile.sands, icon: statIconAssets[0] }, { label: "ถ้วย", values: profile.goblet, icon: statIconAssets[1] }, { label: "หัว", values: profile.circlet, icon: statIconAssets[2] }, ].map((stat) => (
{stat.label}
{stat.values.length ? stat.values.map((value, index) => {statLabel(value)}) : "—"}
))}
{profile.substats.length ? (
ลำดับค่าสถานะ:
{profile.substats.map((stat, index) => {statLabel(stat)})}
) : null} {detailHeight > 60 ? (
รายละเอียดเพิ่มเติม
{profile.targets.length ? (
ค่าสถานะเป้าหมาย
{profile.targets.map((target, index) => { const iconIndex = targetStatOptions.findIndex((stat) => stat === normalizeTargetStat(target.stat)); return (
{iconIndex >= 0 ? : null}{normalizeTargetStat(target.stat) ?? target.stat}{target.value}
); })}
) : null} {talents.length ? (
ลำดับการอัพพรสวรรค์
{talents.map((talent, index) => (
{talentAssets[index] ? : null} {talent.type}{talent.level ? ` เลเวล ${talent.level}+` : ""} {talentTypeLabel(talent.type)} {index < talents.length - 1 ? {talent.relationToNext === "gte" ? ">=" : ">"} : null}
))}
) : null} {profile.note ? (
โน็ต
{profile.note}
) : null}
) : null}
) : null}
, { width: 1200, height, fonts: fonts.length ? fonts : undefined, headers: { "Cache-Control": "no-store", "Content-Disposition": imageContentDisposition(`${guide.guideName} อาร์ติแฟกต์`, "artifacts"), "Content-Type": "image/png", }, }, ); }