feat(players): app Players library and circular NPC tokens on scenes
Add userData players/teams, scene npcTokens with hex-inscribed sizing, session scale synced to presentation, and Playwright e2e coverage. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
asNpcRelationId,
|
||||
asProjectId,
|
||||
asSceneId,
|
||||
asSceneNpcTokenId,
|
||||
} from '../types/ids';
|
||||
import { PROJECT_SCHEMA_VERSION } from '../types';
|
||||
|
||||
@@ -40,6 +41,7 @@ function scene(id: string, title: string): Scene {
|
||||
darkenScene: false,
|
||||
traps: [],
|
||||
tokens: [],
|
||||
npcTokens: [],
|
||||
grid: { enabled: false, type: 'square', sizeN: 0.06, color: '#ffffff' },
|
||||
media: { videos: [], audios: [] },
|
||||
settings: { autoplayVideo: false, autoplayAudio: false, loopVideo: false, loopAudio: false },
|
||||
@@ -242,6 +244,9 @@ void test('selectNpcsByIds / buildPartialExportProject: explicit NPCs, relations
|
||||
x: 0,
|
||||
y: 0,
|
||||
groupId: gChild,
|
||||
ringColor: '#c9a227',
|
||||
imageOffset: { x: 0, y: 0 },
|
||||
imageScale: 1,
|
||||
},
|
||||
{
|
||||
id: n2,
|
||||
@@ -251,6 +256,9 @@ void test('selectNpcsByIds / buildPartialExportProject: explicit NPCs, relations
|
||||
x: 10,
|
||||
y: 0,
|
||||
groupId: gChild,
|
||||
ringColor: '#c9a227',
|
||||
imageOffset: { x: 0, y: 0 },
|
||||
imageScale: 1,
|
||||
},
|
||||
{
|
||||
id: n3,
|
||||
@@ -260,6 +268,9 @@ void test('selectNpcsByIds / buildPartialExportProject: explicit NPCs, relations
|
||||
x: 20,
|
||||
y: 0,
|
||||
groupId: asNpcGroupId('g_other'),
|
||||
ringColor: '#c9a227',
|
||||
imageOffset: { x: 0, y: 0 },
|
||||
imageScale: 1,
|
||||
},
|
||||
];
|
||||
const relations: ProjectNpcRelation[] = [
|
||||
@@ -308,6 +319,139 @@ void test('selectNpcsByIds / buildPartialExportProject: explicit NPCs, relations
|
||||
assert.ok(!partial.npcGroups.some((g) => g.id === asNpcGroupId('g_other')));
|
||||
});
|
||||
|
||||
void test('buildPartialExportProject: keeps npcTokens only for exported NPCs', () => {
|
||||
const n1 = asNpcId('npc1');
|
||||
const n2 = asNpcId('npc2');
|
||||
const avatar = asAssetId('a1');
|
||||
const sceneId = asSceneId('s1');
|
||||
const source = minimalProject({
|
||||
scenes: {
|
||||
[sceneId]: {
|
||||
...scene('s1', 'Start'),
|
||||
npcTokens: [
|
||||
{ id: asSceneNpcTokenId('nt1'), npcId: n1, nx: 0.2, ny: 0.3, sizeN: 0.1 },
|
||||
{ id: asSceneNpcTokenId('nt2'), npcId: n2, nx: 0.5, ny: 0.5, sizeN: 0.1 },
|
||||
{ id: asSceneNpcTokenId('nt3'), npcId: asNpcId('missing'), nx: 0.1, ny: 0.1, sizeN: 0.1 },
|
||||
],
|
||||
},
|
||||
},
|
||||
sceneGraphNodes: [node('main', 's1', { isStartScene: true })],
|
||||
assets: {
|
||||
[avatar]: {
|
||||
id: avatar,
|
||||
type: 'image',
|
||||
mime: 'image/png',
|
||||
originalName: 'a.png',
|
||||
relPath: 'assets/a.png',
|
||||
sha256: 'abc',
|
||||
sizeBytes: 1,
|
||||
createdAt: '2020-01-01T00:00:00.000Z',
|
||||
},
|
||||
},
|
||||
npcs: [
|
||||
{
|
||||
id: n1,
|
||||
name: 'A',
|
||||
avatarAssetId: avatar,
|
||||
description: '',
|
||||
x: 0,
|
||||
y: 0,
|
||||
groupId: null,
|
||||
ringColor: '#c9a227',
|
||||
imageOffset: { x: 0, y: 0 },
|
||||
imageScale: 1,
|
||||
},
|
||||
{
|
||||
id: n2,
|
||||
name: 'B',
|
||||
avatarAssetId: avatar,
|
||||
description: '',
|
||||
x: 10,
|
||||
y: 0,
|
||||
groupId: null,
|
||||
ringColor: '#c9a227',
|
||||
imageOffset: { x: 0, y: 0 },
|
||||
imageScale: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const partial = buildPartialExportProject(source, [{ kind: 'main' }], {
|
||||
newProjectId: newExportBundleProjectId(),
|
||||
exportTitle: 'Export',
|
||||
labels: LABELS,
|
||||
npcIds: [n1],
|
||||
});
|
||||
const tokens = partial.scenes[sceneId]?.npcTokens ?? [];
|
||||
assert.equal(tokens.length, 1);
|
||||
assert.equal(tokens[0]!.npcId, n1);
|
||||
});
|
||||
|
||||
void test('mergeStorylinesIntoProject: remaps npcTokens to created NPC ids', () => {
|
||||
const avatar = asAssetId('a1');
|
||||
const sourceNpcId = asNpcId('src_npc');
|
||||
const source = minimalProject({
|
||||
scenes: {
|
||||
[asSceneId('s1')]: {
|
||||
...scene('s1', 'Side'),
|
||||
npcTokens: [
|
||||
{ id: asSceneNpcTokenId('nt1'), npcId: sourceNpcId, nx: 0.4, ny: 0.6, sizeN: 0.12 },
|
||||
],
|
||||
},
|
||||
},
|
||||
sceneGraphNodes: [node('side', 's1', { isSideStoryStart: true, sideStoryLineTitle: 'Side', x: 0 })],
|
||||
assets: {
|
||||
[avatar]: {
|
||||
id: avatar,
|
||||
type: 'image',
|
||||
mime: 'image/png',
|
||||
originalName: 'a.png',
|
||||
relPath: 'assets/a.png',
|
||||
sha256: 'npcsha',
|
||||
sizeBytes: 1,
|
||||
createdAt: '2020-01-01T00:00:00.000Z',
|
||||
},
|
||||
},
|
||||
npcs: [
|
||||
{
|
||||
id: sourceNpcId,
|
||||
name: 'Hero',
|
||||
avatarAssetId: avatar,
|
||||
description: '',
|
||||
x: 0,
|
||||
y: 0,
|
||||
groupId: null,
|
||||
ringColor: '#aabbcc',
|
||||
imageOffset: { x: 0.1, y: -0.1 },
|
||||
imageScale: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
const target = minimalProject({
|
||||
scenes: { [asSceneId('t1')]: scene('t1', 'Existing') },
|
||||
sceneGraphNodes: [node('tgn', 't1', { x: 0 })],
|
||||
});
|
||||
|
||||
const { project: merged } = mergeStorylinesIntoProject(
|
||||
target,
|
||||
source,
|
||||
[{ kind: 'side', startGraphNodeId: asGraphNodeId('side') }],
|
||||
[{ sourceSceneId: asSceneId('s1'), mode: 'create' }],
|
||||
{
|
||||
graphOffsetX: 200,
|
||||
npcResolutions: [{ sourceNpcId, mode: 'create' }],
|
||||
},
|
||||
);
|
||||
|
||||
assert.equal(merged.npcs.length, 1);
|
||||
const importedScene = Object.values(merged.scenes).find((s) => s.title === 'Side');
|
||||
assert.ok(importedScene);
|
||||
assert.equal(importedScene!.npcTokens.length, 1);
|
||||
assert.equal(importedScene!.npcTokens[0]!.npcId, merged.npcs[0]!.id);
|
||||
assert.notEqual(importedScene!.npcTokens[0]!.npcId, sourceNpcId);
|
||||
assert.equal(merged.npcs[0]!.ringColor, '#aabbcc');
|
||||
});
|
||||
|
||||
void test('mergeStorylinesIntoProject: imports all NPCs from export bundle', () => {
|
||||
const avatar = asAssetId('a1');
|
||||
const sourceNpcId = asNpcId('src_npc');
|
||||
@@ -335,7 +479,10 @@ void test('mergeStorylinesIntoProject: imports all NPCs from export bundle', ()
|
||||
x: 0,
|
||||
y: 0,
|
||||
groupId: null,
|
||||
},
|
||||
ringColor: '#c9a227',
|
||||
imageOffset: { x: 0, y: 0 },
|
||||
imageScale: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
const target = minimalProject({
|
||||
|
||||
Reference in New Issue
Block a user