34aca29082
- Add persistent 💤 emoji over sleeping NPCs (tracks sprite position) - Buffer history events (inventions, narration, stockpile) in SocketClient to fix race condition where data arrived before GameScene handlers registered - Fix NPCs getting stuck on 'gather' goal: tree trunks are both obstacles and resource tiles, so pathfinding always failed. Now findNearestResource returns adjacent walkable tiles for non-walkable resources, and gatheringSystem checks adjacent tiles. Added wander fallback when pathfinding to any gather target fails. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
135 lines
5.3 KiB
TypeScript
135 lines
5.3 KiB
TypeScript
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<ServerEvents, ClientEvents>;
|
|
|
|
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<string, number>) => 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<string, number> | 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();
|
|
}
|
|
}
|