feat(control): трекер инициативы в отдельном окне
Очередь NPC/игроков с DnD, курсором на сцене, состояниями и запасом; сброс при смене сцены. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -20,6 +20,7 @@ import {
|
||||
} from '../shared/project/projectZipExtension';
|
||||
import type { Project } from '../shared/types';
|
||||
import { asNpcId } from '../shared/types/ids';
|
||||
import { listVisibleSceneNpcTokens } from '../shared/types/appPlayers';
|
||||
import {
|
||||
asPreviewRotationDeg,
|
||||
previewRotationStepsCw,
|
||||
@@ -35,6 +36,7 @@ import { readPresentationSettings, writePresentationSettings } from './preferenc
|
||||
import { MaterialsOverlayStore } from './materials/materialsOverlayStore';
|
||||
import { NpcsOverlayStore } from './npcs/npcsOverlayStore';
|
||||
import { TimerSessionStore } from './sessionTimer/timerSessionStore';
|
||||
import { InitiativeSessionStore } from './initiative/initiativeSessionStore';
|
||||
import { ZipProjectStore } from './project/zipStore';
|
||||
import { SceneViewStore } from './sceneView/sceneViewStore';
|
||||
import { registerDndAssetProtocol } from './protocol/dndAssetProtocol';
|
||||
@@ -69,6 +71,7 @@ import {
|
||||
openMaterialsWindow,
|
||||
openPlaylistsWindow,
|
||||
openTimerWindow,
|
||||
openInitiativeWindow,
|
||||
openMultiWindow,
|
||||
openNpcsEditorWindow,
|
||||
openSceneEditorWindow,
|
||||
@@ -78,6 +81,7 @@ import {
|
||||
closeMaterialsWindow,
|
||||
closePlaylistsWindow,
|
||||
closeTimerWindow,
|
||||
closeInitiativeWindow,
|
||||
closeNpcsEditorWindow,
|
||||
closeSceneEditorWindow,
|
||||
closeTokenPathEditorWindow,
|
||||
@@ -182,6 +186,7 @@ const videoStore = new VideoPlaybackStore();
|
||||
const materialsOverlayStore = new MaterialsOverlayStore();
|
||||
const npcsOverlayStore = new NpcsOverlayStore();
|
||||
const timerSessionStore = new TimerSessionStore();
|
||||
const initiativeSessionStore = new InitiativeSessionStore();
|
||||
const sceneTokensSessionStore = new SceneTokensSessionStore();
|
||||
const tokenPathSessionStore = new TokenPathSessionStore();
|
||||
const sceneNpcTokensSessionStore = new SceneNpcTokensSessionStore();
|
||||
@@ -283,6 +288,64 @@ function emitSessionTimerState(): void {
|
||||
}
|
||||
}
|
||||
|
||||
function emitInitiativeState(): void {
|
||||
const state = initiativeSessionStore.getState();
|
||||
for (const win of BrowserWindow.getAllWindows()) {
|
||||
if (win.isDestroyed() || win.webContents.isDestroyed()) continue;
|
||||
try {
|
||||
win.webContents.send(ipcChannels.initiative.stateChanged, { state });
|
||||
} catch {
|
||||
// Окно может умирать при закрытии пульта/сессии (дочернее initiative).
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function syncInitiativePresenceFromSession(): void {
|
||||
const project = projectStore.getOpenProject();
|
||||
const sceneId = project?.currentSceneId ?? null;
|
||||
const scene = sceneId && project ? project.scenes[sceneId] : undefined;
|
||||
const npcSession = sceneNpcTokensSessionStore.getState();
|
||||
const placements = listVisibleSceneNpcTokens(scene?.npcTokens, npcSession.spawned, sceneId);
|
||||
const npcById = new Map((project?.npcs ?? []).map((n) => [String(n.id), n]));
|
||||
const npcs = placements.map((p) => {
|
||||
const npc = npcById.get(String(p.npcId));
|
||||
return {
|
||||
placementId: String(p.id),
|
||||
npcId: String(p.npcId),
|
||||
name: npc?.name?.trim() || 'NPC',
|
||||
avatarAssetId: npc?.avatarAssetId ? String(npc.avatarAssetId) : null,
|
||||
};
|
||||
});
|
||||
|
||||
const playerSession = scenePlayerTokensSessionStore.getState();
|
||||
const playersLib = playersStore?.listPlayers() ?? [];
|
||||
const playerById = new Map(playersLib.map((p) => [String(p.id), p]));
|
||||
const players = playerSession.selectedPlayerIds.map((id) => {
|
||||
const p = playerById.get(String(id));
|
||||
return {
|
||||
playerId: String(id),
|
||||
name: p?.name?.trim() || 'Player',
|
||||
};
|
||||
});
|
||||
|
||||
const before = initiativeSessionStore.getState().revision;
|
||||
initiativeSessionStore.dispatch({
|
||||
kind: 'syncPresence',
|
||||
npcs,
|
||||
players,
|
||||
playersVisible: playerSession.visible,
|
||||
});
|
||||
if (initiativeSessionStore.getState().revision !== before) {
|
||||
emitInitiativeState();
|
||||
}
|
||||
}
|
||||
|
||||
function clearAndResyncInitiative(): void {
|
||||
initiativeSessionStore.clear();
|
||||
syncInitiativePresenceFromSession();
|
||||
emitInitiativeState();
|
||||
}
|
||||
|
||||
function emitSceneDarknessState(): void {
|
||||
const state = sceneDarknessStore.getState();
|
||||
for (const win of BrowserWindow.getAllWindows()) {
|
||||
@@ -573,6 +636,7 @@ async function main() {
|
||||
scenePlayerTokensSessionStore.reset();
|
||||
tokenGridSnapSessionStore.reset();
|
||||
timerSessionStore.clear();
|
||||
initiativeSessionStore.clear();
|
||||
if (USERS_BRANCH_FEATURES_ENABLED) {
|
||||
const playerIds = Array.isArray(req?.playerIds) ? req.playerIds.map(String).filter(Boolean) : [];
|
||||
if (playerIds.length > 0) {
|
||||
@@ -599,13 +663,17 @@ async function main() {
|
||||
emitTokenGridSnapState();
|
||||
emitEffectsState();
|
||||
emitSessionTimerState();
|
||||
syncInitiativePresenceFromSession();
|
||||
emitInitiativeState();
|
||||
return { ok: true };
|
||||
});
|
||||
registerHandler(ipcChannels.windows.closeMultiWindow, () => {
|
||||
closeMultiWindow();
|
||||
timerSessionStore.clear();
|
||||
initiativeSessionStore.clear();
|
||||
tokenGridSnapSessionStore.reset();
|
||||
emitSessionTimerState();
|
||||
emitInitiativeState();
|
||||
emitTokenGridSnapState();
|
||||
return { ok: true };
|
||||
});
|
||||
@@ -669,6 +737,24 @@ async function main() {
|
||||
closeTimerWindow();
|
||||
return { ok: true };
|
||||
});
|
||||
registerHandler(ipcChannels.windows.openInitiative, () => {
|
||||
openInitiativeWindow({
|
||||
onClosed: () => {
|
||||
initiativeSessionStore.dispatch({ kind: 'setEnabled', enabled: false });
|
||||
emitInitiativeState();
|
||||
},
|
||||
});
|
||||
initiativeSessionStore.dispatch({ kind: 'setEnabled', enabled: true });
|
||||
syncInitiativePresenceFromSession();
|
||||
emitInitiativeState();
|
||||
return { ok: true };
|
||||
});
|
||||
registerHandler(ipcChannels.windows.closeInitiative, () => {
|
||||
closeInitiativeWindow();
|
||||
initiativeSessionStore.dispatch({ kind: 'setEnabled', enabled: false });
|
||||
emitInitiativeState();
|
||||
return { ok: true };
|
||||
});
|
||||
registerHandler(ipcChannels.windows.openNpcsEditor, () => {
|
||||
openNpcsEditorWindow();
|
||||
return { ok: true };
|
||||
@@ -733,6 +819,14 @@ async function main() {
|
||||
emitSessionTimerState();
|
||||
return { state: timerSessionStore.getState() };
|
||||
});
|
||||
registerHandler(ipcChannels.initiative.getState, () => {
|
||||
return { state: initiativeSessionStore.getState() };
|
||||
});
|
||||
registerHandler(ipcChannels.initiative.dispatch, ({ event }) => {
|
||||
initiativeSessionStore.dispatch(event);
|
||||
emitInitiativeState();
|
||||
return { state: initiativeSessionStore.getState() };
|
||||
});
|
||||
|
||||
registerHandler(ipcChannels.project.list, async () => {
|
||||
const projects = await projectStore.listProjects();
|
||||
@@ -775,6 +869,7 @@ async function main() {
|
||||
materialsOverlayStore.clear();
|
||||
npcsOverlayStore.clear();
|
||||
timerSessionStore.clear();
|
||||
initiativeSessionStore.clear();
|
||||
sceneDarknessStore.resetSession();
|
||||
sceneMarkerStore.resetSession();
|
||||
sceneTrapsStore.resetSession();
|
||||
@@ -788,6 +883,7 @@ async function main() {
|
||||
emitMaterialsOverlayState();
|
||||
emitNpcsOverlayState();
|
||||
emitSessionTimerState();
|
||||
emitInitiativeState();
|
||||
emitSceneDarknessState();
|
||||
emitSceneMarkerState();
|
||||
emitSceneTrapsState();
|
||||
@@ -813,6 +909,7 @@ async function main() {
|
||||
npcsOverlayStore.clear();
|
||||
sceneViewStore.reset();
|
||||
scenePlayerTokensSessionStore.clearPlacements();
|
||||
clearAndResyncInitiative();
|
||||
// Token moves persist for the whole play session (reset only on project open/close).
|
||||
const project = projectStore.getOpenProject();
|
||||
if (project) {
|
||||
@@ -843,6 +940,7 @@ async function main() {
|
||||
npcsOverlayStore.clear();
|
||||
sceneViewStore.reset();
|
||||
scenePlayerTokensSessionStore.clearPlacements();
|
||||
clearAndResyncInitiative();
|
||||
// Token moves persist for the whole play session (reset only on project open/close).
|
||||
const project = projectStore.getOpenProject();
|
||||
if (project) {
|
||||
@@ -902,6 +1000,9 @@ async function main() {
|
||||
syncSceneTrapsForProject(project);
|
||||
emitSceneTrapsState();
|
||||
}
|
||||
if (project?.currentSceneId === sceneId && patch.npcTokens !== undefined) {
|
||||
syncInitiativePresenceFromSession();
|
||||
}
|
||||
emitSessionState();
|
||||
return { scene: next };
|
||||
});
|
||||
@@ -1879,6 +1980,9 @@ async function main() {
|
||||
registerHandler(ipcChannels.sceneNpcTokensSession.dispatch, ({ event }) => {
|
||||
sceneNpcTokensSessionStore.dispatch(event);
|
||||
emitSceneNpcTokensSessionState();
|
||||
if (event.kind === 'spawn' || event.kind === 'despawn' || event.kind === 'clear') {
|
||||
syncInitiativePresenceFromSession();
|
||||
}
|
||||
return { ok: true };
|
||||
});
|
||||
registerHandler(ipcChannels.npcMapSpawn.getState, () => {
|
||||
@@ -1935,6 +2039,7 @@ async function main() {
|
||||
disposition: npc.disposition,
|
||||
});
|
||||
emitSceneNpcTokensSessionState();
|
||||
syncInitiativePresenceFromSession();
|
||||
return { spawned: true };
|
||||
});
|
||||
registerHandler(ipcChannels.scenePlayerTokensSession.getState, () => {
|
||||
@@ -1943,6 +2048,15 @@ async function main() {
|
||||
registerHandler(ipcChannels.scenePlayerTokensSession.dispatch, ({ event }) => {
|
||||
scenePlayerTokensSessionStore.dispatch(event);
|
||||
emitScenePlayerTokensSessionState();
|
||||
if (
|
||||
event.kind === 'setSelection' ||
|
||||
event.kind === 'setVisible' ||
|
||||
event.kind === 'show' ||
|
||||
event.kind === 'clear' ||
|
||||
event.kind === 'clearPlacements'
|
||||
) {
|
||||
syncInitiativePresenceFromSession();
|
||||
}
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user