using Godot; using Theriapolis.Core.Data; 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; var btn = new Button { Text = "", Flat = false, ToggleMode = true, ButtonPressed = selected, FocusMode = Control.FocusModeEnum.None, // 200 wide so 3 cards + separators (≈ 632) + page margins + // Aside fit inside ≥ 1024-px viewports (the smaller-screen // floor we want to support). Height held at 200 so the inner // labels render at readable size without a content-driven // height collapse (Button isn't a Container, so child vbox // height doesn't bubble up to the button's intrinsic min size). CustomMinimumSize = new Vector2(200, 200), ClipText = false, Alignment = HorizontalAlignment.Left, }; btn.Pressed += () => { // 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 }, }); }; // Label content stacked inside the button via an anchored VBoxContainer // (Button isn't a Container, so we anchor the vbox to fill the button's // rect and let the children flow within it). var box = new VBoxContainer { MouseFilter = MouseFilterEnum.Ignore, }; box.AnchorRight = 1f; box.AnchorBottom = 1f; box.OffsetLeft = 12; box.OffsetTop = 12; box.OffsetRight = -12; box.OffsetBottom = -12; box.AddThemeConstantOverride("separation", 6); btn.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) { box.AddChild(new Label { Text = $"{clade.Traits.Length} traits, {clade.Detriments.Length} detriments", MouseFilter = MouseFilterEnum.Ignore, }); } return btn; } }