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

127 lines
4.4 KiB
C#

using Godot;
using Theriapolis.GodotHost.UI;
namespace Theriapolis.GodotHost.Scenes;
/// <summary>
/// M7.1 placeholder for the play screen. WorldGenProgressScreen swaps
/// here on success; M7.2 will replace this with the real PlayScreen
/// (walking character, chunk-streamed tactical view, HUD, save layer).
///
/// Reads <see cref="GameSession.Ctx"/> and <see cref="GameSession.PendingCharacter"/>
/// so the play-test confirms the M7.1 hand-off chain end-to-end:
/// Title → Wizard → CharacterAssembler → WorldGenProgress → here.
/// </summary>
public partial class PlayScreenStub : Control
{
public override void _Ready()
{
Theme = CodexTheme.Build();
SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
var bg = new Panel { MouseFilter = MouseFilterEnum.Ignore };
AddChild(bg);
bg.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
MoveChild(bg, 0);
var center = new CenterContainer { MouseFilter = MouseFilterEnum.Ignore };
AddChild(center);
center.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
var col = new VBoxContainer { CustomMinimumSize = new Vector2(640, 0) };
col.AddThemeConstantOverride("separation", 14);
center.AddChild(col);
var session = GameSession.From(this);
col.AddChild(new Label
{
Text = "PLAYSCREEN STUB · M7.1",
ThemeTypeVariation = "Eyebrow",
HorizontalAlignment = HorizontalAlignment.Center,
});
col.AddChild(new Label
{
Text = "World generation complete.",
ThemeTypeVariation = "H2",
HorizontalAlignment = HorizontalAlignment.Center,
});
var ctx = session.Ctx;
if (ctx is not null)
{
var w = ctx.World;
col.AddChild(new Label
{
Text = $"Seed 0x{w.WorldSeed:X} · rivers {w.Rivers.Count} "
+ $"roads {w.Roads.Count} rails {w.Rails.Count} "
+ $"settlements {w.Settlements.Count} bridges {w.Bridges.Count}",
HorizontalAlignment = HorizontalAlignment.Center,
AutowrapMode = TextServer.AutowrapMode.WordSmart,
});
}
else
{
col.AddChild(new Label
{
Text = "(No WorldGenContext on session — this stub was entered out-of-band.)",
HorizontalAlignment = HorizontalAlignment.Center,
ThemeTypeVariation = "Eyebrow",
});
}
var character = session.PendingCharacter;
if (character is not null)
{
string hybridTag = character.Hybrid is not null ? "yes" : "no";
col.AddChild(new Label
{
Text = $"Character: {session.PendingName} · HP {character.MaxHp} "
+ $"· class {character.ClassDef.Id} · hybrid: {hybridTag} "
+ $"· skills: {character.SkillProficiencies.Count}",
HorizontalAlignment = HorizontalAlignment.Center,
AutowrapMode = TextServer.AutowrapMode.WordSmart,
});
}
else
{
col.AddChild(new Label
{
Text = "(No character attached — load path will fill this in once M7.3 ships.)",
HorizontalAlignment = HorizontalAlignment.Center,
ThemeTypeVariation = "Eyebrow",
});
}
col.AddChild(new Label
{
Text = "PlayScreen with walking character + chunk-streamed tactical view lands in M7.2.",
HorizontalAlignment = HorizontalAlignment.Center,
AutowrapMode = TextServer.AutowrapMode.WordSmart,
});
var titleBtn = new Button
{
Text = "← Title",
CustomMinimumSize = new Vector2(220, 44),
SizeFlagsHorizontal = SizeFlags.ShrinkCenter,
};
titleBtn.Pressed += BackToTitle;
col.AddChild(titleBtn);
}
private void BackToTitle()
{
var session = GameSession.From(this);
session.ClearPending();
session.Ctx = null;
var parent = GetParent();
if (parent is null) return;
foreach (Node sibling in parent.GetChildren())
if (sibling != this) sibling.QueueFree();
parent.AddChild(new TitleScreen());
QueueFree();
}
}