阶段六开发进度

This commit is contained in:
tianmo committed 2026-08-29 01:31:08 +08:00
1 parent f27167621c
commit b98d67fdab
5 files changed
+309 -66

No files matched your search

+71 -27
View File
@@ -272,6 +272,15 @@ export interface EmitterConfig {
collision: boolean
/** 开启路径跟随(预留) */
pathFollow: boolean
emitterFollow: boolean
emitterFollowMode: 'none' | 'path'
emitterFollowPathId: number
emitterAngleMode: 'fixed' | 'direction'
emitterFollowCurve: CurvePoint[]
emitterFollowDuration: number
emitterFollowSpace: 'local' | 'world'
emitterFollowDirection: 'forward' | 'reverse'
emitterFollowOffset: number
}
/** 粒子瞬间状态(供骨骼同步 / 导出) */
@@ -490,6 +499,15 @@ export function defaultConfig(): EmitterConfig {
trailGridCols: 4,
collision: false,
pathFollow: false,
emitterFollow: false,
emitterFollowMode: 'none',
emitterFollowPathId: 0,
emitterAngleMode: 'fixed',
emitterFollowCurve: [{ x: 0, y: 0 }, { x: 1, y: 1 }],
emitterFollowDuration: 2,
emitterFollowSpace: 'world',
emitterFollowDirection: 'forward',
emitterFollowOffset: 0,
}
}
@@ -711,6 +729,10 @@ export class ParticleEmitter extends Container {
private _lastSeed = -1
private trailDebugGfx = new Graphics()
private colliders: SceneCollider[] = []
private followSpawnX = 0
private followSpawnY = 0
private followSpawnAngle = 0
private simulationTransform: { centerX: number; centerY: number; rotation: number } | null = null
constructor(config: EmitterConfig, maxParticles = MAX_PARTICLES) {
super()
@@ -766,12 +788,20 @@ export class ParticleEmitter extends Container {
setEmitterPos(fn: () => IPointData) { this._emitterPos = fn }
setColliders(colliders: SceneCollider[]) { this.colliders = colliders }
setEmitterFollowSpawn(x: number, y: number, angleDegrees: number) {
this.followSpawnX = x
this.followSpawnY = y
this.followSpawnAngle = angleDegrees
}
setSimulationTransform(centerX: number, centerY: number, rotation: number) {
this.simulationTransform = { centerX, centerY, rotation }
}
private applyColliders(p: Particle, particleIndex: number) {
if (!this.colliders.length) return false
const cfg = this.cfg
let world = localToWorldMath(cfg, p.x, p.y)
let velocity = localVelocityToWorldMath(cfg, p.vx, p.vy)
let world = localToWorldMath(cfg, p.x, p.y, this.simulationTransform)
let velocity = localVelocityToWorldMath(cfg, p.vx, p.vy, this.simulationTransform)
for (const collider of this.colliders) {
if (!collider.enabled) continue
const hit = resolveColliderPoint(collider, world.x, world.y)
@@ -794,8 +824,8 @@ export class ParticleEmitter extends Container {
velocity.x = tangentX * tangentScale + reflectedNormal * hit.nx
velocity.y = tangentY * tangentScale + reflectedNormal * hit.ny
}
const local = worldMathToLocal(cfg, world.x, world.y)
const localVelocity = worldVelocityToLocalMath(cfg, velocity.x, velocity.y)
const local = worldMathToLocal(cfg, world.x, world.y, this.simulationTransform)
const localVelocity = worldVelocityToLocalMath(cfg, velocity.x, velocity.y, this.simulationTransform)
p.x = local.x
p.y = local.y
p.vx = localVelocity.x
@@ -871,10 +901,12 @@ export class ParticleEmitter extends Container {
if (!p.collisionPaused && cfg.forceField && cfg.forceStrength !== 0 && cfg.forceRadius > 0) {
const rootScaleX = Math.max(0.0001, Math.abs(cfg.rootScaleX))
const rootScaleY = Math.max(0.0001, Math.abs(cfg.rootScaleY))
const rootAngle = -cfg.rootRotation * Math.PI / 180
const rootAngle = -(this.simulationTransform?.rotation ?? cfg.rootRotation) * Math.PI / 180
const cosRoot = Math.cos(rootAngle), sinRoot = Math.sin(rootAngle)
const particleScreenX = cfg.centerX + cosRoot * rootScaleX * p.x - sinRoot * rootScaleY * p.y
const particleScreenY = -cfg.centerY + sinRoot * rootScaleX * p.x + cosRoot * rootScaleY * p.y
const centerX = this.simulationTransform?.centerX ?? cfg.centerX
const centerY = this.simulationTransform?.centerY ?? cfg.centerY
const particleScreenX = centerX + cosRoot * rootScaleX * p.x - sinRoot * rootScaleY * p.y
const particleScreenY = -centerY + sinRoot * rootScaleX * p.x + cosRoot * rootScaleY * p.y
const particleWorldX = particleScreenX
const particleWorldY = -particleScreenY
const dx = cfg.forceCenterX - particleWorldX
@@ -914,7 +946,7 @@ export class ParticleEmitter extends Container {
? age >= Math.max(0, cfg.attractionDelay)
: this._elapsed - this._attractionEnabledAt >= Math.max(0, cfg.attractionDelay)
if (delayReached) {
const currentWorld = localToWorldMath(cfg, p.x, p.y)
const currentWorld = localToWorldMath(cfg, p.x, p.y, this.simulationTransform)
const targetX = cfg.attractionCenterX
const targetY = cfg.attractionCenterY
const radius = Math.max(0, cfg.attractionRadius)
@@ -951,7 +983,7 @@ export class ParticleEmitter extends Container {
this.kill(i)
continue
}
const local = worldMathToLocal(cfg, worldX, worldY)
const local = worldMathToLocal(cfg, worldX, worldY, this.simulationTransform)
p.x = local.x
p.y = local.y
// 吸附期间位置由路径控制,清除速度以免抵达后发生额外积分偏移。
@@ -1268,14 +1300,23 @@ export class ParticleEmitter extends Container {
const s = cfg.shape
if (s === 'circle') {
const a = this.rng() * Math.PI * 2, r = this.rng() * cfg.radius
return { x: ox + Math.cos(a) * r, y: oy + Math.sin(a) * r }
return this.applyFollowToOrigin(ox + Math.cos(a) * r, oy + Math.sin(a) * r)
} else if (s === 'rect') {
return { x: ox + (this.rng() - 0.5) * cfg.rectW, y: oy + (this.rng() - 0.5) * cfg.rectH }
return this.applyFollowToOrigin(ox + (this.rng() - 0.5) * cfg.rectW, oy + (this.rng() - 0.5) * cfg.rectH)
} else if (s === 'cone') {
// 锥体:粒子从中心(锥形顶点)发射,方向由 spawnOne 在 [direction ± coneAngle/2] 扇形内随机
return { x: ox, y: oy }
return this.applyFollowToOrigin(ox, oy)
}
return this.applyFollowToOrigin(ox, oy)
}
private applyFollowToOrigin(x: number, y: number) {
const angle = this.followSpawnAngle * Math.PI / 180
const cos = Math.cos(angle), sin = Math.sin(angle)
return {
x: this.followSpawnX + cos * x - sin * y,
y: this.followSpawnY + sin * x + cos * y,
}
return { x: ox, y: oy }
}
private spawnOne() {
@@ -1288,9 +1329,9 @@ export class ParticleEmitter extends Container {
// 锥体:发射方向用锥形开口角度(coneAngle)散布,且 y 朝数学上正(与扇区绘制一致);其余形状沿用 spread + 屏幕坐标
let dirAng: number
if (cfg.shape === 'cone') {
dirAng = (cfg.direction + lerp(-cfg.coneAngle / 2, cfg.coneAngle / 2, this.rng())) * Math.PI / 180
dirAng = (cfg.direction + this.followSpawnAngle + lerp(-cfg.coneAngle / 2, cfg.coneAngle / 2, this.rng())) * Math.PI / 180
} else {
dirAng = (cfg.direction + lerp(-cfg.spread / 2, cfg.spread / 2, this.rng())) * Math.PI / 180
dirAng = (cfg.direction + this.followSpawnAngle + lerp(-cfg.spread / 2, cfg.spread / 2, this.rng())) * Math.PI / 180
}
const p = this.pool[idx]
const resource = this.pickImageResource()
@@ -1499,40 +1540,43 @@ export class ParticleEmitter extends Container {
function lerp(a: number, b: number, t: number) { return a + (b - a) * t }
function finite(value: number, fallback: number) { return Number.isFinite(value) ? value : fallback }
function localToWorldMath(cfg: EmitterConfig, localX: number, localY: number) {
type SimulationTransform = { centerX: number; centerY: number; rotation: number } | null
function localToWorldMath(cfg: EmitterConfig, localX: number, localY: number, transform: SimulationTransform = null) {
const scaleX = Math.max(0.0001, Math.abs(cfg.rootScaleX))
const scaleY = Math.max(0.0001, Math.abs(cfg.rootScaleY))
const angle = -cfg.rootRotation * Math.PI / 180
const angle = -(transform?.rotation ?? cfg.rootRotation) * Math.PI / 180
const cos = Math.cos(angle), sin = Math.sin(angle)
const screenX = cfg.centerX + cos * scaleX * localX - sin * scaleY * localY
const screenY = -cfg.centerY + sin * scaleX * localX + cos * scaleY * localY
const centerX = transform?.centerX ?? cfg.centerX
const centerY = transform?.centerY ?? cfg.centerY
const screenX = centerX + cos * scaleX * localX - sin * scaleY * localY
const screenY = -centerY + sin * scaleX * localX + cos * scaleY * localY
return { x: screenX, y: -screenY }
}
function worldMathToLocal(cfg: EmitterConfig, worldX: number, worldY: number) {
function worldMathToLocal(cfg: EmitterConfig, worldX: number, worldY: number, transform: SimulationTransform = null) {
const scaleX = Math.max(0.0001, Math.abs(cfg.rootScaleX))
const scaleY = Math.max(0.0001, Math.abs(cfg.rootScaleY))
const angle = -cfg.rootRotation * Math.PI / 180
const angle = -(transform?.rotation ?? cfg.rootRotation) * Math.PI / 180
const cos = Math.cos(angle), sin = Math.sin(angle)
const screenX = worldX - cfg.centerX
const screenY = -worldY + cfg.centerY
const screenX = worldX - (transform?.centerX ?? cfg.centerX)
const screenY = -worldY + (transform?.centerY ?? cfg.centerY)
return {
x: (cos * screenX + sin * screenY) / scaleX,
y: (-sin * screenX + cos * screenY) / scaleY,
}
}
function localVelocityToWorldMath(cfg: EmitterConfig, vx: number, vy: number) {
function localVelocityToWorldMath(cfg: EmitterConfig, vx: number, vy: number, transform: SimulationTransform = null) {
const scaleX = Math.max(0.0001, Math.abs(cfg.rootScaleX))
const scaleY = Math.max(0.0001, Math.abs(cfg.rootScaleY))
const angle = -cfg.rootRotation * Math.PI / 180
const angle = -(transform?.rotation ?? cfg.rootRotation) * Math.PI / 180
const cos = Math.cos(angle), sin = Math.sin(angle)
const screenX = cos * scaleX * vx - sin * scaleY * vy
const screenY = sin * scaleX * vx + cos * scaleY * vy
return { x: screenX, y: -screenY }
}
function worldVelocityToLocalMath(cfg: EmitterConfig, vx: number, vy: number) {
function worldVelocityToLocalMath(cfg: EmitterConfig, vx: number, vy: number, transform: SimulationTransform = null) {
const scaleX = Math.max(0.0001, Math.abs(cfg.rootScaleX))
const scaleY = Math.max(0.0001, Math.abs(cfg.rootScaleY))
const angle = -cfg.rootRotation * Math.PI / 180
const angle = -(transform?.rotation ?? cfg.rootRotation) * Math.PI / 180
const cos = Math.cos(angle), sin = Math.sin(angle)
const screenY = -vy
return {