feat(vfx): bind effects to live animated poses

This commit is contained in:
Erik 2026-07-14 10:56:01 +02:00
parent 96ddfdf175
commit 542dcfc384
41 changed files with 3246 additions and 741 deletions

View file

@ -75,6 +75,31 @@ public sealed class ParticleSystem : IParticleSystem
return SpawnEmitter(desc, anchor, rot, attachedObjectId, attachedPartIndex, renderPass);
}
public bool TrySpawnEmitterById(
uint emitterId,
Vector3 anchor,
Quaternion? rot,
uint attachedObjectId,
int attachedPartIndex,
ParticleRenderPass renderPass,
out int handle)
{
if (!_registry.TryGet(emitterId, out EmitterDesc? desc))
{
handle = 0;
return false;
}
handle = SpawnEmitter(
desc,
anchor,
rot,
attachedObjectId,
attachedPartIndex,
renderPass);
return true;
}
public void PlayScript(uint scriptId, uint targetObjectId, float modifier = 1f)
{
// Full PhysicsScript scheduling lives in PhysicsScriptRunner.
@ -90,6 +115,12 @@ public sealed class ParticleSystem : IParticleSystem
{
for (int i = 0; i < em.Particles.Length; i++)
em.Particles[i].Alive = false;
// Retail DestroyParticleEmitter removes the table entry now; it
// does not wait for the next update. This is also required for a
// hard-stopped emitter whose cell-less simulation is paused.
_byHandle.Remove(handle);
_handleOrder.Remove(handle);
EmitterDied?.Invoke(handle);
}
}
@ -107,10 +138,50 @@ public sealed class ParticleSystem : IParticleSystem
if (!_byHandle.TryGetValue(handle, out var em))
return;
em.AnchorPos = anchor;
if (!em.SimulationEnabled)
em.LastEmitOffset = anchor;
if (rot.HasValue)
em.AnchorRot = rot.Value;
}
/// <summary>
/// Changes only render presentation. Logical lifetime is unaffected.
/// </summary>
public void SetEmitterPresentationVisible(int handle, bool visible)
{
if (_byHandle.TryGetValue(handle, out ParticleEmitter? emitter))
emitter.PresentationVisible = visible;
}
/// <summary>
/// Applies retail's in-cell update gate without ending emitter ownership.
/// Retail retains absolute creation timestamps while cell-less; the next
/// update observes the elapsed wall-clock interval and expires old state.
/// Only acdream's legacy rate accumulator is rebased to prevent a synthetic
/// multi-particle catch-up burst that retail's one-shot emission path lacks.
/// </summary>
public void SetEmitterSimulationEnabled(int handle, bool enabled)
{
if (!_byHandle.TryGetValue(handle, out ParticleEmitter? emitter)
|| emitter.SimulationEnabled == enabled)
{
return;
}
if (!enabled)
{
emitter.SimulationEnabled = false;
return;
}
if (emitter.Desc.Birthrate <= 0f && emitter.Desc.EmitRate > 0f)
{
emitter.LastEmitTime = _time;
emitter.EmittedAccumulator = 0f;
}
emitter.SimulationEnabled = true;
}
/// <summary>True when the given handle still maps to a live emitter.</summary>
public bool IsEmitterAlive(int handle) => _byHandle.ContainsKey(handle);
@ -136,6 +207,8 @@ public sealed class ParticleSystem : IParticleSystem
int handle = _handleOrder[i];
if (!_byHandle.TryGetValue(handle, out var em))
continue;
if (!em.SimulationEnabled)
continue;
AdvanceEmitter(em);
int live = CountAlive(em);