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
+19 -7
View File
@@ -127,15 +127,27 @@ public partial class StepReview : VBoxContainer, IStep
{
if (WizardValidation.FirstIncomplete(_draft) != -1) return;
// Persist the draft so a future load path can pick it up.
const string SavePath = "user://character.tres";
var err = ResourceSaver.Save(_draft, SavePath);
if (err != Error.Ok)
GD.PushWarning($"[review] ResourceSaver.Save failed: {err}");
// Persist the draft so a future load path can resume editing.
const string DraftPath = "user://character.tres";
var saveErr = ResourceSaver.Save(_draft, DraftPath);
if (saveErr != Error.Ok)
GD.PushWarning($"[review] ResourceSaver.Save failed: {saveErr}");
else
GD.Print($"[review] Saved character draft to {SavePath}");
GD.Print($"[review] Saved character draft to {DraftPath}");
GD.Print($"[review] Confirmed: {Summarise(_draft)}");
// The actual handoff: build the runtime Character via Core's
// CharacterBuilder. Failure here is a content/wiring bug, not a
// user error — the wizard's validation should have caught everything
// the builder rejects. Surface the message and stay on this step.
if (!CharacterAssembler.TryBuild(_draft, out var built, out string buildError))
{
GD.PushError($"[review] Character build failed: {buildError}");
_confirmStatus.Text = $"Could not finalize character — {buildError}";
return;
}
GD.Print($"[review] Confirmed: {Summarise(_draft)} → HP={built!.MaxHp}, "
+ $"hybrid={built.Hybrid is not null}, skills={built.SkillProficiencies.Count}");
EmitSignal(SignalName.CharacterConfirmed, _draft);