using Godot; using System.Linq; using Theriapolis.Core.Data; using Theriapolis.GodotHost.Scenes.Widgets; using Theriapolis.GodotHost.UI; namespace Theriapolis.GodotHost.Scenes.Steps; /// /// Step III — Calling. Direct port of StepClass in /// src/steps.jsx: card per class showing hit die, primary /// abilities, saves, and level-1 features (subclass-selection /// stubs filtered out per the React prototype's contract). Class /// change clears chosen skills and the previously-chosen subclass. /// public partial class StepClass : VBoxContainer, IStep { private CharacterDraft _draft = null!; private GridContainer _grid = null!; public void Bind(CharacterDraft draft) { _draft = draft; // Defer Refresh so it runs after the click callback that triggered // Changed completes (avoids the Free()-during-signal duplicate bug). _draft.Changed += () => Callable.From(Refresh).CallDeferred(); Build(); } public string? Validate() => string.IsNullOrEmpty(_draft?.ClassId) ? "Pick a calling." : null; private void Build() { AddThemeConstantOverride("separation", 16); var intro = new VBoxContainer(); intro.AddThemeConstantOverride("separation", 6); AddChild(intro); intro.AddChild(new Label { Text = "FOLIO III · CALLING", ThemeTypeVariation = "Eyebrow" }); intro.AddChild(new Label { Text = "Choose a Calling", ThemeTypeVariation = "H2" }); intro.AddChild(new Label { Text = "Your character's path — fighter, hunter, scholar, or something stranger. " + "The calling fixes your hit die, primary abilities, saving-throw " + "proficiencies, and the level-1 feature set you start play with.", AutowrapMode = TextServer.AutowrapMode.WordSmart, }); // Single-column layout matches StepClade / StepSpecies — each card // spans the wizard's content width so the description text fits // comfortably and the calling's tone lands before the mechanics. _grid = new GridContainer { Columns = 1, SizeFlagsHorizontal = SizeFlags.ExpandFill }; _grid.AddThemeConstantOverride("v_separation", 12); AddChild(_grid); Refresh(); } private void Refresh() { if (_grid is null) return; foreach (var c in _grid.GetChildren()) c.QueueFree(); foreach (var cls in CodexContent.Classes) _grid.AddChild(BuildCard(cls)); } private Control BuildCard(ClassDef cls) { bool selected = _draft.ClassId == cls.Id; var card = CodexCard.Make(); card.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill; CodexCard.SetSelected(card, selected); card.GuiInput += (InputEvent e) => { if (e is InputEventMouseButton mb && mb.Pressed && mb.ButtonIndex == MouseButton.Left) { // Class change: reset subclass + chosen skills, mirroring // app.jsx's useEffect on classId. _draft.Patch(new Godot.Collections.Dictionary { { "class_id", cls.Id }, { "subclass_id", "" }, { "chosen_skills", new Godot.Collections.Array() }, }); } }; var box = new VBoxContainer { MouseFilter = MouseFilterEnum.Pass }; box.AddThemeConstantOverride("separation", 6); card.AddChild(box); box.AddChild(new Label { Text = cls.Name, ThemeTypeVariation = "CardName" }); box.AddChild(new Label { Text = $"d{cls.HitDie} · {string.Join("/", cls.PrimaryAbility)}", ThemeTypeVariation = "CardMeta", }); if (!string.IsNullOrEmpty(cls.Description)) { box.AddChild(new Label { Text = cls.Description, AutowrapMode = TextServer.AutowrapMode.WordSmart, MouseFilter = Control.MouseFilterEnum.Pass, }); } if (cls.Saves.Length > 0) { var savesRow = new HBoxContainer(); savesRow.AddThemeConstantOverride("separation", 6); box.AddChild(savesRow); savesRow.AddChild(new Label { Text = "SAVES", ThemeTypeVariation = "Eyebrow" }); foreach (var s in cls.Saves) savesRow.AddChild(new Label { Text = s, ThemeTypeVariation = "CardMeta" }); } // Level-1 features. Filter out stubs and subclass-selection markers // (the React prototype hides the subclass picker on the class card). var lvl1 = cls.LevelTable.FirstOrDefault(e => e.Level == 1); if (lvl1?.Features.Length > 0) { var featChips = new HFlowContainer { MouseFilter = MouseFilterEnum.Pass }; featChips.AddThemeConstantOverride("h_separation", 6); featChips.AddThemeConstantOverride("v_separation", 4); box.AddChild(featChips); foreach (var fid in lvl1.Features) { if (!cls.FeatureDefinitions.TryGetValue(fid, out var fd)) continue; if (fd.Kind == "stub" || fid.StartsWith("subclass_")) continue; featChips.AddChild(new TraitChip { TraitName = fd.Name, Description = fd.Description, Tag = fd.Kind, }); } } return card; } }