minor optimizations and weapon sim

This commit is contained in:
Kamal Tufekcic 2026-07-07 20:23:39 +03:00
commit fc7887ce11
10 changed files with 419 additions and 11 deletions

View file

@ -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" : "—";
}