cs2-outnumbered/Outnumbered/Domain/StatResolver.cs
Kamal Tufekcic d701598350
All checks were successful
CI / build (push) Successful in 32s
CI / release (push) Successful in 32s
CI / lint (push) Successful in 30s
initial commit
2026-07-05 13:28:35 +03:00

41 lines
2.2 KiB
C#

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<string, StatDef> 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<string, StatDef> 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<string, StatDef> defs, int baseMax) =>
baseMax + (int)EffRun(s, StatKeys.MaxHp, defs);
public static int MaxArmor(in PlayerSnapshot s, FrozenDictionary<string, StatDef> 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);
}