using System.Reflection; using Microsoft.Extensions.Logging; using Outnumbered.Config; using Outnumbered.Data; using Outnumbered.Domain; namespace Outnumbered; // The per-player handicap (the balance spine; the HUD shows each player their live deal/take/XP bands). The MATH lives in Outnumbered.Domain.HandicapModel — a single // signed index t in [-1,+1] driving deal/take/XP together so they reach their extremes at the SAME thresholds. These are // zero-math accessors that snapshot the player and call it. The hot damage hook reaches HandicapModel through // CombatResolver (one snapshot); the HUD + progression use these adapters. public sealed partial class OutnumberedPlugin { // The effective handicap for the active mode = base Config.Handicap with the mode's overrides applied, wrapped in a // ResolvedHandicap (config + precomputed guard denominators). Rebuilt at driver-select (Initialize_Driver) and on // !og_reload, so it stays live-tunable AND ComputeT never recomputes the config-invariant terms per hit/tick. private ResolvedHandicap _hcap = new(new HandicapConfig()); internal void RebuildEffectiveHandicap() => _hcap = new ResolvedHandicap(_driver?.Handicap is { } o ? o.ApplyTo(Config.Handicap) : Config.Handicap); // The XP-rate bridge (used by GrantXp + the HUD). The deal/take chains go straight through CombatResolver -> // HandicapModel.MDeal/MTake on a snapshot, so there are no MDeal(pd)/MTake(pd) bridge methods here. private double HandicapXpMult(PlayerData pd) => HandicapModel.XpMult(Snapshot(pd), _hcap); // One-time structural guard that HandicapOverride stays a faithful mirror of HandicapConfig: every base field needs a // matching nullable override AND ApplyTo must actually wire it. Catches the documented footgun — add a field to both // POCOs but forget the `X ?? b.X` line in ApplyTo, and that field's per-mode override is silently ignored (a tuner sees // "the GunGame/Survival override doesn't work"). Probes ApplyTo functionally via reflection. Structural, not value- // dependent, so it runs once at load (not per !og_reload). All current HandicapConfig fields are bool/int/double. private void Initialize_Handicap() { var baseCfg = new HandicapConfig(); foreach (var bp in typeof(HandicapConfig).GetProperties(BindingFlags.Public | BindingFlags.Instance)) { if (!bp.CanRead || !bp.CanWrite) continue; var op = typeof(HandicapOverride).GetProperty(bp.Name); if (op is null) { Logger.LogError("Outnumbered: HandicapOverride is missing field '{Field}' that HandicapConfig defines (add a nullable mirror + an ApplyTo line).", bp.Name); continue; } object? sentinel = bp.GetValue(baseCfg) switch { bool b => !b, int i => i + 1, double d => d + 1.0, _ => null }; if (sentinel is null) continue; // non-bool/int/double field: can't auto-probe ApplyTo wiring here var ov = new HandicapOverride(); op.SetValue(ov, sentinel); if (!Equals(bp.GetValue(ov.ApplyTo(baseCfg)), sentinel)) Logger.LogError("Outnumbered: HandicapOverride.ApplyTo does not propagate '{Field}' — its per-mode override is silently ignored (add the matching `?? b.` line).", bp.Name); } } }