namespace Theriapolis.GodotHost.UI; /// /// Per-step validation against a CharacterDraft. Mirrors app.jsx's /// validate(i) exactly: returns null when step i's /// requirements are met, or a short error message otherwise. Static so /// the Wizard can ask "is step N satisfied?" without instantiating each /// step's UI — needed to compute Locked/Pending states across the whole /// flow, not just the current step. /// /// React prototype's app.jsx logic: /// firstIncomplete = STEPS.findIndex(j => validate(j)); /// locked = i > step && firstIncomplete !== -1 && firstIncomplete < i; /// /// (i.e. a step is locked iff some EARLIER step fails its validator). /// public static class WizardValidation { public static string? Validate(int step, CharacterDraft draft) => step switch { 0 => ValidateClade(draft), 1 => ValidateSpecies(draft), 2 => string.IsNullOrEmpty(draft.ClassId) ? "Pick a calling." : null, 3 => string.IsNullOrEmpty(draft.SubclassId) ? "Pick a subclass." : null, 4 => string.IsNullOrEmpty(draft.BackgroundId) ? "Pick a history." : null, 5 => draft.StatAssign.Count == 6 ? null : $"Assign all six abilities ({draft.StatAssign.Count}/6).", 6 => ValidateSkills(draft), 7 => string.IsNullOrWhiteSpace(draft.CharacterName) ? "Enter a name." : null, _ => null, }; private static string? ValidateClade(CharacterDraft draft) { if (draft.IsHybrid) { if (string.IsNullOrEmpty(draft.SireCladeId)) return "Pick a sire clade."; if (string.IsNullOrEmpty(draft.DamCladeId)) return "Pick a dam clade."; if (draft.SireCladeId == draft.DamCladeId) return "Sire and dam must be different clades."; return null; } return string.IsNullOrEmpty(draft.CladeId) ? "Pick a clade." : null; } private static string? ValidateSpecies(CharacterDraft draft) { if (draft.IsHybrid) { if (string.IsNullOrEmpty(draft.SireSpeciesId)) return "Pick a sire species."; if (string.IsNullOrEmpty(draft.DamSpeciesId)) return "Pick a dam species."; return null; } return string.IsNullOrEmpty(draft.SpeciesId) ? "Pick a species." : null; } private static string? ValidateSkills(CharacterDraft draft) { var cls = CodexContent.Class(draft.ClassId); int need = cls?.SkillsChoose ?? 0; int got = draft.ChosenSkills.Count; if (need == 0) return null; // class has no skill picks (shouldn't happen, defensive) if (got == need) return null; return $"Pick {need} skill{(need == 1 ? "" : "s")} ({got}/{need})."; } /// /// Index of the lowest step whose validator currently fails, or -1 when /// every step is satisfied. The forward-lock rule is: a step i /// after the current step is locked iff FirstIncomplete < i. /// public static int FirstIncomplete(CharacterDraft draft, int stepCount = 8) { for (int i = 0; i < stepCount; i++) if (Validate(i, draft) is not null) return i; return -1; } }