b451f83174
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>
196 lines
7.2 KiB
C#
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() { }
|
|
}
|