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:
Ivan Fontosh
2026-07-30 13:38:35 +08:00
parent a3a03eb9e3
commit 101f595bac
57 changed files with 4176 additions and 317 deletions
@@ -70,3 +70,52 @@ void test('WindowErrorBoundary component exists and catches errors', () => {
assert.ok(src.includes('componentDidCatch'));
assert.ok(src.includes('role="alert"'));
});
void test('EditorApp: Players header after File, opens modal', () => {
const src = fs.readFileSync(path.join(rendererRoot, 'editor/EditorApp.tsx'), 'utf8');
const fileIdx = src.indexOf("t('top.file')");
const playersIdx = src.indexOf('data-testid="players-header-btn"');
assert.ok(fileIdx > 0, 'File menu present');
assert.ok(playersIdx > fileIdx, 'Players button must follow File in source order');
assert.ok(src.includes('PlayersManagerModal'));
assert.ok(src.includes('setPlayersManagerOpen(true)'));
});
void test('Players modal: flat teams only, progress overlay, PlayerTokenView', () => {
const src = fs.readFileSync(path.join(rendererRoot, 'editor/PlayersModals.tsx'), 'utf8');
assert.ok(src.includes('PlayerTokenView'));
assert.ok(src.includes('data-testid="players-modal"'));
assert.ok(src.includes('data-testid="players-save-progress"'));
assert.doesNotMatch(src, /parentId|подкоманд|subteam|sub-team/i);
assert.ok(src.includes("application/x-dnd-player-id"));
});
void test('Scene NPC accordion: separate mime and PlayerTokenView markers', () => {
const scene = fs.readFileSync(path.join(rendererRoot, 'sceneEditor/SceneEditorApp.tsx'), 'utf8');
const tokenTile = fs.readFileSync(path.join(rendererRoot, 'sceneEditor/TokenTile.tsx'), 'utf8');
assert.ok(scene.includes('data-testid="scene-npc-accordion"'));
assert.ok(scene.includes("application/x-dnd-scene-npc-id"));
assert.ok(tokenTile.includes("application/x-dnd-app-token-id"));
assert.notEqual(
'application/x-dnd-scene-npc-id',
'application/x-dnd-app-token-id',
'NPC scene mime must differ from app token mime',
);
assert.ok(scene.includes('PlayerTokenView'));
assert.ok(scene.includes('persistNpcTokens([])'));
assert.doesNotMatch(scene, /SceneTokenMarker[\s\S]{0,80}npcTokens/);
});
void test('Control and Presentation use SceneNpcTokensOverlay', () => {
const control = fs.readFileSync(path.join(rendererRoot, 'control/ControlApp.tsx'), 'utf8');
const presentation = fs.readFileSync(path.join(rendererRoot, 'shared/PresentationView.tsx'), 'utf8');
assert.ok(control.includes('SceneNpcTokensOverlay'));
assert.ok(presentation.includes('SceneNpcTokensOverlay'));
assert.ok(control.includes('sceneNpcTokensSession'));
});
void test('NpcsEditorApp: token appearance via PlayerTokenView', () => {
const src = fs.readFileSync(path.join(rendererRoot, 'npcs/NpcsEditorApp.tsx'), 'utf8');
assert.ok(src.includes('PlayerTokenView'));
assert.ok(src.includes('ringColor') || src.includes('updateNpcFields'));
});