M6.19: Confirm & Begin → CharacterBuilder handoff + hybrid pick plumbing

The wizard now produces a real runtime Character instead of just
persisting the draft Resource. New Theriapolis.Godot/UI/CharacterAssembler
bridges CharacterDraft → CharacterBuilder, picking the purebred Build
or hybrid TryBuildHybrid path, threading clade/species/class/background
lookups, ability scores, skill picks, dominant-parent, subclass id,
and the items table for the starting kit. The captured
PlayerCharacterState writes to user://character.json (resumability
before the M7 save format lands) and the live Character is held in
CharacterAssembler.LastBuilt for future PlayScreen pickup.

StepReview.OnConfirmPressed surfaces build errors on the status label
instead of crashing, logs HP/hybrid/skill totals on success, and
keeps emitting the existing CharacterConfirmed signal.

Hybrid lineage bonuses now match the wizard's preview math.
CharacterBuilder gains HybridSireChosenAbility / HybridDamChosenAbility;
TryBuildHybrid replaces the old "apply both clades' full mods + both
species mods" blend with "apply each parent's chosen mod only" —
species mods don't apply for hybrids per project decision. Picks
stack additively when both parents land on the same ability
(canidae +1 CON × ursidae +2 CON → +3 CON). Empty pick = no bonus
(defensive fallback for headless builds). HybridCharacterTests'
BlendsAbilityMods rewritten for the new rule, plus two new tests
for stacking and empty-pick fallback.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Christopher Wiebe
2026-05-09 21:26:16 -07:00
parent 97b49d4145
commit f7cadaeb68
4 changed files with 276 additions and 31 deletions
+47 -14
View File
@@ -93,27 +93,60 @@ public sealed class HybridCharacterTests
}
[Fact]
public void TryBuildHybrid_BlendsAbilityMods()
public void TryBuildHybrid_AppliesChosenAbilityFromEachParentClade()
{
// Wolf-Folk Sire:
// canidae clade: +1 CON, +1 WIS
// wolf species: +1 STR
// × Rabbit-Folk Dam:
// leporidae clade: -1 STR, +2 DEX
// rabbit species: +1 WIS
// Hybrid PCs take ONE ability mod from each parent clade — the
// wizard's StepClade picker records the choices, and the builder
// applies exactly those. Species mods don't apply for hybrids.
//
// Wolf-Folk Sire (canidae: +1 CON, +1 WIS) — sire picks CON.
// Rabbit-Folk Dam (leporidae: -1 STR, +2 DEX) — dam picks DEX.
// Base 10 across the board.
var b = MakeHybridBuilder("canidae", "wolf", "leporidae", "rabbit");
b.BaseAbilities = new AbilityScores(10, 10, 10, 10, 10, 10);
b.HybridSireChosenAbility = "CON";
b.HybridDamChosenAbility = "DEX";
bool ok = b.TryBuildHybrid(_content.Items, out var c, out _);
Assert.True(ok);
Assert.Equal(10, c!.Abilities.STR); // no -1 (dam didn't pick STR), no +1 (no species mods)
Assert.Equal(12, c.Abilities.DEX); // dam pick: +2
Assert.Equal(11, c.Abilities.CON); // sire pick: +1
Assert.Equal(10, c.Abilities.WIS); // no +1 (sire picked CON, not WIS)
}
[Fact]
public void TryBuildHybrid_StacksWhenBothParentsPickSameAbility()
{
// Canidae gives +1 CON, ursidae gives +2 CON. If both parents pick
// CON, both bonuses apply additively.
var b = NewBuilderWithClassAndSkills();
b.HybridSireClade = _content.Clades["canidae"];
b.HybridSireSpecies = _content.Species["wolf"];
b.HybridDamClade = _content.Clades["ursidae"];
b.HybridDamSpecies = _content.Species["brown_bear"];
b.BaseAbilities = new AbilityScores(10, 10, 10, 10, 10, 10);
b.HybridSireChosenAbility = "CON";
b.HybridDamChosenAbility = "CON";
bool ok = b.TryBuildHybrid(_content.Items, out var c, out _);
Assert.True(ok);
Assert.Equal(13, c!.Abilities.CON); // 10 + 1 (canid) + 2 (ursid)
}
[Fact]
public void TryBuildHybrid_NoBonusWhenPickIsEmpty()
{
// Defensive: empty pick = no bonus from that side. Headless tests
// and old saves leave the field blank; the builder should not
// silently fall back to the old "apply everything" rule.
var b = MakeHybridBuilder("canidae", "wolf", "leporidae", "rabbit");
b.BaseAbilities = new AbilityScores(10, 10, 10, 10, 10, 10);
// No sire or dam pick set.
bool ok = b.TryBuildHybrid(_content.Items, out var c, out _);
Assert.True(ok);
// Net STR: 10 + 1 (wolf) - 1 (leporid) = 10.
// Net DEX: 10 + 2 (leporid) = 12.
// Net CON: 10 + 1 (canid) = 11.
// Net WIS: 10 + 1 (canid) + 1 (rabbit) = 12.
Assert.Equal(10, c!.Abilities.STR);
Assert.Equal(12, c.Abilities.DEX);
Assert.Equal(11, c.Abilities.CON);
Assert.Equal(12, c.Abilities.WIS);
Assert.Equal(10, c.Abilities.DEX);
Assert.Equal(10, c.Abilities.CON);
Assert.Equal(10, c.Abilities.WIS);
}
// ── Cross-clade pairings smoke ────────────────────────────────────────