feat: enhance asset management with weapon and relic stores, add fight property enums

This commit is contained in:
2026-03-14 00:37:12 +07:00 Unverified
parent 1a823d1006
commit b28ac7b9db
8 changed files with 452 additions and 22 deletions
+4 -1
View File
@@ -12,4 +12,7 @@ To start the development server run:
bun run dev bun run dev
``` ```
Open http://localhost:3000/ with your browser to see the result. Open http://localhost:3000/ with your browser to see the result.
## Credit
enka
+1 -1
View File
@@ -20,7 +20,7 @@ build({
target: "bun", target: "bun",
entrypoints: ["src/index.ts"], entrypoints: ["src/index.ts"],
outdir: "dist", outdir: "dist",
features: ["FAKE_REQUIRE"], features: ["FAKE_REQUIRE", "API"],
}); });
for (const f of nativeFiles.scanSync()) { for (const f of nativeFiles.scanSync()) {
+18
View File
@@ -0,0 +1,18 @@
// source: buzzy
export const star = (key: number) => (
<svg
key={key}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 576 512"
style={{
fill: "rgba(255,204,50)",
filter: "drop-shadow(#e3721b 0px 0px 1px)",
height: 20,
width: 20,
}}
>
<title>Star</title>
<path d="M381.2 150.3l143.7 21.2c11.9 1.7 21.9 10.1 25.7 21.6 3.8 11.6.7 24.2-7.9 32.8L438.5 328.1l24.6 146.6c2 12-2.9 24.2-12.9 31.3-9.9 7.1-23 8-33.7 2.3l-128.4-68.5-128.3 68.5c-10.8 5.7-23.9 4.8-33.8-2.3-9.9-7.1-14.9-19.3-12.8-31.3l24.6-146.6L33.58 225.9c-8.61-8.6-11.67-21.2-7.89-32.8 3.77-11.5 13.74-19.9 25.73-21.6L195 150.3l64.4-132.33C264.7 6.954 275.9-.04 288.1-.04c12.3 0 23.5 6.994 28.8 18.01l64.3 132.33z"></path>
</svg>
);
+82
View File
@@ -0,0 +1,82 @@
import { CSSProperties } from "react";
export function WeaponGlazeSvg({
tw,
style,
}: {
tw: string;
style: CSSProperties;
}) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 7 1"
tw={tw}
style={style}
>
<title>Weapon Glaze</title>
<defs>
<filter
id="svg-Sheen__a"
width="1.028"
height="2.42"
x="-.014"
y="-.71"
color-interpolation-filters="sRGB"
>
<feGaussianBlur stdDeviation=".037"></feGaussianBlur>
</filter>
<filter
id="svg-Sheen__b"
width="1"
height="1.013"
x="0"
y="-.007"
style={{ colorInterpolationFilters: "sRGB" }}
>
<feGaussianBlur></feGaussianBlur>
</filter>
<filter
id="svg-Sheen__c"
width="1.004"
height="1.613"
x="-.002"
y="-.306"
style={{ colorInterpolationFilters: "sRGB" }}
>
<feGaussianBlur stdDeviation=".005"></feGaussianBlur>
</filter>
</defs>
<path
fill="currentColor"
d="m77.486 180.56 3.227-.063 3.227.063-3.227.063z"
filter="url(#svg-Sheen__a)"
style={{ mixBlendMode: "normal", filter: "url(#svg-Sheen__a)" }}
transform="matrix(.8616 0 0 3.6457 -66.042 -657.768)"
></path>
<path
fill="currentColor"
d="m77.457 180.56 3.256-.059 3.257.059-3.257.059z"
opacity=".7"
style={{
opacity: 0.7,
strokeWidth: "1.09436",
filter: "url(#svg-Sheen__b)",
}}
transform="matrix(.8168 0 0 1.54812 -62.427 -279.03)"
></path>
<path
fill="#fff"
d="m77.457 180.56 3.256-.021 3.257.021-3.257.021z"
opacity=".7"
style={{
opacity: 1,
mixBlendMode: "normal",
strokeWidth: "0.661912",
filter: 'url("#svg-Sheen__c")',
}}
transform="matrix(.81587 0 0 1 -62.351 -180.06)"
></path>
</svg>
);
}
+55 -6
View File
@@ -2,7 +2,7 @@ import { mkdir, readdir } 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 { TTLCache } from "enka.ts";
import { AvatarStore } from "@/type/enka"; import { AvatarStore, EnkaLocs, RelicStore, WeaponStore } from "@/type/enka";
export const cache = new TTLCache(); export const cache = new TTLCache();
@@ -10,17 +10,21 @@ export class AssetManager {
private cache = new Map<string, ArrayBuffer>(); private cache = new Map<string, ArrayBuffer>();
async getMissingAssets() { async getMissingAssets() {
const store = await getEnkaAvatarStore(); const avatars = await getEnkaAvatarStore();
const weapons = await getEnkaWeaponStore();
const relics = await getEnkaRelicStore();
await mkdir("store").catch(() => {}); await mkdir("store").catch(() => {});
const existing = await readdir("store"); const existing = await readdir("store");
const missing = new Set<string>(); const missing = new Set<string>();
Object.values(store) [
.flatMap((a) => [ ...Object.values(avatars).flatMap((a) => [
...a.Consts, ...a.Consts,
...Object.values(a.Skills), ...Object.values(a.Skills),
a.SideIconName.replace("AvatarIcon_Side", "Gacha_AvatarImg"), a.SideIconName.replace("AvatarIcon_Side", "Gacha_AvatarImg"),
]) ]),
.map((n) => existing.includes(hash(n).toString(36)) || missing.add(n)); ...Object.values(weapons).flatMap((w) => [w.Icon, w.AwakenIcon]),
...Object.values(relics.Items).map((r) => r.Icon),
].map((n) => existing.includes(hash(n).toString(36)) || missing.add(n));
console.log( console.log(
`${missing.size} assets missing, ${existing.length} assets in store`, `${missing.size} assets missing, ${existing.length} assets in store`,
@@ -89,6 +93,15 @@ export class AssetManager {
} }
} }
export async function getEnkaStores() {
return Promise.all([
getEnkaAvatarStore(),
getEnkaWeaponStore(),
getEnkaRelicStore(),
getEnkaLocs(),
]);
}
export async function getEnkaAvatarStore(): Promise<AvatarStore> { export async function getEnkaAvatarStore(): Promise<AvatarStore> {
const cached = cache.get("enkaAvatarStore"); const cached = cache.get("enkaAvatarStore");
if (cached) return cached as AvatarStore; if (cached) return cached as AvatarStore;
@@ -100,3 +113,39 @@ export async function getEnkaAvatarStore(): Promise<AvatarStore> {
cache.set("enkaAvatarStore", data, 86400); // 24 hours cache.set("enkaAvatarStore", data, 86400); // 24 hours
return data; return data;
} }
export async function getEnkaWeaponStore(): Promise<WeaponStore> {
const cached = cache.get("enkaWeaponStore");
if (cached) return cached as WeaponStore;
const response = await fetch(
"https://github.com/EnkaNetwork/API-docs/raw/refs/heads/master/store/gi/weapons.json",
);
const data = await response.json();
cache.set("enkaWeaponStore", data, 86400); // 24 hours
return data;
}
export async function getEnkaRelicStore(): Promise<RelicStore> {
const cached = cache.get("enkaRelicStore");
if (cached) return cached as RelicStore;
const response = await fetch(
"https://github.com/EnkaNetwork/API-docs/raw/refs/heads/master/store/gi/relics.json",
);
const data = await response.json();
cache.set("enkaRelicStore", data, 86400); // 24 hours
return data;
}
export async function getEnkaLocs(): Promise<EnkaLocs> {
const cached = cache.get("enkaLocs");
if (cached) return cached as EnkaLocs;
const response = await fetch(
"https://github.com/EnkaNetwork/API-docs/raw/refs/heads/master/store/gi/locs.json",
);
const data = await response.json();
cache.set("enkaLocs", data, 86400); // 24 hours
return data;
}
+41
View File
@@ -65,3 +65,44 @@ export type WeaponType =
| "WEAPON_BOW"; | "WEAPON_BOW";
export type QualityType = "QUALITY_ORANGE" | "QUALITY_PURPLE"; export type QualityType = "QUALITY_ORANGE" | "QUALITY_PURPLE";
export interface WeaponConfig {
Rarity: number;
WeaponType: number;
Icon: string;
AwakenIcon: string;
NameTextMapHash: number;
BaseProps: Record<string, number>;
PropGrowCurves: Record<string, number>;
BasePromote: number[];
RefineProps: Record<string, unknown>;
}
export type WeaponStore = Record<string, WeaponConfig>;
export interface RelicConfig {
Rarity: number;
EquipType: number;
Icon: string;
SetId: number;
}
export type RelicConfigMap = Record<string, RelicConfig>;
export interface RelicSetBonus {
Name: number; // TextMapHash
AddProps: Record<number, number>;
}
export type RelicSetMap = Record<number, RelicSetBonus>;
export interface RelicStore {
Items: RelicConfigMap;
Sets: RelicSetMap;
}
export interface EnkaLocs {
en: Record<string, string>;
}
+148
View File
@@ -0,0 +1,148 @@
enum FightPropMapEnum {
None = 0,
// Base Stats
BaseHP = 1, // FIGHT_PROP_BASE_HP
HP_Flat = 2, // FIGHT_PROP_HP (flat HP bonus)
HPPercentage = 3, // FIGHT_PROP_HP_PERCENT
BaseAttack = 4, // FIGHT_PROP_BASE_ATTACK
Attack_Flat = 5, // FIGHT_PROP_ATTACK (flat ATK bonus)
AttackPercentage = 6, // FIGHT_PROP_ATTACK_PERCENT
BaseDefense = 7, // FIGHT_PROP_BASE_DEFENSE
Defense_Flat = 8, // FIGHT_PROP_DEFENSE (flat DEF bonus)
DefensePercentage = 9, // FIGHT_PROP_DEFENSE_PERCENT
BaseSpeed = 10, // FIGHT_PROP_BASE_SPEED
SpeedPercentage = 11, // FIGHT_PROP_SPEED_PERCENT
HPMPPercentage = 12, // FIGHT_PROP_HP_MP_PERCENT
AttackMPPercentage = 13, // FIGHT_PROP_ATTACK_MP_PERCENT
// Critical Stats
CriticalRate = 20, // FIGHT_PROP_CRITICAL
AntiCritical = 21, // FIGHT_PROP_ANTI_CRITICAL
CriticalDamage = 22, // FIGHT_PROP_CRITICAL_HURT
// Energy & Mastery
EnergyRecharge = 23, // FIGHT_PROP_CHARGE_EFFICIENCY
AddHurt = 24, // FIGHT_PROP_ADD_HURT (all damage bonus)
SubHurt = 25, // FIGHT_PROP_SUB_HURT (all damage reduction)
HealingBonus = 26, // FIGHT_PROP_HEAL_ADD
IncomingHealingBonus = 27, // FIGHT_PROP_HEALED_ADD
ElementalMastery = 28, // FIGHT_PROP_ELEMENT_MASTERY
// Physical Stats
PhysicalResistance = 29, // FIGHT_PROP_PHYSICAL_SUB_HURT
PhysicalDamageBonus = 30, // FIGHT_PROP_PHYSICAL_ADD_HURT
// Defense Ignore
DefenseIgnoreRatio = 31, // FIGHT_PROP_DEFENCE_IGNORE_RATIO
DefenseIgnoreDelta = 32, // FIGHT_PROP_DEFENCE_IGNORE_DELTA
// Elemental Damage Bonuses
PyroDamageBonus = 40, // FIGHT_PROP_FIRE_ADD_HURT
ElectroDamageBonus = 41, // FIGHT_PROP_ELEC_ADD_HURT
HydroDamageBonus = 42, // FIGHT_PROP_WATER_ADD_HURT
DendroDamageBonus = 43, // FIGHT_PROP_GRASS_ADD_HURT
AnemoDamageBonus = 44, // FIGHT_PROP_WIND_ADD_HURT
GeoDamageBonus = 45, // FIGHT_PROP_ROCK_ADD_HURT
CryoDamageBonus = 46, // FIGHT_PROP_ICE_ADD_HURT
HeadAddHurt = 47, // FIGHT_PROP_HIT_HEAD_ADD_HURT
// Elemental Resistances
PyroResistance = 50, // FIGHT_PROP_FIRE_SUB_HURT
ElectroResistance = 51, // FIGHT_PROP_ELEC_SUB_HURT
HydroResistance = 52, // FIGHT_PROP_WATER_SUB_HURT
DendroResistance = 53, // FIGHT_PROP_GRASS_SUB_HURT
AnemoResistance = 54, // FIGHT_PROP_WIND_SUB_HURT
GeoResistance = 55, // FIGHT_PROP_ROCK_SUB_HURT
CryoResistance = 56, // FIGHT_PROP_ICE_SUB_HURT
// Effect Stats
EffectHit = 60, // FIGHT_PROP_EFFECT_HIT
EffectResist = 61, // FIGHT_PROP_EFFECT_RESIST
FreezeResist = 62, // FIGHT_PROP_FREEZE_RESIST
DizzyResist = 64, // FIGHT_PROP_DIZZY_RESIST
FreezeShorten = 65, // FIGHT_PROP_FREEZE_SHORTEN
DizzyShorten = 67, // FIGHT_PROP_DIZZY_SHORTEN
// Max Energy
MaxPyroEnergy = 70, // FIGHT_PROP_MAX_FIRE_ENERGY
MaxElectroEnergy = 71, // FIGHT_PROP_MAX_ELEC_ENERGY
MaxHydroEnergy = 72, // FIGHT_PROP_MAX_WATER_ENERGY
MaxDendroEnergy = 73, // FIGHT_PROP_MAX_GRASS_ENERGY
MaxAnemoEnergy = 74, // FIGHT_PROP_MAX_WIND_ENERGY
MaxCryoEnergy = 75, // FIGHT_PROP_MAX_ICE_ENERGY
MaxGeoEnergy = 76, // FIGHT_PROP_MAX_ROCK_ENERGY
// Utility
CooldownReduction = 80, // FIGHT_PROP_SKILL_CD_MINUS_RATIO
ShieldStrength = 81, // FIGHT_PROP_SHIELD_COST_MINUS_RATIO
// Current Energy
CurrentPyroEnergy = 1000, // FIGHT_PROP_CUR_FIRE_ENERGY
CurrentElectroEnergy = 1001, // FIGHT_PROP_CUR_ELEC_ENERGY
CurrentHydroEnergy = 1002, // FIGHT_PROP_CUR_WATER_ENERGY
CurrentDendroEnergy = 1003, // FIGHT_PROP_CUR_GRASS_ENERGY
CurrentAnemoEnergy = 1004, // FIGHT_PROP_CUR_WIND_ENERGY
CurrentCryoEnergy = 1005, // FIGHT_PROP_CUR_ICE_ENERGY
CurrentGeoEnergy = 1006, // FIGHT_PROP_CUR_ROCK_ENERGY
CurrentHP = 1010, // FIGHT_PROP_CUR_HP
// Final Stats
HP = 2000, // FIGHT_PROP_MAX_HP
Attack = 2001, // FIGHT_PROP_CUR_ATTACK
Defense = 2002, // FIGHT_PROP_CUR_DEFENSE
Speed = 2003, // FIGHT_PROP_CUR_SPEED
CurrentHPDebts = 2004, // FIGHT_PROP_CUR_HP_DEBTS
CurrentHPPaidDebts = 2005, // FIGHT_PROP_CUR_HP_PAID_DEBTS
// Non-Extra Stats (base stats without bonuses)
NonExtraAttack = 3000, // FIGHT_PROP_NONEXTRA_ATTACK
NonExtraDefense = 3001, // FIGHT_PROP_NONEXTRA_DEFENSE
NonExtraCritical = 3002, // FIGHT_PROP_NONEXTRA_CRITICAL
NonExtraAntiCritical = 3003, // FIGHT_PROP_NONEXTRA_ANTI_CRITICAL
NonExtraCriticalHurt = 3004, // FIGHT_PROP_NONEXTRA_CRITICAL_HURT
NonExtraChargeEfficiency = 3005, // FIGHT_PROP_NONEXTRA_CHARGE_EFFICIENCY
NonExtraElementMastery = 3006, // FIGHT_PROP_NONEXTRA_ELEMENT_MASTERY
NonExtraPhysicalSubHurt = 3007, // FIGHT_PROP_NONEXTRA_PHYSICAL_SUB_HURT
NonExtraPyroDamageBonus = 3008, // FIGHT_PROP_NONEXTRA_FIRE_ADD_HURT
NonExtraElectroDamageBonus = 3009, // FIGHT_PROP_NONEXTRA_ELEC_ADD_HURT
NonExtraHydroDamageBonus = 3010, // FIGHT_PROP_NONEXTRA_WATER_ADD_HURT
NonExtraDendroDamageBonus = 3011, // FIGHT_PROP_NONEXTRA_GRASS_ADD_HURT
NonExtraAnemoDamageBonus = 3012, // FIGHT_PROP_NONEXTRA_WIND_ADD_HURT
NonExtraGeoDamageBonus = 3013, // FIGHT_PROP_NONEXTRA_ROCK_ADD_HURT
NonExtraCryoDamageBonus = 3014, // FIGHT_PROP_NONEXTRA_ICE_ADD_HURT
NonExtraPyroResistance = 3015, // FIGHT_PROP_NONEXTRA_FIRE_SUB_HURT
NonExtraElectroResistance = 3016, // FIGHT_PROP_NONEXTRA_ELEC_SUB_HURT
NonExtraHydroResistance = 3017, // FIGHT_PROP_NONEXTRA_WATER_SUB_HURT
NonExtraDendroResistance = 3018, // FIGHT_PROP_NONEXTRA_GRASS_SUB_HURT
NonExtraAnemoResistance = 3019, // FIGHT_PROP_NONEXTRA_WIND_SUB_HURT
NonExtraGeoResistance = 3020, // FIGHT_PROP_NONEXTRA_ROCK_SUB_HURT
NonExtraCryoResistance = 3021, // FIGHT_PROP_NONEXTRA_ICE_SUB_HURT
NonExtraCooldownReduction = 3022, // FIGHT_PROP_NONEXTRA_SKILL_CD_MINUS_RATIO
NonExtraShieldStrength = 3023, // FIGHT_PROP_NONEXTRA_SHIELD_COST_MINUS_RATIO
NonExtraPhysicalDamageBonus = 3024, // FIGHT_PROP_NONEXTRA_PHYSICAL_ADD_HURT
// Elemental Reaction Stats
ElementalReactionCritRate = 3025, // FIGHT_PROP_ELEM_REACT_CRITICAL
ElementalReactionCritDamage = 3026, // FIGHT_PROP_ELEM_REACT_CRITICAL_HURT
OverloadedCritRate = 3027, // FIGHT_PROP_ELEM_REACT_EXPLODE_CRITICAL
OverloadedCritDamage = 3028, // FIGHT_PROP_ELEM_REACT_EXPLODE_CRITICAL_HURT
SwirlCritRate = 3029, // FIGHT_PROP_ELEM_REACT_SWIRL_CRITICAL
SwirlCritDamage = 3030, // FIGHT_PROP_ELEM_REACT_SWIRL_CRITICAL_HURT
ElectroChargedCritRate = 3031, // FIGHT_PROP_ELEM_REACT_ELECTRIC_CRITICAL
ElectroChargedCritDamage = 3032, // FIGHT_PROP_ELEM_REACT_ELECTRIC_CRITICAL_HURT
SuperconductCritRate = 3033, // FIGHT_PROP_ELEM_REACT_SCONDUCT_CRITICAL
SuperconductCritDamage = 3034, // FIGHT_PROP_ELEM_REACT_SCONDUCT_CRITICAL_HURT
BurnCritRate = 3035, // FIGHT_PROP_ELEM_REACT_BURN_CRITICAL
BurnCritDamage = 3036, // FIGHT_PROP_ELEM_REACT_BURN_CRITICAL_HURT
ShatteredCritRate = 3037, // FIGHT_PROP_ELEM_REACT_FROZENBROKEN_CRITICAL
ShatteredCritDamage = 3038, // FIGHT_PROP_ELEM_REACT_FROZENBROKEN_CRITICAL_HURT
BloomCritRate = 3039, // FIGHT_PROP_ELEM_REACT_OVERGROW_CRITICAL
BloomCritDamage = 3040, // FIGHT_PROP_ELEM_REACT_OVERGROW_CRITICAL_HURT
BurgeonCritRate = 3041, // FIGHT_PROP_ELEM_REACT_OVERGROW_FIRE_CRITICAL
BurgeonCritDamage = 3042, // FIGHT_PROP_ELEM_REACT_OVERGROW_FIRE_CRITICAL_HURT
HyperbloomCritRate = 3043, // FIGHT_PROP_ELEM_REACT_OVERGROW_ELECTRIC_CRITICAL
HyperbloomCritDamage = 3044, // FIGHT_PROP_ELEM_REACT_OVERGROW_ELECTRIC_CRITICAL_HURT
BaseElementalReactionCritRate = 3045, // FIGHT_PROP_BASE_ELEM_REACT_CRITICAL
BaseElementalReactionCritDamage = 3046, // FIGHT_PROP_BASE_ELEM_REACT_CRITICAL_HURT
}
+103 -14
View File
@@ -1,3 +1,4 @@
import { feature } from "bun:bundle";
import { ImageResponse } from "@takumi-rs/image-response"; import { ImageResponse } from "@takumi-rs/image-response";
import { file, hash } from "bun"; import { file, hash } from "bun";
import { NotFoundError, status, t } from "elysia"; import { NotFoundError, status, t } from "elysia";
@@ -16,8 +17,11 @@ import { ConstsSvg } from "@/assets/consts";
import { featherImage } from "@/assets/feather"; import { featherImage } from "@/assets/feather";
import shadeSvg from "@/assets/shade.svg" with { type: "file" }; import shadeSvg from "@/assets/shade.svg" with { type: "file" };
import fontFile from "@/assets/shinshin.woff2" with { type: "file" }; import fontFile from "@/assets/shinshin.woff2" with { type: "file" };
import { star } from "@/assets/stars";
import { TalentSvg } from "@/assets/talent"; import { TalentSvg } from "@/assets/talent";
import { AssetManager, getEnkaAvatarStore } from "@/lib/assets"; import { WeaponGlazeSvg } from "@/assets/weapon-glaze";
import { AssetManager, getEnkaStores } from "@/lib/assets";
import { mockData } from "@/lib/mock";
const assets = new AssetManager(); const assets = new AssetManager();
const cache = new TTLCache(); const cache = new TTLCache();
@@ -38,16 +42,19 @@ export function card(app: App) {
async ({ params: { uid, char: charId }, store: { start } }) => { async ({ params: { uid, char: charId }, store: { start } }) => {
try { try {
// const data = mockData; // const data = mockData;
const [avatarStore, data] = await Promise.all([ const [[avatarStore, weaponStore, relicStore, locs], data] =
getEnkaAvatarStore().finally(() => await Promise.all([
console.log( getEnkaStores().finally(() =>
`\n${Date.now() - start}ms - Avatar store loaded`, console.log(`\n${Date.now() - start}ms - Stores loaded`),
), ),
), feature("API")
fetchEnka(uid, false).finally(() => ? fetchEnka(uid, false).finally(() =>
console.log(`${Date.now() - start}ms - Enka data loaded`), console.log(
), `${Date.now() - start}ms - Enka data loaded`,
]); ),
)
: mockData,
]);
if (typeof data.avatarInfoList === "undefined") if (typeof data.avatarInfoList === "undefined")
return status(400, "The showcase for this UID is private"); return status(400, "The showcase for this UID is private");
const char = data.avatarInfoList.find( const char = data.avatarInfoList.find(
@@ -62,11 +69,33 @@ export function card(app: App) {
"Unsupported character, avatar store doesn't have this character", "Unsupported character, avatar store doesn't have this character",
); );
if (!char.equipList)
return status(501, "INTERNAL: No equipList provided");
const weapon = {
data: char.equipList.find((e) => e.weapon)!.weapon!,
store:
weaponStore[
char.equipList.find((e) => e.weapon)!.itemId ?? "Unknown"
],
};
const cacheKey = hash(JSON.stringify(data)).toString(36); const cacheKey = hash(JSON.stringify(data)).toString(36);
const cached = cache.get<ImageResponse>(cacheKey); if (feature("API")) {
if (cached) return cached.clone(); const cached = cache.get<ImageResponse>(cacheKey);
if (cached) return cached.clone();
}
const resources = await Promise.all([ const resources = await Promise.all([
assets
.load(
weapon.data.promoteLevel > 2
? weapon.store.AwakenIcon
: weapon.store.Icon,
)
.then((data) => ({
src: "/weapon",
data,
})),
...[...avatar.Consts, ...Object.values(avatar.Skills)].map( ...[...avatar.Consts, ...Object.values(avatar.Skills)].map(
async (path) => ({ async (path) => ({
src: path, src: path,
@@ -97,7 +126,10 @@ export function card(app: App) {
src: "/bg", src: "/bg",
data, data,
})), })),
]); ] satisfies Promise<{
src: string;
data: ArrayBuffer | Buffer;
}>[]);
console.log( console.log(
`${Date.now() - start}ms - Image resources loaded\n${resources.map((r) => `- ${r.src} ${(r.data.byteLength / 1000).toFixed(2)}Kb`).join("\n")}`, `${Date.now() - start}ms - Image resources loaded\n${resources.map((r) => `- ${r.src} ${(r.data.byteLength / 1000).toFixed(2)}Kb`).join("\n")}`,
); );
@@ -232,7 +264,46 @@ export function card(app: App) {
</div> </div>
</div> </div>
{/* middle section */} {/* middle section */}
<div tw="absolute top-0 bottom-0 right-[23.1em] w-[21.5em] p-[1.4em_.7em_.7em]"></div> <div tw="absolute top-0 bottom-0 right-[23.1em] w-[21.5em] p-[1.4em_.7em_.7em] flex flex-col">
{/* weapon */}
<div tw="flex flex-start" className="Weapon">
{/* display */}
<div tw="w-[27%] leading-none shrink-0 min-h-[5.5em] relative">
<img src="/weapon" alt="Weapon" tw="w-full" />
<WeaponGlazeSvg
tw="absolute top-[90%] -left-[15%] w-[130%]"
style={{
color: {
1: "#868a82",
2: "#70ca7a",
3: "#5cabff",
4: "#cc7bff",
5: "#dd8637",
}[weapon.store.Rarity],
}}
/>
<div tw="absolute w-full top-[93%] flex justify-center">
{Array.from({ length: weapon.store.Rarity }, (_, i) =>
star(i),
)}
</div>
</div>
{/* caption */}
<div tw="px-[5%] grow text-white self-center">
<div
tw="text-[108%] leading-[1.3]"
style={{
textShadow:
"0 .08em .1em rgba(0,0,0,.4),0 .1em .3em rgba(0,0,0,.4)",
}}
>
<span>{locs.en[weapon.store.NameTextMapHash]}</span>
</div>
{/* stats */}
<div tw="pt-[3%] pb-[2%]"></div>
</div>
</div>
</div>
{/* right section */} {/* 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]"></div>
</div>, </div>,
@@ -261,6 +332,24 @@ export function card(app: App) {
.Talents.Fire { color: #541900!important } .Talents.Fire { color: #541900!important }
.Grass { color: #a5ffab; } .Grass { color: #a5ffab; }
.Talents.Grass { color: #134517!important } .Talents.Grass { color: #134517!important }
.Substat {
display: inline-block;
background: rgba(255,255,255,.15);
border-radius: .2em;
padding: 2% 3%;
box-sizing: content-box;
margin-right: .5em;
line-height: 1;
}
.Weapon .Substat:before {
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: .2em
}
`, `,
], ],
}, },