RED→GREEN: updated test expectations first (4 failures on velocity), then changed shells.js data, then fixed all downstream hardcoded velocity values in ammo/projectile/integration tests. 142 tests pass across 4 suites; integration suite failures are pre-existing Phaser mock issues.
428 lines
12 KiB
JavaScript
428 lines
12 KiB
JavaScript
/**
|
|
* Slice 2 Integration Tests — verify combat loop wiring in MainGame.
|
|
*
|
|
* Tests that MainGame imports, constructs, and wires four new systems:
|
|
* AmmoSystem, PatternManager, Enemy, Projectile.
|
|
*
|
|
* Unit tests for each system already exist in:
|
|
* tests/slice2_ammo.test.js, tests/slice2_enemy.test.js
|
|
* tests/slice2_patternmanager.test.js, tests/slice2_projectile.test.js
|
|
*/
|
|
|
|
// ── Phaser mock (inline factory — moduleNameMapper bypasses __mocks__/) ─
|
|
jest.mock('phaser', () => {
|
|
class Scene {
|
|
constructor(config) {
|
|
this.scene = config;
|
|
this.game = { __saveManager: undefined };
|
|
}
|
|
}
|
|
|
|
Scene.prototype.add = {
|
|
image() { return { setOrigin() { return this; } }; },
|
|
rectangle(x, y) { return { setOrigin() { return this; }, rotation: 0, x, y }; },
|
|
existing() { return this; },
|
|
graphics() {
|
|
const g = {
|
|
setDepth() { return g; }, clear() {}, fillStyle() {}, fillRect() {},
|
|
setBlendMode() {}, beginPath() {}, moveTo() {}, lineTo() {},
|
|
closePath() {}, fillPath() {}, strokePath() {}, lineStyle() {},
|
|
lineBetween() {}, strokeCircle() {},
|
|
};
|
|
return g;
|
|
},
|
|
text(x, y, str) { return { setDepth() { return this; }, x, y, text: str }; },
|
|
};
|
|
|
|
Scene.prototype.cameras = { main: { startFollow() {} } };
|
|
Scene.prototype.textures = { exists() { return false; } };
|
|
Scene.prototype.input = {
|
|
keyboard: {
|
|
addKeys() {
|
|
return {
|
|
W: { isDown: false }, A: { isDown: false },
|
|
S: { isDown: false }, D: { isDown: false },
|
|
E: { isDown: false },
|
|
ONE: { isDown: false }, TWO: { isDown: false },
|
|
THREE: { isDown: false }, FOUR: { isDown: false },
|
|
};
|
|
},
|
|
},
|
|
on() {},
|
|
};
|
|
|
|
Scene.prototype.physics = {
|
|
add: {
|
|
group() {
|
|
return {
|
|
getFirstDead() { return null; },
|
|
create(x, y) { return { active: true, visible: true, x, y, body: { enable: true, velocity: { x: 0, y: 0 } } }; },
|
|
killAndHide(sprite) { if (sprite) { sprite.active = false; sprite.visible = false; } },
|
|
getLength() { return 0; },
|
|
getChildren() { return []; },
|
|
clear() {},
|
|
};
|
|
},
|
|
existing() {},
|
|
overlap() {},
|
|
collider() {},
|
|
},
|
|
};
|
|
|
|
Scene.prototype.scene = { scenes: [], start() {} };
|
|
Scene.prototype.scale = { width: 640, height: 360 };
|
|
Scene.prototype.make = { graphics() { return {}; } };
|
|
Scene.prototype.load = { image() {} };
|
|
|
|
let Phaser = {
|
|
__esModule: true,
|
|
Scene,
|
|
Physics: { Arcade: { Sprite: class {} } },
|
|
Input: {
|
|
Keyboard: {
|
|
KeyCodes: {
|
|
W: 87, A: 65, S: 83, D: 68, E: 69,
|
|
ONE: 49, TWO: 50, THREE: 51, FOUR: 52,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
Phaser.default = Phaser;
|
|
return Phaser;
|
|
});
|
|
|
|
const { MainGame } = require('../src/game/scenes/MainGame');
|
|
const { AmmoSystem } = require('../src/game/systems/AmmoSystem');
|
|
const { PatternManager } = require('../src/game/systems/PatternManager');
|
|
const { Enemy } = require('../src/game/entities/Enemy');
|
|
const { Projectile } = require('../src/game/entities/Projectile');
|
|
const { shells } = require('../src/data/shells');
|
|
const { enemies } = require('../src/data/enemies');
|
|
|
|
|
|
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
|
|
function makeMG() {
|
|
return new MainGame();
|
|
}
|
|
|
|
|
|
// ── Suite 1: Import verification ────────────────────────────────────────
|
|
|
|
describe('Slice 2 — Import verification', () => {
|
|
test('MainGame imports AmmoSystem', () => {
|
|
expect(AmmoSystem).toBeDefined();
|
|
expect(typeof AmmoSystem).toBe('function');
|
|
});
|
|
|
|
test('MainGame imports PatternManager', () => {
|
|
expect(PatternManager).toBeDefined();
|
|
expect(typeof PatternManager).toBe('function');
|
|
});
|
|
|
|
test('MainGame imports Enemy', () => {
|
|
expect(Enemy).toBeDefined();
|
|
expect(typeof Enemy).toBe('function');
|
|
});
|
|
|
|
test('MainGame imports Projectile', () => {
|
|
expect(Projectile).toBeDefined();
|
|
expect(typeof Projectile).toBe('function');
|
|
});
|
|
|
|
test('shells data has APCBC', () => {
|
|
expect(shells).toBeDefined();
|
|
expect(shells.apcbc).toBeDefined();
|
|
});
|
|
|
|
test('enemies data has type59', () => {
|
|
expect(enemies).toBeDefined();
|
|
expect(enemies.type59).toBeDefined();
|
|
});
|
|
});
|
|
|
|
|
|
// ── Suite 2: AmmoSystem wiring ──────────────────────────────────────────
|
|
|
|
describe('Slice 2 — AmmoSystem integration', () => {
|
|
test('MainGame constructs AmmoSystem during create()', () => {
|
|
const mg = makeMG();
|
|
mg.create();
|
|
|
|
expect(mg.ammoSystem).toBeDefined();
|
|
expect(mg.ammoSystem instanceof AmmoSystem).toBe(true);
|
|
});
|
|
|
|
test('AmmoSystem inventory populated from shells.js', () => {
|
|
const mg = makeMG();
|
|
mg.create();
|
|
|
|
const inv = mg.ammoSystem.getInventory();
|
|
expect(inv.apcbc).toBe(87);
|
|
expect(inv.apcr).toBe(6);
|
|
expect(inv.he).toBe(20);
|
|
expect(inv.heat).toBe(10);
|
|
});
|
|
|
|
test('key 1 selects APCBC', () => {
|
|
const mg = makeMG();
|
|
mg.create();
|
|
mg.keys.ONE.isDown = true;
|
|
mg.update(0, 16);
|
|
expect(mg.ammoSystem._active).toBe('apcbc');
|
|
});
|
|
|
|
test('key 2 selects APCR', () => {
|
|
const mg = makeMG();
|
|
mg.create();
|
|
mg.keys.TWO.isDown = true;
|
|
mg.update(0, 16);
|
|
expect(mg.ammoSystem._active).toBe('apcr');
|
|
});
|
|
|
|
test('key 3 selects HE', () => {
|
|
const mg = makeMG();
|
|
mg.create();
|
|
mg.keys.THREE.isDown = true;
|
|
mg.update(0, 16);
|
|
expect(mg.ammoSystem._active).toBe('he');
|
|
});
|
|
|
|
test('key 4 selects HEAT', () => {
|
|
const mg = makeMG();
|
|
mg.create();
|
|
mg.keys.FOUR.isDown = true;
|
|
mg.update(0, 16);
|
|
expect(mg.ammoSystem._active).toBe('heat');
|
|
});
|
|
|
|
test('AmmoSystem.fire() returns projectile config', () => {
|
|
const mg = makeMG();
|
|
mg.create();
|
|
|
|
const result = mg.ammoSystem.fire();
|
|
expect(result.type).toBeDefined();
|
|
expect(result.velocity).toBeDefined();
|
|
expect(result.penetration).toBeDefined();
|
|
});
|
|
|
|
test('ammo depletes on fire', () => {
|
|
const mg = makeMG();
|
|
mg.create();
|
|
|
|
const before = mg.ammoSystem.getTotalRemaining();
|
|
mg.ammoSystem.fire();
|
|
const after = mg.ammoSystem.getTotalRemaining();
|
|
expect(after).toBe(before - 1);
|
|
});
|
|
|
|
test('APCR fire uses velocity 1200', () => {
|
|
const mg = makeMG();
|
|
mg.create();
|
|
|
|
mg.ammoSystem.selectShell('apcr');
|
|
const result = mg.ammoSystem.fire();
|
|
expect(result.velocity).toBe(1200);
|
|
expect(result.type).toBe('apcr');
|
|
});
|
|
});
|
|
|
|
|
|
// ── Suite 3: Enemy spawn wiring ─────────────────────────────────────────
|
|
|
|
describe('Slice 2 — Enemy integration', () => {
|
|
test('MainGame initializes enemies array in create()', () => {
|
|
const mg = makeMG();
|
|
mg.create();
|
|
|
|
expect(Array.isArray(mg.enemies)).toBe(true);
|
|
});
|
|
|
|
test('has enemy spawn timer state', () => {
|
|
const mg = makeMG();
|
|
mg.create();
|
|
|
|
expect(mg._enemySpawnTimer).toBeDefined();
|
|
expect(typeof mg._enemySpawnTimer).toBe('number');
|
|
});
|
|
|
|
test('enemy spawn timer accumulates in update()', () => {
|
|
const mg = makeMG();
|
|
mg.create();
|
|
|
|
const before = mg._enemySpawnTimer;
|
|
mg.update(0, 16);
|
|
expect(mg._enemySpawnTimer).toBe(before + 16);
|
|
});
|
|
|
|
test('spawn triggers after timer threshold (3s)', () => {
|
|
const mg = makeMG();
|
|
mg.create();
|
|
|
|
mg._enemySpawnTimer = 3000;
|
|
mg.update(0, 16);
|
|
|
|
expect(mg.enemies.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
test('spawned enemies are Enemy instances', () => {
|
|
const mg = makeMG();
|
|
mg.create();
|
|
|
|
mg._enemySpawnTimer = 3000;
|
|
mg.update(0, 16);
|
|
|
|
for (const enemy of mg.enemies) {
|
|
expect(enemy instanceof Enemy).toBe(true);
|
|
expect(enemy.active).toBe(true);
|
|
}
|
|
});
|
|
|
|
test('enemy update() called each frame', () => {
|
|
const mg = makeMG();
|
|
mg.create();
|
|
|
|
const updateSpy = jest.fn();
|
|
const spyEnemy = {
|
|
active: true, typeKey: 'type62', x: 400, y: 200,
|
|
update: updateSpy, executePattern: jest.fn(),
|
|
patterns: ['infantry_wall'],
|
|
};
|
|
mg.enemies.push(spyEnemy);
|
|
|
|
mg.update(0, 16);
|
|
expect(updateSpy).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test('dead enemies (active=false) are purged', () => {
|
|
const mg = makeMG();
|
|
mg.create();
|
|
|
|
mg.enemies = [
|
|
{ active: true, x: 400, y: 200, update: jest.fn(), executePattern: jest.fn(), patterns: [] },
|
|
{ active: false, x: 500, y: 200, update: jest.fn(), executePattern: jest.fn(), patterns: [] },
|
|
{ active: true, x: 600, y: 200, update: jest.fn(), executePattern: jest.fn(), patterns: [] },
|
|
];
|
|
|
|
mg.update(0, 16);
|
|
|
|
expect(mg.enemies.length).toBe(2);
|
|
expect(mg.enemies.every(e => e.active)).toBe(true);
|
|
});
|
|
});
|
|
|
|
|
|
// ── Suite 4: PatternManager + Enemy trigger ─────────────────────────────
|
|
|
|
describe('Slice 2 — PatternManager + Enemy trigger', () => {
|
|
test('patternManager is accessible on MainGame', () => {
|
|
const mg = makeMG();
|
|
mg.create();
|
|
|
|
expect(mg.patternManager).toBeDefined();
|
|
expect(mg.patternManager instanceof PatternManager).toBe(true);
|
|
});
|
|
|
|
test('patternManager.trigger() fires a bullet pattern', () => {
|
|
const mg = makeMG();
|
|
mg.create();
|
|
|
|
const result = mg.patternManager.trigger('infantry_wall', { x: 320, y: 180 });
|
|
expect(result).toBeDefined();
|
|
expect(result.fired).toBe(true);
|
|
expect(result.count).toBeGreaterThan(0);
|
|
});
|
|
|
|
test('enemy executePattern delegates to patternManager.trigger()', () => {
|
|
const mg = makeMG();
|
|
mg.create();
|
|
|
|
const triggerSpy = jest.fn(() => ({
|
|
patternId: 'infantry_wall', count: 5, projectiles: [],
|
|
staggered: false, delayed: false, fired: true, rejected: false, reason: null,
|
|
}));
|
|
mg.patternManager.trigger = triggerSpy;
|
|
|
|
const mockEnemy = {
|
|
active: true, x: 400, y: 200, typeKey: 'type62',
|
|
patterns: ['infantry_wall'],
|
|
update() { this.executePattern(this.patterns[0]); },
|
|
executePattern(pid) {
|
|
return mg.patternManager.trigger(pid, { x: this.x, y: this.y });
|
|
},
|
|
};
|
|
mg.enemies = [mockEnemy];
|
|
|
|
mg.update(0, 16);
|
|
|
|
expect(triggerSpy).toHaveBeenCalledWith('infantry_wall', { x: 400, y: 200 });
|
|
});
|
|
|
|
test('patternManager rejects when zone at capacity', () => {
|
|
const mg = makeMG();
|
|
mg.create();
|
|
|
|
// Fill zone 1 past capacity via pending count
|
|
mg.patternManager._zoneActive[1] = [];
|
|
mg.patternManager._zonePending[1] = 2; // exactly at capacity (max 2)
|
|
|
|
const result = mg.patternManager.trigger('infantry_wall', { x: 320, y: 180 }, { zone: 1 });
|
|
expect(result.rejected).toBe(true);
|
|
});
|
|
});
|
|
|
|
|
|
// ── Suite 5: Keyboard keys ──────────────────────────────────────────────
|
|
|
|
describe('Slice 2 — Keyboard addKeys', () => {
|
|
test('addKeys registered for WASD + E + 1-4', () => {
|
|
const mg = makeMG();
|
|
mg.create();
|
|
|
|
expect(mg.keys.W).toBeDefined();
|
|
expect(mg.keys.A).toBeDefined();
|
|
expect(mg.keys.S).toBeDefined();
|
|
expect(mg.keys.D).toBeDefined();
|
|
expect(mg.keys.E).toBeDefined();
|
|
expect(mg.keys.ONE).toBeDefined();
|
|
expect(mg.keys.TWO).toBeDefined();
|
|
expect(mg.keys.THREE).toBeDefined();
|
|
expect(mg.keys.FOUR).toBeDefined();
|
|
});
|
|
|
|
test('key default state is not pressed', () => {
|
|
const mg = makeMG();
|
|
mg.create();
|
|
|
|
expect(mg.keys.W.isDown).toBe(false);
|
|
expect(mg.keys.ONE.isDown).toBe(false);
|
|
expect(mg.keys.FOUR.isDown).toBe(false);
|
|
});
|
|
});
|
|
|
|
|
|
// ── Suite 6: Projectile group wiring ────────────────────────────────────
|
|
|
|
describe('Slice 2 — Projectile group setup', () => {
|
|
test('MainGame sets up projectile group for player projectiles', () => {
|
|
const mg = makeMG();
|
|
mg.create();
|
|
|
|
expect(mg.projectileGroup).toBeDefined();
|
|
});
|
|
|
|
test('MainGame sets up projectile group for enemy projectiles', () => {
|
|
const mg = makeMG();
|
|
mg.create();
|
|
|
|
expect(mg.enemyProjectileGroup).toBeDefined();
|
|
});
|
|
|
|
test('projectile groups are distinct objects', () => {
|
|
const mg = makeMG();
|
|
mg.create();
|
|
|
|
expect(mg.projectileGroup).not.toBe(mg.enemyProjectileGroup);
|
|
});
|
|
});
|