A pane's blend slot keeps the outgoing sequence on its own clock (#1881)
A held paperdoll turn re-arms a shuffle once per cycle. When it fell inside Stand's half-blend, the pane reused the one track it held for that clip, so the fading shuffle jumped back about 3 frames every 0.53 s, and the fade's end restarted the primary. 1.12.1's arm (`0x7121a0`) copies the outgoing primary into the blend slot with its own clock (`0x7125d6`). A refused arm (`0x7125c9`/`0x7125d4`) keeps that slot, the latched shortcut (`0x712549`) leaves it alone, and the slot ends on its own (`0x7147b1`), weighted by the smoothstep (`0x714880`). Now each sequence has a second graph node, and the pane's blend slot runs on whichever node the primary is not on. A refused arm leaves it untouched, and the window's end stops only the secondary. Headless player tests cover the held turn, the fade end and the shortcut. Fixes #1845 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,7 @@ fn clip(anim_id: u16, node: u32, looping: bool) -> AnimClip {
|
||||
anim_id,
|
||||
seq_index: 0,
|
||||
node: AnimationNodeIndex::new(node as usize),
|
||||
blend_node: None,
|
||||
looping,
|
||||
duration: 1.0,
|
||||
move_speed: 0.0,
|
||||
@@ -284,6 +285,7 @@ fn spawn_fidgeter(app: &mut App) -> (Entity, Vec<AnimationNodeIndex>) {
|
||||
.add(graph);
|
||||
let seq = |id: u16, node, duration: f32, frequency: u16| AnimClip {
|
||||
node,
|
||||
blend_node: None,
|
||||
duration,
|
||||
blend_time: 0.0,
|
||||
frequency,
|
||||
@@ -3113,6 +3115,7 @@ fn spawn_vendor(app: &mut App) -> (Entity, Vec<AnimationNodeIndex>) {
|
||||
.add(graph);
|
||||
let seq = |id: u16, node, duration: f32, blend_time: f32| AnimClip {
|
||||
node,
|
||||
blend_node: None,
|
||||
duration,
|
||||
blend_time,
|
||||
replay: (0, 0),
|
||||
|
||||
@@ -39,6 +39,7 @@ fn spawn_orc(app: &mut App) -> (Entity, [AnimationNodeIndex; 3]) {
|
||||
.add(graph);
|
||||
let seq = |id: u16, node, looping| AnimClip {
|
||||
node,
|
||||
blend_node: None,
|
||||
duration: SPAN,
|
||||
blend_time: 0.0,
|
||||
replay: (0, 0),
|
||||
@@ -392,6 +393,7 @@ fn spawn_runner(app: &mut App) -> (Entity, [AnimationNodeIndex; 6]) {
|
||||
.add(graph);
|
||||
let seq = |id: u16, node, looping| AnimClip {
|
||||
node,
|
||||
blend_node: None,
|
||||
duration: SPAN,
|
||||
blend_time: 0.0,
|
||||
replay: (0, 0),
|
||||
|
||||
@@ -413,6 +413,7 @@ mod tests {
|
||||
anim_id: 11,
|
||||
seq_index: 38,
|
||||
node: AnimationNodeIndex::new(7),
|
||||
blend_node: None,
|
||||
looping: true,
|
||||
duration: 0.5,
|
||||
move_speed: 0.0,
|
||||
|
||||
@@ -201,6 +201,7 @@ mod tests {
|
||||
anim_id: 0,
|
||||
seq_index: 0,
|
||||
node,
|
||||
blend_node: None,
|
||||
looping: true,
|
||||
duration: DUR,
|
||||
move_speed: 0.0,
|
||||
|
||||
@@ -16,6 +16,7 @@ fn clip(anim_id: u16, move_speed: f32) -> AnimClip {
|
||||
anim_id,
|
||||
seq_index: 0,
|
||||
node: AnimationNodeIndex::new(0),
|
||||
blend_node: None,
|
||||
looping: true,
|
||||
duration: 1.0,
|
||||
move_speed,
|
||||
|
||||
@@ -154,6 +154,7 @@ mod tests {
|
||||
anim_id,
|
||||
seq_index: node,
|
||||
node: AnimationNodeIndex::new(node),
|
||||
blend_node: None,
|
||||
looping,
|
||||
duration: 1.0,
|
||||
move_speed: 0.0,
|
||||
|
||||
@@ -871,6 +871,7 @@ mod tests {
|
||||
anim_id: id,
|
||||
seq_index: 0,
|
||||
node,
|
||||
blend_node: None,
|
||||
// The crate's clips are bit-0-clear bands: the kernel loops them.
|
||||
looping: true,
|
||||
duration: dur,
|
||||
|
||||
@@ -1192,6 +1192,7 @@ mod tests {
|
||||
anim_id: 0,
|
||||
seq_index: 0,
|
||||
node: bevy::animation::graph::AnimationNodeIndex::new(1),
|
||||
blend_node: None,
|
||||
looping: true,
|
||||
duration: 0.5,
|
||||
move_speed: 0.0,
|
||||
|
||||
@@ -2624,6 +2624,7 @@ mod tests {
|
||||
anim_id: 0,
|
||||
seq_index: 0,
|
||||
node: node(STAND_NODE),
|
||||
blend_node: None,
|
||||
looping: true,
|
||||
duration: 2.0,
|
||||
move_speed: 0.0,
|
||||
|
||||
@@ -178,7 +178,8 @@ struct StandIn {
|
||||
pub(super) struct Turn {
|
||||
/// The serial of the engine arm last played.
|
||||
played: Option<u64>,
|
||||
/// The node the primary runs; the spawn's idle Stand until the first arm (`0x70ebd0`).
|
||||
/// The node the primary runs, either of its clip's two tracks; the spawn's idle Stand until
|
||||
/// the first arm (`0x70ebd0`).
|
||||
pub(super) playing: Option<AnimationNodeIndex>,
|
||||
pub(super) fade: Option<Fade>,
|
||||
/// When the primary's window closes, `start + span·R` (`0x712692`); `None` once reported.
|
||||
@@ -187,7 +188,8 @@ pub(super) struct Turn {
|
||||
completed: bool,
|
||||
}
|
||||
|
||||
/// The secondary blend slot (`[blk+0xc4]`, `0x7125d6`): the pose an arm replaced, faded out by λ.
|
||||
/// The secondary blend slot (`[blk+0xc4]`, `0x7125d6`): the pose an arm replaced, on its own clock
|
||||
/// and a track the primary is not using, faded out by λ.
|
||||
#[derive(Clone, Copy)]
|
||||
pub(super) struct Fade {
|
||||
pub(super) node: AnimationNodeIndex,
|
||||
@@ -202,6 +204,13 @@ fn fade_frac(fade: &Fade, now: f64) -> f32 {
|
||||
((fade.until - now) / f64::from(fade.span)) as f32
|
||||
}
|
||||
|
||||
/// Empty the secondary slot, stopping its track.
|
||||
fn clear_fade(player: &mut AnimationPlayer, turn: &mut Turn) {
|
||||
if let Some(old) = turn.fade.take() {
|
||||
player.stop(old.node);
|
||||
}
|
||||
}
|
||||
|
||||
/// One engine arm, `0x7121a0(bone −1, id, variation −1, offset, 1.0, blend, 1)`. The id resolves
|
||||
/// through the model's fallback chain (`0x711bf0` at `0x71245d`); a variation is rolled and the
|
||||
/// play count drawn (`0x712692`). False only when nothing resolves (`0x712484`).
|
||||
@@ -222,100 +231,81 @@ fn play_arm(
|
||||
return false;
|
||||
};
|
||||
let replays = rng.replay_count(clip.replay);
|
||||
let (node, looping, blend_time) = (clip.node, clip.looping, clip.blend_time.max(0.0));
|
||||
let same = outgoing == Some(node);
|
||||
// The shortcut (`0x712553`): a blended arm of the sequence that already ran to its end
|
||||
// restarts it with no blend slot.
|
||||
let shortcut = blend && same && turn.completed;
|
||||
if !blend || shortcut {
|
||||
let blend_time = clip.blend_time.max(0.0);
|
||||
// The primary's record (`[blk+0xa4]`), on either of its clip's tracks.
|
||||
let same = outgoing.is_some_and(|n| n == clip.node || Some(n) == clip.blend_node);
|
||||
if !blend {
|
||||
// No blend: the secondary slot is cleared (`0x712635`, `[blk+0xd0] = −1`).
|
||||
if let Some(old) = turn.fade.take() {
|
||||
if old.node != node {
|
||||
player.stop(old.node);
|
||||
}
|
||||
}
|
||||
}
|
||||
// The half-blend refusal (`0x7125c9`/`0x7125d4`): a running blend with λ > 0.5 keeps its
|
||||
// secondary and window, and the outgoing primary is dropped.
|
||||
let refused = blend
|
||||
&& !shortcut
|
||||
&& turn
|
||||
.fade
|
||||
.is_some_and(|f| crate::creature_anim::select::blend_lambda(fade_frac(&f, now)) > 0.5);
|
||||
if refused {
|
||||
if let Some(out) = outgoing.filter(|o| *o != node) {
|
||||
player.stop(out);
|
||||
}
|
||||
} else if blend && !shortcut {
|
||||
// One secondary slot: a fade in flight is displaced, unless it is the node replaying.
|
||||
if let Some(old) = turn.fade.take() {
|
||||
if old.node != node && Some(old.node) != outgoing {
|
||||
player.stop(old.node);
|
||||
}
|
||||
}
|
||||
}
|
||||
let fade = if refused {
|
||||
turn.fade
|
||||
} else if blend && !shortcut && !same && blend_time > 0.0 {
|
||||
outgoing.map(|node| Fade {
|
||||
clear_fade(player, turn);
|
||||
} else if same && turn.completed {
|
||||
// The shortcut (`0x712553` → `0x71262d`): the sequence that ran to its end is written
|
||||
// again as the primary alone; the secondary and its window stay as they are.
|
||||
} else if turn
|
||||
.fade
|
||||
.is_none_or(|f| crate::creature_anim::select::blend_lambda(fade_frac(&f, now)) <= 0.5)
|
||||
{
|
||||
// The copy (`0x7125d6`): the outgoing primary becomes the secondary, its clock kept, over
|
||||
// the incoming clip's blendTime (`0x7125f2`). A zero blendTime ends it on the next pass.
|
||||
clear_fade(player, turn);
|
||||
turn.fade = outgoing.filter(|_| blend_time > 0.0).map(|node| Fade {
|
||||
node,
|
||||
until: now + f64::from(blend_time),
|
||||
span: blend_time,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// A blended re-arm of the running sequence, latch clear, fades from that sequence on its own
|
||||
// clock (`0x71255f` → `0x7125d6`). The player holds one track per clip, so the track runs on
|
||||
// through the window instead: no restart, no jump.
|
||||
let continues = blend && same && !shortcut && !refused;
|
||||
{
|
||||
// Set explicitly: `play` is idempotent on a live node, which would keep its old clock and
|
||||
// weight.
|
||||
let active = player.play(node);
|
||||
active.set_repeat(if looping {
|
||||
RepeatAnimation::Forever
|
||||
} else {
|
||||
RepeatAnimation::Never
|
||||
});
|
||||
active.set_speed(1.0);
|
||||
if !continues {
|
||||
active.seek_to(offset);
|
||||
}
|
||||
// λ = 1 on a blended arm's first frame, so the pose does not move on the frame it lands.
|
||||
active.set_weight(if fade.is_some() { 0.0 } else { 1.0 });
|
||||
}
|
||||
if fade.is_none() {
|
||||
if let Some(out) = outgoing.filter(|o| *o != node) {
|
||||
player.stop(out);
|
||||
// Else the half-blend refusal (`0x7125c9`/`0x7125d4`): λ > 0.5 keeps the secondary and its
|
||||
// window, and the primary alone is written.
|
||||
// The primary runs on the clip's track the secondary is not on, so a re-arm of the sequence
|
||||
// the secondary holds leaves that clock alone.
|
||||
let node = match (turn.fade, clip.blend_node) {
|
||||
(Some(f), Some(twin)) if f.node == clip.node => twin,
|
||||
(Some(f), None) if f.node == clip.node => {
|
||||
// A model built with one track per clip: the arm takes it, and the secondary ends.
|
||||
turn.fade = None;
|
||||
clip.node
|
||||
}
|
||||
_ => clip.node,
|
||||
};
|
||||
// The primary write replaces the outgoing primary (`0x712692`), unless the copy kept it.
|
||||
if let Some(out) = outgoing.filter(|&o| o != node && turn.fade.is_none_or(|f| f.node != o)) {
|
||||
player.stop(out);
|
||||
}
|
||||
let active = player.start(node);
|
||||
active.set_repeat(if clip.looping {
|
||||
RepeatAnimation::Forever
|
||||
} else {
|
||||
RepeatAnimation::Never
|
||||
});
|
||||
active.set_speed(1.0);
|
||||
active.seek_to(offset);
|
||||
// λ = 1 on a blended arm's first frame, so the pose does not move on the frame it lands.
|
||||
active.set_weight(if turn.fade.is_some() { 0.0 } else { 1.0 });
|
||||
turn.playing = Some(node);
|
||||
turn.fade = fade;
|
||||
turn.completed = false;
|
||||
turn.ends = Some(now - f64::from(offset) + f64::from(clip.duration) * f64::from(replays));
|
||||
true
|
||||
}
|
||||
|
||||
/// Advance the fade: λ, smoothstep of the window still to run (`0x714880`), weights the outgoing
|
||||
/// pose and the incoming takes `1 − λ`.
|
||||
/// Advance the fade: λ, smoothstep of the window still to run (`0x714880`), weights the secondary
|
||||
/// and the primary takes `1 − λ`. At the window's end (`0x7147b1`) the secondary alone stops.
|
||||
fn advance_fade(player: &mut AnimationPlayer, turn: &mut Turn, now: f64) {
|
||||
let Some(fade) = turn.fade else {
|
||||
return;
|
||||
};
|
||||
let frac = fade_frac(&fade, now);
|
||||
if frac <= 0.0 {
|
||||
let lambda = if frac <= 0.0 {
|
||||
turn.fade = None;
|
||||
player.stop(fade.node);
|
||||
if let Some(cur) = turn.playing {
|
||||
player.play(cur).set_weight(1.0);
|
||||
}
|
||||
0.0
|
||||
} else {
|
||||
let lambda = crate::creature_anim::select::blend_lambda(frac);
|
||||
player.play(fade.node).set_weight(lambda);
|
||||
if let Some(cur) = turn.playing {
|
||||
player.play(cur).set_weight(1.0 - lambda);
|
||||
if let Some(out) = player.animation_mut(fade.node) {
|
||||
out.set_weight(lambda);
|
||||
}
|
||||
lambda
|
||||
};
|
||||
if let Some(cur) = turn.playing.and_then(|n| player.animation_mut(n)) {
|
||||
cur.set_weight(1.0 - lambda);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ fn turning_model() -> benilla_assets::ModelAnimations {
|
||||
anim_id,
|
||||
seq_index: 0,
|
||||
node: AnimationNodeIndex::new(node),
|
||||
// The blend slot's own track of each, nodes 5 to 8.
|
||||
blend_node: Some(AnimationNodeIndex::new(node + 4)),
|
||||
looping: true,
|
||||
duration: 0.5,
|
||||
move_speed: 0.0,
|
||||
@@ -316,11 +318,12 @@ fn an_arm_reports_its_natural_end_once() {
|
||||
}
|
||||
|
||||
/// `0x7121a0` resolves an id the model lacks through its fallback (`0x711bf0`, default Stand),
|
||||
/// so a pet with no shuffles plays Stand for one; the paint's Stand return then re-arms the
|
||||
/// running sequence with the latch clear, which blends (`0x71255f`) rather than snapping the idle
|
||||
/// back to its first frame.
|
||||
/// so a pet with no shuffles plays Stand for one. That re-arm of the running Stand, latch clear,
|
||||
/// copies it to the secondary on its own clock (`0x71255f` → `0x7125d6`) and restarts the primary
|
||||
/// on the clip's other track; the paint's Stand return, inside that blend's first half, is refused
|
||||
/// (`0x7125d4`) and restarts the primary alone while the idle fades on.
|
||||
#[test]
|
||||
fn a_display_with_no_shuffle_turns_on_stand_and_returns_without_a_snap() {
|
||||
fn a_display_with_no_shuffle_turns_on_stand_and_fades_from_the_idles_own_clock() {
|
||||
let mut anims = turning_model();
|
||||
anims.clips.retain(|c| c.anim_id == ANIM_STAND);
|
||||
// The model's own baked resolution: both shuffles fall back to Stand.
|
||||
@@ -340,17 +343,131 @@ fn a_display_with_no_shuffle_turns_on_stand_and_returns_without_a_snap() {
|
||||
c.step(arm(ANIM_STAND, 1, true), 0.6); // the idle's fade done
|
||||
c.player.play(node(2)).seek_to(0.3);
|
||||
c.step(arm(ANIM_SHUFFLE_LEFT, 2, true), 1.0);
|
||||
assert_eq!(c.turn.playing, Some(node(2)), "the substitute plays");
|
||||
assert_eq!(
|
||||
c.turn.playing,
|
||||
Some(node(6)),
|
||||
"the substitute restarts on Stand's second track"
|
||||
);
|
||||
assert!(c.turn.ends.is_some(), "an arm that played owes its end");
|
||||
// The paint's return, 100 ms on.
|
||||
assert_eq!(c.turn.fade.map(|f| (f.node, f.until)), Some((node(2), 1.5)));
|
||||
// The paint's return, 110 ms on.
|
||||
c.step(arm(ANIM_STAND, 3, true), 1.11);
|
||||
assert!(c.turn.fade.is_none(), "nothing else to fade from");
|
||||
assert_eq!(weight(&c.player, 2), Some(1.0));
|
||||
assert_eq!(c.turn.playing, Some(node(6)));
|
||||
assert_eq!(
|
||||
c.turn.fade.map(|f| (f.node, f.until)),
|
||||
Some((node(2), 1.5)),
|
||||
"the refused arm keeps the window"
|
||||
);
|
||||
assert_eq!(
|
||||
c.player.animation(node(2)).map(|a| a.seek_time()),
|
||||
Some(0.3),
|
||||
"the idle runs on, no snap to frame 0"
|
||||
"the idle fades out on its own clock, no snap to frame 0"
|
||||
);
|
||||
assert_eq!(
|
||||
c.player.animation(node(6)).map(|a| a.seek_time()),
|
||||
Some(0.0)
|
||||
);
|
||||
let (out, inc) = (weight(&c.player, 2).unwrap(), weight(&c.player, 6).unwrap());
|
||||
assert!((out + inc - 1.0).abs() < 1e-5 && out > 0.5, "{out} / {inc}");
|
||||
}
|
||||
|
||||
/// A held arrow, frame by frame: the shuffle runs to its end, the callback arms Stand (`0x5062e4`),
|
||||
/// which copies the shuffle to the secondary (`0x7125d6`), and the next `SetRotation` re-arms the
|
||||
/// shuffle inside Stand's first half, refused (`0x7125d4`). The secondary keeps the old shuffle on
|
||||
/// its clock while the new one runs from 0 on the clip's other track.
|
||||
fn held_turn() -> Copy {
|
||||
let mut c = Copy::standing();
|
||||
let first = arm(ANIM_SHUFFLE_RIGHT, 2, true);
|
||||
c.step(first, 1.0);
|
||||
c.step(first, 1.3); // the shuffle's own fade done
|
||||
c.player.play(node(4)).seek_to(0.45);
|
||||
assert_eq!(c.step(first, 1.5), Some(2), "its window closes");
|
||||
c.step(arm(ANIM_STAND, 3, true), 1.517);
|
||||
c.step(arm(ANIM_SHUFFLE_RIGHT, 4, true), 1.533);
|
||||
c
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_held_turns_rearm_inside_stands_blend_keeps_the_old_shuffle_on_its_own_clock() {
|
||||
let c = held_turn();
|
||||
let fade = c.turn.fade.expect("Stand's fade still runs");
|
||||
assert_eq!(fade.node, node(4), "fading the old shuffle");
|
||||
assert!(
|
||||
(fade.until - 2.017).abs() < 1e-9,
|
||||
"on Stand's window, untouched"
|
||||
);
|
||||
assert_eq!(
|
||||
c.player.animation(node(4)).map(|a| a.seek_time()),
|
||||
Some(0.45),
|
||||
"the old shuffle is not seeked back"
|
||||
);
|
||||
assert_eq!(c.turn.playing, Some(node(8)), "the new one is on the twin");
|
||||
assert_eq!(
|
||||
c.player.animation(node(8)).map(|a| a.seek_time()),
|
||||
Some(0.0)
|
||||
);
|
||||
let (out, inc) = (weight(&c.player, 4).unwrap(), weight(&c.player, 8).unwrap());
|
||||
assert!(
|
||||
out > 0.99 && (out + inc - 1.0).abs() < 1e-5,
|
||||
"{out} / {inc}"
|
||||
);
|
||||
assert_eq!(
|
||||
weight(&c.player, 2),
|
||||
None,
|
||||
"the overwritten Stand is dropped"
|
||||
);
|
||||
}
|
||||
|
||||
/// At the window's end (`0x7147b1`) the secondary slot alone is emptied; the primary runs on.
|
||||
#[test]
|
||||
fn the_fade_window_ends_the_secondary_alone() {
|
||||
let mut c = held_turn();
|
||||
c.player.play(node(8)).seek_to(0.3);
|
||||
c.step(arm(ANIM_SHUFFLE_RIGHT, 4, true), 2.02);
|
||||
assert!(c.turn.fade.is_none());
|
||||
assert_eq!(weight(&c.player, 4), None, "the old shuffle is stopped");
|
||||
let primary = c
|
||||
.player
|
||||
.animation(node(8))
|
||||
.expect("the primary still plays");
|
||||
assert_eq!(
|
||||
(primary.seek_time(), primary.weight(), primary.repeat_mode()),
|
||||
(0.3, 1.0, RepeatAnimation::Forever),
|
||||
"on its clock, whole, still looping"
|
||||
);
|
||||
}
|
||||
|
||||
/// The shortcut (`0x712553` → `0x71262d`) writes the primary alone: a fade in flight keeps its
|
||||
/// slot, clock and window.
|
||||
#[test]
|
||||
fn a_latched_same_sequence_rearm_leaves_a_live_fade_alone() {
|
||||
let mut c = Copy::standing();
|
||||
// 400 ms in, the shuffle's window closes before its own 0.25 s fade from Stand does.
|
||||
let late = PaneArm {
|
||||
offset_ms: 400,
|
||||
..arm(ANIM_SHUFFLE_LEFT, 2, true)
|
||||
};
|
||||
c.step(late, 1.0);
|
||||
c.player.play(node(2)).seek_to(0.2);
|
||||
assert_eq!(c.step(late, 1.1), Some(2));
|
||||
c.step(arm(ANIM_SHUFFLE_LEFT, 3, true), 1.15);
|
||||
assert_eq!(
|
||||
c.turn.fade.map(|f| (f.node, f.until)),
|
||||
Some((node(2), 1.25)),
|
||||
"the secondary slot is left as it is"
|
||||
);
|
||||
assert_eq!(
|
||||
c.player.animation(node(2)).map(|a| a.seek_time()),
|
||||
Some(0.2)
|
||||
);
|
||||
assert_eq!(c.turn.playing, Some(node(3)));
|
||||
assert_eq!(
|
||||
c.player.animation(node(3)).map(|a| a.seek_time()),
|
||||
Some(0.0),
|
||||
"restarted"
|
||||
);
|
||||
let (out, inc) = (weight(&c.player, 2).unwrap(), weight(&c.player, 3).unwrap());
|
||||
assert!(out > 0.0 && (out + inc - 1.0).abs() < 1e-5, "{out} / {inc}");
|
||||
}
|
||||
|
||||
// ── The composite ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -235,6 +235,7 @@ mod tests {
|
||||
anim_id,
|
||||
seq_index: 0,
|
||||
node: AnimationNodeIndex::new(node as usize),
|
||||
blend_node: None,
|
||||
looping,
|
||||
duration,
|
||||
move_speed: 0.0,
|
||||
|
||||
@@ -510,6 +510,10 @@ impl AssetLoader for M2ModelLoader {
|
||||
let clip = ctx.add_labeled_asset(format!("clip{i}"), clip);
|
||||
let node = graph.add_clip(clip.clone(), 1.0, root);
|
||||
pose.set_node(node, pose_idx, 0);
|
||||
// The key bone's blend slot keeps the outgoing sequence on its own clock
|
||||
// (`0x7125ea`), and it can be the sequence the primary re-arms.
|
||||
let blend_node = graph.add_clip(clip.clone(), 1.0, root);
|
||||
pose.set_node(blend_node, pose_idx, 0);
|
||||
// The sheath family (89/90) gets per-arm masked nodes.
|
||||
let arm_nodes =
|
||||
(matches!(anim.anim_id, 89 | 90) && arm_roots.is_some()).then(|| {
|
||||
@@ -532,6 +536,7 @@ impl AssetLoader for M2ModelLoader {
|
||||
anim_id: anim.anim_id,
|
||||
seq_index: anim.seq_index,
|
||||
node,
|
||||
blend_node: Some(blend_node),
|
||||
looping: anim.looping,
|
||||
duration: anim.duration,
|
||||
move_speed: anim.move_speed,
|
||||
|
||||
@@ -32,6 +32,9 @@ pub struct AnimClip {
|
||||
/// The file slot the per-sequence bakes key on, not the index in [`ModelAnimations::clips`].
|
||||
pub seq_index: usize,
|
||||
pub node: AnimationNodeIndex,
|
||||
/// A second full-body node on the same clip, so a key bone's blend slot can run this sequence
|
||||
/// on a clock of its own while the primary re-arms it (`+0xc4`, copied whole at `0x7125ea`).
|
||||
pub blend_node: Option<AnimationNodeIndex>,
|
||||
pub looping: bool,
|
||||
/// Length in seconds; a unit streamed in dead seeks its one-shot to the end pose.
|
||||
pub duration: f32,
|
||||
@@ -146,6 +149,13 @@ impl ModelAnimations {
|
||||
.or_else(|| self.clips.first())
|
||||
}
|
||||
|
||||
/// The clip a graph node plays as its full-body pose, on either of its two tracks.
|
||||
pub fn clip_at(&self, node: AnimationNodeIndex) -> Option<&AnimClip> {
|
||||
self.clips
|
||||
.iter()
|
||||
.find(|c| c.node == node || c.blend_node == Some(node))
|
||||
}
|
||||
|
||||
/// Pick a variation of `anim_id` for a `rand()` roll, the reference's weighted walk
|
||||
/// (`0x7121a0`, variation −1). Exhaustion keeps the head: only the win path (`0x7124e9`)
|
||||
/// overwrites the slot set at `0x71247a`. Variations are the sequences sharing `anim_id` in
|
||||
@@ -223,6 +233,7 @@ mod tests {
|
||||
anim_id,
|
||||
seq_index: 0,
|
||||
node: AnimationNodeIndex::new(0),
|
||||
blend_node: None,
|
||||
looping: true,
|
||||
duration: 1.0,
|
||||
move_speed: 0.0,
|
||||
|
||||
@@ -121,7 +121,7 @@ pub fn playing_seq(player: &AnimationPlayer, anims: &ModelAnimations) -> Option<
|
||||
player
|
||||
.playing_animations()
|
||||
.filter_map(|(node, active)| {
|
||||
let clip = anims.clips.iter().find(|c| c.node == *node)?;
|
||||
let clip = anims.clip_at(*node)?;
|
||||
Some((clip.seq_index, active.seek_time(), active.weight()))
|
||||
})
|
||||
.max_by(|a, b| a.2.total_cmp(&b.2))
|
||||
|
||||
@@ -487,6 +487,7 @@ mod tests {
|
||||
anim_id,
|
||||
seq_index: 0,
|
||||
node: AnimationNodeIndex::new(1),
|
||||
blend_node: None,
|
||||
looping: true,
|
||||
duration: 2.0,
|
||||
move_speed: 0.0,
|
||||
@@ -554,6 +555,7 @@ mod tests {
|
||||
AnimClip {
|
||||
seq_index,
|
||||
node: AnimationNodeIndex::new(node),
|
||||
blend_node: None,
|
||||
..clip(anim_id)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user