using Theriapolis.Core.Data; namespace Theriapolis.Core.Rules.Reputation; /// /// Phase 6 M2 — Layer 2 of the disposition stack. The player's /// faction-affiliated reputation: Dictionary<FactionId, int> /// clamped to ±C.REP_MAX, with the opposition matrix from /// reputation.md §I-2 applied automatically on every change. /// /// Phase 6 M2 ships the score-only contract — propagation by distance /// + time decay arrives in M5. Until then, every call /// fires an event that updates the at-origin standing immediately and /// cascades through opposition; nothing else happens elsewhere on the map. /// public sealed class FactionStanding { private readonly Dictionary _standings = new(System.StringComparer.OrdinalIgnoreCase); /// Snapshot of every faction's current standing. public IReadOnlyDictionary Standings => _standings; /// /// Score with . Returns 0 (neutral) when /// the faction has never been touched. /// public int Get(string factionId) => _standings.TryGetValue(factionId, out int v) ? v : 0; /// Direct setter, no opposition cascade. Used by save-load and tests. public void Set(string factionId, int value) { _standings[factionId] = System.Math.Clamp(value, C.REP_MIN, C.REP_MAX); } /// /// Apply to 's /// standing and cascade the opposition matrix. Returns the list of /// (factionId, delta) tuples actually applied (caller can log them). /// /// Cascading is single-hop: ['inheritors'].opposition /// is read once. Phase 6 M2 doesn't iterate (no transitive opposition). /// public List<(string FactionId, int Delta)> Apply( string factionId, int delta, IReadOnlyDictionary factions) { var applied = new List<(string, int)>(); if (delta == 0) return applied; // Direct change first — clamped delta accounts for floor/ceiling. int actualDelta = ApplyClamped(factionId, delta); applied.Add((factionId, actualDelta)); // Cascade through opposition (use the *requested* delta, not the // possibly-truncated one, so a clamp at the source doesn't mute // downstream effects too). if (factions.TryGetValue(factionId, out var def)) { foreach (var (otherId, mult) in def.Opposition) { if (mult == 0f) continue; int subDelta = (int)System.Math.Round(delta * mult); if (subDelta == 0) continue; int actualSub = ApplyClamped(otherId, subDelta); if (actualSub != 0) applied.Add((otherId, actualSub)); } } return applied; } private int ApplyClamped(string factionId, int delta) { int current = Get(factionId); int next = System.Math.Clamp(current + delta, C.REP_MIN, C.REP_MAX); if (next == current) return 0; _standings[factionId] = next; return next - current; } public void Clear() => _standings.Clear(); }