Files
DndGamePlayer/app/shared/ipc/contracts.ts
T
Ivan Fontosh bdeb64e356 feat(scene): battle grid overlay with color and trap VFX stacking
Add configurable square/hex grid in the scene editor (persisted and shown on control/presentation) and keep trap activation effects above trap icons.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 06:47:59 +08:00

712 lines
22 KiB
TypeScript

import type { LicenseSnapshot } from '../license/licenseSnapshot';
import type {
AssetId,
EffectsEvent,
EffectsState,
GraphNodeId,
MaterialId,
MaterialLegend,
MaterialsOverlayEvent,
MaterialsOverlayState,
MediaAsset,
NpcId,
NpcRelationId,
NpcGroupId,
NpcBinding,
NpcsOverlayEvent,
NpcsOverlayState,
Project,
ProjectId,
Scene,
SceneDarknessEvent,
SceneDarknessState,
SceneGrid,
SceneId,
SceneTrap,
SceneTrapsEvent,
SceneTrapsState,
SceneViewEvent,
SceneViewState,
VideoPlaybackEvent,
VideoPlaybackState,
} from '../types';
import type {
NpcImportResolution,
SceneImportResolution,
StorylineImportMergeReport,
StorylineLabels,
StorylineListItem,
StorylineSelection,
} from '../graph/storylineExportImport';
export const ipcChannels = {
app: {
quit: 'app.quit',
getVersion: 'app.getVersion',
},
updater: {
check: 'updater.check',
downloadAndRestart: 'updater.downloadAndRestart',
progress: 'updater.progress',
},
project: {
list: 'project.list',
create: 'project.create',
open: 'project.open',
close: 'project.close',
saveNow: 'project.saveNow',
get: 'project.get',
updateScene: 'project.updateScene',
updateConnections: 'project.updateConnections',
setCurrentScene: 'project.setCurrentScene',
setCurrentGraphNode: 'project.setCurrentGraphNode',
importMedia: 'project.importMedia',
importCampaignAudio: 'project.importCampaignAudio',
updateCampaignAudios: 'project.updateCampaignAudios',
upsertMaterial: 'project.upsertMaterial',
setMaterialRotation: 'project.setMaterialRotation',
setMaterialLegend: 'project.setMaterialLegend',
deleteMaterial: 'project.deleteMaterial',
setMaterialsOrder: 'project.setMaterialsOrder',
pickMaterialImage: 'project.pickMaterialImage',
upsertNpc: 'project.upsertNpc',
updateNpcFields: 'project.updateNpcFields',
updateNpcPosition: 'project.updateNpcPosition',
deleteNpc: 'project.deleteNpc',
setNpcsOrder: 'project.setNpcsOrder',
pickNpcAvatar: 'project.pickNpcAvatar',
upsertNpcRelation: 'project.upsertNpcRelation',
deleteNpcRelation: 'project.deleteNpcRelation',
upsertNpcGroup: 'project.upsertNpcGroup',
deleteNpcGroup: 'project.deleteNpcGroup',
setNpcGroupsOrder: 'project.setNpcGroupsOrder',
importScenePreview: 'project.importScenePreview',
clearScenePreview: 'project.clearScenePreview',
assetFileUrl: 'project.assetFileUrl',
updateSceneGraphNodePosition: 'project.updateSceneGraphNodePosition',
addSceneGraphNode: 'project.addSceneGraphNode',
removeSceneGraphNode: 'project.removeSceneGraphNode',
addSceneGraphEdge: 'project.addSceneGraphEdge',
removeSceneGraphEdge: 'project.removeSceneGraphEdge',
setSceneGraphNodeStart: 'project.setSceneGraphNodeStart',
setSceneGraphNodeSideStoryStart: 'project.setSceneGraphNodeSideStoryStart',
updateSideStoryLineTitle: 'project.updateSideStoryLineTitle',
deleteScene: 'project.deleteScene',
setSceneListOrder: 'project.setSceneListOrder',
rename: 'project.rename',
importZip: 'project.importZip',
exportZip: 'project.exportZip',
getProjectStorylines: 'project.getProjectStorylines',
peekImportZip: 'project.peekImportZip',
pickImportZipFile: 'project.pickImportZipFile',
peekImportZipPath: 'project.peekImportZipPath',
peekImportFromProject: 'project.peekImportFromProject',
mergeImportZip: 'project.mergeImportZip',
mergeImportFromProject: 'project.mergeImportFromProject',
importZipFromPath: 'project.importZipFromPath',
importFoundry: 'project.importFoundry',
pickFoundrySource: 'project.pickFoundrySource',
deleteProject: 'project.deleteProject',
importZipProgress: 'project.importZipProgress',
exportZipProgress: 'project.exportZipProgress',
scenePreviewImportProgress: 'project.scenePreviewImportProgress',
materialUpsertProgress: 'project.materialUpsertProgress',
},
windows: {
openMultiWindow: 'windows.openMultiWindow',
closeMultiWindow: 'windows.closeMultiWindow',
togglePresentationFullscreen: 'windows.togglePresentationFullscreen',
getMultiWindowState: 'windows.getMultiWindowState',
multiWindowStateChanged: 'windows.multiWindowStateChanged',
openSceneDescription: 'windows.openSceneDescription',
closeSceneDescription: 'windows.closeSceneDescription',
getSceneDescriptionContent: 'windows.getSceneDescriptionContent',
sceneDescriptionContent: 'windows.sceneDescriptionContent',
openMaterials: 'windows.openMaterials',
closeMaterials: 'windows.closeMaterials',
openNpcsEditor: 'windows.openNpcsEditor',
closeNpcsEditor: 'windows.closeNpcsEditor',
openNpcs: 'windows.openNpcs',
closeNpcs: 'windows.closeNpcs',
openSceneEditor: 'windows.openSceneEditor',
closeSceneEditor: 'windows.closeSceneEditor',
},
session: {
stateChanged: 'session.stateChanged',
},
effects: {
getState: 'effects.getState',
dispatch: 'effects.dispatch',
stateChanged: 'effects.stateChanged',
},
materialsOverlay: {
getState: 'materialsOverlay.getState',
dispatch: 'materialsOverlay.dispatch',
stateChanged: 'materialsOverlay.stateChanged',
},
npcsOverlay: {
getState: 'npcsOverlay.getState',
dispatch: 'npcsOverlay.dispatch',
stateChanged: 'npcsOverlay.stateChanged',
},
sceneDarkness: {
getState: 'sceneDarkness.getState',
dispatch: 'sceneDarkness.dispatch',
stateChanged: 'sceneDarkness.stateChanged',
},
sceneTraps: {
getState: 'sceneTraps.getState',
dispatch: 'sceneTraps.dispatch',
stateChanged: 'sceneTraps.stateChanged',
},
sceneView: {
getState: 'sceneView.getState',
dispatch: 'sceneView.dispatch',
stateChanged: 'sceneView.stateChanged',
},
video: {
getState: 'video.getState',
dispatch: 'video.dispatch',
stateChanged: 'video.stateChanged',
},
license: {
getStatus: 'license.getStatus',
setToken: 'license.setToken',
clearToken: 'license.clearToken',
acceptEula: 'license.acceptEula',
statusChanged: 'license.statusChanged',
},
} as const;
export type ZipProgressEvent = {
kind: 'import' | 'export';
stage: 'copy' | 'unzip' | 'zip' | 'done';
percent: number; // 0..100
detail?: string;
};
export type ScenePreviewImportEvent = {
sceneId: SceneId;
assetId: AssetId;
phase: 'queued' | 'optimizing' | 'thumbnail' | 'done' | 'error';
project?: Project;
message?: string;
};
export type MaterialUpsertProgressEvent = {
percent: number;
stage: string;
detail?: string;
};
export type UpdaterCheckResponse =
| { outcome: 'not_packaged' }
| { outcome: 'no_license' }
| { outcome: 'current'; currentVersion: string }
| { outcome: 'available'; version: string }
| { outcome: 'error'; message: string };
export type UpdaterDownloadResponse = { ok: true } | { ok: false; message: string };
export type UpdaterProgressPhase =
| 'checking'
| 'available'
| 'not-available'
| 'downloading'
| 'installing'
| 'error';
export type UpdaterProgressEvent = {
phase: UpdaterProgressPhase;
/** 0..100 при phase=downloading */
percent?: number;
version?: string;
message?: string;
};
export type IpcEventMap = {
[ipcChannels.session.stateChanged]: { state: SessionState };
[ipcChannels.effects.stateChanged]: { state: EffectsState };
[ipcChannels.materialsOverlay.stateChanged]: { state: MaterialsOverlayState };
[ipcChannels.npcsOverlay.stateChanged]: { state: NpcsOverlayState };
[ipcChannels.sceneDarkness.stateChanged]: { state: SceneDarknessState };
[ipcChannels.sceneTraps.stateChanged]: { state: SceneTrapsState };
[ipcChannels.sceneView.stateChanged]: { state: SceneViewState };
[ipcChannels.video.stateChanged]: { state: VideoPlaybackState };
[ipcChannels.license.statusChanged]: { snapshot: LicenseSnapshot };
[ipcChannels.windows.multiWindowStateChanged]: { open: boolean };
[ipcChannels.windows.sceneDescriptionContent]: { html: string };
[ipcChannels.project.importZipProgress]: ZipProgressEvent;
[ipcChannels.project.exportZipProgress]: ZipProgressEvent;
[ipcChannels.project.scenePreviewImportProgress]: ScenePreviewImportEvent;
[ipcChannels.project.materialUpsertProgress]: MaterialUpsertProgressEvent;
[ipcChannels.updater.progress]: UpdaterProgressEvent;
};
export type IpcInvokeMap = {
[ipcChannels.app.quit]: {
req: Record<string, never>;
res: { ok: true };
};
[ipcChannels.app.getVersion]: {
req: Record<string, never>;
res: { version: string; buildNumber: string | null; packaged: boolean };
};
[ipcChannels.updater.check]: {
req: Record<string, never>;
res: UpdaterCheckResponse;
};
[ipcChannels.updater.downloadAndRestart]: {
req: { version: string };
res: UpdaterDownloadResponse;
};
[ipcChannels.project.list]: {
req: Record<string, never>;
res: { projects: { id: ProjectId; name: string; updatedAt: string; fileName: string }[] };
};
[ipcChannels.project.create]: {
req: { name: string };
res: { project: Project };
};
[ipcChannels.project.open]: {
req: { projectId: ProjectId };
res: { project: Project };
};
[ipcChannels.project.close]: {
req: Record<string, never>;
res: { ok: true };
};
[ipcChannels.project.get]: {
req: Record<string, never>;
res: { project: Project | null };
};
[ipcChannels.project.saveNow]: {
req: Record<string, never>;
res: { ok: true };
};
[ipcChannels.project.updateScene]: {
req: { sceneId: SceneId; patch: ScenePatch };
res: { scene: Scene };
};
[ipcChannels.project.updateConnections]: {
req: { sceneId: SceneId; connections: SceneId[] };
res: { scene: Scene };
};
[ipcChannels.project.setCurrentScene]: {
req: { sceneId: SceneId | null };
res: { currentSceneId: SceneId | null };
};
[ipcChannels.project.setCurrentGraphNode]: {
req: { graphNodeId: GraphNodeId | null };
res: { currentGraphNodeId: GraphNodeId | null; currentSceneId: SceneId | null };
};
[ipcChannels.project.importMedia]: {
req: { sceneId: SceneId; filePaths?: string[] };
res: { project: Project; imported: MediaAsset[] };
};
[ipcChannels.project.importCampaignAudio]: {
req: { filePaths?: string[] };
res: { canceled: boolean; project: Project; imported: MediaAsset[] };
};
[ipcChannels.project.updateCampaignAudios]: {
req: { audios: Project['campaignAudios'] };
res: { project: Project };
};
[ipcChannels.project.upsertMaterial]: {
req: { materialId?: MaterialId; name: string; filePath?: string };
res: { project: Project };
};
[ipcChannels.project.setMaterialRotation]: {
req: { materialId: MaterialId; rotationDeg: 0 | 90 | 180 | 270 };
res: { project: Project };
};
[ipcChannels.project.setMaterialLegend]: {
req: { materialId: MaterialId; legend: MaterialLegend | null };
res: { project: Project };
};
[ipcChannels.project.deleteMaterial]: {
req: { materialId: MaterialId };
res: { project: Project };
};
[ipcChannels.project.setMaterialsOrder]: {
req: { materialIds: MaterialId[] };
res: { project: Project };
};
[ipcChannels.project.pickMaterialImage]: {
req: Record<string, never>;
res:
| { canceled: true }
| { canceled: false; filePath: string; previewDataUrl: string };
};
[ipcChannels.project.upsertNpc]: {
req: {
npcId?: NpcId;
name: string;
description?: string;
filePath?: string;
groupId?: NpcGroupId | null;
binding?: NpcBinding;
};
res: { project: Project };
};
[ipcChannels.project.updateNpcFields]: {
req: {
npcId: NpcId;
name?: string;
description?: string;
groupId?: NpcGroupId | null;
binding?: NpcBinding;
};
res: { project: Project };
};
[ipcChannels.project.updateNpcPosition]: {
req: { npcId: NpcId; x: number; y: number };
res: { project: Project };
};
[ipcChannels.project.deleteNpc]: {
req: { npcId: NpcId };
res: { project: Project };
};
[ipcChannels.project.setNpcsOrder]: {
req: { npcIds: NpcId[] };
res: { project: Project };
};
[ipcChannels.project.pickNpcAvatar]: {
req: Record<string, never>;
res:
| { canceled: true }
| { canceled: false; filePath: string; previewDataUrl: string };
};
[ipcChannels.project.upsertNpcRelation]: {
req: {
relationId?: NpcRelationId;
sourceNpcId: NpcId;
targetNpcId: NpcId;
label: string;
};
res: { project: Project };
};
[ipcChannels.project.deleteNpcRelation]: {
req: { relationId: NpcRelationId };
res: { project: Project };
};
[ipcChannels.project.upsertNpcGroup]: {
req: {
groupId?: NpcGroupId;
name: string;
color?: string;
parentId?: NpcGroupId | null;
};
res: { project: Project };
};
[ipcChannels.project.deleteNpcGroup]: {
req: { groupId: NpcGroupId };
res: { project: Project };
};
[ipcChannels.project.setNpcGroupsOrder]: {
req: { groupIds: NpcGroupId[] };
res: { project: Project };
};
[ipcChannels.project.importScenePreview]: {
req: { sceneId: SceneId; filePath?: string };
res: { project: Project; assetId: AssetId | null; background: boolean };
};
[ipcChannels.project.clearScenePreview]: {
req: { sceneId: SceneId };
res: { project: Project };
};
[ipcChannels.project.assetFileUrl]: {
req: { assetId: AssetId };
res: { url: string | null };
};
[ipcChannels.project.updateSceneGraphNodePosition]: {
req: { nodeId: GraphNodeId; x: number; y: number };
res: { project: Project };
};
[ipcChannels.project.addSceneGraphNode]: {
req: { sceneId: SceneId; x: number; y: number };
res: { project: Project };
};
[ipcChannels.project.removeSceneGraphNode]: {
req: { nodeId: GraphNodeId };
res: { project: Project };
};
[ipcChannels.project.addSceneGraphEdge]: {
req: { sourceGraphNodeId: GraphNodeId; targetGraphNodeId: GraphNodeId };
res: { project: Project };
};
[ipcChannels.project.removeSceneGraphEdge]: {
req: { edgeId: string };
res: { project: Project };
};
[ipcChannels.project.setSceneGraphNodeStart]: {
req: { graphNodeId: GraphNodeId | null };
res: { project: Project };
};
[ipcChannels.project.setSceneGraphNodeSideStoryStart]: {
req: { graphNodeId: GraphNodeId };
res: { project: Project };
};
[ipcChannels.project.updateSideStoryLineTitle]: {
req: { graphNodeId: GraphNodeId; title: string };
res: { project: Project };
};
[ipcChannels.project.deleteScene]: {
req: { sceneId: SceneId };
res: { project: Project };
};
[ipcChannels.project.setSceneListOrder]: {
req: { sceneListOrder: SceneId[] };
res: { project: Project };
};
[ipcChannels.project.rename]: {
req: { name: string; fileBaseName: string };
res: { project: Project };
};
[ipcChannels.project.importZip]: {
req: Record<string, never>;
res: { canceled: true } | { canceled: false; project: Project };
};
[ipcChannels.project.getProjectStorylines]: {
req: { projectId: ProjectId; labels: StorylineLabels };
res: { storylines: StorylineListItem[] };
};
[ipcChannels.project.peekImportZip]: {
req: { labels: StorylineLabels; targetHasMainStart: boolean };
res:
| { canceled: true }
| {
canceled: false;
filePath: string;
projectName: string;
storylines: StorylineListItem[];
sourceProject: Project;
};
};
[ipcChannels.project.pickImportZipFile]: {
req: Record<string, never>;
res: { canceled: true } | { canceled: false; filePath: string };
};
[ipcChannels.project.peekImportZipPath]: {
req: { filePath: string; labels: StorylineLabels; targetHasMainStart: boolean };
res: {
filePath: string;
projectName: string;
storylines: StorylineListItem[];
sourceProject: Project;
};
};
[ipcChannels.project.peekImportFromProject]: {
req: { sourceProjectId: ProjectId; labels: StorylineLabels; targetHasMainStart: boolean };
res: {
sourceProjectId: ProjectId;
projectName: string;
storylines: StorylineListItem[];
sourceProject: Project;
};
};
[ipcChannels.project.mergeImportZip]: {
req: {
filePath: string;
storylineSelections: StorylineSelection[];
sceneResolutions: SceneImportResolution[];
npcResolutions?: NpcImportResolution[];
};
res: { project: Project; report: StorylineImportMergeReport };
};
[ipcChannels.project.mergeImportFromProject]: {
req: {
sourceProjectId: ProjectId;
storylineSelections: StorylineSelection[];
sceneResolutions: SceneImportResolution[];
npcResolutions?: NpcImportResolution[];
};
res: { project: Project; report: StorylineImportMergeReport };
};
[ipcChannels.project.importZipFromPath]: {
req: { filePath: string };
res: { project: Project };
};
[ipcChannels.project.pickFoundrySource]: {
req: { mode: 'folder' | 'archive' };
res: { canceled: true } | { canceled: false; sourcePath: string };
};
[ipcChannels.project.importFoundry]: {
req: { sourcePath: string };
res: { project: Project };
};
[ipcChannels.project.exportZip]: {
req: { projectId: ProjectId; storylineSelections: StorylineSelection[]; labels: StorylineLabels };
res: { canceled: true } | { canceled: false };
};
[ipcChannels.project.deleteProject]: {
req: { projectId: ProjectId };
res: { ok: true };
};
[ipcChannels.windows.openMultiWindow]: {
req: Record<string, never>;
res: { ok: true };
};
[ipcChannels.windows.closeMultiWindow]: {
req: Record<string, never>;
res: { ok: true };
};
[ipcChannels.windows.togglePresentationFullscreen]: {
req: Record<string, never>;
res: { ok: true; isFullScreen: boolean };
};
[ipcChannels.windows.getMultiWindowState]: {
req: Record<string, never>;
res: { open: boolean };
};
[ipcChannels.windows.openSceneDescription]: {
req: { html: string };
res: { ok: true };
};
[ipcChannels.windows.closeSceneDescription]: {
req: Record<string, never>;
res: { ok: true };
};
[ipcChannels.windows.getSceneDescriptionContent]: {
req: Record<string, never>;
res: { html: string };
};
[ipcChannels.windows.openMaterials]: {
req: Record<string, never>;
res: { ok: true };
};
[ipcChannels.windows.closeMaterials]: {
req: Record<string, never>;
res: { ok: true };
};
[ipcChannels.windows.openNpcsEditor]: {
req: Record<string, never>;
res: { ok: true };
};
[ipcChannels.windows.closeNpcsEditor]: {
req: Record<string, never>;
res: { ok: true };
};
[ipcChannels.windows.openNpcs]: {
req: Record<string, never>;
res: { ok: true };
};
[ipcChannels.windows.closeNpcs]: {
req: Record<string, never>;
res: { ok: true };
};
[ipcChannels.windows.openSceneEditor]: {
req: Record<string, never>;
res: { ok: true };
};
[ipcChannels.windows.closeSceneEditor]: {
req: Record<string, never>;
res: { ok: true };
};
[ipcChannels.materialsOverlay.getState]: {
req: Record<string, never>;
res: { state: MaterialsOverlayState };
};
[ipcChannels.materialsOverlay.dispatch]: {
req: { event: MaterialsOverlayEvent };
res: { ok: true };
};
[ipcChannels.npcsOverlay.getState]: {
req: Record<string, never>;
res: { state: NpcsOverlayState };
};
[ipcChannels.npcsOverlay.dispatch]: {
req: { event: NpcsOverlayEvent };
res: { ok: true };
};
[ipcChannels.effects.getState]: {
req: Record<string, never>;
res: { state: EffectsState };
};
[ipcChannels.effects.dispatch]: {
req: { event: EffectsEvent };
res: { ok: true };
};
[ipcChannels.sceneDarkness.getState]: {
req: Record<string, never>;
res: { state: SceneDarknessState };
};
[ipcChannels.sceneDarkness.dispatch]: {
req: { event: SceneDarknessEvent };
res: { ok: true };
};
[ipcChannels.sceneTraps.getState]: {
req: Record<string, never>;
res: { state: SceneTrapsState };
};
[ipcChannels.sceneTraps.dispatch]: {
req: { event: SceneTrapsEvent };
res: { ok: true };
};
[ipcChannels.sceneView.getState]: {
req: Record<string, never>;
res: { state: SceneViewState };
};
[ipcChannels.sceneView.dispatch]: {
req: { event: SceneViewEvent };
res: { ok: true };
};
[ipcChannels.video.getState]: {
req: Record<string, never>;
res: { state: VideoPlaybackState };
};
[ipcChannels.video.dispatch]: {
req: { event: VideoPlaybackEvent };
res: { ok: true };
};
[ipcChannels.license.getStatus]: {
req: Record<string, never>;
res: LicenseSnapshot;
};
[ipcChannels.license.setToken]: {
req: { token: string };
res: LicenseSnapshot;
};
[ipcChannels.license.clearToken]: {
req: Record<string, never>;
res: LicenseSnapshot;
};
[ipcChannels.license.acceptEula]: {
req: { version: number };
res: { ok: true };
};
};
export type SessionState = {
project: Project | null;
currentSceneId: SceneId | null;
};
export type LegacyIpcEventMap = {
[ipcChannels.session.stateChanged]: { state: SessionState };
[ipcChannels.effects.stateChanged]: { state: EffectsState };
[ipcChannels.materialsOverlay.stateChanged]: { state: MaterialsOverlayState };
[ipcChannels.npcsOverlay.stateChanged]: { state: NpcsOverlayState };
[ipcChannels.sceneDarkness.stateChanged]: { state: SceneDarknessState };
[ipcChannels.sceneTraps.stateChanged]: { state: SceneTrapsState };
[ipcChannels.sceneView.stateChanged]: { state: SceneViewState };
[ipcChannels.video.stateChanged]: { state: VideoPlaybackState };
[ipcChannels.license.statusChanged]: Record<string, never>;
};
export type ScenePatch = {
title?: string;
description?: string;
previewAssetId?: AssetId | null;
previewAssetType?: 'image' | 'video' | null;
previewThumbAssetId?: AssetId | null;
previewVideoAutostart?: boolean;
previewRotationDeg?: 0 | 90 | 180 | 270;
darkenScene?: boolean;
traps?: SceneTrap[];
grid?: SceneGrid;
settings?: Partial<Scene['settings']>;
media?: Partial<Scene['media']>;
layout?: Partial<Scene['layout']>;
};