feat: artifact section
This commit is contained in:
+140
-18
@@ -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;
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user