diff --git a/app/main/index.ts b/app/main/index.ts index 05856a7..e8b42b6 100644 --- a/app/main/index.ts +++ b/app/main/index.ts @@ -20,6 +20,10 @@ import { } from '../shared/project/projectZipExtension'; import type { Project } from '../shared/types'; import { asNpcId } from '../shared/types/ids'; +import { + asPreviewRotationDeg, + previewRotationStepsCw, +} from '../shared/types/scenePreviewRotation'; import { EffectsStore, effectsDefaultTool } from './effects/effectsStore'; import { SceneDarknessStore } from './effects/sceneDarknessStore'; @@ -678,6 +682,7 @@ async function main() { }; }); registerHandler(ipcChannels.project.updateScene, async ({ sceneId, patch }) => { + const before = projectStore.getOpenProject()?.scenes[sceneId]; const next = await projectStore.updateScene(sceneId, patch); const project = projectStore.getOpenProject(); if (project?.currentSceneId === sceneId && patch.darkenScene !== undefined) { @@ -688,6 +693,29 @@ async function main() { syncSceneTrapsForProject(project); 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(); return { scene: next }; }); diff --git a/app/main/players/sceneNpcTokensSessionStore.ts b/app/main/players/sceneNpcTokensSessionStore.ts index 935896b..4ea686e 100644 --- a/app/main/players/sceneNpcTokensSessionStore.ts +++ b/app/main/players/sceneNpcTokensSessionStore.ts @@ -6,6 +6,7 @@ import { type SceneNpcTokensSessionState, } from '../../shared/types/appPlayers'; import { normalizeNpcDisposition } from '../../shared/types/npcDisposition'; +import { rotateMapNormPointByCwSteps } from '../../shared/types/scenePreviewRotation'; function emptyState(revision = 1): SceneNpcTokensSessionState { return { @@ -63,6 +64,34 @@ export class SceneNpcTokensSessionStore { 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 { switch (event.kind) { case 'clear': diff --git a/app/main/players/scenePlayerTokensSessionStore.test.ts b/app/main/players/scenePlayerTokensSessionStore.test.ts index 45924c3..416e1da 100644 --- a/app/main/players/scenePlayerTokensSessionStore.test.ts +++ b/app/main/players/scenePlayerTokensSessionStore.test.ts @@ -37,3 +37,13 @@ void test('ScenePlayerTokensSessionStore move and reset', () => { assert.equal(s2.visible, false); 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); +}); diff --git a/app/main/players/scenePlayerTokensSessionStore.ts b/app/main/players/scenePlayerTokensSessionStore.ts index a23b98a..b94beda 100644 --- a/app/main/players/scenePlayerTokensSessionStore.ts +++ b/app/main/players/scenePlayerTokensSessionStore.ts @@ -5,6 +5,7 @@ import { type ScenePlayerTokensSessionEvent, type ScenePlayerTokensSessionState, } from '../../shared/types/appPlayers'; +import { rotateMapNormPointByCwSteps } from '../../shared/types/scenePreviewRotation'; function emptyState(revision = 1): ScenePlayerTokensSessionState { return { @@ -60,6 +61,27 @@ export class ScenePlayerTokensSessionStore { 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 { switch (event.kind) { case 'clear': diff --git a/app/main/project/zipStore.ts b/app/main/project/zipStore.ts index e52c8e6..de858b6 100644 --- a/app/main/project/zipStore.ts +++ b/app/main/project/zipStore.ts @@ -67,6 +67,12 @@ import { type NpcDisposition, } from '../../shared/types/npcDisposition'; 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 type { AssetId, @@ -787,6 +793,24 @@ export class ZipProjectStore { ...(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) => { const scenes = { ...p.scenes, [sceneId]: next }; const sceneListOrder = diff --git a/app/main/tokens/sceneTokensSessionStore.ts b/app/main/tokens/sceneTokensSessionStore.ts index 50c897c..fd866bc 100644 --- a/app/main/tokens/sceneTokensSessionStore.ts +++ b/app/main/tokens/sceneTokensSessionStore.ts @@ -1,4 +1,5 @@ import type { SceneTokensSessionEvent, SceneTokensSessionState } from '../../shared/types'; +import { rotateMapNormPointByCwSteps } from '../../shared/types/scenePreviewRotation'; function emptyState(): SceneTokensSessionState { return { @@ -23,6 +24,26 @@ export class SceneTokensSessionStore { 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 { switch (event.kind) { case 'clear': diff --git a/app/renderer/editor/state/projectState.ts b/app/renderer/editor/state/projectState.ts index 502ab9e..26267bf 100644 --- a/app/renderer/editor/state/projectState.ts +++ b/app/renderer/editor/state/projectState.ts @@ -20,6 +20,7 @@ import type { } from '../../../shared/types'; import { getDndApi } from '../../shared/dndApi'; import { invalidateAssetUrlCache } from '../../shared/useAssetImageUrl'; +import { applyPreviewRotationToSceneMarkers } from '../../../shared/types/scenePreviewRotation'; 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), 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 project: Project = { ...p, scenes }; return { ...s, project }; diff --git a/app/shared/types/index.ts b/app/shared/types/index.ts index a7a7195..9d6c7e8 100644 --- a/app/shared/types/index.ts +++ b/app/shared/types/index.ts @@ -10,6 +10,7 @@ export * from './npcs'; export * from './sceneDarkness'; export * from './sceneGrid'; export * from './sceneGridSnap'; +export * from './scenePreviewRotation'; export * from './sceneTraps'; export * from './sceneView'; export * from './videoPlayback'; diff --git a/app/shared/types/scenePreviewRotation.test.ts b/app/shared/types/scenePreviewRotation.test.ts new file mode 100644 index 0000000..fca8890 --- /dev/null +++ b/app/shared/types/scenePreviewRotation.test.ts @@ -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 }); +}); diff --git a/app/shared/types/scenePreviewRotation.ts b/app/shared/types/scenePreviewRotation.ts new file mode 100644 index 0000000..de00e0c --- /dev/null +++ b/app/shared/types/scenePreviewRotation.ts @@ -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( + 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( + 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 { + 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']), + }; +} diff --git a/package.json b/package.json index bf9f3d7..c1bae32 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "build:obfuscate": "node scripts/build.mjs --production --obfuscate", "lint": "eslint . --max-warnings 0", "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:write": "prettier . --write", "postinstall": "patch-package",