Files
TheriapolisV3/Theriapolis.Godot/Scenes/Steps/StepReview.cs
T

174 lines
6.5 KiB
C#
Raw Normal View History

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);
2026-05-03 22:04:24 -07:00
intro.AddChild(new Label { Text = "FOLIO VIII · SIGN", ThemeTypeVariation = "Eyebrow" });
intro.AddChild(new Label { Text = "Sign the Codex", ThemeTypeVariation = "H2" });
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);
2026-05-03 22:04:24 -07:00
nameBlock.AddChild(new Label { Text = "NAME", ThemeTypeVariation = "Eyebrow" });
_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),
2026-05-03 22:04:24 -07:00
ThemeTypeVariation = "PrimaryButton",
};
_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 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 {DraftPath}");
// 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);
_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(),
};
}