feat: character list
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
"@takumi-rs/image-response": "^0.71.7",
|
||||
"@types/react": "^19.2.14",
|
||||
"elysia": "latest",
|
||||
"enka.ts": "^1.1.9",
|
||||
"enka.ts": "^1.1.10",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
},
|
||||
@@ -114,7 +114,7 @@
|
||||
|
||||
"elysia": ["[email protected]", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-2UlmNEjPJVA/WZVPYKy+KdsrfFwwNlqSBW1lHz6i2AHc75k7gV4Rhm01kFeotH7PDiHIX2G8X3KnRPc33SGVIg=="],
|
||||
|
||||
"enka.ts": ["[email protected].9", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-6dBNMIQ/LQ2y6RtcxJCGBjTPOyGcF93OdGbxJ5NW/p9KZ+0Jaqj4dZfrVvYpynbBMGj1NNuvIW3f+g6m5SGrlg=="],
|
||||
"enka.ts": ["[email protected].10", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-Lf6nEc2kPiycqS4Is4WxJI8iGMWcOYOphEX42TTy1zn6dizTDjoF8ViD2L2pAjf98gSXz5DzIA6uK0/1AfRVEg=="],
|
||||
|
||||
"exact-mirror": ["[email protected]", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-+MeEmDcLA4o/vjK2zujgk+1VTxPR4hdp23qLqkWfStbECtAq9gmsvQa3LW6z/0GXZyHJobrCnmy1cdeE7BjsYg=="],
|
||||
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@
|
||||
"@takumi-rs/image-response": "^0.71.7",
|
||||
"@types/react": "^19.2.14",
|
||||
"elysia": "latest",
|
||||
"enka.ts": "^1.1.9",
|
||||
"enka.ts": "^1.1.10",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4"
|
||||
},
|
||||
|
||||
+2
-1
@@ -37,10 +37,11 @@ export class AssetManager {
|
||||
...Object.values(a.Skills),
|
||||
...Object.values(a.Costumes ?? {}).flatMap((c) => [c.Art, c.SideIcon]),
|
||||
a.SideIconName.replace("AvatarIcon_Side", "Gacha_AvatarImg"),
|
||||
a.SideIconName.replace("_Side", ""),
|
||||
]),
|
||||
...Object.values(weapons).flatMap((w) => [w.Icon, w.AwakenIcon]),
|
||||
...Object.values(relics.Items).map((r) => r.Icon),
|
||||
...Object.values(pfps).map((p) => p.IconPath),
|
||||
...Object.values(pfps).map((p) => p.IconPath.replace("_Circle", "")),
|
||||
...Object.values(namecards).map((p) => p.Icon),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -50,3 +50,5 @@ export function createLogger(label?: string) {
|
||||
console.write(normalized);
|
||||
};
|
||||
}
|
||||
|
||||
export type Logger = ReturnType<typeof createLogger>;
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
// GENERATED BY util/genNamecardRelation
|
||||
|
||||
export const CharacterNamecardMap: Record<string, number> = {
|
||||
"10000002": 210081,
|
||||
"10000003": 210008,
|
||||
"10000006": 210011,
|
||||
"10000014": 210009,
|
||||
"10000015": 210010,
|
||||
"10000016": 210005,
|
||||
"10000020": 210006,
|
||||
"10000021": 210003,
|
||||
"10000022": 210007,
|
||||
"10000023": 210017,
|
||||
"10000024": 210039,
|
||||
"10000025": 210018,
|
||||
"10000026": 210058,
|
||||
"10000027": 210038,
|
||||
"10000029": 210004,
|
||||
"10000030": 210044,
|
||||
"10000031": 210013,
|
||||
"10000032": 210016,
|
||||
"10000033": 210046,
|
||||
"10000034": 210014,
|
||||
"10000035": 210019,
|
||||
"10000036": 210040,
|
||||
"10000037": 210053,
|
||||
"10000038": 210054,
|
||||
"10000039": 210043,
|
||||
"10000041": 210015,
|
||||
"10000042": 210020,
|
||||
"10000043": 210012,
|
||||
"10000044": 210045,
|
||||
"10000045": 210063,
|
||||
"10000046": 210059,
|
||||
"10000047": 210073,
|
||||
"10000048": 210069,
|
||||
"10000049": 210082,
|
||||
"10000050": 210101,
|
||||
"10000051": 210070,
|
||||
"10000052": 210092,
|
||||
"10000053": 210083,
|
||||
"10000054": 210093,
|
||||
"10000055": 210104,
|
||||
"10000056": 210094,
|
||||
"10000057": 210105,
|
||||
"10000058": 210112,
|
||||
"10000059": 210122,
|
||||
"10000060": 210119,
|
||||
"10000061": 210162,
|
||||
"10000062": 210095,
|
||||
"10000063": 210107,
|
||||
"10000064": 210108,
|
||||
"10000065": 210120,
|
||||
"10000066": 210114,
|
||||
"10000067": 210125,
|
||||
"10000068": 210126,
|
||||
"10000069": 210124,
|
||||
"10000070": 210136,
|
||||
"10000071": 210134,
|
||||
"10000072": 210135,
|
||||
"10000073": 210140,
|
||||
"10000074": 210141,
|
||||
"10000075": 210143,
|
||||
"10000076": 210144,
|
||||
"10000077": 210148,
|
||||
"10000078": 210147,
|
||||
"10000079": 210152,
|
||||
"10000080": 210153,
|
||||
"10000081": 210156,
|
||||
"10000082": 210155,
|
||||
"10000083": 210166,
|
||||
"10000084": 210165,
|
||||
"10000085": 210167,
|
||||
"10000086": 210175,
|
||||
"10000087": 210174,
|
||||
"10000088": 210181,
|
||||
"10000089": 210180,
|
||||
"10000090": 210185,
|
||||
"10000091": 210184,
|
||||
"10000092": 210188,
|
||||
"10000093": 210187,
|
||||
"10000094": 210193,
|
||||
"10000095": 210201,
|
||||
"10000096": 210195,
|
||||
"10000097": 210202,
|
||||
"10000098": 210200,
|
||||
"10000099": 210205,
|
||||
"10000100": 210210,
|
||||
"10000101": 210208,
|
||||
"10000102": 210209,
|
||||
"10000103": 210219,
|
||||
"10000104": 210221,
|
||||
"10000105": 210222,
|
||||
"10000106": 210226,
|
||||
"10000107": 210227,
|
||||
"10000108": 210228,
|
||||
"10000109": 210233,
|
||||
"10000110": 210237,
|
||||
"10000111": 210236,
|
||||
"10000112": 210241,
|
||||
"10000113": 210242,
|
||||
"10000114": 210244,
|
||||
"10000115": 210245,
|
||||
"10000116": 121279,
|
||||
"10000119": 210253,
|
||||
"10000120": 210254,
|
||||
"10000121": 121279,
|
||||
"10000122": 210261,
|
||||
"10000123": 210263,
|
||||
"10000124": 210264,
|
||||
"10000125": 210267,
|
||||
"10000126": 121462,
|
||||
"10000127": 210269,
|
||||
"10000128": 210273,
|
||||
};
|
||||
+6
-1
@@ -107,6 +107,7 @@ export function card(app: App) {
|
||||
.get(
|
||||
"/genshin/:uid",
|
||||
async ({ params: { uid }, query: { debug }, log }) => {
|
||||
// #region Character List
|
||||
const [[avatarStore, locs, pfps, namecards], data] =
|
||||
await Promise.all([
|
||||
deduper.acquire("stores", () =>
|
||||
@@ -128,7 +129,10 @@ export function card(app: App) {
|
||||
pfps[
|
||||
`${data.playerInfo.profilePicture.id ?? 1}`
|
||||
].IconPath.replace("_Circle", ""),
|
||||
namecards[`${data.playerInfo.namecardId ?? 210001}`].Icon,
|
||||
namecards[`${data.playerInfo.nameCardId ?? 210001}`].Icon,
|
||||
namecards,
|
||||
avatarStore,
|
||||
log,
|
||||
);
|
||||
},
|
||||
{
|
||||
@@ -163,6 +167,7 @@ export function card(app: App) {
|
||||
description:
|
||||
"Generate a Genshin Impact character list card image from a UID.",
|
||||
},
|
||||
afterResponse: ({ log }) => log`Rendering completed`,
|
||||
},
|
||||
)
|
||||
.get(
|
||||
|
||||
+125
-1
@@ -1,16 +1,27 @@
|
||||
import { ResolvedRoot } from "enka.ts";
|
||||
import { assets } from "@/lib/assets";
|
||||
import { Logger } from "@/lib/logger";
|
||||
import { CharacterNamecardMap } from "@/lib/namecards";
|
||||
import { AvatarStore, EnkaNamecards } from "@/type/enka";
|
||||
import { CardResponse } from "./response";
|
||||
|
||||
export async function ListPage(
|
||||
{ playerInfo: p, uid }: ResolvedRoot,
|
||||
pfp: string,
|
||||
namecard: string,
|
||||
namecards: EnkaNamecards,
|
||||
avatars: AvatarStore,
|
||||
log: Logger,
|
||||
) {
|
||||
return CardResponse(
|
||||
<div tw="flex font-[Open_Sans]">
|
||||
{/* left bar */}
|
||||
<div tw="flex flex-col justify-between p-[1em] w-[37.5%]">
|
||||
<div
|
||||
tw="flex flex-col justify-between p-[1em] w-[37.5%]"
|
||||
style={{
|
||||
textShadow: "0px .08em .2em black",
|
||||
}}
|
||||
>
|
||||
{/* profile */}
|
||||
<div tw="flex gap-[1em] relative">
|
||||
<div tw="absolute -top-1 -left-1 -bottom-1 -right-1 overflow-hidden rounded-[.3em] opacity-50">
|
||||
@@ -45,11 +56,124 @@ export async function ListPage(
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div tw="pt-[.2em]">
|
||||
<div tw="opacity-50">UID</div>
|
||||
{`: ${uid}`}
|
||||
</div>
|
||||
<div tw="text-[1.2em] p-[.7em]">
|
||||
<div tw="grid grid-cols-[2em_1fr_1.25fr] gap-y-[1em]">
|
||||
<img tw="aspect-square h-[2em]" src="/i/achi" alt="achievements" />
|
||||
<div tw="flex items-center font-[ShinShin]">
|
||||
{p.finishAchievementNum}
|
||||
</div>
|
||||
<div tw="text-[.9em] flex items-center justify-end opacity-50 pb-2">
|
||||
Achievements
|
||||
</div>
|
||||
{p.towerFloorIndex && (
|
||||
<>
|
||||
<img tw="aspect-square h-[2em]" src="/i/abyss" alt="abyss" />
|
||||
<div tw="flex items-center font-[ShinShin]">
|
||||
{p.towerFloorIndex}-{p.towerLevelIndex}
|
||||
{p.towerStarIndex ? ` | ${p.towerStarIndex}★` : ""}
|
||||
</div>
|
||||
<div tw="text-[.9em] flex items-center justify-end opacity-50 pb-2">
|
||||
Spiral Abyss
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{p.theaterActIndex && (
|
||||
<>
|
||||
<img
|
||||
tw="aspect-square h-[2em]"
|
||||
src="/i/theater"
|
||||
alt="theater"
|
||||
/>
|
||||
<div tw="flex items-center font-[ShinShin]">{`${p.theaterActIndex} | ${p.theaterStarIndex}`}</div>
|
||||
<div tw="text-[.9em] flex items-center justify-end opacity-50 pb-2">
|
||||
Theater
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{p.stygianSeconds && (
|
||||
<>
|
||||
<img
|
||||
tw="aspect-square h-[2em]"
|
||||
src="/i/stygian"
|
||||
alt="stygian"
|
||||
/>
|
||||
<div tw="flex items-center font-[ShinShin]">
|
||||
{p.stygianSeconds}s
|
||||
</div>
|
||||
<div tw="text-[.9em] flex items-center justify-end opacity-50 pb-2">
|
||||
Stygian Onslaught
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* character list (for some reason ________ absolute hates me) */}
|
||||
<div tw="absolute top-0 right-0 w-[62.5%] h-[117%]">
|
||||
<div tw="flex relative flex-wrap gap-x-[1.25%] gap-y-[0.75%] p-[.5em] w-full">
|
||||
{p.showAvatarInfoList.map((a) => (
|
||||
<div tw="w-[49.625%] bg-black/50 relative overflow-hidden rounded-lg flex">
|
||||
<img
|
||||
src={`/namecard/${a.avatar.avatarId}`}
|
||||
alt="namecard"
|
||||
tw="absolute top-0 left-0 right-0 bottom-0 object-cover brightness-50"
|
||||
/>
|
||||
<img
|
||||
src={`/pfp/${a.avatar.avatarId}`}
|
||||
alt="pfp"
|
||||
tw="absolute top-0 left-0 bottom-0 aspect-square brightness-0 scale-120 opacity-20"
|
||||
/>
|
||||
<img
|
||||
src={`/pfp/${a.avatar.avatarId}`}
|
||||
alt="pfp"
|
||||
tw="absolute top-0 left-0 bottom-0 aspect-square"
|
||||
/>
|
||||
<div tw="ml-[20%] p-[.25em] font-[ShinShin] flex flex-col">
|
||||
<div>{a.avatar.name}</div>
|
||||
<div>Lv. {a.level}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
[
|
||||
{ src: "/pfp", data: assets.load(pfp) },
|
||||
{ src: "/namecard", data: assets.load(namecard) },
|
||||
{ src: "/i/achi", data: assets.load("/img/achievements.png") },
|
||||
{ src: "/i/abyss", data: assets.load("/img/abyss.png") },
|
||||
{ src: "/i/theater", data: assets.load("/ui/UI_Icon_Tower_Star1.png") },
|
||||
{
|
||||
src: "/i/stygian",
|
||||
data: assets.load(
|
||||
`/ui/UI_LeyLineChallenge_Medal_${p.stygianIndex ?? 1}.png`,
|
||||
),
|
||||
},
|
||||
...(p.showAvatarInfoList
|
||||
.flatMap((a) => {
|
||||
const card =
|
||||
namecards[`${CharacterNamecardMap[`${a.avatar.avatarId}`]}`];
|
||||
return [
|
||||
card
|
||||
? {
|
||||
src: `/namecard/${a.avatar.avatarId}`,
|
||||
data: assets.load(card ? card.Icon : namecard),
|
||||
}
|
||||
: null,
|
||||
{
|
||||
src: `/pfp/${a.avatar.avatarId}`,
|
||||
data: assets.load(
|
||||
avatars[a.avatar.avatarId].SideIconName.replace("_Side", ""),
|
||||
),
|
||||
},
|
||||
];
|
||||
})
|
||||
.filter(Boolean) as { src: string; data: Promise<ArrayBuffer> }[]),
|
||||
],
|
||||
log,
|
||||
);
|
||||
}
|
||||
|
||||
+18
-5
@@ -1,7 +1,9 @@
|
||||
import { feature } from "bun:bundle";
|
||||
import ImageResponse from "@takumi-rs/image-response";
|
||||
import { MaybePromise } from "bun";
|
||||
import { ReactNode } from "react";
|
||||
import { font, iconFont, sansFont } from "@/assets/fonts";
|
||||
import { createLogger } from "@/lib/logger";
|
||||
|
||||
export async function CardResponse(
|
||||
children: ReactNode,
|
||||
@@ -9,6 +11,7 @@ export async function CardResponse(
|
||||
src: string;
|
||||
data: MaybePromise<ArrayBuffer | Buffer>;
|
||||
}>[] = [],
|
||||
log = createLogger("RES"),
|
||||
) {
|
||||
return new ImageResponse(
|
||||
<div
|
||||
@@ -41,11 +44,21 @@ export async function CardResponse(
|
||||
format: "webp",
|
||||
// same ratio as card
|
||||
width: 1228,
|
||||
fetchedResources: await Promise.all(assets).then((list) =>
|
||||
Promise.all(
|
||||
list.map(async (a) => ({ src: a.src, data: await a.data })),
|
||||
),
|
||||
),
|
||||
fetchedResources: await Promise.all(assets)
|
||||
.then((list) =>
|
||||
Promise.all(
|
||||
list.map(async (a) => ({ src: a.src, data: await a.data })),
|
||||
),
|
||||
)
|
||||
.then((l) => {
|
||||
const res = l.filter((r) => {
|
||||
if (r.data.byteLength) return true;
|
||||
if (!feature("API")) console.warn(`[DEV] ${r.src} is blank`);
|
||||
return false;
|
||||
});
|
||||
log`Image resources loaded\n${res.map((r) => `- ${r.src} ${(r.data.byteLength / 1000).toFixed(2)}Kb`).join("\n")}`;
|
||||
return res;
|
||||
}),
|
||||
height: 512,
|
||||
// drawDebugBorder: true,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { join } from "node:path";
|
||||
import { $, MaybePromise, write } from "bun";
|
||||
import { createLogger } from "@/lib/logger";
|
||||
|
||||
const log = createLogger();
|
||||
fetch("https://gi.yatta.moe/api/v2/en/avatar")
|
||||
.then((a) => a.json())
|
||||
.then(async (a) => {
|
||||
const avatars = Object.keys(a.data.items);
|
||||
log`fetch complete, ${avatars.length} avatars`;
|
||||
const map = Object.fromEntries(
|
||||
(
|
||||
await asyncMap(avatars, (a) =>
|
||||
fetch(`https://gi.yatta.moe/api/v2/en/avatar/${a}`)
|
||||
.then((c) => c.text())
|
||||
.then((t) => {
|
||||
try {
|
||||
const c = JSON.parse(t);
|
||||
log`${c.data.route}`;
|
||||
return [a, c.data.other?.nameCard?.id] as const;
|
||||
} catch {
|
||||
log`${a}: ${t}`;
|
||||
return [];
|
||||
}
|
||||
}),
|
||||
)
|
||||
).filter((e) => e[1]),
|
||||
);
|
||||
log`crawling complete`;
|
||||
const target = join(__dirname, "../src/lib/namecards.ts");
|
||||
await write(
|
||||
target,
|
||||
`// GENERATED BY util/genNamecardRelation\n\nexport const CharacterNamecardMap: Record<string, number> = ${JSON.stringify(map)}`,
|
||||
);
|
||||
await $`bun biome format ${target} --fix`;
|
||||
});
|
||||
|
||||
async function asyncMap<S, R>(arr: S[], fn: (a: S) => MaybePromise<R>) {
|
||||
const res: R[] = [];
|
||||
for (const ent of arr) res.push(await fn(ent));
|
||||
return res;
|
||||
}
|
||||
Reference in New Issue
Block a user