51 lines
3.3 KiB
C#
51 lines
3.3 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|