feat(players): app Players library and circular NPC tokens on scenes

Add userData players/teams, scene npcTokens with hex-inscribed sizing, session scale synced to presentation, and Playwright e2e coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-07-30 13:38:35 +08:00
parent a3a03eb9e3
commit 101f595bac
57 changed files with 4176 additions and 317 deletions
+97
View File
@@ -17,11 +17,20 @@ import type {
Project,
ProjectId,
AppToken,
AppPlayer,
AppPlayerTeam,
PlayerId,
PlayerTeamId,
PlayerImageOffset,
PlayersUpsertProgressEvent,
Scene,
SceneDarknessEvent,
SceneDarknessState,
SceneGrid,
SceneId,
SceneNpcToken,
SceneNpcTokensSessionEvent,
SceneNpcTokensSessionState,
SceneToken,
SceneTokensSessionEvent,
SceneTokensSessionState,
@@ -182,6 +191,25 @@ 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',
},
video: {
getState: 'video.getState',
dispatch: 'video.dispatch',
@@ -254,6 +282,9 @@ 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.video.stateChanged]: { state: VideoPlaybackState };
[ipcChannels.license.statusChanged]: { snapshot: LicenseSnapshot };
[ipcChannels.windows.multiWindowStateChanged]: { open: boolean };
@@ -368,6 +399,9 @@ export type IpcInvokeMap = {
description?: string;
filePath?: string;
groupId?: NpcGroupId | null;
ringColor?: string;
imageOffset?: PlayerImageOffset;
imageScale?: number;
};
res: { project: Project };
};
@@ -377,6 +411,9 @@ export type IpcInvokeMap = {
name?: string;
description?: string;
groupId?: NpcGroupId | null;
ringColor?: string;
imageOffset?: PlayerImageOffset;
imageScale?: number;
};
res: { project: Project };
};
@@ -708,6 +745,62 @@ 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.video.getState]: {
req: Record<string, never>;
res: { state: VideoPlaybackState };
@@ -749,6 +842,9 @@ 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.video.stateChanged]: { state: VideoPlaybackState };
[ipcChannels.license.statusChanged]: Record<string, never>;
};
@@ -764,6 +860,7 @@ export type ScenePatch = {
darkenScene?: boolean;
traps?: SceneTrap[];
tokens?: SceneToken[];
npcTokens?: SceneNpcToken[];
grid?: SceneGrid;
settings?: Partial<Scene['settings']>;
media?: Partial<Scene['media']>;