Merge branch 'users' into main

Players library, circular NPC tokens, disposition types; keep main overlay/release fixes.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-08-04 08:03:22 +08:00
69 changed files with 5943 additions and 327 deletions
+18
View File
@@ -0,0 +1,18 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
const here = path.dirname(fileURLToPath(import.meta.url));
const contractsPath = path.join(here, 'contracts.ts');
void test('contracts: players and sceneNpcTokensSession channels exist', () => {
const src = fs.readFileSync(contractsPath, 'utf8');
assert.match(src, /players:\s*\{/);
assert.match(src, /list:\s*'players\.list'/);
assert.match(src, /upsertProgress:\s*'players\.upsertProgress'/);
assert.match(src, /sceneNpcTokensSession:\s*\{/);
assert.match(src, /scenePlayerTokensSession:\s*\{/);
assert.match(src, /npcTokens\?:\s*SceneNpcToken\[\]/);
});
+118 -1
View File
@@ -12,16 +12,28 @@ import type {
NpcId,
NpcRelationId,
NpcGroupId,
NpcDisposition,
NpcsOverlayEvent,
NpcsOverlayState,
Project,
ProjectId,
AppToken,
AppPlayer,
AppPlayerTeam,
PlayerId,
PlayerTeamId,
PlayerImageOffset,
PlayersUpsertProgressEvent,
Scene,
SceneDarknessEvent,
SceneDarknessState,
SceneGrid,
SceneId,
SceneNpcToken,
SceneNpcTokensSessionEvent,
SceneNpcTokensSessionState,
ScenePlayerTokensSessionEvent,
ScenePlayerTokensSessionState,
SceneToken,
SceneTokensSessionEvent,
SceneTokensSessionState,
@@ -185,6 +197,30 @@ export const ipcChannels = {
dispatch: 'sceneTokensSession.dispatch',
stateChanged: 'sceneTokensSession.stateChanged',
},
players: {
list: 'players.list',
upsert: 'players.upsert',
delete: 'players.delete',
setOrder: 'players.setOrder',
upsertTeam: 'players.upsertTeam',
deleteTeam: 'players.deleteTeam',
setTeamsOrder: 'players.setTeamsOrder',
assignTeam: 'players.assignTeam',
pickImage: 'players.pickImage',
imageUrl: 'players.imageUrl',
upsertProgress: 'players.upsertProgress',
stateChanged: 'players.stateChanged',
},
sceneNpcTokensSession: {
getState: 'sceneNpcTokensSession.getState',
dispatch: 'sceneNpcTokensSession.dispatch',
stateChanged: 'sceneNpcTokensSession.stateChanged',
},
scenePlayerTokensSession: {
getState: 'scenePlayerTokensSession.getState',
dispatch: 'scenePlayerTokensSession.dispatch',
stateChanged: 'scenePlayerTokensSession.stateChanged',
},
video: {
getState: 'video.getState',
dispatch: 'video.dispatch',
@@ -257,6 +293,10 @@ export type IpcEventMap = {
[ipcChannels.sceneView.stateChanged]: { state: SceneViewState };
[ipcChannels.tokens.stateChanged]: { tokens: AppToken[] };
[ipcChannels.sceneTokensSession.stateChanged]: { state: SceneTokensSessionState };
[ipcChannels.players.stateChanged]: { players: AppPlayer[]; teams: AppPlayerTeam[] };
[ipcChannels.players.upsertProgress]: PlayersUpsertProgressEvent;
[ipcChannels.sceneNpcTokensSession.stateChanged]: { state: SceneNpcTokensSessionState };
[ipcChannels.scenePlayerTokensSession.stateChanged]: { state: ScenePlayerTokensSessionState };
[ipcChannels.video.stateChanged]: { state: VideoPlaybackState };
[ipcChannels.license.statusChanged]: { snapshot: LicenseSnapshot };
[ipcChannels.windows.multiWindowStateChanged]: { open: boolean };
@@ -372,6 +412,10 @@ export type IpcInvokeMap = {
description?: string;
filePath?: string;
groupId?: NpcGroupId | null;
ringColor?: string;
disposition?: NpcDisposition;
imageOffset?: PlayerImageOffset;
imageScale?: number;
};
res: { project: Project };
};
@@ -381,6 +425,10 @@ export type IpcInvokeMap = {
name?: string;
description?: string;
groupId?: NpcGroupId | null;
ringColor?: string;
disposition?: NpcDisposition;
imageOffset?: PlayerImageOffset;
imageScale?: number;
};
res: { project: Project };
};
@@ -577,7 +625,7 @@ export type IpcInvokeMap = {
res: { ok: true };
};
[ipcChannels.windows.openMultiWindow]: {
req: Record<string, never>;
req: { playerIds?: string[] };
res: { ok: true };
};
[ipcChannels.windows.closeMultiWindow]: {
@@ -720,6 +768,70 @@ export type IpcInvokeMap = {
req: { event: SceneTokensSessionEvent };
res: { ok: true };
};
[ipcChannels.players.list]: {
req: Record<string, never>;
res: { players: AppPlayer[]; teams: AppPlayerTeam[] };
};
[ipcChannels.players.upsert]: {
req: {
id?: PlayerId | null;
name: string;
filePath?: string | null;
teamId?: PlayerTeamId | null;
ringColor?: string;
imageOffset?: PlayerImageOffset;
imageScale?: number;
};
res: { player: AppPlayer };
};
[ipcChannels.players.delete]: {
req: { id: PlayerId };
res: { ok: true };
};
[ipcChannels.players.setOrder]: {
req: { playerIds: PlayerId[] };
res: { players: AppPlayer[] };
};
[ipcChannels.players.upsertTeam]: {
req: { id?: PlayerTeamId | null; name: string; color?: string };
res: { team: AppPlayerTeam };
};
[ipcChannels.players.deleteTeam]: {
req: { id: PlayerTeamId };
res: { ok: true };
};
[ipcChannels.players.setTeamsOrder]: {
req: { teamIds: PlayerTeamId[] };
res: { teams: AppPlayerTeam[] };
};
[ipcChannels.players.assignTeam]: {
req: { playerId: PlayerId; teamId: PlayerTeamId | null };
res: { player: AppPlayer | null };
};
[ipcChannels.players.pickImage]: {
req: Record<string, never>;
res: { canceled: true } | { canceled: false; filePath: string; previewDataUrl: string };
};
[ipcChannels.players.imageUrl]: {
req: { id: PlayerId };
res: { url: string | null };
};
[ipcChannels.sceneNpcTokensSession.getState]: {
req: Record<string, never>;
res: { state: SceneNpcTokensSessionState };
};
[ipcChannels.sceneNpcTokensSession.dispatch]: {
req: { event: SceneNpcTokensSessionEvent };
res: { ok: true };
};
[ipcChannels.scenePlayerTokensSession.getState]: {
req: Record<string, never>;
res: { state: ScenePlayerTokensSessionState };
};
[ipcChannels.scenePlayerTokensSession.dispatch]: {
req: { event: ScenePlayerTokensSessionEvent };
res: { ok: true };
};
[ipcChannels.video.getState]: {
req: Record<string, never>;
res: { state: VideoPlaybackState };
@@ -761,6 +873,10 @@ export type LegacyIpcEventMap = {
[ipcChannels.sceneView.stateChanged]: { state: SceneViewState };
[ipcChannels.tokens.stateChanged]: { tokens: AppToken[] };
[ipcChannels.sceneTokensSession.stateChanged]: { state: SceneTokensSessionState };
[ipcChannels.players.stateChanged]: { players: AppPlayer[]; teams: AppPlayerTeam[] };
[ipcChannels.players.upsertProgress]: PlayersUpsertProgressEvent;
[ipcChannels.sceneNpcTokensSession.stateChanged]: { state: SceneNpcTokensSessionState };
[ipcChannels.scenePlayerTokensSession.stateChanged]: { state: ScenePlayerTokensSessionState };
[ipcChannels.video.stateChanged]: { state: VideoPlaybackState };
[ipcChannels.license.statusChanged]: Record<string, never>;
};
@@ -776,6 +892,7 @@ export type ScenePatch = {
darkenScene?: boolean;
traps?: SceneTrap[];
tokens?: SceneToken[];
npcTokens?: SceneNpcToken[];
grid?: SceneGrid;
settings?: Partial<Scene['settings']>;
media?: Partial<Scene['media']>;