Files
TheriapolisV3/Theriapolis.Godot/Scenes/Wizard.cs
T
Christopher Wiebe bf0041605f M7.1-7.2: Play-loop hand-off — Wizard → WorldGen → PlayScreen
Lands the M7 plan's first two sub-milestones on port/godot.
theriapolis-rpg-implementation-plan-godot-port-m7.md is the design
doc (six screens collapse to four scenes + a camera mode, with
per-screen behavioural contracts and a six-step sub-milestone
breakdown).

M7.1 — WorldGenProgressScreen + GameSession autoload + wizard
hand-off rewrite. GameSession holds the cross-scene state that
outlives any single screen: seed, post-worldgen Ctx, pending
character (from the M6 wizard) and pending save snapshot (for
M7.3's load path). Wizard forwards StepReview.CharacterConfirmed
upward, and TitleScreen swaps to the progress screen instead of
just printing the build summary. The progress screen runs the
23-stage pipeline on a background thread, drives a ProgressBar
from ctx.ProgressCallback, and writes the full exception trace to
user://worldgen_error.log on failure. Escape cancels at the next
stage boundary and returns to title.

M7.2 — PlayScreen with a walking character. Extracted
WorldRenderNode from the M2+M4 WorldView demo so PlayScreen and
WorldView mount the same renderer (biome image + polylines +
bridges + settlement dots + tactical chunk lifecycle + PanZoomCamera
+ per-frame layer visibility + line-width counter-scaling).
PlayScreen owns the streamer (M7.3 save needs it), composes
ContentResolver + ActorManager + WorldClock + AnchorRegistry +
PlayerController, spawns the player at the Tier-1 anchor, and
wires resident + non-resident NPC spawning from chunk-load events
with allegiance-tinted markers.

PlayerController ported engine-agnostic to Theriapolis.Godot/Input/.
Takes pre-resolved dx/dy/dt/isTactical/isFocused instead of poking
MonoGame InputManager + Camera2D, so the arithmetic that advances
PlayerActor.Position and WorldClock.InGameSeconds is bit-identical
to the MonoGame version — saves round-trip cleanly.

Click-to-travel in world-map mode (camera zoom <
TacticalRenderZoomMin), WASD step in tactical mode with axis-
separated motion + encumbrance + sub-second clock carry. HUD
overlay top-left shows HP/AC/seed/tile/biome/view-mode/time. Esc
returns to title (M7.4 replaces this with a pause menu).

Namespace gotcha: Theriapolis.GodotHost.Input shadows the engine's
Godot.Input static class for any file under the GodotHost
namespace tree. Files needing keyboard polls (WorldView,
PlayScreen) fully qualify as Godot.Input.IsKeyPressed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 18:07:28 -07:00

241 lines
9.3 KiB
C#

using Godot;
namespace Theriapolis.GodotHost.Scenes;
/// <summary>
/// Character creation wizard shell. Mirrors <c>src/app.jsx</c> per
/// GODOT_PORTING_GUIDE.md §4: header + stepper + page (step + aside) +
/// nav bar. Owns the <see cref="CharacterDraft"/> resource and dispatches
/// each step's content into the StepHost.
///
/// The codex Theme is applied at this root in <see cref="_Ready"/> and
/// cascades through every descendant — steps, Aside, popover layer.
/// </summary>
public partial class Wizard : Control
{
[Signal] public delegate void BackToTitleEventHandler();
/// <summary>Forwarded from <c>StepReview.CharacterConfirmed</c> so
/// the wizard's owner (TitleScreen / Main) can hand off to the
/// WorldGenProgressScreen without reaching into the step tree.</summary>
[Signal] public delegate void CharacterConfirmedEventHandler(UI.CharacterDraft draft);
private static readonly string[] StepKeys =
{ "clade", "species", "class", "subclass", "background", "stats", "skills", "review" };
private static readonly string[] StepNames =
{ "Clade", "Species", "Calling", "Subclass", "History", "Abilities", "Skills", "Sign" };
public UI.CharacterDraft Character { get; private set; } = null!;
private UI.Widgets.CodexStepper _stepper = null!;
private Control _stepHost = null!;
private Label _folioLabel = null!;
private Label _validation = null!;
private Label _navProgress = null!;
private Button _backBtn = null!;
private Button _nextBtn = null!;
// Scroll preservation: snapshot scroll position when the draft changes
// (which fires before the active step's Refresh tears down + rebuilds
// child nodes), then restore on the next _Process frame so the user
// doesn't get punted to the top of the page when selecting a card.
private ScrollContainer? _scroll;
private int _savedScroll = -1;
private bool _scrollPending;
private int _step;
private Steps.IStep? _activeStep;
private static readonly System.Type?[] StepTypes =
{
typeof(Steps.StepClade), // 0 Clade
typeof(Steps.StepSpecies), // 1 Species
typeof(Steps.StepClass), // 2 Calling
typeof(Steps.StepSubclass), // 3 Subclass
typeof(Steps.StepBackground), // 4 History
typeof(Steps.StepStats), // 5 Abilities
typeof(Steps.StepSkills), // 6 Skills
typeof(Steps.StepReview), // 7 Sign
};
public override void _Ready()
{
Theme = UI.CodexTheme.Build();
// The wizard root is a Control, which paints nothing — without a
// backing Panel the viewport's default grey clear color shows
// through. Insert a Panel sized to the full rect so the theme's
// parchment Bg fills the canvas, then move it behind the existing
// children so it doesn't intercept mouse events.
var bg = new Panel
{
AnchorRight = 1,
AnchorBottom = 1,
MouseFilter = MouseFilterEnum.Ignore,
};
AddChild(bg);
MoveChild(bg, 0);
Character = new UI.CharacterDraft();
_stepper = GetNode<UI.Widgets.CodexStepper>("%Stepper");
_stepHost = GetNode<Control>("%StepHost");
_folioLabel = GetNode<Label>("%FolioLabel");
_validation = GetNode<Label>("%ValidationLabel");
_navProgress = GetNode<Label>("%NavProgress");
_backBtn = GetNode<Button>("%BackButton");
_nextBtn = GetNode<Button>("%NextButton");
_scroll = GetNode<ScrollContainer>("%Scroll");
var aside = GetNode<Aside>("%Aside");
aside.SetDraft(Character);
_stepper.StepClicked += OnStepperClicked;
_backBtn.Pressed += OnBackPressed;
_nextBtn.Pressed += OnNextPressed;
Character.Changed += UpdateChrome;
SwitchToStep(0);
}
// ──────────────────────────────────────────────────────────────────────
// Step lifecycle
private void SwitchToStep(int index)
{
if (index < 0 || index >= StepKeys.Length) return;
_step = index;
foreach (var c in _stepHost.GetChildren()) c.QueueFree();
_activeStep = null;
var t = StepTypes[index];
if (t is null)
{
_stepHost.AddChild(new Label
{
Text = $"{StepNames[index]} step — coming soon.",
});
}
else
{
var instance = (Steps.IStep)System.Activator.CreateInstance(t)!;
_activeStep = instance;
instance.Bind(Character);
_stepHost.AddChild((Control)instance);
// Forward the final-step confirmation upward so TitleScreen
// (or whatever shell owns the wizard) can swap to M7.1's
// WorldGenProgressScreen without coupling to the step tree.
if (instance is Steps.StepReview review)
review.CharacterConfirmed += draft =>
EmitSignal(SignalName.CharacterConfirmed, draft);
}
UpdateChrome();
}
private void OnStepperClicked(int index)
{
if (index <= _step) { SwitchToStep(index); return; }
// Forward jump requires every step in [0..index-1] satisfied — not
// just the current step. Otherwise picking a clade would let you
// skip straight to Abilities without picking species/calling/etc.
for (int i = 0; i < index; i++)
if (UI.WizardValidation.Validate(i, Character) is not null) return;
SwitchToStep(index);
}
private void OnBackPressed()
{
if (_step == 0) { EmitSignal(SignalName.BackToTitle); return; }
SwitchToStep(_step - 1);
}
private void OnNextPressed()
{
if (_step < StepKeys.Length - 1) SwitchToStep(_step + 1);
}
// ──────────────────────────────────────────────────────────────────────
// Chrome (header, stepper, nav-bar) refresh
private void UpdateChrome()
{
// Snapshot scroll BEFORE the active step's Refresh handler fires
// (it's the next subscriber in the Changed chain). The scroll
// position then survives the rebuild via _Process below.
if (_scroll is not null)
{
_savedScroll = _scroll.ScrollVertical;
if (_savedScroll > 0) _scrollPending = true;
}
_folioLabel.Text = $"Folio {Roman(_step + 1)} of VIII — {StepNames[_step]}";
// Validate via the static helper so stepper-state propagation and
// the active-step banner share one source of truth.
string? err = UI.WizardValidation.Validate(_step, Character);
bool valid = err is null;
_validation.Text = err ?? (_step == StepKeys.Length - 1 ? "Ready to sign" : "Folio complete");
_nextBtn.Disabled = !valid;
_nextBtn.Visible = _step < StepKeys.Length - 1;
_backBtn.Text = _step == 0 ? "← Title" : "← Back";
_navProgress.Text = $"{_step + 1} / {StepKeys.Length}";
RebuildStepperStates();
}
private void RebuildStepperStates()
{
// Mirrors app.jsx's lock semantics exactly: a step is Locked iff
// some EARLIER step's validator fails. Use FirstIncomplete to find
// the boundary, then state each step accordingly. This is what
// makes "pick a clade and skip straight to Abilities" impossible —
// any step after FirstIncomplete is Locked.
int firstIncomplete = UI.WizardValidation.FirstIncomplete(Character, StepNames.Length);
var states = new UI.Widgets.CodexStepper.StepState[StepNames.Length];
for (int i = 0; i < StepNames.Length; i++)
{
if (i == _step)
{
states[i] = UI.Widgets.CodexStepper.StepState.Active;
}
else if (i < _step)
{
// Already-visited step. Complete if it still validates,
// Locked if the user has since invalidated it (e.g. cleared
// a field). Locked variants past _step also show.
states[i] = UI.WizardValidation.Validate(i, Character) is null
? UI.Widgets.CodexStepper.StepState.Complete
: UI.Widgets.CodexStepper.StepState.Locked;
}
else
{
// Future step. Locked iff some earlier step is incomplete.
bool locked = firstIncomplete != -1 && firstIncomplete < i;
states[i] = locked
? UI.Widgets.CodexStepper.StepState.Locked
: UI.Widgets.CodexStepper.StepState.Pending;
}
}
_stepper.SetSteps(StepNames, states);
}
public override void _Process(double delta)
{
if (_scrollPending && _scroll is not null && IsInstanceValid(_scroll))
{
_scroll.ScrollVertical = _savedScroll;
}
_scrollPending = false;
}
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(),
};
}