cs2-web/Pages/Theory.cshtml
2026-07-07 20:23:39 +03:00

293 lines
15 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@page "/theory"
@using CsWeb.Services
@model TheoryModel
@{
ViewData["Title"] = "Theorycrafting";
var b = Model.B;
}
<h1>Theorycrafting</h1>
<p>
Numbers below are <strong>live</strong> — 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 <strong>editable</strong>: change any knob to explore; <em>Reset</em> returns to server values.
</p>
<p class="muted small">
Showing values for <strong>@Fmt.ModeName(b?.Mode)</strong> —
view for <a href="/theory?mode=tdm">TDM</a> · <a href="/theory?mode=gungame">Gun Game</a> · <a href="/theory?mode=survival">Survival</a>
(modes can override handicap bands).
</p>
@if (b is null)
{
<div class="notice err">No server reachable right now — theory needs a live config to be honest. Try again shortly.</div>
}
else
{
@if (Model.ModeMismatch)
{
<div class="notice">No @Fmt.ModeName(Model.ModeParam) server is reachable right now — showing <strong>@Fmt.ModeName(b.Mode)</strong> values (the only live config available).</div>
}
@if (Model.Balance is { Online: false })
{
<div class="notice">Servers offline — showing the last known config (@Fmt.Age(Model.Balance!)).</div>
}
<h2>The handicap</h2>
<p>
One signed index <em>t</em> summarizes how dominant you are (K/D, headshot rate, killstreak, level, and the
mode's progress axis, weighted and eased). <em>t</em>&nbsp;=&nbsp;0 is neutral; +1 is fully dominant;
1 is struggling. All three multipliers are driven by the same <em>t</em>, so they hit their extremes together:
dominate and you deal less, take more, and level faster — all at once.
</p>
@if (Model.Panels.Count > 0)
{
<figure id="curves-fig">
<div class="readout" id="readout">
<label>t <input type="range" id="tslider" min="-1" max="1" step="0.01" value="0" /></label>
<span id="tval">t = 0.00</span>
@foreach (var p in Model.Panels)
{
<span><i class="dot" style="background:@p.Color"></i> <span id="ro-@p.Key">×1.00</span></span>
}
<span class="muted small">dashed green line = your simulated player</span>
</div>
@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);
<svg class="panel" viewBox="0 0 @TheoryModel.W @height" data-key="@p.Key" role="img" aria-label="@p.Title versus t">
<text class="ptitle" x="@TheoryModel.L" y="@(TheoryModel.T - 1)">@p.Title</text>
<line class="grid" x1="@TheoryModel.L" y1="@Model.YFor(p, 0)" x2="@(TheoryModel.W - TheoryModel.R)" y2="@Model.YFor(p, 0)" />
<line class="grid" x1="@TheoryModel.L" y1="@Model.YFor(p, p.YMax)" x2="@(TheoryModel.W - TheoryModel.R)" y2="@Model.YFor(p, p.YMax)" />
<line class="grid one" x1="@TheoryModel.L" y1="@p.OneY" x2="@(TheoryModel.W - TheoryModel.R)" y2="@p.OneY" />
<text class="tick ymax" x="@(TheoryModel.L - 5)" y="@(Model.YFor(p, p.YMax) + 4)">@p.YMax.ToString("0.#")</text>
<text class="tick" x="@(TheoryModel.L - 5)" y="@(p.OneY + 4)">×1</text>
<text class="tick" x="@(TheoryModel.L - 5)" y="@(Model.YFor(p, 0) + 4)">0</text>
<polyline class="series" points="@p.Points" style="stroke:@p.Color" />
<line class="simt" x1="@(TheoryModel.L + Model.PlotW / 2.0)" y1="@TheoryModel.T" x2="@(TheoryModel.L + Model.PlotW / 2.0)" y2="@(TheoryModel.T + TheoryModel.PlotH)" />
<line class="cross" x1="@(TheoryModel.L + Model.PlotW / 2.0)" y1="@TheoryModel.T" x2="@(TheoryModel.L + Model.PlotW / 2.0)" y2="@(TheoryModel.T + TheoryModel.PlotH)" />
@if (last)
{
@* SVG <text> 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($"<text class=\"tick mid\" x=\"{x:0.#}\" y=\"{TheoryModel.T + TheoryModel.PlotH + 16}\">{tv:0.#}</text>")
}
}
</svg>
}
<figcaption class="muted small">← struggling &nbsp;·&nbsp; t &nbsp;·&nbsp; dominant → &nbsp; (drag the slider or hover the chart; curves follow your edits below)</figcaption>
</figure>
}
<div class="notice err" id="sim-drift" hidden>
The simulator's local math no longer matches the server-computed curves (site/mod version drift).
The charts and key points shown are still server truth, but <strong>edited-knob results may be inaccurate</strong>
until the site is updated. Details in the browser console.
</div>
<h2 id="sim">Simulator</h2>
<p class="small muted">
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.
</p>
<div class="sim">
<div class="simout" id="simout">
<span><span class="lbl">index</span><b id="sim-t">0.00</b></span>
<span><span class="lbl">deal band</span><b id="sim-deal">×1.00</b></span>
<span><span class="lbl">take band</span><b id="sim-take">×1.00</b></span>
<span><span class="lbl">xp band</span><b id="sim-xpband">×1.00</b></span>
<span><span class="lbl">Out × (with stats)</span><b id="sim-out">×1.00</b></span>
<span><span class="lbl">In ×</span><b id="sim-in">×1.00</b></span>
<span><span class="lbl">HS ×</span><b id="sim-hs">×1.00</b></span>
<span><span class="lbl">XP × (total)</span><b id="sim-xp">×1.00</b></span>
<button class="btn" id="sim-reset" type="button">Reset to server</button>
</div>
<fieldset>
<legend>Player state</legend>
<div class="fields">
<label>Level <input type="number" data-sim="level" min="0" max="100" value="0" /></label>
<label>Prestige <input type="number" data-sim="prestige" min="0" value="0" /></label>
<label>Kills <input type="number" data-sim="kills" min="0" value="0" /></label>
<label>Deaths <input type="number" data-sim="deaths" min="0" value="0" /></label>
<label>HS kills <input type="number" data-sim="hsk" min="0" value="0" /></label>
<label>Streak <input type="number" data-sim="streak" min="0" value="0" /></label>
<label>Mode progress (0-1) <input type="number" data-sim="progress" min="0" max="1" step="0.05" value="0" /></label>
<label>Escalation floor t <input type="number" data-sim="floor" min="-1" max="1" step="0.05" value="-1" /></label>
</div>
</fieldset>
<div class="cols">
<fieldset>
<legend>Stat tree (invested levels)</legend>
<div class="tablewrap">
<table>
<tr><th>Stat</th><th class="num">Base</th><th class="num">Per lvl</th><th class="num">Invested</th><th class="num">Effective</th></tr>
@foreach (var s in Model.StatRows())
{
<tr>
<td>@s.Name</td>
<td class="num">@s.Base</td>
<td class="num">@s.PerLevel</td>
<td class="num"><input type="number" data-stat="@s.Key" min="0" max="@s.MaxLevel" value="0" title="0-@s.MaxLevel" aria-label="@s.Name invested levels" /></td>
<td class="num" id="eff-@s.Key">@s.Base</td>
</tr>
}
</table>
</div>
</fieldset>
<div>
<fieldset>
<legend>Survival cards (run-scoped picks)</legend>
<div class="tablewrap">
<table>
<tr><th>Card</th><th class="num">Per pick</th><th class="num">Picks</th></tr>
@foreach (var c in Model.CardRows())
{
dynamic card = c;
<tr>
<td>@card.Name @(card.IsTeam ? Html.Raw("<span class=\"badge\">team</span>") : Html.Raw(""))</td>
<td class="num">@card.PerPick</td>
<td class="num"><input type="number" data-card="@card.Key" data-team="@(card.IsTeam ? 1 : 0)" data-perpick="@card.PerPick" min="0" max="@card.Cap" value="0" title="0-@card.Cap" aria-label="@card.Name picks" /></td>
</tr>
}
</table>
</div>
</fieldset>
<fieldset>
<legend>XP</legend>
<div class="fields">
<label>Prestige boost %/prestige <input type="number" data-sim="prestigeBoost" step="1" value="@Model.PrestigeBoostPercent" /></label>
</div>
</fieldset>
</div>
</div>
<fieldset>
<legend>Handicap knobs (@Fmt.ModeName(b.Mode) effective)</legend>
<div class="kgroups">
@foreach (var (group, rows) in Model.HandicapGroups())
{
<section class="kgroup">
<h4>@group</h4>
@foreach (var r in rows)
{
if (r.IsBool)
{
<label>@r.Name <input type="checkbox" data-h="@r.Name" checked="@(r.Num != 0)" /></label>
}
else
{
<label>@r.Name <input type="number" data-h="@r.Name" step="@TheoryModel.StepFor(r.Num)" value="@r.Num.ToString("0.###", System.Globalization.CultureInfo.InvariantCulture)" /></label>
}
}
</section>
}
</div>
</fieldset>
</div>
<h3>Key points (server values)</h3>
<div class="tablewrap">
<table>
<tr><th class="num">t</th><th class="num">Deal ×</th><th class="num">Take ×</th><th class="num">XP ×</th></tr>
@foreach (var r in Model.KeyRows())
{
<tr>
<td class="num">@r.T.ToString("0.0")</td>
<td class="num">@r.Deal.ToString("0.00")</td>
<td class="num">@r.Take.ToString("0.00")</td>
<td class="num">@r.Xp.ToString("0.00")</td>
</tr>
}
</table>
</div>
<script type="application/json" id="sim-data">@Html.Raw(Model.SimJson)</script>
}
<h2 id="weapons">Weapons — shots to kill</h2>
@if (Model.Weap is null)
{
<div class="notice">Weapon data hasn't been generated on this host yet (deploy-web/extract-weapons.sh).</div>
}
else
{
<p>
Vanilla CS2 bullet math, computed from numbers extracted out of the <strong>installed game's own files</strong>
(re-extracted on every Valve update — never hand-copied). Shots to kill a <strong>100 HP</strong> 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 <em>apply my build &amp; handicap</em> to layer your
simulated player from above onto the same math.
</p>
<div class="notice err" id="wpn-drift" hidden>
This table's local math no longer matches the server-computed values (site version drift). The numbers shown
are still server truth, but <strong>the controls below may compute inaccurate results</strong> until the site
is updated. Details in the browser console.
</div>
<div class="sim">
<fieldset>
<legend>Target &amp; range</legend>
<div class="fields">
<label>Distance <input type="range" id="wpn-dist" min="0" max="3000" step="25" value="0" autocomplete="off" /> <b id="wpn-distv">0 u</b></label>
<label>Kevlar + helmet <input type="checkbox" id="wpn-armor" checked autocomplete="off" /></label>
<label title="Out × and HS × from the simulator above, versus a bot (100 HP, assault suit — always armored). Crits and abilities stay excluded.">
Apply my build &amp; handicap <input type="checkbox" id="wpn-mod" autocomplete="off" />
</label>
</div>
<p class="muted small">500 units ≈ one falloff step · a &mdash; cell means the weapon's bullets don't reach that distance at all (shotguns cut off earliest) · AWP mid ≈ 1500&ndash;2500 units.</p>
</fieldset>
</div>
<div class="tablewrap">
<table id="wpn-table">
<tr>
<th>Weapon</th>
<th class="num" title="damage per shot into the chest at the chosen distance/armor">Body dmg</th>
<th class="num">Head</th><th class="num">Chest</th><th class="num">Stomach</th><th class="num">Legs</th>
<th class="num" title="(shots 1) × cycle time, chest — the first shot lands at t = 0">TTK</th>
</tr>
@foreach (var (label, rows) in Model.WeaponGroups())
{
<tr class="wcat"><th colspan="7">@label</th></tr>
foreach (var w in rows)
{
var shot = WeaponMath.Hitgroups.Select(hg => WeaponMath.Shot(w, 0, hg, true)).ToArray();
<tr data-wid="@w.Id">
<td>@Html.Raw(Model.IconFor(w))@w.Name @(w.Pellets > 1 ? Html.Raw($"<span class=\"muted small\">&times;{w.Pellets}</span>") : Html.Raw(""))</td>
<td class="num" data-c="dmg">@shot[1]</td>
<td class="num" data-c="head">@TheoryModel.StkStr(shot[0])</td>
<td class="num" data-c="chest">@TheoryModel.StkStr(shot[1])</td>
<td class="num" data-c="stomach">@TheoryModel.StkStr(shot[2])</td>
<td class="num" data-c="legs">@TheoryModel.StkStr(shot[3])</td>
<td class="num" data-c="ttk">@TheoryModel.TtkStr(w, shot[1])</td>
</tr>
}
}
</table>
</div>
<p class="muted small">
From CS2 build @Model.Weap.GameVersion (extracted @(Model.Weap.GeneratedAt is { Length: >= 10 } g ? g[..10] : Model.Weap.GeneratedAt)).
&ldquo;Kevlar + helmet&rdquo; armors every hitgroup except legs — legs never take armor reduction.
Shotgun rows (&times;pellets) assume every pellet lands in the same hitgroup.
Damage is truncated per pellet, exactly like the engine does it.
</p>
<script type="application/json" id="weapons-data">@Html.Raw(Model.WeaponsJson)</script>
}
@section Scripts {
<script src="/js/theory.js" defer></script>
<script src="/js/weapons.js" defer></script>
}