feat(control): аккордеон «Сцены» на пульте управления
Список сцен активной линии в порядке бокового списка редактора; переход с записью в историю. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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'));
|
||||
});
|
||||
Reference in New Issue
Block a user