import { clampNpcsLayout, DEFAULT_NPCS_OVERLAY_LAYOUT, type NpcId, type NpcsOverlayEvent, type NpcsOverlayLayout, type NpcsOverlayState, type NpcsZoomTool, zoomNpcsLayoutAt, } from '../../shared/types'; function emptyState(): NpcsOverlayState { return { revision: 1, activeNpcIds: [], layouts: {}, focusNpcId: null, zoomTool: null, }; } function layoutFor(state: NpcsOverlayState, npcId: NpcId): NpcsOverlayLayout { return state.layouts[npcId] ?? { ...DEFAULT_NPCS_OVERLAY_LAYOUT }; } export class NpcsOverlayStore { private state: NpcsOverlayState = emptyState(); getState(): NpcsOverlayState { return this.state; } clear(): NpcsOverlayState { if (this.state.activeNpcIds.length === 0 && this.state.zoomTool === null) { return this.state; } this.state = { revision: this.state.revision + 1, activeNpcIds: [], layouts: {}, focusNpcId: null, zoomTool: null, }; return this.state; } dispatch(event: NpcsOverlayEvent): NpcsOverlayState { switch (event.kind) { case 'hide': return this.clear(); case 'show': { if (this.state.activeNpcIds.includes(event.npcId)) { this.state = { ...this.state, revision: this.state.revision + 1, focusNpcId: event.npcId, }; return this.state; } this.state = { revision: this.state.revision + 1, activeNpcIds: [...this.state.activeNpcIds, event.npcId], layouts: { ...this.state.layouts, [event.npcId]: { ...DEFAULT_NPCS_OVERLAY_LAYOUT }, }, focusNpcId: event.npcId, zoomTool: this.state.zoomTool, }; return this.state; } case 'toggle': { if (this.state.activeNpcIds.includes(event.npcId)) { const activeNpcIds = this.state.activeNpcIds.filter((id) => id !== event.npcId); const layouts = { ...this.state.layouts }; delete layouts[event.npcId]; const focusNpcId = this.state.focusNpcId === event.npcId ? (activeNpcIds[activeNpcIds.length - 1] ?? null) : this.state.focusNpcId; this.state = { revision: this.state.revision + 1, activeNpcIds, layouts, focusNpcId, zoomTool: activeNpcIds.length === 0 ? null : this.state.zoomTool, }; return this.state; } this.state = { revision: this.state.revision + 1, activeNpcIds: [...this.state.activeNpcIds, event.npcId], layouts: { ...this.state.layouts, [event.npcId]: { ...DEFAULT_NPCS_OVERLAY_LAYOUT }, }, focusNpcId: event.npcId, zoomTool: this.state.zoomTool, }; return this.state; } case 'layout.set': { if (!this.state.activeNpcIds.includes(event.npcId)) return this.state; this.state = { ...this.state, revision: this.state.revision + 1, focusNpcId: event.npcId, layouts: { ...this.state.layouts, [event.npcId]: clampNpcsLayout({ ...layoutFor(this.state, event.npcId), ...event.layout, }), }, }; return this.state; } case 'zoomTool.set': { const tool: NpcsZoomTool = event.tool; this.state = { ...this.state, revision: this.state.revision + 1, zoomTool: tool, }; return this.state; } case 'zoomAt': { if (this.state.activeNpcIds.length === 0 || !this.state.zoomTool) return this.state; const targetId = (event.npcId && this.state.activeNpcIds.includes(event.npcId) ? event.npcId : null) ?? this.state.focusNpcId ?? this.state.activeNpcIds[this.state.activeNpcIds.length - 1] ?? null; if (!targetId) return this.state; const factor = this.state.zoomTool === 'zoomIn' ? 1.25 : 1 / 1.25; const layout: NpcsOverlayLayout = zoomNpcsLayoutAt( layoutFor(this.state, targetId), event.nx, event.ny, factor, ); this.state = { ...this.state, revision: this.state.revision + 1, focusNpcId: targetId, layouts: { ...this.state.layouts, [targetId]: layout, }, }; return this.state; } default: return this.state; } } ensureNpcStillExists(npcIds: ReadonlySet): NpcsOverlayState { const activeNpcIds = this.state.activeNpcIds.filter((id) => npcIds.has(id)); if (activeNpcIds.length === this.state.activeNpcIds.length) return this.state; if (activeNpcIds.length === 0) return this.clear(); const layouts: Record = {}; for (const id of activeNpcIds) { const layout = this.state.layouts[id]; if (layout) layouts[id] = layout; } const focusNpcId = this.state.focusNpcId && activeNpcIds.includes(this.state.focusNpcId) ? this.state.focusNpcId : (activeNpcIds[activeNpcIds.length - 1] ?? null); this.state = { revision: this.state.revision + 1, activeNpcIds, layouts, focusNpcId, zoomTool: this.state.zoomTool, }; return this.state; } }