characters.test.ts 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. import { describe, expect, it } from 'vitest';
  2. import { loadCharacterStats, loadMap } from '../src/server/assets';
  3. import { Game } from '../src/server/game';
  4. import { Anim, CHARACTER_IDS } from '../src/shared/characters';
  5. import { MATERIAL_WOOL, TICK_RATE } from '../src/shared/constants';
  6. import { Button } from '../src/shared/input';
  7. import type { FxEvent } from '../src/shared/protocol';
  8. import { ASSETS, realMaterials } from './helpers';
  9. const materials = realMaterials();
  10. function newGame(): Game {
  11. const { definition, terrain } = loadMap(ASSETS, 'gateau-etages', materials);
  12. return new Game({ map: definition, terrain, materials, seed: 5, loadCharacter: (id) => loadCharacterStats(ASSETS, id) });
  13. }
  14. function run(game: Game, ticks: number): FxEvent[] {
  15. const fx: FxEvent[] = [];
  16. for (let i = 0; i < ticks; i++) fx.push(...game.step().fx);
  17. return fx;
  18. }
  19. const me = (game: Game, id: number) => game.snapshot().find((p) => p.id === id)!;
  20. const countWool = (game: Game): number => game.terrain.data.reduce((n, m) => n + (m === MATERIAL_WOOL ? 1 : 0), 0);
  21. /** Presses `buttons` aimed at (x, y) for one tick, then releases. */
  22. function press(game: Game, id: number, buttons: number, x: number, y: number): FxEvent[] {
  23. game.setInput(id, { buttons, aimX: Math.round(x), aimY: Math.round(y) });
  24. const fx = run(game, 1);
  25. game.setInput(id, { buttons: 0, aimX: Math.round(x), aimY: Math.round(y) });
  26. return fx;
  27. }
  28. describe('roster', () => {
  29. it('every character has data that loads', () => {
  30. expect(CHARACTER_IDS).toEqual(['castor', 'mamie', 'pyromanchot', 'generic']);
  31. for (const id of CHARACTER_IDS) expect(loadCharacterStats(ASSETS, id).id).toBe(id);
  32. });
  33. });
  34. describe('Mamie Tricot', () => {
  35. it('her yarn ball spins a wool thread that holds when it reaches a wall', () => {
  36. const game = newGame();
  37. const { id } = game.addPlayer('Mamie', 'mamie');
  38. game.teleport(id, 640, 440); // middle tier, left of the top tier wall (x ≈ 780)
  39. run(game, 60);
  40. const m = me(game, id);
  41. press(game, id, Button.Special, 800, m.y - 40);
  42. run(game, 2 * TICK_RATE);
  43. const wool = countWool(game);
  44. expect(wool).toBeGreaterThan(150);
  45. // Let anything unanchored fall and merge or vanish: most of the thread must still be in the bitmap.
  46. run(game, 4 * TICK_RATE);
  47. expect(countWool(game)).toBeGreaterThan(wool * 0.5);
  48. });
  49. it('her needles barely push but stun for a long time', () => {
  50. const game = newGame();
  51. const mamie = game.addPlayer('Mamie', 'mamie');
  52. const victim = game.addPlayer('Victime', 'generic');
  53. run(game, 90);
  54. const m = me(game, mamie.id);
  55. game.teleport(victim.id, m.x + 40, m.y);
  56. press(game, mamie.id, Button.Attack, m.x + 40, m.y);
  57. const v = me(game, victim.id);
  58. expect(v.anim & Anim.Stunned).toBeTruthy();
  59. expect(Math.hypot(v.vx, v.vy)).toBeLessThan(400);
  60. run(game, Math.round(0.6 * TICK_RATE));
  61. expect(me(game, victim.id).anim & Anim.Stunned).toBeTruthy();
  62. });
  63. it('her wool burns: an explosion nearby consumes it', () => {
  64. const game = newGame();
  65. const mamie = game.addPlayer('Mamie', 'mamie');
  66. const pyro = game.addPlayer('Pyro', 'pyromanchot');
  67. game.teleport(mamie.id, 640, 440);
  68. run(game, 60);
  69. const m = me(game, mamie.id);
  70. press(game, mamie.id, Button.Special, 800, m.y - 40);
  71. run(game, 2 * TICK_RATE);
  72. const before = countWool(game);
  73. expect(before).toBeGreaterThan(0);
  74. // A firecracker thrown into the wool.
  75. game.teleport(pyro.id, 700, 300);
  76. run(game, 2);
  77. const fx = press(game, pyro.id, Button.Attack, 760, m.y - 30).concat(run(game, TICK_RATE));
  78. expect(fx.some((e) => e.kind === 'explosion')).toBe(true);
  79. run(game, 2);
  80. expect(countWool(game)).toBeLessThan(before);
  81. });
  82. });
  83. describe('Pyromanchot', () => {
  84. it('throws firecrackers that explode away from him, and every third one backfires', () => {
  85. const game = newGame();
  86. const { id } = game.addPlayer('Pyro', 'pyromanchot');
  87. run(game, 90);
  88. const explosions: { x: number; y: number }[] = [];
  89. for (let shot = 0; shot < 3; shot++) {
  90. const p = me(game, id);
  91. const fx = press(game, id, Button.Attack, p.x + 300, p.y - 100).concat(run(game, TICK_RATE));
  92. for (const e of fx) if (e.kind === 'explosion') explosions.push({ x: e.x - p.x, y: e.y - p.y });
  93. run(game, Math.round(0.5 * TICK_RATE));
  94. }
  95. expect(explosions).toHaveLength(3);
  96. // The first two flew off; the third went off right next to him.
  97. expect(Math.hypot(explosions[0].x, explosions[0].y)).toBeGreaterThan(60);
  98. expect(Math.hypot(explosions[1].x, explosions[1].y)).toBeGreaterThan(60);
  99. expect(Math.hypot(explosions[2].x, explosions[2].y)).toBeLessThan(40);
  100. });
  101. it('a backfire stuns him', () => {
  102. const game = newGame();
  103. const { id } = game.addPlayer('Pyro', 'pyromanchot');
  104. run(game, 90);
  105. for (let shot = 0; shot < 3; shot++) {
  106. const p = me(game, id);
  107. press(game, id, Button.Attack, p.x + 300, p.y - 100);
  108. if (shot < 2) run(game, TICK_RATE);
  109. }
  110. expect(me(game, id).anim & Anim.Stunned).toBeTruthy();
  111. });
  112. it('belly slide: fast, then an explosion at the end that spares him', () => {
  113. const game = newGame();
  114. const pyro = game.addPlayer('Pyro', 'pyromanchot');
  115. const victim = game.addPlayer('Victime', 'generic');
  116. game.teleport(pyro.id, 900, 680); // bottom tier, lots of room to the right
  117. game.teleport(victim.id, 1500, 680);
  118. run(game, 60);
  119. const start = me(game, pyro.id);
  120. press(game, pyro.id, Button.Special, start.x + 500, start.y);
  121. expect(me(game, pyro.id).anim & Anim.Special).toBeTruthy();
  122. const fx = run(game, TICK_RATE);
  123. const end = me(game, pyro.id);
  124. expect(end.x - start.x).toBeGreaterThan(200);
  125. expect(fx.some((e) => e.kind === 'explosion')).toBe(true);
  126. expect(end.anim & Anim.Stunned).toBe(0);
  127. });
  128. });