Files
buzz-sheet/lib/guides/schemas.ts
T
gunshiz fd3472bc8d
CI / Verify (push) Successful in 52s
CI / Build immutable images and deploy (push) Successful in 1m34s
feat : update
2026-08-30 11:04:34 +00:00

125 lines
4.9 KiB
TypeScript

import * as z from "zod";
export const guideSectionKinds = ["weapon", "artifact", "constellations", "team"] as const;
export type GuideSectionKind = (typeof guideSectionKinds)[number];
const percent = z.coerce.number().finite().min(0).max(9999);
const catalogKey = z.string().trim().min(1).max(64);
export const createGuideSchema = z.strictObject({
characterKey: catalogKey,
coverMediaId: z.string().uuid(),
overview: z.string().trim().max(500),
});
export const updateGuideSchema = z.strictObject({
expectedVersion: z.number().int().positive(),
coverMediaId: z.string().uuid(),
overview: z.string().trim().min(1).max(500),
});
export const weaponSectionSchema = z.strictObject({
expectedVersion: z.number().int().positive(),
rows: z.array(z.strictObject({
weaponKey: catalogKey,
refinement: z.number().int().min(1).max(5),
overallPercent: percent.nullable(),
personalPercent: percent.nullable(),
}).refine((row) => row.overallPercent !== null || row.personalPercent !== null, {
message: "อาวุธต้องมีเปอร์เซ็นต์อย่างน้อยหนึ่งค่า",
})).max(100),
});
export const mainStatOptions = {
sands: ["EM", "ER", "ATK%", "HP%", "DEF%"],
goblet: ["EM", "ATK%", "DEF%", "HP%", "Pyro DMG%", "Hydro DMG%", "Cryo DMG%", "Electro DMG%", "Anemo DMG%", "Geo DMG%", "Dendro DMG%", "Physical DMG%"],
circlet: ["HP%", "ATK%", "DEF%", "EM", "CRIT Rate%/CRIT DMG%", "Healing Bonus%"],
} as const;
export const substatOptions = ["CRIT Rate%/CRIT DMG%", "HP%", "ATK%", "DEF%", "EM", "ER"] as const;
export const talentLevelOptions = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"] as const;
const artifactGroupSchema = (name: "Late Game" | "Early Game") => z.strictObject({
name: z.literal(name),
rows: z.array(z.strictObject({
firstArtifactKey: catalogKey,
secondArtifactKey: catalogKey.nullable(),
percent,
})).max(50),
});
export const artifactSectionSchema = z.strictObject({
expectedVersion: z.number().int().positive(),
groups: z.tuple([
artifactGroupSchema("Late Game"),
artifactGroupSchema("Early Game"),
]),
sands: z.array(z.enum(mainStatOptions.sands)).max(2),
goblet: z.array(z.enum(mainStatOptions.goblet)).max(2),
circlet: z.array(z.enum(mainStatOptions.circlet)).max(2),
substats: z.array(z.enum(substatOptions)).max(substatOptions.length),
targets: z.array(z.strictObject({
stat: z.string().trim().min(1).max(80),
value: z.string().trim().min(1).max(80),
})).max(20),
talents: z.array(z.strictObject({
type: z.enum(["NA", "E", "Q"]),
priority: z.number().int().min(1).max(3),
level: z.enum(talentLevelOptions).optional(),
})).max(3),
note: z.string().trim().max(2_000).nullable(),
});
export const constellationSectionSchema = z.strictObject({
expectedVersion: z.number().int().positive(),
rows: z.array(z.strictObject({
level: z.number().int().min(1).max(6),
detail: z.string().trim().min(1).max(2_000),
cumulativePercent: percent,
})).max(6),
});
export const teamSectionSchema = z.strictObject({
expectedVersion: z.number().int().positive(),
teams: z.array(z.strictObject({
title: z.string().trim().min(1).max(160),
rotationSeconds: z.coerce.number().finite().positive().max(9999),
combo: z.string().trim().min(1).max(2_000),
members: z.array(z.strictObject({
characterKey: catalogKey,
weaponKey: catalogKey.nullable(),
artifactKey: catalogKey.nullable(),
constellation: z.number().int().min(0).max(6),
refinement: z.number().int().min(1).max(5),
damage: z.coerce.number().finite().nonnegative().max(999_999_999_999),
})).length(4).refine((members) => new Set(members.map((member) => member.characterKey)).size === 4, {
message: "Team ต้องมีตัวละครไม่ซ้ำกัน 4 ตัว",
}).refine((members) => members.some((member) => member.damage > 0), {
message: "ความเสียหายรวมต้องมากกว่า 0",
}),
})).max(30),
});
export const extraSectionSchema = z.strictObject({
expectedVersion: z.number().int().positive(),
sections: z.array(z.strictObject({
title: z.string().trim().min(1).max(160),
rows: z.array(z.strictObject({
mediaId: z.string().uuid().nullable(),
heading: z.string().trim().min(1).max(200),
body: z.string().trim().max(2_000),
percent: percent.nullable(),
})).max(50),
})).max(20),
});
export type WeaponSectionInput = z.output<typeof weaponSectionSchema>;
export type ArtifactSectionInput = z.output<typeof artifactSectionSchema>;
export type ConstellationSectionInput = z.output<typeof constellationSectionSchema>;
export type TeamSectionInput = z.output<typeof teamSectionSchema>;
export function slugFromCharacterName(name: string, key: string): string {
const slug = name.normalize("NFKD").toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "");
return slug || `character-${key.toLowerCase()}`;
}