feat(players): NPC disposition types and launch-with-players session tokens

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>
This commit is contained in:
Ivan Fontosh
2026-07-30 17:11:01 +08:00
parent 101f595bac
commit cc50e64e21
38 changed files with 1803 additions and 59 deletions
+37 -3
View File
@@ -31,6 +31,7 @@ import { SceneViewStore } from './sceneView/sceneViewStore';
import { registerDndAssetProtocol } from './protocol/dndAssetProtocol';
import { PlayersStore } from './players/playersStore';
import { SceneNpcTokensSessionStore } from './players/sceneNpcTokensSessionStore';
import { ScenePlayerTokensSessionStore } from './players/scenePlayerTokensSessionStore';
import { SceneTokensSessionStore } from './tokens/sceneTokensSessionStore';
import { TokensStore } from './tokens/tokensStore';
import { installAutoUpdater } from './update/installAutoUpdater';
@@ -174,6 +175,7 @@ const materialsOverlayStore = new MaterialsOverlayStore();
const npcsOverlayStore = new NpcsOverlayStore();
const sceneTokensSessionStore = new SceneTokensSessionStore();
const sceneNpcTokensSessionStore = new SceneNpcTokensSessionStore();
const scenePlayerTokensSessionStore = new ScenePlayerTokensSessionStore();
let tokensStore: TokensStore | null = null;
let playersStore: PlayersStore | null = null;
@@ -265,6 +267,13 @@ function emitSceneNpcTokensSessionState(): void {
}
}
function emitScenePlayerTokensSessionState(): void {
const state = scenePlayerTokensSessionStore.getState();
for (const win of BrowserWindow.getAllWindows()) {
win.webContents.send(ipcChannels.scenePlayerTokensSession.stateChanged, { state });
}
}
function syncSceneDarknessForProject(project: Project): void {
const cacheKey = project.currentGraphNodeId ?? project.currentSceneId ?? null;
const scene = project.currentSceneId ? project.scenes[project.currentSceneId] : undefined;
@@ -435,11 +444,16 @@ async function main() {
registerHandler(ipcChannels.license.setToken, async ({ token }) => licenseService.setToken(token));
registerHandler(ipcChannels.license.clearToken, () => licenseService.clearToken());
registerHandler(ipcChannels.license.acceptEula, ({ version }) => licenseService.acceptEula(version));
registerHandler(ipcChannels.windows.openMultiWindow, () => {
registerHandler(ipcChannels.windows.openMultiWindow, (req) => {
sceneDarknessStore.resetSession();
sceneTrapsStore.resetSession();
sceneTokensSessionStore.reset();
sceneNpcTokensSessionStore.reset();
scenePlayerTokensSessionStore.reset();
const playerIds = Array.isArray(req?.playerIds) ? req.playerIds.map(String).filter(Boolean) : [];
if (playerIds.length > 0) {
scenePlayerTokensSessionStore.dispatch({ kind: 'setSelection', playerIds });
}
effectsStore.dispatch({ kind: 'tool.set', tool: effectsDefaultTool() });
openMultiWindow();
const project = projectStore.getOpenProject();
@@ -451,6 +465,7 @@ async function main() {
emitSceneTrapsState();
emitSceneTokensSessionState();
emitSceneNpcTokensSessionState();
emitScenePlayerTokensSessionState();
emitEffectsState();
return { ok: true };
});
@@ -548,9 +563,11 @@ async function main() {
sceneViewStore.reset();
sceneTokensSessionStore.reset();
sceneNpcTokensSessionStore.reset();
scenePlayerTokensSessionStore.reset();
emitSceneViewState();
emitSceneTokensSessionState();
emitSceneNpcTokensSessionState();
emitScenePlayerTokensSessionState();
emitSessionState();
warmNpcsEditorWindow();
return { project };
@@ -566,6 +583,7 @@ async function main() {
sceneViewStore.reset();
sceneTokensSessionStore.reset();
sceneNpcTokensSessionStore.reset();
scenePlayerTokensSessionStore.reset();
emitEffectsState();
emitMaterialsOverlayState();
emitNpcsOverlayState();
@@ -574,6 +592,7 @@ async function main() {
emitSceneViewState();
emitSceneTokensSessionState();
emitSceneNpcTokensSessionState();
emitScenePlayerTokensSessionState();
emitSessionState();
return { ok: true };
});
@@ -590,6 +609,7 @@ async function main() {
materialsOverlayStore.clear();
npcsOverlayStore.clear();
sceneViewStore.reset();
scenePlayerTokensSessionStore.clearPlacements();
// Token moves persist for the whole play session (reset only on project open/close).
const project = projectStore.getOpenProject();
if (project) {
@@ -603,6 +623,7 @@ async function main() {
emitSceneTrapsState();
emitSceneViewState();
emitSceneTokensSessionState();
emitScenePlayerTokensSessionState();
emitSessionState();
return { currentSceneId: projectStore.getOpenProject()?.currentSceneId ?? null };
});
@@ -619,6 +640,7 @@ async function main() {
materialsOverlayStore.clear();
npcsOverlayStore.clear();
sceneViewStore.reset();
scenePlayerTokensSessionStore.clearPlacements();
// Token moves persist for the whole play session (reset only on project open/close).
const project = projectStore.getOpenProject();
if (project) {
@@ -632,6 +654,7 @@ async function main() {
emitSceneTrapsState();
emitSceneViewState();
emitSceneTokensSessionState();
emitScenePlayerTokensSessionState();
emitSessionState();
const p = projectStore.getOpenProject();
return {
@@ -784,7 +807,7 @@ async function main() {
});
registerHandler(
ipcChannels.project.upsertNpc,
async ({ npcId, name, description, filePath: pathFromDrop, groupId, ringColor, imageOffset }) => {
async ({ npcId, name, description, filePath: pathFromDrop, groupId, ringColor, disposition, imageOffset }) => {
let filePath = pathFromDrop;
if (!filePath && !npcId) {
const { canceled, filePaths } = await dialog.showOpenDialog({
@@ -809,6 +832,7 @@ async function main() {
...(filePath ? { filePath } : {}),
...(groupId !== undefined ? { groupId } : {}),
...(ringColor !== undefined ? { ringColor } : {}),
...(disposition !== undefined ? { disposition } : {}),
...(imageOffset !== undefined ? { imageOffset } : {}),
},
(p) => emitNpcUpsertProgress(p),
@@ -821,13 +845,15 @@ async function main() {
);
registerHandler(
ipcChannels.project.updateNpcFields,
async ({ npcId, name, description, groupId, ringColor, imageOffset }) => {
async ({ npcId, name, description, groupId, ringColor, disposition, imageOffset, imageScale }) => {
const project = await projectStore.updateNpcFields(npcId, {
...(typeof name === 'string' ? { name } : {}),
...(typeof description === 'string' ? { description } : {}),
...(groupId !== undefined ? { groupId } : {}),
...(ringColor !== undefined ? { ringColor } : {}),
...(disposition !== undefined ? { disposition } : {}),
...(imageOffset !== undefined ? { imageOffset } : {}),
...(imageScale !== undefined ? { imageScale } : {}),
});
emitSessionState();
return { project };
@@ -1428,6 +1454,14 @@ async function main() {
emitSceneNpcTokensSessionState();
return { ok: true };
});
registerHandler(ipcChannels.scenePlayerTokensSession.getState, () => {
return { state: scenePlayerTokensSessionStore.getState() };
});
registerHandler(ipcChannels.scenePlayerTokensSession.dispatch, ({ event }) => {
scenePlayerTokensSessionStore.dispatch(event);
emitScenePlayerTokensSessionState();
return { ok: true };
});
registerHandler(ipcChannels.video.getState, () => {
return { state: videoStore.getState() };