bf0041605f
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>
241 lines
9.3 KiB
C#
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(),
|
|
};
|
|
}
|