2026-05-03 20:51:55 -07:00
|
|
|
|
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" });
|
2026-05-03 20:51:55 -07:00
|
|
|
|
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" });
|
2026-05-03 20:51:55 -07:00
|
|
|
|
_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",
|
2026-05-03 20:51:55 -07:00
|
|
|
|
};
|
|
|
|
|
|
_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(),
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|