阶段六开发进度
This commit is contained in:
1 parent
f27167621c
commit
b98d67fdab
5 files changed
+309
-66
No files matched your search
+71
-27
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user