Files
TheriapolisV3/Theriapolis.Game/Screens/PauseMenuScreen.cs
T
Christopher Wiebe b451f83174 Initial commit: Theriapolis baseline at port/godot branch point
Captures the pre-Godot-port state of the codebase. This is the rollback
anchor for the Godot port (M0 of theriapolis-rpg-implementation-plan-godot-port.md).
All Phase 0 through Phase 6.5 work is included; Phase 7 is in flight.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:40:51 -07:00

196 lines
7.2 KiB
C#

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Myra.Graphics2D;
using Myra.Graphics2D.Brushes;
using Myra.Graphics2D.UI;
using Theriapolis.Core;
using Theriapolis.Game.Platform;
namespace Theriapolis.Game.Screens;
/// <summary>
/// Phase 5 M2 pause menu. Pushed by <see cref="PlayScreen"/> when the player
/// presses ESC during play. Provides Save Game (any slot, any time —
/// "save-anywhere" per the Phase 5 plan), Resume, and Quit to Title.
///
/// Cut-scene exclusion is forward-compat: Phase 5 has no cut scenes, so
/// save-anywhere is unconditional. When cut scenes arrive (Phase 6+), the
/// caller can suppress this push or grey out the Save Game button.
/// </summary>
public sealed class PauseMenuScreen : IScreen
{
private readonly PlayScreen _playScreen;
private Game1 _game = null!;
private Desktop _desktop = null!;
private Label? _statusLabel;
private bool _showingSlots;
// ESC was already down when PauseMenu was pushed (the same press that
// opened it). Wait for release before treating ESC as "close the menu".
private bool _escWasDown = true;
public PauseMenuScreen(PlayScreen playScreen)
{
_playScreen = playScreen ?? throw new ArgumentNullException(nameof(playScreen));
}
public void Initialize(Game1 game)
{
_game = game;
BuildMain();
}
private void BuildMain()
{
_showingSlots = false;
var root = new VerticalStackPanel
{
Spacing = 12,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Background = new SolidBrush(new Color(0, 0, 0, 200)),
Padding = new Thickness(40, 30, 40, 30),
};
root.Widgets.Add(new Label { Text = "PAUSED", HorizontalAlignment = HorizontalAlignment.Center });
root.Widgets.Add(new Label { Text = " " });
var resume = new TextButton { Text = "Resume", Width = 220, HorizontalAlignment = HorizontalAlignment.Center };
resume.Click += (_, _) => _game.Screens.Pop();
root.Widgets.Add(resume);
// Phase 6.5 M0 — surface the level-up affordance when eligible.
var pcChar = _playScreen.PlayerCharacter();
if (pcChar is not null && Theriapolis.Core.Rules.Character.LevelUpFlow.CanLevelUp(pcChar))
{
var lvlBtn = new TextButton
{
Text = $"★ Level Up ({pcChar.Level} → {pcChar.Level + 1})",
Width = 220,
HorizontalAlignment = HorizontalAlignment.Center,
};
lvlBtn.Click += (_, _) =>
{
_game.Screens.Pop();
_game.Screens.Push(new LevelUpScreen(
pcChar,
_playScreen.WorldSeed(),
subclasses: _playScreen.ContentResolver?.Subclasses));
};
root.Widgets.Add(lvlBtn);
}
var saveBtn = new TextButton { Text = "Save Game", Width = 220, HorizontalAlignment = HorizontalAlignment.Center };
saveBtn.Click += (_, _) => BuildSlotPicker();
root.Widgets.Add(saveBtn);
var quickSave = new TextButton { Text = "Quicksave (autosave slot)", Width = 220, HorizontalAlignment = HorizontalAlignment.Center };
quickSave.Click += (_, _) =>
{
bool ok = _playScreen.SaveTo(SavePaths.AutosavePath());
ShowStatus(ok ? "Quicksaved." : "Quicksave failed.");
};
root.Widgets.Add(quickSave);
var quit = new TextButton { Text = "Quit to Title", Width = 220, HorizontalAlignment = HorizontalAlignment.Center };
quit.Click += (_, _) =>
{
// Autosave on quit-to-title (matches existing Phase 4 behaviour).
_playScreen.SaveTo(SavePaths.AutosavePath());
// Pop the pause menu, then pop the play screen.
_game.Screens.Pop();
_game.Screens.Pop();
};
root.Widgets.Add(quit);
_statusLabel = new Label { Text = " ", HorizontalAlignment = HorizontalAlignment.Center };
root.Widgets.Add(_statusLabel);
_desktop = new Desktop { Root = root };
}
private void BuildSlotPicker()
{
_showingSlots = true;
var root = new VerticalStackPanel
{
Spacing = 6,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Background = new SolidBrush(new Color(0, 0, 0, 200)),
Padding = new Thickness(40, 20, 40, 20),
};
root.Widgets.Add(new Label { Text = "Save to slot:", HorizontalAlignment = HorizontalAlignment.Center });
root.Widgets.Add(new Label { Text = " " });
for (int i = 1; i <= C.SAVE_SLOT_COUNT; i++)
{
int slotNum = i; // capture
string path = SavePaths.SlotPath(slotNum);
string label = $"Slot {slotNum:D2}";
if (File.Exists(path))
{
try
{
var bytes = File.ReadAllBytes(path);
var header = Theriapolis.Core.Persistence.SaveCodec.DeserializeHeaderOnly(bytes);
label += " — " + header.SlotLabel();
}
catch { label += " — <unreadable>"; }
}
else { label += " — <empty>"; }
var btn = new TextButton { Text = label, Width = 480, HorizontalAlignment = HorizontalAlignment.Center };
btn.Click += (_, _) =>
{
bool ok = _playScreen.SaveTo(path);
if (ok)
{
ShowStatus($"Saved to slot {slotNum:D2}.");
BuildMain();
}
else ShowStatus("Save failed.");
};
root.Widgets.Add(btn);
}
root.Widgets.Add(new Label { Text = " " });
var back = new TextButton { Text = "Back", Width = 220, HorizontalAlignment = HorizontalAlignment.Center };
back.Click += (_, _) => BuildMain();
root.Widgets.Add(back);
_statusLabel = new Label { Text = " ", HorizontalAlignment = HorizontalAlignment.Center };
root.Widgets.Add(_statusLabel);
_desktop.Root = root;
}
private void ShowStatus(string text)
{
if (_statusLabel is not null) _statusLabel.Text = text;
}
public void Update(GameTime gt)
{
// ESC dismisses the pause menu (resume), or backs out of the slot picker.
// Edge-detect so the press that opened the menu doesn't immediately close it.
bool down = Keyboard.GetState().IsKeyDown(Keys.Escape);
bool justPressed = down && !_escWasDown;
_escWasDown = down;
if (justPressed)
{
if (_showingSlots) BuildMain();
else _game.Screens.Pop();
}
}
public void Draw(GameTime gt, SpriteBatch sb)
{
// Don't clear — let the play screen's last frame remain visible underneath.
_desktop.Render();
}
public void Deactivate() { }
public void Reactivate() { }
}