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, inspectNpcIds: [], inspectFocusNpcId: null, }; } function layoutFor(state: NpcsOverlayState, npcId: NpcId): NpcsOverlayLayout { return state.layouts[npcId] ?? { ...DEFAULT_NPCS_OVERLAY_LAYOUT }; } function addInspect(ids: NpcId[], npcId: NpcId): NpcId[] { if (ids.includes(npcId)) return ids; return [...ids, npcId]; } function removeInspect(ids: NpcId[], npcId: NpcId): NpcId[] { return ids.filter((id) => id !== npcId); } export class NpcsOverlayStore { private state: NpcsOverlayState = emptyState(); getState(): NpcsOverlayState { return this.state; } /** Полный сброс (закрытие проекта / конец сессии). */ clear(): NpcsOverlayState { const s = this.state; if ( s.activeNpcIds.length === 0 && s.zoomTool === null && s.inspectNpcIds.length === 0 && s.inspectFocusNpcId === null ) { return s; } this.state = emptyState(); this.state.revision = s.revision + 1; return this.state; } /** Снять аватары с экрана, карточки описания оставить. */ hideAvatars(): NpcsOverlayState { if (this.state.activeNpcIds.length === 0 && this.state.zoomTool === null) { return this.state; } 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.hideAvatars(); 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 = { ...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, }; 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 = { ...this.state, revision: this.state.revision + 1, activeNpcIds, layouts, focusNpcId, zoomTool: activeNpcIds.length === 0 ? null : this.state.zoomTool, }; return this.state; } 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, }; return this.state; } case 'inspect.toggle': { if (this.state.inspectNpcIds.includes(event.npcId)) { const inspectNpcIds = removeInspect(this.state.inspectNpcIds, event.npcId); const inspectFocusNpcId = this.state.inspectFocusNpcId === event.npcId ? (inspectNpcIds[inspectNpcIds.length - 1] ?? null) : this.state.inspectFocusNpcId; this.state = { ...this.state, revision: this.state.revision + 1, inspectNpcIds, inspectFocusNpcId, }; return this.state; } this.state = { ...this.state, revision: this.state.revision + 1, inspectNpcIds: addInspect(this.state.inspectNpcIds, event.npcId), inspectFocusNpcId: event.npcId, }; return this.state; } case 'inspect.show': { this.state = { ...this.state, revision: this.state.revision + 1, inspectNpcIds: addInspect(this.state.inspectNpcIds, event.npcId), inspectFocusNpcId: event.npcId, }; 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)); const inspectNpcIds = this.state.inspectNpcIds.filter((id) => npcIds.has(id)); const sameActive = activeNpcIds.length === this.state.activeNpcIds.length; const sameInspect = inspectNpcIds.length === this.state.inspectNpcIds.length; if (sameActive && sameInspect) return this.state; if (activeNpcIds.length === 0 && inspectNpcIds.length === 0) { const zoomTool = this.state.zoomTool; this.state = { ...emptyState(), revision: this.state.revision + 1, zoomTool, }; return this.state; } 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); const inspectFocusNpcId = this.state.inspectFocusNpcId && inspectNpcIds.includes(this.state.inspectFocusNpcId) ? this.state.inspectFocusNpcId : (inspectNpcIds[inspectNpcIds.length - 1] ?? null); this.state = { revision: this.state.revision + 1, activeNpcIds, layouts, focusNpcId, zoomTool: activeNpcIds.length === 0 ? null : this.state.zoomTool, inspectNpcIds, inspectFocusNpcId, }; return this.state; } load(next: Pick): NpcsOverlayState { this.state = { revision: this.state.revision + 1, activeNpcIds: [...next.activeNpcIds], layouts: { ...next.layouts }, focusNpcId: next.focusNpcId, zoomTool: null, inspectNpcIds: [], inspectFocusNpcId: null, }; return this.state; } }