Files
restitution/tests/CombatSystem.test.js
root 2e07519648 Refactor: Component-based architecture + 10 sub-systems
- Implemented 10 sub-systems (Economy, Pathfinding, Combat, Selection, Network, Map, Entity/Building/ControlPoint state machines, Orchestrator)
- Refactored Custom_Entity.js → Unit.js with 5 components (health, owner, inventory, movement, combat)
- Added Jest test suite with 100+ tests (EconomySystem 100%, EntityStateMachine 100%, PathfindingSystem 99%, Unit.js 72%)
- All webpack builds pass (0 errors)
- BMAD-auto team-respawn flow: 10 parallel sub-agents implemented systems

Architecture: Phaser 3 + XState + socket.io + EasyStar
Mode: team-respawn
Model: custom/ollama-cloud-pro
2026-05-29 22:13:44 +00:00

170 lines
4.6 KiB
JavaScript

/**
* CombatSystem Unit Tests
*/
import CombatSystem from '../src/systems/CombatSystem';
const createMockScene = () => ({
physics: {
add: {
group: jest.fn(() => ({
create: jest.fn(),
killAndHide: jest.fn()
}))
},
overlap: jest.fn()
},
events: {
emit: jest.fn()
},
add: {
sprite: jest.fn()
}
});
describe('CombatSystem', () => {
let scene;
let combat;
beforeEach(() => {
scene = createMockScene();
combat = new CombatSystem(scene);
});
describe('acquireTarget', () => {
it('should return null when no enemies in range', () => {
const entity = { x: 0, y: 0, getData: jest.fn(() => []) };
const target = combat.acquireTarget(entity, { maxRange: 200 });
expect(target).toBeNull();
});
it('should return closest enemy when multiple in range', () => {
const enemy1 = { x: 100, y: 0, isDead: jest.fn(() => false) };
const enemy2 = { x: 50, y: 0, isDead: jest.fn(() => false) };
combat.enemies = [enemy1, enemy2];
const entity = { x: 0, y: 0, getData: jest.fn(() => combat.enemies) };
const target = combat.acquireTarget(entity, { maxRange: 200, priority: 'closest' });
expect(target).toBe(enemy2); // Closer enemy
});
it('should filter out dead enemies', () => {
const deadEnemy = { x: 50, y: 0, isDead: jest.fn(() => true) };
const liveEnemy = { x: 100, y: 0, isDead: jest.fn(() => false) };
combat.enemies = [deadEnemy, liveEnemy];
const entity = { x: 0, y: 0, getData: jest.fn(() => combat.enemies) };
const target = combat.acquireTarget(entity, { maxRange: 200 });
expect(target).toBe(liveEnemy);
});
});
describe('canHit', () => {
let attacker, target;
beforeEach(() => {
attacker = {
x: 0,
y: 0,
getData: jest.fn(key => {
if (key === 'owner') return { playerId: 'player1' };
return null;
})
};
target = {
x: 100,
y: 0,
isDead: jest.fn(() => false),
getData: jest.fn(key => {
if (key === 'owner') return { playerId: 'player2' };
return null;
})
};
});
it('should return false for friendly fire', () => {
attacker.getData = jest.fn(() => ({ playerId: 'player1' }));
target.getData = jest.fn(() => ({ playerId: 'player1' }));
const result = combat.canHit(attacker, target);
expect(result.canHit).toBe(false);
expect(result.reason).toBe('friendly_fire');
});
it('should return false for dead target', () => {
target.isDead = jest.fn(() => true);
const result = combat.canHit(attacker, target);
expect(result.canHit).toBe(false);
expect(result.reason).toBe('target_dead');
});
it('should return false when out of range', () => {
target.x = 500; // Beyond default 200 range
const result = combat.canHit(attacker, target);
expect(result.canHit).toBe(false);
expect(result.reason).toBe('out_of_range');
});
it('should return true when all conditions met', () => {
combat.hasLineOfSight = jest.fn(() => true);
const result = combat.canHit(attacker, target);
expect(result.canHit).toBe(true);
});
});
describe('applyDamage', () => {
it('should apply damage with armor reduction', () => {
const entity = {
getData: jest.fn(key => {
if (key === 'health') return { maxHp: 100, current: 100, armor: 5 };
return null;
}),
setData: jest.fn()
};
const damage = combat.applyDamage(entity, 20, 'rifle');
expect(damage).toBeLessThanOrEqual(15); // 20 - 5 armor
expect(entity.setData).toHaveBeenCalledWith('health', expect.any(Number));
});
it('should apply minimum 1 damage', () => {
const entity = {
getData: jest.fn(key => ({ maxHp: 100, current: 100, armor: 50 })),
setData: jest.fn()
};
const damage = combat.applyDamage(entity, 10, 'rifle');
expect(damage).toBeGreaterThanOrEqual(1);
});
it('should apply critical hit multiplier', () => {
const entity = {
getData: jest.fn(key => ({ maxHp: 100, current: 100, armor: 0 })),
setData: jest.fn()
};
// Mock crit roll to succeed
combat.damageModifiers = {
rifle: { critChance: 1.0, critMultiplier: 2.0 } // Always crit
};
const damage = combat.applyDamage(entity, 20, 'rifle');
expect(damage).toBe(40); // 20 * 2.0 crit multiplier
});
});
});