using Godot; using Theriapolis.GodotHost.UI; namespace Theriapolis.GodotHost.Scenes.Steps; /// /// Step VIII — Sign. Direct port of StepReview in /// src/steps.jsx: 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. /// public partial class StepReview : VBoxContainer, IStep { /// 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.). [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", 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); 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), 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 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(), }; }