M6.15: Unified hybrid clade grid with Sire/Dam toggle headers
Replace the two stacked sire/dam clade grids in StepClade hybrid mode with one unified grid where each card carries Sire and Dam toggle buttons side-by-side in its header (right edge of the title row). A single clade can become either parent; picking Sire on a card currently set as Dam atomically clears the Dam pick (and vice versa). Card body click is disabled in hybrid mode — only the toggles change selection. Mechanics: BuildLineageCladePatch extracts the per-lineage clear logic from the old OnLineageCladePicked into a patch builder so OnHybridParentPressed can compose multi-lineage patches into one atomic Patch call (the cross-clear case applies both lineage updates in a single Changed signal). UpdateHybridCards walks the unified card dictionary and syncs both toggle states + the card's selected highlight. Drops the old _sireCards / _damCards dictionaries and the OnLineageCladePicked callback, since hybrid mode no longer routes through the per-grid click handlers. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -42,8 +42,12 @@ public partial class StepClade : VBoxContainer, IStep
|
||||
private Label _damTraitHeader = null!;
|
||||
|
||||
private readonly Dictionary<string, PanelContainer> _purebredCards = new();
|
||||
private readonly Dictionary<string, PanelContainer> _sireCards = new();
|
||||
private readonly Dictionary<string, PanelContainer> _damCards = new();
|
||||
private readonly Dictionary<string, PanelContainer> _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<string, Button> _sireToggles = new();
|
||||
private readonly Dictionary<string, Button> _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
|
||||
@@ -134,15 +138,24 @@ public partial class StepClade : VBoxContainer, IStep
|
||||
_hybridSection.AddThemeConstantOverride("separation", 16);
|
||||
AddChild(_hybridSection);
|
||||
|
||||
_hybridSection.AddChild(new Label { Text = "SIRE — Paternal Lineage", ThemeTypeVariation = "Eyebrow" });
|
||||
var sireGrid = MakeGrid();
|
||||
_hybridSection.AddChild(sireGrid);
|
||||
PopulateGrid(sireGrid, _sireCards, id => OnLineageCladePicked("sire", id));
|
||||
|
||||
_hybridSection.AddChild(new Label { Text = "DAM — Maternal Lineage", ThemeTypeVariation = "Eyebrow" });
|
||||
var damGrid = MakeGrid();
|
||||
_hybridSection.AddChild(damGrid);
|
||||
PopulateGrid(damGrid, _damCards, id => OnLineageCladePicked("dam", id));
|
||||
// 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.
|
||||
@@ -293,8 +306,7 @@ public partial class StepClade : VBoxContainer, IStep
|
||||
if (_sexFemaleBtn.ButtonPressed != isFemale) _sexFemaleBtn.SetPressedNoSignal(isFemale);
|
||||
|
||||
UpdateSelection(_purebredCards, _draft.CladeId);
|
||||
UpdateSelection(_sireCards, _draft.SireCladeId);
|
||||
UpdateSelection(_damCards, _draft.DamCladeId);
|
||||
UpdateHybridCards();
|
||||
|
||||
int dominantIdx = _draft.DominantParent == "dam" ? 1 : 0;
|
||||
if (_dominantToggle.Selected != dominantIdx) _dominantToggle.Select(dominantIdx);
|
||||
@@ -382,7 +394,14 @@ public partial class StepClade : VBoxContainer, IStep
|
||||
_draft.Patch(patch);
|
||||
}
|
||||
|
||||
private void OnLineageCladePicked(string lineage, string cladeId)
|
||||
/// <summary>
|
||||
/// Build the patch dict that records "<paramref name="lineage"/>'s
|
||||
/// clade is now <paramref name="cladeId"/>" — 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.
|
||||
/// </summary>
|
||||
private Godot.Collections.Dictionary BuildLineageCladePatch(string lineage, string cladeId)
|
||||
{
|
||||
var patch = new Godot.Collections.Dictionary
|
||||
{
|
||||
@@ -396,17 +415,57 @@ public partial class StepClade : VBoxContainer, IStep
|
||||
patch[lineage + "_chosen_species_trait"] = "";
|
||||
patch[lineage + "_chosen_species_detriment"] = "";
|
||||
}
|
||||
|
||||
// Clade swap invalidates the previously-picked lineage bonus
|
||||
// (different clade has different mod options) and the clade
|
||||
// trait picks.
|
||||
patch[lineage + "_chosen_ability"] = "";
|
||||
patch[lineage + "_chosen_clade_traits"] = new Godot.Collections.Array<string>();
|
||||
return patch;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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
|
||||
@@ -579,4 +638,94 @@ public partial class StepClade : VBoxContainer, IStep
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user