feat(scene): keep map tokens with preview rotation

When previewRotationDeg changes, remap non-player, NPC and trap
markers (plus live session token overrides) so they stay on the art.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-08-11 12:29:20 +08:00
parent 7362a36fe5
commit 46bec1a86a
11 changed files with 312 additions and 1 deletions
+28
View File
@@ -20,6 +20,10 @@ import {
} from '../shared/project/projectZipExtension'; } from '../shared/project/projectZipExtension';
import type { Project } from '../shared/types'; import type { Project } from '../shared/types';
import { asNpcId } from '../shared/types/ids'; import { asNpcId } from '../shared/types/ids';
import {
asPreviewRotationDeg,
previewRotationStepsCw,
} from '../shared/types/scenePreviewRotation';
import { EffectsStore, effectsDefaultTool } from './effects/effectsStore'; import { EffectsStore, effectsDefaultTool } from './effects/effectsStore';
import { SceneDarknessStore } from './effects/sceneDarknessStore'; import { SceneDarknessStore } from './effects/sceneDarknessStore';
@@ -678,6 +682,7 @@ async function main() {
}; };
}); });
registerHandler(ipcChannels.project.updateScene, async ({ sceneId, patch }) => { registerHandler(ipcChannels.project.updateScene, async ({ sceneId, patch }) => {
const before = projectStore.getOpenProject()?.scenes[sceneId];
const next = await projectStore.updateScene(sceneId, patch); const next = await projectStore.updateScene(sceneId, patch);
const project = projectStore.getOpenProject(); const project = projectStore.getOpenProject();
if (project?.currentSceneId === sceneId && patch.darkenScene !== undefined) { if (project?.currentSceneId === sceneId && patch.darkenScene !== undefined) {
@@ -688,6 +693,29 @@ async function main() {
syncSceneTrapsForProject(project); syncSceneTrapsForProject(project);
emitSceneTrapsState(); emitSceneTrapsState();
} }
if (
project?.currentSceneId === sceneId &&
patch.previewRotationDeg !== undefined &&
before
) {
const steps = previewRotationStepsCw(
asPreviewRotationDeg(before.previewRotationDeg),
asPreviewRotationDeg(patch.previewRotationDeg),
);
if (steps !== 0) {
sceneTokensSessionStore.rotateMapCwSteps(steps);
sceneNpcTokensSessionStore.rotateMapCwSteps(steps);
scenePlayerTokensSessionStore.rotateMapCwSteps(steps);
emitSceneTokensSessionState();
emitSceneNpcTokensSessionState();
emitScenePlayerTokensSessionState();
}
}
if (project && project.currentSceneId === sceneId && patch.previewRotationDeg !== undefined) {
// Trap ids stay the same; runtime statuses remain valid after coordinate remap in project.
syncSceneTrapsForProject(project);
emitSceneTrapsState();
}
emitSessionState(); emitSessionState();
return { scene: next }; return { scene: next };
}); });
@@ -6,6 +6,7 @@ import {
type SceneNpcTokensSessionState, type SceneNpcTokensSessionState,
} from '../../shared/types/appPlayers'; } from '../../shared/types/appPlayers';
import { normalizeNpcDisposition } from '../../shared/types/npcDisposition'; import { normalizeNpcDisposition } from '../../shared/types/npcDisposition';
import { rotateMapNormPointByCwSteps } from '../../shared/types/scenePreviewRotation';
function emptyState(revision = 1): SceneNpcTokensSessionState { function emptyState(revision = 1): SceneNpcTokensSessionState {
return { return {
@@ -63,6 +64,34 @@ export class SceneNpcTokensSessionStore {
return this.state; return this.state;
} }
/** Rotate session position overrides with the scene preview (CSS rotate steps). */
rotateMapCwSteps(steps: number): SceneNpcTokensSessionState {
const n = ((steps % 4) + 4) % 4;
if (n === 0) return this.state;
const ids = Object.keys(this.state.byPlacementId);
if (ids.length === 0) return this.state;
let changed = false;
const byPlacementId: SceneNpcTokensSessionState['byPlacementId'] = {};
for (const id of ids) {
const prev = this.state.byPlacementId[id];
if (!prev) continue;
if (typeof prev.nx === 'number' && typeof prev.ny === 'number') {
const p = rotateMapNormPointByCwSteps(prev.nx, prev.ny, n);
byPlacementId[id] = { ...prev, nx: p.nx, ny: p.ny };
changed = true;
} else {
byPlacementId[id] = prev;
}
}
if (!changed) return this.state;
this.state = {
revision: this.state.revision + 1,
byPlacementId,
scale: this.state.scale,
};
return this.state;
}
dispatch(event: SceneNpcTokensSessionEvent): SceneNpcTokensSessionState { dispatch(event: SceneNpcTokensSessionEvent): SceneNpcTokensSessionState {
switch (event.kind) { switch (event.kind) {
case 'clear': case 'clear':
@@ -37,3 +37,13 @@ void test('ScenePlayerTokensSessionStore move and reset', () => {
assert.equal(s2.visible, false); assert.equal(s2.visible, false);
assert.deepEqual(s2.byPlayerId, {}); assert.deepEqual(s2.byPlayerId, {});
}); });
void test('ScenePlayerTokensSessionStore rotateMapCwSteps', () => {
const store = new ScenePlayerTokensSessionStore();
store.dispatch({ kind: 'setSelection', playerIds: ['p1'] });
store.dispatch({ kind: 'show', sizeN: 0.1 });
store.dispatch({ kind: 'move', playerId: 'p1', nx: 0.2, ny: 0.1 });
const s = store.rotateMapCwSteps(1);
assert.equal(s.byPlayerId.p1?.nx, 0.9);
assert.equal(s.byPlayerId.p1?.ny, 0.2);
});
@@ -5,6 +5,7 @@ import {
type ScenePlayerTokensSessionEvent, type ScenePlayerTokensSessionEvent,
type ScenePlayerTokensSessionState, type ScenePlayerTokensSessionState,
} from '../../shared/types/appPlayers'; } from '../../shared/types/appPlayers';
import { rotateMapNormPointByCwSteps } from '../../shared/types/scenePreviewRotation';
function emptyState(revision = 1): ScenePlayerTokensSessionState { function emptyState(revision = 1): ScenePlayerTokensSessionState {
return { return {
@@ -60,6 +61,27 @@ export class ScenePlayerTokensSessionStore {
return this.state; return this.state;
} }
/** Rotate live player tokens with the scene preview (CSS rotate steps). */
rotateMapCwSteps(steps: number): ScenePlayerTokensSessionState {
const n = ((steps % 4) + 4) % 4;
if (n === 0) return this.state;
const ids = Object.keys(this.state.byPlayerId);
if (ids.length === 0) return this.state;
const byPlayerId: ScenePlayerTokensSessionState['byPlayerId'] = {};
for (const id of ids) {
const prev = this.state.byPlayerId[id];
if (!prev) continue;
const p = rotateMapNormPointByCwSteps(prev.nx, prev.ny, n);
byPlayerId[id] = { ...prev, nx: p.nx, ny: p.ny };
}
this.state = {
...this.state,
revision: this.state.revision + 1,
byPlayerId,
};
return this.state;
}
dispatch(event: ScenePlayerTokensSessionEvent): ScenePlayerTokensSessionState { dispatch(event: ScenePlayerTokensSessionEvent): ScenePlayerTokensSessionState {
switch (event.kind) { switch (event.kind) {
case 'clear': case 'clear':
+24
View File
@@ -67,6 +67,12 @@ import {
type NpcDisposition, type NpcDisposition,
} from '../../shared/types/npcDisposition'; } from '../../shared/types/npcDisposition';
import { DEFAULT_SCENE_GRID, normalizeSceneGrid } from '../../shared/types/sceneGrid'; import { DEFAULT_SCENE_GRID, normalizeSceneGrid } from '../../shared/types/sceneGrid';
import {
asPreviewRotationDeg,
previewRotationStepsCw,
rotateMapMarkersByCwSteps,
rotateSceneTokensByCwSteps,
} from '../../shared/types/scenePreviewRotation';
import { normalizeSceneTrap } from '../../shared/types/sceneTraps'; import { normalizeSceneTrap } from '../../shared/types/sceneTraps';
import type { import type {
AssetId, AssetId,
@@ -787,6 +793,24 @@ export class ZipProjectStore {
...(patch.layout ? { layout: { ...base.layout, ...patch.layout } } : null), ...(patch.layout ? { layout: { ...base.layout, ...patch.layout } } : null),
}; };
// Keep map markers glued to the art when previewRotationDeg changes (image/video).
if (patch.previewRotationDeg !== undefined) {
const fromRot = asPreviewRotationDeg(base.previewRotationDeg);
const toRot = asPreviewRotationDeg(patch.previewRotationDeg);
const steps = previewRotationStepsCw(fromRot, toRot);
if (steps !== 0) {
if (patch.tokens === undefined) {
next.tokens = rotateSceneTokensByCwSteps(base.tokens ?? [], steps);
}
if (patch.npcTokens === undefined) {
next.npcTokens = rotateMapMarkersByCwSteps(base.npcTokens ?? [], steps);
}
if (patch.traps === undefined) {
next.traps = rotateMapMarkersByCwSteps(base.traps ?? [], steps);
}
}
}
await this.updateProject((p) => { await this.updateProject((p) => {
const scenes = { ...p.scenes, [sceneId]: next }; const scenes = { ...p.scenes, [sceneId]: next };
const sceneListOrder = const sceneListOrder =
@@ -1,4 +1,5 @@
import type { SceneTokensSessionEvent, SceneTokensSessionState } from '../../shared/types'; import type { SceneTokensSessionEvent, SceneTokensSessionState } from '../../shared/types';
import { rotateMapNormPointByCwSteps } from '../../shared/types/scenePreviewRotation';
function emptyState(): SceneTokensSessionState { function emptyState(): SceneTokensSessionState {
return { return {
@@ -23,6 +24,26 @@ export class SceneTokensSessionStore {
return this.state; return this.state;
} }
/** Rotate session position overrides with the scene preview (CSS rotate steps). */
rotateMapCwSteps(steps: number): SceneTokensSessionState {
const n = ((steps % 4) + 4) % 4;
if (n === 0) return this.state;
const ids = Object.keys(this.state.byPlacementId);
if (ids.length === 0) return this.state;
const byPlacementId: SceneTokensSessionState['byPlacementId'] = {};
for (const id of ids) {
const prev = this.state.byPlacementId[id];
if (!prev) continue;
const next = rotateMapNormPointByCwSteps(prev.nx, prev.ny, n);
byPlacementId[id] = { nx: next.nx, ny: next.ny };
}
this.state = {
revision: this.state.revision + 1,
byPlacementId,
};
return this.state;
}
dispatch(event: SceneTokensSessionEvent): SceneTokensSessionState { dispatch(event: SceneTokensSessionEvent): SceneTokensSessionState {
switch (event.kind) { switch (event.kind) {
case 'clear': case 'clear':
+20
View File
@@ -20,6 +20,7 @@ import type {
} from '../../../shared/types'; } from '../../../shared/types';
import { getDndApi } from '../../shared/dndApi'; import { getDndApi } from '../../shared/dndApi';
import { invalidateAssetUrlCache } from '../../shared/useAssetImageUrl'; import { invalidateAssetUrlCache } from '../../shared/useAssetImageUrl';
import { applyPreviewRotationToSceneMarkers } from '../../../shared/types/scenePreviewRotation';
type ProjectSummary = { id: ProjectId; name: string; updatedAt: string; fileName: string }; type ProjectSummary = { id: ProjectId; name: string; updatedAt: string; fileName: string };
@@ -600,6 +601,25 @@ export function useProjectState(licenseActive: boolean, opts?: ProjectStateOpts)
...(patch.media ? { media: { ...scene.media, ...patch.media } } : null), ...(patch.media ? { media: { ...scene.media, ...patch.media } } : null),
layout: patch.layout ? { ...scene.layout, ...patch.layout } : scene.layout, layout: patch.layout ? { ...scene.layout, ...patch.layout } : scene.layout,
}; };
if (patch.previewRotationDeg !== undefined) {
const remapped = applyPreviewRotationToSceneMarkers(
{
previewRotationDeg: scene.previewRotationDeg ?? 0,
tokens: scene.tokens ?? [],
npcTokens: scene.npcTokens ?? [],
traps: scene.traps ?? [],
},
patch.previewRotationDeg,
{
tokensProvided: patch.tokens !== undefined,
npcTokensProvided: patch.npcTokens !== undefined,
trapsProvided: patch.traps !== undefined,
},
);
if (patch.tokens === undefined) next.tokens = remapped.tokens;
if (patch.npcTokens === undefined) next.npcTokens = remapped.npcTokens;
if (patch.traps === undefined) next.traps = remapped.traps;
}
const scenes = { ...p.scenes, [sceneId]: next }; const scenes = { ...p.scenes, [sceneId]: next };
const project: Project = { ...p, scenes }; const project: Project = { ...p, scenes };
return { ...s, project }; return { ...s, project };
+1
View File
@@ -10,6 +10,7 @@ export * from './npcs';
export * from './sceneDarkness'; export * from './sceneDarkness';
export * from './sceneGrid'; export * from './sceneGrid';
export * from './sceneGridSnap'; export * from './sceneGridSnap';
export * from './scenePreviewRotation';
export * from './sceneTraps'; export * from './sceneTraps';
export * from './sceneView'; export * from './sceneView';
export * from './videoPlayback'; export * from './videoPlayback';
@@ -0,0 +1,52 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
applyPreviewRotationToSceneMarkers,
previewRotationStepsCw,
rotateMapNormPointByCwSteps,
rotateMapNormPointCw90,
rotateSceneTokensByCwSteps,
} from './scenePreviewRotation';
void test('previewRotationStepsCw wraps correctly', () => {
assert.equal(previewRotationStepsCw(0, 90), 1);
assert.equal(previewRotationStepsCw(90, 180), 1);
assert.equal(previewRotationStepsCw(0, 270), 3);
assert.equal(previewRotationStepsCw(270, 0), 1);
assert.equal(previewRotationStepsCw(90, 90), 0);
});
void test('rotateMapNormPointCw90 matches CSS rotate(90deg) y-down', () => {
assert.deepEqual(rotateMapNormPointCw90(0, 0), { nx: 1, ny: 0 });
assert.deepEqual(rotateMapNormPointCw90(1, 0), { nx: 1, ny: 1 });
assert.deepEqual(rotateMapNormPointCw90(1, 1), { nx: 0, ny: 1 });
assert.deepEqual(rotateMapNormPointCw90(0, 1), { nx: 0, ny: 0 });
assert.deepEqual(rotateMapNormPointCw90(0.25, 0.1), { nx: 0.9, ny: 0.25 });
});
void test('four Cw90 steps return to start', () => {
const start = { nx: 0.2, ny: 0.7 };
assert.deepEqual(rotateMapNormPointByCwSteps(start.nx, start.ny, 4), start);
});
void test('rotateSceneTokensByCwSteps also turns facing', () => {
const [tok] = rotateSceneTokensByCwSteps([{ id: 'a', nx: 0.2, ny: 0.1, rotationDeg: 15 }], 1);
assert.ok(tok);
assert.equal(tok.nx, 0.9);
assert.equal(tok.ny, 0.2);
assert.equal(tok.rotationDeg, 105);
});
void test('applyPreviewRotationToSceneMarkers remaps when rotation changes', () => {
const scene = {
previewRotationDeg: 0 as const,
tokens: [{ id: 't', nx: 0.2, ny: 0.1, rotationDeg: 0 }],
npcTokens: [{ id: 'n', nx: 0.5, ny: 0.5 }],
traps: [{ id: 'tr', nx: 0, ny: 0 }],
};
const next = applyPreviewRotationToSceneMarkers(scene, 90);
assert.deepEqual(next.tokens[0], { id: 't', nx: 0.9, ny: 0.2, rotationDeg: 90 });
assert.deepEqual(next.npcTokens[0], { id: 'n', nx: 0.5, ny: 0.5 });
assert.deepEqual(next.traps[0], { id: 'tr', nx: 1, ny: 0 });
});
+104
View File
@@ -0,0 +1,104 @@
/** Rotate map-normalized markers with scene previewRotationDeg (CSS rotate, y-down). */
export type PreviewRotationDeg = 0 | 90 | 180 | 270;
export function asPreviewRotationDeg(raw: unknown): PreviewRotationDeg {
return raw === 90 || raw === 180 || raw === 270 ? raw : 0;
}
/** How many 90° clockwise steps take `from` to `to` (0..3). */
export function previewRotationStepsCw(from: PreviewRotationDeg, to: PreviewRotationDeg): number {
return ((((to - from) / 90) % 4) + 4) % 4;
}
/**
* One 90° clockwise step in image/AABB normalized space (origin top-left, y down).
* Matches CSS `transform: rotate(90deg)` of the media around its center.
*/
export function rotateMapNormPointCw90(nx: number, ny: number): { nx: number; ny: number } {
return {
nx: clamp01(1 - ny),
ny: clamp01(nx),
};
}
export function rotateMapNormPointByCwSteps(
nx: number,
ny: number,
steps: number,
): { nx: number; ny: number } {
let x = nx;
let y = ny;
const n = ((steps % 4) + 4) % 4;
for (let i = 0; i < n; i += 1) {
const next = rotateMapNormPointCw90(x, y);
x = next.nx;
y = next.ny;
}
return { nx: x, ny: y };
}
function clamp01(v: number): number {
if (!Number.isFinite(v)) return 0;
return Math.max(0, Math.min(1, v));
}
export function rotateMapMarkersByCwSteps<T extends { nx: number; ny: number }>(
items: readonly T[],
steps: number,
): T[] {
const n = ((steps % 4) + 4) % 4;
if (n === 0) return items.map((item) => ({ ...item }));
return items.map((item) => {
const p = rotateMapNormPointByCwSteps(item.nx, item.ny, n);
return { ...item, nx: p.nx, ny: p.ny };
});
}
/** Non-player tokens: move with the map and keep facing relative to the art. */
export function rotateSceneTokensByCwSteps<T extends { nx: number; ny: number; rotationDeg: number }>(
items: readonly T[],
steps: number,
): T[] {
const n = ((steps % 4) + 4) % 4;
if (n === 0) return items.map((item) => ({ ...item }));
const delta = n * 90;
return items.map((item) => {
const p = rotateMapNormPointByCwSteps(item.nx, item.ny, n);
return { ...item, nx: p.nx, ny: p.ny, rotationDeg: item.rotationDeg + delta };
});
}
/**
* Apply marker remaps when preview rotation changes.
* Skips collections explicitly provided in the patch (caller already sent final values).
*/
export function applyPreviewRotationToSceneMarkers<
TScene extends {
previewRotationDeg: PreviewRotationDeg;
tokens: readonly { nx: number; ny: number; rotationDeg: number }[];
npcTokens: readonly { nx: number; ny: number }[];
traps: readonly { nx: number; ny: number }[];
},
>(
scene: TScene,
nextRotationDeg: PreviewRotationDeg,
opts?: {
tokensProvided?: boolean;
npcTokensProvided?: boolean;
trapsProvided?: boolean;
},
): Pick<TScene, 'tokens' | 'npcTokens' | 'traps'> {
const steps = previewRotationStepsCw(scene.previewRotationDeg, nextRotationDeg);
return {
tokens: opts?.tokensProvided
? (scene.tokens as TScene['tokens'])
: (rotateSceneTokensByCwSteps(scene.tokens, steps) as TScene['tokens']),
npcTokens: opts?.npcTokensProvided
? (scene.npcTokens as TScene['npcTokens'])
: (rotateMapMarkersByCwSteps(scene.npcTokens, steps) as TScene['npcTokens']),
traps: opts?.trapsProvided
? (scene.traps as TScene['traps'])
: (rotateMapMarkersByCwSteps(scene.traps, steps) as TScene['traps']),
};
}
+1 -1
View File
@@ -10,7 +10,7 @@
"build:obfuscate": "node scripts/build.mjs --production --obfuscate", "build:obfuscate": "node scripts/build.mjs --production --obfuscate",
"lint": "eslint . --max-warnings 0", "lint": "eslint . --max-warnings 0",
"typecheck": "tsc -p tsconfig.eslint.json --noEmit", "typecheck": "tsc -p tsconfig.eslint.json --noEmit",
"test": "tsx --test app/renderer/shared/ui/controls.tooltip.test.ts app/renderer/shared/ui/secondaryWindows.stability.test.ts app/renderer/npcs/tiptapEditorSafe.test.ts app/renderer/editor/state/projectState.race.test.ts app/renderer/editor/fileDrop.test.ts app/shared/graph/sceneListOrder.test.ts app/renderer/editor/graph/sceneCardById.test.ts app/renderer/editor/i18n/editorMessages.locale.test.ts app/renderer/editor/help/helpLinkify.test.ts app/renderer/editor/sceneDescriptionHtml.test.ts app/shared/graph/storylineExportImport.test.ts app/shared/foundry/foundryGraph.test.ts app/shared/ipc/contracts.mediaRemoval.test.ts app/shared/effectEraserHitTest.test.ts app/shared/fieldEffectEraser.test.ts app/renderer/control/controlApp.effectsPanel.test.ts app/renderer/control/controlApp.audioPerf.networkRegression.test.ts app/renderer/control/controlApp.brushPerf.networkRegression.test.ts app/renderer/shared/useAssetImageUrl.cache.test.ts app/main/sessionIpc.phase6.networkRegression.test.ts app/renderer/shared/sceneOverlay/sceneOverlayHost.test.ts app/renderer/shared/effects/PxiEffectsOverlay.pointer.test.ts app/renderer/shared/traps/trapActivation.test.ts app/renderer/shared/videoSceneMapParity.test.ts app/main/windows/createWindows.editorClose.test.ts app/main/safeConsole.test.ts app/main/windows/bootWindow.test.ts app/main/effects/effectsStore.test.ts app/main/effects/sceneDarknessStore.test.ts app/main/sceneTraps/sceneTrapsStore.test.ts app/main/project/assetPrune.test.ts app/main/project/optimizeImageImport.test.ts app/main/project/scenePreviewThumbnail.test.ts app/main/project/fsRetry.test.ts app/main/project/zipRead.test.ts app/main/project/replaceFileAtomic.test.ts app/main/project/zipStore.legacyContract.test.ts app/shared/package.build.test.ts app/shared/license/canonicalJson.test.ts app/shared/license/productKey.test.ts app/shared/license/licenseService.networkRegression.test.ts app/shared/video/videoPlaybackPerf.networkRegression.test.ts app/shared/video/videoPlaybackLoop.networkRegression.test.ts app/main/license/verifyLicenseToken.test.ts app/main/license/machineFingerprint.test.ts app/shared/types/appPlayers.test.ts app/shared/types/npcDisposition.test.ts app/shared/types/sceneGrid.test.ts app/shared/types/sceneGridSnap.test.ts app/main/tokens/tokenGridSnapSessionStore.test.ts app/shared/players/playerTeams.test.ts app/shared/players/launchPlayersSelection.test.ts app/main/players/playersStore.test.ts app/main/players/sceneNpcTokensSessionStore.test.ts app/main/players/scenePlayerTokensSessionStore.test.ts app/shared/ipc/contracts.players.test.ts app/shared/types/containMediaRect.test.ts && node --test scripts/build-env.test.mjs scripts/obfuscate-main.test.mjs scripts/release-mac-prep.test.mjs scripts/release-native-prep.test.mjs scripts/verify-packaged-sharp.test.mjs app/main/project/sharpRuntime.test.mjs", "test": "tsx --test app/renderer/shared/ui/controls.tooltip.test.ts app/renderer/shared/ui/secondaryWindows.stability.test.ts app/renderer/npcs/tiptapEditorSafe.test.ts app/renderer/editor/state/projectState.race.test.ts app/renderer/editor/fileDrop.test.ts app/shared/graph/sceneListOrder.test.ts app/renderer/editor/graph/sceneCardById.test.ts app/renderer/editor/i18n/editorMessages.locale.test.ts app/renderer/editor/help/helpLinkify.test.ts app/renderer/editor/sceneDescriptionHtml.test.ts app/shared/graph/storylineExportImport.test.ts app/shared/foundry/foundryGraph.test.ts app/shared/ipc/contracts.mediaRemoval.test.ts app/shared/effectEraserHitTest.test.ts app/shared/fieldEffectEraser.test.ts app/renderer/control/controlApp.effectsPanel.test.ts app/renderer/control/controlApp.audioPerf.networkRegression.test.ts app/renderer/control/controlApp.brushPerf.networkRegression.test.ts app/renderer/shared/useAssetImageUrl.cache.test.ts app/main/sessionIpc.phase6.networkRegression.test.ts app/renderer/shared/sceneOverlay/sceneOverlayHost.test.ts app/renderer/shared/effects/PxiEffectsOverlay.pointer.test.ts app/renderer/shared/traps/trapActivation.test.ts app/renderer/shared/videoSceneMapParity.test.ts app/main/windows/createWindows.editorClose.test.ts app/main/safeConsole.test.ts app/main/windows/bootWindow.test.ts app/main/effects/effectsStore.test.ts app/main/effects/sceneDarknessStore.test.ts app/main/sceneTraps/sceneTrapsStore.test.ts app/main/project/assetPrune.test.ts app/main/project/optimizeImageImport.test.ts app/main/project/scenePreviewThumbnail.test.ts app/main/project/fsRetry.test.ts app/main/project/zipRead.test.ts app/main/project/replaceFileAtomic.test.ts app/main/project/zipStore.legacyContract.test.ts app/shared/package.build.test.ts app/shared/license/canonicalJson.test.ts app/shared/license/productKey.test.ts app/shared/license/licenseService.networkRegression.test.ts app/shared/video/videoPlaybackPerf.networkRegression.test.ts app/shared/video/videoPlaybackLoop.networkRegression.test.ts app/main/license/verifyLicenseToken.test.ts app/main/license/machineFingerprint.test.ts app/shared/types/appPlayers.test.ts app/shared/types/npcDisposition.test.ts app/shared/types/sceneGrid.test.ts app/shared/types/sceneGridSnap.test.ts app/main/tokens/tokenGridSnapSessionStore.test.ts app/shared/players/playerTeams.test.ts app/shared/players/launchPlayersSelection.test.ts app/main/players/playersStore.test.ts app/main/players/sceneNpcTokensSessionStore.test.ts app/main/players/scenePlayerTokensSessionStore.test.ts app/shared/ipc/contracts.players.test.ts app/shared/types/containMediaRect.test.ts app/shared/types/scenePreviewRotation.test.ts && node --test scripts/build-env.test.mjs scripts/obfuscate-main.test.mjs scripts/release-mac-prep.test.mjs scripts/release-native-prep.test.mjs scripts/verify-packaged-sharp.test.mjs app/main/project/sharpRuntime.test.mjs",
"format": "prettier . --check", "format": "prettier . --check",
"format:write": "prettier . --write", "format:write": "prettier . --write",
"postinstall": "patch-package", "postinstall": "patch-package",