// 离线回归:scene 多轨数据源 + 重叠演出(无浏览器,stub window 加载 timeline.js)。 // 跑法:node test_scene.js const fs = require("fs"), path = require("path"); global.window = {}; require("./web/static/timeline.js"); // 设置 window.Timeline(加载期不碰 DOM) const T = global.window.Timeline; function loadAnchors(psName) { // 复刻 app.py /api/pointsets:把点位集转成 anchors [{name,pos:[x,y,z]}] const p = path.join("E:/Library/SGame/Assets/StreamingAssets/Story/PointSets", psName + ".points.json"); const ps = JSON.parse(fs.readFileSync(p, "utf-8")); return (ps.points || []).map(pt => ({ name: pt.name, pos: pt.pos || [0, 0, 0], rot: pt.rot || 0 })); } let fails = 0; function ok(cond, msg) { console.log((cond ? "PASS " : "FAIL ") + msg); if (!cond) fails++; } function overlap(a, b) { return a.start < b.start + b.dur && b.start < a.start + a.dur; } const ir = JSON.parse(fs.readFileSync("samples/scene_demo.ir.json", "utf-8")); const anchors = loadAnchors("QY_TLDEMO"); const M = T._buildModel(ir, anchors); // 1) scene clips 被铺出来:P1/NP1 各有 move,NP2 有 dialogue,镜头有 camera const p1move = M.clips.find(c => c.actor === "P1" && c.kind === "move"); const np1move = M.clips.find(c => c.actor === "NP1" && c.kind === "move"); const np1say = M.clips.find(c => c.actor === "NP1" && c.kind === "dialogue"); const cam = M.clips.find(c => c.kind === "camera"); ok(!!p1move, "P1 走位 clip 存在"); ok(!!np1move, "NP1 走位 clip 存在"); ok(!!np1say, "NP1 对话 clip 存在"); ok(!!cam && cam.focus === "PT_CENTER", "镜头 clip 对焦 PT_CENTER"); // 2) 核心:A 走与 1.5s 后 B 走在时间上重叠(两点同时移动) ok(overlap(p1move, np1move), `P1/NP1 走位重叠:P1[${p1move.start.toFixed(2)},${(p1move.start + p1move.dur).toFixed(2)}] NP1[${np1move.start.toFixed(2)},${(np1move.start + np1move.dur).toFixed(2)}]`); // 3) NP1 比 P1 晚 1.5s 起步(authored start 偏移被尊重;scene 整体在前置 narration 之后铺轴) ok(Math.abs((np1move.start - p1move.start) - 1.5) < 1e-6, `NP1 比 P1 晚起步 ${(np1move.start - p1move.start).toFixed(2)}s 应=1.5`); // 4) move from 续连:P1 从初始锚点(-6) 起步、终点 PT_CENTER(+1.5) ok(Math.abs(p1move.from.z - (-6)) < 1e-6 && Math.abs(p1move.to.z - 1.5) < 1e-6, `P1 move from.z=${p1move.from.z}→to.z=${p1move.to.z}`); // 5) scene 之后能续演到 choice(extendSegment 流过 scene→n_choice) const s = T._prepare(ir, anchors); let r = T._extend(s, T._firstNode(ir)); ok(r && (r.kind === "choice"), "scene 后停在 choice:kind=" + (r && r.kind)); // 6) NP2 对话比 P1 走位晚 4.5s(scene-local start 偏移) const np2say = M.clips.find(c => c.actor === "NP2" && c.kind === "dialogue"); ok(np2say && Math.abs((np2say.start - p1move.start) - 4.5) < 1e-6, "NP2 对话比 P1 晚 " + (np2say && (np2say.start - p1move.start).toFixed(2)) + "s 应=4.5"); // 7) 重叠时刻两人都在走(数据层:取两 move 区间交集中点) const lo = Math.max(p1move.start, np1move.start), hi = Math.min(p1move.start + p1move.dur, np1move.start + np1move.dur); const tau = (lo + hi) / 2; const p1moving = tau >= p1move.start && tau < p1move.start + p1move.dur; const np1moving = tau >= np1move.start && tau < np1move.start + np1move.dur; ok(p1moving && np1moving, `tau=${tau.toFixed(2)}(重叠区间中点)时 P1 走=${p1moving} NP1 走=${np1moving}(应同时为真)`); console.log("\n" + (fails ? (fails + " 个断言失败") : "全部通过")); process.exit(fails ? 1 : 0);