feat(anim): Phase E.1 hook router + GameWindow wiring
Adds IAnimationHookSink + AnimationHookRouter for fan-out of animation hooks to downstream subsystems (audio, particles, combat, renderer mutators). GameWindow.TickAnimations now drains ConsumePendingHooks every tick and broadcasts each hook via the router with the entity's world position pre-computed. The router is a composite sink: register N sinks once at startup, each sees every hook. Registration is idempotent, unregister works, and a throwing sink no longer poisons dispatch (each OnHook call is wrapped in try/catch so one bad subsystem can't halt the whole animation tick). A NullAnimationHookSink is provided for headless tests / offline mode. 6 router tests verify: single/multi sink fan-out, idempotent register, unregister, throwing-sink isolation, null-sink no-op. Total: 376 Core tests + 109 Core.Net = 485 (up from 479). This closes Phase E.1 plumbing; E.2 (audio) and E.3 (particles) will each register a concrete sink that translates their hook types into real-world effects. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4db0b2f16c
commit
b04d393329
4 changed files with 319 additions and 0 deletions
|
|
@ -123,6 +123,12 @@ public sealed class GameWindow : IDisposable
|
|||
|
||||
private AcDream.Core.Physics.DatCollectionLoader? _animLoader;
|
||||
|
||||
// Phase E.1: central fan-out for animation hooks. Audio (E.2),
|
||||
// particles (E.3), combat (E.4), and renderer state mutators all
|
||||
// register sinks at startup. The router is always non-null so the
|
||||
// per-entity tick loop can just call it unconditionally.
|
||||
private readonly AcDream.Core.Physics.AnimationHookRouter _hookRouter = new();
|
||||
|
||||
// Phase B.2: player movement mode.
|
||||
private AcDream.App.Input.PlayerMovementController? _playerController;
|
||||
private AcDream.App.Rendering.ChaseCamera? _chaseCamera;
|
||||
|
|
@ -2923,6 +2929,22 @@ public sealed class GameWindow : IDisposable
|
|||
if (ae.Sequencer is not null)
|
||||
{
|
||||
seqFrames = ae.Sequencer.Advance(dt);
|
||||
|
||||
// Phase E.1: drain animation hooks (footstep sounds, attack
|
||||
// damage frames, particle spawns, part swaps, etc.) and fan
|
||||
// them out to registered subsystems via the hook router.
|
||||
// Mirrors ACE's PhysicsObj.add_anim_hook dispatch path.
|
||||
var hooks = ae.Sequencer.ConsumePendingHooks();
|
||||
if (hooks.Count > 0)
|
||||
{
|
||||
System.Numerics.Vector3 worldPos = ae.Entity.Position;
|
||||
for (int hi = 0; hi < hooks.Count; hi++)
|
||||
{
|
||||
var hook = hooks[hi];
|
||||
if (hook is null) continue;
|
||||
_hookRouter.OnHook(ae.Entity.Id, worldPos, hook);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue