feat : buzz feedback
CI / Verify (push) Successful in 1m5s
CI / Build immutable images and deploy (push) Successful in 1m41s

This commit is contained in:
2026-08-30 15:11:35 +00:00 Unverified
parent b2e9b69619
commit 14a98c55f1
13 changed files with 418 additions and 96 deletions
+13
View File
@@ -35,6 +35,19 @@ describe("structured guide validation", () => {
expect(artifactSectionSchema.safeParse({ ...base, groups: [{ name: "Late Game", rows: [{ ...row, note: "x".repeat(2_001) }] }, artifactGroups[1]] }).success).toBe(false);
});
it("accepts fixed target stats with text values", () => {
const base = { expectedVersion: 1, groups: artifactGroups, sands: [], goblet: [], circlet: [], substats: [], talents: [], note: null };
expect(artifactSectionSchema.safeParse({
...base,
targets: [
{ stat: "ER", value: "100%" },
{ stat: "CRIT RATE", value: "5%" },
{ stat: "CRIT DAMAGE", value: "-%" },
],
}).success).toBe(true);
expect(artifactSectionSchema.safeParse({ ...base, targets: [{ stat: "Energy Recharge", value: "100%" }] }).success).toBe(false);
});
it("requires the fixed Late Game and Early Game artifact groups", () => {
const base = { expectedVersion: 1, sands: [], goblet: [], circlet: [], substats: [], targets: [], talents: [], note: null };
expect(artifactSectionSchema.safeParse({ ...base, groups: artifactGroups }).success).toBe(true);
+3 -1
View File
@@ -1,5 +1,7 @@
import * as z from "zod";
import { targetStatOptions } from "@/lib/guides/stats";
export const guideSectionKinds = ["weapon", "artifact", "constellations", "team"] as const;
export type GuideSectionKind = (typeof guideSectionKinds)[number];
@@ -62,7 +64,7 @@ export const artifactSectionSchema = z.strictObject({
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),
stat: z.enum(targetStatOptions),
value: z.string().trim().min(1).max(80),
})).max(20),
talents: z.array(z.strictObject({
+55
View File
@@ -0,0 +1,55 @@
export const targetStatOptions = [
"HP",
"ATK",
"DEF",
"EM",
"ER",
"CRIT RATE",
"CRIT DAMAGE",
] as const;
export type TargetStat = (typeof targetStatOptions)[number];
export const targetStatDefaults: Array<{ stat: TargetStat; value: string }> = [
{ stat: "HP", value: "" },
{ stat: "ATK", value: "" },
{ stat: "DEF", value: "" },
{ stat: "EM", value: "" },
{ stat: "ER", value: "100%" },
{ stat: "CRIT RATE", value: "5%" },
{ stat: "CRIT DAMAGE", value: "50%" },
];
const legacyTargetStatNames: Record<string, TargetStat> = {
"CRIT Rate": "CRIT RATE",
"CRIT DMG": "CRIT DAMAGE",
};
export function normalizeTargetStat(value: string): TargetStat | null {
const normalized = legacyTargetStatNames[value] ?? value;
return targetStatOptions.includes(normalized as TargetStat)
? (normalized as TargetStat)
: null;
}
export function targetStatIconName(value: string): string | null {
const stat = normalizeTargetStat(value);
if (!stat) return null;
return stat === "CRIT RATE"
? "critrate"
: stat === "CRIT DAMAGE"
? "critdmg"
: stat.toLowerCase();
}
export function statPriorityColorClass(index: number): string {
return index === 0
? "text-tier-gold"
: index === 1
? "text-tier-silver"
: "text-tier-copper";
}
export function statPriorityColor(index: number): string {
return index === 0 ? "#e0b35f" : index === 1 ? "#c7cbd3" : "#bd8055";
}
+7
View File
@@ -0,0 +1,7 @@
export function navigationIconPath(slug: string): string | null {
if (slug === "weapon") return "/icon/nav/Weapon.png";
if (slug === "artifact") return "/icon/nav/Artifact.webp";
if (slug === "constellations") return "/icon/nav/Constellations.webp";
if (slug === "team") return "/icon/nav/Team-calc.webp";
return null;
}