using System.Collections.Frozen; using CounterStrikeSharp.API; using CounterStrikeSharp.API.Core; using Outnumbered.Config; using Outnumbered.Data; using Outnumbered.Domain; namespace Outnumbered; // The single engine site that reads a player into an immutable PlayerSnapshot for the pure Domain math, plus the stat // registry (_statDefs) the Domain resolvers consume. Everything pure (handicap / combat / progression / stat resolution) // takes a snapshot + _statDefs and never touches a pawn/controller — so the CS2/CSSharp surface lives only here. public sealed partial class OutnumberedPlugin { private const int BaseMaxHp = 100; private const int BaseMaxArmor = 100; // key -> StatDef, rebuilt from Config.Stats on Load + every !og_reload. This is the stat registry the Domain resolvers // index (StatResolver/CombatResolver take it) — the key->def view of the named Config.Stats fields (which stay, for JSON // back-compat). Projected straight from StatRegistry (Stats.cs), so a new stat is one registry row, not a line here. // FrozenDictionary: built once per load/reload, read per-hit (OffenseMultiplier alone does 3+ lookups/hit) — frozen // has faster reads than Dictionary. Ordinal matches Dictionary's default, so lookups are bit-identical. private FrozenDictionary _statDefs = FrozenDictionary.Empty; private void RebuildStatDefs() => _statDefs = StatRegistry.ToFrozenDictionary(r => r.Key, r => r.Def(Config.Stats), StringComparer.Ordinal); // Build the immutable Domain input for one player. `p` is optional: it supplies the live pawn Health (drives the // missing-HP fraction for Berserk) — null is fine for paths that don't read Health (XP rate, where t ignores HP). // Upgrades + Cards are held by reference, so building a snapshot per hit/tick is allocation-free. internal PlayerSnapshot Snapshot(PlayerData pd, CCSPlayerController? p = null) { var pawn = p?.PlayerPawn.Value; double now = Server.CurrentTime; // read once for the 3 ability checks below (can't advance within this build) return new PlayerSnapshot { Level = pd.Level, Prestige = pd.Prestige, Xp = pd.Xp, Kills = pd.Kills, Deaths = pd.Deaths, HeadshotKills = pd.HeadshotKills, Streak = pd.Streak, Health = pawn?.Health ?? 0, HandicapProgress = _driver?.HandicapProgress(pd) ?? 0.0, HandicapFloor = _driver?.HandicapFloor(pd) ?? -1.0, TeamDealMult = _driver?.TeamDealMult() ?? 1.0, TeamTakeMult = _driver?.TeamTakeMult() ?? 1.0, OverchargeActive = AbilityActive(pd, AbOvercharge, now), // CombatResolver gates these on Abilities.Enabled BerserkActive = AbilityActive(pd, AbBerserk, now), AdrenalineActive = AbilityActive(pd, AbAdrenaline, now), Upgrades = pd.Upgrades, Cards = _driver?.CardSource(pd), // survival run (IStatBonusSource); null in TDM/GG }; } }