minor optimizations and weapon sim
This commit is contained in:
parent
2d966b8198
commit
fc7887ce11
10 changed files with 419 additions and 11 deletions
113
Services/Weapons.cs
Normal file
113
Services/Weapons.cs
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
using System.Text.Json;
|
||||
|
||||
namespace CsWeb.Services;
|
||||
|
||||
// ---- weapons.json DTOs (produced by deploy-web/extract-weapons.sh from the installed game's files) ----
|
||||
|
||||
public sealed class WeaponRow
|
||||
{
|
||||
public string Id { get; init; } = "";
|
||||
public string Name { get; init; } = "";
|
||||
public string Cat { get; init; } = "";
|
||||
public int Dmg { get; init; }
|
||||
public double ArmorRatio { get; init; }
|
||||
public double RangeMod { get; init; }
|
||||
public double HsMult { get; init; }
|
||||
public double Range { get; init; }
|
||||
public double Cycle { get; init; }
|
||||
public int Clip { get; init; }
|
||||
public int Pellets { get; init; }
|
||||
public string Icon { get; init; } = "";
|
||||
}
|
||||
|
||||
public sealed class WeaponsDoc
|
||||
{
|
||||
public int V { get; init; }
|
||||
public string GeneratedAt { get; init; } = "";
|
||||
public string GameVersion { get; init; } = "";
|
||||
public List<WeaponRow> Weapons { get; init; } = [];
|
||||
}
|
||||
|
||||
// File-backed twin of Fleet's socket caches: weapons.json + icons re-read at TTL pace with last-good retention,
|
||||
// so an extractor run mid-request (or a briefly missing file during its swap) never blanks the table.
|
||||
public sealed class Weapons(IConfiguration cfg, IWebHostEnvironment env, ILogger<Weapons> log)
|
||||
{
|
||||
private static readonly TimeSpan Ttl = TimeSpan.FromSeconds(60);
|
||||
private static readonly JsonSerializerOptions JsonOpts = new() { PropertyNameCaseInsensitive = true };
|
||||
|
||||
// Relative dir = dev convenience (resolved against the project root); prod config uses the absolute path.
|
||||
private readonly string _dir = Path.IsPathRooted(cfg["Outnumbered:WeaponsDir"] ?? "")
|
||||
? cfg["Outnumbered:WeaponsDir"]!
|
||||
: Path.Combine(env.ContentRootPath, cfg["Outnumbered:WeaponsDir"] ?? "weapons-data");
|
||||
|
||||
private readonly object _lock = new();
|
||||
private WeaponsDoc? _doc;
|
||||
private Dictionary<string, string> _icons = [];
|
||||
private DateTimeOffset _lastAttempt = DateTimeOffset.MinValue;
|
||||
|
||||
public WeaponsDoc? Doc
|
||||
{
|
||||
get { Refresh(); lock (_lock) return _doc; }
|
||||
}
|
||||
|
||||
public string? IconSvg(string icon)
|
||||
{
|
||||
Refresh();
|
||||
lock (_lock) return _icons.GetValueOrDefault(icon);
|
||||
}
|
||||
|
||||
private void Refresh()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (DateTimeOffset.UtcNow - _lastAttempt < Ttl) return;
|
||||
_lastAttempt = DateTimeOffset.UtcNow; // stamped before the read: a missing dir is retried at TTL pace
|
||||
}
|
||||
try
|
||||
{
|
||||
var doc = JsonSerializer.Deserialize<WeaponsDoc>(File.ReadAllText(Path.Combine(_dir, "weapons.json")), JsonOpts);
|
||||
if (doc is null || doc.Weapons.Count == 0) return;
|
||||
var icons = new Dictionary<string, string>();
|
||||
foreach (var icon in doc.Weapons.Select(w => w.Icon))
|
||||
{
|
||||
var p = Path.Combine(_dir, "icons", icon + ".svg");
|
||||
if (File.Exists(p)) icons[icon] = File.ReadAllText(p);
|
||||
}
|
||||
lock (_lock) { _doc = doc; _icons = icons; }
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log.LogWarning(ex, "weapons data read failed from {Dir} (keeping last-good)", _dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vanilla CS2 bullet math. Per-weapon numbers come from weapons.json; the four constants below live in compiled
|
||||
// engine code (no data file carries them) and are validated against anchor damage numbers (AK-47 helmet HS = 111,
|
||||
// USP-S = 70, Glock = 56, AWP armored legs = 86) — wwwroot/js/weapons.js mirrors this class function-for-function,
|
||||
// with THE SAME operation order so the load-time parity check compares bit-identical doubles.
|
||||
public static class WeaponMath
|
||||
{
|
||||
// Hitgroup order used everywhere (SSR cells, parity blob, JS): head, chest, stomach, legs.
|
||||
public static readonly string[] Hitgroups = ["head", "chest", "stomach", "legs"];
|
||||
|
||||
// Raw (unfloored) per-pellet damage. Armor gates: legs never take armor reduction; "armored" means
|
||||
// kevlar + helmet together (the head is armored only via helmet — one toggle covers the full buy).
|
||||
public static double Pellet(WeaponRow w, double dist, string hg, bool armored)
|
||||
{
|
||||
if (dist > w.Range) return 0; // beyond the weapon's own max bullet range nothing connects at all
|
||||
double d = w.Dmg * Math.Pow(w.RangeMod, dist / 500.0);
|
||||
d *= hg switch { "head" => w.HsMult, "stomach" => 1.25, "legs" => 0.75, _ => 1.0 };
|
||||
if (armored && hg != "legs") d *= w.ArmorRatio * 0.5;
|
||||
return d;
|
||||
}
|
||||
|
||||
// Damage is floored per pellet at application (each pellet is its own trace); a "shot" assumes every pellet
|
||||
// lands in the same hitgroup at the same distance — the standard convention for shotgun table numbers.
|
||||
public static int Shot(WeaponRow w, double dist, string hg, bool armored) =>
|
||||
(int)Math.Floor(Pellet(w, dist, hg, armored)) * w.Pellets;
|
||||
|
||||
public static int Stk(int shot) => shot <= 0 ? 0 : (int)Math.Ceiling(100.0 / shot); // 0 = can't kill (out of range)
|
||||
|
||||
public static double Ttk(WeaponRow w, int stk) => stk <= 0 ? 0 : (stk - 1) * w.Cycle; // first shot at t=0
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue