Files
TheriapolisV3/Theriapolis.Game/Screens/ReputationScreen.cs
T

195 lines
7.9 KiB
C#
Raw Normal View History

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