| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177 |
- import { describe, expect, it } from 'vitest';
- import { loadCharacterStats, loadMap } from '../src/server/assets';
- import { Game } from '../src/server/game';
- import { Anim, CHARACTER_IDS } from '../src/shared/characters';
- import { MATERIAL_WOOL, TICK_RATE } from '../src/shared/constants';
- import { Button } from '../src/shared/input';
- import type { FxEvent } from '../src/shared/protocol';
- import { ASSETS, realMaterials } from './helpers';
- const materials = realMaterials();
- function newGame(): Game {
- const { definition, terrain } = loadMap(ASSETS, 'gateau-etages', materials);
- return new Game({ map: definition, terrain, materials, seed: 5, loadCharacter: (id) => loadCharacterStats(ASSETS, id) });
- }
- function run(game: Game, ticks: number): FxEvent[] {
- const fx: FxEvent[] = [];
- for (let i = 0; i < ticks; i++) fx.push(...game.step().fx);
- return fx;
- }
- const me = (game: Game, id: number) => game.snapshot().find((p) => p.id === id)!;
- const countWool = (game: Game): number => game.terrain.data.reduce((n, m) => n + (m === MATERIAL_WOOL ? 1 : 0), 0);
- /** Presses `buttons` aimed at (x, y) for one tick, then releases. */
- function press(game: Game, id: number, buttons: number, x: number, y: number): FxEvent[] {
- game.setInput(id, { buttons, aimX: Math.round(x), aimY: Math.round(y) });
- const fx = run(game, 1);
- game.setInput(id, { buttons: 0, aimX: Math.round(x), aimY: Math.round(y) });
- return fx;
- }
- describe('roster', () => {
- it('every character has data that loads', () => {
- expect(CHARACTER_IDS).toEqual(['castor', 'mamie', 'pyromanchot', 'generic']);
- for (const id of CHARACTER_IDS) expect(loadCharacterStats(ASSETS, id).id).toBe(id);
- });
- });
- describe('Mamie Tricot', () => {
- it('her yarn ball lays a taut wool bridge that holds: nothing falls, nobody is hurt', () => {
- const game = newGame();
- const { id } = game.addPlayer('Mamie', 'mamie');
- game.teleport(id, 640, 440); // middle tier, left of the top tier wall (x ≈ 780)
- run(game, 60);
- const m = me(game, id);
- let detached = 0;
- let crushed = 0;
- game.setInput(id, { buttons: Button.Special, aimX: 800, aimY: Math.round(m.y - 40) });
- for (let i = 0; i < 4 * TICK_RATE; i++) {
- const r = game.step();
- if (i === 0) game.setInput(id, { buttons: 0, aimX: 800, aimY: Math.round(m.y - 40) });
- detached += r.terrain.filter((e) => e.ev.kind === 'detach').length;
- crushed += r.crushed.length;
- }
- expect(countWool(game)).toBeGreaterThan(300);
- expect(detached).toBe(0);
- expect(crushed).toBe(0);
- expect(me(game, id).anim & Anim.Dead).toBe(0);
- });
- it('the thread follows the ball in flight, tied under her feet', () => {
- const game = newGame();
- const { id } = game.addPlayer('Mamie', 'mamie');
- game.teleport(id, 640, 440);
- run(game, 60);
- const m = me(game, id);
- press(game, id, Button.Special, 800, m.y - 40);
- const ball = game.itemStates().find((it) => it.kind === 'yarn')!;
- expect(ball.thread).toBeDefined();
- const [ax, ay] = ball.thread!;
- expect(Math.abs(ax - m.x)).toBeLessThan(20);
- expect(ay).toBeGreaterThan(m.y); // on the ground under her
- expect(game.terrain.isSolid(Math.round(ax), Math.round(ay))).toBe(true);
- });
- it('a yarn ball thrown at her own feet never buries or kills her', () => {
- const game = newGame();
- const { id } = game.addPlayer('Mamie', 'mamie');
- game.teleport(id, 640, 440);
- run(game, 60);
- for (const [ax, ay] of [[0, 60], [0, -200], [40, 20], [-40, 20]]) {
- const m = me(game, id);
- press(game, id, Button.Special, m.x + ax, m.y + ay);
- run(game, 3 * TICK_RATE);
- const after = me(game, id);
- expect(after.anim & Anim.Dead).toBe(0);
- // Not stuck inside wool: no solid pixel at her centre.
- expect(game.terrain.isSolid(Math.round(after.x), Math.round(after.y))).toBe(false);
- }
- });
- it('her needles barely push but stun for a long time', () => {
- const game = newGame();
- const mamie = game.addPlayer('Mamie', 'mamie');
- const victim = game.addPlayer('Victime', 'generic');
- run(game, 90);
- const m = me(game, mamie.id);
- game.teleport(victim.id, m.x + 40, m.y);
- press(game, mamie.id, Button.Attack, m.x + 40, m.y);
- const v = me(game, victim.id);
- expect(v.anim & Anim.Stunned).toBeTruthy();
- expect(Math.hypot(v.vx, v.vy)).toBeLessThan(400);
- run(game, Math.round(0.6 * TICK_RATE));
- expect(me(game, victim.id).anim & Anim.Stunned).toBeTruthy();
- });
- it('her wool burns: an explosion nearby consumes it', () => {
- const game = newGame();
- const mamie = game.addPlayer('Mamie', 'mamie');
- const pyro = game.addPlayer('Pyro', 'pyromanchot');
- game.teleport(mamie.id, 640, 440);
- run(game, 60);
- const m = me(game, mamie.id);
- press(game, mamie.id, Button.Special, 800, m.y - 40);
- run(game, 2 * TICK_RATE);
- const before = countWool(game);
- expect(before).toBeGreaterThan(0);
- // A firecracker thrown into the wool.
- game.teleport(pyro.id, 700, 300);
- run(game, 2);
- const fx = press(game, pyro.id, Button.Attack, 760, m.y - 30).concat(run(game, TICK_RATE));
- expect(fx.some((e) => e.kind === 'explosion')).toBe(true);
- run(game, 2);
- expect(countWool(game)).toBeLessThan(before);
- });
- });
- describe('Pyromanchot', () => {
- it('throws firecrackers that explode away from him, and every third one backfires', () => {
- const game = newGame();
- const { id } = game.addPlayer('Pyro', 'pyromanchot');
- run(game, 90);
- const explosions: { x: number; y: number }[] = [];
- for (let shot = 0; shot < 3; shot++) {
- const p = me(game, id);
- const fx = press(game, id, Button.Attack, p.x + 300, p.y - 100).concat(run(game, TICK_RATE));
- for (const e of fx) if (e.kind === 'explosion') explosions.push({ x: e.x - p.x, y: e.y - p.y });
- run(game, Math.round(0.5 * TICK_RATE));
- }
- expect(explosions).toHaveLength(3);
- // The first two flew off; the third went off right next to him.
- expect(Math.hypot(explosions[0].x, explosions[0].y)).toBeGreaterThan(60);
- expect(Math.hypot(explosions[1].x, explosions[1].y)).toBeGreaterThan(60);
- expect(Math.hypot(explosions[2].x, explosions[2].y)).toBeLessThan(40);
- });
- it('a backfire stuns him', () => {
- const game = newGame();
- const { id } = game.addPlayer('Pyro', 'pyromanchot');
- run(game, 90);
- for (let shot = 0; shot < 3; shot++) {
- const p = me(game, id);
- press(game, id, Button.Attack, p.x + 300, p.y - 100);
- if (shot < 2) run(game, TICK_RATE);
- }
- expect(me(game, id).anim & Anim.Stunned).toBeTruthy();
- });
- it('belly slide: fast, then an explosion at the end that spares him', () => {
- const game = newGame();
- const pyro = game.addPlayer('Pyro', 'pyromanchot');
- const victim = game.addPlayer('Victime', 'generic');
- game.teleport(pyro.id, 900, 680); // bottom tier, lots of room to the right
- game.teleport(victim.id, 1500, 680);
- run(game, 60);
- const start = me(game, pyro.id);
- press(game, pyro.id, Button.Special, start.x + 500, start.y);
- expect(me(game, pyro.id).anim & Anim.Special).toBeTruthy();
- const fx = run(game, TICK_RATE);
- const end = me(game, pyro.id);
- expect(end.x - start.x).toBeGreaterThan(200);
- expect(fx.some((e) => e.kind === 'explosion')).toBe(true);
- expect(end.anim & Anim.Stunned).toBe(0);
- });
- });
|