feat: complete character list

im too sleepless to document the commits
This commit is contained in:
2026-03-22 11:12:56 +07:00 Unverified
parent 39d21ef9ba
commit e228e47ceb
4 changed files with 67 additions and 39 deletions
+23 -1
View File
@@ -2,9 +2,10 @@ import { readdirSync } from "node:fs";
import { mkdir, readdir, rm } from "node:fs/promises"; import { mkdir, readdir, rm } from "node:fs/promises";
import { join } from "node:path"; import { join } from "node:path";
import { file, hash, write } from "bun"; import { file, hash, write } from "bun";
import { TTLCache } from "enka.ts"; import { ResolvedPlayerInfo, TTLCache } from "enka.ts";
import { import {
AvatarStore, AvatarStore,
ElementList,
EnkaLocs, EnkaLocs,
EnkaNamecards, EnkaNamecards,
EnkaPfps, EnkaPfps,
@@ -199,3 +200,24 @@ export async function getEnkaStore<K extends keyof StoreTypeMap>(
cache.set(`store.${key}`, data, 86400); // 24 hours cache.set(`store.${key}`, data, 86400); // 24 hours
return data; return data;
} }
export function getAvatar(
store: AvatarStore,
playerInfo: ResolvedPlayerInfo,
id: number,
) {
return (
Object.entries(store).find(
([avatarId, avatar]) =>
avatarId.split("-")[0] === `${id}` &&
avatar.Element ===
ElementList[
(
playerInfo.showAvatarInfoList.find(
(e) => e.avatar.avatarId === id,
) as { energyType: number } | undefined
)?.energyType ?? 0
],
)?.[1] || store[id]
);
}
+9 -16
View File
@@ -22,13 +22,13 @@ import { star } from "@/assets/stars";
import { TalentSvg } from "@/assets/talent"; import { TalentSvg } from "@/assets/talent";
import { WeaponGlazeSvg } from "@/assets/weapon-glaze"; import { WeaponGlazeSvg } from "@/assets/weapon-glaze";
import type { App } from "@/index"; import type { App } from "@/index";
import { assets, batchStore } from "@/lib/assets"; import { assets, batchStore, getAvatar } from "@/lib/assets";
import { Deduplicator } from "@/lib/dedupe"; import { Deduplicator } from "@/lib/dedupe";
import { cardErrorLocs } from "@/lib/locs"; import { cardErrorLocs } from "@/lib/locs";
import { createLogger } from "@/lib/logger"; import { createLogger } from "@/lib/logger";
import { mockData } from "@/lib/mock"; import { mockData } from "@/lib/mock";
import { buildDisplayRows } from "@/lib/rows"; import { buildDisplayRows } from "@/lib/rows";
import { ElementList, Locale, SubstatOrder } from "@/type/enka"; import { Locale, SubstatOrder } from "@/type/enka";
import { PercentageFightProp } from "@/type/fightPropMap"; import { PercentageFightProp } from "@/type/fightPropMap";
import { ErrorPage } from "./error"; import { ErrorPage } from "./error";
import { ListPage } from "./list"; import { ListPage } from "./list";
@@ -106,7 +106,7 @@ export function card(app: App) {
) )
.get( .get(
"/genshin/:uid", "/genshin/:uid",
async ({ params: { uid }, query: { debug }, log }) => { async ({ params: { uid }, query: { debug, lang }, log }) => {
// #region Character List // #region Character List
const [[avatarStore, locs, pfps, namecards], data] = const [[avatarStore, locs, pfps, namecards], data] =
await Promise.all([ await Promise.all([
@@ -132,6 +132,7 @@ export function card(app: App) {
namecards[`${data.playerInfo.nameCardId ?? 210001}`].Icon, namecards[`${data.playerInfo.nameCardId ?? 210001}`].Icon,
namecards, namecards,
avatarStore, avatarStore,
locs[lang ?? "en"],
log, log,
); );
}, },
@@ -210,19 +211,11 @@ export function card(app: App) {
); );
if (!char) return error(400, errorLoc.notInShowcase); if (!char) return error(400, errorLoc.notInShowcase);
const avatar = const avatar = getAvatar(
Object.entries(avatarStore).find( avatarStore,
([avatarId, avatar]) => data.playerInfo,
avatarId.split("-")[0] === `${char.avatar.avatarId}` && char.avatar.avatarId,
avatar.Element === );
ElementList[
(
data.playerInfo.showAvatarInfoList.find(
(e) => e.avatar.avatarId === char.avatar.avatarId,
) as { energyType: number } | undefined
)?.energyType ?? 0
],
)?.[1] || avatarStore[char.avatar.avatarId];
if (!avatar) return error(400, errorLoc.unsupported); if (!avatar) return error(400, errorLoc.unsupported);
if (!char.equipList) if (!char.equipList)
+34 -20
View File
@@ -1,8 +1,8 @@
import { ResolvedRoot } from "enka.ts"; import { ResolvedRoot } from "enka.ts";
import { assets } from "@/lib/assets"; import { assets, getAvatar } from "@/lib/assets";
import { Logger } from "@/lib/logger"; import { Logger } from "@/lib/logger";
import { CharacterNamecardMap } from "@/lib/namecards"; import { CharacterNamecardMap } from "@/lib/namecards";
import { AvatarStore, EnkaNamecards } from "@/type/enka"; import { AvatarStore, EnkaLocsLanguage, EnkaNamecards } from "@/type/enka";
import { CardResponse } from "./response"; import { CardResponse } from "./response";
export async function ListPage( export async function ListPage(
@@ -11,6 +11,7 @@ export async function ListPage(
namecard: string, namecard: string,
namecards: EnkaNamecards, namecards: EnkaNamecards,
avatars: AvatarStore, avatars: AvatarStore,
locs: EnkaLocsLanguage,
log: Logger, log: Logger,
) { ) {
return CardResponse( return CardResponse(
@@ -66,8 +67,8 @@ export async function ListPage(
<div tw="flex items-center font-[ShinShin]"> <div tw="flex items-center font-[ShinShin]">
{p.finishAchievementNum} {p.finishAchievementNum}
</div> </div>
<div tw="text-[.9em] flex items-center justify-end opacity-50 pb-2"> <div tw="text-[.75em] flex items-center justify-end opacity-50 pb-2 whitespace-nowrap text-ellipsis">
Achievements {locs.achievements}
</div> </div>
{p.towerFloorIndex && ( {p.towerFloorIndex && (
<> <>
@@ -76,8 +77,8 @@ export async function ListPage(
{p.towerFloorIndex}-{p.towerLevelIndex} {p.towerFloorIndex}-{p.towerLevelIndex}
{p.towerStarIndex ? ` | ${p.towerStarIndex}★` : ""} {p.towerStarIndex ? ` | ${p.towerStarIndex}★` : ""}
</div> </div>
<div tw="text-[.9em] flex items-center justify-end opacity-50 pb-2"> <div tw="text-[.75em] flex items-center justify-end opacity-50 pb-2 whitespace-nowrap text-ellipsis">
Spiral Abyss {locs.abyss}
</div> </div>
</> </>
)} )}
@@ -89,8 +90,8 @@ export async function ListPage(
alt="theater" alt="theater"
/> />
<div tw="flex items-center font-[ShinShin]">{`${p.theaterActIndex} | ${p.theaterStarIndex}`}</div> <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"> <div tw="text-[.75em] flex items-center justify-end opacity-50 pb-2 whitespace-nowrap text-ellipsis">
Theater {locs.theater}
</div> </div>
</> </>
)} )}
@@ -104,8 +105,8 @@ export async function ListPage(
<div tw="flex items-center font-[ShinShin]"> <div tw="flex items-center font-[ShinShin]">
{p.stygianSeconds}s {p.stygianSeconds}s
</div> </div>
<div tw="text-[.9em] flex items-center justify-end opacity-50 pb-2"> <div tw="text-[.75em] flex items-center justify-end opacity-50 pb-2 whitespace-nowrap text-ellipsis">
Stygian Onslaught {locs.stygian}
</div> </div>
</> </>
)} )}
@@ -113,14 +114,14 @@ export async function ListPage(
</div> </div>
</div> </div>
{/* character list (for some reason ________ absolute hates me) */} {/* character list (for some reason ________ absolute hates me) */}
<div tw="absolute top-0 right-0 w-[62.5%] h-[117%]"> <div tw="absolute top-0 right-0 w-[62.5%] h-[97vh]">
<div tw="flex relative flex-wrap gap-x-[1.25%] gap-y-[0.75%] p-[.5em] w-full"> <div tw="flex relative flex-wrap gap-x-[1.25%] gap-y-[0.75%] p-[.5em] w-full h-full">
{p.showAvatarInfoList.map((a) => ( {p.showAvatarInfoList.map((a) => (
<div tw="w-[49.625%] bg-black/50 relative overflow-hidden rounded-lg flex"> <div tw="w-[49.625%] h-[16.04166667%] bg-black/50 relative overflow-hidden rounded-lg flex">
<img <img
src={`/namecard/${a.avatar.avatarId}`} src={`/namecard/${a.avatar.avatarId}`}
alt="namecard" alt="namecard"
tw="absolute top-0 left-0 right-0 bottom-0 object-cover brightness-50" tw="absolute top-0 left-0 right-0 bottom-0 object-cover blur-[2px] brightness-50"
/> />
<img <img
src={`/pfp/${a.avatar.avatarId}`} src={`/pfp/${a.avatar.avatarId}`}
@@ -132,9 +133,23 @@ export async function ListPage(
alt="pfp" alt="pfp"
tw="absolute top-0 left-0 bottom-0 aspect-square" tw="absolute top-0 left-0 bottom-0 aspect-square"
/> />
<div tw="ml-[20%] p-[.25em] font-[ShinShin] flex flex-col"> <div tw="ml-[22.5%] flex">
<div>{a.avatar.name}</div> <hr tw="border-none h-[90%] w-px mt-[5%] bg-white/60" />
<div>Lv. {a.level}</div> <div tw="p-[.25em] font-[ShinShin] flex flex-col grow">
<div>{a.avatar.name}</div>
<div>
{locs.level} {a.level}
</div>
</div>
</div>
<div tw="absolute bottom-[.2em] right-[.2em] leading-none p-[.2em] rounded-md bg-black/50">
{uid
.slice(-7)
.split("")
.map((c, i) => (
<div style={{ opacity: i / 5 }}>{c}</div>
))}
/{a.avatar.avatarId}
</div> </div>
</div> </div>
))} ))}
@@ -155,6 +170,7 @@ export async function ListPage(
}, },
...(p.showAvatarInfoList ...(p.showAvatarInfoList
.flatMap((a) => { .flatMap((a) => {
const avatar = getAvatar(avatars, p, a.avatar.avatarId);
const card = const card =
namecards[`${CharacterNamecardMap[`${a.avatar.avatarId}`]}`]; namecards[`${CharacterNamecardMap[`${a.avatar.avatarId}`]}`];
return [ return [
@@ -166,9 +182,7 @@ export async function ListPage(
: null, : null,
{ {
src: `/pfp/${a.avatar.avatarId}`, src: `/pfp/${a.avatar.avatarId}`,
data: assets.load( data: assets.load(avatar.SideIconName.replace("_Side", "")),
avatars[a.avatar.avatarId].SideIconName.replace("_Side", ""),
),
}, },
]; ];
}) })
+1 -2
View File
@@ -24,8 +24,7 @@ export async function CardResponse(
}} }}
> >
{children} {children}
<div tw="grow" /> <div tw="absolute bottom-0 left-0 ml-[.5em] flex items-center">
<div tw="ml-[.5em] flex items-center">
<div tw="text-[2em] font-[shicon] pb-[.1em] -mr-[.3em] text-white/25"> <div tw="text-[2em] font-[shicon] pb-[.1em] -mr-[.3em] text-white/25">
</div> </div>