import assert from 'node:assert/strict'; import { test } from 'node:test'; import { extractMonksTeleportEdges, filterScenesForImport, pickFoundryStartSceneId, planFoundrySceneGraph, sceneBackgroundSrc, } from './foundryGraph'; import { decodeFoundryAssetPath } from './foundryPaths'; import type { FoundrySceneDoc } from './foundryTypes'; import { isSupportedFoundryPackage } from './foundryVersion'; function scene(id: string, name: string, extra: Partial = {}): FoundrySceneDoc { return { _id: id, name, navigation: true, ...extra }; } function teleportTile(targetId: string) { return { flags: { 'monks-active-tiles': { active: true, actions: [{ action: 'scene', data: { sceneid: { id: `Scene.${targetId}`, name: 'x' } } }], }, }, }; } void test('filterScenesForImport keeps only navigation when present', () => { const scenes = [ scene('aaaaaaaaaaaaaaaa', 'Nav', { navigation: true }), scene('bbbbbbbbbbbbbbbb', 'Hidden', { navigation: false }), ]; assert.equal(filterScenesForImport(scenes).length, 1); assert.equal(filterScenesForImport(scenes)[0]?._id, 'aaaaaaaaaaaaaaaa'); }); void test('pickFoundryStartSceneId: min sort, then video, not name', () => { const scenes = [ scene('aaaaaaaaaaaaaaaa', 'Тент', { sort: 0, tiles: [{}] }), scene('bbbbbbbbbbbbbbbb', 'Стартовая', { sort: 0, background: { src: 'modules/x/Map/Swamp_start.mp4' }, tiles: [{}], }), scene('cccccccccccccccc', 'Болота', { sort: 100000 }), ]; assert.equal(pickFoundryStartSceneId(scenes), 'bbbbbbbbbbbbbbbb'); }); void test('planFoundrySceneGraph: monks teleports create branches, not a line', () => { const hub = 'aaaaaaaaaaaaaaaa'; const a = 'bbbbbbbbbbbbbbbb'; const b = 'cccccccccccccccc'; const start = 'dddddddddddddddd'; const scenes = [ scene(hub, 'Hub', { sort: 100000, tiles: [teleportTile(a), teleportTile(b)], }), scene(a, 'A', { sort: 200000, tiles: [teleportTile(hub)] }), scene(b, 'B', { sort: 300000, tiles: [teleportTile(hub)] }), scene(start, 'Intro', { sort: 0, background: { src: 'intro.mp4' }, tiles: [teleportTile(hub)], }), ]; const plan = planFoundrySceneGraph(scenes, [], []); assert.equal(plan.heuristic.kind, 'monks-teleports'); assert.equal(plan.startSceneId, start); assert.ok(plan.edges.some((e) => e.sourceId === hub && e.targetId === a)); assert.ok(plan.edges.some((e) => e.sourceId === hub && e.targetId === b)); assert.ok(plan.edges.some((e) => e.sourceId === start && e.targetId === hub)); // Не линейная цепочка из 3 рёбер подряд по всем сценам. assert.ok(plan.edges.length >= 3); }); void test('extractMonksTeleportEdges ignores inactive tiles', () => { const scenes = [ scene('aaaaaaaaaaaaaaaa', 'A', { tiles: [ { flags: { 'monks-active-tiles': { active: false, actions: [{ action: 'scene', data: { sceneid: 'Scene.bbbbbbbbbbbbbbbb' } }], }, }, }, ], }), scene('bbbbbbbbbbbbbbbb', 'B'), ]; assert.equal(extractMonksTeleportEdges(scenes).length, 0); }); void test('planFoundrySceneGraph: sort-order fallback when no teleports', () => { const scenes = [ scene('aaaaaaaaaaaaaaaa', 'Z', { sort: 3 }), scene('bbbbbbbbbbbbbbbb', 'A', { sort: 1 }), scene('cccccccccccccccc', 'M', { sort: 2 }), ]; const plan = planFoundrySceneGraph(scenes, [], []); assert.equal(plan.heuristic.kind, 'navigation'); assert.equal(plan.startSceneId, 'bbbbbbbbbbbbbbbb'); assert.deepEqual(plan.orderedSceneIds[0], 'bbbbbbbbbbbbbbbb'); }); void test('sceneBackgroundSrc prefers background.src', () => { assert.equal( sceneBackgroundSrc({ _id: 'aaaaaaaaaaaaaaaa', name: 'S', img: 'old.png', background: { src: 'new.webp' }, }), 'new.webp', ); }); void test('isSupportedFoundryPackage rejects pre-v11 maximum', () => { assert.equal( isSupportedFoundryPackage({ compatibility: { maximum: '10' }, coreVersion: '10.291' }).ok, false, ); assert.equal( isSupportedFoundryPackage({ compatibility: { minimum: '11', verified: '12' }, coreVersion: '12.331' }).ok, true, ); }); void test('decodeFoundryAssetPath decodes %20', () => { assert.equal( decodeFoundryAssetPath('modules/x/Unwelcome_Spirits/Map/The%20Withered%20Grove%20(day).webp'), 'modules/x/Unwelcome_Spirits/Map/The Withered Grove (day).webp', ); });