feat: left section complete
This commit is contained in:
+1
-1
@@ -2,7 +2,7 @@ FROM oven/bun:alpine AS builder
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package.json bun.lock ./
|
COPY package.json bun.lock ./
|
||||||
# COPY patches/ ./patches
|
COPY patches/ ./patches
|
||||||
RUN --mount=type=cache,target=/root/.bun/install/cache bun i --frozen-lockfile
|
RUN --mount=type=cache,target=/root/.bun/install/cache bun i --frozen-lockfile
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|||||||
@@ -20,6 +20,9 @@
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"patchedDependencies": {
|
||||||
|
"@takumi-rs/[email protected]": "patches/@takumi-rs%[email protected]",
|
||||||
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
"@alloc/quick-lru": ["@alloc/[email protected]", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
"@alloc/quick-lru": ["@alloc/[email protected]", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -20,5 +20,8 @@
|
|||||||
"@biomejs/biome": "^2.4.5",
|
"@biomejs/biome": "^2.4.5",
|
||||||
"bun-types": "latest"
|
"bun-types": "latest"
|
||||||
},
|
},
|
||||||
"module": "src/index.js"
|
"module": "src/index.js",
|
||||||
|
"patchedDependencies": {
|
||||||
|
"@takumi-rs/[email protected]": "patches/@takumi-rs%[email protected]"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
diff --git a/index.js b/index.js
|
||||||
|
index 053400fcfd69663e1aa12dd1740a24f806747278..d0326bec5feaed5fa98c558fac44b5ee70581741 100644
|
||||||
|
--- a/index.js
|
||||||
|
+++ b/index.js
|
||||||
|
@@ -3,11 +3,32 @@
|
||||||
|
// @ts-nocheck
|
||||||
|
/* auto-generated by NAPI-RS */
|
||||||
|
|
||||||
|
-import { createRequire } from 'node:module'
|
||||||
|
-const require = createRequire(import.meta.url)
|
||||||
|
+import { createRequire } from 'node:module';
|
||||||
|
+const realRequire = createRequire(import.meta.url)
|
||||||
|
+
|
||||||
|
+import * as path from "node:path";
|
||||||
|
+import * as fs from "node:fs";
|
||||||
|
+import { feature } from 'bun:bundle';
|
||||||
|
+const { readFileSync } = require('node:fs');
|
||||||
|
+
|
||||||
|
+function require(name) {
|
||||||
|
+ if(!feature("FAKE_REQUIRE")) return realRequire(name);
|
||||||
|
+ const exeDir = path.dirname(process.execPath);
|
||||||
|
+ const candidate = path.join(exeDir, name);
|
||||||
|
+
|
||||||
|
+ if (fs.existsSync(candidate)) {
|
||||||
|
+ return realRequire(candidate);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ try {
|
||||||
|
+ return realRequire(name);
|
||||||
|
+ } catch {
|
||||||
|
+ return realRequire(`./${name}`);
|
||||||
|
+ }
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
const __dirname = new URL('.', import.meta.url).pathname
|
||||||
|
|
||||||
|
-const { readFileSync } = require('node:fs')
|
||||||
|
let nativeBinding = null
|
||||||
|
const loadErrors = []
|
||||||
|
|
||||||
@@ -3,7 +3,7 @@ export function ConstsSvg({ locked = false }: { locked?: boolean }) {
|
|||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 14 14"
|
viewBox="0 0 14 14"
|
||||||
style={{ width: "2.5em" }}
|
style={{ width: "3em" }}
|
||||||
className={locked ? "locked" : ""}
|
className={locked ? "locked" : ""}
|
||||||
>
|
>
|
||||||
<title>Consts</title>
|
<title>Consts</title>
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
export function TalentSvg({tw}: {tw?: string}) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 14 14"
|
||||||
|
tw={tw || "w-[3.6em] opacity-80 text-black"}
|
||||||
|
>
|
||||||
|
<title>Talent</title>
|
||||||
|
<defs>
|
||||||
|
<linearGradient
|
||||||
|
xlinkHref="#svg-Circl__linearGradient826"
|
||||||
|
id="svg-Circl__a"
|
||||||
|
x1="7.03"
|
||||||
|
x2="7.03"
|
||||||
|
y1="1.455"
|
||||||
|
y2="12.322"
|
||||||
|
gradientTransform="matrix(.80836 0 0 .82012 1.247 1.345)"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
style={{ stopColor: "currentcolor", stopOpacity: 0 }}
|
||||||
|
></stop>
|
||||||
|
<stop
|
||||||
|
offset=".446"
|
||||||
|
style={{ stopColor: "currentcolor", stopOpacity: 0.8 }}
|
||||||
|
></stop>
|
||||||
|
<stop
|
||||||
|
offset=".548"
|
||||||
|
style={{ stopColor: "currentcolor", stopOpacity: 0.8 }}
|
||||||
|
></stop>
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
style={{ stopColor: "currentcolor", stopOpacity: 0 }}
|
||||||
|
></stop>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="svg-Circl__b"
|
||||||
|
x1="7.03"
|
||||||
|
x2="7.03"
|
||||||
|
y1="1.794"
|
||||||
|
y2="12.257"
|
||||||
|
gradientTransform="translate(1.239 1.243)scale(.82247)"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
>
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
style={{ stopColor: "rgb(255, 255, 255)", stopOpacity: 0 }}
|
||||||
|
></stop>
|
||||||
|
<stop
|
||||||
|
offset=".446"
|
||||||
|
style={{ stopColor: "rgb(255, 255, 255)", stopOpacity: 0.7 }}
|
||||||
|
></stop>
|
||||||
|
<stop
|
||||||
|
offset=".548"
|
||||||
|
style={{ stopColor: "rgb(255, 255, 255)", stopOpacity: 0.7 }}
|
||||||
|
></stop>
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
style={{ stopColor: "rgb(255, 255, 255)", stopOpacity: 0 }}
|
||||||
|
></stop>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<g style={{ display: "inline" }}>
|
||||||
|
<path
|
||||||
|
d="M6.998 1.638c2.778 0 5.056 2.197 5.266 4.963.01.137-.447.433-.447.433s.457.287.445.435c-.226 2.748-2.497 4.893-5.264 4.893-2.779 0-5.077-2.162-5.289-4.925-.01-.138.444-.418.444-.418s-.455-.275-.444-.41c.209-2.77 2.508-4.971 5.289-4.971"
|
||||||
|
style={{
|
||||||
|
opacity: 1,
|
||||||
|
fill: "url(#svg-Circl__a)",
|
||||||
|
fillOpacity: 1,
|
||||||
|
strokeWidth: 20.1392,
|
||||||
|
paintOrder: "stroke markers fill",
|
||||||
|
}}
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M6.713 1.638C3.972 1.766 1.82 3.695 1.483 6.3l-.075-.11s-.786.828-1.451.828c.67 0 1.45.83 1.45.83s.03-.047.076-.112c.338 2.607 2.5 4.507 5.25 4.625-2.657-.138-4.816-2.284-5.023-4.925.17-.208.38-.418.526-.418-.147 0-.355-.209-.526-.417.207-2.645 2.366-4.824 5.023-4.964zm.56 0c2.655.138 4.78 2.317 4.986 4.962-.174.213-.388.434-.538.434.15 0 .363.218.536.43-.217 2.63-2.336 4.762-4.983 4.898 2.739-.117 4.866-2.002 5.211-4.594l.064.095s.786-.83 1.451-.83c-.206 0-.423-.078-.624-.186a3.8 3.8 0 0 1-.827-.642s-.024.039-.06.091c-.335-2.61-2.467-4.54-5.217-4.658Z"
|
||||||
|
style={{
|
||||||
|
fill: "url(#svg-Circl__b)",
|
||||||
|
strokeWidth: 19.7069,
|
||||||
|
paintOrder: "stroke markers fill",
|
||||||
|
}}
|
||||||
|
></path>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
export type AvatarStore = Record<number, Avatar>;
|
||||||
|
|
||||||
|
export interface Avatar {
|
||||||
|
Element: ElementType;
|
||||||
|
|
||||||
|
/** Constellation icons */
|
||||||
|
Consts: string[];
|
||||||
|
|
||||||
|
/** Talent order (normal, skill, burst) */
|
||||||
|
SkillOrder: number[];
|
||||||
|
|
||||||
|
/** Skill ID -> icon path */
|
||||||
|
Skills: Record<number, string>;
|
||||||
|
|
||||||
|
/** Skill ID -> proud skill group */
|
||||||
|
ProudMap: Record<number, number>;
|
||||||
|
|
||||||
|
NameTextMapHash: number;
|
||||||
|
|
||||||
|
SideIconName: string;
|
||||||
|
|
||||||
|
QualityType: QualityType;
|
||||||
|
|
||||||
|
WeaponType: WeaponType;
|
||||||
|
|
||||||
|
/** Optional costumes */
|
||||||
|
Costumes?: Record<number, AvatarCostume>;
|
||||||
|
|
||||||
|
/** Base stat values keyed by prop ID */
|
||||||
|
BaseProps: Record<number, number>;
|
||||||
|
|
||||||
|
/** Growth curve IDs keyed by prop ID */
|
||||||
|
PropGrowCurves: Record<number, number>;
|
||||||
|
|
||||||
|
/** Ascension stat bonuses */
|
||||||
|
PromoteProps: PromoteProps[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AvatarCostume {
|
||||||
|
SideIcon: string;
|
||||||
|
Icon: string;
|
||||||
|
Art: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PromoteProps entries are stat maps keyed by property ID
|
||||||
|
* Example: HP(1), ATK(4), DEF(7), CritRate(20), etc.
|
||||||
|
*/
|
||||||
|
export type PromoteProps = Record<number, number>;
|
||||||
|
|
||||||
|
export type ElementType =
|
||||||
|
| "Fire"
|
||||||
|
| "Water"
|
||||||
|
| "Wind"
|
||||||
|
| "Electric"
|
||||||
|
| "Rock"
|
||||||
|
| "Ice"
|
||||||
|
| "Grass";
|
||||||
|
|
||||||
|
export type WeaponType =
|
||||||
|
| "WEAPON_SWORD_ONE_HAND"
|
||||||
|
| "WEAPON_CLAYMORE"
|
||||||
|
| "WEAPON_POLE"
|
||||||
|
| "WEAPON_CATALYST"
|
||||||
|
| "WEAPON_BOW";
|
||||||
|
|
||||||
|
export type QualityType = "QUALITY_ORANGE" | "QUALITY_PURPLE";
|
||||||
+157
-48
@@ -7,12 +7,18 @@ import {
|
|||||||
InvalidFormatError,
|
InvalidFormatError,
|
||||||
MaintenanceError,
|
MaintenanceError,
|
||||||
RatelimitError,
|
RatelimitError,
|
||||||
|
TTLCache,
|
||||||
} from "enka.ts";
|
} from "enka.ts";
|
||||||
import { twj } from "tw-to-css";
|
import { twj } from "tw-to-css";
|
||||||
import type { App } from "@";
|
import type { App } from "@";
|
||||||
import { ConstsSvg } from "@/assets/consts";
|
import { ConstsSvg } from "@/assets/consts";
|
||||||
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 { TalentSvg } from "@/assets/talent";
|
||||||
|
import { mockData } from "@/lib/mock";
|
||||||
|
import { AvatarStore } from "@/type/enka";
|
||||||
|
|
||||||
|
const cache = new TTLCache();
|
||||||
|
|
||||||
const font = file(fontFile);
|
const font = file(fontFile);
|
||||||
const shade = file(shadeSvg);
|
const shade = file(shadeSvg);
|
||||||
@@ -21,16 +27,22 @@ export function card(app: App) {
|
|||||||
return app
|
return app
|
||||||
.group("/card", (app) =>
|
.group("/card", (app) =>
|
||||||
app
|
app
|
||||||
// .onBeforeHandle(({ key }) => {
|
.state("start", Date.now())
|
||||||
// if (!key.valid) throw status(401);
|
.derive(({ store }) => {
|
||||||
// if (!key.has("card")) throw status(403);
|
store.start = Date.now();
|
||||||
// })
|
})
|
||||||
|
.onAfterResponse(({ store: { start } }) =>
|
||||||
|
console.log(`${Date.now() - start}ms - Rendering completed`),
|
||||||
|
)
|
||||||
.get(
|
.get(
|
||||||
"/genshin/:uid/:char",
|
"/genshin/:uid/:char",
|
||||||
async ({ params: { uid, char: charId } }) => {
|
async ({ params: { uid, char: charId }, store: { start } }) => {
|
||||||
try {
|
try {
|
||||||
// const data = mockData;
|
const avatarStore = await getEnkaAvatarStore();
|
||||||
const data = await fetchEnka(uid, false);
|
console.log(`${Date.now() - start}ms - Avatar store loaded`);
|
||||||
|
// const data = await fetchEnka(uid, false);
|
||||||
|
const data = mockData;
|
||||||
|
console.log(`${Date.now() - start}ms - Enka data loaded`);
|
||||||
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(
|
||||||
@@ -38,39 +50,76 @@ export function card(app: App) {
|
|||||||
);
|
);
|
||||||
if (!char) return status(400, "Character not found in showcase");
|
if (!char) return status(400, "Character not found in showcase");
|
||||||
|
|
||||||
|
const avatar = avatarStore[char.avatar.avatarId];
|
||||||
|
if (!avatar)
|
||||||
|
return status(
|
||||||
|
500,
|
||||||
|
"Unsupported character, avatar store doesn't have this character",
|
||||||
|
);
|
||||||
|
const resources = await Promise.all(
|
||||||
|
[
|
||||||
|
...avatar.Consts,
|
||||||
|
...Object.values(avatar.Skills),
|
||||||
|
...Object.values(avatar.Costumes ?? {})
|
||||||
|
.map((c) => c.Art)
|
||||||
|
.slice(0, 1),
|
||||||
|
avatar.SideIconName.replace(
|
||||||
|
"AvatarIcon_Side",
|
||||||
|
"Gacha_AvatarImg",
|
||||||
|
),
|
||||||
|
].map(async (path) => ({
|
||||||
|
src: path,
|
||||||
|
data: await fetch(new URL(path, "https://enka.network")).then(
|
||||||
|
(res) => res.arrayBuffer(),
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
console.log(`${Date.now() - start}ms - Image resources loaded`);
|
||||||
|
|
||||||
return new ImageResponse(
|
return new ImageResponse(
|
||||||
<div style={twj("relative text-[14.8667px] text-white")}>
|
<div tw="relative text-[14.8667px] text-white">
|
||||||
<div style={twj("aspect-[1218/508] h-full")}></div>
|
<div tw="aspect-[1218/508] h-full"></div>
|
||||||
|
{/* avatar */}
|
||||||
|
<img
|
||||||
|
src={
|
||||||
|
avatar.Costumes?.[0]?.Art ||
|
||||||
|
avatar.SideIconName.replace(
|
||||||
|
"AvatarIcon_Side",
|
||||||
|
"Gacha_AvatarImg",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
alt="char"
|
||||||
|
tw="object-cover h-full aspect-[1024/1124] absolute top-0 -left-[2.5em] scale-x-[1.1] scale-y-[1.15]"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* left section */}
|
||||||
<div
|
<div
|
||||||
|
tw="absolute top-0 bottom-0 left-0 w-[27.4em] p-[1.4em] flex flex-col"
|
||||||
style={{
|
style={{
|
||||||
...twj(
|
|
||||||
"absolute top-0 bottom-0 left-0 w-[27.4em] p-[1.4em] bg-red-500/50",
|
|
||||||
"flex flex-col",
|
|
||||||
),
|
|
||||||
textShadow:
|
textShadow:
|
||||||
"0 .08em .1em black,0 .1em .3em rgba(0,0,0,.4)",
|
"0 .08em .1em black,0 .1em .3em rgba(0,0,0,.4)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={twj("absolute top-0 left-0 bottom-0 right-0")}>
|
{/* left side vignette */}
|
||||||
<img src="shade" alt="shade" style={twj("opacity-40")} />
|
<div tw="absolute top-0 left-0 bottom-0 right-0">
|
||||||
|
<img src="shade" alt="shade" tw="opacity-40" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
{/* char and username */}
|
||||||
style={twj(
|
<div tw="text-[130%] pb-[.5%] flex items-center gap-[.5em]">
|
||||||
"text-[130%] pb-[.5%] flex items-center gap-[.5em]",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{char.avatar.name}
|
{char.avatar.name}
|
||||||
<div style={twj("text-[11.596px] opacity-80")}>
|
<div tw="text-[11.596px] opacity-80">
|
||||||
▴ {data.playerInfo.nickname}
|
▴ {data.playerInfo.nickname}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* level */}
|
||||||
<div>
|
<div>
|
||||||
Lv. 80 /<span style={twj("opacity-60 pl-1")}>80</span>
|
Lv. 80 /<span tw="opacity-60 pl-1">80</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={twj("flex gap-1 items-end")}>
|
{/* fren */}
|
||||||
|
<div tw="flex gap-1 items-end">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
style={twj("h-[1em] mb-1")}
|
tw="h-[1em] mb-1"
|
||||||
viewBox="0 0 14 14"
|
viewBox="0 0 14 14"
|
||||||
>
|
>
|
||||||
<title>fren</title>
|
<title>fren</title>
|
||||||
@@ -82,39 +131,78 @@ export function card(app: App) {
|
|||||||
8
|
8
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={twj("grow")} />
|
{/* sep */}
|
||||||
|
<div tw="grow" />
|
||||||
|
{/* constellation */}
|
||||||
<div
|
<div
|
||||||
style={twj("flex align-start flex-col text-[1.12em]")}
|
tw="flex align-start flex-col text-[1.12em]"
|
||||||
className={`${char.avatar.element} locked`}
|
className={`${char.avatar.element} locked`}
|
||||||
>
|
>
|
||||||
{Array(6)
|
{avatar.Consts.map((c, i) => (
|
||||||
.fill(0)
|
<div
|
||||||
.map((_, i) => (
|
style={{
|
||||||
<div>
|
position: "relative",
|
||||||
<ConstsSvg
|
margin: "-.5em 0 0em -.6em",
|
||||||
locked={i >= (char.talentIdList?.length || 0)}
|
}}
|
||||||
/>
|
>
|
||||||
</div>
|
<ConstsSvg
|
||||||
))}
|
locked={i >= (char.talentIdList?.length || 0)}
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
src={c}
|
||||||
|
alt={`Constellation ${i + 1}`}
|
||||||
|
style={twj(
|
||||||
|
"absolute top-0 left-0 w-[1.6em] mt-[.72em] ml-[.7em] pointer-events-auto",
|
||||||
|
i >= (char.talentIdList?.length || 0)
|
||||||
|
? "opacity-40"
|
||||||
|
: "",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
{/* talent-wrap */}
|
||||||
<div
|
<div
|
||||||
style={twj(
|
style={{
|
||||||
"mb-[-1em] ml-[-.3em] mt-[.3em] text-[.9em] opacity-80",
|
display: "flex",
|
||||||
)}
|
position: "absolute",
|
||||||
|
right: "5%",
|
||||||
|
bottom: "6%",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "flex-end",
|
||||||
|
width: "60%",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
|
{Object.entries(avatar.Skills).map(([key, src]) => {
|
||||||
|
const talentLevel = char.skillLevelMap?.[key] ?? 0;
|
||||||
|
return (
|
||||||
|
<div tw="mt-[.3em] relative">
|
||||||
|
<TalentSvg
|
||||||
|
tw={`Talents ${char.avatar.element} w-[3.6em] opacity-80 text-black`}
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
key={key}
|
||||||
|
src={src}
|
||||||
|
alt={`Talent ${key}`}
|
||||||
|
tw="w-[2.5em] absolute left-[.15em] top-[.15em] m-[.4em_0_0_.4em]"
|
||||||
|
/>
|
||||||
|
<div tw="absolute -bottom-[.1em] w-full flex justify-center">
|
||||||
|
<div tw="bg-[#323232b3] py-[5%] pl-[7%] pr-[15%] text-[.8em] rounded-[1.4em] flex items-center justify-center">
|
||||||
|
{talentLevel}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div tw="mb-[-1em] ml-[-.3em] mt-[.3em] text-[.9em] opacity-80">
|
||||||
{data.uid}
|
{data.uid}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
{/* middle section */}
|
||||||
style={twj(
|
<div tw="absolute top-0 bottom-0 right-[23.1em] w-[21.5em] p-[1.4em_.7em_.7em] bg-green-500/50"></div>
|
||||||
"absolute top-0 bottom-0 right-[23.1em] w-[21.5em] p-[1.4em_.7em_.7em] bg-green-500/50",
|
{/* right section */}
|
||||||
)}
|
<div tw="absolute top-0 bottom-0 right-0 w-[23.1em] p-[.7em] bg-blue-500/50"></div>
|
||||||
></div>
|
|
||||||
<div
|
|
||||||
style={twj(
|
|
||||||
"absolute top-0 bottom-0 right-0 w-[23.1em] p-[.7em] bg-blue-500/50",
|
|
||||||
)}
|
|
||||||
></div>
|
|
||||||
</div>,
|
</div>,
|
||||||
{
|
{
|
||||||
fonts: [await font.arrayBuffer()],
|
fonts: [await font.arrayBuffer()],
|
||||||
@@ -122,16 +210,25 @@ export function card(app: App) {
|
|||||||
persistentImages: [
|
persistentImages: [
|
||||||
{ data: await shade.arrayBuffer(), src: "shade" },
|
{ data: await shade.arrayBuffer(), src: "shade" },
|
||||||
],
|
],
|
||||||
|
fetchedResources: resources,
|
||||||
|
format: "webp",
|
||||||
// drawDebugBorder: true,
|
// drawDebugBorder: true,
|
||||||
stylesheets: [
|
stylesheets: [
|
||||||
`
|
`
|
||||||
.Rock { color: #ffc64a; }
|
.Rock { color: #ffc64a; }
|
||||||
|
.Talents.Rock { color: #4a3a18!important }
|
||||||
.Wind { color: #71fdec; }
|
.Wind { color: #71fdec; }
|
||||||
|
.Talents.Wind { color: #1d4d47!important }
|
||||||
.Ice { color: #98efff; }
|
.Ice { color: #98efff; }
|
||||||
|
.Talents.Ice { color: #193d43!important }
|
||||||
.Water { color: #56a2ff; }
|
.Water { color: #56a2ff; }
|
||||||
|
.Talents.Water { color: #18293e!important }
|
||||||
.Electric { color: #ffa1ff; }
|
.Electric { color: #ffa1ff; }
|
||||||
|
.Talents.Electric { color: #34123b!important }
|
||||||
.Fire { color: #f58453; }
|
.Fire { color: #f58453; }
|
||||||
|
.Talents.Fire { color: #541900!important }
|
||||||
.Grass { color: #a5ffab; }
|
.Grass { color: #a5ffab; }
|
||||||
|
.Talents.Grass { color: #134517!important }
|
||||||
`,
|
`,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -196,3 +293,15 @@ export function card(app: App) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getEnkaAvatarStore(): Promise<AvatarStore> {
|
||||||
|
const cached = cache.get("enkaAvatarStore");
|
||||||
|
if (cached) return cached as AvatarStore;
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
"https://github.com/EnkaNetwork/API-docs/raw/refs/heads/master/store/gi/avatars.json",
|
||||||
|
);
|
||||||
|
const data = await response.json();
|
||||||
|
cache.set("enkaAvatarStore", data, 86400); // 24 hours
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user