Files
TheriapolisV3/Theriapolis.Godot/Scenes/Steps/StepReview.cs
T
Christopher Wiebe e3f0296e6f M6.7: Parchment theme pass
Lights up the M5 codex design system across the wizard. Default
palette swaps from dark leather to aged-parchment cream with
sealing-wax red selection emphasis, matching the React prototype's
default theme variant. CodexTheme.Build() is applied at the wizard
root so every step + Aside + popover cascades through it.

Theme additions:
- Parchment palette in CodexPalette (Dark retained as alt)
- Type variations registered for Card, CodexPopover, Pill,
  PillDetriment, AbilityToken, AbilitySlot, SkillRow — without
  SetTypeVariation, panel-stylebox lookup falls through to Godot's
  default dark slate, which is what was happening to every bare
  PanelContainer before this pass.
- panel_hover stylebox on Card (gild border) wired via CodexCard's
  MouseEntered/Exited helper; panel_selected bumped to 3px seal-red
  border + soft shadow so selection reads at a glance.

Card selection refactor:
- Replaced the warm-cream Modulate hint on cards with stylebox swaps
  via the new CodexCard.SetSelected helper. The Modulate approach
  was a no-op on cream-on-cream parchment; the stylebox swap looks
  the same on either palette.
- Step intros + Aside section headers now use the existing Eyebrow /
  H2 / H3 / CardName / CardMeta / CardBody label variations.
- Confirm button on Step VIII uses the PrimaryButton variation.

Popover + chip behaviour:
- PopoverLayer is now MouseFilter=Ignore so clicks/scroll/hover all
  pass through. Adjacent chips fire reliably even when the previous
  popover overlaps them spatially.
- Dropped the 80ms grace timer; chip MouseExited closes immediately.
- TraitChip MouseFilter Stop → Pass so clicks bubble up to the
  parent card's GuiInput (selecting the card).

Misc:
- Wizard._Ready inserts a backing Panel so the parchment Bg fills
  the canvas — Wizard root is a plain Control, which paints nothing.
- CodexTheme font lookup tries Cormorant-Medium before -Regular and
  globalizes res://Fonts/ for runtime FontFile load (the previous
  fallback used ContentPaths which points at a sibling data tree).
- StepStats final-score Label rendered at font_size 22 to match the
  AbilityToken die.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-03 22:04:24 -07:00

162 lines
5.8 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", 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(),
};
}