cs2-web/Pages/Theory.cshtml
Kamal Tufekcic 2d966b8198 initial commit
2026-07-05 12:14:39 +03:00

217 lines
11 KiB
Text
Raw Permalink 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>
}
<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>
<p class="muted small">
Weapon-by-weapon time-to-kill tables and per-weapon damage simulation are planned on top of this.
</p>
<script type="application/json" id="sim-data">@Html.Raw(Model.SimJson)</script>
}
@section Scripts {
<script src="/js/theory.js" defer></script>
}