功能修复

This commit is contained in:
tianmo committed 2026-10-29 18:43:11 +08:00
1 parent fed1015849
commit bae179eb93
13 files changed
+274 -81

No files matched your search

+40 -1
View File
@@ -122,6 +122,45 @@ if (delayedTarget.activeCount !== 0) throw new Error('子发射器延迟未到
delayedTarget.update(0.1)
if (delayedTarget.activeCount !== 2) throw new Error(`子发射器延迟后应生成 2 颗,实际 ${delayedTarget.activeCount} 颗`)
// 持续子发射器:每次父事件启动独立窗口,窗口内所有粒子复用该事件的继承快照。
const streamConfig = makeConfig({
mode: 'stream',
streamBehavior: 'normal',
rate: 30,
duration: 0.1,
lifeMin: 1,
lifeMax: 1,
scaleMode: 'fixed',
scaleMin: 1,
scaleMax: 1,
initialRotationMode: 'fixed',
initialRotationMin: 0,
initialRotationMax: 0,
})
streamConfig.imageResources[0].colorMode = 'fixed'
streamConfig.imageResources[0].colorStart = '#102030'
const streamTarget = new ParticleEmitter(streamConfig)
streamTarget.setAutoEmission(false)
const streamInheritAttributes = ['color', 'scale', 'rotation', 'life']
const streamInheritData = { colorHex: 0x778899, scaleX: 2, scaleY: 2, rotation: 90, life: 0.5 }
streamTarget.spawnEventParticle(5, 6, 1, {
attributes: streamInheritAttributes,
colorMode: 'replace',
data: streamInheritData,
})
streamInheritAttributes.length = 0
streamInheritData.colorHex = 0
for (let i = 0; i < 3; i++) streamTarget.update(step)
const streamStates = streamTarget.capture()
if (streamStates.length !== 3) throw new Error(`持续子发射器应按速率生成 3 颗,实际 ${streamStates.length} 颗`)
for (const state of streamStates) {
if (state.colorHex !== 0x778899 || state.scaleX !== 2 || state.scaleY !== 2 || Math.abs(state.rotation - 90) > 1e-6) {
throw new Error('持续发射窗口没有复用父事件的继承快照')
}
}
streamTarget.update(step)
if (streamTarget.capture().length !== 3) throw new Error('持续发射窗口结束后仍继续生成粒子')
// 子发射器:被引用后不自动发射,只接收事件粒子;移除引用后恢复普通系统。
const subEmitter = new ParticleEmitter(makeConfig({ burstCount: 5 }))
subEmitter.setAutoEmission(false)
@@ -224,4 +263,4 @@ if (Math.abs(scaledRectHit.worldX + 125.02) > 0.1 || Math.abs(scaledRectHit.worl
throw new Error(`非等比矩形吸附面错误:实际命中 (${scaledRectHit.worldX}, ${scaledRectHit.worldY})`)
}
console.log('粒子事件校验通过:暖场 / 出生 / 死亡 / 碰撞 / 非等比矩形碰撞面 / 子发射器生成与禁用自动发射 / 属性继承 / 深度上限')
console.log('粒子事件校验通过:暖场 / 出生 / 死亡 / 碰撞 / 非等比矩形碰撞面 / 持续发射 / 子发射器生成与禁用自动发射 / 属性继承 / 深度上限')
+7
View File
@@ -78,6 +78,13 @@ const source = `
failures++
console.error('骨骼池复用:死亡/复用边界应写阶跃关键帧,实际为 ' + JSON.stringify(poolStepped))
}
const limitedResult = buildSpineJson({ systems: [poolSystem], timeline, settings: poolSettings, playbackEnd: 4 })
const limitedFramesAt = limitedResult.json.animations.animation.bones.verify_0.translate
.map((key) => Math.round(key.time * 30))
if (JSON.stringify(limitedFramesAt) !== JSON.stringify([0, 4])) {
failures++
console.error('导出范围:不应写出实际播放末端之后的关键帧,实际为 ' + JSON.stringify(limitedFramesAt))
}
// 子发射器的帧从父事件时刻才开始,不能按自身生命周期从 0f 截断。
const childFrames = Array.from({ length: 80 }, (_, frame) => (
+33 -8
View File
@@ -28,35 +28,38 @@ const source = `
probability: 1, collisionLimit: 1, inherit: [], colorInheritMode: 'add',
}]
// 子发射器自身的 10s 发射时长不应算入时间轴。
// 持续子发射器每次事件都会运行自身的 10s 发射窗口。
child.config.mode = 'stream'
child.config.streamBehavior = 'normal'
child.config.delay = 0
child.config.duration = 10
setLife(child, 1.5)
// 死亡触发:父粒子最晚 3s 死亡,子发射器再存活 1.5s → 4.5s。
// 死亡触发:父粒子最晚 3s 死亡,子发射器持续到 13s,粒子再存活 1.5s → 14.5s。
let frames = store.recalcTotalFrames()
assert(frames === Math.ceil(4.5 * 30) + 1, '死亡子发射器时间轴错误:' + frames)
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 - 4.5) < 1e-6, '死亡子发射器终点应为 4.5s')
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(4.7 * 30) + 1, '子发射器延迟未计入时间轴:' + frames)
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
// 出生触发:子发射器与父粒子同时开始,取较长者 2s → 3s。
// 出生触发:子发射器持续到 11s,粒子存活的 1.5s 继续至 12.5s。
parent.config.events[0].event = 'birth'
frames = store.recalcTotalFrames()
assert(frames === Math.ceil(3 * 30) + 1, '出生子发射器时间轴错误:' + frames)
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 - 2.5) < 1e-6, '出生子发射器终点应为 2.5s')
assert(birthTiming && Math.abs(birthTiming.end - 12.5) < 1e-6, '出生子发射器终点应为 12.5s')
// 关闭“开启子发射器”后,保留的规则不应继续把目标标记为子发射器。
parent.config.eventEnabled = false
@@ -71,6 +74,28 @@ const source = `
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, '播放范围没有停在时间条末端')
console.log('子发射器时间轴校验通过:死亡/出生触发与移除引用后的总长度')
`