diff --git a/server/src/game/GameLoop.ts b/server/src/game/GameLoop.ts index 0d45eff..6d15305 100644 --- a/server/src/game/GameLoop.ts +++ b/server/src/game/GameLoop.ts @@ -21,11 +21,14 @@ 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, InventionSummary } from '@dflike/shared'; +import type { EntityId, InventionSummary, Position } from '@dflike/shared'; +import type { StructureData } from '../systems/buildingSystem.js'; import { createThoughtSystem, type ThoughtSystem } from '../systems/thoughtSystem.js'; import { createEventMemoryService, type EventMemoryService } from '../llm/eventMemoryService.js'; import { createInventionSystem, type InventionSystem } from '../systems/inventionSystem.js'; import { createInventionTimeline } from '../industry/inventionTimeline.js'; +import { createStockpileLog } from '../industry/stockpileLog.js'; +import type { StockpileLogEntry } from '@dflike/shared'; export class GameLoop { readonly world: World; @@ -37,6 +40,7 @@ export class GameLoop { readonly inventionSystem: InventionSystem; public followedEntityIds: Set = new Set(); public onInventionCreated: ((summary: InventionSummary) => void) | null = null; + public onStockpileEvent: ((entry: StockpileLogEntry) => void) | null = null; private tick = 0; private interval: ReturnType | null = null; private onBroadcast: (() => void) | null = null; @@ -47,6 +51,7 @@ export class GameLoop { this.world.setSingleton('itemRegistry', ItemRegistry.createDefault()); this.world.setSingleton('recipeRegistry', RecipeRegistry.createDefault()); this.world.setSingleton('inventionTimeline', createInventionTimeline()); + this.world.setSingleton('stockpileLog', createStockpileLog()); this.map = new GameMap(); this.llmService = createLlmService(); this.narrationService = createNarrationService(this.llmService); @@ -59,6 +64,7 @@ export class GameLoop { (summary) => this.onInventionCreated?.(summary), ); this.setupMap(); + this.spawnDefaultStockpiles(); this.spawnInitialNPCs(8); } @@ -83,6 +89,39 @@ export class GameLoop { } } + private spawnDefaultStockpiles(): void { + const cx = Math.floor(this.map.width / 2); + const cy = Math.floor(this.map.height / 2); + + // Find two walkable positions near the center for wood and stone stockpiles + const positions: Position[] = []; + outer: + for (let r = 0; positions.length < 2; r++) { + for (let dx = -r; dx <= r; dx++) { + for (let dy = -r; dy <= r; dy++) { + if (r > 0 && Math.abs(dx) !== r && Math.abs(dy) !== r) continue; + const x = cx + dx; + const y = cy + dy; + if (this.map.isWalkable(x, y) && !positions.some(p => p.x === x && p.y === y)) { + positions.push({ x, y }); + if (positions.length >= 2) break outer; + } + } + } + } + + const subtypes = ['wood', 'stone']; + for (let i = 0; i < positions.length; i++) { + const entity = this.world.createEntity(); + this.world.addComponent(entity, 'position', positions[i]); + this.world.addComponent(entity, 'structure', { + type: 'stockpile', + subtype: subtypes[i], + inventory: new Map(), + }); + } + } + generateNpcBackstory(entityId: number): void { generateBackstory(this.world, entityId, this.llmService); } diff --git a/server/src/network/SocketServer.ts b/server/src/network/SocketServer.ts index b153dd7..694771c 100644 --- a/server/src/network/SocketServer.ts +++ b/server/src/network/SocketServer.ts @@ -9,6 +9,8 @@ import { serializeWorldState, serializeStateUpdate } from './stateSerializer.js' import { computeSuperlatives } from '../systems/superlativesComputer.js'; import type { BondRegistry } from '../systems/bondRegistry.js'; import type { InventionTimeline } from '../industry/inventionTimeline.js'; +import type { StockpileLog } from '../industry/stockpileLog.js'; +import type { StructureData } from '../systems/buildingSystem.js'; export class SocketServer { private io: Server; @@ -51,6 +53,11 @@ export class SocketServer { this.io.emit('invention-event', summary); }; + this.gameLoop.onStockpileEvent = (entry) => { + this.io.emit('stockpile-event', entry); + this.io.emit('stockpile-summary', this.computeStockpileSummary()); + }; + this.gameLoop.eventMemoryService.onEventRecorded = (entityId, event) => { // Only send to clients following this NPC for (const [socketId, followedId] of this.followedEntities) { @@ -94,6 +101,13 @@ export class SocketServer { socket.emit('invention-history', inventionTimeline.getSummaries()); } + // Send stockpile history and summary + const stockpileLog = this.gameLoop.world.getSingleton('stockpileLog'); + if (stockpileLog) { + socket.emit('stockpile-history', stockpileLog.getRecent()); + } + socket.emit('stockpile-summary', this.computeStockpileSummary()); + // Notify others socket.broadcast.emit('player-joined', { playerId, entityId: entity }); @@ -163,6 +177,18 @@ export class SocketServer { }); } + private computeStockpileSummary(): Record { + const summary: Record = {}; + for (const entity of this.gameLoop.world.query('structure')) { + const s = this.gameLoop.world.getComponent(entity, 'structure')!; + if (s.type !== 'stockpile' || s.buildProgress !== undefined) continue; + for (const [itemId, qty] of s.inventory) { + summary[itemId] = (summary[itemId] ?? 0) + qty; + } + } + return summary; + } + private broadcastSuperlatives(): void { if (this.superlativesSubscribers.size === 0) return; const registry = this.gameLoop.world.getSingleton('bondRegistry');