feat(vfx): bind effects to live animated poses
This commit is contained in:
parent
96ddfdf175
commit
542dcfc384
41 changed files with 3246 additions and 741 deletions
|
|
@ -43,6 +43,7 @@ public sealed class EntityScriptActivatorTests
|
|||
private record Pipeline(
|
||||
ParticleSystem System,
|
||||
ParticleHookSink Sink,
|
||||
EntityEffectPoseRegistry Poses,
|
||||
PhysicsScriptRunner Runner,
|
||||
RecordingSink Recording);
|
||||
|
||||
|
|
@ -50,14 +51,15 @@ public sealed class EntityScriptActivatorTests
|
|||
{
|
||||
var registry = new EmitterDescRegistry();
|
||||
var system = new ParticleSystem(registry);
|
||||
var hookSink = new ParticleHookSink(system); // for activator's StopAllForEntity
|
||||
var poses = new EntityEffectPoseRegistry();
|
||||
var hookSink = new ParticleHookSink(system, poses); // for activator's StopAllForEntity
|
||||
var recording = new RecordingSink(); // for runner's hook dispatch
|
||||
var table = new Dictionary<uint, DatPhysicsScript>();
|
||||
foreach (var (id, s) in scripts) table[id] = s;
|
||||
var runner = new PhysicsScriptRunner(
|
||||
id => table.TryGetValue(id, out var s) ? s : null,
|
||||
recording);
|
||||
return new Pipeline(system, hookSink, runner, recording);
|
||||
return new Pipeline(system, hookSink, poses, runner, recording);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -74,7 +76,7 @@ public sealed class EntityScriptActivatorTests
|
|||
{
|
||||
var p = BuildPipeline(
|
||||
(0xAAu, BuildScript((0.0, new CreateParticleHook { EmitterInfoId = 100 }))));
|
||||
var activator = new EntityScriptActivator(p.Runner, p.Sink, StaticResolver(0xAAu));
|
||||
var activator = new EntityScriptActivator(p.Runner, p.Sink, p.Poses, StaticResolver(0xAAu));
|
||||
var entity = MakeEntity(serverGuid: 0xCAFEu, position: new Vector3(1, 2, 3));
|
||||
|
||||
activator.OnCreate(entity);
|
||||
|
|
@ -90,7 +92,7 @@ public sealed class EntityScriptActivatorTests
|
|||
public void OnCreate_WithoutDefaultScript_DoesNothing()
|
||||
{
|
||||
var p = BuildPipeline(); // no scripts registered
|
||||
var activator = new EntityScriptActivator(p.Runner, p.Sink, _ => null);
|
||||
var activator = new EntityScriptActivator(p.Runner, p.Sink, p.Poses, _ => null);
|
||||
var entity = MakeEntity(serverGuid: 0xCAFEu, position: Vector3.Zero);
|
||||
|
||||
activator.OnCreate(entity);
|
||||
|
|
@ -125,11 +127,11 @@ public sealed class EntityScriptActivatorTests
|
|||
};
|
||||
|
||||
[Fact]
|
||||
public void OnCreate_SetsEntityRotationForHookOffsetTransform()
|
||||
public void OnCreate_PublishesEntityRotationForHookOffsetTransform()
|
||||
{
|
||||
// The CreateParticleHook's Offset is in entity-local frame; the sink
|
||||
// needs the entity's rotation to transform it to world space. If the
|
||||
// activator forgets SetEntityRotation, the offset goes off in world
|
||||
// reads the published entity root to transform it to world space. If
|
||||
// the activator omits that pose, the offset goes off in world
|
||||
// axes — visual symptom: portal swirls misaligned to the portal stone.
|
||||
// This test verifies the seed happens by checking the spawned particle's
|
||||
// world position matches the rotated offset, not the unrotated offset.
|
||||
|
|
@ -138,7 +140,8 @@ public sealed class EntityScriptActivatorTests
|
|||
registry.Register(BuildPersistentEmitterDesc());
|
||||
|
||||
var system = new ParticleSystem(registry);
|
||||
var hookSink = new ParticleHookSink(system);
|
||||
var poses = new EntityEffectPoseRegistry();
|
||||
var hookSink = new ParticleHookSink(system, poses);
|
||||
|
||||
// Hook offset = (1, 0, 0) in entity-local frame.
|
||||
var hookOffset = new Frame
|
||||
|
|
@ -147,13 +150,18 @@ public sealed class EntityScriptActivatorTests
|
|||
Orientation = Quaternion.Identity,
|
||||
};
|
||||
var script = BuildScript(
|
||||
(0.0, new CreateParticleHook { EmitterInfoId = 100u, Offset = hookOffset }));
|
||||
(0.0, new CreateParticleHook
|
||||
{
|
||||
EmitterInfoId = 100u,
|
||||
Offset = hookOffset,
|
||||
PartIndex = uint.MaxValue,
|
||||
}));
|
||||
var table = new Dictionary<uint, DatPhysicsScript> { [0xAAu] = script };
|
||||
var runner = new PhysicsScriptRunner(
|
||||
id => table.TryGetValue(id, out var s) ? s : null,
|
||||
hookSink);
|
||||
|
||||
var activator = new EntityScriptActivator(runner, hookSink, StaticResolver(0xAAu));
|
||||
var activator = new EntityScriptActivator(runner, hookSink, poses, StaticResolver(0xAAu));
|
||||
|
||||
// Entity rotated 90° around world-Z (yaw left); local +X maps to world +Y.
|
||||
var entityRotation = Quaternion.CreateFromAxisAngle(
|
||||
|
|
@ -193,15 +201,21 @@ public sealed class EntityScriptActivatorTests
|
|||
registry.Register(BuildPersistentEmitterDesc());
|
||||
|
||||
var system = new ParticleSystem(registry);
|
||||
var hookSink = new ParticleHookSink(system);
|
||||
var poses = new EntityEffectPoseRegistry();
|
||||
var hookSink = new ParticleHookSink(system, poses);
|
||||
|
||||
var script = BuildScript((0.0, new CreateParticleHook { EmitterInfoId = 100u, Offset = new Frame() }));
|
||||
var script = BuildScript((0.0, new CreateParticleHook
|
||||
{
|
||||
EmitterInfoId = 100u,
|
||||
Offset = new Frame(),
|
||||
PartIndex = uint.MaxValue,
|
||||
}));
|
||||
var table = new Dictionary<uint, DatPhysicsScript> { [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, StaticResolver(0xAAu));
|
||||
var activator = new EntityScriptActivator(runner, hookSink, poses, StaticResolver(0xAAu));
|
||||
var entity = MakeEntity(serverGuid: 0xCAFEu, position: Vector3.Zero);
|
||||
|
||||
activator.OnCreate(entity);
|
||||
|
|
@ -226,7 +240,7 @@ public sealed class EntityScriptActivatorTests
|
|||
// OnCreate must use entity.Id as the key (not skip).
|
||||
var p = BuildPipeline(
|
||||
(0xAAu, BuildScript((0.0, new CreateParticleHook { EmitterInfoId = 100 }))));
|
||||
var activator = new EntityScriptActivator(p.Runner, p.Sink, StaticResolver(0xAAu));
|
||||
var activator = new EntityScriptActivator(p.Runner, p.Sink, p.Poses, StaticResolver(0xAAu));
|
||||
var entity = new WorldEntity
|
||||
{
|
||||
Id = 0x40A9B401u, // dat-hydrated interior id
|
||||
|
|
@ -251,7 +265,7 @@ public sealed class EntityScriptActivatorTests
|
|||
{
|
||||
var p = BuildPipeline(
|
||||
(0xAAu, BuildScript((1.0, new CreateParticleHook { EmitterInfoId = 100 }))));
|
||||
var activator = new EntityScriptActivator(p.Runner, p.Sink, StaticResolver(0xAAu));
|
||||
var activator = new EntityScriptActivator(p.Runner, p.Sink, p.Poses, StaticResolver(0xAAu));
|
||||
WorldEntity first = MakeDatStatic(0x80A9B400u, new Vector3(1, 0, 0));
|
||||
WorldEntity second = MakeDatStatic(0x80A9B400u, new Vector3(2, 0, 0));
|
||||
|
||||
|
|
@ -268,7 +282,7 @@ public sealed class EntityScriptActivatorTests
|
|||
public void LiveParticleFilterUsesCanonicalLocalEffectOwnerNotServerGuid()
|
||||
{
|
||||
var p = BuildPipeline();
|
||||
var activator = new EntityScriptActivator(p.Runner, p.Sink, _ => null);
|
||||
var activator = new EntityScriptActivator(p.Runner, p.Sink, p.Poses, _ => null);
|
||||
var entity = new WorldEntity
|
||||
{
|
||||
Id = 1_000_123u,
|
||||
|
|
@ -285,6 +299,34 @@ public sealed class EntityScriptActivatorTests
|
|||
Assert.NotEqual(entity.ServerGuid, GameWindow.ParticleEntityKey(entity));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemovingLiveOwner_DoesNotClearIndependentDatStaticPose()
|
||||
{
|
||||
var p = BuildPipeline();
|
||||
var activator = new EntityScriptActivator(
|
||||
p.Runner,
|
||||
p.Sink,
|
||||
p.Poses,
|
||||
StaticResolver(0));
|
||||
WorldEntity datStatic = MakeDatStatic(0x40A9B410u, Vector3.One);
|
||||
var live = new WorldEntity
|
||||
{
|
||||
Id = 1_000_410u,
|
||||
ServerGuid = 0x70000410u,
|
||||
SourceGfxObjOrSetupId = 0x02000001u,
|
||||
Position = new Vector3(2, 2, 2),
|
||||
Rotation = Quaternion.Identity,
|
||||
MeshRefs = Array.Empty<MeshRef>(),
|
||||
};
|
||||
activator.OnCreate(datStatic);
|
||||
activator.OnCreate(live);
|
||||
|
||||
activator.OnRemove(live);
|
||||
|
||||
Assert.True(p.Poses.TryGetRootPose(datStatic.Id, out _));
|
||||
Assert.False(p.Poses.TryGetRootPose(live.Id, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OnCreate_PassesPartTransformsToSink()
|
||||
{
|
||||
|
|
@ -295,7 +337,8 @@ public sealed class EntityScriptActivatorTests
|
|||
var registry = new EmitterDescRegistry();
|
||||
registry.Register(BuildPersistentEmitterDesc());
|
||||
var system = new ParticleSystem(registry);
|
||||
var hookSink = new ParticleHookSink(system);
|
||||
var poses = new EntityEffectPoseRegistry();
|
||||
var hookSink = new ParticleHookSink(system, poses);
|
||||
|
||||
var hookOffset = new Frame { Origin = new Vector3(1f, 0, 0), Orientation = Quaternion.Identity };
|
||||
var script = BuildScript(
|
||||
|
|
@ -311,7 +354,7 @@ public sealed class EntityScriptActivatorTests
|
|||
Matrix4x4.CreateTranslation(0f, 0f, 1f),
|
||||
};
|
||||
|
||||
var activator = new EntityScriptActivator(runner, hookSink,
|
||||
var activator = new EntityScriptActivator(runner, hookSink, poses,
|
||||
_ => new ScriptActivationInfo(0xAAu, partTransforms));
|
||||
var entity = MakeEntity(serverGuid: 0xCAFEu, position: Vector3.Zero);
|
||||
|
||||
|
|
@ -335,15 +378,21 @@ public sealed class EntityScriptActivatorTests
|
|||
var registry = new EmitterDescRegistry();
|
||||
registry.Register(BuildPersistentEmitterDesc());
|
||||
var system = new ParticleSystem(registry);
|
||||
var hookSink = new ParticleHookSink(system);
|
||||
var poses = new EntityEffectPoseRegistry();
|
||||
var hookSink = new ParticleHookSink(system, poses);
|
||||
|
||||
var script = BuildScript((0.0, new CreateParticleHook { EmitterInfoId = 100u, Offset = new Frame() }));
|
||||
var script = BuildScript((0.0, new CreateParticleHook
|
||||
{
|
||||
EmitterInfoId = 100u,
|
||||
Offset = new Frame(),
|
||||
PartIndex = uint.MaxValue,
|
||||
}));
|
||||
var table = new Dictionary<uint, DatPhysicsScript> { [0xAAu] = script };
|
||||
var runner = new PhysicsScriptRunner(
|
||||
id => table.TryGetValue(id, out var s) ? s : null,
|
||||
hookSink);
|
||||
|
||||
var activator = new EntityScriptActivator(runner, hookSink, StaticResolver(0xAAu));
|
||||
var activator = new EntityScriptActivator(runner, hookSink, poses, StaticResolver(0xAAu));
|
||||
var entity = new WorldEntity
|
||||
{
|
||||
Id = 0x40A9B402u,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue