Files
TheriapolisV3/Theriapolis.Godot/Scenes/Steps/StepReview.cs
T
Christopher Wiebe bb986d49f9 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>
2026-05-03 20:51:55 -07:00

161 lines
5.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Godot;
using Theriapolis.GodotHost.UI;
namespace Theriapolis.GodotHost.Scenes.Steps;
/// <summary>
/// Step VIII — Sign. Direct port of <c>StepReview</c> in
/// <c>src/steps.jsx</c>: name entry plus the Confirm button that
/// composes the final character and emits the handoff signal per
/// GODOT_PORTING_GUIDE.md §11.
///
/// The full per-step review already lives in the Aside summary, so
/// this step focuses on what's new at this stage: the name input
/// and the Confirm action. Bottom of the page mirrors the React
/// prototype — single big primary button.
/// </summary>
public partial class StepReview : VBoxContainer, IStep
{
/// <summary>Fired when Confirm is pressed and every step validates.
/// The parent Wizard catches this and persists the draft + advances
/// the harness; the rest of the game-side handoff lands in a future
/// milestone (intro scene, world load, etc.).</summary>
[Signal] public delegate void CharacterConfirmedEventHandler(CharacterDraft draft);
private CharacterDraft _draft = null!;
private LineEdit _nameField = null!;
private Button _confirmBtn = null!;
private Label _confirmStatus = null!;
public void Bind(CharacterDraft draft)
{
_draft = draft;
_draft.Changed += () => Callable.From(Refresh).CallDeferred();
Build();
}
public string? Validate() => WizardValidation.Validate(7, _draft);
private void Build()
{
AddThemeConstantOverride("separation", 18);
var intro = new VBoxContainer();
intro.AddThemeConstantOverride("separation", 6);
AddChild(intro);
intro.AddChild(new Label { Text = "FOLIO VIII · SIGN" });
intro.AddChild(new Label { Text = "Sign the Codex" });
intro.AddChild(new Label
{
Text = "Review the right-rail summary, then sign your name. "
+ "The name you sign here is the one the world will speak.",
AutowrapMode = TextServer.AutowrapMode.WordSmart,
});
// Name field.
var nameBlock = new VBoxContainer();
nameBlock.AddThemeConstantOverride("separation", 6);
AddChild(nameBlock);
nameBlock.AddChild(new Label { Text = "NAME" });
_nameField = new LineEdit
{
PlaceholderText = "Enter your character's name...",
Text = _draft.CharacterName,
CustomMinimumSize = new Vector2(360, 0),
};
_nameField.TextChanged += OnNameChanged;
nameBlock.AddChild(_nameField);
// Confirm action — disabled until every step validates.
var actionBlock = new VBoxContainer();
actionBlock.AddThemeConstantOverride("separation", 8);
AddChild(actionBlock);
_confirmStatus = new Label
{
Text = "",
AutowrapMode = TextServer.AutowrapMode.WordSmart,
};
actionBlock.AddChild(_confirmStatus);
_confirmBtn = new Button
{
Text = "Confirm & Begin",
CustomMinimumSize = new Vector2(220, 0),
};
_confirmBtn.Pressed += OnConfirmPressed;
actionBlock.AddChild(_confirmBtn);
Refresh();
}
private void OnNameChanged(string newText)
{
// Patch only when the field actually differs from the draft —
// otherwise the Changed signal would refresh us in a loop.
if (newText == _draft.CharacterName) return;
_draft.Patch(new Godot.Collections.Dictionary { { "character_name", newText } });
}
private void Refresh()
{
if (_nameField is null) return;
// Sync the field if external state changed without going through
// the LineEdit (e.g. loading a saved draft someday).
if (_nameField.Text != _draft.CharacterName)
_nameField.Text = _draft.CharacterName;
int firstUnmet = WizardValidation.FirstIncomplete(_draft);
bool allValid = firstUnmet == -1;
_confirmBtn.Disabled = !allValid;
if (allValid)
{
_confirmStatus.Text = $"Ready: {_draft.CharacterName} stands at the threshold.";
}
else if (firstUnmet == 7)
{
_confirmStatus.Text = "Enter a name to sign.";
}
else
{
_confirmStatus.Text = $"Some folios remain — see Folio {Roman(firstUnmet + 1)}.";
}
}
private void OnConfirmPressed()
{
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}");
else
GD.Print($"[review] Saved character draft to {SavePath}");
GD.Print($"[review] Confirmed: {Summarise(_draft)}");
EmitSignal(SignalName.CharacterConfirmed, _draft);
_confirmBtn.Disabled = true;
_confirmStatus.Text = $"{_draft.CharacterName} steps into Theriapolis.";
}
private static string Summarise(CharacterDraft d)
{
string lineage = d.IsHybrid
? $"hybrid {d.SireCladeId}/{d.SireSpeciesId} × {d.DamCladeId}/{d.DamSpeciesId} (dom={d.DominantParent})"
: $"{d.CladeId}/{d.SpeciesId}";
return $"{d.CharacterName} — {lineage}, {d.ClassId}/{d.SubclassId}, bg={d.BackgroundId}, "
+ $"skills=[{string.Join(",", d.ChosenSkills)}]";
}
private static string Roman(int n) => n switch
{
1 => "I", 2 => "II", 3 => "III", 4 => "IV",
5 => "V", 6 => "VI", 7 => "VII", 8 => "VIII",
_ => n.ToString(),
};
}