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 · M7.6"; 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 = !AnyCompatibleSaveExists(); 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); 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()); } /// M7.1 hand-off: snapshot the built character + chosen /// name onto , default the seed (a seed-entry /// UI lands later), and swap to . 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() { var parent = GetParent(); if (parent is null) return; foreach (Node sibling in parent.GetChildren()) if (sibling != this) sibling.QueueFree(); parent.AddChild(new SaveLoadScreen()); QueueFree(); } private void OnQuit() => GetTree().Quit(); /// True iff at least one slot under /// has a header that /// accepts. Cheap: /// reads only the JSON prefix, not the binary body. private static bool AnyCompatibleSaveExists() { try { string dir = Platform.SavePaths.SavesDir; if (!System.IO.Directory.Exists(dir)) return false; foreach (var path in System.IO.Directory.EnumerateFiles(dir, "*.trps")) { try { var bytes = System.IO.File.ReadAllBytes(path); var header = Theriapolis.Core.Persistence.SaveCodec.DeserializeHeaderOnly(bytes); if (Theriapolis.Core.Persistence.SaveCodec.IsCompatible(header)) return true; } catch { /* skip broken slot */ } } } catch { /* defensive */ } return false; } }