M6.6: StepReview signing + hybrid math revision
- New Step VIII (Review): name input and Confirm button that saves the finalized character to user://character.tres. - Hybrid lineage rules simplified per project decision: drop the "no-stack on overlap, take +1 free elsewhere" rule from theriapolis-rpg-clades.md. Hybrids now pick one ability mod from each parent clade and they sum if they overlap. Removes HybridFreeAbility, the free-bonus picker row, and the overlap special case from AbilityCalc + WizardValidation. - StepClade bonus rows now mutate in place (sync ButtonPressed) instead of tearing down on every Refresh — the old path freed the very button mid-Pressed-signal, leaving stale buttons next to the new ones. - StepSkills drops the redundant "Calling: X · History: Y" meta line; both are already shown in the Aside summary. - Aside hybrid section adds dual-species traits and the universal-hybrid detriment pills. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -26,10 +26,25 @@ public partial class StepClade : VBoxContainer, IStep
|
||||
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!;
|
||||
|
||||
private readonly Dictionary<string, PanelContainer> _purebredCards = new();
|
||||
private readonly Dictionary<string, PanelContainer> _sireCards = new();
|
||||
private readonly Dictionary<string, PanelContainer> _damCards = 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<string, Button> _sireBonusButtons = new();
|
||||
private readonly Dictionary<string, Button> _damBonusButtons = new();
|
||||
private string _sireBonusBuiltFor = "";
|
||||
private string _damBonusBuiltFor = "";
|
||||
|
||||
public void Bind(CharacterDraft draft)
|
||||
{
|
||||
_draft = draft;
|
||||
@@ -86,6 +101,21 @@ public partial class StepClade : VBoxContainer, IStep
|
||||
_hybridSection.AddChild(damGrid);
|
||||
PopulateGrid(damGrid, _damCards, id => OnLineageCladePicked("dam", id));
|
||||
|
||||
// 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" });
|
||||
|
||||
_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);
|
||||
@@ -131,6 +161,8 @@ public partial class StepClade : VBoxContainer, IStep
|
||||
patch["sire_species_id"] = "";
|
||||
patch["dam_clade_id"] = "";
|
||||
patch["dam_species_id"] = "";
|
||||
patch["sire_chosen_ability"] = "";
|
||||
patch["dam_chosen_ability"] = "";
|
||||
}
|
||||
ClearBackgroundIfInvalidated(patch);
|
||||
_draft.Patch(patch);
|
||||
@@ -159,6 +191,63 @@ public partial class StepClade : VBoxContainer, IStep
|
||||
|
||||
int dominantIdx = _draft.DominantParent == "dam" ? 1 : 0;
|
||||
if (_dominantToggle.Selected != dominantIdx) _dominantToggle.Select(dominantIdx);
|
||||
|
||||
if (hybrid) RebuildBonusPickers();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<string, Button> 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<string, PanelContainer> cards, string selectedId)
|
||||
@@ -192,10 +281,23 @@ public partial class StepClade : VBoxContainer, IStep
|
||||
var sp = CodexContent.SpeciesById(currentSpecies);
|
||||
if (sp is null || !string.Equals(sp.CladeId, cladeId, System.StringComparison.OrdinalIgnoreCase))
|
||||
patch[lineage + "_species_id"] = "";
|
||||
|
||||
// Clade swap invalidates the previously-picked lineage bonus
|
||||
// (different clade has different mod options).
|
||||
patch[lineage + "_chosen_ability"] = "";
|
||||
|
||||
ClearBackgroundIfInvalidated(patch);
|
||||
_draft.Patch(patch);
|
||||
}
|
||||
|
||||
private void OnLineageBonusPicked(string lineage, string ability)
|
||||
{
|
||||
_draft.Patch(new Godot.Collections.Dictionary
|
||||
{
|
||||
{ lineage + "_chosen_ability", ability },
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clade changes can make a previously-valid background unavailable
|
||||
/// (e.g. picking Felidae while Pack-Raised is selected). Build the
|
||||
|
||||
Reference in New Issue
Block a user