feat: artifact section

This commit is contained in:
2026-03-20 21:51:47 +07:00 Unverified
parent 4ca75243d4
commit 9b5e776f40
8 changed files with 219 additions and 84 deletions
+140 -18
View File
@@ -3,6 +3,7 @@ import { ImageResponse } from "@takumi-rs/image-response";
import { file, hash } from "bun";
import { NotFoundError, status, t } from "elysia";
import {
EquipType,
fetchEnka,
InternalError,
InvalidFormatError,
@@ -26,6 +27,7 @@ import { AssetManager, getEnkaStores } from "@/lib/assets";
import { Deduplicator } from "@/lib/dedupe";
import { mockData } from "@/lib/mock";
import { buildDisplayRows } from "@/lib/rows";
import { SubstatOrder } from "@/type/enka";
import { PercentageFightProp } from "@/type/fightPropMap";
const assets = new AssetManager();
@@ -36,7 +38,6 @@ const font = file(fontFile);
const shade = file(shadeSvg);
queueMicrotask(async () => {
const assets = new AssetManager();
assets.getMissingAssets();
setInterval(() => {
@@ -146,6 +147,51 @@ export function card(app: App) {
}
}
// #region Processing
const charLvl = char.propMap?.[4001].val ?? 1;
const ascension = char.propMap?.[1002].val ?? 1;
const promoteMaxLevelMap = [20, 40, 50, 60, 70, 80, 90, 100];
const charLvlMax =
ascension * 10 +
(ascension > 0 ? 10 : 0) +
20 +
(charLvl > 90 ? 10 : 0);
const statRows = buildDisplayRows(char, locs);
const artifacts = (
Object.keys(EquipType) as (keyof typeof EquipType)[]
).map((t) => {
const a = char.equipList!.find((e) => e.flat.equipType === t);
if (!a) return null;
return {
set: a.flat.setId!, // it is an artifact, therefore this always exist
stars: a.flat.rankLevel,
level: a.reliquary!.level - 1,
icon: `/ui/${a.flat.icon}.png`,
main: a.flat.reliquaryMainstat!,
sub: a.flat.reliquarySubstats!.sort(
(a, b) =>
(SubstatOrder[a.appendPropId] ?? 999) -
(SubstatOrder[b.appendPropId] ?? 999),
),
};
});
const setCounts = artifacts.reduce<Record<number, number>>(
(acc, a) => {
if (!a) return acc;
acc[a.set] = (acc[a.set] || 0) + 1;
return acc;
},
{},
);
const artifactSets = Object.entries(setCounts)
.filter(([, count]) => count >= 2)
.map(([set, count]) => ({
set: locs.en[relicStore.Sets[set].Name],
count,
}));
// #region Assets loading
const resources = await Promise.all([
assets
@@ -165,6 +211,21 @@ export function card(app: App) {
data: await assets.load(path),
}),
),
...artifacts
.filter(
(x): x is NonNullable<(typeof artifacts)[number]> => !!x,
)
.map(async (art) => ({
src: art.icon,
data: await assets.hook(art.icon, (buf) =>
featherImage(buf, {
passes: [
[0, 0.7, 0, 0],
[0, 0.6, 0, 0],
],
}),
),
})),
assets
.hook(
char.costumeId
@@ -173,7 +234,11 @@ export function card(app: App) {
"AvatarIcon_Side",
"Gacha_AvatarImg",
),
(buf) => featherImage(buf, [0, 0.3, 0, 0]),
(buf) =>
featherImage(buf, {
passes: [[0, 0.3, 0, 0]],
ratio: 1024 / 1124,
}),
)
.then(async (data) => ({
src: "/avatar",
@@ -199,19 +264,6 @@ export function card(app: App) {
`${Date.now() - start}ms - Image resources loaded\n${resources.map((r) => `- ${r.src} ${(r.data.byteLength / 1000).toFixed(2)}Kb`).join("\n")}`,
);
// #region Processing
const charLvl = char.propMap?.[4001].val ?? 1;
const ascension = char.propMap?.[1002].val ?? 1;
const promoteMaxLevelMap = [20, 40, 50, 60, 70, 80, 90, 100];
const charLvlMax =
ascension * 10 +
(ascension > 0 ? 10 : 0) +
20 +
(charLvl > 90 ? 10 : 0);
const statRows = buildDisplayRows(char, locs);
const artifacts = char.equipList;
if (debug) {
console.log(
`${Date.now() - start}ms - Debug endpoint, returning JSON`,
@@ -231,6 +283,8 @@ export function card(app: App) {
char,
weapon,
statRows,
artifacts,
artifactSets,
},
null,
2,
@@ -404,8 +458,8 @@ export function card(app: App) {
}}
/>
<div tw="absolute w-full top-[93%] flex justify-center">
{Array.from({ length: weapon.store.Rarity }, (_, i) =>
star(i),
{Array.from({ length: weapon.store.Rarity }, () =>
star(".7em"),
)}
</div>
</div>
@@ -503,7 +557,74 @@ export function card(app: App) {
</div>
</div>
{/* right section */}
<div tw="absolute top-0 bottom-0 right-0 w-[23.1em] p-[.7em]"></div>
<div tw="absolute top-0 bottom-0 right-0 w-[23.1em] p-[.7em] flex flex-col justify-between">
{artifacts.map((a) =>
a ? (
<div tw="h-[18%] bg-[#0000004f] rounded-[.3em] relative text-white overflow-hidden flex justify-between gap-[.75em]">
<img
src={a.icon}
alt="artifact"
tw="absolute top-1/2 -left-[10%] w-[45%] -translate-y-1/2 -translate-x-[10%] object-contain"
width={256}
height={256}
/>
{/* main */}
<div tw="h-full w-[30%] text-right shrink-0 flex flex-col justify-center">
<EnkaIcon
icon={a.main.mainPropId}
tw="h-[1em] w-[1em] ml-auto max-h-[22%]"
/>
<div
tw="text-[1.1em] py-[2%] ml-auto"
style={{ textShadow: "0 .07em .1em black" }}
>
{PercentageFightProp.includes(
a.main
.mainPropId as (typeof PercentageFightProp)[number],
)
? `${a.main.statValue.toFixed(1)}%`
: a.main.statValue.toLocaleString()}
</div>
<div tw="flex grow max-h-[20%] items-end justify-end gap-[.75em]">
<div>
{Array.from({ length: a.stars ?? 0 }, () =>
star(".6em"),
)}
</div>
<div tw="text-[75%] leading-none bg-black/50 px-[.25em] py-[.2em] rounded-[.2em] -mb-[.2em]">
+{a.level}
</div>
</div>
</div>
{/* sep */}
<hr tw="border-none h-[84%] w-[2px] mt-[2%] bg-white/60" />
{/* substats */}
<div tw="flex flex-col flex-wrap mt-[.35em] mb-[.35em] mr-[.35em] w-full">
{a.sub.map((s, i) => (
<div
tw={`flex items-center gap-[.25em] w-1/2 h-1/2 ${i === 3 ? "opacity-80" : ""}`}
>
<EnkaIcon
icon={s.appendPropId}
tw="h-[1em] w-[1em]"
/>
<div>
+
{PercentageFightProp.includes(
s.appendPropId as (typeof PercentageFightProp)[number],
)
? `${s.statValue.toFixed(1)}%`
: s.statValue.toLocaleString()}
</div>
</div>
))}
</div>
</div>
) : (
<div tw="h-[18%] bg-[#0000004f] rounded-[.3em] opacity-20" />
),
)}
</div>
</div>,
{
fonts: [await font.arrayBuffer()],
@@ -553,6 +674,7 @@ export function card(app: App) {
);
else if (err instanceof InternalError)
return status(502, "Internal server error");
console.error(err);
throw err;
}
},