@page "/theory" @using CsWeb.Services @model TheoryModel @{ ViewData["Title"] = "Theorycrafting"; var b = Model.B; }

Theorycrafting

Numbers below are live — read from the running servers' effective config, including any hot-reloaded balance tuning. The curves are computed by the same compiled code that scales damage in game. Everything is also editable: change any knob to explore; Reset returns to server values.

Showing values for @Fmt.ModeName(b?.Mode) — view for TDM · Gun Game · Survival (modes can override handicap bands).

@if (b is null) {
No server reachable right now — theory needs a live config to be honest. Try again shortly.
} else { @if (Model.ModeMismatch) {
No @Fmt.ModeName(Model.ModeParam) server is reachable right now — showing @Fmt.ModeName(b.Mode) values (the only live config available).
} @if (Model.Balance is { Online: false }) {
Servers offline — showing the last known config (@Fmt.Age(Model.Balance!)).
}

The handicap

One signed index t summarizes how dominant you are (K/D, headshot rate, killstreak, level, and the mode's progress axis, weighted and eased). t = 0 is neutral; +1 is fully dominant; −1 is struggling. All three multipliers are driven by the same t, so they hit their extremes together: dominate and you deal less, take more, and level faster — all at once.

@if (Model.Panels.Count > 0) {
t = 0.00 @foreach (var p in Model.Panels) { ×1.00 } dashed green line = your simulated player
@foreach (var (p, i) in Model.Panels.Select((p, i) => (p, i))) { var last = i == Model.Panels.Count - 1; var height = TheoryModel.T + TheoryModel.PlotH + (last ? 26 : 8); @p.Title @p.YMax.ToString("0.#") ×1 0 @if (last) { @* SVG collides with Razor's literal pseudo-tag inside code blocks -> raw emit *@ foreach (var tv in new[] { -1.0, -0.5, 0.0, 0.5, 1.0 }) { var x = TheoryModel.L + (tv + 1) / 2 * Model.PlotW; @Html.Raw($"{tv:0.#}") } } }
← struggling  ·  t  ·  dominant →   (drag the slider or hover the chart; curves follow your edits below)
}

Simulator

Seeded from the live server config. Edit anything — player state, stat levels, card picks, handicap knobs — and every readout and curve recomputes. Abilities, crits and headshots are excluded (parity with the in-game HUD's base readout). Edited fields get an amber outline.

index0.00 deal band×1.00 take band×1.00 xp band×1.00 Out × (with stats)×1.00 In ××1.00 HS ××1.00 XP × (total)×1.00
Player state
Stat tree (invested levels)
@foreach (var s in Model.StatRows()) { }
StatBasePer lvlInvestedEffective
@s.Name @s.Base @s.PerLevel @s.Base
Survival cards (run-scoped picks)
@foreach (var c in Model.CardRows()) { dynamic card = c; }
CardPer pickPicks
@card.Name @(card.IsTeam ? Html.Raw("team") : Html.Raw("")) @card.PerPick
XP
Handicap knobs (@Fmt.ModeName(b.Mode) effective)
@foreach (var (group, rows) in Model.HandicapGroups()) {

@group

@foreach (var r in rows) { if (r.IsBool) { } else { } }
}

Key points (server values)

@foreach (var r in Model.KeyRows()) { }
tDeal ×Take ×XP ×
@r.T.ToString("0.0") @r.Deal.ToString("0.00") @r.Take.ToString("0.00") @r.Xp.ToString("0.00")
}

Weapons — shots to kill

@if (Model.Weap is null) {
Weapon data hasn't been generated on this host yet (deploy-web/extract-weapons.sh).
} else {

Vanilla CS2 bullet math, computed from numbers extracted out of the installed game's own files (re-extracted on every Valve update — never hand-copied). Shots to kill a 100 HP target per hitgroup, with distance falloff, per-weapon armor penetration and the per-weapon headshot multipliers (the Deagle's ×3.9 and M4A1-S's ×3.475 included). Tick apply my build & handicap to layer your simulated player from above onto the same math.

Target & range

500 units ≈ one falloff step · a — cell means the weapon's bullets don't reach that distance at all (shotguns cut off earliest) · AWP mid ≈ 1500–2500 units.

@foreach (var (label, rows) in Model.WeaponGroups()) { foreach (var w in rows) { var shot = WeaponMath.Hitgroups.Select(hg => WeaponMath.Shot(w, 0, hg, true)).ToArray(); } }
Weapon Body dmg HeadChestStomachLegs TTK
@label
@Html.Raw(Model.IconFor(w))@w.Name @(w.Pellets > 1 ? Html.Raw($"×{w.Pellets}") : Html.Raw("")) @shot[1] @TheoryModel.StkStr(shot[0]) @TheoryModel.StkStr(shot[1]) @TheoryModel.StkStr(shot[2]) @TheoryModel.StkStr(shot[3]) @TheoryModel.TtkStr(w, shot[1])

From CS2 build @Model.Weap.GameVersion (extracted @(Model.Weap.GeneratedAt is { Length: >= 10 } g ? g[..10] : Model.Weap.GeneratedAt)). “Kevlar + helmet” armors every hitgroup except legs — legs never take armor reduction. Shotgun rows (×pellets) assume every pellet lands in the same hitgroup. Damage is truncated per pellet, exactly like the engine does it.

} @section Scripts { }