initial commit
This commit is contained in:
commit
d701598350
67 changed files with 9351 additions and 0 deletions
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