Files
restitution/tests/Unit.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

281 lines
7.2 KiB
JavaScript

/**
* Unit Entity Unit Tests
*/
jest.mock('Systems/EntityStateMachine', () => ({
forEntity: jest.fn(() => ({
tick: jest.fn(),
send: jest.fn(),
destroy: jest.fn(),
getState: jest.fn(() => 'IDLING')
}))
}));
import Unit from '../src/entities/Unit';
import EntityStateMachine from 'Systems/EntityStateMachine';
const createMockScene = () => ({
add: {
existing: jest.fn()
},
physics: {
world: {
enableBody: jest.fn()
}
},
interface: {
generateWorldXY: jest.fn(tile => ({ x: tile.x * 64, y: tile.y * 64 }))
},
orchestrator: {
systems: {
EntityStateMachine: { forEntity: jest.fn() },
combat: { fireProjectile: jest.fn() },
pathfinding: { findPath: jest.fn() },
selection: { add: jest.fn() }
}
},
events: {
emit: jest.fn()
},
tweens: {
addCounter: jest.fn(() => ({ stop: jest.fn() }))
}
});
describe('Unit', () => {
let scene;
let unit;
beforeEach(() => {
scene = createMockScene();
unit = new Unit(scene, 'tank_texture', { x: 5, y: 5 }, {
maxHp: 100,
armor: 5,
playerId: 'player1',
team: 'good',
weaponRange: 200,
damage: 25
});
});
describe('Component Access', () => {
it('should have health component', () => {
const health = unit.getComponent('health');
expect(health.maxHp).toBe(100);
expect(health.current).toBe(100);
expect(health.armor).toBe(5);
});
it('should have owner component', () => {
const owner = unit.getComponent('owner');
expect(owner.playerId).toBe('player1');
expect(owner.team).toBe('good');
});
it('should have combat component', () => {
const combat = unit.getComponent('combat');
expect(combat.weaponRange).toBe(200);
expect(combat.damage).toBe(25);
});
it('should update component with setComponent', () => {
unit.setComponent('health', { current: 50 });
const health = unit.getComponent('health');
expect(health.current).toBe(50);
expect(health.maxHp).toBe(100); // Unchanged
});
});
describe('Damage System', () => {
it('should apply damage with armor reduction', () => {
const damageTaken = unit.damage(30, 'rifle');
expect(damageTaken).toBeLessThanOrEqual(25); // 30 - 5 armor
expect(unit.getComponent('health').current).toBeLessThan(100);
});
it('should apply minimum 1 damage', () => {
const damageTaken = unit.damage(2, 'rifle');
expect(damageTaken).toBeGreaterThanOrEqual(1);
});
it('should emit unit:damaged event', () => {
unit.damage(20);
expect(scene.events.emit).toHaveBeenCalledWith(
'unit:damaged',
expect.objectContaining({
unit: unit,
amount: expect.any(Number),
remaining: expect.any(Number)
})
);
});
it('should mark unit as dead when health reaches 0', () => {
unit.getComponent('health').current = 5;
unit.damage(10);
expect(unit.dead).toBe(true);
});
it('should not damage if already dead', () => {
unit.dead = true;
const damageTaken = unit.damage(50);
expect(damageTaken).toBe(0);
});
});
describe('Heal System', () => {
it('should heal unit', () => {
unit.damage(30);
const healed = unit.heal(20);
expect(healed).toBe(20);
expect(unit.getComponent('health').current).toBe(90);
});
it('should not exceed max HP', () => {
const healed = unit.heal(50);
expect(unit.getComponent('health').current).toBe(100); // Capped at max
});
});
describe('Combat', () => {
let target;
beforeEach(() => {
target = {
x: 150,
y: 0,
isDead: jest.fn(() => false),
getData: jest.fn(() => ({ playerId: 'enemy' }))
};
});
it('should return true when target in range', () => {
expect(unit.canHitBody(target)).toBe(true);
});
it('should return false when target out of range', () => {
target.x = 300; // Beyond 200 range
expect(unit.canHitBody(target)).toBe(false);
});
it('should return false when target is dead', () => {
target.isDead = jest.fn(() => true);
expect(unit.canHitBody(target)).toBe(false);
});
it('should attack target when in range', () => {
const result = unit.attackTarget(target);
expect(result).toBe(true);
expect(scene.orchestrator.systems.combat.fireProjectile).toHaveBeenCalled();
});
it('should not attack when target out of range', () => {
target.x = 300;
const result = unit.attackTarget(target);
expect(result).toBe(false);
expect(scene.orchestrator.systems.combat.fireProjectile).not.toHaveBeenCalled();
});
it('should respect fire rate', () => {
unit.attackTarget(target);
const result2 = unit.attackTarget(target);
// Second attack should fail due to fire rate cooldown
expect(result2).toBe(false);
});
});
describe('Selection', () => {
it('should select unit', () => {
unit.select();
expect(unit.getData('selected')).toBe(true);
expect(scene.tweens.addCounter).toHaveBeenCalled();
});
it('should unselect unit', () => {
unit.select();
unit.unSelect();
expect(unit.getData('selected')).toBe(false);
});
it('should tint based on team', () => {
unit.select();
expect(unit.setTint).toHaveBeenCalled();
});
});
describe('Movement', () => {
it('should move to tile', () => {
const result = unit.moveToTile({ x: 10, y: 10 });
expect(result).toBe(true);
expect(unit.setPosition).toHaveBeenCalledWith(640, 640); // 10 * 64
});
it('should set target tile data', () => {
unit.moveToTile({ x: 10, y: 10 });
expect(unit.getData('targetTile')).toEqual({ x: 10, y: 10 });
});
it('should orient to target', () => {
const target = { x: 100, y: 0 };
unit.orientToTarget(target);
expect(unit.setFlipX).toHaveBeenCalledWith(true); // EAST direction
});
});
describe('State Machine', () => {
it('should initialize state machine', () => {
expect(unit.stateMachine).toBeDefined();
expect(scene.orchestrator.systems.EntityStateMachine.forEntity).toHaveBeenCalled();
});
it('should tick state machine in preUpdate', () => {
const tickSpy = jest.spyOn(unit.stateMachine, 'tick');
unit.preUpdate(Date.now(), 16);
expect(tickSpy).toHaveBeenCalled();
});
});
describe('Death', () => {
it('should trigger death when health reaches 0', () => {
const dieSpy = jest.spyOn(unit.stateMachine, 'send');
unit.getComponent('health').current = 5;
unit.damage(10);
expect(dieSpy).toHaveBeenCalledWith('DIE');
expect(scene.events.emit).toHaveBeenCalledWith('unit:dying', expect.anything());
});
it('should cleanup on destroy', () => {
unit.pulse = { stop: jest.fn() };
const destroySpy = jest.spyOn(unit.stateMachine, 'destroy');
unit.destroy();
expect(unit.pulse.stop).toHaveBeenCalled();
expect(destroySpy).toHaveBeenCalled();
});
});
});