Problem
ImmersiveGol needs to replay a one-shot kicking animation and release a Jolt ball when playback crosses the foot-contact time. It also needs to align the shooter with the ball by sampling the contact joint before playback, without changing the visible pose.
The existing animation API needs explicit support for restarting an already selected clip, reading playback time, and sampling a joint in world space. The compiled sampler also wraps non-looping clips at their duration, causing a completed kick to jump back to its initial pose. Restarting should preserve warmed compiled animation caches when the clip configuration has not changed.
Local implementation to integrate
The following demo-related changes are currently uncommitted and unstaged in the local engine checkout:
Sources/UntoldEngine/Systems/AnimationSystem.swift
restartAnimation(entityId:name:repeats:withPause:): restart from time zero, including when the same clip is already selected; apply to matching animated descendants, cancel transitions, and reset root-motion history. Preserve compiled caches unless the looping configuration changes.
getAnimationPlaybackTime(entityId:): return the selected clip's current time from the first animated part of a root/split character, or nil when unavailable.
sampleAnimationJointPosition(entityId:name:jointName:time:): sample a named joint in world space using the compiled pose, joint hierarchy, rest scales, and entity world transform. Preserve current playback time and pause state; return nil for an unavailable joint.
Sources/UntoldEngine/Animation/ClipSampler.swift: clamp non-repeating clip time to its duration; retain wrapping for repeating clips.
Tests/UntoldEngineTests/AnimationCompiledSamplerTests.swift: add coverage for final-pose holding, restarting the selected clip, sampling under world translation/rotation/scale without changing playback, and preserving/invalidation of compiled caches according to looping changes.
The staged shadow/shader/render-pass changes and compiled Metal libraries in the same working tree are outside this issue's scope.
Demo integration and evidence
The demo uses forwardstrike.untoldanim; the reviewed foot-contact sample is frame 13 (about 0.433 seconds at 30 fps), rather than frame 15. It samples RightToeBase to align the player in X/Z while keeping the ball on the floor, then releases the ball when actual animation playback reaches the contact time. A countdown precedes each kick, and repeated kicks use the restart helper.
Headset testing confirmed that the grounded ball and frame-13 launch work. Earlier validation passed all 18 focused animation sampler tests, including the four added tests, and the demo's visionOS simulator build passed. These results are from prior validation; engine tests were not rerun when creating this issue.
The helpers preserve compiled caches to avoid unnecessary recompilation. Fast glove movement still has a separately reported disappearance/stuttering problem; its cause has not been established, and this change does not claim to resolve it.
Acceptance criteria
Problem
ImmersiveGol needs to replay a one-shot kicking animation and release a Jolt ball when playback crosses the foot-contact time. It also needs to align the shooter with the ball by sampling the contact joint before playback, without changing the visible pose.
The existing animation API needs explicit support for restarting an already selected clip, reading playback time, and sampling a joint in world space. The compiled sampler also wraps non-looping clips at their duration, causing a completed kick to jump back to its initial pose. Restarting should preserve warmed compiled animation caches when the clip configuration has not changed.
Local implementation to integrate
The following demo-related changes are currently uncommitted and unstaged in the local engine checkout:
Sources/UntoldEngine/Systems/AnimationSystem.swiftrestartAnimation(entityId:name:repeats:withPause:): restart from time zero, including when the same clip is already selected; apply to matching animated descendants, cancel transitions, and reset root-motion history. Preserve compiled caches unless the looping configuration changes.getAnimationPlaybackTime(entityId:): return the selected clip's current time from the first animated part of a root/split character, or nil when unavailable.sampleAnimationJointPosition(entityId:name:jointName:time:): sample a named joint in world space using the compiled pose, joint hierarchy, rest scales, and entity world transform. Preserve current playback time and pause state; return nil for an unavailable joint.Sources/UntoldEngine/Animation/ClipSampler.swift: clamp non-repeating clip time to its duration; retain wrapping for repeating clips.Tests/UntoldEngineTests/AnimationCompiledSamplerTests.swift: add coverage for final-pose holding, restarting the selected clip, sampling under world translation/rotation/scale without changing playback, and preserving/invalidation of compiled caches according to looping changes.The staged shadow/shader/render-pass changes and compiled Metal libraries in the same working tree are outside this issue's scope.
Demo integration and evidence
The demo uses
forwardstrike.untoldanim; the reviewed foot-contact sample is frame 13 (about 0.433 seconds at 30 fps), rather than frame 15. It samplesRightToeBaseto align the player in X/Z while keeping the ball on the floor, then releases the ball when actual animation playback reaches the contact time. A countdown precedes each kick, and repeated kicks use the restart helper.Headset testing confirmed that the grounded ball and frame-13 launch work. Earlier validation passed all 18 focused animation sampler tests, including the four added tests, and the demo's visionOS simulator build passed. These results are from prior validation; engine tests were not rerun when creating this issue.
The helpers preserve compiled caches to avoid unnecessary recompilation. Fast glove movement still has a separately reported disappearance/stuttering problem; its cause has not been established, and this change does not claim to resolve it.
Acceptance criteria