using System.Collections.Frozen; using Outnumbered.Config; namespace Outnumbered.Domain; // Resolves a player's effective stat values from their invested levels + run-card bonuses. Takes a stat-def lookup // (key -> StatDef) rather than the named-field config, so it's already shaped for the stat registry — the engine builds // the dictionary once and passes it. `Eff` = permanent only; `EffRun` = permanent + survival card; `CardMag` = the // effect-card magnitude (a non-stat key, cards only). public static class StatResolver { private static readonly StatDef Zero = new(0, 0); private static int LevelOf(in PlayerSnapshot s, string key) => s.Upgrades is not null && s.Upgrades.TryGetValue(key, out var l) ? l : 0; public static double Eff(in PlayerSnapshot s, string key, FrozenDictionary defs) { var d = defs.TryGetValue(key, out var def) ? def : Zero; return d.Base + LevelOf(s, key) * d.PerLevel; } public static double EffRun(in PlayerSnapshot s, string key, FrozenDictionary defs) => Eff(s, key, defs) + (s.Cards?.Bonus(key) ?? 0.0); public static double CardMag(in PlayerSnapshot s, string key) => s.Cards?.Bonus(key) ?? 0.0; public static int MaxHp(in PlayerSnapshot s, FrozenDictionary defs, int baseMax) => baseMax + (int)EffRun(s, StatKeys.MaxHp, defs); public static int MaxArmor(in PlayerSnapshot s, FrozenDictionary defs, int baseMax) => baseMax + (int)EffRun(s, StatKeys.MaxArmor, defs); // 0 at full HP, 1 near death — drives Berserk (ability + passive card). Health<=0 (a snapshot built with no live // pawn, or a downed pawn) yields 0, NOT 1: missing-HP scaling only applies to a live attacker, so "no pawn" must // mean "no bonus", not "max bonus" (guards the pd-less Snapshot path where Health defaults to 0). public static double MissingHpFraction(in PlayerSnapshot s, int maxHp) => // Health>=1 (guarded) and maxHp>=1 => 1 - Health/maxHp < 1, so the upper clamp can't bind; only the lower // (overheal -> negative) is live. Math.Max(0, x) is bit-identical to Math.Clamp(x, 0, 1) here. maxHp <= 0 || s.Health <= 0 ? 0.0 : Math.Max(0.0, 1.0 - s.Health / (double)maxHp); }