import { clampSceneNpcTokenSizeN, DEFAULT_SCENE_NPC_TOKEN_SIZE_N, layoutPlayerTokensBottom, type ScenePlayerTokensSessionEvent, type ScenePlayerTokensSessionState, } from '../../shared/types/appPlayers'; import { rotateMapNormPointByCwSteps } from '../../shared/types/scenePreviewRotation'; function emptyState(revision = 1): ScenePlayerTokensSessionState { return { revision, selectedPlayerIds: [], visible: false, byPlayerId: {}, }; } function normalizePlayerIds(raw: readonly string[]): string[] { const seen = new Set(); const out: string[] = []; for (const id of raw) { const s = String(id ?? '').trim(); if (!s || seen.has(s)) continue; seen.add(s); out.push(s); } return out; } export class ScenePlayerTokensSessionStore { private state: ScenePlayerTokensSessionState = emptyState(); getState(): ScenePlayerTokensSessionState { return this.state; } reset(): ScenePlayerTokensSessionState { if ( this.state.selectedPlayerIds.length === 0 && !this.state.visible && Object.keys(this.state.byPlayerId).length === 0 ) { return this.state; } this.state = emptyState(this.state.revision + 1); return this.state; } /** Смена сцены: спрятать токены, сбросить позиции; выбор игроков сохранить. */ clearPlacements(): ScenePlayerTokensSessionState { if (!this.state.visible && Object.keys(this.state.byPlayerId).length === 0) { return this.state; } this.state = { ...this.state, revision: this.state.revision + 1, visible: false, byPlayerId: {}, }; return this.state; } /** Rotate live player tokens with the scene preview (CSS rotate steps). */ rotateMapCwSteps(steps: number): ScenePlayerTokensSessionState { const n = ((steps % 4) + 4) % 4; if (n === 0) return this.state; const ids = Object.keys(this.state.byPlayerId); if (ids.length === 0) return this.state; const byPlayerId: ScenePlayerTokensSessionState['byPlayerId'] = {}; for (const id of ids) { const prev = this.state.byPlayerId[id]; if (!prev) continue; const p = rotateMapNormPointByCwSteps(prev.nx, prev.ny, n); byPlayerId[id] = { ...prev, nx: p.nx, ny: p.ny }; } this.state = { ...this.state, revision: this.state.revision + 1, byPlayerId, }; return this.state; } dispatch(event: ScenePlayerTokensSessionEvent): ScenePlayerTokensSessionState { switch (event.kind) { case 'clear': return this.reset(); case 'clearPlacements': return this.clearPlacements(); case 'setSelection': { const selectedPlayerIds = normalizePlayerIds(event.playerIds); const same = selectedPlayerIds.length === this.state.selectedPlayerIds.length && selectedPlayerIds.every((id, i) => id === this.state.selectedPlayerIds[i]); if (same) return this.state; this.state = { revision: this.state.revision + 1, selectedPlayerIds, visible: false, byPlayerId: {}, }; return this.state; } case 'setVisible': { const visible = Boolean(event.visible); if (this.state.visible === visible) return this.state; if (visible && this.state.selectedPlayerIds.length === 0) return this.state; this.state = { ...this.state, revision: this.state.revision + 1, visible }; return this.state; } case 'show': { if (this.state.selectedPlayerIds.length === 0) return this.state; const sizeN = clampSceneNpcTokenSizeN(event.sizeN); const byPlayerId = Object.keys(this.state.byPlayerId).length > 0 ? this.state.byPlayerId : layoutPlayerTokensBottom(this.state.selectedPlayerIds, sizeN); if (this.state.visible && byPlayerId === this.state.byPlayerId) return this.state; this.state = { ...this.state, revision: this.state.revision + 1, visible: true, byPlayerId, }; return this.state; } case 'seedBottom': { if (this.state.selectedPlayerIds.length === 0) return this.state; if (Object.keys(this.state.byPlayerId).length > 0) return this.state; const sizeN = clampSceneNpcTokenSizeN(event.sizeN); this.state = { ...this.state, revision: this.state.revision + 1, byPlayerId: layoutPlayerTokensBottom(this.state.selectedPlayerIds, sizeN), }; return this.state; } case 'move': { const playerId = String(event.playerId ?? ''); if (!playerId || !this.state.selectedPlayerIds.includes(playerId)) return this.state; const nx = Math.max(0, Math.min(1, event.nx)); const ny = Math.max(0, Math.min(1, event.ny)); if (!Number.isFinite(nx) || !Number.isFinite(ny)) return this.state; const prev = this.state.byPlayerId[playerId]; if (prev && prev.nx === nx && prev.ny === ny) return this.state; const sizeN = clampSceneNpcTokenSizeN(prev?.sizeN ?? DEFAULT_SCENE_NPC_TOKEN_SIZE_N); this.state = { ...this.state, revision: this.state.revision + 1, byPlayerId: { ...this.state.byPlayerId, [playerId]: { nx, ny, sizeN }, }, }; return this.state; } default: { const _exhaustive: never = event; void _exhaustive; return this.state; } } } }