import { io, Socket } from 'socket.io-client'; import type { ServerEvents, ClientEvents, WorldState, StateUpdate, PlayerJoined, PlayerLeft, PlayerInput, SuperlativesData, NarrationEvent, MemoryEvent, InventionSummary, StockpileLogEntry } from '@dflike/shared'; type TypedSocket = Socket; export class SocketClient { private socket: TypedSocket; private _playerId: string | null = null; private _entityId: number | null = null; // Event callbacks onWorldState: ((data: WorldState) => void) | null = null; onStateUpdate: ((data: StateUpdate) => void) | null = null; onPlayerJoined: ((data: PlayerJoined) => void) | null = null; onPlayerLeft: ((data: PlayerLeft) => void) | null = null; onSuperlativesUpdate: ((data: SuperlativesData) => void) | null = null; onNarrationEvent: ((data: NarrationEvent) => void) | null = null; onNarrationUpdate: ((data: { id: number; narration: string }) => void) | null = null; onNarrationHistory: ((data: NarrationEvent[]) => void) | null = null; onNpcThought: ((data: { entityId: number; text: string; emoji: string }) => void) | null = null; onMemoryEvent: ((data: { entityId: number; event: MemoryEvent }) => void) | null = null; onMemoryHistory: ((data: { entityId: number; events: MemoryEvent[] }) => void) | null = null; onInventionEvent: ((data: InventionSummary) => void) | null = null; onInventionHistory: ((data: InventionSummary[]) => void) | null = null; onStockpileEvent: ((data: StockpileLogEntry) => void) | null = null; onStockpileHistory: ((data: StockpileLogEntry[]) => void) | null = null; onStockpileSummary: ((data: Record) => void) | null = null; // Buffers for history events that may arrive before handlers are registered private _bufferedInventionHistory: InventionSummary[] | null = null; private _bufferedNarrationHistory: NarrationEvent[] | null = null; private _bufferedStockpileHistory: StockpileLogEntry[] | null = null; private _bufferedStockpileSummary: Record | null = null; constructor(url: string) { this.socket = io(url); this.socket.on('world-state', (data) => { this.onWorldState?.(data); }); this.socket.on('state-update', (data) => { this.onStateUpdate?.(data); }); this.socket.on('player-joined', (data) => { if (!this._playerId) { this._playerId = data.playerId; this._entityId = data.entityId; } this.onPlayerJoined?.(data); }); this.socket.on('player-left', (data) => { this.onPlayerLeft?.(data); }); this.socket.on('superlatives-update', (data) => { this.onSuperlativesUpdate?.(data); }); this.socket.on('narration-event', (data) => this.onNarrationEvent?.(data)); this.socket.on('narration-update', (data) => this.onNarrationUpdate?.(data)); this.socket.on('narration-history', (data) => { if (this.onNarrationHistory) this.onNarrationHistory(data); else this._bufferedNarrationHistory = data; }); this.socket.on('npc-thought', (data) => this.onNpcThought?.(data)); this.socket.on('memory-event', (data) => this.onMemoryEvent?.(data)); this.socket.on('memory-history', (data) => this.onMemoryHistory?.(data)); this.socket.on('invention-event', (data) => this.onInventionEvent?.(data)); this.socket.on('invention-history', (data) => { if (this.onInventionHistory) this.onInventionHistory(data); else this._bufferedInventionHistory = data; }); this.socket.on('stockpile-event', (data) => this.onStockpileEvent?.(data)); this.socket.on('stockpile-history', (data) => { if (this.onStockpileHistory) this.onStockpileHistory(data); else this._bufferedStockpileHistory = data; }); this.socket.on('stockpile-summary', (data) => { if (this.onStockpileSummary) this.onStockpileSummary(data); else this._bufferedStockpileSummary = data; }); } get playerId(): string | null { return this._playerId; } get entityId(): number | null { return this._entityId; } /** Flush any history events that arrived before handlers were registered */ flushBufferedHistory(): void { if (this._bufferedNarrationHistory && this.onNarrationHistory) { this.onNarrationHistory(this._bufferedNarrationHistory); this._bufferedNarrationHistory = null; } if (this._bufferedInventionHistory && this.onInventionHistory) { this.onInventionHistory(this._bufferedInventionHistory); this._bufferedInventionHistory = null; } if (this._bufferedStockpileHistory && this.onStockpileHistory) { this.onStockpileHistory(this._bufferedStockpileHistory); this._bufferedStockpileHistory = null; } if (this._bufferedStockpileSummary && this.onStockpileSummary) { this.onStockpileSummary(this._bufferedStockpileSummary); this._bufferedStockpileSummary = null; } } sendInput(input: PlayerInput): void { this.socket.emit('player-input', input); } spawnNpc(x: number, y: number): void { this.socket.emit('spawn-npc', { x, y }); } followNpc(entityId: number | null): void { this.socket.emit('follow-npc', { entityId }); } subscribeSuperlatives(): void { this.socket.emit('superlatives-subscribe'); } unsubscribeSuperlatives(): void { this.socket.emit('superlatives-unsubscribe'); } disconnect(): void { this.socket.disconnect(); } }