feat(server): wire StockpileLog into GameLoop and SocketServer
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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<EntityId> = new Set();
|
||||
public onInventionCreated: ((summary: InventionSummary) => void) | null = null;
|
||||
public onStockpileEvent: ((entry: StockpileLogEntry) => void) | null = null;
|
||||
private tick = 0;
|
||||
private interval: ReturnType<typeof setInterval> | 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<Position>(entity, 'position', positions[i]);
|
||||
this.world.addComponent<StructureData>(entity, 'structure', {
|
||||
type: 'stockpile',
|
||||
subtype: subtypes[i],
|
||||
inventory: new Map(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
generateNpcBackstory(entityId: number): void {
|
||||
generateBackstory(this.world, entityId, this.llmService);
|
||||
}
|
||||
|
||||
@@ -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<ClientEvents, ServerEvents>;
|
||||
@@ -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>('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<string, number> {
|
||||
const summary: Record<string, number> = {};
|
||||
for (const entity of this.gameLoop.world.query('structure')) {
|
||||
const s = this.gameLoop.world.getComponent<StructureData>(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>('bondRegistry');
|
||||
|
||||
Reference in New Issue
Block a user