M6.8: Hybrid trait pickers (Phase B)

Per theriapolis-rpg-clades.md "Building a Hybrid": hybrids now pick
two clade traits from the dominant parent + one from the other (2/1
split keyed off DominantParent), and one species trait + one species
detriment from each parent. All clade detriments still inherit fully
from both parents. Universal hybrid detriments unchanged.

CharacterDraft gains six new fields (sire/dam clade-trait arrays,
sire/dam species trait/detriment ids) and a CladeTraitLimit(lineage)
helper. Step 0/1 validators enforce the picks; Aside renders only the
chosen subset for hybrids.

Cascading clears: clade swap clears that lineage's bonus + clade
traits + (if species also invalidated) species pick; species swap
clears that lineage's species trait/detriment; dominant flip trims
overflow from the end (non-destructive when possible); hybrid-off
clears all six new fields.

Toggle buttons in both steps wire MouseEntered/Exited into
PopoverLayer so the player can read each trait's description on
hover (detriment buttons get the red-tinted "DETRIMENT" popover).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Christopher Wiebe
2026-05-04 20:12:56 -07:00
parent e3f0296e6f
commit 8bf9eba2a7
5 changed files with 432 additions and 15 deletions
+45 -8
View File
@@ -249,24 +249,30 @@ public partial class Aside : MarginContainer
flow.AddThemeConstantOverride("v_separation", 6);
_content.AddChild(flow);
// Clade traits (purebred = single clade; hybrid = both).
// Clade — purebred: full trait+detriment list of the one clade.
// Hybrid: only the player-picked clade traits per parent (2/1 split),
// but ALL clade detriments from BOTH parents per doc rule.
if (_draft!.IsHybrid)
{
AddCladeTraits(flow, CodexContent.Clade(_draft.SireCladeId));
AddCladeTraits(flow, CodexContent.Clade(_draft.DamCladeId));
AddPickedCladeTraits(flow, CodexContent.Clade(_draft.SireCladeId), _draft.SireChosenCladeTraits);
AddPickedCladeTraits(flow, CodexContent.Clade(_draft.DamCladeId), _draft.DamChosenCladeTraits);
AddCladeDetriments(flow, CodexContent.Clade(_draft.SireCladeId));
AddCladeDetriments(flow, CodexContent.Clade(_draft.DamCladeId));
}
else
{
AddCladeTraits(flow, CodexContent.Clade(_draft.CladeId));
}
// Species traits — purebred uses the single species; hybrids show
// BOTH parent species per theriapolis-rpg-clades.md ("Choose ONE
// species trait from each parent").
// Species — purebred shows everything from the single species.
// Hybrid shows only the picked trait + picked detriment per parent
// (single-pick each, per doc) plus the four universal detriments.
if (_draft.IsHybrid)
{
AddSpeciesTraits(flow, CodexContent.SpeciesById(_draft.SireSpeciesId));
AddSpeciesTraits(flow, CodexContent.SpeciesById(_draft.DamSpeciesId));
AddPickedSpeciesPick(flow, CodexContent.SpeciesById(_draft.SireSpeciesId),
_draft.SireChosenSpeciesTrait, _draft.SireChosenSpeciesDetriment);
AddPickedSpeciesPick(flow, CodexContent.SpeciesById(_draft.DamSpeciesId),
_draft.DamChosenSpeciesTrait, _draft.DamChosenSpeciesDetriment);
// Universal hybrid detriments — every hybrid has all four.
foreach (var (name, desc) in UniversalHybridDetriments)
@@ -338,6 +344,37 @@ public partial class Aside : MarginContainer
flow.AddChild(new TraitChip { TraitName = d.Name, Description = d.Description, Detriment = true });
}
/// <summary>Hybrid: only the chosen subset of clade traits.</summary>
private static void AddPickedCladeTraits(HFlowContainer flow, Theriapolis.Core.Data.CladeDef? clade,
Godot.Collections.Array<string> chosen)
{
if (clade is null) return;
foreach (var t in clade.Traits)
if (chosen.Contains(t.Id))
flow.AddChild(new TraitChip { TraitName = t.Name, Description = t.Description });
}
/// <summary>Hybrid: all clade detriments from this parent (both inherited).</summary>
private static void AddCladeDetriments(HFlowContainer flow, Theriapolis.Core.Data.CladeDef? clade)
{
if (clade is null) return;
foreach (var d in clade.Detriments)
flow.AddChild(new TraitChip { TraitName = d.Name, Description = d.Description, Detriment = true });
}
/// <summary>Hybrid: one chosen species trait + one chosen species detriment.</summary>
private static void AddPickedSpeciesPick(HFlowContainer flow, Theriapolis.Core.Data.SpeciesDef? species,
string chosenTraitId, string chosenDetrimentId)
{
if (species is null) return;
foreach (var t in species.Traits)
if (t.Id == chosenTraitId)
flow.AddChild(new TraitChip { TraitName = t.Name, Description = t.Description });
foreach (var d in species.Detriments)
if (d.Id == chosenDetrimentId)
flow.AddChild(new TraitChip { TraitName = d.Name, Description = d.Description, Detriment = true });
}
/// <summary>
/// Hardcoded from theriapolis-rpg-clades.md "Universal Hybrid Detriments"
/// (lines 749-753). Every hybrid character has all four. The schema