minor optimizations and weapon sim
This commit is contained in:
parent
2d966b8198
commit
fc7887ce11
10 changed files with 419 additions and 11 deletions
|
|
@ -83,6 +83,12 @@ else
|
|||
</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 —
|
||||
|
|
@ -205,13 +211,83 @@ else
|
|||
</table>
|
||||
</div>
|
||||
|
||||
<p class="muted small">
|
||||
Weapon-by-weapon time-to-kill tables and per-weapon damage simulation are planned on top of this.
|
||||
<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 & handicap</em> to layer your
|
||||
simulated player from above onto the same math.
|
||||
</p>
|
||||
|
||||
<script type="application/json" id="sim-data">@Html.Raw(Model.SimJson)</script>
|
||||
<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 & 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 & handicap <input type="checkbox" id="wpn-mod" autocomplete="off" />
|
||||
</label>
|
||||
</div>
|
||||
<p class="muted small">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.</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\">×{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)).
|
||||
“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.
|
||||
</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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using CsWeb.Services;
|
||||
|
|
@ -6,7 +7,7 @@ using Microsoft.AspNetCore.Mvc.RazorPages;
|
|||
|
||||
namespace CsWeb.Pages;
|
||||
|
||||
public class TheoryModel(Fleet fleet) : PageModel
|
||||
public class TheoryModel(Fleet fleet, Weapons weapons) : PageModel
|
||||
{
|
||||
// Panel geometry in viewBox units (SVG scales to container width).
|
||||
public const int W = 720, PlotH = 134, L = 46, R = 10, T = 10;
|
||||
|
|
@ -48,6 +49,7 @@ public class TheoryModel(Fleet fleet) : PageModel
|
|||
public async Task OnGetAsync(string? mode)
|
||||
{
|
||||
ModeParam = mode;
|
||||
BuildWeapons(); // file-backed, independent of the sockets — the STK table renders even with the fleet down
|
||||
(_, Balance) = await fleet.BalanceForMode(mode ?? "");
|
||||
var c = B?.Curves;
|
||||
if (c is null || c.T.Length < 2) return;
|
||||
|
|
@ -167,4 +169,58 @@ public class TheoryModel(Fleet fleet) : PageModel
|
|||
|
||||
// Spinner step by magnitude, so Curve 0.8 steps by 0.05 instead of the browser default 1 (typing stays free-form).
|
||||
public static string StepFor(double v) => Math.Abs(v) < 1 ? "0.05" : Math.Abs(v) < 10 ? "0.1" : "1";
|
||||
|
||||
// ---- weapons / STK table (numbers from extract-weapons.sh via the Weapons store; math in WeaponMath) ----
|
||||
|
||||
public WeaponsDoc? Weap { get; private set; }
|
||||
public string WeaponsJson { get; private set; } = "null";
|
||||
|
||||
private void BuildWeapons()
|
||||
{
|
||||
Weap = weapons.Doc;
|
||||
if (Weap is null) return;
|
||||
WeaponsJson = JsonSerializer.Serialize(new
|
||||
{
|
||||
Weap.GameVersion,
|
||||
Weap.Weapons,
|
||||
// Parity target: the default table state (distance 0, armored, vanilla) that weapons.js recomputes at
|
||||
// load. Distance 0 keeps pow() out of the picture (x^0 = 1 exactly), so with the same operation order
|
||||
// C# and JS produce bit-identical doubles and the comparison is exact-integer, no tolerance needed.
|
||||
Ssr = Weap.Weapons.Select(w => new
|
||||
{
|
||||
w.Id,
|
||||
Shot = WeaponMath.Hitgroups.Select(hg => WeaponMath.Shot(w, 0, hg, true)).ToArray(),
|
||||
}).ToList(),
|
||||
});
|
||||
}
|
||||
|
||||
private static readonly (string Key, string Label)[] CatOrder =
|
||||
[("rifles", "Rifles"), ("pistols", "Pistols"), ("smgs", "SMGs"), ("snipers", "Snipers"), ("shotguns", "Shotguns"), ("heavy", "Heavy")];
|
||||
|
||||
public IEnumerable<(string Label, List<WeaponRow> Rows)> WeaponGroups()
|
||||
{
|
||||
if (Weap is null) yield break;
|
||||
foreach (var (key, label) in CatOrder)
|
||||
{
|
||||
var rows = Weap.Weapons.Where(w => w.Cat == key).OrderBy(w => w.Name, StringComparer.Ordinal).ToList();
|
||||
if (rows.Count > 0) yield return (label, rows);
|
||||
}
|
||||
var known = CatOrder.Select(c => c.Key).ToHashSet();
|
||||
var other = Weap.Weapons.Where(w => !known.Contains(w.Cat)).OrderBy(w => w.Name, StringComparer.Ordinal).ToList();
|
||||
if (other.Count > 0) yield return ("Other", other); // a future extractor category still gets rendered
|
||||
}
|
||||
|
||||
// Inline the extractor's cleaned SVG with img semantics — role + aria-label carry the weapon name (the "alt").
|
||||
public string IconFor(WeaponRow w)
|
||||
{
|
||||
var svg = weapons.IconSvg(w.Icon);
|
||||
if (svg is null) return "";
|
||||
int i = svg.IndexOf("<svg", StringComparison.Ordinal);
|
||||
return i < 0 ? "" : svg.Insert(i + 4, $" class=\"wicon\" role=\"img\" aria-label=\"{WebUtility.HtmlEncode(w.Name)}\"");
|
||||
}
|
||||
|
||||
public static string StkStr(int shot) => WeaponMath.Stk(shot) is var s && s > 0 ? s.ToString(CultureInfo.InvariantCulture) : "—";
|
||||
|
||||
public static string TtkStr(WeaponRow w, int shot) =>
|
||||
WeaponMath.Stk(shot) is var s && s > 0 ? WeaponMath.Ttk(w, s).ToString("0.00", CultureInfo.InvariantCulture) + " s" : "—";
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue