130 lines
4.4 KiB
TypeScript
130 lines
4.4 KiB
TypeScript
|
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|
import { traceProjectile } from '../CombatLogic';
|
|
import type { World, EntityId } from '../../../core/types';
|
|
import { EntityAccessor } from '../../EntityAccessor';
|
|
import { TileType } from '../../../core/terrain';
|
|
import { ECSWorld } from '../../ecs/World';
|
|
|
|
describe('CombatLogic', () => {
|
|
// Mock World
|
|
let mockWorld: World;
|
|
let ecsWorld: ECSWorld;
|
|
let accessor: EntityAccessor;
|
|
|
|
// Helper to set wall
|
|
const setWall = (x: number, y: number) => {
|
|
mockWorld.tiles[y * mockWorld.width + x] = TileType.WALL;
|
|
};
|
|
|
|
beforeEach(() => {
|
|
mockWorld = {
|
|
width: 10,
|
|
height: 10,
|
|
tiles: new Array(100).fill(TileType.EMPTY),
|
|
exit: { x: 9, y: 9 },
|
|
trackPath: []
|
|
};
|
|
ecsWorld = new ECSWorld();
|
|
// Shooter ID 1
|
|
accessor = new EntityAccessor(mockWorld, 1 as EntityId, ecsWorld);
|
|
});
|
|
|
|
function syncActor(actor: any) {
|
|
ecsWorld.addComponent(actor.id as EntityId, "position", actor.pos);
|
|
if (actor.category === 'combatant') {
|
|
ecsWorld.addComponent(actor.id as EntityId, "actorType", { type: actor.type });
|
|
ecsWorld.addComponent(actor.id as EntityId, "stats", { hp: 10 } as any);
|
|
if (actor.isPlayer) ecsWorld.addComponent(actor.id as EntityId, "player", {});
|
|
} else if (actor.category === 'item_drop') {
|
|
ecsWorld.addComponent(actor.id as EntityId, "groundItem", { item: actor.item || {} });
|
|
}
|
|
}
|
|
|
|
describe('traceProjectile', () => {
|
|
it('should travel full path if no obstacles', () => {
|
|
const start = { x: 0, y: 0 };
|
|
const end = { x: 5, y: 0 };
|
|
|
|
const result = traceProjectile(mockWorld, start, end, accessor);
|
|
|
|
expect(result.blockedPos).toEqual(end);
|
|
expect(result.hitActorId).toBeUndefined();
|
|
expect(result.path).toHaveLength(6);
|
|
});
|
|
|
|
it('should stop at wall', () => {
|
|
const start = { x: 0, y: 0 };
|
|
const end = { x: 5, y: 0 };
|
|
setWall(3, 0); // Wall at (3,0)
|
|
|
|
const result = traceProjectile(mockWorld, start, end, accessor);
|
|
|
|
expect(result.blockedPos).toEqual({ x: 2, y: 0 });
|
|
expect(result.hitActorId).toBeUndefined();
|
|
});
|
|
|
|
it('should stop at enemy', () => {
|
|
const start = { x: 0, y: 0 };
|
|
const end = { x: 5, y: 0 };
|
|
|
|
// Place enemy at (3,0)
|
|
const enemyId = 2 as EntityId;
|
|
const enemy = {
|
|
id: enemyId,
|
|
type: 'rat',
|
|
category: 'combatant',
|
|
pos: { x: 3, y: 0 },
|
|
isPlayer: false
|
|
};
|
|
syncActor(enemy);
|
|
|
|
const result = traceProjectile(mockWorld, start, end, accessor, 1 as EntityId); // Shooter 1
|
|
|
|
expect(result.blockedPos).toEqual({ x: 3, y: 0 });
|
|
expect(result.hitActorId).toBe(enemyId);
|
|
});
|
|
|
|
it('should ignore shooter position', () => {
|
|
const start = { x: 0, y: 0 };
|
|
const end = { x: 5, y: 0 };
|
|
|
|
// Shooter at start
|
|
const shooter = {
|
|
id: 1 as EntityId,
|
|
type: 'player',
|
|
category: 'combatant',
|
|
pos: { x: 0, y: 0 },
|
|
isPlayer: true
|
|
};
|
|
syncActor(shooter);
|
|
|
|
const result = traceProjectile(mockWorld, start, end, accessor, 1 as EntityId);
|
|
|
|
// Should not hit self
|
|
expect(result.hitActorId).toBeUndefined();
|
|
expect(result.blockedPos).toEqual(end);
|
|
});
|
|
|
|
it('should ignore non-combatant actors (e.g. items)', () => {
|
|
const start = { x: 0, y: 0 };
|
|
const end = { x: 5, y: 0 };
|
|
|
|
// Item at (3,0)
|
|
const item = {
|
|
id: 99 as EntityId,
|
|
category: 'item_drop',
|
|
pos: { x: 3, y: 0 },
|
|
item: { name: 'Test Item' }
|
|
};
|
|
syncActor(item);
|
|
|
|
const result = traceProjectile(mockWorld, start, end, accessor);
|
|
|
|
// Should pass through item
|
|
expect(result.blockedPos).toEqual(end);
|
|
expect(result.hitActorId).toBeUndefined();
|
|
});
|
|
});
|
|
});
|