feat(control): аккордеон «Сцены» на пульте управления
Список сцен активной линии в порядке бокового списка редактора; переход с записью в историю. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -221,12 +221,28 @@
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.storyAccordions {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.storyAccordionExpanded {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.storyAccordionCollapsed {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.storyWrapCollapsed .historyAccordion {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.historyAccordion {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
isNodeInSideStoryline,
|
||||
listSideStoryStarts,
|
||||
} from '../../shared/graph/sceneGraphLineage';
|
||||
import { listActiveStorylineScenes } from '../../shared/graph/activeStorylineScenes';
|
||||
import type {
|
||||
GraphNodeId,
|
||||
MaterialId,
|
||||
@@ -192,6 +193,7 @@ export function ControlApp() {
|
||||
const historyRef = useRef<GraphNodeId[]>([]);
|
||||
const [history, setHistory] = useState<GraphNodeId[]>([]);
|
||||
const [historyOpen, setHistoryOpen] = useState(true);
|
||||
const [scenesOpen, setScenesOpen] = useState(false);
|
||||
const [darknessBrushShape, setDarknessBrushShape] = useState<SceneDarknessBrushShape>('circle');
|
||||
const darknessBrushShapeRef = useRef(darknessBrushShape);
|
||||
darknessBrushShapeRef.current = darknessBrushShape;
|
||||
@@ -413,6 +415,11 @@ export function ControlApp() {
|
||||
const project = session?.project ?? null;
|
||||
const currentGraphNodeId = project?.currentGraphNodeId ?? null;
|
||||
const currentHistoryIdx = currentGraphNodeId != null ? history.lastIndexOf(currentGraphNodeId) : -1;
|
||||
const historyGraphNodeSet = useMemo(() => new Set(history), [history]);
|
||||
const storylineScenes = useMemo(
|
||||
() => (project ? listActiveStorylineScenes(project) : []),
|
||||
[project],
|
||||
);
|
||||
const currentScene =
|
||||
project && session?.currentSceneId ? project.scenes[session.currentSceneId] : undefined;
|
||||
const visibleNpcTokens = useMemo(
|
||||
@@ -2165,8 +2172,17 @@ export function ControlApp() {
|
||||
</div>
|
||||
<div className={styles.spacer12} />
|
||||
</>
|
||||
<div className={[styles.storyWrap, historyOpen ? '' : styles.storyWrapCollapsed].filter(Boolean).join(' ')}>
|
||||
<div className={styles.historyAccordion} data-testid="control-history-accordion">
|
||||
<div className={[styles.storyWrap, historyOpen || scenesOpen ? '' : styles.storyWrapCollapsed].filter(Boolean).join(' ')}>
|
||||
<div className={styles.storyAccordions}>
|
||||
<div
|
||||
className={[
|
||||
styles.historyAccordion,
|
||||
historyOpen ? styles.storyAccordionExpanded : styles.storyAccordionCollapsed,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
data-testid="control-history-accordion"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.historyAccordionHead}
|
||||
@@ -2214,6 +2230,62 @@ export function ControlApp() {
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div
|
||||
className={[
|
||||
styles.historyAccordion,
|
||||
scenesOpen ? styles.storyAccordionExpanded : styles.storyAccordionCollapsed,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
data-testid="control-scenes-accordion"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.historyAccordionHead}
|
||||
aria-expanded={scenesOpen}
|
||||
onClick={() => setScenesOpen((v) => !v)}
|
||||
>
|
||||
{t('control.scenesAccordion')} {scenesOpen ? '▾' : '▸'}
|
||||
</button>
|
||||
{scenesOpen ? (
|
||||
<div className={styles.storyScroll}>
|
||||
{storylineScenes.map(({ sceneId, graphNodeId }) => {
|
||||
const s = project?.scenes[sceneId];
|
||||
const isCurrent = graphNodeId === currentGraphNodeId;
|
||||
const visited = historyGraphNodeSet.has(graphNodeId);
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={sceneId}
|
||||
disabled={!project || isCurrent}
|
||||
className={[styles.historyBtn, isCurrent ? styles.historyBtnCurrent : '']
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
title={project && !isCurrent ? t('control.gotoScene') : undefined}
|
||||
onClick={() => {
|
||||
if (!project || isCurrent) return;
|
||||
void api.invoke(ipcChannels.project.setCurrentGraphNode, { graphNodeId });
|
||||
}}
|
||||
>
|
||||
{isCurrent ? (
|
||||
<div className={styles.historyBadge}>{t('control.currentSceneBadge')}</div>
|
||||
) : visited ? (
|
||||
<div className={styles.historyMuted}>{t('control.passed')}</div>
|
||||
) : null}
|
||||
<EllipsisText
|
||||
text={s?.title ?? String(sceneId)}
|
||||
className={[styles.historyTitle, ellipsisStyles.root].join(' ')}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{storylineScenes.length === 0 ? (
|
||||
<div className={styles.emptyStory}>{t('control.noActiveScene')}</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Surface>
|
||||
|
||||
|
||||
@@ -151,6 +151,18 @@ void test('ControlApp: история — колонка сверху вниз
|
||||
assert.match(css, /\.branchCard[\s\S]*?background:\s*var\(--color-overlay-dark-2\)/);
|
||||
});
|
||||
|
||||
void test('ControlApp: блок «Сцены» — аккордеон под историей, свёрнут по умолчанию', () => {
|
||||
const src = readControlApp();
|
||||
assert.ok(src.includes('data-testid="control-scenes-accordion"'));
|
||||
assert.ok(src.includes("t('control.scenesAccordion')"));
|
||||
assert.ok(src.includes('const [scenesOpen, setScenesOpen] = useState(false)'));
|
||||
assert.ok(src.includes('listActiveStorylineScenes'));
|
||||
assert.ok(src.includes('storylineScenes.map'));
|
||||
const historyIdx = src.indexOf('data-testid="control-history-accordion"');
|
||||
const scenesIdx = src.indexOf('data-testid="control-scenes-accordion"');
|
||||
assert.ok(historyIdx !== -1 && scenesIdx !== -1 && historyIdx < scenesIdx);
|
||||
});
|
||||
|
||||
void test('ControlApp: клик по истории добавляет новый шаг, не подавляет запись', () => {
|
||||
const src = readControlApp();
|
||||
assert.ok(!src.includes('suppressNextHistoryPushRef'));
|
||||
|
||||
@@ -646,6 +646,7 @@ export const EDITOR_MESSAGES: Record<EditorLocale, Record<string, string>> = {
|
||||
'control.brushShapeSquare': 'Квадратная',
|
||||
'control.effectsSound': 'Звук эффектов',
|
||||
'control.storyLine': 'ИСТОРИЯ',
|
||||
'control.scenesAccordion': 'СЦЕНЫ',
|
||||
'control.gotoScene': 'Перейти к этой сцене',
|
||||
'control.currentSceneBadge': 'ТЕКУЩАЯ СЦЕНА',
|
||||
'control.passed': 'Пройдено',
|
||||
@@ -1297,6 +1298,7 @@ export const EDITOR_MESSAGES: Record<EditorLocale, Record<string, string>> = {
|
||||
'control.brushShapeSquare': 'Square',
|
||||
'control.effectsSound': 'Effects sound',
|
||||
'control.storyLine': 'HISTORY',
|
||||
'control.scenesAccordion': 'SCENES',
|
||||
'control.gotoScene': 'Go to this scene',
|
||||
'control.currentSceneBadge': 'CURRENT SCENE',
|
||||
'control.passed': 'Visited',
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
|
||||
import type { Project, SceneGraphEdge, SceneGraphNode } from '../types';
|
||||
import { asGraphNodeId, asSceneId } from '../types/ids';
|
||||
|
||||
import { getActiveStorylineGraphNodeIds, listActiveStorylineScenes } from './activeStorylineScenes';
|
||||
|
||||
function node(id: string, sceneId: string, extra: Partial<SceneGraphNode> = {}): SceneGraphNode {
|
||||
return {
|
||||
id: asGraphNodeId(id),
|
||||
sceneId: asSceneId(sceneId),
|
||||
x: 0,
|
||||
y: 0,
|
||||
isStartScene: false,
|
||||
isSideStoryStart: false,
|
||||
sideStoryLineTitle: '',
|
||||
...extra,
|
||||
};
|
||||
}
|
||||
|
||||
function edge(source: string, target: string): SceneGraphEdge {
|
||||
return { sourceGraphNodeId: asGraphNodeId(source), targetGraphNodeId: asGraphNodeId(target) };
|
||||
}
|
||||
|
||||
function minimalProject(partial: Partial<Project> & Pick<Project, 'sceneGraphNodes' | 'sceneGraphEdges'>): Project {
|
||||
return {
|
||||
id: partial.id ?? ('p1' as Project['id']),
|
||||
title: partial.title ?? 'Test',
|
||||
scenes: partial.scenes ?? {},
|
||||
sceneListOrder: partial.sceneListOrder ?? [],
|
||||
sceneGraphNodes: partial.sceneGraphNodes,
|
||||
sceneGraphEdges: partial.sceneGraphEdges,
|
||||
currentGraphNodeId: partial.currentGraphNodeId ?? null,
|
||||
currentSceneId: partial.currentSceneId ?? null,
|
||||
materials: partial.materials ?? [],
|
||||
campaignAudio: partial.campaignAudio ?? [],
|
||||
npcs: partial.npcs ?? [],
|
||||
npcGroups: partial.npcGroups ?? [],
|
||||
npcRelations: partial.npcRelations ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
void test('listActiveStorylineScenes: main line ordered by sceneListOrder, deduped', () => {
|
||||
const s1 = asSceneId('s1');
|
||||
const s2 = asSceneId('s2');
|
||||
const s3 = asSceneId('s3');
|
||||
const nodes = [
|
||||
node('main', 's1', { isStartScene: true }),
|
||||
node('a', 's2'),
|
||||
node('b', 's3'),
|
||||
node('dup', 's2'),
|
||||
];
|
||||
const edges = [edge('main', 'a'), edge('a', 'b'), edge('a', 'dup')];
|
||||
const project = minimalProject({
|
||||
scenes: {
|
||||
[s1]: { id: s1, title: 'Start' },
|
||||
[s2]: { id: s2, title: 'Mid' },
|
||||
[s3]: { id: s3, title: 'End' },
|
||||
},
|
||||
sceneListOrder: [s3, s1, s2],
|
||||
sceneGraphNodes: nodes,
|
||||
sceneGraphEdges: edges,
|
||||
currentGraphNodeId: asGraphNodeId('a'),
|
||||
});
|
||||
|
||||
const list = listActiveStorylineScenes(project);
|
||||
assert.deepEqual(
|
||||
list.map((e) => e.sceneId),
|
||||
[s3, s1, s2],
|
||||
);
|
||||
assert.equal(list[2]?.graphNodeId, asGraphNodeId('a'));
|
||||
});
|
||||
|
||||
void test('listActiveStorylineScenes: side line only', () => {
|
||||
const s1 = asSceneId('s1');
|
||||
const s2 = asSceneId('s2');
|
||||
const sSide = asSceneId('sSide');
|
||||
const nodes = [
|
||||
node('main', 's1', { isStartScene: true }),
|
||||
node('side', 'sSide', { isSideStoryStart: true }),
|
||||
node('x', 's2'),
|
||||
];
|
||||
const edges = [edge('side', 'x')];
|
||||
const project = minimalProject({
|
||||
scenes: {
|
||||
[s1]: { id: s1, title: 'Main' },
|
||||
[s2]: { id: s2, title: 'Side step' },
|
||||
[sSide]: { id: sSide, title: 'Side start' },
|
||||
},
|
||||
sceneListOrder: [s1, s2, sSide],
|
||||
sceneGraphNodes: nodes,
|
||||
sceneGraphEdges: edges,
|
||||
currentGraphNodeId: asGraphNodeId('x'),
|
||||
});
|
||||
|
||||
const ids = getActiveStorylineGraphNodeIds(nodes, edges, asGraphNodeId('x'));
|
||||
assert.deepEqual([...ids].sort(), [asGraphNodeId('side'), asGraphNodeId('x')].sort());
|
||||
|
||||
const list = listActiveStorylineScenes(project);
|
||||
assert.deepEqual(
|
||||
list.map((e) => e.sceneId),
|
||||
[s2, sSide],
|
||||
);
|
||||
});
|
||||
|
||||
void test('listActiveStorylineScenes: current node preferred for duplicate sceneId', () => {
|
||||
const s2 = asSceneId('s2');
|
||||
const nodes = [
|
||||
node('main', 's1', { isStartScene: true }),
|
||||
node('a', 's2'),
|
||||
node('dup', 's2'),
|
||||
];
|
||||
const edges = [edge('main', 'a'), edge('main', 'dup')];
|
||||
const project = minimalProject({
|
||||
scenes: { [asSceneId('s1')]: { id: asSceneId('s1'), title: 'S1' }, [s2]: { id: s2, title: 'S2' } },
|
||||
sceneListOrder: [asSceneId('s1'), s2],
|
||||
sceneGraphNodes: nodes,
|
||||
sceneGraphEdges: edges,
|
||||
currentGraphNodeId: asGraphNodeId('dup'),
|
||||
});
|
||||
|
||||
const list = listActiveStorylineScenes(project);
|
||||
assert.equal(list.find((e) => e.sceneId === s2)?.graphNodeId, asGraphNodeId('dup'));
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
import type { GraphNodeId, Project, SceneGraphEdge, SceneGraphNode, SceneId } from '../types';
|
||||
|
||||
import { reconcileSceneListOrder } from './sceneListOrder';
|
||||
import {
|
||||
buildStoryRootMap,
|
||||
getNodeStoryRoot,
|
||||
getSideStoryComponentNodeIds,
|
||||
isSideStoryRoot,
|
||||
} from './sceneGraphLineage';
|
||||
|
||||
export type ActiveStorylineSceneEntry = {
|
||||
sceneId: SceneId;
|
||||
graphNodeId: GraphNodeId;
|
||||
};
|
||||
|
||||
/** Узлы графа, принадлежащие активной сюжетной линии текущего положения. */
|
||||
export function getActiveStorylineGraphNodeIds(
|
||||
nodes: SceneGraphNode[],
|
||||
edges: SceneGraphEdge[],
|
||||
currentGraphNodeId: GraphNodeId,
|
||||
): Set<GraphNodeId> {
|
||||
const root = getNodeStoryRoot(nodes, edges, currentGraphNodeId);
|
||||
if (root === 'main') {
|
||||
const rootMap = buildStoryRootMap(nodes, edges);
|
||||
const ids = new Set<GraphNodeId>();
|
||||
for (const [id, r] of rootMap) {
|
||||
if (r === 'main') ids.add(id);
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
if (isSideStoryRoot(root)) {
|
||||
return getSideStoryComponentNodeIds(nodes, edges, root);
|
||||
}
|
||||
return new Set();
|
||||
}
|
||||
|
||||
function pickGraphNodeForScene(
|
||||
sceneId: SceneId,
|
||||
storylineNodeIds: Set<GraphNodeId>,
|
||||
nodes: SceneGraphNode[],
|
||||
currentGraphNodeId: GraphNodeId | null,
|
||||
): GraphNodeId | null {
|
||||
const matching = nodes.filter((n) => storylineNodeIds.has(n.id) && n.sceneId === sceneId);
|
||||
if (matching.length === 0) return null;
|
||||
if (currentGraphNodeId && matching.some((n) => n.id === currentGraphNodeId)) {
|
||||
return currentGraphNodeId;
|
||||
}
|
||||
return matching[0]?.id ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Сцены активной линии в порядке бокового списка редактора; каждая sceneId — один раз.
|
||||
*/
|
||||
export function listActiveStorylineScenes(project: Project): ActiveStorylineSceneEntry[] {
|
||||
const currentGraphNodeId = project.currentGraphNodeId;
|
||||
if (!currentGraphNodeId) return [];
|
||||
|
||||
const storylineNodeIds = getActiveStorylineGraphNodeIds(
|
||||
project.sceneGraphNodes,
|
||||
project.sceneGraphEdges,
|
||||
currentGraphNodeId,
|
||||
);
|
||||
if (storylineNodeIds.size === 0) return [];
|
||||
|
||||
const order = reconcileSceneListOrder(project.scenes, project.sceneListOrder);
|
||||
const out: ActiveStorylineSceneEntry[] = [];
|
||||
for (const sceneId of order) {
|
||||
const graphNodeId = pickGraphNodeForScene(
|
||||
sceneId,
|
||||
storylineNodeIds,
|
||||
project.sceneGraphNodes,
|
||||
currentGraphNodeId,
|
||||
);
|
||||
if (graphNodeId) out.push({ sceneId, graphNodeId });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
+1
-1
@@ -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/preferences/appPreferences.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/effects/sceneMarkerStore.test.ts app/shared/sceneMarkerEraser.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/shared/types/materialLegend.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/shared/graph/activeStorylineScenes.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/preferences/appPreferences.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/effects/sceneMarkerStore.test.ts app/shared/sceneMarkerEraser.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/shared/types/materialLegend.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",
|
||||
|
||||
Reference in New Issue
Block a user