Files
dflike/server/src/game/GameLoop.ts
T
2026-03-08 22:50:48 +00:00

128 lines
4.8 KiB
TypeScript

import { TICK_RATE, BROADCAST_EVERY_N_TICKS, ENERGY_DECAY_PER_TICK, DAY_NIGHT_RATIO } from '@dflike/shared';
import { World } from '../ecs/World.js';
import { GameMap } from '../map/GameMap.js';
import { generateMap } from '../map/mapGenerator.js';
import { needsDecaySystem } from '../systems/needsDecaySystem.js';
import { npcBrainSystem } from '../systems/npcBrainSystem.js';
import { movementSystem } from '../systems/movementSystem.js';
import { socialSystem } from '../systems/socialSystem.js';
import { statModifierSystem } from '../systems/statModifierSystem.js';
import { relationshipSystem } from '../systems/relationshipSystem.js';
import { gatheringSystem } from '../systems/gatheringSystem.js';
import { createBondRegistry } from '../systems/bondRegistry.js';
import { ItemRegistry } from '../industry/itemRegistry.js';
import { spawnNPC } from './spawner.js';
import { createLlmService, type LlmService } from '../llm/llmService.js';
import { generateBackstory } from '../llm/backstoryGenerator.js';
import { createNarrationService, type NarrationService } from '../llm/narrationService.js';
import { narrationEmitter } from '../systems/narrationEmitter.js';
import type { EntityId } from '@dflike/shared';
import { createThoughtSystem, type ThoughtSystem } from '../systems/thoughtSystem.js';
import { createEventMemoryService, type EventMemoryService } from '../llm/eventMemoryService.js';
export class GameLoop {
readonly world: World;
readonly map: GameMap;
readonly llmService: LlmService;
readonly narrationService: NarrationService;
readonly eventMemoryService: EventMemoryService;
readonly thoughtSystem: ThoughtSystem;
public followedEntityIds: Set<EntityId> = new Set();
private tick = 0;
private interval: ReturnType<typeof setInterval> | null = null;
private onBroadcast: (() => void) | null = null;
constructor() {
this.world = new World();
this.world.setSingleton('bondRegistry', createBondRegistry());
this.world.setSingleton('itemRegistry', ItemRegistry.createDefault());
this.map = new GameMap();
this.llmService = createLlmService();
this.narrationService = createNarrationService(this.llmService);
this.eventMemoryService = createEventMemoryService();
this.thoughtSystem = createThoughtSystem(this.llmService, this.narrationService, this.eventMemoryService);
this.setupMap();
this.spawnInitialNPCs(8);
}
private setupMap(): void {
const generated = generateMap(this.map.width, this.map.height);
// Load terrain and decorations
this.map.terrain = generated.terrain;
this.map.decorations = generated.decorations;
this.map.trunkDecorations = generated.trunkDecorations;
this.map.loadObstacles(generated.obstacles);
// Load resource tiles
this.map.resourceTiles = generated.resourceTiles;
// Points of interest from generator
for (const pos of generated.foodPositions) {
this.map.addPointOfInterest({ type: 'food', position: pos });
}
for (const pos of generated.restPositions) {
this.map.addPointOfInterest({ type: 'rest', position: pos });
}
}
generateNpcBackstory(entityId: number): void {
generateBackstory(this.world, entityId, this.llmService);
}
private spawnInitialNPCs(count: number): void {
for (let i = 0; i < count; i++) {
const entity = spawnNPC(this.world, this.map, undefined, this.eventMemoryService);
this.generateNpcBackstory(entity);
}
}
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
statModifierSystem(this.world);
needsDecaySystem(this.world, this.eventMemoryService);
npcBrainSystem(this.world, this.map, this.eventMemoryService);
socialSystem(this.world, this.eventMemoryService);
narrationEmitter(this.world, this.narrationService, this.followedEntityIds, this.eventMemoryService);
relationshipSystem(this.world, this.eventMemoryService);
gatheringSystem(this.world, this.map);
movementSystem(this.world);
this.thoughtSystem.update(this.world, this.followedEntityIds, this.tick);
// Broadcast state periodically
if (this.tick % BROADCAST_EVERY_N_TICKS === 0 && this.onBroadcast) {
this.onBroadcast();
}
}
getTick(): number {
return this.tick;
}
getGameTime(): number {
const dayTicks = 100 / ENERGY_DECAY_PER_TICK;
const nightTicks = dayTicks / DAY_NIGHT_RATIO;
const cycleTicks = Math.round(dayTicks + nightTicks);
return (this.tick % cycleTicks) / cycleTicks;
}
}