using Godot; using Theriapolis.Core.Data; using Theriapolis.GodotHost.Scenes.Widgets; using Theriapolis.GodotHost.UI; namespace Theriapolis.GodotHost.Scenes.Steps; /// /// Step II — Species. Direct port of StepSpecies in /// src/steps.jsx plus the Phase 6.5 hybrid extension: when /// is true the step shows two /// stacked grids, one filtered by SireCladeId and one by DamCladeId. /// public partial class StepSpecies : VBoxContainer, IStep { private CharacterDraft _draft = null!; private VBoxContainer _purebredSection = null!; private VBoxContainer _hybridSection = null!; private GridContainer _purebredGrid = null!; private GridContainer _sireGrid = null!; private GridContainer _damGrid = null!; public void Bind(CharacterDraft draft) { _draft = draft; // Defer Refresh so it runs after the click callback that triggered // Changed completes — Free() called on a card mid-callback returns // without freeing, leaving a stale duplicate in the grid. _draft.Changed += () => Callable.From(Refresh).CallDeferred(); Build(); } public string? Validate() => WizardValidation.Validate(1, _draft); private void Build() { AddThemeConstantOverride("separation", 16); var intro = new VBoxContainer(); intro.AddThemeConstantOverride("separation", 6); AddChild(intro); intro.AddChild(new Label { Text = "FOLIO II · SPECIES" }); intro.AddChild(new Label { Text = "Choose a Species" }); intro.AddChild(new Label { Text = "Refine your line. Species inherits the clade's traits and adds its " + "own — body size, base movement speed, and species-specific abilities. " + "Hybrid characters pick one species per parent lineage.", AutowrapMode = TextServer.AutowrapMode.WordSmart, }); _purebredSection = new VBoxContainer(); _purebredSection.AddThemeConstantOverride("separation", 6); AddChild(_purebredSection); _purebredGrid = MakeGrid(); _purebredSection.AddChild(_purebredGrid); _hybridSection = new VBoxContainer(); _hybridSection.AddThemeConstantOverride("separation", 16); AddChild(_hybridSection); _hybridSection.AddChild(new Label { Text = "SIRE — Paternal Lineage" }); _sireGrid = MakeGrid(); _hybridSection.AddChild(_sireGrid); _hybridSection.AddChild(new Label { Text = "DAM — Maternal Lineage" }); _damGrid = MakeGrid(); _hybridSection.AddChild(_damGrid); Refresh(); } private static GridContainer MakeGrid() { var grid = new GridContainer { Columns = 3, SizeFlagsHorizontal = SizeFlags.ExpandFill }; grid.AddThemeConstantOverride("h_separation", 16); grid.AddThemeConstantOverride("v_separation", 16); return grid; } private void Refresh() { if (_purebredGrid is null) return; bool hybrid = _draft.IsHybrid; _purebredSection.Visible = !hybrid; _hybridSection.Visible = hybrid; if (hybrid) { RefreshGrid(_sireGrid, _draft.SireCladeId, _draft.SireSpeciesId, spId => _draft.Patch(new Godot.Collections.Dictionary { { "sire_species_id", spId } })); RefreshGrid(_damGrid, _draft.DamCladeId, _draft.DamSpeciesId, spId => _draft.Patch(new Godot.Collections.Dictionary { { "dam_species_id", spId } })); } else { RefreshGrid(_purebredGrid, _draft.CladeId, _draft.SpeciesId, spId => _draft.Patch(new Godot.Collections.Dictionary { { "species_id", spId } })); } } private static void RefreshGrid(GridContainer grid, string cladeId, string selectedSpecies, System.Action onClick) { foreach (var c in grid.GetChildren()) c.Free(); if (string.IsNullOrEmpty(cladeId)) return; foreach (var sp in CodexContent.SpeciesOfClade(cladeId)) grid.AddChild(BuildCard(sp, sp.Id == selectedSpecies, onClick)); } private static Control BuildCard(SpeciesDef sp, bool selected, System.Action onClick) { var card = new PanelContainer { CustomMinimumSize = new Vector2(200, 0), MouseFilter = MouseFilterEnum.Stop, }; if (selected) card.Modulate = new Color(1f, 0.95f, 0.85f); card.GuiInput += (InputEvent e) => { if (e is InputEventMouseButton mb && mb.Pressed && mb.ButtonIndex == MouseButton.Left) onClick(sp.Id); }; var box = new VBoxContainer { MouseFilter = MouseFilterEnum.Pass }; box.AddThemeConstantOverride("separation", 6); card.AddChild(box); box.AddChild(new Label { Text = sp.Name }); box.AddChild(new Label { Text = $"{sp.Size.ToUpperInvariant()} · {sp.BaseSpeedFt} FT/TURN" }); if (sp.AbilityMods.Count > 0) { var modsRow = new HBoxContainer(); modsRow.AddThemeConstantOverride("separation", 8); box.AddChild(modsRow); foreach (var (k, v) in sp.AbilityMods) modsRow.AddChild(new Label { Text = $"{k} {(v >= 0 ? "+" : "")}{v}" }); } if (sp.Traits.Length > 0 || sp.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 sp.Traits) chips.AddChild(new TraitChip { TraitName = t.Name, Description = t.Description }); foreach (var d in sp.Detriments) chips.AddChild(new TraitChip { TraitName = d.Name, Description = d.Description, Detriment = true }); } return card; } }