From ea718a26df9f254a1446bcc06147e5e3beb0965c Mon Sep 17 00:00:00 2001 From: Ivan Fontosh Date: Wed, 2 Sep 2026 10:00:10 +0800 Subject: [PATCH] =?UTF-8?q?feat(control):=20=D0=B8=D1=81=D1=82=D0=BE=D1=80?= =?UTF-8?q?=D0=B8=D1=8F-=D0=B0=D0=BA=D0=BA=D0=BE=D1=80=D0=B4=D0=B5=D0=BE?= =?UTF-8?q?=D0=BD=20=D0=B8=20=D1=84=D0=BE=D1=80=D0=BC=D0=B0=20=D0=BA=D0=B8?= =?UTF-8?q?=D1=81=D1=82=D0=B8=20=D0=B7=D0=B0=D1=82=D0=B5=D0=BC=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Переименовать блок сюжетной линии в «История» и сделать его сворачиваемым. Добавить выбор круглой/квадратной формы для кистей открытия и закрытия с сохранением в штрихах и сбросом при смене инструмента или сцены без затемнения. Co-authored-by: Cursor --- app/renderer/control/ControlApp.module.css | 65 +++++++++- app/renderer/control/ControlApp.tsx | 122 +++++++++++++++--- .../control/controlApp.effectsPanel.test.ts | 28 +++- app/renderer/editor/i18n/editorMessages.ts | 10 +- .../shared/effects/SceneDarknessOverlay.tsx | 62 ++++++++- app/shared/types/sceneDarkness.test.ts | 20 +++ app/shared/types/sceneDarkness.ts | 9 ++ package.json | 2 +- 8 files changed, 288 insertions(+), 30 deletions(-) create mode 100644 app/shared/types/sceneDarkness.test.ts diff --git a/app/renderer/control/ControlApp.module.css b/app/renderer/control/ControlApp.module.css index 049b12a..f99d483 100644 --- a/app/renderer/control/ControlApp.module.css +++ b/app/renderer/control/ControlApp.module.css @@ -217,11 +217,42 @@ flex-direction: column; } -.storyScroll { - flex: 0 0 70%; +.storyWrapCollapsed { + flex: 0 0 auto; +} + +.storyWrapCollapsed .historyAccordion { + flex: 0 0 auto; +} + +.historyAccordion { + flex: 1; min-height: 0; - border-radius: var(--radius-lg); + display: flex; + flex-direction: column; border: 1px solid var(--stroke); + border-radius: var(--radius-lg); + overflow: hidden; + background: rgba(255, 255, 255, 0.02); +} + +.historyAccordionHead { + width: 100%; + text-align: left; + padding: 10px 12px; + font-size: var(--text-xs); + font-weight: 800; + color: var(--text2); + background: transparent; + border: 0; + cursor: pointer; + flex-shrink: 0; +} + +.storyScroll { + flex: 1; + min-height: 0; + border-top: 1px solid var(--stroke); background: var(--color-overlay-dark-2); overflow: auto; padding: 10px; @@ -394,6 +425,34 @@ pointer-events: none; } +.brushCursorSquare { + border-radius: 2px; +} + +.brushShapeRow { + display: grid; + grid-template-columns: 120px 1fr; + align-items: center; + gap: 8px; + margin-top: 4px; +} + +.brushShapeButtons { + display: flex; + gap: 8px; + flex-wrap: wrap; + align-items: center; +} + +.brushShapeIcon { + display: flex; + width: 20px; + height: 20px; + align-items: center; + justify-content: center; + color: var(--text1); +} + .brushLayer { position: absolute; inset: 0; diff --git a/app/renderer/control/ControlApp.tsx b/app/renderer/control/ControlApp.tsx index 4dab764..96f6533 100644 --- a/app/renderer/control/ControlApp.tsx +++ b/app/renderer/control/ControlApp.tsx @@ -16,6 +16,7 @@ import type { MaterialId, NpcDisposition, Scene, + SceneDarknessBrushShape, SceneId, SceneViewCamera, } from '../../shared/types'; @@ -185,6 +186,10 @@ export function ControlApp() { const [session, setSession] = useState(null); const historyRef = useRef([]); const [history, setHistory] = useState([]); + const [historyOpen, setHistoryOpen] = useState(true); + const [darknessBrushShape, setDarknessBrushShape] = useState('circle'); + const darknessBrushShapeRef = useRef(darknessBrushShape); + darknessBrushShapeRef.current = darknessBrushShape; /** Сцена основного сюжета, с которой ушли в побочную линию (только текущая сессия). */ const mainStoryReturnRef = useRef(null); const [mainStoryReturnGraphNodeId, setMainStoryReturnGraphNodeId] = useState(null); @@ -399,6 +404,12 @@ export function ControlApp() { ); const isDarkenScene = Boolean(currentScene?.darkenScene); + useEffect(() => { + if (!isDarkenScene) { + setDarknessBrushShape('circle'); + } + }, [isDarkenScene]); + const snapNormActive = useCallback( (nx: number, ny: number) => { const grid = currentScene?.grid; @@ -1105,9 +1116,25 @@ export function ControlApp() { /** Повторный клик по активному инструменту снимает выбор. */ function selectEffectTool(next: Exclude): void { const toolName: EffectToolType = tool.tool === next ? 'none' : next; + const leavingDarknessBrush = + (tool.tool === 'exploreBrush' || tool.tool === 'closeBrush') && + toolName !== 'exploreBrush' && + toolName !== 'closeBrush'; + if (leavingDarknessBrush) { + setDarknessBrushShape('circle'); + } void fx.dispatch({ kind: 'tool.set', tool: { ...tool, tool: toolName } }); } + function darknessBrushDraft(points: { x: number; y: number; tMs: number }[], mode: 'reveal' | 'cover') { + return { + points, + radiusN: toolRef.current.radiusN, + mode, + shape: darknessBrushShapeRef.current, + }; + } + function layoutBrushCursor(): void { const el = brushCursorElRef.current; const p = cursorPosRef.current; @@ -1120,6 +1147,13 @@ export function ControlApp() { return; } el.style.visibility = 'visible'; + const isDarknessBrush = t.tool === 'exploreBrush' || t.tool === 'closeBrush'; + el.className = [ + styles.brushCursor, + isDarknessBrush && darknessBrushShapeRef.current === 'square' ? styles.brushCursorSquare : '', + ] + .filter(Boolean) + .join(' '); const ox = cr ? cr.x : 0; const oy = cr ? cr.y : 0; const cw = cr ? cr.w : ps.w; @@ -1297,11 +1331,10 @@ export function ControlApp() { ) { void sd.dispatch({ kind: 'draft.set', - draft: { - points: b.points, - radiusN: toolRef.current.radiusN, - mode: b.tool === 'closeBrush' ? 'cover' : 'reveal', - }, + draft: darknessBrushDraft( + b.points, + b.tool === 'closeBrush' ? 'cover' : 'reveal', + ), }); } }); @@ -1309,7 +1342,7 @@ export function ControlApp() { useLayoutEffect(() => { layoutBrushCursor(); - }, [tool.radiusN, previewContentRect, previewSize.w, previewSize.h]); + }, [tool.radiusN, previewContentRect, previewSize.w, previewSize.h, darknessBrushShape]); useEffect(() => { return () => { @@ -1459,6 +1492,7 @@ export function ControlApp() { points: b.points, radiusN: tool.radiusN, mode: b.tool === 'closeBrush' ? 'cover' : 'reveal', + shape: darknessBrushShapeRef.current, }, }); } @@ -1891,6 +1925,55 @@ export function ControlApp() { />
{Math.round(tool.radiusN * 100)}
+ {tool.tool === 'exploreBrush' || tool.tool === 'closeBrush' ? ( +
+
{t('control.brushShape')}
+
+ + +
+
+ ) : null}
{t('control.effectsSound')}
-
-
{t('control.storyLine')}
-
-
+
+
+ + {historyOpen ? ( +
{history.map((gnId, idx) => { const gn = project?.sceneGraphNodes.find((n) => n.id === gnId); const s = gn ? project?.scenes[gn.sceneId] : undefined; @@ -1952,6 +2043,8 @@ export function ControlApp() { {history.length === 0 ? (
{t('control.noActiveScene')}
) : null} +
+ ) : null}
@@ -2175,11 +2268,10 @@ export function ControlApp() { if (tool.tool === 'exploreBrush' || tool.tool === 'closeBrush') { void sd.dispatch({ kind: 'draft.set', - draft: { - points: [startPt], - radiusN: tool.radiusN, - mode: tool.tool === 'closeBrush' ? 'cover' : 'reveal', - }, + draft: darknessBrushDraft( + [startPt], + tool.tool === 'closeBrush' ? 'cover' : 'reveal', + ), }); } pushDraftToPixi(); diff --git a/app/renderer/control/controlApp.effectsPanel.test.ts b/app/renderer/control/controlApp.effectsPanel.test.ts index d3e976b..f3676d9 100644 --- a/app/renderer/control/controlApp.effectsPanel.test.ts +++ b/app/renderer/control/controlApp.effectsPanel.test.ts @@ -113,7 +113,7 @@ void test('ControlApp: эффекты в пульте, иконки с тулт instruments !== -1 && fx !== -1 && instruments < fx, 'Блок инструментов должен быть выше эффектов', ); - assert.ok(fx !== -1 && story !== -1 && fx < story, 'Блок эффектов должен быть выше сюжетной линии'); + assert.ok(fx !== -1 && story !== -1 && fx < story, 'Блок эффектов должен быть выше истории'); }); void test('ControlApp: чекбокс привязки токенов к сетке', () => { @@ -128,11 +128,14 @@ void test('ControlApp: чекбокс привязки токенов к сет assert.ok(css.includes('.snapToGrid')); }); -void test('ControlApp: сюжетная линия — колонка сверху вниз и фон как у карточек ветвления', () => { +void test('ControlApp: история — колонка сверху вниз и фон как у карточек ветвления', () => { const src = readControlApp(); const css = readControlAppCss(); const story = src.indexOf("t('control.storyLine')"); assert.ok(story !== -1); + assert.ok(src.includes('data-testid="control-history-accordion"')); + assert.ok(src.includes('aria-expanded={historyOpen}')); + assert.ok(src.includes('const [historyOpen, setHistoryOpen] = useState(true)')); assert.ok(src.includes('className={styles.storyScroll}')); assert.match(css, /\.storyScroll[\s\S]*?justify-content:\s*flex-start/); assert.match(css, /\.storyScroll[\s\S]*?background:\s*var\(--color-overlay-dark-2\)/); @@ -173,6 +176,27 @@ void test('ControlApp: слой кисти не использует курсо assert.match(css, /\.brushLayer[\s\S]*?cursor:\s*crosshair/); }); +void test('ControlApp: форма кисти затемнения — UI, shape в штрихах и сброс', () => { + const src = readControlApp(); + const css = readControlAppCss(); + assert.ok(src.includes("t('control.brushShape')")); + assert.ok(src.includes("t('control.brushShapeCircle')")); + assert.ok(src.includes("t('control.brushShapeSquare')")); + assert.ok(src.includes('darknessBrushShape')); + assert.ok(src.includes('darknessBrushDraft')); + assert.ok(src.includes("shape: darknessBrushShapeRef.current")); + assert.ok(src.includes('setDarknessBrushShape')); + assert.ok(src.includes('styles.brushShapeRow')); + assert.ok(src.includes("tool.tool === 'exploreBrush' || tool.tool === 'closeBrush'")); + assert.ok(css.includes('.brushCursorSquare')); + assert.ok(css.includes('.brushShapeRow')); + const radius = src.indexOf("t('control.brushRadius')"); + const shape = src.indexOf("t('control.brushShape')"); + const sound = src.indexOf("t('control.effectsSound')"); + assert.ok(radius !== -1 && shape !== -1 && sound !== -1); + assert.ok(radius < shape && shape < sound, 'Форма кисти между радиусом и звуком эффектов'); +}); + void test('ControlApp: радиус кисти не в блоке предпросмотра', () => { const src = readControlApp(); const previewLabel = src.indexOf("t('control.screenPreview')"); diff --git a/app/renderer/editor/i18n/editorMessages.ts b/app/renderer/editor/i18n/editorMessages.ts index 12fe2d8..a6c8a58 100644 --- a/app/renderer/editor/i18n/editorMessages.ts +++ b/app/renderer/editor/i18n/editorMessages.ts @@ -630,8 +630,11 @@ export const EDITOR_MESSAGES: Record> = { 'control.poisonCloud': 'Облако яда', 'control.explosion': 'Взрыв', 'control.brushRadius': 'Радиус кисти', + 'control.brushShape': 'Форма кисти', + 'control.brushShapeCircle': 'Круглая', + 'control.brushShapeSquare': 'Квадратная', 'control.effectsSound': 'Звук эффектов', - 'control.storyLine': 'СЮЖЕТНАЯ ЛИНИЯ', + 'control.storyLine': 'ИСТОРИЯ', 'control.gotoScene': 'Перейти к этой сцене', 'control.currentSceneBadge': 'ТЕКУЩАЯ СЦЕНА', 'control.passed': 'Пройдено', @@ -1267,8 +1270,11 @@ export const EDITOR_MESSAGES: Record> = { 'control.poisonCloud': 'Poison cloud', 'control.explosion': 'Explosion', 'control.brushRadius': 'Brush radius', + 'control.brushShape': 'Brush shape', + 'control.brushShapeCircle': 'Circle', + 'control.brushShapeSquare': 'Square', 'control.effectsSound': 'Effects sound', - 'control.storyLine': 'STORYLINE', + 'control.storyLine': 'HISTORY', 'control.gotoScene': 'Go to this scene', 'control.currentSceneBadge': 'CURRENT SCENE', 'control.passed': 'Visited', diff --git a/app/renderer/shared/effects/SceneDarknessOverlay.tsx b/app/renderer/shared/effects/SceneDarknessOverlay.tsx index 170c47b..048cb1e 100644 --- a/app/renderer/shared/effects/SceneDarknessOverlay.tsx +++ b/app/renderer/shared/effects/SceneDarknessOverlay.tsx @@ -1,11 +1,15 @@ import React, { useEffect, useRef } from 'react'; import type { + SceneDarknessBrushShape, SceneDarknessRevealStroke, SceneDarknessState, SceneDarknessStrokeMode, } from '../../../shared/types'; -import { normalizeSceneDarknessStrokeMode } from '../../../shared/types/sceneDarkness'; +import { + normalizeSceneDarknessBrushShape, + normalizeSceneDarknessStrokeMode, +} from '../../../shared/types/sceneDarkness'; export type SceneDarknessOverlayProps = { state: SceneDarknessState | null; @@ -15,18 +19,44 @@ export type SceneDarknessOverlayProps = { style?: React.CSSProperties; }; -function drawDarknessStroke( +type DarknessStrokeLike = SceneDarknessRevealStroke | { + points: { x: number; y: number }[]; + radiusN: number; + mode?: SceneDarknessStrokeMode; + shape?: SceneDarknessBrushShape; +}; + +function stampSquare( ctx: CanvasRenderingContext2D, - stroke: SceneDarknessRevealStroke | { points: { x: number; y: number }[]; radiusN: number; mode?: SceneDarknessStrokeMode }, + x: number, + y: number, + side: number, +): void { + ctx.fillRect(x - side / 2, y - side / 2, side, side); +} + +function drawSquareDarknessStroke( + ctx: CanvasRenderingContext2D, + stroke: DarknessStrokeLike, + w: number, + h: number, +): void { + const pts = stroke.points; + if (pts.length === 0) return; + const side = stroke.radiusN * Math.min(w, h) * 2; + for (const p of pts) { + stampSquare(ctx, p.x * w, p.y * h, side); + } +} + +function drawCircleDarknessStroke( + ctx: CanvasRenderingContext2D, + stroke: DarknessStrokeLike, w: number, h: number, ): void { const pts = stroke.points; if (pts.length === 0) return; - const mode = normalizeSceneDarknessStrokeMode(stroke.mode); - ctx.globalCompositeOperation = mode === 'cover' ? 'source-over' : 'destination-out'; - ctx.fillStyle = '#000000'; - ctx.strokeStyle = '#000000'; const r = stroke.radiusN * Math.min(w, h); if (pts.length === 1) { const p = pts[0]; @@ -51,6 +81,24 @@ function drawDarknessStroke( ctx.stroke(); } +function drawDarknessStroke( + ctx: CanvasRenderingContext2D, + stroke: DarknessStrokeLike, + w: number, + h: number, +): void { + const mode = normalizeSceneDarknessStrokeMode(stroke.mode); + const shape = normalizeSceneDarknessBrushShape(stroke.shape); + ctx.globalCompositeOperation = mode === 'cover' ? 'source-over' : 'destination-out'; + ctx.fillStyle = '#000000'; + ctx.strokeStyle = '#000000'; + if (shape === 'square') { + drawSquareDarknessStroke(ctx, stroke, w, h); + return; + } + drawCircleDarknessStroke(ctx, stroke, w, h); +} + export function SceneDarknessOverlay({ state, viewport, diff --git a/app/shared/types/sceneDarkness.test.ts b/app/shared/types/sceneDarkness.test.ts new file mode 100644 index 0000000..79618b4 --- /dev/null +++ b/app/shared/types/sceneDarkness.test.ts @@ -0,0 +1,20 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { + normalizeSceneDarknessBrushShape, + normalizeSceneDarknessStrokeMode, +} from './sceneDarkness'; + +void test('normalizeSceneDarknessStrokeMode: reveal по умолчанию', () => { + assert.equal(normalizeSceneDarknessStrokeMode(undefined), 'reveal'); + assert.equal(normalizeSceneDarknessStrokeMode('reveal'), 'reveal'); + assert.equal(normalizeSceneDarknessStrokeMode('cover'), 'cover'); +}); + +void test('normalizeSceneDarknessBrushShape: circle по умолчанию', () => { + assert.equal(normalizeSceneDarknessBrushShape(undefined), 'circle'); + assert.equal(normalizeSceneDarknessBrushShape('circle'), 'circle'); + assert.equal(normalizeSceneDarknessBrushShape('square'), 'square'); + assert.equal(normalizeSceneDarknessBrushShape('triangle'), 'circle'); +}); diff --git a/app/shared/types/sceneDarkness.ts b/app/shared/types/sceneDarkness.ts index 927fe21..e8bb296 100644 --- a/app/shared/types/sceneDarkness.ts +++ b/app/shared/types/sceneDarkness.ts @@ -3,6 +3,8 @@ import type { NPoint } from './effects'; /** reveal — снимает тьму (Кисть Открытия); cover — возвращает тьму (Кисть Закрытия). */ export type SceneDarknessStrokeMode = 'reveal' | 'cover'; +export type SceneDarknessBrushShape = 'circle' | 'square'; + export type SceneDarknessRevealStroke = { id: string; seed: number; @@ -11,12 +13,15 @@ export type SceneDarknessRevealStroke = { radiusN: number; /** По умолчанию reveal (старые штрихи без поля). */ mode?: SceneDarknessStrokeMode; + /** По умолчанию circle (старые штрихи без поля). */ + shape?: SceneDarknessBrushShape; }; export type SceneDarknessDraft = { points: NPoint[]; radiusN: number; mode?: SceneDarknessStrokeMode; + shape?: SceneDarknessBrushShape; }; export type SceneDarknessState = { @@ -35,3 +40,7 @@ export type SceneDarknessEvent = export function normalizeSceneDarknessStrokeMode(raw: unknown): SceneDarknessStrokeMode { return raw === 'cover' ? 'cover' : 'reveal'; } + +export function normalizeSceneDarknessBrushShape(raw: unknown): SceneDarknessBrushShape { + return raw === 'square' ? 'square' : 'circle'; +} diff --git a/package.json b/package.json index 076ac36..cda2cc6 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/npcExternalUrl.test.ts app/main/npcs/npcsOverlayStore.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/main/tokens/sceneTokensSessionStore.test.ts app/shared/ipc/contracts.players.test.ts app/shared/types/containMediaRect.test.ts app/shared/types/scenePreviewRotation.test.ts app/shared/types/tokenPath.test.ts app/shared/types/tokenPathPlayback.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/npcExternalUrl.test.ts app/main/npcs/npcsOverlayStore.test.ts app/shared/types/sceneGrid.test.ts app/shared/types/sceneGridSnap.test.ts app/shared/types/sceneDarkness.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/main/tokens/sceneTokensSessionStore.test.ts app/shared/ipc/contracts.players.test.ts app/shared/types/containMediaRect.test.ts app/shared/types/scenePreviewRotation.test.ts app/shared/types/tokenPath.test.ts app/shared/types/tokenPathPlayback.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",