217 lines
11 KiB
Text
217 lines
11 KiB
Text
@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> = 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 · t · dominant → (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>
|
||
}
|