228 lines
9.3 KiB
JavaScript
228 lines
9.3 KiB
JavaScript
import { build } from 'esbuild'
|
|
|
|
const bundled = await build({
|
|
bundle: true,
|
|
entryPoints: ['src/core/particleEmitter.ts'],
|
|
format: 'esm',
|
|
platform: 'node',
|
|
write: false,
|
|
alias: { 'pixi.js': './scripts/pixi-stub.mjs' },
|
|
})
|
|
const { ParticleEmitter, defaultConfig } = await import(
|
|
`data:text/javascript;base64,${Buffer.from(bundled.outputFiles[0].text).toString('base64')}`
|
|
)
|
|
const { Texture } = await import('./pixi-stub.mjs')
|
|
|
|
if (defaultConfig().warmupFrames !== 0) throw new Error('暖场帧数默认值必须为 0')
|
|
const normalizedWarmup = makeConfig({ warmupFrames: -2.7 })
|
|
const normalizedWarmupEmitter = new ParticleEmitter(normalizedWarmup)
|
|
if (normalizedWarmup.warmupFrames !== 0 || normalizedWarmupEmitter.activeCount !== 0) {
|
|
throw new Error('暖场帧数必须归一化为不小于 0 的整数')
|
|
}
|
|
|
|
const subBundled = await build({
|
|
bundle: true,
|
|
entryPoints: ['src/core/subEmitterIds.ts'],
|
|
format: 'esm',
|
|
platform: 'node',
|
|
write: false,
|
|
})
|
|
const { collectSubEmitterIds, collectEmitterAncestors } = await import(
|
|
`data:text/javascript;base64,${Buffer.from(subBundled.outputFiles[0].text).toString('base64')}`
|
|
)
|
|
|
|
function makeConfig(overrides = {}) {
|
|
const config = defaultConfig()
|
|
config.texture = new Texture()
|
|
for (const resource of config.imageResources) resource.texture = new Texture()
|
|
for (const resource of config.trailResources) resource.texture = new Texture()
|
|
Object.assign(config, {
|
|
mode: 'burst',
|
|
burstCount: 1,
|
|
burstLoop: false,
|
|
delay: 0,
|
|
duration: 1,
|
|
lifeMode: 'fixed',
|
|
lifeMin: 0.1,
|
|
lifeMax: 0.1,
|
|
speedMin: 0,
|
|
speedMax: 0,
|
|
shape: 'point',
|
|
collision: false,
|
|
eventEnabled: false,
|
|
}, overrides)
|
|
return config
|
|
}
|
|
|
|
const step = 1 / 30
|
|
const emitter = new ParticleEmitter(makeConfig())
|
|
const events = []
|
|
emitter.onParticleEvent = (event) => events.push(event)
|
|
emitter.update(step, 'force')
|
|
if (!events.some((event) => event.kind === 'birth')) throw new Error('未收到出生事件')
|
|
for (let i = 0; i < 8; i++) emitter.update(step)
|
|
if (!events.some((event) => event.kind === 'death')) throw new Error('未收到死亡事件')
|
|
|
|
const target = new ParticleEmitter(makeConfig({ lifeMin: 1, lifeMax: 1 }))
|
|
if (!target.spawnEventParticle(12, 34, 1)) throw new Error('事件粒子生成失败')
|
|
if (target.activeCount !== 1) throw new Error('事件粒子未进入目标粒子池')
|
|
if (target.spawnEventParticle(0, 0, 3)) throw new Error('事件链深度上限未生效')
|
|
|
|
const inheritConfig = makeConfig({
|
|
burstCount: 0,
|
|
lifeMin: 1,
|
|
lifeMax: 1,
|
|
scaleMode: 'fixed',
|
|
scaleMin: 2,
|
|
scaleMax: 2,
|
|
initialRotationMode: 'fixed',
|
|
initialRotationMin: 0,
|
|
initialRotationMax: 0,
|
|
})
|
|
inheritConfig.imageResources[0].colorMode = 'fixed'
|
|
inheritConfig.imageResources[0].colorStart = '#102030'
|
|
const inheritTarget = new ParticleEmitter(inheritConfig)
|
|
inheritTarget.spawnEventParticle(0, 0, 1, {
|
|
attributes: ['color', 'scale', 'rotation', 'life'],
|
|
colorMode: 'add',
|
|
data: { colorHex: 0x010203, scaleX: 1.5, scaleY: 1.5, rotation: 90, life: 0.5 },
|
|
})
|
|
inheritTarget.update(0)
|
|
const inherited = inheritTarget.capture()[0]
|
|
if (!inherited) throw new Error('继承属性的子发射器粒子未生成')
|
|
if (inherited.colorHex !== 0x112233) throw new Error(`颜色继承应相加,实际 ${inherited.colorHex.toString(16)}`)
|
|
if (Math.abs(inherited.scaleX - 3) > 1e-6 || Math.abs(inherited.scaleY - 3) > 1e-6) throw new Error('大小继承应乘以目标整体缩放')
|
|
if (Math.abs(inherited.rotation - 90) > 1e-6) throw new Error('旋转继承应作为整体方向偏移')
|
|
for (let i = 0; i < 12; i++) inheritTarget.update(step)
|
|
if (inheritTarget.activeCount !== 1) throw new Error('生命周期继承过短')
|
|
for (let i = 0; i < 6; i++) inheritTarget.update(step)
|
|
if (inheritTarget.activeCount !== 0) throw new Error('生命周期继承未生效')
|
|
|
|
const replaceColorTarget = new ParticleEmitter(makeConfig({ burstCount: 0, lifeMin: 1, lifeMax: 1 }))
|
|
replaceColorTarget.spawnEventParticle(0, 0, 1, {
|
|
attributes: ['color'],
|
|
colorMode: 'replace',
|
|
data: { colorHex: 0x445566, scaleX: 1, scaleY: 1, rotation: 0, life: 1 },
|
|
})
|
|
replaceColorTarget.update(0)
|
|
if (replaceColorTarget.capture()[0]?.colorHex !== 0x445566) throw new Error('颜色覆盖继承失败')
|
|
|
|
// 子发射器爆发模式:一次事件应按“单次数量”生成多颗粒子。
|
|
const burstTarget = new ParticleEmitter(makeConfig({ burstCount: 10 }))
|
|
if (!burstTarget.spawnEventParticle(0, 0, 1)) throw new Error('子发射器爆发事件生成失败')
|
|
if (burstTarget.activeCount !== 10) throw new Error(`子发射器应按单次数量生成 10 颗,实际 ${burstTarget.activeCount} 颗`)
|
|
|
|
// 子发射器 delay:事件粒子应延迟发射,而不是立即生成。
|
|
const delayedTarget = new ParticleEmitter(makeConfig({ delay: 0.2, burstCount: 2, lifeMin: 1, lifeMax: 1 }))
|
|
delayedTarget.setAutoEmission(false)
|
|
if (!delayedTarget.spawnEventParticle(0, 0, 1)) throw new Error('延迟子发射器事件未入队')
|
|
if (delayedTarget.activeCount !== 0) throw new Error('子发射器 delay 未生效,粒子立即生成了')
|
|
delayedTarget.update(0.1)
|
|
if (delayedTarget.activeCount !== 0) throw new Error('子发射器延迟未到时间就发射了')
|
|
delayedTarget.update(0.1)
|
|
if (delayedTarget.activeCount !== 2) throw new Error(`子发射器延迟后应生成 2 颗,实际 ${delayedTarget.activeCount} 颗`)
|
|
|
|
// 子发射器:被引用后不自动发射,只接收事件粒子;移除引用后恢复普通系统。
|
|
const subEmitter = new ParticleEmitter(makeConfig({ burstCount: 5 }))
|
|
subEmitter.setAutoEmission(false)
|
|
subEmitter.update(step, 'force')
|
|
if (subEmitter.activeCount !== 0) throw new Error('子发射器不应自动发射')
|
|
if (!subEmitter.spawnEventParticle(0, 0, 1)) throw new Error('子发射器无法接收事件粒子')
|
|
subEmitter.setAutoEmission(true)
|
|
subEmitter.update(step, 'force')
|
|
if (subEmitter.activeCount <= 1) throw new Error('移除引用后应恢复自动发射')
|
|
|
|
const subIds = collectSubEmitterIds([
|
|
{ id: 1, config: { events: [{ targetSystemId: 2 }, { targetSystemId: 1 }] } },
|
|
{ id: 2, config: {} },
|
|
{ id: 3, config: {} },
|
|
])
|
|
if (!subIds.has(2) || subIds.has(1) || subIds.has(3)) throw new Error('子发射器引用集合计算错误')
|
|
|
|
const chainSystems = [
|
|
{ id: 1, config: { events: [{ targetSystemId: 2 }] } },
|
|
{ id: 2, config: { events: [{ targetSystemId: 3 }] } },
|
|
{ id: 3, config: {} },
|
|
]
|
|
const ancestors = collectEmitterAncestors(chainSystems, 3)
|
|
if (!ancestors.has(1) || !ancestors.has(2) || ancestors.has(3)) {
|
|
throw new Error('子发射器上游父系统计算错误,无法阻止循环依赖')
|
|
}
|
|
|
|
const collider = {
|
|
id: 1, shape: 'circle', enabled: true,
|
|
x: 0, y: 0, rotation: 0, scaleX: 1, scaleY: 1,
|
|
radius: 50, radiusX: 50, radiusY: 50, width: 100, height: 100, sides: 6,
|
|
response: 'physics', elasticity: 1, friction: 0,
|
|
}
|
|
const collisionEmitter = new ParticleEmitter(makeConfig({ collision: true, lifeMin: 1, lifeMax: 1 }))
|
|
collisionEmitter.setColliders(Array.from({ length: 4 }, (_, index) => ({
|
|
...collider,
|
|
id: index + 1,
|
|
radius: 50 + index * 10,
|
|
})))
|
|
collisionEmitter.setAutoEmission(false)
|
|
collisionEmitter.spawnEventParticle(0, 0, 1)
|
|
const collisionEvents = []
|
|
collisionEmitter.onParticleEvent = (event) => collisionEvents.push(event)
|
|
collisionEmitter.update(step, 'force')
|
|
if (!collisionEvents.some((event) => event.kind === 'collision')) throw new Error('未收到碰撞事件')
|
|
const collisionCountsBySpawn = new Map()
|
|
for (const event of collisionEvents) {
|
|
if (event.kind !== 'collision') continue
|
|
const counts = collisionCountsBySpawn.get(event.spawnId) || []
|
|
counts.push(event.collisionCount)
|
|
collisionCountsBySpawn.set(event.spawnId, counts)
|
|
}
|
|
if (![...collisionCountsBySpawn.values()].some((counts) => counts[0] === 1 && counts.at(-1) === 4)) {
|
|
throw new Error('同一粒子的碰撞次数未正确累计')
|
|
}
|
|
|
|
const warmupEmitter = new ParticleEmitter(makeConfig({
|
|
mode: 'stream',
|
|
streamBehavior: 'normal',
|
|
rate: 30,
|
|
duration: 10,
|
|
lifeMin: 10,
|
|
lifeMax: 10,
|
|
speedMin: 0,
|
|
speedMax: 0,
|
|
}))
|
|
warmupEmitter.reset()
|
|
warmupEmitter.warmup(3, step)
|
|
if (warmupEmitter.activeCount !== 3) {
|
|
throw new Error(`暖场 3 帧后应有 3 颗粒子,实际 ${warmupEmitter.activeCount} 颗`)
|
|
}
|
|
|
|
// 非等比缩放下,应按世界距离选择最近面,不能让宽扁矩形的顶面碰撞跳到侧面。
|
|
const scaledRectEmitter = new ParticleEmitter(makeConfig({
|
|
collision: true,
|
|
lifeMin: 1,
|
|
lifeMax: 1,
|
|
speedMin: 0,
|
|
speedMax: 0,
|
|
useGravity: false,
|
|
}))
|
|
scaledRectEmitter.setColliders([{
|
|
...collider,
|
|
shape: 'rect',
|
|
x: 0,
|
|
y: -60,
|
|
scaleX: 4,
|
|
scaleY: 0.5,
|
|
width: 120,
|
|
height: 80,
|
|
}])
|
|
scaledRectEmitter.setAutoEmission(false)
|
|
scaledRectEmitter.spawnEventParticle(-125.02, -56.58, 1)
|
|
const scaledRectEvents = []
|
|
scaledRectEmitter.onParticleEvent = (event) => scaledRectEvents.push(event)
|
|
scaledRectEmitter.update(step, 'force')
|
|
const scaledRectHit = scaledRectEvents.find((event) => event.kind === 'collision')
|
|
if (!scaledRectHit) throw new Error('非等比矩形未产生碰撞事件')
|
|
if (Math.abs(scaledRectHit.worldX + 125.02) > 0.1 || Math.abs(scaledRectHit.worldY + 40) > 0.1) {
|
|
throw new Error(`非等比矩形吸附面错误:实际命中 (${scaledRectHit.worldX}, ${scaledRectHit.worldY})`)
|
|
}
|
|
|
|
console.log('粒子事件校验通过:暖场 / 出生 / 死亡 / 碰撞 / 非等比矩形碰撞面 / 子发射器生成与禁用自动发射 / 属性继承 / 深度上限')
|