using Godot; using Theriapolis.Core.Data; using Theriapolis.GodotHost.Scenes.Widgets; using Theriapolis.GodotHost.UI; namespace Theriapolis.GodotHost.Scenes.Steps; /// /// Step I — Clade. Direct port of StepClade in /// src/steps.jsx: intro paragraph, then a card grid with one /// card per clade. Click selects via . /// /// Default theme only at this layer (per GODOT_PORTING_GUIDE.md §12 build /// order); the parchment look lands in the final theming pass. /// public partial class StepClade : VBoxContainer, IStep { private CharacterDraft _draft = null!; private GridContainer _grid = null!; public void Bind(CharacterDraft draft) { _draft = draft; _draft.Changed += Refresh; Build(); } public string? Validate() => string.IsNullOrEmpty(_draft?.CladeId) ? "Pick a clade." : null; private void Build() { AddThemeConstantOverride("separation", 16); var intro = new VBoxContainer(); intro.AddThemeConstantOverride("separation", 6); AddChild(intro); intro.AddChild(new Label { Text = "FOLIO I · CLADE" }); intro.AddChild(new Label { Text = "Choose a Clade" }); intro.AddChild(new Label { Text = "The broad mammalian family of your line. Clade defines the largest " + "strokes — predator or prey, communal or solitary, scent-driven or " + "sight-driven. Each clade carries inherited traits and limits that " + "no character escapes.", AutowrapMode = TextServer.AutowrapMode.WordSmart, CustomMinimumSize = new Vector2(0, 0), }); _grid = new GridContainer { Columns = 3, SizeFlagsHorizontal = SizeFlags.ExpandFill, }; _grid.AddThemeConstantOverride("h_separation", 16); _grid.AddThemeConstantOverride("v_separation", 16); AddChild(_grid); Refresh(); } private void Refresh() { if (_grid is null) return; foreach (var c in _grid.GetChildren()) c.QueueFree(); foreach (var clade in CodexContent.Clades) _grid.AddChild(BuildCard(clade)); } private Control BuildCard(CladeDef clade) { bool selected = _draft.CladeId == clade.Id; // PanelContainer (a Container subclass) so the card height is // driven by its inner VBoxContainer's content. Switching from // Button avoids the issue where Button's intrinsic min size // doesn't aggregate from non-Button children, causing chips to // overflow into the cards below at high trait counts. var card = new PanelContainer { CustomMinimumSize = new Vector2(200, 0), MouseFilter = MouseFilterEnum.Stop, }; if (selected) card.Modulate = new Color(1f, 0.95f, 0.85f); // gild tint placeholder until theming card.GuiInput += (InputEvent e) => { if (e is InputEventMouseButton mb && mb.Pressed && mb.ButtonIndex == MouseButton.Left) { // Default species for the new clade — match React app.jsx: // when clade changes, species defaults to first species in clade. string speciesId = ""; foreach (var s in CodexContent.SpeciesOfClade(clade.Id)) { speciesId = s.Id; break; } _draft.Patch(new Godot.Collections.Dictionary { { "clade_id", clade.Id }, { "species_id", speciesId }, }); } }; var box = new VBoxContainer { MouseFilter = MouseFilterEnum.Pass }; box.AddThemeConstantOverride("separation", 6); card.AddChild(box); box.AddChild(new Label { Text = clade.Name, MouseFilter = MouseFilterEnum.Ignore }); box.AddChild(new Label { Text = clade.Kind.ToUpperInvariant(), MouseFilter = MouseFilterEnum.Ignore, }); if (clade.AbilityMods.Count > 0) { var modsRow = new HBoxContainer { MouseFilter = MouseFilterEnum.Ignore }; modsRow.AddThemeConstantOverride("separation", 8); box.AddChild(modsRow); foreach (var (k, v) in clade.AbilityMods) modsRow.AddChild(new Label { Text = $"{k} {(v >= 0 ? "+" : "")}{v}", MouseFilter = MouseFilterEnum.Ignore, }); } if (clade.Traits.Length > 0 || clade.Detriments.Length > 0) { var chips = new HFlowContainer { MouseFilter = MouseFilterEnum.Pass }; chips.AddThemeConstantOverride("h_separation", 6); chips.AddThemeConstantOverride("v_separation", 4); box.AddChild(chips); foreach (var t in clade.Traits) { var chip = new TraitChip { TraitName = t.Name, Description = t.Description, }; chips.AddChild(chip); } foreach (var d in clade.Detriments) { var chip = new TraitChip { TraitName = d.Name, Description = d.Description, Detriment = true, }; chips.AddChild(chip); } } return card; } }