feat: add game loop with NPC spawning and system execution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 18:53:19 -05:00
parent 8b2e895da2
commit ca05d36bc0
3 changed files with 105 additions and 0 deletions
+76
View File
@@ -0,0 +1,76 @@
import { TICK_RATE, BROADCAST_EVERY_N_TICKS } from '@dflike/shared';
import { World } from '../ecs/World.js';
import { GameMap } from '../map/GameMap.js';
import { needsDecaySystem } from '../systems/needsDecaySystem.js';
import { npcBrainSystem } from '../systems/npcBrainSystem.js';
import { movementSystem } from '../systems/movementSystem.js';
import { spawnNPC } from './spawner.js';
export class GameLoop {
readonly world: World;
readonly map: GameMap;
private tick = 0;
private interval: ReturnType<typeof setInterval> | null = null;
private onBroadcast: (() => void) | null = null;
constructor() {
this.world = new World();
this.map = new GameMap();
this.setupMap();
this.spawnInitialNPCs(8);
}
private setupMap(): void {
// Add some obstacle clusters for pathfinding interest
for (let x = 10; x <= 14; x++) for (let y = 10; y <= 12; y++) this.map.setObstacle(x, y);
for (let x = 30; x <= 33; x++) for (let y = 25; y <= 28; y++) this.map.setObstacle(x, y);
for (let x = 50; x <= 53; x++) for (let y = 40; y <= 43; y++) this.map.setObstacle(x, y);
// Points of interest
this.map.addPointOfInterest({ type: 'food', position: { x: 15, y: 15 } });
this.map.addPointOfInterest({ type: 'food', position: { x: 45, y: 30 } });
this.map.addPointOfInterest({ type: 'rest', position: { x: 8, y: 8 } });
this.map.addPointOfInterest({ type: 'rest', position: { x: 55, y: 50 } });
}
private spawnInitialNPCs(count: number): void {
for (let i = 0; i < count; i++) {
spawnNPC(this.world, this.map);
}
}
setBroadcastHandler(handler: () => void): void {
this.onBroadcast = handler;
}
start(): void {
const tickInterval = 1000 / TICK_RATE;
this.interval = setInterval(() => this.update(), tickInterval);
console.log(`Game loop started at ${TICK_RATE} ticks/sec`);
}
stop(): void {
if (this.interval) {
clearInterval(this.interval);
this.interval = null;
}
}
private update(): void {
this.tick++;
// Run systems in order
needsDecaySystem(this.world);
npcBrainSystem(this.world, this.map);
movementSystem(this.world);
// Broadcast state periodically
if (this.tick % BROADCAST_EVERY_N_TICKS === 0 && this.onBroadcast) {
this.onBroadcast();
}
}
getTick(): number {
return this.tick;
}
}
+28
View File
@@ -0,0 +1,28 @@
import type { World } from '../ecs/World.js';
import type { GameMap } from '../map/GameMap.js';
import { generateRandomAppearance } from '../spawner/appearanceGenerator.js';
import type { EntityId, Position, Needs, Movement, NPCBrain, Appearance } from '@dflike/shared';
export function spawnNPC(world: World, map: GameMap): EntityId {
const entity = world.createEntity();
const pos = map.getRandomWalkable();
world.addComponent<Position>(entity, 'position', pos);
world.addComponent<Needs>(entity, 'needs', {
hunger: 40 + Math.random() * 40, // 40-80
energy: 40 + Math.random() * 40,
});
world.addComponent<Movement>(entity, 'movement', {
state: 'idle',
target: null,
path: [],
direction: 0,
});
world.addComponent<NPCBrain>(entity, 'npcBrain', {
currentGoal: null,
goalQueue: [],
});
world.addComponent<Appearance>(entity, 'appearance', generateRandomAppearance());
return entity;
}
+1
View File
@@ -5,6 +5,7 @@
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"composite": true,
"declaration": true,
"outDir": "dist",
"rootDir": "src"