feat : add metrial fetcher
This commit is contained in:
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
avatarIconName,
|
||||
ascensionSnapshot,
|
||||
characterDetailSchema,
|
||||
cleanLunarisText,
|
||||
constellationSnapshot,
|
||||
@@ -58,6 +59,26 @@ describe("Lunaris character data", () => {
|
||||
expect(constellationSnapshot(detail, (icon) => icon)).toEqual([]);
|
||||
});
|
||||
|
||||
it("normalizes scalar and grouped ascension materials", () => {
|
||||
const detail = characterDetailSchema.parse({
|
||||
ascension: {
|
||||
"202": 7049900,
|
||||
exp: { "104001": 2, "104003": 418 },
|
||||
},
|
||||
});
|
||||
|
||||
expect(ascensionSnapshot(detail)).toEqual([
|
||||
{ type: "202", items: [{ key: "202", amount: 7049900 }] },
|
||||
{
|
||||
type: "exp",
|
||||
items: [
|
||||
{ key: "104001", amount: 2 },
|
||||
{ key: "104003", amount: 418 },
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("reads the supported weapon type from character details", () => {
|
||||
const detail = characterDetailSchema.parse({
|
||||
info: { element: "Cryo", weapon: "WEAPON_SWORD_ONE_HAND" },
|
||||
|
||||
@@ -14,6 +14,25 @@ export interface CatalogTalent {
|
||||
iconKey: string | null;
|
||||
}
|
||||
|
||||
export interface CatalogAscensionMaterial {
|
||||
key: string;
|
||||
amount: number;
|
||||
}
|
||||
|
||||
export interface CatalogAscensionGroup {
|
||||
type: string;
|
||||
items: CatalogAscensionMaterial[];
|
||||
}
|
||||
|
||||
export interface CatalogMaterial {
|
||||
key: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
rarity: number | null;
|
||||
type: string | null;
|
||||
imageKey: string;
|
||||
}
|
||||
|
||||
const constellationSchema = z.object({
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
@@ -37,6 +56,10 @@ export const characterDetailSchema = z.object({
|
||||
elementalskill: skillSchema.optional(),
|
||||
elementalburst: skillSchema.optional(),
|
||||
}).nullish(),
|
||||
ascension: z.record(
|
||||
z.string(),
|
||||
z.union([z.number().int(), z.record(z.string(), z.number().int())]),
|
||||
).nullish(),
|
||||
});
|
||||
|
||||
export function cleanLunarisText(value: string): string {
|
||||
@@ -106,3 +129,15 @@ export function talentSnapshot(
|
||||
iconKey: iconKey(talent.icon),
|
||||
}] : []);
|
||||
}
|
||||
|
||||
export function ascensionSnapshot(
|
||||
value: z.infer<typeof characterDetailSchema>,
|
||||
): CatalogAscensionGroup[] {
|
||||
return Object.entries(value.ascension ?? {}).map(([type, entries]) => ({
|
||||
type,
|
||||
items:
|
||||
typeof entries === "number"
|
||||
? [{ key: type, amount: entries }]
|
||||
: Object.entries(entries).map(([key, amount]) => ({ key, amount })),
|
||||
}));
|
||||
}
|
||||
|
||||
+53
-2
@@ -6,9 +6,11 @@ import { getDb } from "@/db/client";
|
||||
import {
|
||||
catalogArtifacts,
|
||||
catalogCharacters,
|
||||
catalogMaterials,
|
||||
catalogWeapons,
|
||||
} from "@/db/schema";
|
||||
import {
|
||||
ascensionSnapshot,
|
||||
avatarIconName,
|
||||
characterDetailSchema,
|
||||
constellationSnapshot,
|
||||
@@ -17,7 +19,9 @@ import {
|
||||
talentSnapshot,
|
||||
} from "@/lib/catalog/lunaris";
|
||||
import type {
|
||||
CatalogAscensionGroup,
|
||||
CatalogConstellation,
|
||||
CatalogMaterial,
|
||||
CatalogTalent,
|
||||
} from "@/lib/catalog/lunaris";
|
||||
import { customTravelers, isUpstreamTraveler } from "@/lib/catalog/travelers";
|
||||
@@ -63,6 +67,16 @@ const artifactSchema = z.record(
|
||||
qualityType: z.string().optional(),
|
||||
}),
|
||||
);
|
||||
const materialSchema = z.record(
|
||||
z.string(),
|
||||
z.object({
|
||||
enName: z.string(),
|
||||
enDescription: z.string().optional(),
|
||||
icon: z.string(),
|
||||
qualityType: z.string().optional(),
|
||||
type: z.string().optional(),
|
||||
}),
|
||||
);
|
||||
|
||||
interface SyncedCharacter {
|
||||
key: string;
|
||||
@@ -80,6 +94,7 @@ interface SyncedCharacter {
|
||||
localImage: string | null;
|
||||
constellations: CatalogConstellation[];
|
||||
talents: CatalogTalent[];
|
||||
ascension: CatalogAscensionGroup[];
|
||||
}
|
||||
|
||||
async function getJson(url: string): Promise<unknown> {
|
||||
@@ -107,6 +122,10 @@ function webpName(value: string): string {
|
||||
return value.endsWith(".webp") ? value : `${value}.webp`;
|
||||
}
|
||||
|
||||
function pngName(value: string): string {
|
||||
return value.endsWith(".png") ? value : `${value}.png`;
|
||||
}
|
||||
|
||||
function rarity(value: string | undefined): number | null {
|
||||
if (!value) return null;
|
||||
return (
|
||||
@@ -186,14 +205,26 @@ async function uploadAssets(
|
||||
}
|
||||
|
||||
async function syncCatalogVersion(version: string): Promise<void> {
|
||||
const [charactersRaw, weaponsRaw, artifactsRaw] = await Promise.all([
|
||||
const [charactersRaw, weaponsRaw, artifactsRaw, materialsRaw] = await Promise.all([
|
||||
getJson(`${API_ROOT}/${version}/charlist.json`),
|
||||
getJson(`${API_ROOT}/${version}/weaponlist.json`),
|
||||
getJson(`${API_ROOT}/${version}/artifactlist.json`),
|
||||
getJson(`${API_ROOT}/${version}/materiallist.json`),
|
||||
]);
|
||||
const characterData = characterSchema.parse(charactersRaw);
|
||||
const weaponData = weaponSchema.parse(weaponsRaw);
|
||||
const artifactData = artifactSchema.parse(artifactsRaw);
|
||||
const materialData = materialSchema.parse(materialsRaw);
|
||||
const materials: CatalogMaterial[] = Object.entries(materialData).map(
|
||||
([key, value]) => ({
|
||||
key,
|
||||
name: value.enName,
|
||||
description: value.enDescription ?? null,
|
||||
rarity: rarity(value.qualityType),
|
||||
type: value.type ?? null,
|
||||
imageKey: `catalog/materials/${pngName(value.icon)}`,
|
||||
}),
|
||||
);
|
||||
|
||||
const remoteCharacters: SyncedCharacter[] = await mapConcurrent(
|
||||
Object.entries(characterData).filter(
|
||||
@@ -207,7 +238,7 @@ async function syncCatalogVersion(version: string): Promise<void> {
|
||||
const detailRaw = await getOptionalJson(
|
||||
`${API_ROOT}/${version}/en/char/${encodeURIComponent(key)}.json`,
|
||||
);
|
||||
const detail = detailRaw
|
||||
const detail = detailRaw
|
||||
? characterDetailSchema.parse(detailRaw)
|
||||
: null;
|
||||
const avatarIcon = webpName(avatarIconName(value.CardImg));
|
||||
@@ -242,6 +273,7 @@ async function syncCatalogVersion(version: string): Promise<void> {
|
||||
(icon) => `catalog/characters/talents/${webpName(icon)}`,
|
||||
)
|
||||
: [],
|
||||
ascension: detail ? ascensionSnapshot(detail) : [],
|
||||
};
|
||||
},
|
||||
);
|
||||
@@ -275,6 +307,7 @@ async function syncCatalogVersion(version: string): Promise<void> {
|
||||
detail,
|
||||
(icon) => `catalog/characters/talents/${webpName(icon)}`,
|
||||
),
|
||||
ascension: ascensionSnapshot(detail),
|
||||
};
|
||||
},
|
||||
);
|
||||
@@ -339,6 +372,14 @@ async function syncCatalogVersion(version: string): Promise<void> {
|
||||
source: item.sourceImage,
|
||||
target: item.imageKey,
|
||||
});
|
||||
for (const material of materials) {
|
||||
const source = materialData[material.key].icon;
|
||||
uniqueAssets.set(material.imageKey, {
|
||||
source: `${API_ROOT}/assets/items/${encodeURIComponent(pngName(source))}`,
|
||||
target: material.imageKey,
|
||||
type: "image/png",
|
||||
});
|
||||
}
|
||||
for (const character of characters) {
|
||||
if (character.elementIconKey && character.element)
|
||||
uniqueAssets.set(character.elementIconKey, {
|
||||
@@ -414,6 +455,7 @@ async function syncCatalogVersion(version: string): Promise<void> {
|
||||
nameCardImageKey: item.nameCardImageKey,
|
||||
constellations: item.constellations,
|
||||
talents: item.talents,
|
||||
ascension: item.ascension,
|
||||
})),
|
||||
),
|
||||
{ type: "application/json", acl: "public-read" },
|
||||
@@ -443,10 +485,16 @@ async function syncCatalogVersion(version: string): Promise<void> {
|
||||
),
|
||||
{ type: "application/json", acl: "public-read" },
|
||||
),
|
||||
storage.write(
|
||||
"catalog/materials.json",
|
||||
JSON.stringify(materials),
|
||||
{ type: "application/json", acl: "public-read" },
|
||||
),
|
||||
]);
|
||||
|
||||
await getDb().transaction(async (tx) => {
|
||||
await tx.delete(catalogCharacters);
|
||||
await tx.delete(catalogMaterials);
|
||||
await tx.delete(catalogWeapons);
|
||||
await tx.delete(catalogArtifacts);
|
||||
if (characters.length)
|
||||
@@ -465,8 +513,11 @@ async function syncCatalogVersion(version: string): Promise<void> {
|
||||
nameCardImageKey: item.nameCardImageKey,
|
||||
constellations: item.constellations,
|
||||
talents: item.talents,
|
||||
ascension: item.ascension,
|
||||
})),
|
||||
);
|
||||
if (materials.length)
|
||||
await tx.insert(catalogMaterials).values(materials);
|
||||
if (weapons.length)
|
||||
await tx
|
||||
.insert(catalogWeapons)
|
||||
|
||||
Reference in New Issue
Block a user