diff --git a/crates/benilla-app/src/creature_anim/driver/tests.rs b/crates/benilla-app/src/creature_anim/driver/tests.rs index 00c42fcfa..56347d048 100644 --- a/crates/benilla-app/src/creature_anim/driver/tests.rs +++ b/crates/benilla-app/src/creature_anim/driver/tests.rs @@ -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) { .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) { .add(graph); let seq = |id: u16, node, duration: f32, blend_time: f32| AnimClip { node, + blend_node: None, duration, blend_time, replay: (0, 0), diff --git a/crates/benilla-app/src/creature_anim/driver/tests/state_kit_pin.rs b/crates/benilla-app/src/creature_anim/driver/tests/state_kit_pin.rs index 672e39719..6f4fbf38b 100644 --- a/crates/benilla-app/src/creature_anim/driver/tests/state_kit_pin.rs +++ b/crates/benilla-app/src/creature_anim/driver/tests/state_kit_pin.rs @@ -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), diff --git a/crates/benilla-app/src/creature_anim/events.rs b/crates/benilla-app/src/creature_anim/events.rs index e20bf8f35..c9b54ef25 100644 --- a/crates/benilla-app/src/creature_anim/events.rs +++ b/crates/benilla-app/src/creature_anim/events.rs @@ -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, diff --git a/crates/benilla-app/src/creature_anim/lod.rs b/crates/benilla-app/src/creature_anim/lod.rs index c374c9d17..f05c4403d 100644 --- a/crates/benilla-app/src/creature_anim/lod.rs +++ b/crates/benilla-app/src/creature_anim/lod.rs @@ -201,6 +201,7 @@ mod tests { anim_id: 0, seq_index: 0, node, + blend_node: None, looping: true, duration: DUR, move_speed: 0.0, diff --git a/crates/benilla-app/src/creature_anim/select/tests.rs b/crates/benilla-app/src/creature_anim/select/tests.rs index a1eab4824..4fa6e3bcd 100644 --- a/crates/benilla-app/src/creature_anim/select/tests.rs +++ b/crates/benilla-app/src/creature_anim/select/tests.rs @@ -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, diff --git a/crates/benilla-app/src/entities/spell_fx/lifecycle.rs b/crates/benilla-app/src/entities/spell_fx/lifecycle.rs index 27bf63616..ca58aef75 100644 --- a/crates/benilla-app/src/entities/spell_fx/lifecycle.rs +++ b/crates/benilla-app/src/entities/spell_fx/lifecycle.rs @@ -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, diff --git a/crates/benilla-app/src/go_anim.rs b/crates/benilla-app/src/go_anim.rs index 1edd12f1d..46ad1b0c6 100644 --- a/crates/benilla-app/src/go_anim.rs +++ b/crates/benilla-app/src/go_anim.rs @@ -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, diff --git a/crates/benilla-app/src/portrait/booth.rs b/crates/benilla-app/src/portrait/booth.rs index cf37475e5..0629f86fd 100644 --- a/crates/benilla-app/src/portrait/booth.rs +++ b/crates/benilla-app/src/portrait/booth.rs @@ -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, diff --git a/crates/benilla-app/src/portrait/glue_booth.rs b/crates/benilla-app/src/portrait/glue_booth.rs index 558d3a9f7..006434fb9 100644 --- a/crates/benilla-app/src/portrait/glue_booth.rs +++ b/crates/benilla-app/src/portrait/glue_booth.rs @@ -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, diff --git a/crates/benilla-app/src/portrait/panes.rs b/crates/benilla-app/src/portrait/panes.rs index 5acb8b8bf..5b1efe692 100644 --- a/crates/benilla-app/src/portrait/panes.rs +++ b/crates/benilla-app/src/portrait/panes.rs @@ -178,7 +178,8 @@ struct StandIn { pub(super) struct Turn { /// The serial of the engine arm last played. played: Option, - /// 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, pub(super) fade: Option, /// 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); } } diff --git a/crates/benilla-app/src/portrait/panes_tests.rs b/crates/benilla-app/src/portrait/panes_tests.rs index 960f4c90f..933d0c4c1 100644 --- a/crates/benilla-app/src/portrait/panes_tests.rs +++ b/crates/benilla-app/src/portrait/panes_tests.rs @@ -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 ───────────────────────────────────────────────────────────────────────── diff --git a/crates/benilla-app/src/ranged_flex.rs b/crates/benilla-app/src/ranged_flex.rs index 944a421d5..dffb2c624 100644 --- a/crates/benilla-app/src/ranged_flex.rs +++ b/crates/benilla-app/src/ranged_flex.rs @@ -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, diff --git a/crates/benilla-assets/src/m2.rs b/crates/benilla-assets/src/m2.rs index e67280913..d7762cc11 100644 --- a/crates/benilla-assets/src/m2.rs +++ b/crates/benilla-assets/src/m2.rs @@ -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, diff --git a/crates/benilla-assets/src/model/anims.rs b/crates/benilla-assets/src/model/anims.rs index 3c2bd54a2..fda68ceed 100644 --- a/crates/benilla-assets/src/model/anims.rs +++ b/crates/benilla-assets/src/model/anims.rs @@ -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, 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, diff --git a/crates/benilla-world/src/doodad_anim/mat_anim.rs b/crates/benilla-world/src/doodad_anim/mat_anim.rs index 1d7586b93..88aff142f 100644 --- a/crates/benilla-world/src/doodad_anim/mat_anim.rs +++ b/crates/benilla-world/src/doodad_anim/mat_anim.rs @@ -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)) diff --git a/crates/benilla-world/src/doodad_anim/mod.rs b/crates/benilla-world/src/doodad_anim/mod.rs index 11e6606b4..56a1090a4 100644 --- a/crates/benilla-world/src/doodad_anim/mod.rs +++ b/crates/benilla-world/src/doodad_anim/mod.rs @@ -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) } }