| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540 |
- 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();
- /** `params` overrides character tuning values (by character id), e.g. to force a random outcome. */
- function newGame(params: Record<string, Record<string, number>> = {}): Game {
- const { definition, terrain } = loadMap(ASSETS, 'gateau-etages', materials);
- const loadCharacter = (id: string) => {
- const stats = loadCharacterStats(ASSETS, id);
- return { ...stats, params: { ...stats.params, ...params[id] } };
- };
- return new Game({ map: definition, terrain, materials, seed: 5, loadCharacter });
- }
- 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', 'gege', 'pompier', 'prout', 'chat', 'magicien', 'tatie', 'leon', '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.4 * 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 far away from him', () => {
- const game = newGame({ pyromanchot: { backfireChance: 0 } });
- 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);
- // Aimed up and across: it flies until it lands somewhere on the cake.
- const fx = press(game, id, Button.Attack, p.x + 300, p.y - 300).concat(run(game, 2 * TICK_RATE));
- for (const e of fx) if (e.kind === 'explosion') explosions.push({ x: e.x - p.x, y: e.y - p.y });
- }
- expect(explosions).toHaveLength(3);
- for (const e of explosions) expect(Math.hypot(e.x, e.y)).toBeGreaterThan(200);
- });
- it('only some firecrackers backfire', () => {
- const game = newGame();
- const { id } = game.addPlayer('Pyro', 'pyromanchot');
- run(game, 90);
- let backfires = 0;
- const shots = 40;
- for (let shot = 0; shot < shots; shot++) {
- const p = me(game, id);
- const fx = press(game, id, Button.Attack, p.x + 300, p.y - 300);
- if (fx.some((e) => e.kind === 'cue' && e.cue === 'backfire')) backfires++;
- run(game, Math.round(0.5 * TICK_RATE));
- }
- expect(backfires).toBeGreaterThan(0);
- expect(backfires).toBeLessThan(shots / 4);
- });
- it('a backfire stuns him', () => {
- const game = newGame({ pyromanchot: { backfireChance: 1 } });
- const { id } = game.addPlayer('Pyro', 'pyromanchot');
- run(game, 90);
- const p = me(game, id);
- press(game, id, Button.Attack, p.x + 300, p.y - 100);
- 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);
- });
- });
- describe('Gégé le Ver', () => {
- it('swims down into the cake, then bursts out under a player and sends them flying', () => {
- const game = newGame();
- const gege = game.addPlayer('Gégé', 'gege');
- const victim = game.addPlayer('Victime', 'generic');
- game.teleport(gege.id, 600, 420); // middle tier top
- game.teleport(victim.id, 730, 420);
- run(game, 60);
- // Dive.
- for (let i = 0; i < 40; i++) {
- const g = me(game, gege.id);
- game.setInput(gege.id, { buttons: Button.Special, aimX: Math.round(g.x), aimY: Math.round(g.y + 200) });
- game.step();
- }
- expect(me(game, gege.id).anim & Anim.Underground).toBeTruthy();
- // Tunnel toward the victim, under them, then let go: he bursts out.
- for (let i = 0; i < 200; i++) {
- const g = me(game, gege.id);
- const v = me(game, victim.id);
- const under = Math.abs(g.x - v.x) < 20;
- game.setInput(gege.id, { buttons: Button.Special, aimX: Math.round(v.x), aimY: Math.round(under ? v.y : 505) });
- game.step();
- if (under && g.y - v.y < 70) break;
- }
- game.setInput(gege.id, { buttons: 0, aimX: 0, aimY: 0 });
- const fx = run(game, 2);
- expect(fx).toContainEqual(expect.objectContaining({ kind: 'cue', id: gege.id, cue: 'emerge' }));
- const v = me(game, victim.id);
- expect(v.anim & Anim.Stunned).toBeTruthy();
- expect(v.vy).toBeLessThan(-300);
- });
- });
- describe('Bébé Pompier', () => {
- it('the water jet pushes a player away and the recoil sends him backwards', () => {
- const game = newGame();
- const baby = game.addPlayer('Bébé', 'pompier');
- const victim = game.addPlayer('Victime', 'generic');
- game.teleport(baby.id, 600, 420);
- run(game, 60);
- const b = me(game, baby.id);
- game.teleport(victim.id, b.x + 80, b.y);
- run(game, 1);
- const fx = press(game, baby.id, Button.Attack, b.x + 200, b.y);
- expect(fx).toContainEqual(expect.objectContaining({ kind: 'cue', cue: 'water' }));
- expect(me(game, victim.id).vx).toBeGreaterThan(200);
- expect(me(game, baby.id).vx).toBeLessThan(-200);
- });
- it('soaking turns sponge into soaked sponge, which then sags on its own', () => {
- const game = newGame();
- const baby = game.addPlayer('Bébé', 'pompier');
- game.teleport(baby.id, 600, 420);
- run(game, 60);
- const b = me(game, baby.id);
- press(game, baby.id, Button.Special, b.x + 60, b.y + 120);
- const soaked = (): number => game.terrain.data.reduce((n, m) => n + (m === 8 ? 1 : 0), 0);
- const after = soaked();
- expect(after).toBeGreaterThan(1000);
- run(game, 4 * TICK_RATE);
- expect(soaked()).toBeLessThan(after); // crumbling
- });
- it('the spray drenches players caught in it: slower for a while', () => {
- const game = newGame();
- const baby = game.addPlayer('Bébé', 'pompier');
- const victim = game.addPlayer('Victime', 'generic');
- game.teleport(baby.id, 600, 420);
- run(game, 60);
- const b = me(game, baby.id);
- game.teleport(victim.id, b.x + 100, b.y);
- run(game, 1);
- press(game, baby.id, Button.Special, b.x + 100, b.y);
- expect(me(game, victim.id).anim & Anim.Soaked).toBeTruthy();
- expect(me(game, baby.id).anim & Anim.Soaked).toBeFalsy();
- });
- });
- describe('Captain Prout', () => {
- it('each thrust lifts him, is heard by all, and leaves a cloud that poisons players inside', () => {
- const game = newGame();
- const prout = game.addPlayer('Prout', 'prout');
- const victim = game.addPlayer('Victime', 'generic');
- game.teleport(prout.id, 600, 420);
- run(game, 60);
- const p0 = me(game, prout.id);
- const fx = press(game, prout.id, Button.Special, p0.x, p0.y - 100);
- expect(fx).toContainEqual(expect.objectContaining({ kind: 'cue', id: prout.id, cue: 'prout' }));
- expect(me(game, prout.id).vy).toBeLessThan(-300);
- expect(game.roundState().clouds.length).toBe(1);
- const [cx, cy] = game.roundState().clouds[0];
- game.teleport(victim.id, cx, cy);
- run(game, 2);
- expect(me(game, victim.id).anim & Anim.Poisoned).toBeTruthy();
- run(game, 1);
- game.teleport(prout.id, cx, cy);
- run(game, 2);
- expect(me(game, prout.id).anim & Anim.Poisoned).toBeFalsy(); // immune to his own clouds
- });
- it('only a few thrusts before touching the ground again: he hops, he does not fly', () => {
- const game = newGame();
- const prout = game.addPlayer('Prout', 'prout');
- game.teleport(prout.id, 600, 420);
- run(game, 60);
- let puffs = 0;
- // One try per cooldown: he does not land before the fourth one.
- for (let i = 0; i < 4; i++) {
- const p = me(game, prout.id);
- const fx = press(game, prout.id, Button.Special, p.x, p.y - 100);
- if (fx.some((e) => e.kind === 'cue' && e.cue === 'prout')) puffs++;
- if (i < 3) {
- run(game, 24);
- expect(me(game, prout.id).grounded).toBe(false);
- }
- }
- expect(puffs).toBe(loadCharacterStats(ASSETS, 'prout').params.airPuffs);
- });
- });
- describe('Le Chat', () => {
- it('pounces claws first: digs scratches and sends the player he lands on flying', () => {
- const game = newGame();
- const cat = game.addPlayer('Chat', 'chat');
- const victim = game.addPlayer('Victime', 'generic');
- game.teleport(cat.id, 600, 420);
- run(game, 60);
- const c = me(game, cat.id);
- game.teleport(victim.id, c.x + 90, c.y);
- run(game, 1);
- const before = game.terrain.data.reduce((n, m) => n + (m !== 0 ? 1 : 0), 0);
- const fx = press(game, cat.id, Button.Special, c.x + 200, c.y + 30);
- expect(fx).toContainEqual(expect.objectContaining({ kind: 'cue', cue: 'pounce' }));
- let fastest = 0;
- for (let i = 0; i < 20; i++) {
- run(game, 1);
- const v = me(game, victim.id);
- fastest = Math.max(fastest, Math.hypot(v.vx, v.vy));
- }
- expect(fastest).toBeGreaterThan(300);
- expect(game.terrain.data.reduce((n, m) => n + (m !== 0 ? 1 : 0), 0)).toBeLessThan(before);
- });
- it('shoves a stunned player hard', () => {
- const game = newGame();
- const cat = game.addPlayer('Chat', 'chat');
- const a = game.addPlayer('Assommé', 'generic');
- game.teleport(cat.id, 600, 420);
- run(game, 60);
- const c = me(game, cat.id);
- game.teleport(a.id, c.x + 50, c.y);
- run(game, 1);
- press(game, cat.id, Button.Attack, c.x + 50, c.y); // paw: stuns
- expect(me(game, a.id).anim & Anim.Stunned).toBeTruthy();
- const now = me(game, cat.id);
- game.teleport(a.id, now.x + 55 * now.facing, now.y);
- press(game, cat.id, Button.Special, now.x + 200 * now.facing, now.y);
- expect(Math.abs(me(game, a.id).vx)).toBeGreaterThan(400);
- });
- it('stops to lick his paw now and then, ignoring input', () => {
- const game = newGame();
- const cat = game.addPlayer('Chat', 'chat');
- run(game, 60);
- let licked = false;
- for (let i = 0; i < 26 * TICK_RATE && !licked; i++) licked = game.step().fx.some((e) => e.kind === 'cue' && e.cue === 'lick');
- expect(licked).toBe(true);
- expect(me(game, cat.id).anim & Anim.Busy).toBeTruthy();
- const x0 = me(game, cat.id).x;
- game.setInput(cat.id, { buttons: Button.Right, aimX: 0, aimY: 0 });
- run(game, 20);
- expect(Math.abs(me(game, cat.id).x - x0)).toBeLessThan(5);
- });
- });
- describe('Magicien Raté', () => {
- function castMany(leading: boolean, casts: number): Record<string, number> {
- const game = newGame();
- const mage = game.addPlayer('Mage', 'magicien');
- const other = game.addPlayer('Autre', 'generic');
- game.teleport(mage.id, 600, 420);
- game.teleport(other.id, 1400, 420);
- run(game, 60);
- // Make him the leader (or last) by giving round points.
- const players = (game as unknown as { players: Map<number, { score: number }> }).players;
- players.get(mage.id)!.score = leading ? 50 : 0;
- players.get(other.id)!.score = leading ? 0 : 50;
- const counts: Record<string, number> = {};
- for (let i = 0; i < casts; i++) {
- const m = me(game, mage.id);
- const fx = press(game, mage.id, Button.Special, m.x + 100, m.y - 50);
- for (const e of fx) if (e.kind === 'cue' && e.cue.startsWith('spell-')) counts[e.cue] = (counts[e.cue] ?? 0) + 1;
- run(game, Math.ceil(2.3 * TICK_RATE));
- }
- return counts;
- }
- it('casts every kind of spell, and fizzles far more often when leading', () => {
- const last = castMany(false, 40);
- expect(Object.keys(last).sort()).toEqual(['spell-jelly', 'spell-pfft', 'spell-rabbit', 'spell-swap']);
- const lead = castMany(true, 40);
- expect(lead['spell-pfft'] ?? 0).toBeGreaterThan((last['spell-pfft'] ?? 0) * 2);
- });
- it('the giant rabbit appears in the air and falls (an unanchored sponge chunk)', () => {
- const game = newGame();
- const mage = game.addPlayer('Mage', 'magicien');
- game.teleport(mage.id, 600, 420);
- run(game, 60);
- for (let i = 0; i < 60; i++) {
- const m = me(game, mage.id);
- game.setInput(mage.id, { buttons: Button.Special, aimX: Math.round(m.x + 100), aimY: Math.round(m.y - 50) });
- const r = game.step();
- game.setInput(mage.id, { buttons: 0, aimX: 0, aimY: 0 });
- if (!r.fx.some((e) => e.kind === 'cue' && e.cue === 'spell-rabbit')) {
- run(game, Math.ceil(2.3 * TICK_RATE));
- continue;
- }
- // The rabbit is filled in this very tick, and comes loose in the same detach pass.
- expect(r.terrain.some((e) => e.ev.kind === 'detach' && !e.ev.debris)).toBe(true);
- return;
- }
- throw new Error('no rabbit in 60 casts');
- });
- });
- describe('Tatie Douille', () => {
- it('builds a turret on the ground, which shoots the others but never her', () => {
- const game = newGame();
- const tatie = game.addPlayer('Tatie', 'tatie');
- const victim = game.addPlayer('Victime', 'generic');
- game.teleport(tatie.id, 600, 420); // middle tier top
- game.teleport(victim.id, 770, 440); // further along the middle tier, in sight
- run(game, 60);
- const t = me(game, tatie.id);
- const fx = press(game, tatie.id, Button.Special, t.x + 60, t.y);
- expect(fx).toContainEqual(expect.objectContaining({ kind: 'cue', cue: 'build' }));
- run(game, 2);
- const [turret] = game.roundState().turrets;
- expect(turret).toMatchObject({ owner: tatie.id });
- // Its base is cake: biscuit right under the head.
- expect(game.terrain.data[(turret.y + 14) * game.terrain.width + turret.x]).toBe(5);
- let shots = 0;
- let victimHit = false;
- for (let i = 0; i < 4 * TICK_RATE; i++) {
- const step = game.step().fx;
- shots += step.filter((e) => e.kind === 'cue' && e.cue === 'turret').length;
- if (me(game, victim.id).anim & Anim.Stunned) victimHit = true;
- }
- expect(shots).toBeGreaterThanOrEqual(2);
- expect(victimHit).toBe(true);
- // Its own shots never knock it down.
- expect(game.roundState().turrets).toHaveLength(1);
- expect(game.stats.get(tatie.id, 'hits')).toBeGreaterThan(0);
- });
- it('a turret falls with the cake under it, and a third one replaces the oldest', () => {
- const game = newGame();
- const tatie = game.addPlayer('Tatie', 'tatie');
- game.teleport(tatie.id, 600, 420);
- run(game, 60);
- // Three turrets along the exposed part of the middle tier (x 520 to 780).
- for (const x of [560, 650, 720]) {
- press(game, tatie.id, Button.Special, x, me(game, tatie.id).y);
- run(game, Math.ceil(2.6 * TICK_RATE));
- }
- const turrets = game.roundState().turrets;
- expect(turrets).toHaveLength(2);
- expect(turrets[0].x).toBeGreaterThan(600); // the first one (x ≈ 560) was replaced
- // Blow the ground away under one of them.
- const target = turrets[0];
- game.queueOp({ kind: 'carve', x: target.x, y: target.y + 18, r: 30, power: 99 });
- const fx = run(game, 10);
- expect(fx).toContainEqual(expect.objectContaining({ kind: 'cue', cue: 'turretDown' }));
- expect(game.roundState().turrets.map((t) => t.id)).toEqual([turrets[1].id]);
- });
- it('cannot build in thin air', () => {
- const game = newGame();
- const tatie = game.addPlayer('Tatie', 'tatie');
- game.teleport(tatie.id, 150, 300); // left of the cake: only the custard below
- run(game, 2);
- const t = me(game, tatie.id);
- const fx = press(game, tatie.id, Button.Special, t.x - 100, t.y);
- expect(fx.some((e) => e.kind === 'cue' && e.cue === 'build')).toBe(false);
- expect(game.roundState().turrets).toHaveLength(0);
- });
- });
- describe('Léon le Caméléon', () => {
- it('turns invisible, and shows up again when he attacks', () => {
- const game = newGame();
- const leon = game.addPlayer('Léon', 'leon');
- run(game, 60);
- const l = me(game, leon.id);
- const fx = press(game, leon.id, Button.Special, l.x, l.y);
- expect(fx).toContainEqual(expect.objectContaining({ kind: 'cue', cue: 'vanish' }));
- expect(me(game, leon.id).anim & Anim.Invisible).toBeTruthy();
- run(game, TICK_RATE);
- expect(me(game, leon.id).anim & Anim.Invisible).toBeTruthy();
- const back = press(game, leon.id, Button.Attack, l.x + 100, l.y);
- expect(back).toContainEqual(expect.objectContaining({ kind: 'cue', cue: 'appear' }));
- expect(me(game, leon.id).anim & Anim.Invisible).toBeFalsy();
- });
- it('invisibility wears off on its own, and a hit reveals him', () => {
- const game = newGame();
- const leon = game.addPlayer('Léon', 'leon');
- const cat = game.addPlayer('Chat', 'chat');
- game.teleport(leon.id, 600, 420);
- run(game, 60);
- const l = me(game, leon.id);
- press(game, leon.id, Button.Special, l.x, l.y);
- run(game, Math.ceil(loadCharacterStats(ASSETS, 'leon').params.invisibleTime * TICK_RATE) + 2);
- expect(me(game, leon.id).anim & Anim.Invisible).toBeFalsy();
- run(game, 6 * TICK_RATE); // special cooldown
- const l2 = me(game, leon.id);
- press(game, leon.id, Button.Special, l2.x, l2.y);
- expect(me(game, leon.id).anim & Anim.Invisible).toBeTruthy();
- game.teleport(cat.id, l2.x - 40, l2.y);
- run(game, 1);
- press(game, cat.id, Button.Attack, l2.x, l2.y);
- expect(me(game, leon.id).anim & Anim.Invisible).toBeFalsy();
- });
- it('his tongue pulls a player back to him and licks a groove in the cake', () => {
- const game = newGame();
- const leon = game.addPlayer('Léon', 'leon');
- const victim = game.addPlayer('Victime', 'generic');
- game.teleport(leon.id, 600, 420);
- run(game, 60);
- const l = me(game, leon.id);
- game.teleport(victim.id, l.x + 125, l.y - 10);
- run(game, 1);
- press(game, leon.id, Button.Attack, l.x + 125, l.y - 10);
- expect(me(game, victim.id).vx).toBeLessThan(-300);
- expect(me(game, victim.id).anim & Anim.Stunned).toBeTruthy();
- // Aimed down into the cake: a groove.
- const solid = (): number => game.terrain.data.reduce((n, m) => n + (m !== 0 ? 1 : 0), 0);
- run(game, TICK_RATE);
- const before = solid();
- const l2 = me(game, leon.id);
- press(game, leon.id, Button.Attack, l2.x + 60, l2.y + 100);
- expect(solid()).toBeLessThan(before - 200);
- });
- });
|