Begin refactoring GameScene

This commit is contained in:
Peter Stockings
2026-01-26 15:30:14 +11:00
parent 1d7be54fd9
commit ef7d85750f
46 changed files with 2459 additions and 1291 deletions

View File

@@ -1,54 +1,54 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { traceProjectile } from '../CombatLogic';
import type { World } from '../../../core/types';
import { EntityManager } from '../../EntityManager';
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
const mockWorld: World = {
width: 10,
height: 10,
tiles: new Array(100).fill(TileType.EMPTY),
actors: new Map(),
exit: { x: 9, y: 9 }
};
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;
};
// Helper to clear world
const clearWorld = () => {
mockWorld.tiles.fill(TileType.EMPTY);
mockWorld.actors.clear();
};
// Mock EntityManager
const mockEntityManager = {
getActorsAt: (x: number, y: number) => {
return [...mockWorld.actors.values()].filter(a => a.pos.x === x && a.pos.y === y);
}
} as unknown as EntityManager;
beforeEach(() => {
clearWorld();
mockWorld = {
width: 10,
height: 10,
tiles: new Array(100).fill(TileType.EMPTY),
exit: { x: 9, y: 9 }
};
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, mockEntityManager);
const result = traceProjectile(mockWorld, start, end, accessor);
expect(result.blockedPos).toEqual(end);
expect(result.hitActorId).toBeUndefined();
// Path should be (0,0) -> (1,0) -> (2,0) -> (3,0) -> (4,0) -> (5,0)
// But raycast implementation includes start?
// CombatLogic logic: "skip start" -> loop i=1
// So result.path is full array from raycast.
expect(result.path).toHaveLength(6);
});
@@ -57,7 +57,7 @@ describe('CombatLogic', () => {
const end = { x: 5, y: 0 };
setWall(3, 0); // Wall at (3,0)
const result = traceProjectile(mockWorld, start, end, mockEntityManager);
const result = traceProjectile(mockWorld, start, end, accessor);
expect(result.blockedPos).toEqual({ x: 2, y: 0 });
expect(result.hitActorId).toBeUndefined();
@@ -68,17 +68,17 @@ describe('CombatLogic', () => {
const end = { x: 5, y: 0 };
// Place enemy at (3,0)
const enemyId = 2;
mockWorld.actors.set(enemyId, {
const enemyId = 2 as EntityId;
const enemy = {
id: enemyId,
type: 'rat',
category: 'combatant',
pos: { x: 3, y: 0 },
isPlayer: false
// ... other props mocked if needed
} as any);
};
syncActor(enemy);
const result = traceProjectile(mockWorld, start, end, mockEntityManager, 1); // Shooter 1
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);
@@ -89,15 +89,16 @@ describe('CombatLogic', () => {
const end = { x: 5, y: 0 };
// Shooter at start
mockWorld.actors.set(1, {
id: 1,
const shooter = {
id: 1 as EntityId,
type: 'player',
category: 'combatant',
pos: { x: 0, y: 0 },
isPlayer: true
} as any);
};
syncActor(shooter);
const result = traceProjectile(mockWorld, start, end, mockEntityManager, 1);
const result = traceProjectile(mockWorld, start, end, accessor, 1 as EntityId);
// Should not hit self
expect(result.hitActorId).toBeUndefined();
@@ -109,13 +110,15 @@ describe('CombatLogic', () => {
const end = { x: 5, y: 0 };
// Item at (3,0)
mockWorld.actors.set(99, {
id: 99,
const item = {
id: 99 as EntityId,
category: 'item_drop',
pos: { x: 3, y: 0 },
} as any);
item: { name: 'Test Item' }
};
syncActor(item);
const result = traceProjectile(mockWorld, start, end, mockEntityManager);
const result = traceProjectile(mockWorld, start, end, accessor);
// Should pass through item
expect(result.blockedPos).toEqual(end);