feat(anim): integrate AnimationSequencer into GameWindow rendering loop
Wire the new AnimationSequencer into per-entity animation playback: - Added `Sequencer` field to `AnimatedEntity`; populated at spawn time by loading the entity's MotionTable and calling SetCycle on the resolved idle (style, motion) so playback starts immediately. - Added `_animLoader` (DatCollectionLoader) initialized alongside `_dats` so sequencer instances share a single animation loader. - `TickAnimations`: when an entity has a sequencer, calls `seq.Advance(dt)` and reads back `PartTransform[]` instead of doing manual frame-index math and Quaternion.Slerp. Falls back to the Phase 6.5 manual slerp for entities whose MotionTable couldn't be loaded (missing dat / offline mode). - `OnLiveMotionUpdated`: calls `sequencer.SetCycle(style, motion)` when available so motion changes prepend transition-link frames for smooth blending. - `UpdatePlayerAnimation`: same — calls `sequencer.SetCycle(NonCombatStyleFull, cmd)` on motion changes; also creates a sequencer when the player entity is lazy-registered for the first time. The existing manual-slerp fallback is kept verbatim so behavior is unchanged for any entity that can't be backed by a sequencer. Build clean, 426 tests green. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f48f2745c4
commit
e4dae3d217
1 changed files with 208 additions and 84 deletions
|
|
@ -21,6 +21,12 @@ public sealed class GameWindow : IDisposable
|
||||||
private CameraController? _cameraController;
|
private CameraController? _cameraController;
|
||||||
private IMouse? _capturedMouse;
|
private IMouse? _capturedMouse;
|
||||||
private DatCollection? _dats;
|
private DatCollection? _dats;
|
||||||
|
/// <summary>
|
||||||
|
/// Adapter so <see cref="AcDream.Core.Physics.AnimationSequencer"/> instances
|
||||||
|
/// can load Animation dats without knowing about DatCollection directly.
|
||||||
|
/// Initialized alongside <see cref="_dats"/> once the dat files are opened.
|
||||||
|
/// </summary>
|
||||||
|
private AcDream.Core.Physics.DatCollectionLoader? _animLoader;
|
||||||
private float _lastMouseX;
|
private float _lastMouseX;
|
||||||
private float _lastMouseY;
|
private float _lastMouseY;
|
||||||
private StaticMeshRenderer? _staticMesh;
|
private StaticMeshRenderer? _staticMesh;
|
||||||
|
|
@ -91,6 +97,16 @@ public sealed class GameWindow : IDisposable
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required IReadOnlyList<(uint GfxObjId, IReadOnlyDictionary<uint, uint>? SurfaceOverrides)> PartTemplate;
|
public required IReadOnlyList<(uint GfxObjId, IReadOnlyDictionary<uint, uint>? SurfaceOverrides)> PartTemplate;
|
||||||
public float CurrFrame; // monotonically increasing within [LowFrame, HighFrame]
|
public float CurrFrame; // monotonically increasing within [LowFrame, HighFrame]
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Phase B.3: full animation sequencer with transition-link support.
|
||||||
|
/// Non-null when a MotionTable was successfully loaded at spawn time.
|
||||||
|
/// When present, <see cref="TickAnimations"/> delegates to this instead
|
||||||
|
/// of performing the manual slerp, and motion changes call
|
||||||
|
/// <see cref="AcDream.Core.Physics.AnimationSequencer.SetCycle"/> so
|
||||||
|
/// transition frames blend smoothly.
|
||||||
|
/// </summary>
|
||||||
|
public AcDream.Core.Physics.AnimationSequencer? Sequencer;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Phase B.2: player movement mode.
|
// Phase B.2: player movement mode.
|
||||||
|
|
@ -323,6 +339,7 @@ public sealed class GameWindow : IDisposable
|
||||||
_cameraController.ModeChanged += OnCameraModeChanged;
|
_cameraController.ModeChanged += OnCameraModeChanged;
|
||||||
|
|
||||||
_dats = new DatCollection(_datDir, DatAccessType.Read);
|
_dats = new DatCollection(_datDir, DatAccessType.Read);
|
||||||
|
_animLoader = new AcDream.Core.Physics.DatCollectionLoader(_dats);
|
||||||
|
|
||||||
uint centerLandblockId = 0xA9B4FFFFu;
|
uint centerLandblockId = 0xA9B4FFFFu;
|
||||||
Console.WriteLine($"loading world view centered on 0x{centerLandblockId:X8}");
|
Console.WriteLine($"loading world view centered on 0x{centerLandblockId:X8}");
|
||||||
|
|
@ -822,6 +839,29 @@ public sealed class GameWindow : IDisposable
|
||||||
for (int i = 0; i < meshRefs.Count; i++)
|
for (int i = 0; i < meshRefs.Count; i++)
|
||||||
template[i] = (meshRefs[i].GfxObjId, meshRefs[i].SurfaceOverrides);
|
template[i] = (meshRefs[i].GfxObjId, meshRefs[i].SurfaceOverrides);
|
||||||
|
|
||||||
|
// Phase B.3: try to build an AnimationSequencer for smooth
|
||||||
|
// transition-link blending. Requires a MotionTable in the dats.
|
||||||
|
AcDream.Core.Physics.AnimationSequencer? sequencer = null;
|
||||||
|
if (_animLoader is not null)
|
||||||
|
{
|
||||||
|
uint mtId = spawn.MotionTableId ?? (uint)setup.DefaultMotionTable;
|
||||||
|
var mtable = mtId != 0 ? _dats!.Get<DatReaderWriter.DBObjs.MotionTable>(mtId) : null;
|
||||||
|
if (mtable is not null)
|
||||||
|
{
|
||||||
|
sequencer = new AcDream.Core.Physics.AnimationSequencer(setup, mtable, _animLoader);
|
||||||
|
// Prime the sequencer on the idle cycle so it starts playing
|
||||||
|
// immediately. Use the resolved style from the spawn motion state,
|
||||||
|
// defaulting to NonCombat (0x003D).
|
||||||
|
uint spawnStyle = stanceOverride.HasValue
|
||||||
|
? ((uint)stanceOverride.Value << 16)
|
||||||
|
: ((uint)mtable.DefaultStyle & 0xFFFF0000u);
|
||||||
|
uint spawnMotion = commandOverride.HasValue
|
||||||
|
? (uint)commandOverride.Value
|
||||||
|
: (uint)mtable.StyleDefaults.GetValueOrDefault(mtable.DefaultStyle);
|
||||||
|
sequencer.SetCycle(spawnStyle, spawnMotion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_animatedEntities[entity.Id] = new AnimatedEntity
|
_animatedEntities[entity.Id] = new AnimatedEntity
|
||||||
{
|
{
|
||||||
Entity = entity,
|
Entity = entity,
|
||||||
|
|
@ -833,6 +873,7 @@ public sealed class GameWindow : IDisposable
|
||||||
Scale = scale,
|
Scale = scale,
|
||||||
PartTemplate = template,
|
PartTemplate = template,
|
||||||
CurrFrame = idleCycle.LowFrame,
|
CurrFrame = idleCycle.LowFrame,
|
||||||
|
Sequencer = sequencer,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -920,6 +961,18 @@ public sealed class GameWindow : IDisposable
|
||||||
if (!newCycleIsGood)
|
if (!newCycleIsGood)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// Phase B.3: if this entity has a sequencer, delegate the cycle switch
|
||||||
|
// to it so transition-link frames are prepended automatically. The
|
||||||
|
// sequencer's SetCycle fast-path will no-op if nothing actually changed.
|
||||||
|
if (ae.Sequencer is not null)
|
||||||
|
{
|
||||||
|
uint styleCmd = (uint)stance << 16;
|
||||||
|
uint motionCmd = command.HasValue ? (uint)command.Value : 0u;
|
||||||
|
ae.Sequencer.SetCycle(styleCmd, motionCmd);
|
||||||
|
// Keep ae.Animation/LowFrame/HighFrame in sync so the fallback
|
||||||
|
// path (and diagnostics) are accurate if the sequencer is ever removed.
|
||||||
|
}
|
||||||
|
|
||||||
ae.Animation = newCycle!.Animation;
|
ae.Animation = newCycle!.Animation;
|
||||||
ae.LowFrame = Math.Max(0, newCycle.LowFrame);
|
ae.LowFrame = Math.Max(0, newCycle.LowFrame);
|
||||||
ae.HighFrame = Math.Min(newCycle.HighFrame, newCycle.Animation.PartFrames.Count - 1);
|
ae.HighFrame = Math.Min(newCycle.HighFrame, newCycle.Animation.PartFrames.Count - 1);
|
||||||
|
|
@ -1825,104 +1878,154 @@ public sealed class GameWindow : IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Phase 6.4: advance every animated entity's frame counter by
|
/// Phase 6.4 / B.3: advance every animated entity's playback by
|
||||||
/// <paramref name="dt"/> * Framerate, wrapping around the cycle's
|
/// <paramref name="dt"/> seconds, then rebuild that entity's MeshRefs
|
||||||
/// [LowFrame..HighFrame] interval, then rebuild that entity's
|
/// from the resulting per-part transforms. Static entities (no
|
||||||
/// MeshRefs from the new frame's per-part transforms. Static
|
/// AnimatedEntity record) are untouched.
|
||||||
/// entities (no AnimatedEntity record) are untouched. The static
|
///
|
||||||
/// renderer reads the new MeshRefs on the next Draw call.
|
/// When the entity has a <see cref="AcDream.Core.Physics.AnimationSequencer"/>
|
||||||
|
/// (Phase B.3), delegates frame advancement and blending to it — this gives
|
||||||
|
/// retail-accurate quaternion slerp and smooth transition-link blending on
|
||||||
|
/// motion changes. Falls back to the Phase 6.5 manual slerp for entities
|
||||||
|
/// whose MotionTable couldn't be loaded (e.g. offline mode or a missing dat).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void TickAnimations(float dt)
|
private void TickAnimations(float dt)
|
||||||
{
|
{
|
||||||
foreach (var kv in _animatedEntities)
|
foreach (var kv in _animatedEntities)
|
||||||
{
|
{
|
||||||
var ae = kv.Value;
|
var ae = kv.Value;
|
||||||
int span = ae.HighFrame - ae.LowFrame;
|
|
||||||
if (span <= 0) continue;
|
|
||||||
|
|
||||||
ae.CurrFrame += dt * ae.Framerate;
|
// ── Phase B.3 path: sequencer-driven ────────────────────────────
|
||||||
// Wrap into [LowFrame, HighFrame]. Use a guarded modulo so
|
if (ae.Sequencer is { } seq)
|
||||||
// big dts (first frame after a stall) don't blow the loop.
|
|
||||||
if (ae.CurrFrame > ae.HighFrame)
|
|
||||||
{
|
{
|
||||||
float over = ae.CurrFrame - ae.LowFrame;
|
var transforms = seq.Advance(dt);
|
||||||
ae.CurrFrame = ae.LowFrame + (over % (span + 1));
|
int partCount = ae.PartTemplate.Count;
|
||||||
}
|
var newMeshRefs = new List<AcDream.Core.World.MeshRef>(partCount);
|
||||||
else if (ae.CurrFrame < ae.LowFrame)
|
var scaleMat = ae.Scale == 1.0f
|
||||||
{
|
? System.Numerics.Matrix4x4.Identity
|
||||||
ae.CurrFrame = ae.LowFrame;
|
: System.Numerics.Matrix4x4.CreateScale(ae.Scale);
|
||||||
}
|
|
||||||
|
|
||||||
// Phase 6.5: blend between adjacent keyframes using the fractional
|
for (int i = 0; i < partCount; i++)
|
||||||
// part of CurrFrame so the animation is smooth at any framerate
|
|
||||||
// instead of snapping to integer frame indices.
|
|
||||||
int frameIdx = (int)Math.Floor(ae.CurrFrame);
|
|
||||||
if (frameIdx < ae.LowFrame || frameIdx > ae.HighFrame
|
|
||||||
|| frameIdx >= ae.Animation.PartFrames.Count)
|
|
||||||
frameIdx = ae.LowFrame;
|
|
||||||
|
|
||||||
int nextIdx = frameIdx + 1;
|
|
||||||
if (nextIdx > ae.HighFrame || nextIdx >= ae.Animation.PartFrames.Count)
|
|
||||||
nextIdx = ae.LowFrame; // cycle wraps within [LowFrame, HighFrame]
|
|
||||||
|
|
||||||
float t = ae.CurrFrame - frameIdx;
|
|
||||||
if (t < 0f) t = 0f; else if (t > 1f) t = 1f;
|
|
||||||
|
|
||||||
var partFrames = ae.Animation.PartFrames[frameIdx].Frames;
|
|
||||||
var partFramesNext = ae.Animation.PartFrames[nextIdx].Frames;
|
|
||||||
|
|
||||||
int partCount = ae.PartTemplate.Count;
|
|
||||||
var newMeshRefs = new List<AcDream.Core.World.MeshRef>(partCount);
|
|
||||||
var scaleMat = ae.Scale == 1.0f
|
|
||||||
? System.Numerics.Matrix4x4.Identity
|
|
||||||
: System.Numerics.Matrix4x4.CreateScale(ae.Scale);
|
|
||||||
|
|
||||||
for (int i = 0; i < partCount; i++)
|
|
||||||
{
|
|
||||||
// Slerp between the current and next keyframe per part.
|
|
||||||
// Out-of-range parts get an identity transform — defensive
|
|
||||||
// for setups whose part count exceeds the animation's bone
|
|
||||||
// count.
|
|
||||||
System.Numerics.Vector3 origin;
|
|
||||||
System.Numerics.Quaternion orientation;
|
|
||||||
if (i < partFrames.Count)
|
|
||||||
{
|
{
|
||||||
var f0 = partFrames[i];
|
var pt = i < transforms.Count ? transforms[i]
|
||||||
var f1 = i < partFramesNext.Count ? partFramesNext[i] : f0;
|
: new AcDream.Core.Physics.PartTransform(
|
||||||
origin = System.Numerics.Vector3.Lerp(f0.Origin, f1.Origin, t);
|
System.Numerics.Vector3.Zero,
|
||||||
orientation = System.Numerics.Quaternion.Slerp(f0.Orientation, f1.Orientation, t);
|
System.Numerics.Quaternion.Identity);
|
||||||
|
|
||||||
|
// Per-part default scale from the Setup, matching
|
||||||
|
// SetupMesh.Flatten's composition order: scale → rotate → translate.
|
||||||
|
var defaultScale = i < ae.Setup.DefaultScale.Count
|
||||||
|
? ae.Setup.DefaultScale[i]
|
||||||
|
: System.Numerics.Vector3.One;
|
||||||
|
|
||||||
|
var partTransform =
|
||||||
|
System.Numerics.Matrix4x4.CreateScale(defaultScale) *
|
||||||
|
System.Numerics.Matrix4x4.CreateFromQuaternion(pt.Orientation) *
|
||||||
|
System.Numerics.Matrix4x4.CreateTranslation(pt.Origin);
|
||||||
|
|
||||||
|
if (ae.Scale != 1.0f)
|
||||||
|
partTransform = partTransform * scaleMat;
|
||||||
|
|
||||||
|
var template = ae.PartTemplate[i];
|
||||||
|
newMeshRefs.Add(new AcDream.Core.World.MeshRef(template.GfxObjId, partTransform)
|
||||||
|
{
|
||||||
|
SurfaceOverrides = template.SurfaceOverrides,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
origin = System.Numerics.Vector3.Zero;
|
|
||||||
orientation = System.Numerics.Quaternion.Identity;
|
|
||||||
}
|
|
||||||
var frame = new DatReaderWriter.Types.Frame { Origin = origin, Orientation = orientation };
|
|
||||||
|
|
||||||
// Per-part default scale from the Setup, matching SetupMesh.Flatten's
|
ae.Entity.MeshRefs = newMeshRefs;
|
||||||
// composition order: scale → rotate → translate.
|
continue;
|
||||||
var defaultScale = i < ae.Setup.DefaultScale.Count
|
|
||||||
? ae.Setup.DefaultScale[i]
|
|
||||||
: System.Numerics.Vector3.One;
|
|
||||||
|
|
||||||
var partTransform =
|
|
||||||
System.Numerics.Matrix4x4.CreateScale(defaultScale) *
|
|
||||||
System.Numerics.Matrix4x4.CreateFromQuaternion(frame.Orientation) *
|
|
||||||
System.Numerics.Matrix4x4.CreateTranslation(frame.Origin);
|
|
||||||
|
|
||||||
// Bake the entity's ObjScale on top, matching the hydration
|
|
||||||
// order (PartTransform * scaleMat) — see comment in OnLiveEntitySpawned.
|
|
||||||
if (ae.Scale != 1.0f)
|
|
||||||
partTransform = partTransform * scaleMat;
|
|
||||||
|
|
||||||
var template = ae.PartTemplate[i];
|
|
||||||
newMeshRefs.Add(new AcDream.Core.World.MeshRef(template.GfxObjId, partTransform)
|
|
||||||
{
|
|
||||||
SurfaceOverrides = template.SurfaceOverrides,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ae.Entity.MeshRefs = newMeshRefs;
|
// ── Phase 6.4 / 6.5 fallback path: manual slerp ─────────────────
|
||||||
|
{
|
||||||
|
int span = ae.HighFrame - ae.LowFrame;
|
||||||
|
if (span <= 0) continue;
|
||||||
|
|
||||||
|
ae.CurrFrame += dt * ae.Framerate;
|
||||||
|
// Wrap into [LowFrame, HighFrame]. Use a guarded modulo so
|
||||||
|
// big dts (first frame after a stall) don't blow the loop.
|
||||||
|
if (ae.CurrFrame > ae.HighFrame)
|
||||||
|
{
|
||||||
|
float over = ae.CurrFrame - ae.LowFrame;
|
||||||
|
ae.CurrFrame = ae.LowFrame + (over % (span + 1));
|
||||||
|
}
|
||||||
|
else if (ae.CurrFrame < ae.LowFrame)
|
||||||
|
{
|
||||||
|
ae.CurrFrame = ae.LowFrame;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase 6.5: blend between adjacent keyframes using the fractional
|
||||||
|
// part of CurrFrame so the animation is smooth at any framerate
|
||||||
|
// instead of snapping to integer frame indices.
|
||||||
|
int frameIdx = (int)Math.Floor(ae.CurrFrame);
|
||||||
|
if (frameIdx < ae.LowFrame || frameIdx > ae.HighFrame
|
||||||
|
|| frameIdx >= ae.Animation.PartFrames.Count)
|
||||||
|
frameIdx = ae.LowFrame;
|
||||||
|
|
||||||
|
int nextIdx = frameIdx + 1;
|
||||||
|
if (nextIdx > ae.HighFrame || nextIdx >= ae.Animation.PartFrames.Count)
|
||||||
|
nextIdx = ae.LowFrame; // cycle wraps within [LowFrame, HighFrame]
|
||||||
|
|
||||||
|
float t = ae.CurrFrame - frameIdx;
|
||||||
|
if (t < 0f) t = 0f; else if (t > 1f) t = 1f;
|
||||||
|
|
||||||
|
var partFrames = ae.Animation.PartFrames[frameIdx].Frames;
|
||||||
|
var partFramesNext = ae.Animation.PartFrames[nextIdx].Frames;
|
||||||
|
|
||||||
|
int partCount = ae.PartTemplate.Count;
|
||||||
|
var newMeshRefs = new List<AcDream.Core.World.MeshRef>(partCount);
|
||||||
|
var scaleMat = ae.Scale == 1.0f
|
||||||
|
? System.Numerics.Matrix4x4.Identity
|
||||||
|
: System.Numerics.Matrix4x4.CreateScale(ae.Scale);
|
||||||
|
|
||||||
|
for (int i = 0; i < partCount; i++)
|
||||||
|
{
|
||||||
|
// Slerp between the current and next keyframe per part.
|
||||||
|
// Out-of-range parts get an identity transform — defensive
|
||||||
|
// for setups whose part count exceeds the animation's bone
|
||||||
|
// count.
|
||||||
|
System.Numerics.Vector3 origin;
|
||||||
|
System.Numerics.Quaternion orientation;
|
||||||
|
if (i < partFrames.Count)
|
||||||
|
{
|
||||||
|
var f0 = partFrames[i];
|
||||||
|
var f1 = i < partFramesNext.Count ? partFramesNext[i] : f0;
|
||||||
|
origin = System.Numerics.Vector3.Lerp(f0.Origin, f1.Origin, t);
|
||||||
|
orientation = System.Numerics.Quaternion.Slerp(f0.Orientation, f1.Orientation, t);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
origin = System.Numerics.Vector3.Zero;
|
||||||
|
orientation = System.Numerics.Quaternion.Identity;
|
||||||
|
}
|
||||||
|
var frame = new DatReaderWriter.Types.Frame { Origin = origin, Orientation = orientation };
|
||||||
|
|
||||||
|
// Per-part default scale from the Setup, matching SetupMesh.Flatten's
|
||||||
|
// composition order: scale → rotate → translate.
|
||||||
|
var defaultScale = i < ae.Setup.DefaultScale.Count
|
||||||
|
? ae.Setup.DefaultScale[i]
|
||||||
|
: System.Numerics.Vector3.One;
|
||||||
|
|
||||||
|
var partTransform =
|
||||||
|
System.Numerics.Matrix4x4.CreateScale(defaultScale) *
|
||||||
|
System.Numerics.Matrix4x4.CreateFromQuaternion(frame.Orientation) *
|
||||||
|
System.Numerics.Matrix4x4.CreateTranslation(frame.Origin);
|
||||||
|
|
||||||
|
// Bake the entity's ObjScale on top, matching the hydration
|
||||||
|
// order (PartTransform * scaleMat) — see comment in OnLiveEntitySpawned.
|
||||||
|
if (ae.Scale != 1.0f)
|
||||||
|
partTransform = partTransform * scaleMat;
|
||||||
|
|
||||||
|
var template = ae.PartTemplate[i];
|
||||||
|
newMeshRefs.Add(new AcDream.Core.World.MeshRef(template.GfxObjId, partTransform)
|
||||||
|
{
|
||||||
|
SurfaceOverrides = template.SurfaceOverrides,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ae.Entity.MeshRefs = newMeshRefs;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1968,6 +2071,16 @@ public sealed class GameWindow : IDisposable
|
||||||
for (int i = 0; i < pe.MeshRefs.Count; i++)
|
for (int i = 0; i < pe.MeshRefs.Count; i++)
|
||||||
template[i] = (pe.MeshRefs[i].GfxObjId, pe.MeshRefs[i].SurfaceOverrides);
|
template[i] = (pe.MeshRefs[i].GfxObjId, pe.MeshRefs[i].SurfaceOverrides);
|
||||||
|
|
||||||
|
// Phase B.3: try to attach an AnimationSequencer for the player.
|
||||||
|
AcDream.Core.Physics.AnimationSequencer? playerSeq = null;
|
||||||
|
if (_animLoader is not null)
|
||||||
|
{
|
||||||
|
uint mtId = _playerMotionTableId ?? (uint)setup.DefaultMotionTable;
|
||||||
|
var mtable = mtId != 0 ? _dats.Get<DatReaderWriter.DBObjs.MotionTable>(mtId) : null;
|
||||||
|
if (mtable is not null)
|
||||||
|
playerSeq = new AcDream.Core.Physics.AnimationSequencer(setup, mtable, _animLoader);
|
||||||
|
}
|
||||||
|
|
||||||
ae = new AnimatedEntity
|
ae = new AnimatedEntity
|
||||||
{
|
{
|
||||||
Entity = pe,
|
Entity = pe,
|
||||||
|
|
@ -1979,6 +2092,7 @@ public sealed class GameWindow : IDisposable
|
||||||
Scale = 1f,
|
Scale = 1f,
|
||||||
PartTemplate = template,
|
PartTemplate = template,
|
||||||
CurrFrame = 0f,
|
CurrFrame = 0f,
|
||||||
|
Sequencer = playerSeq,
|
||||||
};
|
};
|
||||||
_animatedEntities[pe.Id] = ae;
|
_animatedEntities[pe.Id] = ae;
|
||||||
}
|
}
|
||||||
|
|
@ -1998,6 +2112,16 @@ public sealed class GameWindow : IDisposable
|
||||||
|
|
||||||
if (cycle is null || cycle.Framerate == 0f || cycle.HighFrame <= cycle.LowFrame) return;
|
if (cycle is null || cycle.Framerate == 0f || cycle.HighFrame <= cycle.LowFrame) return;
|
||||||
|
|
||||||
|
// Phase B.3: if the entity has a sequencer, route the motion change
|
||||||
|
// through it so transition-link frames blend smoothly. NonCombat
|
||||||
|
// stance is 0x003D0000 in full style-command form.
|
||||||
|
if (ae.Sequencer is not null)
|
||||||
|
{
|
||||||
|
const uint NonCombatStyleFull = 0x003D0000u;
|
||||||
|
uint motionCmd = (uint)cmdOverride;
|
||||||
|
ae.Sequencer.SetCycle(NonCombatStyleFull, motionCmd);
|
||||||
|
}
|
||||||
|
|
||||||
ae.Animation = cycle.Animation;
|
ae.Animation = cycle.Animation;
|
||||||
ae.LowFrame = Math.Max(0, cycle.LowFrame);
|
ae.LowFrame = Math.Max(0, cycle.LowFrame);
|
||||||
ae.HighFrame = Math.Min(cycle.HighFrame, cycle.Animation.PartFrames.Count - 1);
|
ae.HighFrame = Math.Min(cycle.HighFrame, cycle.Animation.PartFrames.Count - 1);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue