using Godot; using System.Collections.Generic; 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, plus the Phase 6.5 hybrid-origin extension /// per port plan §10: a Hybrid toggle splits the picker into Sire + /// Dam grids, each independent. The dominant-parent radio drives /// for downstream steps. /// /// Cards are built once and mutated in place (Modulate update only) on /// selection change — tearing down + rebuilding inside the click /// callback chain caused duplicates because Free() defers when the /// freed node is currently mid-signal. /// public partial class StepClade : VBoxContainer, IStep { private CharacterDraft _draft = null!; private Button _hybridToggle = null!; private Button _sexMaleBtn = null!; private Button _sexFemaleBtn = null!; private VBoxContainer _purebredSection = null!; private VBoxContainer _hybridSection = null!; private OptionButton _dominantToggle = null!; // Lineage-bonus pickers (per theriapolis-rpg-clades.md hybrid rules, // simplified to allow stacking on the same ability). private VBoxContainer _bonusSection = null!; private HBoxContainer _sireBonusRow = null!; private HBoxContainer _damBonusRow = null!; // Phase B clade-trait pickers: 2 from dominant parent + 1 from non-dominant. private VBoxContainer _traitSection = null!; private VBoxContainer _sireTraitCol = null!; private VBoxContainer _damTraitCol = null!; private Label _sireTraitHeader = null!; private Label _damTraitHeader = null!; private readonly Dictionary _purebredCards = new(); private readonly Dictionary _hybridCards = new(); // Per-card sire/dam toggle buttons in the hybrid grid header. Mutated // in place via SetPressedNoSignal during Refresh — same Free()-defer // hazard as the lineage bonus rows. private readonly Dictionary _sireToggles = new(); private readonly Dictionary _damToggles = new(); // Bonus rows are mutated in place too: when the clade hasn't changed // since the last build, we only flip ButtonPressed states. Rebuilding // would Free() the very button whose Pressed callback we're inside, // which Godot defers — leaving stale buttons next to the new ones. private readonly Dictionary _sireBonusButtons = new(); private readonly Dictionary _damBonusButtons = new(); private string _sireBonusBuiltFor = ""; private string _damBonusBuiltFor = ""; private readonly Dictionary _sireTraitButtons = new(); private readonly Dictionary _damTraitButtons = new(); private string _sireTraitsBuiltFor = ""; private string _damTraitsBuiltFor = ""; public void Bind(CharacterDraft draft) { _draft = draft; _draft.Changed += Refresh; Build(); } public string? Validate() => WizardValidation.Validate(0, _draft); private void Build() { AddThemeConstantOverride("separation", 16); var intro = new VBoxContainer(); intro.AddThemeConstantOverride("separation", 6); AddChild(intro); intro.AddChild(new Label { Text = "FOLIO I · CLADE", ThemeTypeVariation = "Eyebrow" }); intro.AddChild(new Label { Text = "Choose a Clade", ThemeTypeVariation = "H2" }); 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. Hybrid characters blend two lineages.", AutowrapMode = TextServer.AutowrapMode.WordSmart, }); // Sex picker — required for every character. For purebreds with // sex-axis variants (Elk, Lion) this drives variant resolution; // for hybrids it's identity-only since sire/dam are male/female // by parent-role definition. var sexRow = new HBoxContainer(); sexRow.AddThemeConstantOverride("separation", 8); AddChild(sexRow); sexRow.AddChild(new Label { Text = "SEX", ThemeTypeVariation = "Eyebrow" }); _sexMaleBtn = new Button { Text = "Male", ToggleMode = true, FocusMode = Control.FocusModeEnum.None, }; _sexFemaleBtn = new Button { Text = "Female", ToggleMode = true, FocusMode = Control.FocusModeEnum.None, }; _sexMaleBtn.Pressed += () => OnSexPicked("male"); _sexFemaleBtn.Pressed += () => OnSexPicked("female"); sexRow.AddChild(_sexMaleBtn); sexRow.AddChild(_sexFemaleBtn); // Toggle Button (not CheckBox) so the inverted-on-press button style // from the codex theme handles selection visually — no checkbox glyph // needed, the bg colour shift is the affordance. var toggleRow = new HBoxContainer(); toggleRow.AddThemeConstantOverride("separation", 12); AddChild(toggleRow); _hybridToggle = new Button { Text = "Hybrid Origin (two parent lineages)", ToggleMode = true, FocusMode = Control.FocusModeEnum.None, }; _hybridToggle.Toggled += OnHybridToggled; toggleRow.AddChild(_hybridToggle); // Purebred section _purebredSection = new VBoxContainer(); _purebredSection.AddThemeConstantOverride("separation", 6); AddChild(_purebredSection); var purebredGrid = MakeGrid(); _purebredSection.AddChild(purebredGrid); PopulateGrid(purebredGrid, _purebredCards, OnPurebredCladePicked); // Hybrid section _hybridSection = new VBoxContainer(); _hybridSection.AddThemeConstantOverride("separation", 16); AddChild(_hybridSection); // One unified hybrid grid: every clade card carries Sire/Dam toggle // buttons in its header. The same card can become either parent; // picking Sire on a card currently set as Dam clears the Dam pick // (and vice versa) atomically. _hybridSection.AddChild(new Label { Text = "Mark one clade as Sire (paternal) and another as Dam (maternal). " + "A single clade cannot be both.", AutowrapMode = TextServer.AutowrapMode.WordSmart, }); var hybridGrid = MakeGrid(); _hybridSection.AddChild(hybridGrid); foreach (var clade in CodexContent.Clades) { var card = BuildHybridCard(clade); _hybridCards[clade.Id] = card; hybridGrid.AddChild(card); } // Lineage bonus pickers — hybrids pick ONE ability mod from each // parent clade. Stacking on the same ability is allowed; mods sum. _bonusSection = new VBoxContainer(); _bonusSection.AddThemeConstantOverride("separation", 8); _hybridSection.AddChild(_bonusSection); _bonusSection.AddChild(new Label { Text = "LINEAGE BONUSES", ThemeTypeVariation = "Eyebrow" }); _sireBonusRow = new HBoxContainer(); _sireBonusRow.AddThemeConstantOverride("separation", 8); _bonusSection.AddChild(_sireBonusRow); _damBonusRow = new HBoxContainer(); _damBonusRow.AddThemeConstantOverride("separation", 8); _bonusSection.AddChild(_damBonusRow); var dominantRow = new HBoxContainer(); dominantRow.AddThemeConstantOverride("separation", 8); _hybridSection.AddChild(dominantRow); dominantRow.AddChild(new Label { Text = "DOMINANT LINEAGE", ThemeTypeVariation = "Eyebrow" }); _dominantToggle = new OptionButton(); _dominantToggle.AddItem("Sire", 0); _dominantToggle.AddItem("Dam", 1); _dominantToggle.ItemSelected += OnDominantSelected; dominantRow.AddChild(_dominantToggle); // Phase B clade-trait pickers — per doc "2/1 split, player's choice // of which parent is dominant". DominantParent drives the limit. _traitSection = new VBoxContainer(); _traitSection.AddThemeConstantOverride("separation", 8); _hybridSection.AddChild(_traitSection); _traitSection.AddChild(new Label { Text = "CLADE TRAITS", ThemeTypeVariation = "Eyebrow" }); _traitSection.AddChild(new Label { Text = "Pick two clade traits from the dominant parent and one from the other. " + "All clade detriments from both parents are inherited automatically.", AutowrapMode = TextServer.AutowrapMode.WordSmart, }); var traitGrid = new HBoxContainer(); traitGrid.AddThemeConstantOverride("separation", 16); traitGrid.SizeFlagsHorizontal = SizeFlags.ExpandFill; _traitSection.AddChild(traitGrid); _sireTraitCol = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill }; _sireTraitCol.AddThemeConstantOverride("separation", 4); traitGrid.AddChild(_sireTraitCol); _sireTraitHeader = new Label { Text = "SIRE", ThemeTypeVariation = "Eyebrow" }; _sireTraitCol.AddChild(_sireTraitHeader); _damTraitCol = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill }; _damTraitCol.AddThemeConstantOverride("separation", 4); traitGrid.AddChild(_damTraitCol); _damTraitHeader = new Label { Text = "DAM", ThemeTypeVariation = "Eyebrow" }; _damTraitCol.AddChild(_damTraitHeader); Refresh(); } private static GridContainer MakeGrid() { // Single-column layout: each card spans the wizard's content width // and surfaces the clade's description text. Establishes the // world's tone before mechanics — the trade is more scrolling. var grid = new GridContainer { Columns = 1, SizeFlagsHorizontal = SizeFlags.ExpandFill }; grid.AddThemeConstantOverride("v_separation", 12); return grid; } private void PopulateGrid(GridContainer grid, Dictionary cardMap, System.Action onClick) { foreach (var clade in CodexContent.Clades) { var card = BuildCard(clade, onClick); cardMap[clade.Id] = card; grid.AddChild(card); } } private void OnHybridToggled(bool pressed) { var patch = new Godot.Collections.Dictionary { { "is_hybrid", pressed } }; if (pressed) { patch["clade_id"] = ""; patch["species_id"] = ""; } else { patch["sire_clade_id"] = ""; patch["sire_species_id"] = ""; patch["dam_clade_id"] = ""; patch["dam_species_id"] = ""; patch["sire_chosen_ability"] = ""; patch["dam_chosen_ability"] = ""; patch["sire_chosen_clade_traits"] = new Godot.Collections.Array(); patch["dam_chosen_clade_traits"] = new Godot.Collections.Array(); patch["sire_chosen_species_trait"] = ""; patch["dam_chosen_species_trait"] = ""; patch["sire_chosen_species_detriment"] = ""; patch["dam_chosen_species_detriment"] = ""; } ClearBackgroundIfInvalidated(patch); _draft.Patch(patch); } private void OnSexPicked(string sex) => _draft.Patch(new Godot.Collections.Dictionary { { "sex", sex } }); private void OnDominantSelected(long index) { string newDominant = index == 0 ? "sire" : "dam"; var patch = new Godot.Collections.Dictionary { { "dominant_parent", newDominant }, }; // Trim clade-trait picks down to the new limit per side: 2 for the // dominant lineage, 1 for the other. Only trims, never expands — // user re-picks the missing slot manually. TrimToLimit(patch, "sire", _draft.SireChosenCladeTraits, newDominant == "sire" ? 2 : 1); TrimToLimit(patch, "dam", _draft.DamChosenCladeTraits, newDominant == "dam" ? 2 : 1); _draft.Patch(patch); } private static void TrimToLimit(Godot.Collections.Dictionary patch, string lineage, Godot.Collections.Array current, int limit) { if (current.Count <= limit) return; var trimmed = new Godot.Collections.Array(); for (int i = 0; i < limit; i++) trimmed.Add(current[i]); patch[lineage + "_chosen_clade_traits"] = trimmed; } private void Refresh() { if (_hybridToggle is null) return; bool hybrid = _draft.IsHybrid; if (_hybridToggle.ButtonPressed != hybrid) _hybridToggle.SetPressedNoSignal(hybrid); _purebredSection.Visible = !hybrid; _hybridSection.Visible = hybrid; bool isMale = _draft.Sex == "male"; bool isFemale = _draft.Sex == "female"; if (_sexMaleBtn.ButtonPressed != isMale) _sexMaleBtn.SetPressedNoSignal(isMale); if (_sexFemaleBtn.ButtonPressed != isFemale) _sexFemaleBtn.SetPressedNoSignal(isFemale); UpdateSelection(_purebredCards, _draft.CladeId); UpdateHybridCards(); int dominantIdx = _draft.DominantParent == "dam" ? 1 : 0; if (_dominantToggle.Selected != dominantIdx) _dominantToggle.Select(dominantIdx); if (hybrid) { RebuildBonusPickers(); RebuildTraitPickers(); } } /// /// Syncs the lineage-bonus button rows with the current sire/dam /// clade picks. When the clade hasn't changed since the last build /// we only update each button's ButtonPressed state — see field /// comment above for why a full rebuild here is unsafe. /// private void RebuildBonusPickers() { SyncLineageBonusRow(_sireBonusRow, _sireBonusButtons, ref _sireBonusBuiltFor, "sire", _draft.SireCladeId, _draft.SireChosenAbility); SyncLineageBonusRow(_damBonusRow, _damBonusButtons, ref _damBonusBuiltFor, "dam", _draft.DamCladeId, _draft.DamChosenAbility); } private void SyncLineageBonusRow(HBoxContainer row, Dictionary cache, ref string builtFor, string lineage, string cladeId, string chosen) { if (cladeId == builtFor) { foreach (var (ab, btn) in cache) { bool want = ab == chosen; if (btn.ButtonPressed != want) btn.SetPressedNoSignal(want); } return; } foreach (var c in row.GetChildren()) c.Free(); cache.Clear(); builtFor = cladeId; var clade = CodexContent.Clade(cladeId); if (clade is null) { row.AddChild(new Label { Text = "(pick a clade above)" }); return; } foreach (var (ab, value) in clade.AbilityMods) { string captured = ab; var btn = new Button { Text = $"{ab} {(value >= 0 ? "+" : "")}{value}", ToggleMode = true, ButtonPressed = ab == chosen, FocusMode = Control.FocusModeEnum.None, }; btn.Pressed += () => OnLineageBonusPicked(lineage, captured); row.AddChild(btn); cache[ab] = btn; } } private static void UpdateSelection(Dictionary cards, string selectedId) { foreach (var (id, card) in cards) CodexCard.SetSelected(card, id == selectedId); } private void OnPurebredCladePicked(string cladeId) { string speciesId = _draft.SpeciesId; var sp = CodexContent.SpeciesById(speciesId); if (sp is null || !string.Equals(sp.CladeId, cladeId, System.StringComparison.OrdinalIgnoreCase)) speciesId = ""; var patch = new Godot.Collections.Dictionary { { "clade_id", cladeId }, { "species_id", speciesId }, }; ClearBackgroundIfInvalidated(patch); _draft.Patch(patch); } /// /// Build the patch dict that records "'s /// clade is now " — clearing the dependent /// fields (species, lineage bonus, clade traits) so they don't carry /// stale picks from the prior clade. Returned without applying so the /// caller can merge multiple lineage patches into one atomic Patch. /// private Godot.Collections.Dictionary BuildLineageCladePatch(string lineage, string cladeId) { var patch = new Godot.Collections.Dictionary { { lineage + "_clade_id", cladeId }, }; string currentSpecies = lineage == "sire" ? _draft.SireSpeciesId : _draft.DamSpeciesId; var sp = CodexContent.SpeciesById(currentSpecies); if (sp is null || !string.Equals(sp.CladeId, cladeId, System.StringComparison.OrdinalIgnoreCase)) { patch[lineage + "_species_id"] = ""; patch[lineage + "_chosen_species_trait"] = ""; patch[lineage + "_chosen_species_detriment"] = ""; } patch[lineage + "_chosen_ability"] = ""; patch[lineage + "_chosen_clade_traits"] = new Godot.Collections.Array(); return patch; } /// /// User toggled Sire or Dam on a hybrid card. If the toggle was just /// turned ON, record the pick (and clear the OTHER lineage if it was /// already pointing at this same clade — a clade can't be both /// parents). If turned OFF, clear that lineage. /// private void OnHybridParentPressed(string lineage, string cladeId) { string currentForLineage = lineage == "sire" ? _draft.SireCladeId : _draft.DamCladeId; bool wasOn = currentForLineage == cladeId; string newCladeId = wasOn ? "" : cladeId; var patch = BuildLineageCladePatch(lineage, newCladeId); // If the new pick collides with the OTHER parent, clear it. if (!string.IsNullOrEmpty(newCladeId)) { string otherLineage = lineage == "sire" ? "dam" : "sire"; string otherCladeId = lineage == "sire" ? _draft.DamCladeId : _draft.SireCladeId; if (otherCladeId == newCladeId) { var otherPatch = BuildLineageCladePatch(otherLineage, ""); foreach (var key in otherPatch.Keys) patch[(string)key] = otherPatch[key]; } } ClearBackgroundIfInvalidated(patch); _draft.Patch(patch); } private void UpdateHybridCards() { foreach (var (id, card) in _hybridCards) { bool isSire = id == _draft.SireCladeId; bool isDam = id == _draft.DamCladeId; // The card itself gets a "selected" highlight when either // toggle is on, so it pops visually from the unmarked clades. CodexCard.SetSelected(card, isSire || isDam); if (_sireToggles.TryGetValue(id, out var sireBtn) && sireBtn.ButtonPressed != isSire) sireBtn.SetPressedNoSignal(isSire); if (_damToggles.TryGetValue(id, out var damBtn) && damBtn.ButtonPressed != isDam) damBtn.SetPressedNoSignal(isDam); } } private void OnLineageBonusPicked(string lineage, string ability) { _draft.Patch(new Godot.Collections.Dictionary { { lineage + "_chosen_ability", ability }, }); } /// /// Syncs both clade-trait columns. Header text reflects the current /// limit (2 for dominant, 1 for other). Mutate-in-place same as /// SyncLineageBonusRow — Free()-during-Pressed leaves stale buttons. /// private void RebuildTraitPickers() { SyncCladeTraitColumn(_sireTraitCol, _sireTraitHeader, _sireTraitButtons, ref _sireTraitsBuiltFor, "sire", _draft.SireCladeId, _draft.SireChosenCladeTraits, _draft.CladeTraitLimit("sire")); SyncCladeTraitColumn(_damTraitCol, _damTraitHeader, _damTraitButtons, ref _damTraitsBuiltFor, "dam", _draft.DamCladeId, _draft.DamChosenCladeTraits, _draft.CladeTraitLimit("dam")); } private void SyncCladeTraitColumn(VBoxContainer col, Label header, Dictionary cache, ref string builtFor, string lineage, string cladeId, Godot.Collections.Array chosen, int limit) { string headerLabel = lineage.ToUpperInvariant() + (lineage == _draft.DominantParent ? " ★" : ""); header.Text = $"{headerLabel} ({chosen.Count}/{limit})"; if (cladeId == builtFor) { foreach (var (id, btn) in cache) { bool want = chosen.Contains(id); if (btn.ButtonPressed != want) btn.SetPressedNoSignal(want); bool atCap = chosen.Count >= limit && !want; btn.Disabled = atCap; } return; } // Remove old buttons but keep the header (index 0). for (int i = col.GetChildCount() - 1; i >= 1; i--) col.GetChild(i).Free(); cache.Clear(); builtFor = cladeId; var clade = CodexContent.Clade(cladeId); if (clade is null) { col.AddChild(new Label { Text = "(pick a clade above)" }); return; } foreach (var t in clade.Traits) { string captured = t.Id; string capturedName = t.Name; string capturedDesc = t.Description; bool isPicked = chosen.Contains(t.Id); var btn = new Button { Text = t.Name, ToggleMode = true, ButtonPressed = isPicked, FocusMode = Control.FocusModeEnum.None, Disabled = !isPicked && chosen.Count >= limit, SizeFlagsHorizontal = SizeFlags.ExpandFill, }; btn.Toggled += pressed => OnCladeTraitToggled(lineage, captured, pressed); var btnRef = btn; btn.MouseEntered += () => PopoverLayer.Instance?.ShowFor(btnRef, capturedName, capturedDesc, "trait", false); btn.MouseExited += () => PopoverLayer.Instance?.ScheduleClose(); col.AddChild(btn); cache[t.Id] = btn; } } private void OnCladeTraitToggled(string lineage, string traitId, bool pressed) { var current = lineage == "sire" ? _draft.SireChosenCladeTraits : _draft.DamChosenCladeTraits; int limit = _draft.CladeTraitLimit(lineage); var next = new Godot.Collections.Array(current); if (pressed) { if (next.Contains(traitId)) return; if (next.Count >= limit) return; // cap (UI also disables, defense-in-depth) next.Add(traitId); } else { next.Remove(traitId); } _draft.Patch(new Godot.Collections.Dictionary { { lineage + "_chosen_clade_traits", next }, }); } /// /// Clade changes can make a previously-valid background unavailable /// (e.g. picking Felidae while Pack-Raised is selected). Build the /// hypothetical post-patch state via Resource.Duplicate, run it /// through BackgroundAvailability, and clear background_id in the /// patch if the rule no longer matches. /// private void ClearBackgroundIfInvalidated(Godot.Collections.Dictionary patch) { if (string.IsNullOrEmpty(_draft.BackgroundId)) return; var future = (CharacterDraft)_draft.Duplicate(); // Apply the same patch we're about to commit, in-place on the copy. future.Patch(patch); if (!BackgroundAvailability.IsAvailable(_draft.BackgroundId, future)) patch["background_id"] = ""; } private PanelContainer BuildCard(CladeDef clade, System.Action onClick) { var card = CodexCard.Make(); card.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill; card.GuiInput += (InputEvent e) => { if (e is InputEventMouseButton mb && mb.Pressed && mb.ButtonIndex == MouseButton.Left) onClick(clade.Id); }; var box = new VBoxContainer { MouseFilter = MouseFilterEnum.Pass }; box.AddThemeConstantOverride("separation", 6); card.AddChild(box); box.AddChild(new Label { Text = clade.Name, ThemeTypeVariation = "CardName" }); box.AddChild(new Label { Text = clade.Kind.ToUpperInvariant(), ThemeTypeVariation = "CardMeta" }); if (!string.IsNullOrEmpty(clade.Description)) { box.AddChild(new Label { Text = clade.Description, AutowrapMode = TextServer.AutowrapMode.WordSmart, MouseFilter = Control.MouseFilterEnum.Pass, }); } if (clade.AbilityMods.Count > 0) { var modsRow = new HBoxContainer(); modsRow.AddThemeConstantOverride("separation", 8); box.AddChild(modsRow); foreach (var (k, v) in clade.AbilityMods) modsRow.AddChild(new Label { Text = $"{k} {(v >= 0 ? "+" : "")}{v}" }); } 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) chips.AddChild(new TraitChip { TraitName = t.Name, Description = t.Description }); foreach (var d in clade.Detriments) chips.AddChild(new TraitChip { TraitName = d.Name, Description = d.Description, Detriment = true }); } return card; } /// /// Hybrid-mode clade card. Same body as BuildCard but with Sire and /// Dam toggle buttons stacked at the right edge of the title row, so a /// single grid replaces the old two-stacked-grid layout. Body click is /// disabled — only the toggles change selection. /// private PanelContainer BuildHybridCard(CladeDef clade) { var card = CodexCard.Make(); card.SizeFlagsHorizontal = Control.SizeFlags.ExpandFill; var box = new VBoxContainer { MouseFilter = MouseFilterEnum.Pass }; box.AddThemeConstantOverride("separation", 6); card.AddChild(box); // Header HBox: title VBox (expand fill) + Sire/Dam toggle column. var header = new HBoxContainer { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill }; header.AddThemeConstantOverride("separation", 12); box.AddChild(header); var titleCol = new VBoxContainer { SizeFlagsHorizontal = Control.SizeFlags.ExpandFill }; titleCol.AddThemeConstantOverride("separation", 2); header.AddChild(titleCol); titleCol.AddChild(new Label { Text = clade.Name, ThemeTypeVariation = "CardName" }); titleCol.AddChild(new Label { Text = clade.Kind.ToUpperInvariant(), ThemeTypeVariation = "CardMeta" }); var toggleCol = new HBoxContainer { SizeFlagsVertical = Control.SizeFlags.ShrinkBegin, }; toggleCol.AddThemeConstantOverride("separation", 6); header.AddChild(toggleCol); string capturedId = clade.Id; var sireBtn = new Button { Text = "Sire", ToggleMode = true, FocusMode = Control.FocusModeEnum.None, CustomMinimumSize = new Vector2(64, 0), }; sireBtn.Pressed += () => OnHybridParentPressed("sire", capturedId); toggleCol.AddChild(sireBtn); _sireToggles[clade.Id] = sireBtn; var damBtn = new Button { Text = "Dam", ToggleMode = true, FocusMode = Control.FocusModeEnum.None, CustomMinimumSize = new Vector2(64, 0), }; damBtn.Pressed += () => OnHybridParentPressed("dam", capturedId); toggleCol.AddChild(damBtn); _damToggles[clade.Id] = damBtn; if (!string.IsNullOrEmpty(clade.Description)) { box.AddChild(new Label { Text = clade.Description, AutowrapMode = TextServer.AutowrapMode.WordSmart, MouseFilter = Control.MouseFilterEnum.Pass, }); } if (clade.AbilityMods.Count > 0) { var modsRow = new HBoxContainer(); modsRow.AddThemeConstantOverride("separation", 8); box.AddChild(modsRow); foreach (var (k, v) in clade.AbilityMods) modsRow.AddChild(new Label { Text = $"{k} {(v >= 0 ? "+" : "")}{v}" }); } 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) chips.AddChild(new TraitChip { TraitName = t.Name, Description = t.Description }); foreach (var d in clade.Detriments) chips.AddChild(new TraitChip { TraitName = d.Name, Description = d.Description, Detriment = true }); } return card; } }