@wenathlan/stealthhead 0.0.0-stage → 2.0.117
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -2
- package/docs/.gitkeep +0 -0
- package/package.json +161 -4
- package/tests/.gitkeep +0 -0
- package/tests/design.test.ts +160 -0
- package/tests/family.test.ts +81 -0
- package/tests/familyurl.test.ts +25 -0
- package/tests/hud.test.ts +204 -0
- package/tests/matchrules.test.ts +219 -0
- package/tests/netcode.test.ts +251 -0
- package/tests/onboardingstate.test.ts +62 -0
- package/tests/physics.test.ts +185 -0
- package/tests/rankingladder.test.ts +162 -0
- package/tests/spawner.test.ts +210 -0
- package/tests/weaponstats.test.ts +148 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// # weaponstats.test — honest unit tests for the armory terminal ballistics
|
|
2
|
+
// (falloff, shots, ttk, balance), runnable with the vitest runner:
|
|
3
|
+
// pnpm test
|
|
4
|
+
// The falloff specs and balance bounds arrive as parameters — the tests pass
|
|
5
|
+
// their own catalog config, exactly as a season would.
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { describe, it } from "vitest";
|
|
8
|
+
import { WEAPONCATALOG, type Weapon } from "../weapons.ts";
|
|
9
|
+
import {
|
|
10
|
+
damageatmeters,
|
|
11
|
+
type FalloffSpec,
|
|
12
|
+
shotstokill,
|
|
13
|
+
timetokill,
|
|
14
|
+
ttkbydistance,
|
|
15
|
+
validatebalance,
|
|
16
|
+
weaponstatserror,
|
|
17
|
+
} from "../weaponstats.ts";
|
|
18
|
+
|
|
19
|
+
/** the catalog falloff: full damage to 20 m, 60% at 60 m, linear. */
|
|
20
|
+
const LINEAR: FalloffSpec = { startmeters: 20, endmeters: 60, retainfraction: 0.6, curve: 1 };
|
|
21
|
+
|
|
22
|
+
/** the competitor style quadratic falloff (claude-of-duty curve). */
|
|
23
|
+
const QUADRATIC: FalloffSpec = { startmeters: 20, endmeters: 60, retainfraction: 0.6, curve: 2 };
|
|
24
|
+
|
|
25
|
+
/** the standard target health model of the tests. */
|
|
26
|
+
const HEALTH = 100;
|
|
27
|
+
|
|
28
|
+
/** finds one catalog row by id. */
|
|
29
|
+
function weapon(id: string): Weapon {
|
|
30
|
+
const row = WEAPONCATALOG.find((candidate) => candidate.id === id);
|
|
31
|
+
assert.ok(row, `catalog weapon ${id} missing`);
|
|
32
|
+
return row;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("weapon stats damage falloff", () => {
|
|
36
|
+
const rifle = weapon("w-1"); // 28 damage
|
|
37
|
+
|
|
38
|
+
it("keeps the full damage up to the falloff start", () => {
|
|
39
|
+
assert.equal(damageatmeters(rifle, 0, LINEAR), 28);
|
|
40
|
+
assert.equal(damageatmeters(rifle, 20, LINEAR), 28);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("settles at the retained fraction past the end", () => {
|
|
44
|
+
assert.equal(damageatmeters(rifle, 60, LINEAR), 28 * 0.6);
|
|
45
|
+
assert.equal(damageatmeters(rifle, 200, LINEAR), 28 * 0.6);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("fades linearly between start and end", () => {
|
|
49
|
+
const mid = damageatmeters(rifle, 40, LINEAR); // t 0.5
|
|
50
|
+
assert.ok(Math.abs(mid - 28 * (1 - 0.4 * 0.5)) < 1e-9, `expected 22.4, got ${mid}`);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("bends the fade with the curve exponent", () => {
|
|
54
|
+
const linear = damageatmeters(rifle, 30, LINEAR);
|
|
55
|
+
const quad = damageatmeters(rifle, 30, QUADRATIC);
|
|
56
|
+
assert.ok(quad > linear, "a quadratic curve keeps more damage near the start");
|
|
57
|
+
assert.ok(Math.abs(linear - 28 * (1 - 0.4 * 0.25)) < 1e-9, `expected 25.2, got ${linear}`);
|
|
58
|
+
assert.ok(
|
|
59
|
+
Math.abs(quad - 28 * (1 - 0.4 * 0.25 ** 2)) < 1e-9,
|
|
60
|
+
`expected ${28 * (1 - 0.4 * 0.25 ** 2)}, got ${quad}`,
|
|
61
|
+
);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("refuses a bad distance, a bad spec and a bad row", () => {
|
|
65
|
+
assert.throws(() => damageatmeters(rifle, -1, LINEAR), weaponstatserror);
|
|
66
|
+
assert.throws(
|
|
67
|
+
() => damageatmeters(rifle, 10, { ...LINEAR, endmeters: 10 }),
|
|
68
|
+
(error: unknown) => {
|
|
69
|
+
assert.ok(error instanceof weaponstatserror);
|
|
70
|
+
assert.equal(error.code, "bad-falloff");
|
|
71
|
+
return true;
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
assert.throws(() => damageatmeters({ ...rifle, damage: 0 }, 10, LINEAR), weaponstatserror);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe("weapon stats shots and ttk", () => {
|
|
79
|
+
const rifle = weapon("w-1"); // 28 dmg, 660 rpm, 30 mag, 2.1 s reload
|
|
80
|
+
|
|
81
|
+
it("asks more shots as the distance fades the damage", () => {
|
|
82
|
+
const close = shotstokill(rifle, 10, HEALTH, LINEAR);
|
|
83
|
+
const mid = shotstokill(rifle, 40, HEALTH, LINEAR);
|
|
84
|
+
const far = shotstokill(rifle, 200, HEALTH, LINEAR);
|
|
85
|
+
assert.equal(close, 4); // 100 / 28
|
|
86
|
+
assert.ok(mid > close && far > mid, `expected the shots to grow, got ${close}/${mid}/${far}`);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("counts the headshot multiplier before the division", () => {
|
|
90
|
+
const body = shotstokill(rifle, 10, HEALTH, LINEAR, { vitalmultiplier: 1 });
|
|
91
|
+
const head = shotstokill(rifle, 10, HEALTH, LINEAR, { vitalmultiplier: 2 });
|
|
92
|
+
assert.equal(head, Math.ceil(body / 2));
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("computes the ttk from the inter shot gaps and never mutates the row", () => {
|
|
96
|
+
const result = timetokill(rifle, 10, HEALTH, LINEAR);
|
|
97
|
+
assert.equal(result.shots, 4);
|
|
98
|
+
assert.equal(result.reloads, 0);
|
|
99
|
+
const gap = 60 / 660;
|
|
100
|
+
assert.ok(Math.abs(result.seconds - 3 * gap) < 0.0005, `expected ${3 * gap}, got ${result.seconds}`);
|
|
101
|
+
assert.equal(rifle.damage, 28);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("charges one reload when the burst outgrows the magazine", () => {
|
|
105
|
+
const shotgun = weapon("w-4"); // 96 dmg, 70 rpm, 6 mag, 2.8 s reload
|
|
106
|
+
const result = timetokill(shotgun, 10, 600, LINEAR); // 600 hp asks 7 shots
|
|
107
|
+
assert.equal(result.shots, 7);
|
|
108
|
+
assert.equal(result.reloads, 1);
|
|
109
|
+
const gap = 60 / 70;
|
|
110
|
+
assert.ok(Math.abs(result.seconds - (6 * gap + 2.8)) < 0.0005, `expected ${6 * gap + 2.8}, got ${result.seconds}`);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("sweeps a distance ladder in order for the armory curve", () => {
|
|
114
|
+
const curve = ttkbydistance(rifle, [10, 40, 120], HEALTH, LINEAR);
|
|
115
|
+
assert.equal(curve.length, 3);
|
|
116
|
+
assert.ok(curve[0].seconds < curve[1].seconds && curve[1].seconds < curve[2].seconds);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("refuses a bad health pool", () => {
|
|
120
|
+
assert.throws(
|
|
121
|
+
() => shotstokill(rifle, 10, 0, LINEAR),
|
|
122
|
+
(error: unknown) => {
|
|
123
|
+
assert.ok(error instanceof weaponstatserror);
|
|
124
|
+
assert.equal(error.code, "bad-health");
|
|
125
|
+
return true;
|
|
126
|
+
},
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
describe("weapon stats balance validation", () => {
|
|
132
|
+
it("accepts every catalog row inside the season bounds", () => {
|
|
133
|
+
const bounds = { damage: { min: 10, max: 100 }, firerate: { min: 60, max: 1000 }, recoil: { min: 10, max: 90 } };
|
|
134
|
+
const violations = WEAPONCATALOG.flatMap((row) => validatebalance(row, bounds));
|
|
135
|
+
assert.equal(violations.length, 0);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("reports each field that leaves the configured bounds", () => {
|
|
139
|
+
const bounds = { damage: { min: 20, max: 80 }, reloadseconds: { min: 1, max: 3 } };
|
|
140
|
+
const violations = validatebalance(weapon("w-2"), bounds); // 88 dmg, 3.2 s reload
|
|
141
|
+
assert.equal(violations.length, 2);
|
|
142
|
+
assert.deepEqual(violations.map((violation) => violation.field).sort(), ["damage", "reloadseconds"]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("skips the fields without bounds and passes an empty bounds table", () => {
|
|
146
|
+
assert.deepEqual(validatebalance(weapon("w-4"), {}), []);
|
|
147
|
+
});
|
|
148
|
+
});
|