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

164 lines
6.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.Data;
using Theriapolis.Core.Rules.Quests;
namespace Theriapolis.Game.Screens;
/// <summary>
/// Phase 6 M4 — quest journal modal (J key). Two columns: active quests
/// on the left (current step + waypoint hint), completed/failed quests
/// on the right. A tail block shows the engine's recent journal entries.
///
/// Hidden quests stay hidden until they activate; once started, they
/// appear in the active list normally.
/// </summary>
public sealed class QuestLogScreen : IScreen
{
private readonly QuestEngine _engine;
private readonly ContentResolver _content;
private Game1 _game = null!;
private Desktop _desktop = null!;
private bool _jWasDown = true;
private bool _escWasDown = true;
public QuestLogScreen(QuestEngine engine, ContentResolver content)
{
_engine = engine ?? throw new System.ArgumentNullException(nameof(engine));
_content = content ?? throw new System.ArgumentNullException(nameof(content));
}
public void Initialize(Game1 game)
{
_game = game;
BuildUI();
}
private void BuildUI()
{
var root = new VerticalStackPanel
{
Spacing = 8,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Top,
Margin = new Thickness(20),
Padding = new Thickness(20, 12, 20, 12),
Background = new SolidBrush(new Color(15, 12, 8, 235)),
Width = 880,
};
root.Widgets.Add(new Label
{
Text = "QUEST JOURNAL",
HorizontalAlignment = HorizontalAlignment.Center,
TextColor = new Color(255, 230, 170),
});
root.Widgets.Add(new Label { Text = " " });
var twoCol = new HorizontalStackPanel { Spacing = 24 };
// Active.
var leftCol = new VerticalStackPanel { Spacing = 4, Width = 420 };
leftCol.Widgets.Add(new Label { Text = "ACTIVE", TextColor = new Color(200, 180, 130) });
var active = _engine.Active.Values.OrderBy(s => s.StartedAt).ToList();
if (active.Count == 0)
leftCol.Widgets.Add(new Label { Text = "(none yet)", TextColor = new Color(120, 110, 100) });
foreach (var st in active)
{
var def = _content.Quests.TryGetValue(st.QuestId, out var d) ? d : null;
string title = def?.Title ?? st.QuestId;
leftCol.Widgets.Add(new Label
{
Text = $" {title}",
TextColor = new Color(220, 220, 200),
});
// Step description if available.
var step = def?.Steps.FirstOrDefault(x =>
string.Equals(x.Id, st.CurrentStep, System.StringComparison.OrdinalIgnoreCase));
if (step is not null && !string.IsNullOrEmpty(step.Description))
leftCol.Widgets.Add(new Label
{
Text = $" • {step.Description}",
TextColor = new Color(170, 200, 220),
Wrap = true,
Width = 410,
});
if (step is not null && !string.IsNullOrEmpty(step.Waypoint))
leftCol.Widgets.Add(new Label
{
Text = $" → {step.Waypoint}",
TextColor = new Color(140, 180, 110),
});
}
twoCol.Widgets.Add(leftCol);
// Completed / failed.
var rightCol = new VerticalStackPanel { Spacing = 4, Width = 420 };
rightCol.Widgets.Add(new Label { Text = "ARCHIVE", TextColor = new Color(200, 180, 130) });
var done = _engine.Completed.Values.OrderByDescending(s => s.StartedAt).ToList();
if (done.Count == 0)
rightCol.Widgets.Add(new Label { Text = "(none yet)", TextColor = new Color(120, 110, 100) });
foreach (var st in done)
{
var def = _content.Quests.TryGetValue(st.QuestId, out var d) ? d : null;
string title = def?.Title ?? st.QuestId;
string mark = st.Status == QuestStatus.Completed ? "✓" : "✗";
Color color = st.Status == QuestStatus.Completed
? new Color(140, 200, 130)
: new Color(200, 130, 130);
rightCol.Widgets.Add(new Label
{
Text = $" {mark} {title}",
TextColor = color,
});
}
twoCol.Widgets.Add(rightCol);
root.Widgets.Add(twoCol);
// Recent journal tail.
root.Widgets.Add(new Label { Text = " " });
root.Widgets.Add(new Label { Text = "RECENT", TextColor = new Color(200, 180, 130) });
var tail = _engine.Journal.Skip(System.Math.Max(0, _engine.Journal.Count - 8)).ToList();
if (tail.Count == 0)
root.Widgets.Add(new Label { Text = " (no entries)", TextColor = new Color(120, 110, 100) });
foreach (var line in tail)
root.Widgets.Add(new Label
{
Text = $" {line}",
TextColor = new Color(180, 180, 170),
Wrap = true,
Width = 840,
});
root.Widgets.Add(new Label { Text = " " });
root.Widgets.Add(new Label
{
Text = "(J / Esc to close)",
HorizontalAlignment = HorizontalAlignment.Center,
TextColor = new Color(120, 110, 100),
});
_desktop = new Desktop { Root = root };
}
public void Update(GameTime gt)
{
var ks = Keyboard.GetState();
bool j = ks.IsKeyDown(Keys.J);
bool esc = ks.IsKeyDown(Keys.Escape);
bool jPressed = j && !_jWasDown;
bool escPressed = esc && !_escWasDown;
_jWasDown = j; _escWasDown = esc;
if (jPressed || escPressed) _game.Screens.Pop();
}
public void Draw(GameTime gt, SpriteBatch sb) => _desktop.Render();
public void Deactivate() { }
public void Reactivate() { BuildUI(); }
}