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
+12 -15
View File
@@ -50,11 +50,7 @@ type Actions = {
importCampaignAudio: () => Promise<void>;
importCampaignAudioFromPaths: (filePaths: string[]) => Promise<void>;
updateCampaignAudios: (next: Project['campaignAudios']) => Promise<void>;
upsertMaterial: (input: {
materialId?: MaterialId;
name: string;
filePath?: string;
}) => Promise<void>;
upsertMaterial: (input: { materialId?: MaterialId; name: string; filePath?: string }) => Promise<void>;
deleteMaterial: (materialId: MaterialId) => Promise<void>;
setMaterialsOrder: (materialIds: MaterialId[]) => Promise<void>;
setMaterialRotation: (materialId: MaterialId, rotationDeg: 0 | 90 | 180 | 270) => Promise<void>;
@@ -104,7 +100,10 @@ type Actions = {
mode: 'folder' | 'archive',
) => Promise<{ canceled: true } | { canceled: false; sourcePath: string }>;
importFoundryProject: (sourcePath: string) => Promise<void>;
peekImportZip: (labels: StorylineLabels, targetHasMainStart: boolean) => Promise<
peekImportZip: (
labels: StorylineLabels,
targetHasMainStart: boolean,
) => Promise<
| { canceled: true }
| {
canceled: false;
@@ -355,6 +354,7 @@ export function useProjectState(licenseActive: boolean, opts?: ProjectStateOpts)
darkenScene: false,
traps: [],
tokens: [],
npcTokens: [],
grid: { enabled: false, type: 'square', sizeN: 0.06, color: '#ffffff' },
media: { videos: [], audios: [] },
settings: { autoplayVideo: false, autoplayAudio: true, loopVideo: true, loopAudio: true },
@@ -509,11 +509,7 @@ export function useProjectState(licenseActive: boolean, opts?: ProjectStateOpts)
await refreshProjects();
};
const upsertMaterial = async (input: {
materialId?: MaterialId;
name: string;
filePath?: string;
}) => {
const upsertMaterial = async (input: { materialId?: MaterialId; name: string; filePath?: string }) => {
const res = await api.invoke(ipcChannels.project.upsertMaterial, input);
setState((s) => ({ ...s, project: res.project }));
await refreshProjects();
@@ -531,10 +527,7 @@ export function useProjectState(licenseActive: boolean, opts?: ProjectStateOpts)
await refreshProjects();
};
const setMaterialRotation = async (
materialId: MaterialId,
rotationDeg: 0 | 90 | 180 | 270,
) => {
const setMaterialRotation = async (materialId: MaterialId, rotationDeg: 0 | 90 | 180 | 270) => {
const res = await api.invoke(ipcChannels.project.setMaterialRotation, {
materialId,
rotationDeg,
@@ -570,6 +563,9 @@ export function useProjectState(licenseActive: boolean, opts?: ProjectStateOpts)
previewRotationDeg?: 0 | 90 | 180 | 270;
darkenScene?: boolean;
traps?: import('../../../shared/types').SceneTrap[];
tokens?: import('../../../shared/types').SceneToken[];
npcTokens?: import('../../../shared/types').SceneNpcToken[];
grid?: import('../../../shared/types').SceneGrid;
settings?: Partial<Scene['settings']>;
media?: Partial<Scene['media']>;
layout?: { x: number; y: number };
@@ -598,6 +594,7 @@ export function useProjectState(licenseActive: boolean, opts?: ProjectStateOpts)
...(patch.darkenScene !== undefined ? { darkenScene: patch.darkenScene } : null),
...(patch.traps !== undefined ? { traps: patch.traps } : null),
...(patch.tokens !== undefined ? { tokens: patch.tokens } : null),
...(patch.npcTokens !== undefined ? { npcTokens: patch.npcTokens } : null),
...(patch.grid !== undefined ? { grid: patch.grid } : null),
...(patch.settings ? { settings: { ...scene.settings, ...patch.settings } } : null),
...(patch.media ? { media: { ...scene.media, ...patch.media } } : null),