feat : no longer limit stats choosing
This commit is contained in:
@@ -143,6 +143,7 @@ import {
|
||||
normalizeTargetStat,
|
||||
recommendedStatElement,
|
||||
recommendedStatIconName,
|
||||
recommendedStatOptionConflicts,
|
||||
targetStatDefaults,
|
||||
targetStatIconName,
|
||||
targetStatOptions,
|
||||
@@ -716,7 +717,6 @@ function StatSelect({
|
||||
label,
|
||||
options,
|
||||
value,
|
||||
max,
|
||||
onChange,
|
||||
elementIconUrls,
|
||||
priorityColors = false,
|
||||
@@ -724,7 +724,6 @@ function StatSelect({
|
||||
label: string;
|
||||
options: readonly string[];
|
||||
value: string[];
|
||||
max: number;
|
||||
onChange: (value: string[]) => void;
|
||||
elementIconUrls: Record<string, string>;
|
||||
priorityColors?: boolean;
|
||||
@@ -735,9 +734,7 @@ function StatSelect({
|
||||
<Select
|
||||
multiple
|
||||
value={value}
|
||||
onValueChange={(next) => {
|
||||
if (next.length <= max) onChange(next);
|
||||
}}
|
||||
onValueChange={onChange}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<StatIcons values={value} elementIconUrls={elementIconUrls} />
|
||||
@@ -757,7 +754,7 @@ function StatSelect({
|
||||
<SelectItem
|
||||
key={option}
|
||||
value={option}
|
||||
disabled={value.length >= max && !value.includes(option)}
|
||||
disabled={recommendedStatOptionConflicts(option, value)}
|
||||
className={priorityColors && value.includes(option)
|
||||
? statPriorityColorClass(value.indexOf(option))
|
||||
: undefined}
|
||||
@@ -769,7 +766,6 @@ function StatSelect({
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FieldDescription>เลือกได้สูงสุด {max} ค่า</FieldDescription>
|
||||
</Field>
|
||||
);
|
||||
}
|
||||
@@ -2198,12 +2194,11 @@ function ArtifactEditor({
|
||||
<Textarea id={`recommended-condition-note-${recommendedGroups[activeRecommendedGroupIndex]?.clientId}`} value={recommendedGroups[activeRecommendedGroupIndex]?.note ?? ""} onChange={(event) => setRecommendedGroups(recommendedGroups.map((group, index) => index === activeRecommendedGroupIndex ? { ...group, note: event.target.value } : group))} />
|
||||
</Field>
|
||||
|
||||
<div className="grid overflow-hidden rounded-xl border sm:grid-cols-3">
|
||||
<div className="grid gap-4 sm:grid-cols-3">
|
||||
<StatSelect
|
||||
label="Sands / นาฬิกา"
|
||||
options={mainStatOptions.sands}
|
||||
value={sands}
|
||||
max={3}
|
||||
onChange={(values) => setRecommendedStat("sands", values)}
|
||||
elementIconUrls={elementIconUrls}
|
||||
priorityColors
|
||||
@@ -2212,7 +2207,6 @@ function ArtifactEditor({
|
||||
label="Goblet / ถ้วย"
|
||||
options={mainStatOptions.goblet}
|
||||
value={goblet}
|
||||
max={3}
|
||||
onChange={(values) => setRecommendedStat("goblet", values)}
|
||||
elementIconUrls={elementIconUrls}
|
||||
priorityColors
|
||||
@@ -2221,7 +2215,6 @@ function ArtifactEditor({
|
||||
label="Circlet / หมวก"
|
||||
options={mainStatOptions.circlet}
|
||||
value={circlet}
|
||||
max={3}
|
||||
onChange={(values) => setRecommendedStat("circlet", values)}
|
||||
elementIconUrls={elementIconUrls}
|
||||
priorityColors
|
||||
@@ -2231,7 +2224,6 @@ function ArtifactEditor({
|
||||
label="Substats"
|
||||
options={substatOptions}
|
||||
value={substats}
|
||||
max={substatOptions.length}
|
||||
onChange={(values) => setRecommendedStat("substats", values)}
|
||||
elementIconUrls={elementIconUrls}
|
||||
priorityColors
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
import {
|
||||
recommendedStatElement,
|
||||
recommendedStatIconName,
|
||||
recommendedStatOptionConflicts,
|
||||
targetStatIconName,
|
||||
} from "@/lib/guides/stats";
|
||||
|
||||
@@ -84,10 +85,9 @@ describe("structured guide validation", () => {
|
||||
expect(constellationSectionSchema.safeParse({ expectedVersion: 1, note: "x".repeat(2_001), groups: [] }).success).toBe(false);
|
||||
});
|
||||
|
||||
it("limits artifact main stats to three per slot", () => {
|
||||
it("allows every artifact main stat option to be selected", () => {
|
||||
const base = { expectedVersion: 1, groups: artifactGroups, goblet: [], circlet: [], substats: [], targetGroups: [], talents: [], note: null };
|
||||
expect(artifactSectionSchema.safeParse({ ...base, sands: ["EM", "ER", "ATK%"] }).success).toBe(true);
|
||||
expect(artifactSectionSchema.safeParse({ ...base, sands: ["EM", "ER", "ATK%", "HP%"] }).success).toBe(false);
|
||||
expect(artifactSectionSchema.safeParse({ ...base, sands: ["EM", "ER", "ATK%", "HP%", "DEF%"] }).success).toBe(true);
|
||||
});
|
||||
|
||||
it("validates recommended stat conditions independently and preserves their order", () => {
|
||||
@@ -97,7 +97,7 @@ describe("structured guide validation", () => {
|
||||
const result = artifactSectionSchema.parse({ ...base, recommendedGroups: [first, second] });
|
||||
expect(result.recommendedGroups?.map((group) => group.name)).toEqual(["DPS", null]);
|
||||
expect(result.recommendedGroups?.[1].sands).toEqual(["ER"]);
|
||||
expect(artifactSectionSchema.safeParse({ ...base, recommendedGroups: [{ ...first, sands: ["EM", "ER", "ATK%", "HP%"] }] }).success).toBe(false);
|
||||
expect(artifactSectionSchema.safeParse({ ...base, recommendedGroups: [{ ...first, sands: ["EM", "ER", "ATK%", "HP%", "DEF%"] }] }).success).toBe(true);
|
||||
expect(artifactSectionSchema.safeParse({ ...base, recommendedGroups: [{ ...first, goblet: ["invalid"] }] }).success).toBe(false);
|
||||
expect(artifactSectionSchema.safeParse({ ...base, recommendedGroups: [] }).success).toBe(false);
|
||||
expect(artifactSectionSchema.safeParse({ ...base, recommendedGroups: Array(21).fill(first) }).success).toBe(false);
|
||||
@@ -115,6 +115,16 @@ describe("structured guide validation", () => {
|
||||
expect(targetStatIconName("DEF")).toBe("def");
|
||||
});
|
||||
|
||||
it("supports separate CRIT choices and keeps them mutually exclusive with the combined choice", () => {
|
||||
const base = { expectedVersion: 1, groups: artifactGroups, sands: [], goblet: [], targetGroups: [], talents: [], note: null };
|
||||
expect(artifactSectionSchema.safeParse({ ...base, circlet: ["CRIT Rate%", "CRIT DMG%"], substats: ["CRIT Rate%", "CRIT DMG%"] }).success).toBe(true);
|
||||
expect(recommendedStatIconName("CRIT Rate%")).toBe("critrate");
|
||||
expect(recommendedStatIconName("CRIT DMG%")).toBe("critdmg");
|
||||
expect(recommendedStatOptionConflicts("CRIT Rate%/CRIT DMG%", ["CRIT Rate%"])).toBe(true);
|
||||
expect(recommendedStatOptionConflicts("CRIT Rate%", ["CRIT Rate%/CRIT DMG%"])).toBe(true);
|
||||
expect(recommendedStatOptionConflicts("CRIT DMG%", ["CRIT Rate%"])).toBe(false);
|
||||
});
|
||||
|
||||
it("maps elemental damage recommendations to Lunaris element icons", () => {
|
||||
expect(recommendedStatElement("Pyro DMG%")).toBe("Pyro");
|
||||
expect(recommendedStatElement("Hydro DMG%")).toBe("Hydro");
|
||||
|
||||
+10
-10
@@ -70,10 +70,10 @@ export const weaponSectionSchema = z.strictObject({
|
||||
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%"],
|
||||
circlet: ["HP%", "ATK%", "DEF%", "EM", "CRIT Rate%", "CRIT DMG%", "CRIT Rate%/CRIT DMG%", "Healing Bonus%"],
|
||||
} as const;
|
||||
|
||||
export const substatOptions = ["CRIT Rate%/CRIT DMG%", "HP%", "HP", "ATK%", "ATK", "DEF%", "DEF", "EM", "ER"] as const;
|
||||
export const substatOptions = ["CRIT Rate%", "CRIT DMG%", "CRIT Rate%/CRIT DMG%", "HP%", "HP", "ATK%", "ATK", "DEF%", "DEF", "EM", "ER"] as const;
|
||||
export const talentLevelOptions = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"] as const;
|
||||
export const talentRelationOptions = ["gt", "gte", "eq"] as const;
|
||||
|
||||
@@ -97,17 +97,17 @@ export const artifactSectionSchema = z.strictObject({
|
||||
artifactGroupSchema("Early Game"),
|
||||
]),
|
||||
})).min(1).max(20),
|
||||
sands: z.array(z.enum(mainStatOptions.sands)).max(3),
|
||||
goblet: z.array(z.enum(mainStatOptions.goblet)).max(3),
|
||||
circlet: z.array(z.enum(mainStatOptions.circlet)).max(3),
|
||||
substats: z.array(z.enum(substatOptions)).max(substatOptions.length),
|
||||
sands: z.array(z.enum(mainStatOptions.sands)),
|
||||
goblet: z.array(z.enum(mainStatOptions.goblet)),
|
||||
circlet: z.array(z.enum(mainStatOptions.circlet)),
|
||||
substats: z.array(z.enum(substatOptions)),
|
||||
recommendedGroups: z.array(z.strictObject({
|
||||
name: conditionName,
|
||||
note: z.string().trim().max(2_000).nullable(),
|
||||
sands: z.array(z.enum(mainStatOptions.sands)).max(3),
|
||||
goblet: z.array(z.enum(mainStatOptions.goblet)).max(3),
|
||||
circlet: z.array(z.enum(mainStatOptions.circlet)).max(3),
|
||||
substats: z.array(z.enum(substatOptions)).max(substatOptions.length),
|
||||
sands: z.array(z.enum(mainStatOptions.sands)),
|
||||
goblet: z.array(z.enum(mainStatOptions.goblet)),
|
||||
circlet: z.array(z.enum(mainStatOptions.circlet)),
|
||||
substats: z.array(z.enum(substatOptions)),
|
||||
})).min(1).max(20).optional(),
|
||||
targetGroups: z.array(z.strictObject({
|
||||
name: conditionName,
|
||||
|
||||
@@ -44,6 +44,8 @@ export function targetStatIconName(value: string): string | null {
|
||||
|
||||
export function recommendedStatIconName(value: string): string | null {
|
||||
if (value === "CRIT Rate%/CRIT DMG%") return "critrate";
|
||||
if (value === "CRIT Rate%") return "critrate";
|
||||
if (value === "CRIT DMG%") return "critdmg";
|
||||
if (value === "Healing Bonus%") return "healing-bonus";
|
||||
if (value.endsWith(" DMG%")) return "elemental-bonus";
|
||||
|
||||
@@ -52,6 +54,14 @@ export function recommendedStatIconName(value: string): string | null {
|
||||
return ["hp", "atk", "def", "em", "er"].includes(base) ? base : null;
|
||||
}
|
||||
|
||||
export function recommendedStatOptionConflicts(option: string, selected: readonly string[]): boolean {
|
||||
const combined = "CRIT Rate%/CRIT DMG%";
|
||||
const individual = ["CRIT Rate%", "CRIT DMG%"];
|
||||
return option === combined
|
||||
? individual.some((stat) => selected.includes(stat))
|
||||
: individual.includes(option) && selected.includes(combined);
|
||||
}
|
||||
|
||||
export function recommendedStatElement(value: string): string | null {
|
||||
const match = /^(Anemo|Geo|Electro|Dendro|Hydro|Pyro|Cryo) DMG%$/u.exec(value);
|
||||
return match?.[1] ?? null;
|
||||
|
||||
Reference in New Issue
Block a user