Files
Ivan Fontosh f270812219 feat(tokens): app-local non-player tokens with session moves and UI polish
Add token library/placements, keep play-time moves for the session, lock presentation interactions, and fix export/import modal layout plus freeform trap label.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 11:04:12 +08:00

64 lines
2.1 KiB
TypeScript

import { useEffect, useMemo, useRef, useState } from 'react';
import { ipcChannels } from '../../../shared/ipc/contracts';
import type { SceneTokensSessionEvent, SceneTokensSessionState } from '../../../shared/types';
import { getDndApi } from '../dndApi';
function applyEvent(
prev: SceneTokensSessionState | null,
event: SceneTokensSessionEvent,
): SceneTokensSessionState {
const base = prev ?? { revision: 0, byPlacementId: {} };
if (event.kind === 'clear') {
return { revision: base.revision + 1, byPlacementId: {} };
}
const placementId = String(event.placementId ?? '');
return {
revision: base.revision + 1,
byPlacementId: {
...base.byPlacementId,
[placementId]: { nx: event.nx, ny: event.ny },
},
};
}
export function useSceneTokensSession(): [
SceneTokensSessionState | null,
{ dispatch: (event: SceneTokensSessionEvent) => Promise<void> },
] {
const api = getDndApi();
const [state, setState] = useState<SceneTokensSessionState | null>(null);
const localRevisionRef = useRef(0);
useEffect(() => {
void api.invoke(ipcChannels.sceneTokensSession.getState, {}).then(({ state: s }) => {
localRevisionRef.current = Math.max(localRevisionRef.current, s.revision);
setState(s);
});
return api.on(ipcChannels.sceneTokensSession.stateChanged, ({ state: s }) => {
// Не затираем более свежий optimistic-стейт устаревшим broadcast
// (например, emit при смене сцены, пока последний move ещё в полёте).
if (localRevisionRef.current > s.revision) return;
localRevisionRef.current = s.revision;
setState(s);
});
}, [api]);
const apiWrap = useMemo(
() => ({
dispatch: async (event: SceneTokensSessionEvent) => {
setState((prev) => {
const next = applyEvent(prev, event);
localRevisionRef.current = Math.max(localRevisionRef.current, next.revision);
return next;
});
const res = await api.invoke(ipcChannels.sceneTokensSession.dispatch, { event });
void res;
},
}),
[api],
);
return [state, apiWrap];
}