cc50e64e21
Add Hostile/Neutral/Friendly ring types with session-only inactive overrides on control, plus launch-with-players flow and live player tokens on the map. Co-authored-by: Cursor <cursoragent@cursor.com>
103 lines
3.3 KiB
TypeScript
103 lines
3.3 KiB
TypeScript
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<string>();
|
|
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<ScenePlayerTokensSessionState>({
|
|
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 }];
|
|
}
|