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>
195 lines
7.9 KiB
C#
195 lines
7.9 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.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,
|
|
};
|
|
}
|