benjivers/val Public Public: anyone can read this; the crew can change it.
Merge pull request #34 from look-around-always
Record where a player is looking in every phase, alive or not
2 files
changed +62
−1
src/match.test.ts +43
| @@ -414,3 +414,46 @@ test("the deal is the seeded shuffle, so the same match replays the same", () => | ||
| 414 | 414 | expect(newMatch(77).players.map((p) => p.name)).toEqual(newMatch(77).players.map((p) => p.name)); |
| 415 | 415 | expect(newMatch(77).players.map((p) => p.name)).not.toEqual(newMatch(78).players.map((p) => p.name)); |
| 416 | 416 | }); |
| 417 | ||
| 418 | const round1 = (n: number) => Math.round(n * 1000) / 1000; | |
| 419 | ||
| 420 | test("where you are looking is recorded in every phase, alive or not", () => { | |
| 421 | // The bug this pins: `player.angle = input.angle` lived inside movePlayer, | |
| 422 | // which the end and over phases return before reaching and which a dead | |
| 423 | // player never enters. The server's copy of your angle froze, the browser | |
| 424 | // kept its own, and once the angles the client had recently sent aged out | |
| 425 | // of its memory the frozen one read as a correction — so the view snapped | |
| 426 | // back, about 0.8 times a second, in first person only. Looking around is | |
| 427 | // not moving, and no phase and no death stops you doing it. | |
| 428 | const state = newMatch(5); | |
| 429 | const me = state.players[0]!; | |
| 430 | me.bot = false; | |
| 431 | ||
| 432 | const looksFollow = (label: string) => { | |
| 433 | for (const want of [0.4, -1.2, 2.9]) { | |
| 434 | setInput(me, { angle: want }); | |
| 435 | tick(state, 1 / 30); | |
| 436 | expect(`${label}: ${round1(me.angle)}`).toBe(`${label}: ${round1(want)}`); | |
| 437 | } | |
| 438 | }; | |
| 439 | ||
| 440 | looksFollow("live"); | |
| 441 | ||
| 442 | me.alive = false; | |
| 443 | me.hp = 0; | |
| 444 | looksFollow("dead"); | |
| 445 | me.alive = true; | |
| 446 | me.hp = 100; | |
| 447 | ||
| 448 | state.phase = "end"; | |
| 449 | state.clock = 99; | |
| 450 | looksFollow("between rounds"); | |
| 451 | ||
| 452 | state.phase = "over"; | |
| 453 | state.clock = 99; | |
| 454 | looksFollow("match over"); | |
| 455 | ||
| 456 | state.phase = "buy"; | |
| 457 | state.clock = 99; | |
| 458 | looksFollow("buy"); | |
| 459 | }); |
src/match.ts +19 −1
| @@ -314,6 +314,24 @@ export function tick(state: MatchState, dt: number): void { | ||
| 314 | 314 | state.shots = state.shots.filter((s) => s.age < 0.12); |
| 315 | 315 | for (const event of state.events) event.age += dt; |
| 316 | 316 | |
| 317 | // Where you are LOOKING is recorded every tick, in every phase, alive or | |
| 318 | // dead. Moving is what the phases and dying gate; turning your head is not. | |
| 319 | // | |
| 320 | // This used to live inside movePlayer, which the round-end and match-over | |
| 321 | // phases return before reaching and which a dead player never enters. The | |
| 322 | // server's copy of your angle therefore froze between rounds and while you | |
| 323 | // were down — and the browser, which owns your view and only takes the | |
| 324 | // server's angle when the server disagrees, kept looking where the mouse | |
| 325 | // said. Once the angles a client had recently sent aged out of its memory | |
| 326 | // (yaw.js SENT_MEMORY), the frozen one read as a genuine correction and the | |
| 327 | // view snapped back to it. Measured: about 0.8 snaps a second while dead or | |
| 328 | // between rounds, against 0.0 in a live round. It was invisible in the | |
| 329 | // top-down view, which draws the server's angle, and violent in first | |
| 330 | // person, which draws the browser's — and that asymmetry is what made it | |
| 331 | // look like the renderer rather than the rules. | |
| 332 | for (const player of state.players) | |
| 333 | if (Number.isFinite(player.input.angle)) player.angle = player.input.angle; | |
| 334 | ||
| 317 | 335 | if (state.phase === "over") { |
| 318 | 336 | state.clock -= dt; |
| 319 | 337 | if (state.clock <= 0) resetMatch(state); |
| @@ -385,7 +403,7 @@ export function speedOf(state: MatchState, player: Player): number { | ||
| 385 | 403 | function movePlayer(state: MatchState, player: Player, dt: number, penned: boolean): void { |
| 386 | 404 | if (!player.alive) return; |
| 387 | 405 | const input = player.input; |
| 388 | if (Number.isFinite(input.angle)) player.angle = input.angle; | |
| 406 | // The angle is applied for every player in tick(), whatever the phase. | |
| 389 | 407 | let mx = input.mx; |
| 390 | 408 | let my = input.my; |
| 391 | 409 | const len = Math.hypot(mx, my); |