initial commit
This commit is contained in:
commit
d701598350
67 changed files with 9351 additions and 0 deletions
340
Outnumbered/Config/DomainConfig.cs
Normal file
340
Outnumbered/Config/DomainConfig.cs
Normal file
|
|
@ -0,0 +1,340 @@
|
|||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Outnumbered.Config;
|
||||
|
||||
// The config sub-types the pure Domain layer reads (handicap / progression / stat / combat / survival economy). Split out
|
||||
// of OutnumberedConfig.cs — which is CounterStrikeSharp-coupled (BasePluginConfig) — so the xUnit test project can
|
||||
// <Compile Include> these alongside Domain/*.cs with NO CounterStrikeSharp reference. They're plain POCOs (System.Text.Json
|
||||
// attributes only); the engine-coupled config (OutnumberedConfig + Hud/Shop/Match/GunGame/... blocks) stays in OutnumberedConfig.cs.
|
||||
|
||||
// One killstreak ability's tunables (spec §4). Magnitude meaning is per-ability:
|
||||
// No Reload — (unused; clip-refill only)
|
||||
// Adrenaline — Magnitude = % incoming-damage reduction
|
||||
// Overcharge — Magnitude = % bonus outgoing damage
|
||||
// Bloodthirst — Magnitude = +% HP lifesteal, Magnitude2 = +% armor lifesteal (additive, for the duration)
|
||||
// Berserk — Magnitude = bonus damage per full missing-HP fraction, Magnitude2 = bonus crit-damage per missing-HP fraction
|
||||
public sealed class AbilityDef
|
||||
{
|
||||
[JsonPropertyName("StreakReq")] public int StreakReq { get; set; }
|
||||
[JsonPropertyName("Cooldown")] public float Cooldown { get; set; }
|
||||
[JsonPropertyName("Duration")] public float Duration { get; set; }
|
||||
[JsonPropertyName("Magnitude")] public double Magnitude { get; set; }
|
||||
[JsonPropertyName("Magnitude2")] public double Magnitude2 { get; set; }
|
||||
// "Movie filter" tint shown while this ability is active (blended when several are up). Spec §7 juice.
|
||||
[JsonPropertyName("TintR")] public int TintR { get; set; }
|
||||
[JsonPropertyName("TintG")] public int TintG { get; set; }
|
||||
[JsonPropertyName("TintB")] public int TintB { get; set; }
|
||||
}
|
||||
|
||||
// The 5 killstreak abilities + baseline infinite-reserve ammo (spec §4/§8).
|
||||
public sealed class AbilitiesConfig
|
||||
{
|
||||
[JsonPropertyName("Enabled")] public bool Enabled { get; set; } = true;
|
||||
|
||||
// Active-ability screen tint ("movie filter") via the CS2 fade user message. UNVERIFIED format — toggle off
|
||||
// if it misbehaves; the HUD active-banner is the reliable fallback. TintAlpha = overlay strength (0-255).
|
||||
[JsonPropertyName("ScreenTint")] public bool ScreenTint { get; set; } = true;
|
||||
[JsonPropertyName("TintAlpha")] public int TintAlpha { get; set; } = 50;
|
||||
|
||||
// sv_infinite_ammo is cheat-flagged at sv_cheats 0, so reserve is topped up in code (spec §1).
|
||||
[JsonPropertyName("InfiniteReserve")] public bool InfiniteReserve { get; set; } = true;
|
||||
[JsonPropertyName("InfiniteReserveAmount")] public int InfiniteReserveAmount { get; set; } = 250;
|
||||
|
||||
// Grenade-key input: a grenade-select key (slot6-10) only reaches the server while the player holds
|
||||
// that grenade. So we GRANT the matching grenade exactly while its ability is castable and REMOVE it on
|
||||
// use — the key is live only when the ability is, the grenade can never be thrown, and there's no clutter.
|
||||
[JsonPropertyName("GrenadeKeyInput")] public bool GrenadeKeyInput { get; set; } = true;
|
||||
// Indexed by ability (0=No Reload, 1=Adrenaline, 2=Overcharge, 3=Bloodthirst, 4=Berserk). CT uses
|
||||
// weapon_incgrenade (not molotov). Must stay aligned with AbilityForGrenade in Abilities.cs.
|
||||
[JsonPropertyName("AbilityGrenades")]
|
||||
public List<string> AbilityGrenades { get; set; } = new()
|
||||
{ "weapon_hegrenade", "weapon_flashbang", "weapon_smokegrenade", "weapon_incgrenade", "weapon_decoy" };
|
||||
|
||||
[JsonPropertyName("NoReload")] public AbilityDef NoReload { get; set; } = new() { StreakReq = 10, Cooldown = 40, Duration = 12, TintR = 255, TintG = 221, TintB = 85 };
|
||||
[JsonPropertyName("Adrenaline")] public AbilityDef Adrenaline { get; set; } = new() { StreakReq = 20, Cooldown = 50, Duration = 8, Magnitude = 35, TintR = 90, TintG = 150, TintB = 255 };
|
||||
[JsonPropertyName("Overcharge")] public AbilityDef Overcharge { get; set; } = new() { StreakReq = 30, Cooldown = 60, Duration = 8, Magnitude = 50, TintR = 255, TintG = 140, TintB = 40 };
|
||||
[JsonPropertyName("Bloodthirst")] public AbilityDef Bloodthirst { get; set; } = new() { StreakReq = 40, Cooldown = 70, Duration = 8, Magnitude = 60, Magnitude2 = 40, TintR = 90, TintG = 220, TintB = 90 };
|
||||
[JsonPropertyName("Berserk")] public AbilityDef Berserk { get; set; } = new() { StreakReq = 50, Cooldown = 90, Duration = 10, Magnitude = 1.5, Magnitude2 = 1.5, TintR = 255, TintG = 60, TintB = 60 };
|
||||
}
|
||||
|
||||
// The per-player balance handicap (spec §5). A single signed index t in [-1,+1] drives all three
|
||||
// outputs (deal / take / XP) so they move together and hit their extremes at the SAME thresholds.
|
||||
// t = N - B. N (nerf, 0..1) = weighted avg of four factors each normalised to its "maxed" threshold;
|
||||
// B (buff, 0..1) = how far K/D sits below neutral (comeback help).
|
||||
public sealed class HandicapConfig
|
||||
{
|
||||
// These defaults are tuned so the 100-point stat ceiling (~x3 offense) can't outscale the reachable nerf: Kd/Level are
|
||||
// up-weighted and LevelMaxNerf spans the full point-investment range, keeping "better = harder + more XP, worse =
|
||||
// easier + less XP" monotonic (otherwise a leveled/statted player inverts the handicap into a net advantage).
|
||||
[JsonPropertyName("Enabled")] public bool Enabled { get; set; } = true;
|
||||
// Scales the whole signed deviation from neutral. 0 = off, ~1 = mild, 1.6 = retuned designed value, >2 = brutal.
|
||||
[JsonPropertyName("MasterDifficulty")] public double MasterDifficulty { get; set; } = 1.6;
|
||||
// Shapes |t| -> output. 1 = linear; >1 = eases IN (mild leads barely touched, then a hard cliff at the top —
|
||||
// a wide "god with base stats" band); <1 = bites EARLY (debuff ramps fast the moment you pull ahead, then
|
||||
// saturates). Lower = punishes a moderate lead sooner/harder. 1.3 keeps a small mild-lead toe.
|
||||
[JsonPropertyName("Curve")] public double Curve { get; set; } = 1.3;
|
||||
|
||||
// Each nerf factor ramps 0->1 from neutral to its threshold; ALL four at threshold -> full nerf (t=1).
|
||||
[JsonPropertyName("KdNeutral")] public double KdNeutral { get; set; } = 1.5; // K/D break-even (vs-many-bots avg is >1)
|
||||
[JsonPropertyName("KdMaxNerf")] public double KdMaxNerf { get; set; } = 4.0; // K/D that maxes the K/D factor (4 so realistic 3-4 K/D bites, not just 5+)
|
||||
[JsonPropertyName("KdMinBuff")] public double KdMinBuff { get; set; } = 0.5; // K/D that maxes the comeback buff
|
||||
[JsonPropertyName("HsMaxNerf")] public double HsMaxNerf { get; set; } = 0.60; // headshot-kill rate that maxes the HS factor
|
||||
[JsonPropertyName("HsMinKills")] public int HsMinKills { get; set; } = 5; // ignore HS rate until this many kills
|
||||
[JsonPropertyName("StreakMaxNerf")] public int StreakMaxNerf { get; set; } = 20; // killstreak that maxes the streak factor
|
||||
[JsonPropertyName("LevelMaxNerf")] public int LevelMaxNerf { get; set; } = 100; // level that maxes the level factor (100 spans the full point-investment range)
|
||||
|
||||
// Relative weights of the factors in the nerf average (normalised by their sum). Kd + Level up-weighted: those are the
|
||||
// axes a skilled player actually maxes, so winning without farming HS/streak still bites.
|
||||
[JsonPropertyName("KdWeight")] public double KdWeight { get; set; } = 2.0;
|
||||
[JsonPropertyName("HsWeight")] public double HsWeight { get; set; } = 1.0;
|
||||
[JsonPropertyName("StreakWeight")] public double StreakWeight { get; set; } = 1.0;
|
||||
[JsonPropertyName("LevelWeight")] public double LevelWeight { get; set; } = 1.5;
|
||||
// Weight of the active mode's "progress" axis (0..1, supplied by the driver). 0 in TDM (no axis). In Gun Game
|
||||
// it's ladder position, so climbing nerfs you harder and a bot-demotion eases it. Default 0 (off for TDM).
|
||||
[JsonPropertyName("ProgressWeight")] public double ProgressWeight { get; set; } = 0.0;
|
||||
|
||||
// Output bands, linear in t. At t=+1 deal=Floor / take=Ceiling / xp=Ceiling; at t=-1 deal=Ceiling / take=Floor / xp=Floor.
|
||||
[JsonPropertyName("MDealFloor")] public double MDealFloor { get; set; } = 0.033; // dominant players deal ~3.3% base; the +50% Damage stat lands the effective body-shot floor at ~0.05 (GG/Survival pin this back to 0.1, where the ladder/wave needs a viable deal)
|
||||
[JsonPropertyName("MDealCeiling")] public double MDealCeiling { get; set; } = 1.3; // strugglers deal up to 130% (compressed: the buff multiplies onto stats)
|
||||
[JsonPropertyName("MTakeFloor")] public double MTakeFloor { get; set; } = 0.85; // strugglers take down to 85% (compressed comeback band)
|
||||
[JsonPropertyName("MTakeCeiling")] public double MTakeCeiling { get; set; } = 8.0; // dominant players take 800%
|
||||
[JsonPropertyName("XpFloor")] public double XpFloor { get; set; } = 0.1; // strugglers climb at 10%
|
||||
[JsonPropertyName("XpCeiling")] public double XpCeiling { get; set; } = 10.0; // dominant players climb at 1000%
|
||||
}
|
||||
|
||||
// Per-mode handicap override — a nullable mirror of HandicapConfig. The base `Handicap` block is the DEFAULT;
|
||||
// each mode may carry its own `Handicap` sub-object overriding ONLY the fields it sets, e.g.
|
||||
// "GunGame": { "Handicap": { "Curve": 1.6, "MTakeCeiling": 5.0 } } — everything else inherits the base. The effective
|
||||
// config is resolved per active mode (Handicap.cs RebuildEffectiveHandicap). Add new fields here AND in ApplyTo.
|
||||
public sealed class HandicapOverride
|
||||
{
|
||||
// Every field is nullable and WhenWritingNull-ignored: an unset field is OMITTED from the generated JSON entirely
|
||||
// (rather than emitted as a confusing `null`), so a mode's Handicap block lists ONLY the fields it actually overrides
|
||||
// and the rest transparently inherit the base block. A missing field still deserializes to null -> inherit.
|
||||
[JsonPropertyName("Enabled"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public bool? Enabled { get; set; }
|
||||
[JsonPropertyName("MasterDifficulty"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public double? MasterDifficulty { get; set; }
|
||||
[JsonPropertyName("Curve"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public double? Curve { get; set; }
|
||||
[JsonPropertyName("KdNeutral"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public double? KdNeutral { get; set; }
|
||||
[JsonPropertyName("KdMaxNerf"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public double? KdMaxNerf { get; set; }
|
||||
[JsonPropertyName("KdMinBuff"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public double? KdMinBuff { get; set; }
|
||||
[JsonPropertyName("HsMaxNerf"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public double? HsMaxNerf { get; set; }
|
||||
[JsonPropertyName("HsMinKills"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int? HsMinKills { get; set; }
|
||||
[JsonPropertyName("StreakMaxNerf"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int? StreakMaxNerf { get; set; }
|
||||
[JsonPropertyName("LevelMaxNerf"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int? LevelMaxNerf { get; set; }
|
||||
[JsonPropertyName("KdWeight"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public double? KdWeight { get; set; }
|
||||
[JsonPropertyName("HsWeight"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public double? HsWeight { get; set; }
|
||||
[JsonPropertyName("StreakWeight"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public double? StreakWeight { get; set; }
|
||||
[JsonPropertyName("LevelWeight"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public double? LevelWeight { get; set; }
|
||||
[JsonPropertyName("ProgressWeight"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public double? ProgressWeight { get; set; }
|
||||
[JsonPropertyName("MDealFloor"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public double? MDealFloor { get; set; }
|
||||
[JsonPropertyName("MDealCeiling"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public double? MDealCeiling { get; set; }
|
||||
[JsonPropertyName("MTakeFloor"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public double? MTakeFloor { get; set; }
|
||||
[JsonPropertyName("MTakeCeiling"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public double? MTakeCeiling { get; set; }
|
||||
[JsonPropertyName("XpFloor"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public double? XpFloor { get; set; }
|
||||
[JsonPropertyName("XpCeiling"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public double? XpCeiling { get; set; }
|
||||
|
||||
// Produce a fresh effective config = base with this override's non-null fields applied.
|
||||
public HandicapConfig ApplyTo(HandicapConfig b) => new()
|
||||
{
|
||||
Enabled = Enabled ?? b.Enabled,
|
||||
MasterDifficulty = MasterDifficulty ?? b.MasterDifficulty,
|
||||
Curve = Curve ?? b.Curve,
|
||||
KdNeutral = KdNeutral ?? b.KdNeutral,
|
||||
KdMaxNerf = KdMaxNerf ?? b.KdMaxNerf,
|
||||
KdMinBuff = KdMinBuff ?? b.KdMinBuff,
|
||||
HsMaxNerf = HsMaxNerf ?? b.HsMaxNerf,
|
||||
HsMinKills = HsMinKills ?? b.HsMinKills,
|
||||
StreakMaxNerf = StreakMaxNerf ?? b.StreakMaxNerf,
|
||||
LevelMaxNerf = LevelMaxNerf ?? b.LevelMaxNerf,
|
||||
KdWeight = KdWeight ?? b.KdWeight,
|
||||
HsWeight = HsWeight ?? b.HsWeight,
|
||||
StreakWeight = StreakWeight ?? b.StreakWeight,
|
||||
LevelWeight = LevelWeight ?? b.LevelWeight,
|
||||
ProgressWeight = ProgressWeight ?? b.ProgressWeight,
|
||||
MDealFloor = MDealFloor ?? b.MDealFloor,
|
||||
MDealCeiling = MDealCeiling ?? b.MDealCeiling,
|
||||
MTakeFloor = MTakeFloor ?? b.MTakeFloor,
|
||||
MTakeCeiling = MTakeCeiling ?? b.MTakeCeiling,
|
||||
XpFloor = XpFloor ?? b.XpFloor,
|
||||
XpCeiling = XpCeiling ?? b.XpCeiling,
|
||||
};
|
||||
}
|
||||
|
||||
// XP / level / prestige economy (spec §2). Curve: xp to go from level L to L+1
|
||||
// = LevelXpBase + LevelXpStep * (L-1)^LevelXpExponent. Exponent 1.0 = linear; >1 = progressive (accelerating).
|
||||
public sealed class ProgressionConfig
|
||||
{
|
||||
// XP is earned per-hit on damage to bots (not a flat per-kill lump): final HP damage × rate, plus flat
|
||||
// bonuses for headshots/crits/the kill itself. Each component is run through the same XP multipliers
|
||||
// (Xp-Boost stat × prestige × handicap) at grant time. A clean 100-HP bodyshot kill = 100×Rate + KillBonus.
|
||||
[JsonPropertyName("DamageXpPerHp")] public double DamageXpPerHp { get; set; } = 0.15; // XP per point of final HP damage
|
||||
[JsonPropertyName("KillXpBonus")] public double KillXpBonus { get; set; } = 10; // flat, on the lethal blow (0 = kills give no XP)
|
||||
[JsonPropertyName("HeadshotXpBonus")] public double HeadshotXpBonus { get; set; } = 10; // flat, per headshot hit
|
||||
[JsonPropertyName("CritXpBonus")] public double CritXpBonus { get; set; } = 10; // flat, per crit hit
|
||||
[JsonPropertyName("LevelXpBase")] public long LevelXpBase { get; set; } = 100; // flat cost of level 1 -> 2
|
||||
[JsonPropertyName("LevelXpStep")] public long LevelXpStep { get; set; } = 8; // coefficient on the curve term
|
||||
[JsonPropertyName("LevelXpExponent")] public double LevelXpExponent { get; set; } = 1.7; // curve steepness (more convex = gentler low, steeper high)
|
||||
[JsonPropertyName("LevelCap")] public int LevelCap { get; set; } = 100;
|
||||
[JsonPropertyName("PrestigeCap")] public int PrestigeCap { get; set; } = 10;
|
||||
[JsonPropertyName("PrestigeXpBoostPercent")] public double PrestigeXpBoostPercent { get; set; } = 10; // +% XP per prestige (25 let prestige override the struggler XP penalty)
|
||||
}
|
||||
|
||||
// One survival draft card: a run-scoped buff that maps onto an existing stat key (so EffRun stacks it on top of the
|
||||
// permanent stat, "everything stacks") with a per-pick magnitude in Eff units + a pick cap. Cards are STRONG on
|
||||
// purpose — they must be able to outscale the escalating handicap floor. Bias the magnitudes UP and tune live.
|
||||
public sealed class SurvivalCardDef
|
||||
{
|
||||
[JsonPropertyName("Key")] public string Key { get; set; } = ""; // a StatKeys.* value (damage, crit_damage, max_hp, ...) or a CardKeys.* effect key
|
||||
[JsonPropertyName("Name")] public string Name { get; set; } = ""; // shown on the draft card
|
||||
[JsonPropertyName("PerPick")] public double PerPick { get; set; } // added to Eff per pick (Damage 100 = +100% = x2/pick on top of base)
|
||||
[JsonPropertyName("Cap")] public int Cap { get; set; } = 3; // max times this card can be drafted in a run
|
||||
// ---- draft DISPLAY/COUNTING metadata (data-driven so a new card doesn't need code edits in Survival's switches) ----
|
||||
[JsonPropertyName("IsTeam")] public bool IsTeam { get; set; } // squad-wide team card (shared level via MDeal/MTake) vs per-player
|
||||
[JsonPropertyName("Flat")] public bool Flat { get; set; } // the draft shows "+N" flat points (HP/armor caps, flat regen) instead of "+N%"
|
||||
// Optional draft detail line for EFFECT cards where a raw +N% would mislead. A string.Format template; {0} = the
|
||||
// value after the next pick (level x PerPick). Empty -> the card shows the standard Now%->Next% panel. (The compounding
|
||||
// team cards + Burn carry their own computed detail in code; everything else lives here as data.)
|
||||
[JsonPropertyName("Detail")] public string Detail { get; set; } = "";
|
||||
public SurvivalCardDef() { }
|
||||
public SurvivalCardDef(string key, string name, double perPick, int cap, bool isTeam = false, bool flat = false, string detail = "")
|
||||
{ Key = key; Name = name; PerPick = perPick; Cap = cap; IsTeam = isTeam; Flat = flat; Detail = detail; }
|
||||
}
|
||||
|
||||
// Wave Survival mode (Mode="survival", see Survival.cs). Co-op escalating bot waves on the small arms-race maps.
|
||||
// VANILLA bots (never tougher) — escalation is MORE bots + the inherited handicap floor tightening per wave. The
|
||||
// roguelite DRAFT (strong run-scoped cards) is the counter-pressure; you outscale until the floor + horde win.
|
||||
// Run-XP is a SEPARATE accumulator converted to main XP once at run-end. All values live-tunable via !og_reload.
|
||||
public sealed class SurvivalConfig
|
||||
{
|
||||
// ---- bot population (vanilla HP; only the COUNT escalates) ----
|
||||
[JsonPropertyName("AliveCap")] public int AliveCap { get; set; } = 20; // hard ceiling on bots alive at once (CPU lever; 20 = BotsPerHuman 5 x 4 conceptually)
|
||||
// Bots ALIVE at once this wave = AliveBase + AlivePerWave*(wave-1), clamped to [1, AliveCap]. Separate from the kill
|
||||
// budget: you face this many simultaneously (they respawn as you kill) until the wave's total kills are reached.
|
||||
[JsonPropertyName("AliveBase")] public int AliveBase { get; set; } = 4; // alive at once on wave 1 (gentle start)
|
||||
[JsonPropertyName("AlivePerWave")] public int AlivePerWave { get; set; } = 2; // +alive/wave -> hits AliveCap around wave 9
|
||||
[JsonPropertyName("MaxHumansOnCt")] public int MaxHumansOnCt { get; set; } = 5; // co-op cap (EnforceHumanTeam)
|
||||
// Total kills to clear a wave = BudgetBase + BudgetPerWave*(wave-1) + BudgetPerPlayer*aliveHumansAtWaveStart.
|
||||
[JsonPropertyName("BudgetBase")] public int BudgetBase { get; set; } = 6;
|
||||
[JsonPropertyName("BudgetPerWave")] public int BudgetPerWave { get; set; } = 4;
|
||||
[JsonPropertyName("BudgetPerPlayer")] public int BudgetPerPlayer { get; set; } = 3;
|
||||
|
||||
// ---- wave structure ----
|
||||
[JsonPropertyName("WaveCount")] public int WaveCount { get; set; } = 20; // finite campaign; clearing this = WIN. Tune by feel + XP-vs-TDM/GG.
|
||||
[JsonPropertyName("WaveBreakSeconds")] public float WaveBreakSeconds { get; set; } = 15f;
|
||||
[JsonPropertyName("WaveTimeoutSeconds")] public float WaveTimeoutSeconds { get; set; } = 240f; // no kill for this long (despite nudges) = genuine deadlock -> fail the run (measures time since last KILL, not wall-time)
|
||||
[JsonPropertyName("StallNudgeSeconds")] public float StallNudgeSeconds { get; set; } = 20f; // no kill for this long -> teleport the straggler bots to the squad (so the wave never stalls/ends with bots alive)
|
||||
[JsonPropertyName("StartGraceSeconds")] public float StartGraceSeconds { get; set; } = 6f; // wait this long after a (re)spawn-able map before starting wave 1
|
||||
|
||||
// ---- death / revive ----
|
||||
[JsonPropertyName("ReviveOnWaveClear")] public bool ReviveOnWaveClear { get; set; } = true; // false = hardcore "you die, you spectate the rest of the run"
|
||||
[JsonPropertyName("ReviveHpPercent")] public double ReviveHpPercent { get; set; } = 0.5;
|
||||
|
||||
// ---- XP economy (granted PER WAVE at each wave clear, NOT once at run-end). Each cleared wave grants:
|
||||
// rawWaveXp (HP-damage + HS/crit, handicap-mult EXCLUDED) x prestige x waveMult(wave).
|
||||
// WinMult is the ONLY knob: the FINAL-wave / "win" multiplier. The per-wave multiplier ramps exponentially from x1
|
||||
// (wave 1) to xWinMult (wave WaveCount): waveMult(w) = WinMult^((w-1)/(WaveCount-1)) — early waves pay ~nothing, the
|
||||
// last few pay big. NO per-run cap (depth is the gate: you must survive + contribute to reach the big back multipliers;
|
||||
// an in-progress wave is forfeited on a wipe). Sized so a top CONTRIBUTOR (not a carried player) can climb toward max
|
||||
// level over a DEEP run; tune with research/handicap-balance-calc.py --survival-wave. Default fits the ~50-wave
|
||||
// production target — at the current WaveCount it pays proportionally less (re-solve when you raise WaveCount). ----
|
||||
[JsonPropertyName("WinMult")] public double WinMult { get; set; } = 24.0;
|
||||
|
||||
// ---- the draft ----
|
||||
[JsonPropertyName("DraftSize")] public int DraftSize { get; set; } = 3; // cards offered per pick
|
||||
[JsonPropertyName("CardsPerWave")] public int CardsPerWave { get; set; } = 1; // picks granted on each wave clear
|
||||
|
||||
// ---- effect-card tunables (the new logic cards; all live-tunable) ----
|
||||
[JsonPropertyName("BurnDamagePerSecond")] public double BurnDamagePerSecond { get; set; } = 3; // flat HP/s the Burn card deals (armor-skipping). Different attackers STACK; a re-hit just refreshes your own.
|
||||
[JsonPropertyName("BurnDurationSeconds")] public float BurnDurationSeconds { get; set; } = 5f; // each hit (re)applies this much burn time
|
||||
[JsonPropertyName("BurnTickSeconds")] public float BurnTickSeconds { get; set; } = 1f; // burn damage cadence (DPS held at BurnDamagePerSecond; changing this needs a reload to re-arm the timer)
|
||||
[JsonPropertyName("ExplodeBaseDamage")] public double ExplodeBaseDamage { get; set; } = 100; // Explode-on-Kill HE base damage — then scaled UP by the killer's damage build + handicap on detonation
|
||||
[JsonPropertyName("ExplodeRadius")] public double ExplodeRadius { get; set; } = 350; // Explode-on-Kill HE blast radius (engine distance falloff applies before the scaling)
|
||||
[JsonPropertyName("ExplodeFuseSeconds")] public float ExplodeFuseSeconds { get; set; } = 0.1f; // delay before the corpse grenade detonates (0.1 = near-instant; bump for a telegraphed beep-then-boom)
|
||||
|
||||
// ---- handicap escalation ----
|
||||
// The wave floor reaches max nerf (t=1) at MaxNerfWave (feel-tuned; cards must be able to outscale it). Monotonic.
|
||||
[JsonPropertyName("MaxNerfWave")] public int MaxNerfWave { get; set; } = 25;
|
||||
// Full per-mode handicap override (all bands tunable). Seeded with a higher MTakeCeiling than base (10x) because
|
||||
// cards + permanent stats stack — a maxed-HP/lifesteal/regen/thorns build can be unkillable at 8x. Tune live.
|
||||
// In survival the WAVE FLOOR is the difficulty driver, so the inherited factors are heavily down-weighted —
|
||||
// otherwise a high-level player is pre-nerfed to near-max-take on wave 1 (level/KD maxing the factor before the
|
||||
// floor even ramps). Low weights keep a mild skill component; the floor (-> max nerf at MaxNerfWave) does the work.
|
||||
[JsonPropertyName("Handicap")]
|
||||
public HandicapOverride? Handicap { get; set; } = new()
|
||||
{
|
||||
ProgressWeight = 0.0,
|
||||
MTakeCeiling = 10.0,
|
||||
KdWeight = 0.3,
|
||||
HsWeight = 0.3,
|
||||
StreakWeight = 0.3,
|
||||
LevelWeight = 0.1,
|
||||
MDealFloor = 0.1, // pin: the base dropped to 0.033 for TDM, but waves need a viable deal to be clearable
|
||||
};
|
||||
|
||||
// Map pool (share the GG arms-race maps). EMPTY -> falls back to Match.Maps.
|
||||
[JsonPropertyName("Maps")] public List<string> Maps { get; set; } = new();
|
||||
|
||||
// ---- the card catalog (each maps to a StatKeys.* so EffRun stacks it; STRONG on purpose, tune live) ----
|
||||
[JsonPropertyName("Cards")]
|
||||
public List<SurvivalCardDef> Cards { get; set; } = new()
|
||||
{
|
||||
new("damage", "+Damage", 100, 3), // +100%/pick -> x4 dmg from cards alone, maxed
|
||||
new("hs_damage", "+Headshot Dmg", 50, 3), // +50%/pick -> +150% maxed
|
||||
new("crit_chance", "+Crit Chance", 15, 3),// +15%/pick -> +45% maxed
|
||||
new("crit_damage", "+Crit Dmg", 83, 3), // +83%/pick -> ~+250% maxed
|
||||
new("max_hp", "+Max HP", 40, 3, flat: true), // +40 HP/pick -> +120 maxed (flat points, not %)
|
||||
new("max_armor", "+Max Armor", 40, 3, flat: true),
|
||||
new("lifesteal", "+Lifesteal", 8, 3), // +8%/pick -> +24% maxed
|
||||
new("hp_regen", "+HP Regen", 2, 3, flat: true), // FLAT +2 HP/s/pick -> +6 HP/s maxed (stacks on the always-on regen stat)
|
||||
|
||||
// ---- effect-logic cards (NEW keys, NOT StatKeys; the effect hooks read them via StatBonus, never folded into EffRun) ----
|
||||
// 1-pick (Cap 1): a single draft fully unlocks the effect; PerPick on the two FLAG cards is just a presence marker
|
||||
// (their magnitude comes from the Burn*/Explode* knobs above), the other two carry their whole value in one pick.
|
||||
// Detail = the draft line ({0} = level x PerPick); Burn + the two team cards carry computed detail in code.
|
||||
new("explode_kill", "Explode on Kill", 1, 1, detail: "HE blast on kill"), // kill a bot -> real HE blast on the corpse (scales with your dmg build); chains freely
|
||||
new("burn", "Burn", 1, 1), // every hit ignites the bot: flat BurnDamagePerSecond HP/s, armor-skipping, per-attacker stacking
|
||||
new("ability_cdr", "-Ability Cooldown", 30, 1, detail: "-{0:0}% ability cooldown"),
|
||||
new("xp_mult", "+Run XP", 50, 1, detail: "+{0:0}% run XP"),
|
||||
// Leveled (Cap 3): per-player, scale per pick.
|
||||
new("hs_reduction", "Headshot Armor", 15, 3, detail: "-{0:0}% headshot dmg"), // armor doesn't help vs HS; this does
|
||||
new("berserk_passive", "Berserker", 40, 3, detail: "+{0:0}% dmg near death"), // scaled by missing HP (always-on, no streak gate)
|
||||
// Leveled (Cap 3) but TEAM-WIDE: one shared squad level (max 3), compounding, applied to EVERY survivor via MDeal/MTake.
|
||||
new("global_deal", "Team: +Damage", 10, 3, isTeam: true), // +10%/level ALL outgoing dmg, compounding -> x1.331 squad-wide maxed
|
||||
new("global_take", "Team: -Damage Taken", 10, 3, isTeam: true),// -10%/level ALL incoming dmg, compounding -> x0.729 squad-wide maxed
|
||||
};
|
||||
}
|
||||
|
||||
// One tunable stat: Base value at 0 invested levels + PerLevel increment, up to MaxLevel.
|
||||
public sealed class StatDef
|
||||
{
|
||||
[JsonPropertyName("MaxLevel")] public int MaxLevel { get; set; }
|
||||
[JsonPropertyName("PerLevel")] public double PerLevel { get; set; }
|
||||
[JsonPropertyName("Base")] public double Base { get; set; }
|
||||
|
||||
public StatDef() { }
|
||||
public StatDef(int maxLevel, double perLevel, double @base = 0) { MaxLevel = maxLevel; PerLevel = perLevel; Base = @base; }
|
||||
}
|
||||
|
||||
// The passive stat tree (spec §3). All values tunable; max levels sum to 100. Pure POCO (StatDef + primitives) so it lives
|
||||
// here with the other Domain-read configs (the resolvers index it via _statDefs) — CSSharp-free, test-shareable.
|
||||
public sealed class StatsConfig
|
||||
{
|
||||
[JsonPropertyName("Damage")] public StatDef Damage { get; set; } = new(5, 10); // +10% dmg / lvl -> +50% (x1.5) maxed
|
||||
[JsonPropertyName("CritChance")] public StatDef CritChance { get; set; } = new(20, 2.5); // +2.5% chance / lvl -> 50% maxed (slow ramp, de-rushed)
|
||||
[JsonPropertyName("CritDamage")] public StatDef CritDamage { get; set; } = new(10, 10, 100); // base +100% (x2), +10%/lvl -> +200% (x3) maxed
|
||||
[JsonPropertyName("HeadshotDamage")] public StatDef HeadshotDamage { get; set; } = new(5, 10); // +10% / lvl -> +50% (x1.5) on top of engine 4x
|
||||
[JsonPropertyName("MaxHp")] public StatDef MaxHp { get; set; } = new(10, 25); // +25 hp / lvl (over base 100)
|
||||
[JsonPropertyName("MaxArmor")] public StatDef MaxArmor { get; set; } = new(10, 25);
|
||||
[JsonPropertyName("Lifesteal")] public StatDef Lifesteal { get; set; } = new(10, 2.5); // +2.5% of dmg as HP / lvl
|
||||
[JsonPropertyName("ArmorLifesteal")] public StatDef ArmorLifesteal { get; set; } = new(10, 2);
|
||||
[JsonPropertyName("HpRegen")] public StatDef HpRegen { get; set; } = new(5, 1); // FLAT +1 HP / regen-tick(=1s) / lvl -> 5 HP/s maxed. ALWAYS ON (no out-of-combat gate).
|
||||
[JsonPropertyName("ArmorRegen")] public StatDef ArmorRegen { get; set; } = new(5, 1); // FLAT +1 armor / regen-tick(=1s) / lvl. ALWAYS ON.
|
||||
[JsonPropertyName("Thorns")] public StatDef Thorns { get; set; } = new(5, 2); // reflect +2% of damage taken / lvl -> 10% maxed (dealt FLAT back at the bot — a straight % of the hit you took, no build/handicap scaling; hard-capped 25 HP/hit; OFF while a knife/zeus is held so a GG melee finale needs a real kill)
|
||||
[JsonPropertyName("XpBoost")] public StatDef XpBoost { get; set; } = new(5, 20); // +20% / lvl -> +100% (x2) maxed
|
||||
|
||||
[JsonPropertyName("RegenIntervalSeconds")] public float RegenIntervalSeconds { get; set; } = 1f;
|
||||
[JsonPropertyName("RegenNoDamageDelaySeconds")] public float RegenNoDamageDelaySeconds { get; set; } = 5f; // DEPRECATED — regen is now ALWAYS ON (flat). Kept so existing JSON doesn't error; no longer read.
|
||||
|
||||
[JsonPropertyName("CritLifestealMultiplier")] public double CritLifestealMultiplier { get; set; } = 1.5; // crit hits steal +50%
|
||||
[JsonPropertyName("LifestealMinHeal")] public int LifestealMinHeal { get; set; } = 2; // floor on a lifesteal heal (HP)
|
||||
}
|
||||
360
Outnumbered/Config/OutnumberedConfig.cs
Normal file
360
Outnumbered/Config/OutnumberedConfig.cs
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
using System.Text.Json.Serialization;
|
||||
using CounterStrikeSharp.API.Core;
|
||||
|
||||
namespace Outnumbered.Config;
|
||||
|
||||
// Auto-loaded from configs/plugins/outnumbered/outnumbered.json on first load.
|
||||
// NOTE: the config sub-types the pure Domain layer reads (AbilityDef/AbilitiesConfig/HandicapConfig/HandicapOverride/
|
||||
// ProgressionConfig/SurvivalCardDef/SurvivalConfig/StatDef) live in Config/DomainConfig.cs — split out so they're
|
||||
// CounterStrikeSharp-free and the test project can compile them with Domain/. This file holds OutnumberedConfig (which
|
||||
// extends the CSSharp BasePluginConfig) + the engine-coupled / presentation blocks.
|
||||
public sealed class OutnumberedConfig : BasePluginConfig
|
||||
{
|
||||
// Which match driver runs this instance: "tdm" (default), "gungame", or "survival" (Driver.NormalizeMode also accepts
|
||||
// 0/1/2 and aliases gg/wave). A per-instance launch decision — the whole RPG core is shared; only the match ruleset
|
||||
// changes. Source of truth for the accepted values: Driver.ModeRegistry + NormalizeMode. (Mode-driver split, see Modes.cs.)
|
||||
[JsonPropertyName("Mode")]
|
||||
public string Mode { get; set; } = "tdm";
|
||||
|
||||
[JsonPropertyName("Database")]
|
||||
public DatabaseConfig Database { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("Api")]
|
||||
public ApiConfig Api { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("Website")]
|
||||
public WebsiteConfig Website { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("Match")]
|
||||
public MatchConfig Match { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("GunGame")]
|
||||
public GunGameConfig GunGame { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("Survival")]
|
||||
public SurvivalConfig Survival { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("Stats")]
|
||||
public StatsConfig Stats { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("Progression")]
|
||||
public ProgressionConfig Progression { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("Handicap")]
|
||||
public HandicapConfig Handicap { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("Abilities")]
|
||||
public AbilitiesConfig Abilities { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("Hud")]
|
||||
public HudConfig Hud { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("Shop")]
|
||||
public ShopConfig Shop { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("Sounds")]
|
||||
public SoundsConfig Sounds { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("FlushIntervalSeconds")]
|
||||
public float FlushIntervalSeconds { get; set; } = 90f;
|
||||
}
|
||||
|
||||
// Per-player sound cues (spec §7), played via the client `play` command. Paths are built-in CS2 sounds
|
||||
// (same style GG2 uses). Empty string = no cue. Swap freely; `play <path>` resolves under csgo/.
|
||||
public sealed class SoundsConfig
|
||||
{
|
||||
[JsonPropertyName("Enabled")] public bool Enabled { get; set; } = true;
|
||||
[JsonPropertyName("LevelUp")] public string LevelUp { get; set; } = "sounds/ui/armsrace_level_up.wav";
|
||||
[JsonPropertyName("Prestige")] public string Prestige { get; set; } = "sounds/ui/armsrace_level_up.wav";
|
||||
[JsonPropertyName("AbilityReady")] public string AbilityReady { get; set; } = "sounds/ambient/office/tech_oneshot_08.wav";
|
||||
[JsonPropertyName("AbilityActivate")] public string AbilityActivate { get; set; } = "sounds/training/pointscored.wav";
|
||||
[JsonPropertyName("Crit")] public string Crit { get; set; } = ""; // off by default — per-crit can get spammy
|
||||
}
|
||||
|
||||
// The local read-only status/balance/top API, served over a per-instance Unix domain socket (Api.cs). The companion
|
||||
// website is the consumer; there is no network exposure — the socket is a filesystem path, access = file permissions.
|
||||
public sealed class ApiConfig
|
||||
{
|
||||
[JsonPropertyName("Enabled")] public bool Enabled { get; set; } = true;
|
||||
// Directory holding the per-instance sockets (<SocketDir>/<ServerId>.sock). Created externally (systemd tmpfiles.d);
|
||||
// when it's missing or unwritable the API disables itself with one log line instead of failing the plugin load.
|
||||
[JsonPropertyName("SocketDir")] public string SocketDir { get; set; } = "/run/outnumbered";
|
||||
}
|
||||
|
||||
// The companion-website plug: a periodic chat line + an !about line pointing players at the site. Url empty = the
|
||||
// whole feature is OFF (the default — an operator without a site gets no dangling advert). Url is read live per
|
||||
// announce (!og_reload applies); changing the interval needs a plugin reload to re-arm the timer.
|
||||
public sealed class WebsiteConfig
|
||||
{
|
||||
[JsonPropertyName("Url")] public string Url { get; set; } = "";
|
||||
[JsonPropertyName("AnnounceIntervalSeconds")] public float AnnounceIntervalSeconds { get; set; } = 300f;
|
||||
}
|
||||
|
||||
// The always-on HUD (spec §7): handicap multipliers + streak + the 5 ability states.
|
||||
public sealed class HudConfig
|
||||
{
|
||||
[JsonPropertyName("Enabled")] public bool Enabled { get; set; } = true;
|
||||
// "center" = center HTML panel (crisp, multi-color, 2D, no lag — fixed upper-center + width). DEFAULT.
|
||||
// "world" = CPointWorldText entity (positionable/wide/sized, but single-color, 3D parallax, lags on movement
|
||||
// since the viewmodel isn't accessible to parent to — kept as an option, not recommended).
|
||||
[JsonPropertyName("Mode")] public string Mode { get; set; } = "center";
|
||||
// Refresh cadence in ticks. Center HTML fades only after ~5s, so it does NOT need per-tick resends: 4 (=16 Hz at
|
||||
// 64 tick) keeps countdowns smooth at a quarter of the per-human message + string-build cost of 1. Every refresh
|
||||
// sends a reliable usermessage per human, so this dial is also the HUD's client/network footprint.
|
||||
[JsonPropertyName("RefreshEveryTicks")] public int RefreshEveryTicks { get; set; } = 4;
|
||||
|
||||
// Ability icons for the center HUD (indexed 0..4: No Reload, Adrenaline, Overcharge, Bloodthirst, Berserk).
|
||||
// Glyph rendering depends on the client font — if any show as a box, swap it here and `css_plugins reload`.
|
||||
// Defaults kept in BMP symbol blocks that render on the CS2 panel font (astral emoji like 🔥💧 show as boxes).
|
||||
[JsonPropertyName("AbilityIcons")]
|
||||
public List<string> AbilityIcons { get; set; } = new()
|
||||
{ "∞", "⛨", "⇈", "♥", "☠" };
|
||||
|
||||
// ---- world-text placement — LIVE-TUNABLE: edit JSON, then `css_plugins reload outnumbered` (no rebuild) ----
|
||||
[JsonPropertyName("ForwardOffset")] public float ForwardOffset { get; set; } = 7f; // units in front of the eye (keep small so walls don't clip it)
|
||||
[JsonPropertyName("RightOffset")] public float RightOffset { get; set; } = 0f; // + = right, - = left
|
||||
[JsonPropertyName("UpOffset")] public float UpOffset { get; set; } = -2.6f; // - = lower on screen
|
||||
[JsonPropertyName("FontSize")] public float FontSize { get; set; } = 26f;
|
||||
[JsonPropertyName("WorldUnitsPerPx")] public float WorldUnitsPerPx { get; set; } = 0.0075f; // smaller = smaller/finer text
|
||||
[JsonPropertyName("FontName")] public string FontName { get; set; } = "Arial Bold";
|
||||
[JsonPropertyName("DrawBackground")] public bool DrawBackground { get; set; } = true;
|
||||
[JsonPropertyName("ColorR")] public int ColorR { get; set; } = 255;
|
||||
[JsonPropertyName("ColorG")] public int ColorG { get; set; } = 255;
|
||||
[JsonPropertyName("ColorB")] public int ColorB { get; set; } = 255;
|
||||
}
|
||||
|
||||
// The quick-buy shop world-text panel. Opened by switching to the healthshot (X), frozen while open.
|
||||
// Placement is LIVE-TUNABLE: edit JSON, then `css_plugins reload outnumbered` (no rebuild) to reposition.
|
||||
public sealed class ShopConfig
|
||||
{
|
||||
[JsonPropertyName("Enabled")] public bool Enabled { get; set; } = true;
|
||||
[JsonPropertyName("ForwardOffset")] public float ForwardOffset { get; set; } = 7f; // units in front of the eye
|
||||
[JsonPropertyName("RightOffset")] public float RightOffset { get; set; } = 0f; // centre anchor: + = right, - = left
|
||||
[JsonPropertyName("SplitOffset")] public float SplitOffset { get; set; } = 3f; // half-gap: left panel sits -this, right panel +this
|
||||
[JsonPropertyName("CardSpread")] public float CardSpread { get; set; } = 6f; // survival draft: horizontal gap between the 3 cards (centre card at RightOffset)
|
||||
[JsonPropertyName("UpOffset")] public float UpOffset { get; set; } = 0f; // + = higher, - = lower
|
||||
[JsonPropertyName("FontSize")] public float FontSize { get; set; } = 40f;
|
||||
[JsonPropertyName("WorldUnitsPerPx")] public float WorldUnitsPerPx { get; set; } = 0.011f; // smaller = finer
|
||||
[JsonPropertyName("InfoFontSize")] public float InfoFontSize { get; set; } = 34f; // right (info) panel: smaller than shop, but readable
|
||||
[JsonPropertyName("InfoWorldUnitsPerPx")] public float InfoWorldUnitsPerPx { get; set; } = 0.0085f;
|
||||
// Server/about text shown atop the info panel (the !info / !about blurb) — edit per-server. Keep lines SHORT
|
||||
// (this panel is narrow/tall by design); long lines run off-screen at lower resolutions.
|
||||
[JsonPropertyName("InfoLines")]
|
||||
public List<string> InfoLines { get; set; } = new()
|
||||
{
|
||||
"=== OUTNUMBERED ===",
|
||||
"Kill bots -> XP -> level.",
|
||||
"Spend points in Skills.",
|
||||
"L100: !prestige resets you",
|
||||
"for a permanent boost.",
|
||||
"Streaks unlock abilities",
|
||||
"(grenade keys 6-0).",
|
||||
"Handicap scales bots",
|
||||
"to your power.",
|
||||
"",
|
||||
};
|
||||
[JsonPropertyName("FontName")] public string FontName { get; set; } = "Arial Bold";
|
||||
[JsonPropertyName("DrawBackground")] public bool DrawBackground { get; set; } = true;
|
||||
[JsonPropertyName("ColorR")] public int ColorR { get; set; } = 255;
|
||||
[JsonPropertyName("ColorG")] public int ColorG { get; set; } = 255;
|
||||
[JsonPropertyName("ColorB")] public int ColorB { get; set; } = 255;
|
||||
}
|
||||
|
||||
// Separate file outnumbered.ranks.json (NOT auto-loaded by CSSharp — loaded manually, see Ranks.cs). Spec §9.
|
||||
public sealed class RanksConfig
|
||||
{
|
||||
[JsonPropertyName("Enabled")] public bool Enabled { get; set; } = true;
|
||||
[JsonPropertyName("ShowClanTag")] public bool ShowClanTag { get; set; } = true; // scoreboard tag via m_szClan
|
||||
[JsonPropertyName("ChatTag")] public bool ChatTag { get; set; } = true; // coloured rank tag in chat (reformats chat)
|
||||
// Prestige tag colouring: I-V = the unlocked perk's tint; VI-VII 2-colour, VIII-IX 3-colour, X all-5 (HUD animated, chat static).
|
||||
[JsonPropertyName("PrestigeColors")] public bool PrestigeColors { get; set; } = true;
|
||||
[JsonPropertyName("TopCount")] public int TopCount { get; set; } = 10; // !top size
|
||||
// {0} = Roman prestige; only shown when prestige > 0. e.g. "P {0}" -> "P III".
|
||||
[JsonPropertyName("PrestigeTagFormat")] public string PrestigeTagFormat { get; set; } = "P{0}";
|
||||
// Highest MinLevel that is <= the player's level wins. Levels are within a prestige (cap 100).
|
||||
[JsonPropertyName("LevelRanks")]
|
||||
public List<RankTier> LevelRanks { get; set; } = new()
|
||||
{
|
||||
new(1, "Recruit"), new(26, "Soldier"), new(51, "Veteran"), new(76, "Elite"), new(100, "Apex"),
|
||||
};
|
||||
}
|
||||
|
||||
public sealed class RankTier
|
||||
{
|
||||
[JsonPropertyName("MinLevel")] public int MinLevel { get; set; }
|
||||
[JsonPropertyName("Name")] public string Name { get; set; } = "";
|
||||
public RankTier() { }
|
||||
public RankTier(int minLevel, string name) { MinLevel = minLevel; Name = name; }
|
||||
}
|
||||
|
||||
public sealed class DatabaseConfig
|
||||
{
|
||||
// "sqlite" for dev; "postgres" is the final-step swap (no logic change — repository interface).
|
||||
[JsonPropertyName("Provider")]
|
||||
public string Provider { get; set; } = "sqlite";
|
||||
|
||||
// Relative to csgo/ . Defaults next to the plugin's own config.
|
||||
[JsonPropertyName("SqliteFile")]
|
||||
public string SqliteFile { get; set; } = "addons/counterstrikesharp/configs/plugins/outnumbered/outnumbered.db";
|
||||
|
||||
// Unused while Provider == "sqlite"; filled in for the Postgres phase.
|
||||
[JsonPropertyName("PostgresConnectionString")]
|
||||
public string PostgresConnectionString { get; set; } = "Host=localhost;Port=5432;Database=outnumbered;Username=outnumbered;Password=";
|
||||
}
|
||||
|
||||
// The TDM match driver (spec §1). Humans on CT vs bots on T, continuous respawn, no objectives.
|
||||
public sealed class MatchConfig
|
||||
{
|
||||
[JsonPropertyName("BotsPerHuman")] public int BotsPerHuman { get; set; } = 4;
|
||||
[JsonPropertyName("MaxBots")] public int MaxBots { get; set; } = 12;
|
||||
[JsonPropertyName("MaxHumansOnCt")] public int MaxHumansOnCt { get; set; } = 3;
|
||||
[JsonPropertyName("RespawnDelaySeconds")] public float RespawnDelaySeconds { get; set; } = 1.0f; // DEPRECATED — native DM respawn handles timing; no longer read (kept so existing JSON doesn't error).
|
||||
// 5s so you can spend skill points on respawn; it breaks the instant you fire, so it's not abusable (PvE anyway).
|
||||
[JsonPropertyName("SpawnProtectionSeconds")] public float SpawnProtectionSeconds { get; set; } = 5.0f;
|
||||
|
||||
// Map ends when ANY single player reaches this many kills (spec §1).
|
||||
[JsonPropertyName("KillGoal")] public int KillGoal { get; set; } = 250;
|
||||
[JsonPropertyName("MapChangeDelaySeconds")] public float MapChangeDelaySeconds { get; set; } = 6.0f;
|
||||
[JsonPropertyName("Maps")]
|
||||
public List<string> Maps { get; set; } = new()
|
||||
{ "cs_italy", "de_dust2", "de_vertigo", "de_nuke", "de_inferno" };
|
||||
|
||||
// bot_difficulty caps at 4 on this build (+ custom_bot_difficulty) — cookbook.
|
||||
[JsonPropertyName("BotDifficulty")] public int BotDifficulty { get; set; } = 4;
|
||||
|
||||
// Base loadout = the only weapons free from level 1; everything else is level-gated (WeaponUnlockLevel).
|
||||
// Prestige resets level → re-locks everything back to these. Persisted per-player choice falls back here when locked.
|
||||
[JsonPropertyName("HumanPrimary")] public string HumanPrimary { get; set; } = "weapon_mp9";
|
||||
[JsonPropertyName("HumanSecondary")] public string HumanSecondary { get; set; } = "weapon_usp_silencer";
|
||||
|
||||
// weapon -> level required to select it (absent/0 = free). Weak/cheap early, the cheese (AK/AWP/Negev/autos) late.
|
||||
[JsonPropertyName("WeaponUnlockLevel")]
|
||||
public Dictionary<string, int> WeaponUnlockLevel { get; set; } = new()
|
||||
{
|
||||
["weapon_glock"] = 2,
|
||||
["weapon_p250"] = 4,
|
||||
["weapon_mac10"] = 6,
|
||||
["weapon_nova"] = 8,
|
||||
["weapon_hkp2000"] = 10,
|
||||
["weapon_bizon"] = 12,
|
||||
["weapon_mp7"] = 14,
|
||||
["weapon_galilar"] = 16,
|
||||
["weapon_sawedoff"] = 18,
|
||||
["weapon_elite"] = 20,
|
||||
["weapon_famas"] = 22,
|
||||
["weapon_ump45"] = 24,
|
||||
["weapon_fiveseven"] = 27,
|
||||
["weapon_tec9"] = 30,
|
||||
["weapon_mp5sd"] = 33,
|
||||
["weapon_mag7"] = 36,
|
||||
["weapon_ssg08"] = 39,
|
||||
["weapon_p90"] = 42,
|
||||
["weapon_aug"] = 45,
|
||||
["weapon_cz75a"] = 48,
|
||||
["weapon_xm1014"] = 51,
|
||||
["weapon_sg556"] = 55,
|
||||
["weapon_deagle"] = 59,
|
||||
["weapon_revolver"] = 63,
|
||||
["weapon_m4a1"] = 67,
|
||||
["weapon_m4a1_silencer"] = 71,
|
||||
["weapon_ak47"] = 75,
|
||||
["weapon_m249"] = 80,
|
||||
["weapon_scar20"] = 85,
|
||||
["weapon_g3sg1"] = 90,
|
||||
["weapon_negev"] = 95,
|
||||
["weapon_awp"] = 100,
|
||||
};
|
||||
// Free-selection weapon menu (!guns), split into categories (!rifles/!smgs/!snipers/!shotguns/!heavy/!pistols).
|
||||
// Edit to taste; entries must be valid weapon_ item names. "Pistols" are the secondary slot; the rest are primary.
|
||||
[JsonPropertyName("Rifles")]
|
||||
public List<string> Rifles { get; set; } = new()
|
||||
{ "weapon_ak47", "weapon_m4a1_silencer", "weapon_m4a1", "weapon_aug", "weapon_sg556", "weapon_galilar", "weapon_famas" };
|
||||
[JsonPropertyName("Smgs")]
|
||||
public List<string> Smgs { get; set; } = new()
|
||||
{ "weapon_mp9", "weapon_mp7", "weapon_mp5sd", "weapon_ump45", "weapon_p90", "weapon_mac10", "weapon_bizon" };
|
||||
[JsonPropertyName("Snipers")]
|
||||
public List<string> Snipers { get; set; } = new()
|
||||
{ "weapon_awp", "weapon_ssg08", "weapon_scar20", "weapon_g3sg1" };
|
||||
[JsonPropertyName("Shotguns")]
|
||||
public List<string> Shotguns { get; set; } = new()
|
||||
{ "weapon_nova", "weapon_xm1014", "weapon_mag7", "weapon_sawedoff" };
|
||||
[JsonPropertyName("Heavy")]
|
||||
public List<string> Heavy { get; set; } = new()
|
||||
{ "weapon_m249", "weapon_negev" };
|
||||
[JsonPropertyName("Pistols")]
|
||||
public List<string> Pistols { get; set; } = new()
|
||||
{ "weapon_deagle", "weapon_revolver", "weapon_glock", "weapon_usp_silencer", "weapon_hkp2000",
|
||||
"weapon_p250", "weapon_fiveseven", "weapon_tec9", "weapon_cz75a", "weapon_elite" };
|
||||
// Bot squad pool — cycled per bot spawn for a mixed T side (full squad templates come later).
|
||||
[JsonPropertyName("BotWeapons")]
|
||||
public List<string> BotWeapons { get; set; } = new()
|
||||
{ "weapon_ak47", "weapon_ak47", "weapon_awp", "weapon_nova" };
|
||||
[JsonPropertyName("BotGrenades")]
|
||||
public List<string> BotGrenades { get; set; } = new()
|
||||
{ "weapon_hegrenade" };
|
||||
}
|
||||
|
||||
// One Gun Game ladder rung: a weapon + its strength tier (1=weak/hard-to-use .. 5=cheese). The tier indexes the
|
||||
// per-tier kill curves below — it does NOT affect the climb ORDER (that's this list's order, shared by players+bots).
|
||||
public sealed class GunGameRung
|
||||
{
|
||||
[JsonPropertyName("Weapon")] public string Weapon { get; set; } = "";
|
||||
[JsonPropertyName("Tier")] public int Tier { get; set; } = 1;
|
||||
public GunGameRung() { }
|
||||
public GunGameRung(string weapon, int tier) { Weapon = weapon; Tier = tier; }
|
||||
}
|
||||
|
||||
// Gun Game mode (Mode="gungame", see Modes.cs). Climb a weapon ladder by kills; the full RPG kit rides along.
|
||||
// DUAL pacing: kills-to-advance a rung is driven by the weapon's TIER, and players vs bots use INVERSE curves —
|
||||
// players grind weak guns + breeze strong ones (so an HS-demotion into the grind stings); bots skip weak guns
|
||||
// fast + linger on strong ones (so they reach dangerous weapons instead of being stuck on pistols). The ORDER is
|
||||
// shared (bots have no muscle memory) and zig-zags weapon types so muscle memory never settles.
|
||||
public sealed class GunGameConfig
|
||||
{
|
||||
// Kills to clear a rung, indexed by tier-1 (tier 1..5). The player curve grinds weak guns / breezes strong ones.
|
||||
// Bots default to a FLAT 1 kill/rung — with BotSharedLadder (per-batch pooling) a batch then needs ~ladder-length
|
||||
// collective kills to win, which only lands once a player's K/D falls into handicap territory.
|
||||
[JsonPropertyName("PlayerKillsByTier")] public List<int> PlayerKillsByTier { get; set; } = new() { 10, 8, 5, 2, 1 };
|
||||
[JsonPropertyName("BotKillsByTier")] public List<int> BotKillsByTier { get; set; } = new() { 1, 1, 1, 1, 1 };
|
||||
|
||||
// Pool bot ladder progress per BATCH of BotsPerHuman (true, default): bots are grouped by slot rank into batches
|
||||
// of BotsPerHuman, and each batch shares ONE rung that advances on ANY of its members' kills — so a batch (≈ one
|
||||
// player's worth of bots) climbs ~BotsPerHuman× faster and can actually win, and the horde scales with player
|
||||
// count (1 player -> 1 batch, 2 -> 2, ... up to MaxBots). false = each bot climbs its own rung (rarely tops).
|
||||
[JsonPropertyName("BotSharedLadder")] public bool BotSharedLadder { get; set; } = true;
|
||||
|
||||
// Max humans on CT for THIS mode (GG wants more than the "outnumbered" TDM default of 3). Per-mode so ONE shared
|
||||
// config can run both; the driver feeds it to EnforceHumanTeam.
|
||||
[JsonPropertyName("MaxHumansOnCt")] public int MaxHumansOnCt { get; set; } = 5;
|
||||
|
||||
// Append a final knife rung — the classic Gun Game capstone (the win is a knife kill; 1 kill for both sides).
|
||||
[JsonPropertyName("KnifeFinale")] public bool KnifeFinale { get; set; } = true;
|
||||
|
||||
// Map pool for this mode (changelevel rotation on a win). EMPTY = fall back to Match.Maps. Put the small
|
||||
// arms-race maps here once you've confirmed the exact names installed on your server (e.g. ar_shoots, ar_baggage).
|
||||
[JsonPropertyName("Maps")] public List<string> Maps { get; set; } = new();
|
||||
|
||||
// Per-mode handicap override (null = inherit the base Handicap unchanged). GG: weights the ladder-position
|
||||
// progress axis (climbing nerfs you; a bot-demotion eases it), runs a rougher sub-1 Curve (bites from the start),
|
||||
// and DOUBLES MasterDifficulty so the nerf cap (0.1x deal / 8x taken) is actually reachable on small GG maps —
|
||||
// it's hit at n≈0.5 (half-maxed factors) instead of needing K/D+HS+streak+level+ladder ALL maxed, so a good
|
||||
// climber gets driven to the floor and can no longer headshot-suppress the horde. All live-tunable via !og_reload.
|
||||
// (Heads-up: MasterDifficulty scales both sides, so a struggling low-K/D player also gets a stronger comeback buff.)
|
||||
[JsonPropertyName("Handicap")] public HandicapOverride? Handicap { get; set; } = new() { MasterDifficulty = 2.0, ProgressWeight = 3.0, Curve = 0.8, MDealFloor = 0.1 };
|
||||
|
||||
// The ladder: weapon order (shared) + per-weapon tier. EMPTY -> code falls back to a knife-only ladder (safety).
|
||||
// Order zig-zags categories (no two consecutive the same; cheese scattered, not clustered).
|
||||
[JsonPropertyName("Ladder")]
|
||||
public List<GunGameRung> Ladder { get; set; } = new()
|
||||
{
|
||||
new("weapon_usp_silencer", 1), new("weapon_famas", 4), new("weapon_mac10", 2), new("weapon_mag7", 4),
|
||||
new("weapon_aug", 5), new("weapon_fiveseven", 3), new("weapon_ump45", 4), new("weapon_scar20", 5),
|
||||
new("weapon_nova", 3), new("weapon_m4a1", 5), new("weapon_p250", 2), new("weapon_mp5sd", 4),
|
||||
new("weapon_ssg08", 4), new("weapon_galilar", 4), new("weapon_mp7", 3), new("weapon_xm1014", 4),
|
||||
new("weapon_deagle", 5), new("weapon_bizon", 2), new("weapon_sg556", 5), new("weapon_tec9", 4),
|
||||
new("weapon_m249", 4), new("weapon_mp9", 1), new("weapon_g3sg1", 5), new("weapon_sawedoff", 4),
|
||||
new("weapon_revolver", 4), new("weapon_m4a1_silencer", 5), new("weapon_p90", 3), new("weapon_negev", 4),
|
||||
new("weapon_elite", 4), new("weapon_ak47", 5), new("weapon_hkp2000", 1), new("weapon_awp", 5),
|
||||
};
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue