diff --git a/src/AcDream.App/Rendering/Vfx/EntityScriptActivator.cs b/src/AcDream.App/Rendering/Vfx/EntityScriptActivator.cs
index 7211f85..828ccae 100644
--- a/src/AcDream.App/Rendering/Vfx/EntityScriptActivator.cs
+++ b/src/AcDream.App/Rendering/Vfx/EntityScriptActivator.cs
@@ -1,5 +1,4 @@
using System;
-using System.Numerics;
using AcDream.Core.Vfx;
using AcDream.Core.World;
diff --git a/tests/AcDream.Core.Tests/Rendering/Vfx/EntityScriptActivatorTests.cs b/tests/AcDream.Core.Tests/Rendering/Vfx/EntityScriptActivatorTests.cs
index d31ca29..d835e8c 100644
--- a/tests/AcDream.Core.Tests/Rendering/Vfx/EntityScriptActivatorTests.cs
+++ b/tests/AcDream.Core.Tests/Rendering/Vfx/EntityScriptActivatorTests.cs
@@ -89,22 +89,64 @@ public sealed class EntityScriptActivatorTests
Assert.Empty(p.Recording.Calls);
}
+ ///
+ /// Persistent emitter: TotalDuration=0 and TotalParticles=0 prevent
+ /// auto-finish; InitialParticles=1 ensures a particle spawns at t=0
+ /// without waiting for the Birthrate timer; Lifespan=999f keeps that
+ /// particle alive far past the test horizon.
+ ///
+ private static EmitterDesc BuildPersistentEmitterDesc() => new()
+ {
+ DatId = 100u,
+ Type = ParticleType.Still,
+ EmitterKind = ParticleEmitterKind.BirthratePerSec,
+ MaxParticles = 4,
+ InitialParticles = 1,
+ TotalParticles = 0, // 0 = no particle-count cap
+ TotalDuration = 0f, // 0 = no time-based finish
+ Lifespan = 999f,
+ LifetimeMin = 999f,
+ LifetimeMax = 999f,
+ Birthrate = 0.5f,
+ StartSize = 0.5f,
+ EndSize = 0.5f,
+ StartAlpha = 1f,
+ EndAlpha = 1f,
+ };
+
[Fact]
public void OnRemove_StopsScriptsAndEmitters()
{
- var p = BuildPipeline(
- (0xAAu, BuildScript((0.0, new CreateParticleHook { EmitterInfoId = 100 }))));
- var activator = new EntityScriptActivator(p.Runner, p.Sink, _ => 0xAAu);
+ // For this test we need the runner to dispatch into the REAL
+ // ParticleHookSink so OnRemove's sink.StopAllForEntity has a live
+ // emitter to kill. This is the only observable way to verify the
+ // call had effect without subclassing the sealed sink.
+ var registry = new EmitterDescRegistry();
+ registry.Register(BuildPersistentEmitterDesc());
+
+ var system = new ParticleSystem(registry);
+ var hookSink = new ParticleHookSink(system);
+
+ var script = BuildScript((0.0, new CreateParticleHook { EmitterInfoId = 100u, Offset = new Frame() }));
+ var table = new Dictionary { [0xAAu] = script };
+ var runner = new PhysicsScriptRunner(
+ id => table.TryGetValue(id, out var s) ? s : null,
+ hookSink); // runner dispatches into real sink, not RecordingSink
+
+ var activator = new EntityScriptActivator(runner, hookSink, _ => 0xAAu);
var entity = MakeEntity(serverGuid: 0xCAFEu, position: Vector3.Zero);
activator.OnCreate(entity);
- Assert.Equal(1, p.Runner.ActiveScriptCount);
+ runner.Tick(0.001f); // fires the CreateParticleHook → spawns emitter
+
+ Assert.True(system.ActiveEmitterCount > 0,
+ "Setup precondition failed: emitter should be alive after the hook fires.");
activator.OnRemove(0xCAFEu);
- Assert.Equal(0, p.Runner.ActiveScriptCount);
- // Tick after Remove must not surface any further hook fires.
- p.Runner.Tick(1.0f);
- Assert.Empty(p.Recording.Calls);
+ Assert.Equal(0, runner.ActiveScriptCount);
+ // sink.StopAllForEntity marks the emitter Finished; system.Tick reaps it.
+ system.Tick(0.01f);
+ Assert.Equal(0, system.ActiveEmitterCount);
}
}