Files
TheriapolisV3/Theriapolis.Godot/Scenes/TitleScreen.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

174 lines
6.9 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;
/// <summary>
/// Entry-point screen — vertical button stack on a parchment field with the
/// codex title and a version label. Per port-plan §M6, exists primarily to
/// validate the design system in a non-trivial composition before the player
/// reaches character creation.
///
/// Button actions:
/// New Character — swap self for the Wizard scene under the Main parent
/// (siblings cleared so the wizard fills the viewport).
/// Continue — disabled until <see cref="CharacterAssembler.PersistedStatePath"/>
/// exists; full pickup lands with the M7 play loop.
/// Quit — shut down the engine.
/// </summary>
public partial class TitleScreen : Control
{
private const string VersionLabel = "PORT / GODOT · M7.2";
private const string WizardScenePath = "res://Scenes/Wizard.tscn";
public override void _Ready()
{
SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
Theme = CodexTheme.Build();
// Backing panel so the parchment Bg fills the viewport (the Control
// itself paints nothing). Same pattern as Wizard.cs.
// Note: SetAnchorsAndOffsetsPreset is required (not just AnchorRight =
// 1) because Godot's anchor setters preserve visual position by
// adjusting offsets — manual anchor edits leave the control at 0×0.
var bg = new Panel { MouseFilter = MouseFilterEnum.Ignore };
AddChild(bg);
bg.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
MoveChild(bg, 0);
// Centered title + button stack column.
var center = new CenterContainer { MouseFilter = MouseFilterEnum.Ignore };
AddChild(center);
center.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
var col = new VBoxContainer { CustomMinimumSize = new Vector2(360, 0) };
col.AddThemeConstantOverride("separation", 28);
center.AddChild(col);
var titleBlock = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ShrinkCenter };
titleBlock.AddThemeConstantOverride("separation", 4);
col.AddChild(titleBlock);
titleBlock.AddChild(new Label
{
Text = "THERIAPOLIS",
ThemeTypeVariation = "CodexTitle",
HorizontalAlignment = HorizontalAlignment.Center,
});
titleBlock.AddChild(new Label
{
Text = "CODEX OF BECOMING",
ThemeTypeVariation = "Eyebrow",
HorizontalAlignment = HorizontalAlignment.Center,
});
var buttonStack = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
buttonStack.AddThemeConstantOverride("separation", 12);
col.AddChild(buttonStack);
var newBtn = MakeMenuButton("New Character", primary: true);
newBtn.Pressed += OnNewCharacter;
buttonStack.AddChild(newBtn);
var continueBtn = MakeMenuButton("Continue", primary: false);
continueBtn.Disabled = !FileAccess.FileExists(CharacterAssembler.PersistedStatePath);
continueBtn.Pressed += OnContinue;
buttonStack.AddChild(continueBtn);
var quitBtn = MakeMenuButton("Quit", primary: false);
quitBtn.Pressed += OnQuit;
buttonStack.AddChild(quitBtn);
// Version chip in the bottom-right corner — small mono Eyebrow tag,
// sits over the parchment field at a comfortable margin.
var versionLabel = new Label
{
Text = VersionLabel,
ThemeTypeVariation = "Eyebrow",
AnchorLeft = 1, AnchorRight = 1,
AnchorTop = 1, AnchorBottom = 1,
OffsetLeft = -180, OffsetTop = -28,
OffsetRight = -16, OffsetBottom = -10,
HorizontalAlignment = HorizontalAlignment.Right,
};
AddChild(versionLabel);
}
private static Button MakeMenuButton(string text, bool primary)
{
var btn = new Button
{
Text = text,
FocusMode = FocusModeEnum.None,
SizeFlagsHorizontal = SizeFlags.ExpandFill,
CustomMinimumSize = new Vector2(0, 44),
};
if (primary) btn.ThemeTypeVariation = "PrimaryButton";
return btn;
}
private void OnNewCharacter()
{
var packed = ResourceLoader.Load<PackedScene>(WizardScenePath);
if (packed is null)
{
GD.PushError($"[title] Failed to load {WizardScenePath}");
return;
}
var parent = GetParent();
if (parent is null) return;
// Clear siblings so the wizard fills the viewport, then swap in.
foreach (Node sibling in parent.GetChildren())
if (sibling != this) sibling.QueueFree();
var wizardNode = packed.Instantiate();
parent.AddChild(wizardNode);
if (wizardNode is Wizard wizard)
{
// "← Title" back-button (visible on step 0) emits BackToTitle.
wizard.BackToTitle += () => SwapBackToTitle(parent);
// M7.1 — Confirm & Begin in StepReview is forwarded by the
// wizard as CharacterConfirmed. Stash the built character on
// GameSession and hand off to WorldGenProgressScreen.
wizard.CharacterConfirmed += draft => SwapToWorldGen(parent, draft);
}
QueueFree();
}
private static void SwapBackToTitle(Node parent)
{
foreach (Node child in parent.GetChildren()) child.QueueFree();
parent.AddChild(new TitleScreen());
}
/// <summary>M7.1 hand-off: snapshot the built character + chosen
/// name onto <see cref="GameSession"/>, default the seed (a seed-entry
/// UI lands later), and swap to <see cref="WorldGenProgressScreen"/>.</summary>
private static void SwapToWorldGen(Node parent, UI.CharacterDraft draft)
{
var session = GameSession.From(parent);
// CharacterAssembler.LastBuilt is populated by StepReview's
// OnConfirmPressed → TryBuild call immediately before the
// CharacterConfirmed signal fires.
session.PendingCharacter = CharacterAssembler.LastBuilt;
session.PendingName = string.IsNullOrWhiteSpace(draft.CharacterName)
? "Wanderer"
: draft.CharacterName;
session.Seed = 12345UL; // default for M7; seed-entry UI is M8+.
session.PendingRestore = null;
session.PendingHeader = null;
foreach (Node child in parent.GetChildren()) child.QueueFree();
parent.AddChild(new WorldGenProgressScreen());
}
private void OnContinue()
{
// M7 territory — the play-loop screens that consume the persisted
// character don't exist yet. For now, surface a print so the click
// does something visible and the button isn't dead UI.
GD.Print($"[title] Continue: {CharacterAssembler.PersistedStatePath} exists. "
+ "Play-loop pickup lands with M7.");
}
private void OnQuit() => GetTree().Quit();
}