From ca05d36bc043fc36a5de790b607b5ec67dd01dea Mon Sep 17 00:00:00 2001 From: Andy Date: Fri, 6 Mar 2026 18:53:19 -0500 Subject: [PATCH] feat: add game loop with NPC spawning and system execution Co-Authored-By: Claude Opus 4.6 --- server/src/game/GameLoop.ts | 76 +++++++++++++++++++++++++++++++++++++ server/src/game/spawner.ts | 28 ++++++++++++++ shared/tsconfig.json | 1 + 3 files changed, 105 insertions(+) create mode 100644 server/src/game/GameLoop.ts create mode 100644 server/src/game/spawner.ts diff --git a/server/src/game/GameLoop.ts b/server/src/game/GameLoop.ts new file mode 100644 index 0000000..2fe9393 --- /dev/null +++ b/server/src/game/GameLoop.ts @@ -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 | 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; + } +} diff --git a/server/src/game/spawner.ts b/server/src/game/spawner.ts new file mode 100644 index 0000000..942668d --- /dev/null +++ b/server/src/game/spawner.ts @@ -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(entity, 'position', pos); + world.addComponent(entity, 'needs', { + hunger: 40 + Math.random() * 40, // 40-80 + energy: 40 + Math.random() * 40, + }); + world.addComponent(entity, 'movement', { + state: 'idle', + target: null, + path: [], + direction: 0, + }); + world.addComponent(entity, 'npcBrain', { + currentGoal: null, + goalQueue: [], + }); + world.addComponent(entity, 'appearance', generateRandomAppearance()); + + return entity; +} diff --git a/shared/tsconfig.json b/shared/tsconfig.json index 43a4e4a..2a15387 100644 --- a/shared/tsconfig.json +++ b/shared/tsconfig.json @@ -5,6 +5,7 @@ "moduleResolution": "bundler", "strict": true, "esModuleInterop": true, + "composite": true, "declaration": true, "outDir": "dist", "rootDir": "src"