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

71 lines
2.8 KiB
C#

namespace Theriapolis.Core.Rules.Reputation;
/// <summary>
/// Phase 6 M2 — Layer 3 of the disposition stack: how a *specific* NPC
/// feels about the player based on direct personal experience. Per
/// <c>reputation.md §I-3</c>, only NPCs the player has actually
/// interacted with accumulate one of these — generic shopkeepers walked
/// past don't bloat state.
///
/// Keyed by role tag (anchor-prefixed for named NPCs:
/// "millhaven.innkeeper"). Generic NPCs that the player talks to register
/// briefly under their generic tag but typically reset on chunk evict —
/// only named NPCs carry a stable id across reloads.
/// </summary>
public sealed class PersonalDisposition
{
/// <summary>Role tag identifying which NPC this record belongs to.</summary>
public string RoleTag { get; init; } = "";
/// <summary>Score relative to neutral, clamped to <c>±C.REP_MAX</c>.</summary>
public int Score { get; set; }
/// <summary>Trust ladder accumulated across the interaction log.</summary>
public TrustLevel Trust { get; set; } = TrustLevel.Stranger;
/// <summary>True after the player betrayed this specific NPC. Sticky — only narrative
/// effects can clear it.</summary>
public bool Betrayed { get; set; }
/// <summary>Last interaction time in WorldClock seconds. 0 = never interacted.</summary>
public long LastInteractionSeconds { get; set; }
/// <summary>Free-form memory tags ("saved-my-kit", "lied-about-rawfang", ...).</summary>
public HashSet<string> Memory { get; } = new();
/// <summary>Last N events affecting this specific NPC. Bounded — see <see cref="MaxLogEntries"/>.</summary>
public List<RepEvent> Log { get; } = new();
public const int MaxLogEntries = 32;
/// <summary>Append a personal event and apply its magnitude to <see cref="Score"/>.</summary>
public void Apply(RepEvent ev)
{
Score = System.Math.Clamp(Score + ev.Magnitude, C.REP_MIN, C.REP_MAX);
if (ev.Kind == RepEventKind.Betrayal && ev.Magnitude < 0) Betrayed = true;
Log.Add(ev);
if (Log.Count > MaxLogEntries) Log.RemoveAt(0);
LastInteractionSeconds = ev.TimestampSeconds;
Trust = ComputeTrust();
}
/// <summary>
/// Trust ladder derived from positive interaction count. Negative events
/// don't promote; betrayal demotes to Stranger regardless of history.
/// </summary>
private TrustLevel ComputeTrust()
{
if (Betrayed) return TrustLevel.Stranger;
int positives = 0;
foreach (var e in Log) if (e.Magnitude > 0) positives++;
return positives switch
{
>= 12 => TrustLevel.Bonded,
>= 7 => TrustLevel.Trusted,
>= 3 => TrustLevel.Familiar,
>= 1 => TrustLevel.Acquaintance,
_ => TrustLevel.Stranger,
};
}
}