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; /// /// 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. /// 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, }; }