import { build } from 'esbuild' const source = ` import { createPinia, setActivePinia } from 'pinia' import { useParticleStore } from './src/store/particleStore.ts' import { subEmitterTriggerTimings } from './src/core/subEmitterTiming.ts' import { collectSubEmitterIds } from './src/core/subEmitterIds.ts' setActivePinia(createPinia()) const store = useParticleStore() const parent = store.addSystem() const child = store.addSystem() const assert = (condition, message) => { if (!condition) throw new Error(message) } const setLife = (system, life) => { system.config.lifeMode = 'fixed' system.config.lifeMin = life system.config.lifeMax = life } parent.config.mode = 'stream' parent.config.streamBehavior = 'normal' parent.config.delay = 0 parent.config.duration = 1 setLife(parent, 2) parent.config.eventEnabled = true parent.config.events = [{ id: 1, event: 'death', targetSystemId: child.id, probability: 1, collisionLimit: 1, inherit: [], colorInheritMode: 'add', }] // 持续子发射器每次事件都会运行自身的 10s 发射窗口。 child.config.mode = 'stream' child.config.streamBehavior = 'normal' child.config.delay = 0 child.config.duration = 10 setLife(child, 1.5) // 死亡触发:父粒子最晚 3s 死亡,子发射器持续到 13s,粒子再存活 1.5s → 14.5s。 let frames = store.recalcTotalFrames() assert(frames === Math.ceil(14.5 * 30) + 1, '死亡子发射器时间轴错误:' + frames) const deathTiming = subEmitterTriggerTimings(store.systems, 30).get(child.id) assert(deathTiming && Math.abs(deathTiming.firstTrigger - 2) < 1e-6, '死亡子发射器起点应为第一次死亡 2s') assert(deathTiming && Math.abs(deathTiming.lastTrigger - 3) < 1e-6, '死亡子发射器最后触发应为 3s') assert(deathTiming && Math.abs(deathTiming.end - 14.5) < 1e-6, '死亡子发射器终点应为 14.5s') // 子发射器自身 delay 在父事件之后继续延后发射。 child.config.delay = 0.2 frames = store.recalcTotalFrames() assert(frames === Math.ceil(14.7 * 30) + 1, '子发射器延迟未计入时间轴:' + frames) const delayedTiming = subEmitterTriggerTimings(store.systems, 30).get(child.id) assert(delayedTiming && Math.abs(delayedTiming.end - delayedTiming.firstTrigger - 0.2 - 12.5) < 1e-6, '子发射器拖动延迟后,可见条长度不应包含 delay') child.config.delay = 0 // 出生触发:子发射器持续到 11s,粒子存活的 1.5s 继续至 12.5s。 parent.config.events[0].event = 'birth' frames = store.recalcTotalFrames() assert(frames === Math.ceil(12.5 * 30) + 1, '出生子发射器时间轴错误:' + frames) const birthTiming = subEmitterTriggerTimings(store.systems, 30).get(child.id) assert(birthTiming && Math.abs(birthTiming.firstTrigger - 0) < 1e-6, '出生子发射器应从父粒子首次出生开始') assert(birthTiming && Math.abs(birthTiming.lastTrigger - 1) < 1e-6, '出生子发射器最后触发应为发射结束 1s') assert(birthTiming && Math.abs(birthTiming.end - 12.5) < 1e-6, '出生子发射器终点应为 12.5s') // 关闭“开启子发射器”后,保留的规则不应继续把目标标记为子发射器。 parent.config.eventEnabled = false assert(!collectSubEmitterIds(store.systems).has(child.id), '关闭子发射器后目标仍被当作子发射器') assert(!subEmitterTriggerTimings(store.systems, 30).has(child.id), '关闭子发射器后仍有时间轴触发区间') frames = store.recalcTotalFrames() assert(frames === Math.ceil(11.5 * 30) + 1, '关闭子发射器后目标未恢复独立时长') // 移除引用后子发射器恢复正常,自身的 10s 发射时长重新计入。 parent.config.eventEnabled = true parent.config.events = [] frames = store.recalcTotalFrames() assert(frames === Math.ceil(11.5 * 30) + 1, '移除引用后时间轴错误:' + frames) // 理论总长度可以预留最坏情况,但完整播放只到最后一个可见时间条的末端。 const grandchild = store.addSystem() parent.config.mode = 'burst' setLife(parent, 1) parent.config.eventEnabled = true parent.config.events = [{ id: 1, event: 'death', targetSystemId: child.id, probability: 1, collisionLimit: 1, inherit: [], colorInheritMode: 'add', }] child.config.duration = 1 setLife(child, 1) child.config.eventEnabled = true child.config.events = [{ id: 1, event: 'collision', targetSystemId: grandchild.id, probability: 1, collisionLimit: 1, inherit: [], colorInheritMode: 'add', }] grandchild.config.mode = 'burst' setLife(grandchild, 1) frames = store.recalcTotalFrames() assert(frames === Math.ceil(4 * 30) + 1, '最坏情况总长度计算错误:' + frames) assert(store.playbackRange.start === 0 && store.playbackRange.end === 90, '播放范围没有停在时间条末端') assert(store.contentEndFrame === 90 && store.timeline.totalFrames - 1 - store.contentEndFrame === 30, '空白帧数量计算错误') console.log('子发射器时间轴校验通过:死亡/出生触发与移除引用后的总长度') ` const result = await build({ stdin: { contents: source, resolveDir: process.cwd(), sourcefile: 'verify-sub-emitter-timeline.ts', loader: 'ts' }, bundle: true, platform: 'node', format: 'esm', write: false, logLevel: 'silent', }) const code = Buffer.from(result.outputFiles[0].contents).toString('base64') await import(`data:text/javascript;base64,${code}`)