Files
Binding-Fignyaac/tests/movement.test.ts
mayatnikov a72172df72 refactor: комплексный рефакторинг + Isaac-like геймплей
Баги и гигиена (Wave 1):
- Player.addWeapon использует MODE_MELEE вместо литерала 1
- Фильтрация мёртвых врагов из room.enemies (раньше массив рос в долгих боях)
- Кап миньёнов босса (BOSS.maxMinions=4) — иначе комната могла не зачиститься
- aliveCount корректно считает живых в knockback-фазе
- equipSlot валидирует диапазон слота
- Магические числа range=70/spread=0.15 вынесены в WeaponDef (beamRange/spread)
- Регрессия cleared-flag: после фильтрации массива длина 0, но cleared должен стать true

Распил Game.ts 683→465 строк (Wave 2):
- systems/movement.ts: moveEntity(e, dx, dy, room) — убрал 5 копий коллизионного паттерна
- systems/projectiles.ts: applyWeaponProjectileStats, explodeBomb, projectileHitWall
- systems/ai.ts: runAI через диспетчер-таблицу (вместо if/else-if каскада)
- Переходы/этажи оставлены в Game.ts (тесно завязаны на cc/cr/player)

Тесты 33→72 (Wave 3):
- movement.test.ts, projectiles.test.ts, combat.test.ts, regressions.test.ts, items.test.ts
- Покрытие: ближний/дальний бой, огнемёт, бомба, лазер, splitter, сундук→пикап,
  переход комнат, фазы босса + кап миньёнов, фильтрация мёртвых, секретка

Isaac-like геймплей (Wave 4):
- Статы игрока: damageMul/fireRateMul/rangeMul/shotSpeedMul (мультипликативно
  поверх WeaponDef), effectiveDamage/Cooldown, отображение в HUD
- 8-направленный прицел: aimDir: Dir → aimVec: {x,y}, стрелки дают диагонали
- Пассивные предметы (items.ts, 6 штук): сундук дропает 50/50 оружие/предмет
- Новый тип врага splitter: при смерти распадается на двух fast
- Новый тип комнаты secret: +1 max HP один раз при первом входе
2026-06-19 16:50:15 +03:00

67 lines
2.2 KiB
TypeScript

import { describe, it, expect } from 'bun:test';
import { OX, OY, TILE, COLS, ROWS } from '../src/config';
import { Room } from '../src/core/world/Room';
import { moveEntity, type Movable } from '../src/core/systems/movement';
/** Минимальный movable, на котором удобно тестировать moveEntity. */
function box(x: number, y: number, size = 20): Movable & { x: number; y: number } {
const obj: Movable & { x: number; y: number } = {
x, y,
get box() {
return { x: this.x - size / 2, y: this.y - size / 2, w: size, h: size };
},
};
return obj;
}
/** Свежая зачищенная комната: двери открыты, по периметру стены. */
function makeRoom() {
const r = new Room(0, 0, 'spawn');
r.cleared = true;
r.rebuildTiles();
return r;
}
describe('moveEntity', () => {
it('свободно двигается по полу', () => {
const room = makeRoom();
const cx = OX + (COLS / 2) * TILE;
const cy = OY + (ROWS / 2) * TILE;
const e = box(cx, cy);
moveEntity(e, 10, 5, room);
expect(e.x).toBe(cx + 10);
expect(e.y).toBe(cy + 5);
});
it('не проходит сквозь стену по X, но Y применяется (скольжение)', () => {
const room = makeRoom();
const x = OX + 3 * TILE;
const y = OY + (ROWS / 2) * TILE;
const e = box(x, y, 16);
// Большой рывок влево — хитбокс вылетит за col=0, isBlocked вернётся true.
moveEntity(e, -500, 10, room);
expect(e.x).toBe(x); // откатилось
expect(e.y).toBe(y + 10); // применилось
});
it('не проходит сквозь стену по Y, X применяется', () => {
const room = makeRoom();
const x = OX + (COLS / 2) * TILE;
const y = OY + 3 * TILE;
const e = box(x, y, 16);
moveEntity(e, 7, -500, room);
expect(e.x).toBe(x + 7);
expect(e.y).toBe(y);
});
it('ни X, ни Y не применяются, если в углу', () => {
const room = makeRoom();
const x = OX + 3 * TILE;
const y = OY + 3 * TILE;
const e = box(x, y, 16);
moveEntity(e, -200, -200, room);
expect(e.x).toBe(x);
expect(e.y).toBe(y);
});
});