import { useCallback, useEffect, useRef, useState } from 'react'; import { ipcChannels } from '../../../shared/ipc/contracts'; import type { ScenePlayerTokensSessionEvent, ScenePlayerTokensSessionState, } from '../../../shared/types'; import { getDndApi } from '../dndApi'; function applyEvent( prev: ScenePlayerTokensSessionState, event: ScenePlayerTokensSessionEvent, ): ScenePlayerTokensSessionState { if (event.kind === 'clear') { return { revision: prev.revision + 1, selectedPlayerIds: [], visible: false, byPlayerId: {} }; } if (event.kind === 'clearPlacements') { return { ...prev, revision: prev.revision + 1, visible: false, byPlayerId: {} }; } if (event.kind === 'setSelection') { const seen = new Set(); const selectedPlayerIds: string[] = []; for (const id of event.playerIds) { const s = String(id ?? '').trim(); if (!s || seen.has(s)) continue; seen.add(s); selectedPlayerIds.push(s); } return { revision: prev.revision + 1, selectedPlayerIds, visible: false, byPlayerId: {} }; } if (event.kind === 'setVisible') { if (event.visible && prev.selectedPlayerIds.length === 0) return prev; return { ...prev, revision: prev.revision + 1, visible: Boolean(event.visible) }; } if (event.kind === 'show') { if (prev.selectedPlayerIds.length === 0) return prev; // placements приходят с main; локально только включаем visible после seed через broadcast return { ...prev, revision: prev.revision + 1, visible: true }; } if (event.kind === 'seedBottom') { return prev; } if (event.kind === 'move') { const prevPlacement = prev.byPlayerId[event.playerId]; return { ...prev, revision: prev.revision + 1, byPlayerId: { ...prev.byPlayerId, [event.playerId]: { nx: event.nx, ny: event.ny, sizeN: prevPlacement?.sizeN ?? 0.1, }, }, }; } return prev; } export function useScenePlayerTokensSession(): [ ScenePlayerTokensSessionState, { dispatch: (event: ScenePlayerTokensSessionEvent) => void }, ] { const api = getDndApi(); const [state, setState] = useState({ revision: 0, selectedPlayerIds: [], visible: false, byPlayerId: {}, }); const localRevRef = useRef(0); useEffect(() => { void api.invoke(ipcChannels.scenePlayerTokensSession.getState, {}).then((res) => { if (res.state.revision < localRevRef.current) return; localRevRef.current = res.state.revision; setState(res.state); }); return api.on(ipcChannels.scenePlayerTokensSession.stateChanged, ({ state: next }) => { if (next.revision < localRevRef.current) return; localRevRef.current = next.revision; setState(next); }); }, [api]); const dispatch = useCallback( (event: ScenePlayerTokensSessionEvent) => { if (event.kind !== 'seedBottom' && event.kind !== 'show') { setState((prev) => { const next = applyEvent(prev, event); localRevRef.current = Math.max(localRevRef.current, next.revision); return next; }); } void api.invoke(ipcChannels.scenePlayerTokensSession.dispatch, { event }); }, [api], ); return [state, { dispatch }]; }