Files
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

144 lines
4.4 KiB
C#

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Myra.Graphics2D.UI;
using Theriapolis.Core;
using Theriapolis.Core.Persistence;
using Theriapolis.Game.Platform;
namespace Theriapolis.Game.Screens;
/// <summary>
/// Slot picker. Lists C.SAVE_SLOT_COUNT slots plus the autosave slot. Reading
/// each slot only deserializes the JSON header (cheap), so opening the picker
/// is fast even if there are many large saves.
///
/// Phase 4 mode: load only (called from TitleScreen). Save-from-game uses the
/// F5 quicksave; a save-as-slot UI can be added later by extending this screen
/// with an Action.
/// </summary>
public sealed class SaveLoadScreen : IScreen
{
private Game1 _game = null!;
private Desktop _desktop = null!;
public void Initialize(Game1 game)
{
_game = game;
BuildUI();
}
private void BuildUI()
{
var root = new VerticalStackPanel
{
Spacing = 8,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
};
root.Widgets.Add(new Label
{
Text = "LOAD GAME",
HorizontalAlignment = HorizontalAlignment.Center,
});
root.Widgets.Add(new Label { Text = " " });
// Autosave row first.
AddSlotRow(root, "Autosave", SavePaths.AutosavePath());
for (int i = 1; i <= C.SAVE_SLOT_COUNT; i++)
AddSlotRow(root, $"Slot {i:D2}", SavePaths.SlotPath(i));
root.Widgets.Add(new Label { Text = " " });
var back = new TextButton { Text = "Back", Width = 200, HorizontalAlignment = HorizontalAlignment.Center };
back.Click += (_, _) => _game.Screens.Pop();
root.Widgets.Add(back);
_desktop = new Desktop { Root = root };
}
private void AddSlotRow(VerticalStackPanel parent, string label, string path)
{
string text = label;
bool exists = File.Exists(path);
bool compatible = false;
if (exists)
{
try
{
var bytes = File.ReadAllBytes(path);
var header = SaveCodec.DeserializeHeaderOnly(bytes);
if (SaveCodec.IsCompatible(header))
{
text = $"{label}: {header.SlotLabel()}";
compatible = true;
}
else
{
text = $"{label}: <v{header.Version} — incompatible (Phase 5+ only)>";
}
}
catch
{
text = $"{label}: <unreadable>";
}
}
else
{
text = $"{label}: <empty>";
}
var btn = new TextButton
{
Text = text,
Width = 480,
HorizontalAlignment = HorizontalAlignment.Center,
};
if (exists && compatible) btn.Click += (_, _) => LoadSlot(path);
else btn.Enabled = false;
parent.Widgets.Add(btn);
}
private void LoadSlot(string path)
{
try
{
var bytes = File.ReadAllBytes(path);
var headerOnly = SaveCodec.DeserializeHeaderOnly(bytes);
if (!SaveCodec.IsCompatible(headerOnly))
{
var err = new Label
{
Text = SaveCodec.IncompatibilityReason(headerOnly),
HorizontalAlignment = HorizontalAlignment.Center,
};
_desktop.Root = err;
return;
}
var (header, body) = SaveCodec.Deserialize(bytes);
_game.Screens.Pop(); // back to title
_game.Screens.Push(new WorldGenProgressScreen(header.ParseSeed(), restoreFromSave: body, savedHeader: header));
}
catch (Exception ex)
{
// Crude error display: replace the screen content with the error.
var err = new Label { Text = $"Load failed:\n{ex.Message}", HorizontalAlignment = HorizontalAlignment.Center };
_desktop.Root = err;
}
}
public void Update(GameTime gt)
{
if (Keyboard.GetState().IsKeyDown(Keys.Escape)) _game.Screens.Pop();
}
public void Draw(GameTime gt, SpriteBatch sb)
{
_game.GraphicsDevice.Clear(new Color(20, 20, 30));
_desktop.Render();
}
public void Deactivate() { }
public void Reactivate() { }
}