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>
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
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.Reputation;
|
||||
|
||||
namespace Theriapolis.Game.Screens;
|
||||
|
||||
/// <summary>
|
||||
/// Phase 6 M2 — reputation screen (R key). Two columns:
|
||||
/// 1. **Factions:** strip of bars per known faction with NEMESIS..CHAMPION
|
||||
/// colour banding + numeric standing.
|
||||
/// 2. **Recent contacts:** last N NPCs the player has personally
|
||||
/// interacted with — name, role, current personal disposition,
|
||||
/// trust ladder.
|
||||
/// Plus a tail block showing the most recent ledger entries with their
|
||||
/// reasons ("why does so-and-so hate me?" breadcrumbs).
|
||||
///
|
||||
/// Hidden factions (e.g. The Maw before Act I climax) are skipped from
|
||||
/// the faction column; they still accumulate state internally.
|
||||
///
|
||||
/// In debug builds, F12 fires a synthetic "+5 Inheritor" event so we can
|
||||
/// eyeball the cascade live without scripting a quest.
|
||||
/// </summary>
|
||||
public sealed class ReputationScreen : IScreen
|
||||
{
|
||||
private readonly PlayerReputation _rep;
|
||||
private readonly ContentResolver _content;
|
||||
private Game1 _game = null!;
|
||||
private Desktop _desktop = null!;
|
||||
private bool _rWasDown = true;
|
||||
private bool _escWasDown = true;
|
||||
private bool _f12WasDown = true;
|
||||
|
||||
public ReputationScreen(PlayerReputation rep, ContentResolver content)
|
||||
{
|
||||
_rep = rep ?? throw new System.ArgumentNullException(nameof(rep));
|
||||
_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 = "REPUTATION",
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
TextColor = new Color(255, 230, 170),
|
||||
});
|
||||
root.Widgets.Add(new Label { Text = " " });
|
||||
|
||||
var twoCol = new HorizontalStackPanel { Spacing = 24 };
|
||||
|
||||
// ── Factions column ─────────────────────────────────────────────
|
||||
var leftCol = new VerticalStackPanel { Spacing = 4, Width = 420 };
|
||||
leftCol.Widgets.Add(new Label { Text = "FACTIONS", TextColor = new Color(200, 180, 130) });
|
||||
foreach (var f in _content.Factions.Values.Where(f => !f.Hidden).OrderBy(f => f.Name))
|
||||
{
|
||||
int score = _rep.Factions.Get(f.Id);
|
||||
var label = DispositionLabels.For(score);
|
||||
leftCol.Widgets.Add(new Label
|
||||
{
|
||||
Text = $"{f.Name,-24} {score,+4:+#;-#;0} {DispositionLabels.DisplayName(label)}",
|
||||
TextColor = ColorForLabel(label),
|
||||
});
|
||||
}
|
||||
twoCol.Widgets.Add(leftCol);
|
||||
|
||||
// ── Personal column ────────────────────────────────────────────
|
||||
var rightCol = new VerticalStackPanel { Spacing = 4, Width = 420 };
|
||||
rightCol.Widgets.Add(new Label { Text = "RECENT CONTACTS", TextColor = new Color(200, 180, 130) });
|
||||
var personals = _rep.Personal.Values.OrderByDescending(p => p.LastInteractionSeconds).Take(12).ToList();
|
||||
if (personals.Count == 0)
|
||||
rightCol.Widgets.Add(new Label
|
||||
{
|
||||
Text = "(no one has met you yet)",
|
||||
TextColor = new Color(120, 110, 100),
|
||||
});
|
||||
foreach (var p in personals)
|
||||
{
|
||||
var label = DispositionLabels.For(p.Score);
|
||||
rightCol.Widgets.Add(new Label
|
||||
{
|
||||
Text = $"{p.RoleTag,-30} {p.Score,+4:+#;-#;0} {p.Trust}",
|
||||
TextColor = ColorForLabel(label),
|
||||
});
|
||||
}
|
||||
twoCol.Widgets.Add(rightCol);
|
||||
root.Widgets.Add(twoCol);
|
||||
|
||||
// ── Recent ledger ──────────────────────────────────────────────
|
||||
root.Widgets.Add(new Label { Text = " " });
|
||||
root.Widgets.Add(new Label { Text = "RECENT EVENTS", TextColor = new Color(200, 180, 130) });
|
||||
var recent = _rep.Ledger.Entries.Reverse().Take(10).ToList();
|
||||
if (recent.Count == 0)
|
||||
root.Widgets.Add(new Label
|
||||
{
|
||||
Text = "(no events yet)",
|
||||
TextColor = new Color(120, 110, 100),
|
||||
});
|
||||
foreach (var ev in recent)
|
||||
{
|
||||
string what = string.IsNullOrEmpty(ev.FactionId)
|
||||
? (string.IsNullOrEmpty(ev.RoleTag) ? "world" : ev.RoleTag)
|
||||
: ev.FactionId;
|
||||
string note = string.IsNullOrEmpty(ev.Note) ? "" : $" — {ev.Note}";
|
||||
root.Widgets.Add(new Label
|
||||
{
|
||||
Text = $" [{ev.Kind,-9}] {what,-26} {ev.Magnitude,+4:+#;-#;0}{note}",
|
||||
TextColor = ev.Magnitude >= 0 ? new Color(160, 200, 140) : new Color(220, 140, 140),
|
||||
});
|
||||
}
|
||||
|
||||
// ── Footer ─────────────────────────────────────────────────────
|
||||
root.Widgets.Add(new Label { Text = " " });
|
||||
root.Widgets.Add(new Label
|
||||
{
|
||||
Text = "(R / Esc to close · F12 in debug build = +5 Inheritor synthetic event)",
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
TextColor = new Color(120, 110, 100),
|
||||
});
|
||||
|
||||
_desktop = new Desktop { Root = root };
|
||||
}
|
||||
|
||||
public void Update(GameTime gt)
|
||||
{
|
||||
var ks = Keyboard.GetState();
|
||||
bool r = ks.IsKeyDown(Keys.R);
|
||||
bool esc = ks.IsKeyDown(Keys.Escape);
|
||||
bool f12 = ks.IsKeyDown(Keys.F12);
|
||||
|
||||
bool rPressed = r && !_rWasDown;
|
||||
bool escPressed = esc && !_escWasDown;
|
||||
bool f12Pressed = f12 && !_f12WasDown;
|
||||
|
||||
_rWasDown = r; _escWasDown = esc; _f12WasDown = f12;
|
||||
|
||||
if (rPressed || escPressed) { _game.Screens.Pop(); return; }
|
||||
|
||||
#if DEBUG
|
||||
if (f12Pressed)
|
||||
{
|
||||
// Dev affordance — fire a +5 Inheritor event so the cascade is
|
||||
// visible to the eye without authoring a quest.
|
||||
_rep.Submit(new RepEvent
|
||||
{
|
||||
Kind = RepEventKind.Misc,
|
||||
FactionId = "inheritors",
|
||||
Magnitude = 5,
|
||||
Note = "dev affordance (F12)",
|
||||
}, _content.Factions);
|
||||
BuildUI(); // refresh
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public void Draw(GameTime gt, SpriteBatch sb) => _desktop.Render();
|
||||
public void Deactivate() { }
|
||||
public void Reactivate() { }
|
||||
|
||||
private static Color ColorForLabel(DispositionLabel label) => label switch
|
||||
{
|
||||
DispositionLabel.Nemesis => new Color(220, 60, 60),
|
||||
DispositionLabel.Hostile => new Color(220, 110, 90),
|
||||
DispositionLabel.Antagonistic => new Color(220, 160, 110),
|
||||
DispositionLabel.Unfriendly => new Color(200, 180, 130),
|
||||
DispositionLabel.Neutral => new Color(180, 180, 180),
|
||||
DispositionLabel.Favorable => new Color(170, 200, 150),
|
||||
DispositionLabel.Friendly => new Color(140, 210, 130),
|
||||
DispositionLabel.Allied => new Color(110, 220, 130),
|
||||
DispositionLabel.Champion => new Color(150, 230, 200),
|
||||
_ => Color.White,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user