quill/val Public Public: anyone can read this; nobody can change it.

forked from benjivers/val

workbenchthis machine
Files fc929c52f70a5ac50a58539dd3a4f5f52f763122
260 lines · 10.3 KB · ~3.0k tok Raw History
1
import { expect, test } from "bun:test";
2
import {
3
  AGENTS,
4
  agentById,
5
  buyableSlots,
6
  cast,
7
  castable,
8
  facingFlash,
9
  FLASH_CONE,
10
  FLASH_EDGE,
11
  FLASH_GRACE,
12
  flashDuration,
13
  flashReach,
14
  sightBlocked,
15
  SLOTS,
16
  type Ability,
17
} from "./abilities.ts";
18
import { HEIGHT, WIDTH } from "./map.ts";
19
import { newMatch, startRound, tick } from "./match.ts";
20
import type { MatchState, Player, Zone } from "./state.ts";
21
import { FOV } from "../static/engine/raycast.js";
22
23
const flashes = (): Ability[] => AGENTS.flatMap((a) => SLOTS.map((s) => a.abilities[s])).filter((a) => a.kind === "flash");
24
25
const live = (): MatchState => {
26
  const state = newMatch(11);
27
  state.phase = "live";
28
  state.clock = 100;
29
  return state;
30
};
31
const attacker = (state: MatchState): Player => state.players.find((p) => p.side === "atk")!;
32
33
test("every agent has two bought basics, a free signature and an ultimate", () => {
34
  expect(AGENTS.length).toBe(5);
35
  for (const agent of AGENTS) {
36
    for (const slot of SLOTS) expect(agent.abilities[slot].name.length).toBeGreaterThan(0);
37
    for (const slot of buyableSlots) expect(agent.abilities[slot].cost).toBeGreaterThan(0);
38
    expect(agent.abilities.e.cost).toBe(0);
39
    expect(agent.abilities.e.cooldown).toBeGreaterThan(0);
40
    expect(agent.abilities.x.points).toBeGreaterThanOrEqual(6);
41
  }
42
});
43
44
test("a flash blinds by the angle you were facing", () => {
45
  const at = { x: 1000, y: 1000 };
46
  const staring = { x: 1100, y: 1000 };
47
  const facingIt = Math.PI; // looking back down the line at the flash
48
  const facingAway = 0;
49
  expect(flashDuration(1.7, staring, at, facingIt, false, 360)).toBeGreaterThan(1.6);
50
  expect(flashDuration(1.7, staring, at, facingAway, false, 360)).toBe(0);
51
  // Turned just past the edge of the screen is a shorter blind than staring.
52
  const glance = flashDuration(1.7, staring, at, facingIt - 0.8, false, 360);
53
  expect(glance).toBeGreaterThan(0);
54
  expect(glance).toBeLessThan(1.7);
55
});
56
57
test("a wall between you and the flash means no blind at all", () => {
58
  const blind = flashDuration(1.7, { x: 1100, y: 1000 }, { x: 1000, y: 1000 }, Math.PI, true, 360);
59
  expect(blind).toBe(0);
60
});
61
62
test("a flash reaches a room, not the map", () => {
63
  // The map is 2400 by 1520. A 900 radius was 70% of it by area and 75% of
64
  // its width across: weather, not a thrown object. A site is a few hundred
65
  // units across, and that is the scale every flash is kept to.
66
  expect(flashes().length).toBeGreaterThan(0);
67
  for (const flash of flashes()) {
68
    expect(flash.radius).toBeGreaterThanOrEqual(300);
69
    expect(flash.radius).toBeLessThanOrEqual(400);
70
    expect((Math.PI * flash.radius * flash.radius) / (WIDTH * HEIGHT)).toBeLessThan(0.12);
71
    expect((2 * flash.radius) / WIDTH).toBeLessThanOrEqual(0.3);
72
    // And it is a thrown object: it lands past its own radius from the thrower.
73
    expect(flash.range).toBeGreaterThan(flash.radius);
74
  }
75
});
76
77
test("distance matters inside the radius: the centre is the full duration, the edge a flicker", () => {
78
  const at = { x: 1000, y: 1000 };
79
  const r = 360;
80
  const facingIt = Math.PI;
81
  const stand = (d: number) => ({ x: 1000 + d, y: 1000 });
82
  expect(flashDuration(1.7, stand(60), at, facingIt, false, r)).toBe(1.7);
83
  expect(flashDuration(1.7, stand(0.4 * r), at, facingIt, false, r)).toBe(1.7);
84
  const half = flashDuration(1.7, stand(0.7 * r), at, facingIt, false, r);
85
  expect(half).toBeGreaterThan(0.3 * 1.7);
86
  expect(half).toBeLessThan(0.7 * 1.7);
87
  const edge = flashDuration(1.7, stand(r), at, facingIt, false, r);
88
  expect(edge).toBeGreaterThan(0);
89
  expect(edge).toBeLessThanOrEqual(FLASH_EDGE * 1.7 + 0.01);
90
  // Past the radius there is nothing, however hard you were staring.
91
  expect(flashDuration(1.7, stand(r + 1), at, facingIt, false, r)).toBe(0);
92
  expect(flashDuration(1.7, stand(899), at, facingIt, false, r)).toBe(0);
93
  // Closer is never shorter.
94
  let last = Infinity;
95
  for (let d = 20; d <= r; d += 20) {
96
    const now = flashDuration(1.7, stand(d), at, facingIt, false, r);
97
    expect(now).toBeLessThanOrEqual(last);
98
    last = now;
99
  }
100
});
101
102
const deg = (n: number) => (n * Math.PI) / 180;
103
104
test("facing is the gate: on screen is the full duration, behind your back is nothing", () => {
105
  // The browser draws FOV wide. Anything inside half of that was on your
106
  // screen: you looked at it, you pay all of it. Past the screen's edge a
107
  // little grace for turning, gone quickly; beyond that, nothing at all.
108
  expect(FLASH_CONE).toBeCloseTo(FOV / 2, 9);
109
  expect(FLASH_GRACE).toBeLessThanOrEqual(deg(56));
110
  const at = { x: 1000, y: 1000 };
111
  const near = { x: 1100, y: 1000 };
112
  const straight = Math.PI;
113
  for (const off of [0, 15, 30, 37]) {
114
    expect(flashDuration(1.7, near, at, straight + deg(off), false, 360)).toBe(1.7);
115
    expect(flashDuration(1.7, near, at, straight - deg(off), false, 360)).toBe(1.7);
116
  }
117
  // The grace falls away quickly: half way through it, well under half is left.
118
  const grace = flashDuration(1.7, near, at, straight + deg(46), false, 360);
119
  expect(grace).toBeGreaterThan(0);
120
  expect(grace).toBeLessThan(0.5 * 1.7);
121
  expect(flashDuration(1.7, near, at, straight + deg(53), false, 360)).toBeLessThan(0.1);
122
  // Used to blind at full strength out to 45° and taper to 100°.
123
  for (const off of [55, 60, 70, 100, 180]) {
124
    expect(flashDuration(1.7, near, at, straight + deg(off), false, 360)).toBe(0);
125
    expect(facingFlash(near, at, straight + deg(off))).toBe(0);
126
  }
127
  // Turning further is never a longer blind.
128
  let last = Infinity;
129
  for (let off = 0; off <= 180; off += 1) {
130
    const now = facingFlash(near, at, straight + deg(off));
131
    expect(now).toBeLessThanOrEqual(last);
132
    last = now;
133
  }
134
});
135
136
test("facing decides whether, distance decides how long", () => {
137
  const at = { x: 1000, y: 1000 };
138
  const r = 360;
139
  const stand = (d: number) => ({ x: 1000 + d, y: 1000 });
140
  // Facing it exactly is never survivable because of a distance table:
141
  // every distance the light carries to blinds, and further is shorter.
142
  let last = Infinity;
143
  for (let d = 10; d <= r; d += 10) {
144
    const blind = flashDuration(1.7, stand(d), at, Math.PI, false, r);
145
    expect(blind).toBeGreaterThan(0);
146
    expect(blind).toBeLessThanOrEqual(last);
147
    last = blind;
148
  }
149
  expect(flashReach(r, r)).toBeCloseTo(FLASH_EDGE, 9);
150
  expect(flashReach(r + 1, r)).toBe(0);
151
  // And a close flash never blinds a player who had their back to it.
152
  for (let d = 10; d <= r; d += 10) {
153
    expect(flashDuration(1.7, stand(d), at, 0, false, r)).toBe(0);
154
    expect(flashDuration(1.7, stand(d), at, Math.PI / 2, false, r)).toBe(0);
155
    expect(flashDuration(1.7, stand(d), at, Math.PI - deg(60), false, r)).toBe(0);
156
  }
157
  // Behind a wall or a smoke you were not looking at it, whatever your angle says.
158
  expect(flashDuration(1.7, stand(10), at, Math.PI, true, r)).toBe(0);
159
});
160
161
test("a smoke blocks the sight line for as long as it lives", () => {
162
  const smoke: Zone = {
163
    id: 1, kind: "smoke", owner: "x", side: "atk", x: 1200, y: 1000, r: 140,
164
    life: 8, fuse: 0, beat: 0, pulses: 0, damage: 0, blocks: true, solid: false,
165
  };
166
  // Straight down the mid corridor, which is open floor end to end.
167
  const from = { x: 1100, y: 1000 };
168
  const to = { x: 1300, y: 1000 };
169
  expect(sightBlocked(from, to, [])).toBe(false);
170
  expect(sightBlocked(from, to, [smoke])).toBe(true);
171
  // Standing off the line, the smoke is not in the way.
172
  expect(sightBlocked({ x: 1100, y: 700 }, { x: 1300, y: 700 }, [smoke])).toBe(false);
173
});
174
175
test("a charge is spent on a cast and refused when there is none", () => {
176
  const state = live();
177
  const player = attacker(state);
178
  player.agent = "veil";
179
  player.charges.c = 1;
180
  expect(cast(state, player, "c")).toBe(true);
181
  expect(player.charges.c).toBe(0);
182
  expect(state.zones.some((z) => z.kind === "smoke")).toBe(true);
183
  expect(cast(state, player, "c")).toBe(false);
184
  expect(castable(player, "c")).toBe(false);
185
});
186
187
test("the signature goes on cooldown and comes back on its own", () => {
188
  const state = live();
189
  const player = attacker(state);
190
  player.agent = "veil";
191
  const cooldown = agentById("veil").abilities.e.cooldown;
192
  expect(cast(state, player, "e")).toBe(true);
193
  expect(player.charges.e).toBe(0);
194
  expect(player.cooldown.e).toBe(cooldown);
195
  for (let i = 0; i < Math.ceil(cooldown * 10) + 4; i++) tick(state, 0.1);
196
  expect(player.cooldown.e).toBe(0);
197
  expect(castable(player, "e")).toBe(true);
198
});
199
200
test("an ultimate costs its points and empties the meter", () => {
201
  const state = live();
202
  const player = attacker(state);
203
  player.agent = "sable";
204
  player.ult = 3;
205
  expect(cast(state, player, "x")).toBe(false);
206
  player.ult = 7;
207
  expect(cast(state, player, "x")).toBe(true);
208
  expect(player.ult).toBe(0);
209
  // Hunt reveals every enemy and leaves the caster's own side alone.
210
  for (const other of state.players) expect(other.revealed > 0).toBe(other.side !== player.side);
211
});
212
213
test("a molly burns whoever stands in it and leaves the thrower's team alone", () => {
214
  const state = live();
215
  const player = attacker(state);
216
  player.agent = "ember";
217
  player.charges.q = 1;
218
  const victim = state.players.find((p) => p.side === "def")!;
219
  const mate = state.players.find((p) => p.side === "atk" && p.id !== player.id)!;
220
  victim.x = player.x + 120;
221
  victim.y = player.y;
222
  mate.x = player.x + 120;
223
  mate.y = player.y;
224
  player.angle = 0;
225
  player.x = victim.x - 120;
226
  cast(state, player, "q");
227
  for (const zone of state.zones) {
228
    zone.x = victim.x;
229
    zone.y = victim.y;
230
  }
231
  for (let i = 0; i < 20; i++) tick(state, 0.1);
232
  expect(victim.hp).toBeLessThan(100);
233
  expect(mate.hp).toBe(100);
234
});
235
236
test("a wall is solid: it blocks the sight line that was open", () => {
237
  const state = live();
238
  const player = attacker(state);
239
  player.agent = "rook";
240
  // Along the back corridor, which is open from one side of the map to the other.
241
  player.x = 400;
242
  player.y = 1400;
243
  player.angle = 0;
244
  const behind = { x: 1000, y: 1400 };
245
  const openBefore = !sightBlocked(player, behind, state.zones);
246
  expect(cast(state, player, "e")).toBe(true);
247
  expect(openBefore).toBe(true);
248
  expect(sightBlocked(player, behind, state.zones)).toBe(true);
249
  expect(state.zones.every((z) => z.solid)).toBe(true);
250
});
251
252
test("a round starts with the signature ready and no bought charges", () => {
253
  const state = newMatch(3);
254
  startRound(state, true);
255
  for (const player of state.players) {
256
    expect(player.charges.c).toBe(0);
257
    expect(player.charges.q).toBe(0);
258
    expect(player.charges.e).toBe(1);
259
  }
260
});
↑↓ navigate ↵ open esc close
↑↓ navigate ↵ open esc close

Keyboard shortcuts