initial commit

This commit is contained in:
Kamal Tufekcic 2026-07-05 12:14:39 +03:00
commit 2d966b8198
28 changed files with 2901 additions and 0 deletions

217
Pages/Theory.cshtml Normal file
View file

@ -0,0 +1,217 @@
@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>
}