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>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
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];
|
||||
}
|
||||
Reference in New Issue
Block a user