71 lines
2.8 KiB
C#
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,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|