diff --git a/Theriapolis.Godot/Main.cs b/Theriapolis.Godot/Main.cs
index da97fff..ff9f30f 100644
--- a/Theriapolis.Godot/Main.cs
+++ b/Theriapolis.Godot/Main.cs
@@ -1,6 +1,7 @@
using Godot;
using Theriapolis.GodotHost.Platform;
using Theriapolis.GodotHost.Rendering;
+using Theriapolis.GodotHost.Scenes;
using Theriapolis.GodotHost.UI;
namespace Theriapolis.GodotHost;
@@ -128,7 +129,12 @@ public partial class Main : Control
return;
}
- GD.Print("Theriapolis.Godot host ready (M0 hello-world).");
+ // Default entry point — TitleScreen. M0's hello-world Label is no
+ // longer the boot UI; the title swaps itself for the wizard when
+ // "New Character" is clicked, or shuts the engine down on Quit.
+ foreach (Node child in GetChildren())
+ child.QueueFree();
+ AddChild(new TitleScreen());
}
public override void _UnhandledInput(InputEvent @event)
diff --git a/Theriapolis.Godot/Main.tscn b/Theriapolis.Godot/Main.tscn
index 63067b1..39db321 100644
--- a/Theriapolis.Godot/Main.tscn
+++ b/Theriapolis.Godot/Main.tscn
@@ -7,17 +7,3 @@ anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource("1_main")
-
-[node name="Label" type="Label" parent="."]
-anchors_preset = 8
-anchor_left = 0.5
-anchor_top = 0.5
-anchor_right = 0.5
-anchor_bottom = 0.5
-offset_left = -200.0
-offset_top = -16.0
-offset_right = 200.0
-offset_bottom = 16.0
-horizontal_alignment = 1
-vertical_alignment = 1
-text = "Theriapolis · Godot port · M0 · F11 toggles fullscreen"
diff --git a/Theriapolis.Godot/Scenes/TitleScreen.cs b/Theriapolis.Godot/Scenes/TitleScreen.cs
new file mode 100644
index 0000000..48c46d8
--- /dev/null
+++ b/Theriapolis.Godot/Scenes/TitleScreen.cs
@@ -0,0 +1,147 @@
+using Godot;
+using Theriapolis.GodotHost.UI;
+
+namespace Theriapolis.GodotHost.Scenes;
+
+///
+/// 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
+/// exists; full pickup lands with the M7 play loop.
+/// Quit — shut down the engine.
+///
+public partial class TitleScreen : Control
+{
+ private const string VersionLabel = "PORT / GODOT · M6.20";
+ 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(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);
+ // The wizard's "← Title" back-button (visible on step 0) emits
+ // BackToTitle; reinstate this title screen when that fires.
+ if (wizardNode is Wizard wizard)
+ wizard.BackToTitle += () => SwapBackToTitle(parent);
+ QueueFree();
+ }
+
+ private static void SwapBackToTitle(Node parent)
+ {
+ foreach (Node child in parent.GetChildren()) child.QueueFree();
+ parent.AddChild(new TitleScreen());
+ }
+
+ 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();
+}