diff --git a/pomme-client/src/app/core.rs b/pomme-client/src/app/core.rs index f52074e99..1e3a7e91e 100644 --- a/pomme-client/src/app/core.rs +++ b/pomme-client/src/app/core.rs @@ -2560,7 +2560,6 @@ impl AppCore { entity_type, position, velocity, - y_rot_deg, x_rot_deg, head_y_rot_deg, } => { @@ -2572,7 +2571,6 @@ impl AppCore { entity_type, position, LookDirection::new(head_y_rot_deg, x_rot_deg), - y_rot_deg, uuid, ); if entity_type == azalea_registry::builtin::EntityKind::Player { @@ -2835,6 +2833,22 @@ impl AppCore { ); } } + // `Armadillo.handleEntityEvent`: a local sound at its position. + NetworkEvent::ArmadilloPeek { id } => { + if let Some(pos) = game.entity_store.armadillo_peek(id) { + self.audio.play_world_sound( + &crate::audio::SoundRef::event("entity.armadillo.peek"), + crate::audio::CATEGORY_NEUTRAL, + pos, + 1.0, + 1.0, + fastrand::u64(..), + ); + } + } + NetworkEvent::GoatRam { id, lowering } => { + game.entity_store.set_goat_lowering_head(id, lowering); + } NetworkEvent::SquidTentacleReset { id } => { game.entity_store.squid_tentacle_reset(id); } diff --git a/pomme-client/src/app/phases/in_game.rs b/pomme-client/src/app/phases/in_game.rs index bd69efb60..f8b417458 100644 --- a/pomme-client/src/app/phases/in_game.rs +++ b/pomme-client/src/app/phases/in_game.rs @@ -4145,6 +4145,8 @@ pub fn update_game( fox: extras.fox, axolotl: extras.axolotl, panda: extras.panda, + armadillo: extras.armadillo, + goat: extras.goat, parrot: extras.parrot, dolphin_moving: extras.dolphin_moving, turtle: extras.turtle, @@ -5051,6 +5053,8 @@ struct EntityExtras { fox: crate::renderer::entity_model::FoxAnims, axolotl: crate::renderer::entity_model::AxolotlAnims, panda: crate::renderer::entity_model::PandaAnims, + armadillo: crate::renderer::entity_model::ArmadilloAnims, + goat: crate::renderer::entity_model::GoatAnims, parrot: crate::renderer::entity_model::ParrotAnims, dolphin_moving: bool, turtle: crate::renderer::entity_model::TurtleAnims, @@ -5193,6 +5197,19 @@ fn entity_extras( | EntityKind::TropicalFish | EntityKind::Pufferfish => fish_extras(e, alpha), EntityKind::IronGolem => golem_extras(e, alpha), + EntityKind::Armadillo => EntityExtras { + armadillo: armadillo_anims(e, alpha), + ..Default::default() + }, + // `GoatRenderer.extractRenderState`. + EntityKind::Goat => EntityExtras { + goat: crate::renderer::entity_model::GoatAnims { + has_left_horn: e.goat.has_left_horn, + has_right_horn: e.goat.has_right_horn, + ramming_x_head_rot: e.goat.ramming_x_head_rot(e.is_baby), + }, + ..Default::default() + }, // `DolphinRenderer.extractRenderState`. EntityKind::Dolphin => EntityExtras { dolphin_moving: e.velocity.x * e.velocity.x + e.velocity.z * e.velocity.z > 1.0e-7, @@ -5417,6 +5434,25 @@ fn axolotl_anims( anims } +/// Vanilla `ArmadilloRenderer.extractRenderState`. +fn armadillo_anims( + e: &crate::entity::LivingEntity, + alpha: f32, +) -> crate::renderer::entity_model::ArmadilloAnims { + let a = &e.armadillo; + // `AnimationState.getTimeInMillis`, whose start may sit before tick 0 + // after a `fastForward`. + let clock = |start: Option| { + start.map(|s| ((e.age_in_ticks as f32 + alpha - s as f32) * 50.0) as i64 as f32 / 1000.0) + }; + crate::renderer::entity_model::ArmadilloAnims { + hiding_in_shell: a.hiding_in_shell(), + roll_out: clock(a.roll_out_start), + roll_up: clock(a.roll_up_start), + peek: clock(a.peek_start), + } +} + /// Vanilla `SnifferRenderer.extractRenderState`. fn sniffer_anims( e: &crate::entity::LivingEntity, diff --git a/pomme-client/src/entity/mod.rs b/pomme-client/src/entity/mod.rs index 87c984d68..f62728716 100644 --- a/pomme-client/src/entity/mod.rs +++ b/pomme-client/src/entity/mod.rs @@ -268,6 +268,41 @@ impl AxolotlSim { } } +/// Vanilla `Armadillo.ArmadilloState`, in id order. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum ArmadilloState { + #[default] + Idle, + Rolling, + Scared, + Unrolling, +} + +impl ArmadilloState { + /// `ByIdMap.continuous` with `OutOfBoundsStrategy.ZERO`. + fn from_id(id: i32) -> Self { + match id { + 1 => Self::Rolling, + 2 => Self::Scared, + 3 => Self::Unrolling, + _ => Self::Idle, + } + } + + /// `shouldHideInShell`. + fn hides_in_shell(self, ticks_in_state: u64) -> bool { + match self { + Self::Idle => false, + Self::Rolling => ticks_in_state > 5, + Self::Scared => true, + Self::Unrolling => ticks_in_state < 26, + } + } +} + +/// `ArmadilloState.SCARED.animationDuration()`. +const ARMADILLO_SCARED_ANIMATION_TICKS: i64 = 50; + /// Vanilla `Fox` client state: the synced flags byte and the crouch spring /// (`Fox.tick`). The interested head tilt reuses the wolf's spring. #[derive(Clone, Debug, Default, PartialEq)] @@ -455,6 +490,104 @@ impl PandaSim { } } +/// The client half of vanilla `Armadillo`: the synced state, the ticks spent +/// in it, and the start ticks of its three `AnimationState`s. +#[derive(Clone, Debug, Default, PartialEq)] +pub struct ArmadilloSim { + pub state: ArmadilloState, + in_state_ticks: u64, + pub roll_out_start: Option, + pub roll_up_start: Option, + pub peek_start: Option, + peek_received: bool, +} + +impl ArmadilloSim { + /// `onSyncedDataUpdated`. + fn set_state(&mut self, id: i32) { + self.state = ArmadilloState::from_id(id); + self.in_state_ticks = 0; + } + + /// `isScared`. + pub fn is_scared(&self) -> bool { + self.state != ArmadilloState::Idle + } + + /// `shouldHideInShell`. + pub fn hiding_in_shell(&self) -> bool { + self.state.hides_in_shell(self.in_state_ticks) + } + + /// `setupAnimationStates` then `++inStateTicks`, at vanilla's + /// post-increment `tickCount`. + fn tick(&mut self, tick_count: i64) { + use ArmadilloState as S; + let (roll_out, roll_up) = (self.state == S::Unrolling, self.state == S::Rolling); + for (clock, on) in [ + (&mut self.roll_out_start, roll_out), + (&mut self.roll_up_start, roll_up), + ] { + if !on { + *clock = None; + } else if clock.is_none() { + *clock = Some(tick_count); + } + } + if self.state != S::Scared { + self.peek_start = None; + } else { + if self.peek_received { + self.peek_start = None; + self.peek_received = false; + } + if self.in_state_ticks == 0 { + // `start` then `fastForward(animationDuration, 1)`: entering + // the state shows the peek's last frame. + self.peek_start = Some(tick_count - ARMADILLO_SCARED_ANIMATION_TICKS); + } else if self.peek_start.is_none() { + self.peek_start = Some(tick_count); + } + } + self.in_state_ticks += 1; + } +} + +/// The client half of vanilla `Goat`: horn flags and the ram's head-lowering +/// counter. +#[derive(Clone, Debug, PartialEq)] +pub struct GoatSim { + pub has_left_horn: bool, + pub has_right_horn: bool, + lowering_head: bool, + lower_head_tick: i32, +} + +impl Default for GoatSim { + fn default() -> Self { + Self { + has_left_horn: true, + has_right_horn: true, + lowering_head: false, + lower_head_tick: 0, + } + } +} + +impl GoatSim { + /// `Goat.aiStep`'s head-lowering counter. + fn tick(&mut self) { + let step = if self.lowering_head { 1 } else { -2 }; + self.lower_head_tick = (self.lower_head_tick + step).clamp(0, 20); + } + + /// `Goat.getRammingXHeadRot`, radians. + pub fn ramming_x_head_rot(&self, is_baby: bool) -> f32 { + let max = if is_baby { 52.5 } else { 30.0 }; + self.lower_head_tick as f32 / 20.0 * max * (std::f32::consts::PI / 180.0) + } +} + /// A 0..1 pose amount stepping `up` toward 1 while `on`, else `down` /// toward 0. fn spring(cur: f32, on: bool, up: f32, down: f32) -> f32 { @@ -485,6 +618,8 @@ fn is_ageable_mob(kind: EntityKind) -> bool { | EntityKind::WanderingTrader | EntityKind::SulfurCube | EntityKind::Mooshroom + | EntityKind::Armadillo + | EntityKind::Goat | EntityKind::Fox | EntityKind::Panda | EntityKind::Bee @@ -713,6 +848,8 @@ pub struct LivingEntity { pub fox: FoxSim, pub panda: PandaSim, pub axolotl: AxolotlSim, + pub armadillo: ArmadilloSim, + pub goat: GoatSim, /// `Turtle.HAS_EGG` / `LAYING_EGG`. pub has_egg: bool, pub laying_egg: bool, @@ -768,7 +905,8 @@ pub struct LivingEntity { pub(crate) fn baby_dimensions(kind: EntityKind, width: f32, height: f32) -> (f32, f32) { let scale = match kind { EntityKind::Squid | EntityKind::GlowSquid => return (0.5, 0.5), - EntityKind::Fox => 0.6, + EntityKind::Goat => return (0.45, 0.65), + EntityKind::Armadillo | EntityKind::Fox => 0.6, // Their `getAgeScale`. EntityKind::Dolphin => 0.65, EntityKind::Turtle => 0.3, @@ -787,6 +925,8 @@ pub(crate) fn living_entity_dimensions(entity: &LivingEntity) -> EntityDimension (dims.width, dims.height) = baby_dimensions(entity.entity_type, dims.width, dims.height); } dims.height *= entity.stand_height_scale(); + // TODO: `Goat.getDefaultDimensions` scales by 0.7 in `Pose.LONG_JUMPING`; + // pomme tracks only the crouching pose. dims } @@ -834,14 +974,15 @@ fn living_push_impulses(local: Position, remote: Position) -> Option<(DVec3, DVe } impl LivingEntity { + /// Vanilla `LivingEntity.recreateFromPacket`: the head and body both + /// start at the packet's head yaw, which `look_dir` carries. pub fn new( entity_type: EntityKind, position: Position, look_dir: LookDirection, - head_y_rot_deg: f32, - body_y_rot_deg: f32, uuid: uuid::Uuid, ) -> Self { + let y_rot_deg = look_dir.y_rot_deg(); let default_health = if entity_type == EntityKind::IronGolem { 100.0 } else { @@ -852,10 +993,10 @@ impl LivingEntity { prev_position: position, look_dir, prev_look_dir: look_dir, - head_y_rot_deg, - prev_head_y_rot_deg: head_y_rot_deg, - body_y_rot_deg, - prev_body_y_rot_deg: body_y_rot_deg, + head_y_rot_deg: y_rot_deg, + prev_head_y_rot_deg: y_rot_deg, + body_y_rot_deg: y_rot_deg, + prev_body_y_rot_deg: y_rot_deg, entity_type, uuid, player_uuid: (entity_type == EntityKind::Player).then_some(uuid), @@ -949,6 +1090,8 @@ impl LivingEntity { fox: FoxSim::default(), panda: PandaSim::default(), axolotl: AxolotlSim::default(), + armadillo: ArmadilloSim::default(), + goat: GoatSim::default(), has_egg: false, laying_egg: false, puff_state: 0, @@ -985,7 +1128,7 @@ impl LivingEntity { interp_target: position, interp_look_dir: look_dir, interp_steps: 0, - interp_head_y_rot_deg: head_y_rot_deg, + interp_head_y_rot_deg: y_rot_deg, interp_head_y_rot_steps: 0, } } @@ -1384,6 +1527,15 @@ impl LivingEntity { // TODO: `Bee.tick`'s falling-nectar drip particles (no drip // particle type yet). EntityKind::Bee => self.bee.tick(), + // `Armadillo.tick`; a scared armadillo's `getMaxHeadYRot` is 0, + // so `clampHeadRotationToBody` pins the head to the body. + EntityKind::Armadillo => { + self.armadillo.tick(i64::from(self.age_in_ticks) + 1); + if self.armadillo.is_scared() { + self.head_y_rot_deg += wrap_degrees(self.body_y_rot_deg - self.head_y_rot_deg); + } + } + EntityKind::Goat => self.goat.tick(), EntityKind::Fox => self.fox.tick(), // `Axolotl.baseTick` runs before the interpolation step, where a // remote entity's rotation still equals the `setOldPosAndRot` @@ -1437,6 +1589,11 @@ impl LivingEntity { } } + /// Vanilla `LivingEntity.tickHeadTurn`. + // TODO: a `Mob` turns its body through `BodyRotationControl.clientTick` + // instead: the body snaps to its yaw while moving, follows the head past + // `getMaxHeadYRot` (75 by default, 15 for a goat, 1 for an axolotl), and + // turns to face the head after it holds still for 10 ticks. pub fn tick_body_rotation(&mut self) { let dx = self.position.x - self.prev_position.x; let dz = self.position.z - self.prev_position.z; @@ -1806,20 +1963,10 @@ impl EntityStore { entity_type: EntityKind, position: Position, look_dir: LookDirection, - body_y_rot_deg: f32, uuid: uuid::Uuid, ) { - self.living.insert( - id, - LivingEntity::new( - entity_type, - position, - look_dir, - look_dir.y_rot_deg(), - body_y_rot_deg, - uuid, - ), - ); + self.living + .insert(id, LivingEntity::new(entity_type, position, look_dir, uuid)); } pub fn move_living_delta(&mut self, id: i32, dx: f64, dy: f64, dz: f64, on_ground: bool) { @@ -1983,6 +2130,9 @@ impl EntityStore { entity.bat_anim_start = Some(entity.age_in_ticks + 1); } } + (EntityKind::Armadillo, 18, Int(id)) => entity.armadillo.set_state(id), + (EntityKind::Goat, 19, Bool(b)) => entity.goat.has_left_horn = b, + (EntityKind::Goat, 20, Bool(b)) => entity.goat.has_right_horn = b, (EntityKind::Turtle, 18, Bool(b)) => entity.has_egg = b, (EntityKind::Turtle, 19, Bool(b)) => entity.laying_egg = b, // `Axolotl.Variant.byId`: out-of-range ids fall back to LUCY. @@ -2132,6 +2282,26 @@ impl EntityStore { } } + /// `Armadillo.handleEntityEvent` 64: restart the peek animation. Returns + /// where to play the peek sound. + pub fn armadillo_peek(&mut self, id: i32) -> Option { + let entity = self.living.get_mut(&id)?; + if entity.entity_type != EntityKind::Armadillo { + return None; + } + entity.armadillo.peek_received = true; + Some(entity.position) + } + + /// Goat entity events 58 / 59: start / stop lowering the head to ram. + pub fn set_goat_lowering_head(&mut self, id: i32, lowering: bool) { + if let Some(entity) = self.living.get_mut(&id) + && entity.entity_type == EntityKind::Goat + { + entity.goat.lowering_head = lowering; + } + } + /// Iron golem punch (entity event 4): vanilla runs a 10-tick swing. pub fn golem_punch(&mut self, id: i32) { if let Some(entity) = self.living.get_mut(&id) @@ -2207,7 +2377,10 @@ impl EntityStore { let thundering = self.thundering; for (&id, entity) in &mut self.living { entity.tick_interpolation(); - entity.tick_body_rotation(); + // A scared armadillo's body control skips its client tick. + if !(entity.entity_type == EntityKind::Armadillo && entity.armadillo.is_scared()) { + entity.tick_body_rotation(); + } let dx = entity.position.x - entity.prev_position.x; let dz = entity.position.z - entity.prev_position.z; let dy = if omnidirectional_air_mover(entity.entity_type) { @@ -2391,6 +2564,8 @@ pub fn is_living_mob(kind: &EntityKind) -> bool { | EntityKind::Fox | EntityKind::Panda | EntityKind::Mooshroom + | EntityKind::Armadillo + | EntityKind::Goat | EntityKind::Dolphin | EntityKind::Turtle | EntityKind::Axolotl @@ -2447,7 +2622,7 @@ mod tests { fn spawn(store: &mut EntityStore, id: i32, kind: EntityKind, position: Position) { let uuid = uuid::Uuid::from_u128(id as u128); - store.spawn_living(id, kind, position, LookDirection::default(), 0.0, uuid); + store.spawn_living(id, kind, position, LookDirection::default(), uuid); } #[test] @@ -2677,6 +2852,79 @@ mod tests { assert!(store.living[&2].is_baby); } + /// `Armadillo.setupAnimationStates`: each state runs its one clock; + /// entering SCARED fast-forwards the peek to its end, and a peek event + /// restarts it on the next tick. + #[test] + fn armadillo_state_drives_shell_and_clocks() { + let mut store = EntityStore::new(); + spawn(&mut store, 1, EntityKind::Armadillo, Position::default()); + store.living.get_mut(&1).unwrap().age_in_ticks = 100; + let sim = |store: &EntityStore| store.living[&1].armadillo.clone(); + + store.apply_entity_data(1, 18, MetaValue::Int(2)); + assert!(sim(&store).hiding_in_shell()); + tick(&mut store); + assert_eq!(sim(&store).peek_start, Some(101 - 50)); + assert!(store.armadillo_peek(1).is_some()); + tick(&mut store); + assert_eq!(sim(&store).peek_start, Some(102)); + + store.apply_entity_data(1, 18, MetaValue::Int(3)); + tick(&mut store); + let s = sim(&store); + assert_eq!( + (s.roll_out_start, s.roll_up_start, s.peek_start), + (Some(103), None, None) + ); + assert!(s.hiding_in_shell()); + for _ in 0..25 { + tick(&mut store); + } + assert!(!sim(&store).hiding_in_shell()); + + store.apply_entity_data(1, 18, MetaValue::Int(1)); + for _ in 0..5 { + tick(&mut store); + } + let s = sim(&store); + assert_eq!((s.roll_out_start, s.roll_up_start), (None, Some(129))); + assert!(!s.hiding_in_shell()); + tick(&mut store); + assert!(sim(&store).hiding_in_shell()); + + store.apply_entity_data(1, 18, MetaValue::Int(7)); + tick(&mut store); + assert_eq!( + sim(&store), + ArmadilloSim { + in_state_ticks: 1, + ..Default::default() + } + ); + } + + /// `Goat.aiStep`: the ram counter climbs one a tick to 20 and falls two. + #[test] + fn goat_ram_events_lower_the_head() { + let mut store = EntityStore::new(); + spawn(&mut store, 1, EntityKind::Goat, Position::default()); + store.set_goat_lowering_head(1, true); + for _ in 0..25 { + tick(&mut store); + } + let goat = &store.living[&1].goat; + assert_eq!(goat.lower_head_tick, 20); + assert_eq!(goat.ramming_x_head_rot(false), 30f32.to_radians()); + store.set_goat_lowering_head(1, false); + tick(&mut store); + assert_eq!(store.living[&1].goat.lower_head_tick, 18); + + store.apply_entity_data(1, 19, MetaValue::Bool(false)); + assert!(!store.living[&1].goat.has_left_horn); + assert!(store.living[&1].goat.has_right_horn); + } + /// `Axolotl.DATA_VARIANT` (18) and `DATA_PLAYING_DEAD` (19): the adult /// ramps its eased playing-dead animator, the baby solos one /// `AnimationState` that keeps its clock while it stays selected. @@ -2765,8 +3013,6 @@ mod tests { EntityKind::Player, Position::default(), LookDirection::default(), - 0.0, - 0.0, uuid::Uuid::nil(), ); let helmet = ItemStack::new(ItemKind::DiamondHelmet, 1); diff --git a/pomme-client/src/net/entity_data.rs b/pomme-client/src/net/entity_data.rs index cd284d7aa..c1fcb176b 100644 --- a/pomme-client/src/net/entity_data.rs +++ b/pomme-client/src/net/entity_data.rs @@ -242,6 +242,9 @@ pub(super) fn from_azalea(value: &EntityDataValue) -> Option { EntityDataValue::SnifferState(state) => Value::Int(*state as i32), EntityDataValue::CopperGolemState(state) => Value::Int(*state as i32), EntityDataValue::WeatheringCopperState(state) => Value::Int(*state as i32), + // TODO: azalea's `ArmadilloStateKind` lacks UNROLLING (id 3), so a + // translated session fails to decode that state. + EntityDataValue::ArmadilloState(state) => Value::Int(*state as i32), _ => return None, }) } diff --git a/pomme-client/src/net/entity_wire.rs b/pomme-client/src/net/entity_wire.rs index 78c9f8870..4ed93abdc 100644 --- a/pomme-client/src/net/entity_wire.rs +++ b/pomme-client/src/net/entity_wire.rs @@ -41,7 +41,7 @@ fn event(packet: EntityPacket) -> Option { position, movement, x_rot, - y_rot, + y_rot: _, y_head_rot, data: _, } => { @@ -55,7 +55,6 @@ fn event(packet: EntityPacket) -> Option { entity_type, position: position.into(), velocity: movement, - y_rot_deg: y_rot, x_rot_deg: x_rot, head_y_rot_deg: y_head_rot, } @@ -171,7 +170,13 @@ fn entity_event(id: i32, event: i8) -> Option { }, 45 => NetworkEvent::FoxEat { id }, 56 => NetworkEvent::WolfShaking { id, shaking: false }, + 58 => NetworkEvent::GoatRam { id, lowering: true }, + 59 => NetworkEvent::GoatRam { + id, + lowering: false, + }, 63 => NetworkEvent::SnifferDigging { id }, + 64 => NetworkEvent::ArmadilloPeek { id }, _ => return None, }) } diff --git a/pomme-client/src/net/mod.rs b/pomme-client/src/net/mod.rs index 5a7619f30..049a83905 100644 --- a/pomme-client/src/net/mod.rs +++ b/pomme-client/src/net/mod.rs @@ -433,7 +433,6 @@ pub enum NetworkEvent { entity_type: EntityKind, position: Position, velocity: DVec3, - y_rot_deg: f32, x_rot_deg: f32, head_y_rot_deg: f32, }, @@ -567,6 +566,15 @@ pub enum NetworkEvent { FoxEat { id: i32, }, + /// Entity event 64: an armadillo peeks out of its shell. + ArmadilloPeek { + id: i32, + }, + /// Entity events 58 / 59: a goat starts / stops lowering its head to ram. + GoatRam { + id: i32, + lowering: bool, + }, SquidTentacleReset { id: i32, }, diff --git a/pomme-client/src/renderer/entity_model.rs b/pomme-client/src/renderer/entity_model.rs index b7964e26a..b27fb99e3 100644 --- a/pomme-client/src/renderer/entity_model.rs +++ b/pomme-client/src/renderer/entity_model.rs @@ -3,21 +3,25 @@ use glam::{Mat4, Vec3}; use crate::renderer::chunk::mesher::pack_uv; use crate::renderer::rhi; +mod armadillo; mod axolotl; mod bee; mod copper_golem_anim; mod dolphin; mod fox; +mod goat; mod llama; mod panda; mod parrot; mod polar_bear; mod sniffer_anim; mod turtle; +pub use armadillo::*; pub use axolotl::*; pub use bee::*; pub use dolphin::*; pub use fox::*; +pub use goat::*; pub use llama::*; pub use panda::*; pub use parrot::*; diff --git a/pomme-client/src/renderer/entity_model/armadillo.rs b/pomme-client/src/renderer/entity_model/armadillo.rs new file mode 100644 index 000000000..7b06df733 --- /dev/null +++ b/pomme-client/src/renderer/entity_model/armadillo.rs @@ -0,0 +1,1737 @@ +//! Armadillo: `AdultArmadilloModel`/`BabyArmadilloModel` meshes, +//! `ArmadilloModel.setupAnim`, and the `ArmadilloAnimation` / +//! `BabyArmadilloAnimation` keyframe tables (converted mechanically from the +//! decompiled definitions; all-zero channels omitted). + +// Vanilla's radian literals are kept verbatim, not swapped for `consts`. +#![allow(clippy::approx_constant)] + +use glam::Vec3; + +use super::{ + BakedEntityModel, KfAnim, KfChannel, KfDeltas, KfTarget, Kfi, PartAnim, apply_kf_anim, + apply_kf_walk, bake_model, deformed, head_rotation, kf, mirrored, part_index, rotated, vbox, + vpart, +}; + +/// The body's own cubes, split off as a child at the body's pivot so +/// `body.skipDraw` can hide them while the head (also a body child) still +/// draws. Pomme-only; no keyframe channel targets it. +const BODY_SHELL: &str = "body_shell"; + +/// Vanilla `AdultArmadilloModel.createBodyLayer`, 64x64. +pub fn bake_armadillo_model() -> BakedEntityModel { + let leg = |name: &str, x: f32, z: f32, uv: (i32, i32)| { + vpart( + name, + None, + Vec3::new(x, 21.0, z), + vec![vbox(uv, (-1.0, 0.0, -1.0), (2.0, 3.0, 2.0))], + ) + }; + let parts = vec![ + vpart("body", None, Vec3::new(0.0, 21.0, 4.0), vec![]), + vpart( + BODY_SHELL, + Some(0), + Vec3::ZERO, + vec![ + deformed(vbox((0, 20), (-4.0, -7.0, -10.0), (8.0, 8.0, 12.0)), 0.3), + vbox((0, 40), (-4.0, -7.0, -10.0), (8.0, 8.0, 12.0)), + ], + ), + rotated( + vpart( + "tail", + Some(0), + Vec3::new(0.0, -3.0, 1.0), + vec![vbox((44, 53), (-0.5, -0.0865, 0.0933), (1.0, 6.0, 1.0))], + ), + Vec3::new(0.5061, 0.0, 0.0), + ), + vpart("head", Some(0), Vec3::new(0.0, -2.0, -11.0), vec![]), + rotated( + vpart( + "head_cube", + Some(3), + Vec3::ZERO, + vec![vbox((43, 15), (-1.5, -1.0, -1.0), (3.0, 5.0, 2.0))], + ), + Vec3::new(-0.3927, 0.0, 0.0), + ), + vpart("right_ear", Some(3), Vec3::new(-1.0, -1.0, 0.0), vec![]), + rotated( + vpart( + "right_ear_cube", + Some(5), + Vec3::new(-0.5, 0.0, -0.6), + vec![vbox((43, 10), (-2.0, -3.0, 0.0), (2.0, 5.0, 0.0))], + ), + Vec3::new(0.1886, -0.3864, -0.0718), + ), + vpart("left_ear", Some(3), Vec3::new(1.0, -2.0, 0.0), vec![]), + rotated( + vpart( + "left_ear_cube", + Some(7), + Vec3::new(0.5, 1.0, -0.6), + vec![vbox((47, 10), (0.0, -3.0, 0.0), (2.0, 5.0, 0.0))], + ), + Vec3::new(0.1886, 0.3864, 0.0718), + ), + leg("right_hind_leg", -2.0, 4.0, (51, 31)), + leg("left_hind_leg", 2.0, 4.0, (42, 31)), + leg("right_front_leg", -2.0, -4.0, (51, 43)), + leg("left_front_leg", 2.0, -4.0, (42, 43)), + vpart( + "cube", + None, + Vec3::new(0.0, 24.0, 0.0), + vec![vbox((0, 0), (-5.0, -10.0, -6.0), (10.0, 10.0, 10.0))], + ), + ]; + bake_model(parts, 64, 64) +} + +/// Vanilla `BabyArmadilloModel.createBodyLayer`, 64x64. The names are +/// vanilla's: the tail cube is called `right_ear_cube`, the ears hang off +/// `head_cube`, and the right front leg sits at +x. +pub fn bake_baby_armadillo_model() -> BakedEntityModel { + let parts = vec![ + vpart("body", None, Vec3::new(0.0, 20.0, 0.5), vec![]), + vpart( + BODY_SHELL, + Some(0), + Vec3::ZERO, + vec![ + deformed(vbox((0, 0), (-2.5, -2.0, -3.5), (5.0, 4.0, 7.0)), 0.3), + vbox((0, 11), (-2.5, -2.0, -3.0), (5.0, 4.0, 6.0)), + ], + ), + vpart("tail", Some(0), Vec3::new(0.0, 0.0, 3.4), vec![]), + rotated( + vpart( + "right_ear_cube", + Some(2), + Vec3::new(0.0, 1.5, 1.0), + vec![vbox((22, 11), (-0.5, -0.5, -2.0), (1.0, 1.0, 4.0))], + ), + Vec3::new(-1.0472, 0.0, 0.0), + ), + vpart("head", Some(0), Vec3::new(0.0, 0.0, -3.2), vec![]), + rotated( + vpart( + "head_cube", + Some(4), + Vec3::ZERO, + vec![vbox((20, 17), (-1.0, -2.0, -4.0), (2.0, 2.0, 4.0))], + ), + Vec3::new(0.7417649, 0.0, 0.0), + ), + rotated( + vpart( + "right_ear", + Some(5), + Vec3::new(-1.0, -2.0, -0.3), + vec![mirrored(vbox((28, 8), (-1.8, -2.0, 0.0), (2.0, 3.0, 0.0)))], + ), + Vec3::new(-0.4363, -0.1134, 0.0524), + ), + rotated( + vpart( + "left_ear", + Some(5), + Vec3::new(1.0, -2.0, -0.3), + vec![vbox((28, 8), (-0.2, -2.0, 0.0), (2.0, 3.0, 0.0))], + ), + Vec3::new(-0.4363, 0.1134, -0.0524), + ), + vpart( + "right_hind_leg", + None, + Vec3::new(-1.5, 22.0, 2.5), + vec![mirrored(vbox((20, 27), (-1.0, 0.0, -1.0), (2.0, 2.0, 2.0)))], + ), + vpart( + "left_hind_leg", + None, + Vec3::new(1.5, 22.0, 2.5), + vec![vbox((20, 27), (-1.0, 0.0, -1.0), (2.0, 2.0, 2.0))], + ), + vpart( + "right_front_leg", + None, + Vec3::new(1.5, 22.0, -1.5), + vec![vbox((20, 23), (-1.0, 0.0, -1.0), (2.0, 2.0, 2.0))], + ), + vpart( + "left_front_leg", + None, + Vec3::new(-1.5, 22.0, -1.5), + vec![mirrored(vbox((24, 0), (-1.0, 0.0, -1.0), (2.0, 2.0, 2.0)))], + ), + vpart( + "cube", + None, + Vec3::new(0.0, 20.7, 0.5), + vec![deformed( + vbox((0, 25), (-3.0, -3.0, -3.0), (6.0, 6.0, 6.0)), + 0.3, + )], + ), + ]; + bake_model(parts, 64, 64) +} + +/// `ArmadilloRenderState`: the shell flag and the three `AnimationState` +/// clocks (seconds since start; `None` = stopped). +#[derive(Clone, Copy, Default, Debug, PartialEq)] +pub struct ArmadilloAnims { + pub hiding_in_shell: bool, + pub roll_out: Option, + pub roll_up: Option, + pub peek: Option, +} + +/// One mesh's `ArmadilloModel` constructor arguments. +struct ArmadilloTables { + walk: &'static KfAnim, + roll_out: &'static KfAnim, + roll_up: &'static KfAnim, + peek: &'static KfAnim, +} + +static ADULT: ArmadilloTables = ArmadilloTables { + walk: &ARMADILLO_WALK, + roll_out: &ARMADILLO_ROLL_OUT, + roll_up: &ARMADILLO_ROLL_UP, + peek: &ARMADILLO_PEEK, +}; + +static BABY: ArmadilloTables = ArmadilloTables { + walk: &ARMADILLO_BABY_WALK, + roll_out: &ARMADILLO_BABY_ROLL_OUT, + roll_up: &ARMADILLO_BABY_ROLL_UP, + peek: &ARMADILLO_BABY_PEEK, +}; + +/// Vanilla `ArmadilloModel.setupAnim`, shared by the adult and baby meshes. +/// Hidden parts (`visible = false`, `skipDraw`) collapse to a zero scale. +pub fn compute_armadillo_anim( + model: &BakedEntityModel, + head_x_rot_deg: f32, + local_head_y_rot_deg: f32, + walk_pos: f32, + walk_speed: f32, + is_baby: bool, + a: &ArmadilloAnims, +) -> PartAnim { + let t = if is_baby { &BABY } else { &ADULT }; + let mut d = KfDeltas::new(model); + if !a.hiding_in_shell { + d.set_rot( + model, + part_index(&model.parts, "head"), + head_rotation( + head_x_rot_deg.clamp(-22.5, 25.0), + local_head_y_rot_deg.clamp(-32.5, 32.5), + ), + ); + apply_kf_walk(t.walk, model, walk_pos, walk_speed, 16.5, 2.5, &mut d); + } + apply_kf_anim(t.roll_out, model, a.roll_out, &mut d); + apply_kf_anim(t.roll_up, model, a.roll_up, &mut d); + apply_kf_anim(t.peek, model, a.peek, &mut d); + let mut anim = d.emit(model); + let hidden: &[&str] = if a.hiding_in_shell { + &[BODY_SHELL, "left_hind_leg", "right_hind_leg", "tail"] + } else { + &["cube"] + }; + for name in hidden { + anim.scale + .push((part_index(&model.parts, name), Vec3::ZERO)); + } + anim +} + +static ARMADILLO_ROLL_UP: KfAnim = { + use Kfi::Lin; + KfAnim { + length: 0.5, + looping: false, + channels: &[ + KfChannel { + part: "body", + target: KfTarget::Pos, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.1667, 0.0, 5.0, 0.0, Lin), + kf(0.2083, 0.0, 6.0, -1.0, Lin), + kf(0.25, 0.0, 6.0, -1.0, Lin), + kf(0.375, 0.0, -1.0, -1.0, Lin), + ], + }, + KfChannel { + part: "tail", + target: KfTarget::Pos, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.1667, 0.0, 0.0, 0.0, Lin), + kf(0.2083, 0.0, 0.0, -2.0, Lin), + kf(0.25, 0.0, 0.0, -2.0, Lin), + ], + }, + KfChannel { + part: "head", + target: KfTarget::Rot, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.1667, 17.5, 0.0, 0.0, Lin), + kf(0.25, -72.5, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "head", + target: KfTarget::Pos, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.1667, 0.0, -1.0, 0.0, Lin), + kf(0.2083, 0.0, 2.0, 1.0, Lin), + kf(0.25, 0.0, 2.0, 1.0, Lin), + kf(0.2917, 0.0, 2.0, 6.0, Lin), + kf(0.375, 0.0, 2.0, 7.0, Lin), + ], + }, + KfChannel { + part: "right_hind_leg", + target: KfTarget::Rot, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.1667, -35.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "right_hind_leg", + target: KfTarget::Pos, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.1667, 0.0, 5.0, -2.0, Lin), + kf(0.1875, 0.0, 8.0, -3.0, Lin), + kf(0.2083, 0.0, 8.0, -2.0, Lin), + kf(0.25, 0.0, 8.0, -2.0, Lin), + kf(0.375, 1.0, 3.0, -6.0, Lin), + ], + }, + KfChannel { + part: "left_hind_leg", + target: KfTarget::Rot, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.1667, -45.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "left_hind_leg", + target: KfTarget::Pos, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.1667, 0.0, 5.0, -2.0, Lin), + kf(0.1875, 0.0, 8.0, -3.0, Lin), + kf(0.2083, 0.0, 8.0, -2.0, Lin), + kf(0.25, 0.0, 8.0, -2.0, Lin), + kf(0.375, -1.0, 3.0, -6.0, Lin), + ], + }, + KfChannel { + part: "right_front_leg", + target: KfTarget::Rot, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.1667, -27.5, 0.0, 0.0, Lin), + kf(0.2083, -32.5, 0.0, 0.0, Lin), + kf(0.25, -85.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "right_front_leg", + target: KfTarget::Pos, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.1667, 0.0, 5.0, 0.0, Lin), + kf(0.1875, -0.5, 11.5, 0.5, Lin), + kf(0.2083, -1.0, 9.0, -1.0, Lin), + kf(0.25, -1.0, 9.0, -1.0, Lin), + kf(0.375, -1.0, 2.0, 3.0, Lin), + ], + }, + KfChannel { + part: "left_front_leg", + target: KfTarget::Rot, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.1667, -12.5, 0.0, 0.0, Lin), + kf(0.2083, -35.0, 0.0, 0.0, Lin), + kf(0.25, -85.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "left_front_leg", + target: KfTarget::Pos, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.1667, 0.0, 5.0, 0.0, Lin), + kf(0.1875, 0.5, 11.5, 0.5, Lin), + kf(0.2083, 1.0, 9.0, -1.0, Lin), + kf(0.25, 1.0, 9.0, -1.0, Lin), + kf(0.375, 1.0, 2.0, 3.0, Lin), + ], + }, + KfChannel { + part: "cube", + target: KfTarget::Rot, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.1667, 0.0, 0.0, 0.0, Lin), + kf(0.4167, -2.5, 0.0, 0.0, Lin), + kf(0.4583, 5.0, 0.0, 0.0, Lin), + kf(0.5, 0.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "cube", + target: KfTarget::Pos, + frames: &[ + kf(0.0, 0.0, 3.0, 0.0, Lin), + kf(0.1667, 0.0, 8.0, 0.0, Lin), + kf(0.2083, 0.0, 7.0, 0.0, Lin), + kf(0.25, 0.0, 7.0, 0.0, Lin), + kf(0.375, 0.0, 0.0, 0.0, Lin), + kf(0.4167, 0.0, 1.0, 0.0, Lin), + kf(0.4583, 0.0, 0.6, 0.0, Lin), + kf(0.5, 0.0, 0.0, 0.0, Lin), + ], + }, + ], + } +}; + +static ARMADILLO_WALK: KfAnim = { + use Kfi::{Cat, Lin}; + KfAnim { + length: 1.4583, + looping: true, + channels: &[ + KfChannel { + part: "body", + target: KfTarget::Rot, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Cat), + kf(0.25, 0.0, 0.0, 4.6, Cat), + kf(0.2917, 0.0, 0.0, 6.81, Cat), + kf(0.5, 0.0, 0.0, 0.0, Cat), + kf(0.7083, 0.0, 0.0, 0.0, Cat), + kf(0.9583, 0.0, 0.0, -4.6, Cat), + kf(1.0, 0.0, 0.0, -6.89, Cat), + kf(1.25, 0.0, 0.0, 0.0, Cat), + kf(1.4583, 0.0, 0.0, 0.0, Cat), + ], + }, + KfChannel { + part: "body", + target: KfTarget::Pos, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Cat), + kf(0.25, 0.0, -0.2, 0.0, Cat), + kf(0.5, 0.0, 0.0, 0.0, Cat), + kf(0.7083, 0.0, 0.0, 0.0, Cat), + kf(0.9583, 0.0, -0.2, 0.0, Cat), + kf(1.25, 0.0, 0.0, 0.0, Cat), + kf(1.4583, 0.0, 0.0, 0.0, Cat), + ], + }, + KfChannel { + part: "tail", + target: KfTarget::Rot, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.5, -9.17, 0.0, 0.0, Lin), + kf(0.75, 5.0, 0.0, 0.0, Lin), + kf(1.2083, -8.24, 0.0, 0.0, Lin), + kf(1.4583, 0.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "right_hind_leg", + target: KfTarget::Rot, + frames: &[ + kf(0.0, -50.0, 0.0, 0.0, Lin), + kf(0.25, 0.0, 0.0, 0.0, Lin), + kf(0.5, 0.0, 0.0, 0.0, Lin), + kf(0.75, 50.0, 0.0, 0.0, Lin), + kf(1.0, 50.0, 0.0, 0.0, Lin), + kf(1.2917, -20.0, 0.0, 0.0, Lin), + kf(1.4583, -50.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "right_hind_leg", + target: KfTarget::Pos, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.25, 0.0, 0.0, -0.5, Lin), + kf(0.5, 0.0, 0.0, 0.0, Lin), + kf(1.0, 0.0, 0.0, -0.5, Lin), + kf(1.2917, 0.0, 1.0, -0.18, Lin), + kf(1.4583, 0.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "left_hind_leg", + target: KfTarget::Rot, + frames: &[ + kf(0.0, 50.0, 0.0, 0.0, Lin), + kf(0.25, 50.0, 0.0, 0.0, Lin), + kf(0.5417, -20.0, 0.0, 0.0, Lin), + kf(0.7083, -50.0, 0.0, 0.0, Lin), + kf(0.9583, 0.0, 0.0, 0.0, Lin), + kf(1.2083, 0.0, 0.0, 0.0, Lin), + kf(1.4583, 50.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "left_hind_leg", + target: KfTarget::Pos, + frames: &[ + kf(0.0, 0.0, 0.0, -0.25, Lin), + kf(0.25, 0.0, 0.0, -0.5, Lin), + kf(0.5417, 0.0, 1.0, -0.18, Lin), + kf(0.7083, 0.0, 0.0, 0.0, Lin), + kf(0.9583, 0.0, 0.0, -0.5, Lin), + kf(1.2083, 0.0, 0.0, 0.0, Lin), + kf(1.4583, 0.0, 0.0, -0.25, Lin), + ], + }, + KfChannel { + part: "right_front_leg", + target: KfTarget::Rot, + frames: &[ + kf(0.0, 50.0, 0.0, 0.0, Lin), + kf(0.2917, 50.0, 0.0, 0.0, Lin), + kf(0.5417, -20.0, 0.0, 0.0, Lin), + kf(0.7083, -50.0, 0.0, 0.0, Lin), + kf(0.9583, 0.0, 0.0, 0.0, Lin), + kf(1.2083, 0.0, 0.0, 0.0, Lin), + kf(1.4583, 50.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "right_front_leg", + target: KfTarget::Pos, + frames: &[ + kf(0.0, 0.0, 0.0, -0.25, Lin), + kf(0.25, 0.0, 0.0, -0.5, Lin), + kf(0.5417, 0.0, 1.0, -0.18, Lin), + kf(0.7083, 0.0, 0.0, 0.0, Lin), + kf(0.9583, 0.0, 0.0, -0.5, Lin), + kf(1.2083, 0.0, 0.0, 0.0, Lin), + kf(1.4583, 0.0, 0.0, -0.25, Lin), + ], + }, + KfChannel { + part: "left_front_leg", + target: KfTarget::Rot, + frames: &[ + kf(0.0, -50.0, 0.0, 0.0, Lin), + kf(0.25, 0.0, 0.0, 0.0, Lin), + kf(0.5, 0.0, 0.0, 0.0, Lin), + kf(0.75, 50.0, 0.0, 0.0, Lin), + kf(1.0, 50.0, 0.0, 0.0, Lin), + kf(1.2917, -20.0, 0.0, 0.0, Lin), + kf(1.4583, -50.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "left_front_leg", + target: KfTarget::Pos, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.25, 0.0, 0.0, -0.5, Lin), + kf(0.5, 0.0, 0.0, 0.0, Lin), + kf(1.0, 0.0, 0.0, -0.5, Lin), + kf(1.2917, 0.0, 1.0, -0.18, Lin), + kf(1.4583, 0.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "head", + target: KfTarget::Rot, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.25, 0.0, 0.0, -2.5, Lin), + kf(0.5, 0.0, 0.0, 0.0, Lin), + kf(0.7083, 0.0, 0.0, 0.0, Lin), + kf(1.0, 0.0, 0.0, 2.5, Lin), + kf(1.25, 0.0, 0.0, 0.0, Lin), + kf(1.4583, 0.0, 0.0, 0.0, Lin), + ], + }, + ], + } +}; + +static ARMADILLO_PEEK: KfAnim = { + use Kfi::{Cat, Lin}; + KfAnim { + length: 2.5, + looping: false, + channels: &[ + KfChannel { + part: "head", + target: KfTarget::Rot, + frames: &[ + kf(0.0, -70.0, 0.0, 0.0, Lin), + kf(0.15, -65.0, 0.0, 0.0, Lin), + kf(0.4, -50.0, 0.0, 0.0, Lin), + kf(0.5, 0.0, 0.0, 0.0, Lin), + kf(0.7, 0.0, 0.0, 0.0, Lin), + kf(0.9, -7.5, 0.0, 45.0, Cat), + kf(1.15, -7.5, 0.0, 45.0, Lin), + kf(1.3, -0.8639, -1.4959, -39.1287, Cat), + kf(1.6, -0.8639, -1.4959, -39.1287, Lin), + kf(1.75, 0.0, 0.0, 0.0, Lin), + kf(1.8, -25.0, 0.0, 0.0, Lin), + kf(1.85, -70.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "head", + target: KfTarget::Pos, + frames: &[ + kf(0.0, 0.0, 1.0, 7.0, Lin), + kf(0.05, 0.0, 1.0, 4.0, Lin), + kf(0.15, 0.0, 1.0, 4.0, Lin), + kf(0.25, 0.0, 1.0, 5.0, Lin), + kf(0.35, 0.0, 1.0, 0.2, Lin), + kf(0.4, 0.0, 1.0, 0.2, Lin), + kf(0.5, 0.0, 2.1, 1.2, Lin), + kf(0.6, 0.0, 1.1, 0.2, Lin), + kf(0.7, 0.0, 1.1, 0.2, Lin), + kf(1.75, 0.0, 1.1, 0.2, Lin), + kf(1.8, 0.0, 0.1, 0.2, Lin), + kf(1.95, 0.0, 0.1, 5.2, Lin), + kf(2.0, 0.0, 0.1, 7.2, Lin), + kf(2.15, 0.0, 0.1, 8.2, Lin), + kf(2.3, 0.0, 0.1, 5.2, Lin), + ], + }, + KfChannel { + part: "right_hind_leg", + target: KfTarget::Pos, + frames: &[kf(0.0, 0.0, 3.0, -2.0, Lin)], + }, + KfChannel { + part: "left_hind_leg", + target: KfTarget::Pos, + frames: &[kf(0.0, 0.0, 3.0, -2.0, Lin)], + }, + KfChannel { + part: "right_front_leg", + target: KfTarget::Rot, + frames: &[ + kf(0.0, -90.0, 0.0, 0.0, Lin), + kf(0.5833, -90.0, 0.0, 0.0, Cat), + kf(0.8333, -45.0, 0.0, 0.0, Cat), + kf(1.0, -45.0, 0.0, 0.0, Cat), + kf(1.75, -45.0, 0.0, 0.0, Cat), + kf(1.8, -90.0, 0.0, 0.0, Cat), + kf(1.95, -90.0, 0.0, 0.0, Cat), + ], + }, + KfChannel { + part: "right_front_leg", + target: KfTarget::Pos, + frames: &[ + kf(0.0, -1.0, 2.0, 2.0, Lin), + kf(0.5833, -1.0, 2.0, 2.0, Cat), + kf(0.6667, -1.0, 2.0, -2.0, Cat), + kf(0.8333, -1.0, 2.0, -1.0, Cat), + kf(1.0, -1.0, 2.0, -1.0, Cat), + kf(1.75, -1.0, 2.0, -1.0, Cat), + kf(1.95, -1.0, 2.0, -1.0, Cat), + kf(2.0, -1.0, 2.0, 3.0, Cat), + kf(2.15, -1.0, 3.0, 4.0, Cat), + ], + }, + KfChannel { + part: "left_front_leg", + target: KfTarget::Rot, + frames: &[ + kf(0.0, -90.0, 0.0, 0.0, Lin), + kf(0.6667, -90.0, 0.0, 0.0, Cat), + kf(0.8333, -45.0, 0.0, 0.0, Cat), + kf(1.0, -45.0, 0.0, 0.0, Cat), + kf(1.75, -45.0, 0.0, 0.0, Cat), + kf(1.8, -90.0, 0.0, 0.0, Cat), + kf(1.95, -90.0, 0.0, 0.0, Cat), + ], + }, + KfChannel { + part: "left_front_leg", + target: KfTarget::Pos, + frames: &[ + kf(0.0, 1.0, 2.0, 2.0, Lin), + kf(0.6667, 1.0, 2.0, 2.0, Cat), + kf(0.75, 1.0, 2.0, -2.0, Cat), + kf(0.8333, 1.0, 2.0, -1.0, Cat), + kf(1.0, 1.0, 2.0, -1.0, Cat), + kf(1.75, 1.0, 2.0, -1.0, Cat), + kf(1.95, 1.0, 2.0, -1.0, Cat), + kf(2.0, 1.0, 2.0, 3.0, Cat), + kf(2.15, 1.0, 3.0, 4.0, Cat), + ], + }, + KfChannel { + part: "cube", + target: KfTarget::Rot, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.25, 0.0, 0.0, 0.0, Lin), + kf(0.35, 15.0, 0.0, 0.0, Lin), + kf(0.5, -7.5, 0.0, 0.0, Lin), + kf(0.6, 0.0, 0.0, 0.0, Lin), + kf(2.0, 0.0, 0.0, 0.0, Lin), + kf(2.05, -17.5, 0.0, 0.0, Lin), + kf(2.15, -25.0, 0.0, 0.0, Lin), + kf(2.25, 0.0, 0.0, 0.0, Lin), + kf(2.3, 12.5, 0.0, 0.0, Lin), + kf(2.5, 0.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "cube", + target: KfTarget::Pos, + frames: &[ + kf(0.25, 0.0, 0.0, 0.0, Lin), + kf(0.35, 0.0, 1.6, 0.0, Lin), + kf(0.5, 0.0, 0.5, 0.0, Lin), + kf(0.6, 0.0, 0.0, 0.0, Lin), + kf(2.0, 0.0, 0.0, 0.0, Lin), + kf(2.05, 0.0, 1.2, 0.0, Lin), + kf(2.15, 0.0, 1.7, 0.0, Lin), + kf(2.25, 0.0, 0.0, 0.0, Lin), + kf(2.3, 0.0, 1.3, 0.0, Lin), + kf(2.5, 0.0, 0.0, 0.0, Lin), + ], + }, + ], + } +}; + +static ARMADILLO_ROLL_OUT: KfAnim = { + use Kfi::{Cat, Lin}; + KfAnim { + length: 1.5, + looping: false, + channels: &[ + KfChannel { + part: "head", + target: KfTarget::Rot, + frames: &[ + kf(0.1, -50.0, 0.0, 0.0, Lin), + kf(0.15, 0.0, 0.0, 0.0, Lin), + kf(0.25, 0.0, 0.0, 0.0, Lin), + kf(0.4, 0.0, 0.0, 0.0, Lin), + kf(0.65, 0.0, 0.0, 0.0, Lin), + kf(0.7, 0.0, 0.0, 0.0, Lin), + kf(0.85, -2.5, 0.0, 0.0, Lin), + kf(0.9, 0.0, 0.0, 0.0, Lin), + kf(0.95, -7.5, 0.0, 0.0, Lin), + kf(1.05, 0.0, 0.0, 0.0, Lin), + kf(1.1, 7.5, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "head", + target: KfTarget::Pos, + frames: &[ + kf(0.0, 0.0, 1.0, 5.0, Lin), + kf(0.05, 0.0, 1.0, 0.2, Lin), + kf(0.1, 0.0, 1.0, 0.2, Lin), + kf(0.15, 0.0, 2.1, 1.2, Lin), + kf(0.25, 0.0, 1.03, 0.13, Lin), + kf(0.4, 0.0, 1.03, 0.13, Lin), + kf(0.65, 0.0, 1.03, 0.13, Lin), + kf(0.7, 0.0, 1.1, 0.2, Lin), + kf(0.75, 0.0, 4.1, 2.2, Lin), + kf(0.85, 0.0, 5.1, 3.2, Lin), + kf(0.9, 0.0, 0.1, 0.2, Lin), + kf(0.95, 0.0, 0.9, -0.8, Lin), + kf(1.05, 0.0, 0.9, 0.0, Lin), + kf(1.1, 0.0, 2.6, 0.2, Lin), + kf(1.15, 0.0, 2.4, 0.2, Lin), + kf(1.2, 0.0, 0.0, 0.2, Lin), + kf(1.25, 0.0, 0.0, 0.2, Lin), + kf(1.3, 0.0, 0.0, 0.2, Lin), + ], + }, + KfChannel { + part: "right_hind_leg", + target: KfTarget::Rot, + frames: &[ + kf(1.1, 0.0, 0.0, 0.0, Lin), + kf(1.3, 0.0, 0.0, 30.0, Lin), + kf(1.4, 0.0, 0.0, 0.0, Lin), + kf(1.45, 0.0, 0.0, 0.0, Lin), + kf(1.5, 0.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "right_hind_leg", + target: KfTarget::Pos, + frames: &[ + kf(1.1, 0.0, 3.0, -2.0, Lin), + kf(1.2, 0.0, 8.0, -2.0, Lin), + kf(1.3, -1.0, 3.0, 0.0, Lin), + kf(1.4, 0.0, 0.0, 0.0, Lin), + kf(1.45, 0.0, 0.0, 0.0, Lin), + kf(1.5, 0.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "left_hind_leg", + target: KfTarget::Rot, + frames: &[ + kf(1.1, 0.0, 0.0, 0.0, Lin), + kf(1.3, 0.0, 0.0, -30.0, Lin), + kf(1.4, 0.0, 0.0, 0.0, Lin), + kf(1.45, 0.0, 0.0, 0.0, Lin), + kf(1.5, 0.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "left_hind_leg", + target: KfTarget::Pos, + frames: &[ + kf(1.1, 0.0, 3.0, -2.0, Lin), + kf(1.2, 0.0, 8.0, -2.0, Lin), + kf(1.3, 1.0, 3.0, 0.0, Lin), + kf(1.35, 1.0, 3.0, 0.0, Lin), + kf(1.4, 0.0, 0.0, 0.0, Lin), + kf(1.45, 0.0, 0.0, 0.0, Lin), + kf(1.5, 0.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "right_front_leg", + target: KfTarget::Rot, + frames: &[ + kf(0.0, -90.0, 0.0, 0.0, Cat), + kf(0.05, -90.0, 0.0, 0.0, Cat), + kf(0.25, -45.0, 0.0, 0.0, Lin), + kf(0.55, -45.0, 0.0, 0.0, Lin), + kf(0.6, -92.5, 0.0, 0.0, Cat), + kf(1.1, -90.0, 0.0, 0.0, Cat), + kf(1.3, 0.0, 0.0, 30.0, Cat), + kf(1.4, 0.0, 0.0, 0.0, Cat), + kf(1.45, 0.0, 0.0, 0.0, Lin), + kf(1.5, 0.0, 0.0, 0.0, Cat), + ], + }, + KfChannel { + part: "right_front_leg", + target: KfTarget::Pos, + frames: &[ + kf(0.0, -1.0, 2.0, 2.0, Lin), + kf(0.05, -1.0, 2.0, 2.0, Cat), + kf(0.25, -1.0, 2.0, -1.0, Lin), + kf(0.55, -1.0, 2.0, -1.0, Lin), + kf(0.7, -1.0, 2.0, 2.63, Cat), + kf(1.1, -1.0, 2.0, 2.0, Lin), + kf(1.2, -1.0, 7.0, 2.0, Lin), + kf(1.3, -1.0, 3.0, 0.0, Lin), + kf(1.4, 0.0, 0.0, 0.0, Lin), + kf(1.45, 0.0, 0.0, 0.0, Lin), + kf(1.5, 0.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "left_front_leg", + target: KfTarget::Rot, + frames: &[ + kf(0.0, -90.0, 0.0, 0.0, Cat), + kf(0.05, -90.0, 0.0, 0.0, Cat), + kf(0.25, -45.0, 0.0, 0.0, Lin), + kf(0.55, -45.0, 0.0, 0.0, Lin), + kf(0.6, -87.5, 0.0, 0.0, Cat), + kf(1.1, -90.0, 0.0, 0.0, Cat), + kf(1.3, 0.0, 0.0, -30.0, Cat), + kf(1.4, 0.0, 0.0, 0.0, Cat), + kf(1.45, 0.0, 0.0, 0.0, Lin), + kf(1.5, 0.0, 0.0, 0.0, Cat), + ], + }, + KfChannel { + part: "left_front_leg", + target: KfTarget::Pos, + frames: &[ + kf(0.0, 1.0, 2.0, 2.0, Cat), + kf(0.05, 1.0, 2.0, 2.0, Cat), + kf(0.15, 1.0, 2.0, 0.0, Cat), + kf(0.25, 1.0, 2.0, -1.0, Lin), + kf(0.55, 1.0, 2.0, -1.0, Lin), + kf(0.7, 1.0, 2.0, 1.88, Cat), + kf(0.75, 1.0, 2.0, 2.67, Cat), + kf(1.1, 1.0, 2.0, 2.0, Cat), + kf(1.2, 1.0, 8.0, 2.0, Cat), + kf(1.25, 1.06, 5.06, 1.0, Cat), + kf(1.3, 1.0, 3.0, 0.0, Cat), + kf(1.4, 0.0, 0.0, 0.0, Cat), + kf(1.45, 0.0, 0.0, 0.0, Lin), + kf(1.5, 0.0, 0.0, 0.0, Cat), + ], + }, + KfChannel { + part: "cube", + target: KfTarget::Rot, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.05, 15.0, 0.0, 0.0, Lin), + kf(0.15, -7.5, 0.0, 0.0, Lin), + kf(0.25, 0.0, 0.0, 0.0, Lin), + kf(0.7, 0.0, 0.0, 0.0, Lin), + kf(0.75, -17.5, 0.0, 0.0, Lin), + kf(0.85, -25.0, 0.0, 0.0, Lin), + kf(0.9, 0.0, 0.0, 0.0, Lin), + kf(0.95, 12.5, 0.0, 0.0, Lin), + kf(1.05, 0.0, 0.0, 0.0, Lin), + kf(1.1, 0.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "cube", + target: KfTarget::Pos, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.05, 0.0, 1.6, 0.0, Lin), + kf(0.15, 0.0, 0.5, 0.0, Lin), + kf(0.25, 0.0, 0.0, 0.0, Lin), + kf(0.7, 0.0, 0.0, 0.0, Lin), + kf(0.75, 0.0, 1.2, 0.0, Lin), + kf(0.85, 0.0, 1.7, 0.0, Lin), + kf(0.9, 0.0, 0.0, 0.0, Lin), + kf(0.95, 0.0, 1.3, 0.0, Lin), + kf(1.05, 0.0, 0.0, 0.0, Lin), + kf(1.2, 0.0, 5.0, 0.0, Lin), + kf(1.25, 0.0, 8.0, 0.0, Lin), + kf(1.5, 0.0, 1.0, 0.0, Lin), + ], + }, + KfChannel { + part: "body", + target: KfTarget::Pos, + frames: &[ + kf(1.1, 0.0, 0.0, 0.0, Lin), + kf(1.2, 0.0, 4.0, 0.0, Lin), + kf(1.25, 0.0, 5.0, 0.0, Lin), + kf(1.3, 0.0, 4.0, 0.0, Lin), + kf(1.4, 0.0, -1.0, 0.0, Lin), + kf(1.5, 0.0, 0.0, 0.0, Lin), + ], + }, + ], + } +}; + +static ARMADILLO_BABY_ROLL_UP: KfAnim = { + use Kfi::Lin; + KfAnim { + length: 0.5, + looping: false, + channels: &[ + KfChannel { + part: "body", + target: KfTarget::Pos, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.15, 0.0, 5.0, 0.0, Lin), + kf(0.2, 0.0, 6.0, -1.0, Lin), + kf(0.25, 0.0, 6.0, -1.0, Lin), + kf(0.4, 0.0, -1.0, -1.0, Lin), + ], + }, + KfChannel { + part: "tail", + target: KfTarget::Pos, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.15, 0.0, 0.0, 0.0, Lin), + kf(0.2, 0.0, 1.0, -3.0, Lin), + kf(0.25, 0.0, 1.0, -3.0, Lin), + kf(0.4, 0.0, 2.0, -3.0, Lin), + ], + }, + KfChannel { + part: "head", + target: KfTarget::Rot, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.15, 17.5, 0.0, 0.0, Lin), + kf(0.25, -72.5, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "head", + target: KfTarget::Pos, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.15, 0.0, -1.0, 0.0, Lin), + kf(0.2, 0.0, 0.0, 1.0, Lin), + kf(0.25, 0.0, 0.0, 1.0, Lin), + kf(0.3, 0.0, 0.0, 5.0, Lin), + kf(0.4, 0.0, 0.0, 5.0, Lin), + kf(0.5, 0.0, -0.25, 5.0, Lin), + ], + }, + KfChannel { + part: "right_hind_leg", + target: KfTarget::Rot, + frames: &[kf(0.0, 0.0, 0.0, 0.0, Lin), kf(0.15, -35.0, 0.0, 0.0, Lin)], + }, + KfChannel { + part: "right_hind_leg", + target: KfTarget::Pos, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.15, 0.0, 5.0, -2.0, Lin), + kf(0.2, 0.0, 8.0, -2.0, Lin), + kf(0.25, 0.0, 8.0, -2.0, Lin), + kf(0.4, 1.0, 3.0, -2.0, Lin), + ], + }, + KfChannel { + part: "left_hind_leg", + target: KfTarget::Rot, + frames: &[kf(0.0, 0.0, 0.0, 0.0, Lin), kf(0.15, -45.0, 0.0, 0.0, Lin)], + }, + KfChannel { + part: "left_hind_leg", + target: KfTarget::Pos, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.15, 0.0, 5.0, -2.0, Lin), + kf(0.2, 0.0, 8.0, -2.0, Lin), + kf(0.25, 0.0, 8.0, -2.0, Lin), + kf(0.4, -1.0, 3.0, -3.0, Lin), + ], + }, + KfChannel { + part: "right_front_leg", + target: KfTarget::Rot, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.15, -27.5, 0.0, 0.0, Lin), + kf(0.2, -32.5, 0.0, 0.0, Lin), + kf(0.25, -85.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "right_front_leg", + target: KfTarget::Pos, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.15, 0.0, 5.0, 0.0, Lin), + kf(0.2, -1.0, 8.0, -1.0, Lin), + kf(0.25, -1.0, 9.0, -1.0, Lin), + kf(0.4, -1.0, 2.0, 3.0, Lin), + ], + }, + KfChannel { + part: "left_front_leg", + target: KfTarget::Rot, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.15, -12.5, 0.0, 0.0, Lin), + kf(0.2, -35.0, 0.0, 0.0, Lin), + kf(0.25, -85.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "left_front_leg", + target: KfTarget::Pos, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.15, 0.0, 5.0, 0.0, Lin), + kf(0.2, 1.0, 8.0, -1.0, Lin), + kf(0.25, 1.0, 9.0, -1.0, Lin), + kf(0.4, 1.0, 2.0, 3.0, Lin), + ], + }, + KfChannel { + part: "cube", + target: KfTarget::Rot, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.15, 0.0, 0.0, 0.0, Lin), + kf(0.4, -2.5, 0.0, 0.0, Lin), + kf(0.45, 5.0, 0.0, 0.0, Lin), + kf(0.5, 0.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "cube", + target: KfTarget::Pos, + frames: &[ + kf(0.0, 0.0, 3.0, 0.0, Lin), + kf(0.15, 0.0, 8.0, 0.0, Lin), + kf(0.2, 0.0, 7.0, 0.0, Lin), + kf(0.25, 0.0, 7.0, 0.0, Lin), + kf(0.4, 0.0, 1.0, 0.0, Lin), + kf(0.45, 0.0, 0.6, 0.0, Lin), + kf(0.5, 0.0, 0.0, 0.0, Lin), + ], + }, + ], + } +}; + +static ARMADILLO_BABY_WALK: KfAnim = { + use Kfi::{Cat, Lin}; + KfAnim { + length: 1.4583, + looping: true, + channels: &[ + KfChannel { + part: "body", + target: KfTarget::Rot, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Cat), + kf(0.25, 0.0, 0.0, 4.6, Cat), + kf(0.3, 0.0, 0.0, 6.81, Cat), + kf(0.5, 0.0, 0.0, 0.0, Cat), + kf(0.7, 0.0, 0.0, 0.0, Cat), + kf(0.95, 0.0, 0.0, -4.6, Cat), + kf(1.0, 0.0, 0.0, -6.89, Cat), + kf(1.25, 0.0, 0.0, 0.0, Cat), + kf(1.45, 0.0, 0.0, 0.0, Cat), + ], + }, + KfChannel { + part: "body", + target: KfTarget::Pos, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Cat), + kf(0.25, 0.0, -0.2, 0.0, Cat), + kf(0.5, 0.0, 0.0, 0.0, Cat), + kf(0.7, 0.0, 0.0, 0.0, Cat), + kf(0.95, 0.0, -0.2, 0.0, Cat), + kf(1.25, 0.0, 0.0, 0.0, Cat), + kf(1.45, 0.0, 0.0, 0.0, Cat), + ], + }, + KfChannel { + part: "tail", + target: KfTarget::Rot, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.5, -9.17, 0.0, 0.0, Lin), + kf(0.75, 5.0, 0.0, 0.0, Lin), + kf(1.2, -8.24, 0.0, 0.0, Lin), + kf(1.45, 0.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "right_hind_leg", + target: KfTarget::Rot, + frames: &[ + kf(0.0, -50.0, 0.0, 0.0, Lin), + kf(0.25, 0.0, 0.0, 0.0, Lin), + kf(0.5, 0.0, 0.0, 0.0, Lin), + kf(0.75, 50.0, 0.0, 0.0, Lin), + kf(1.0, 50.0, 0.0, 0.0, Lin), + kf(1.3, -20.0, 0.0, 0.0, Lin), + kf(1.45, -50.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "right_hind_leg", + target: KfTarget::Pos, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.25, 0.0, 0.0, -0.5, Lin), + kf(0.5, 0.0, 0.0, 0.0, Lin), + kf(1.0, 0.0, 0.0, -0.5, Lin), + kf(1.3, 0.0, 1.0, -0.18, Lin), + kf(1.45, 0.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "left_hind_leg", + target: KfTarget::Rot, + frames: &[ + kf(0.0, 50.0, 0.0, 0.0, Lin), + kf(0.25, 50.0, 0.0, 0.0, Lin), + kf(0.55, -20.0, 0.0, 0.0, Lin), + kf(0.7, -50.0, 0.0, 0.0, Lin), + kf(0.95, 0.0, 0.0, 0.0, Lin), + kf(1.2, 0.0, 0.0, 0.0, Lin), + kf(1.45, 50.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "left_hind_leg", + target: KfTarget::Pos, + frames: &[ + kf(0.0, 0.0, 0.0, -0.25, Lin), + kf(0.25, 0.0, 0.0, -0.5, Lin), + kf(0.55, 0.0, 1.0, -0.18, Lin), + kf(0.7, 0.0, 0.0, 0.0, Lin), + kf(0.95, 0.0, 0.0, -0.5, Lin), + kf(1.2, 0.0, 0.0, 0.0, Lin), + kf(1.45, 0.0, 0.0, -0.25, Lin), + ], + }, + KfChannel { + part: "right_front_leg", + target: KfTarget::Rot, + frames: &[ + kf(0.0, 50.0, 0.0, 0.0, Lin), + kf(0.3, 50.0, 0.0, 0.0, Lin), + kf(0.55, -20.0, 0.0, 0.0, Lin), + kf(0.7, -50.0, 0.0, 0.0, Lin), + kf(0.95, 0.0, 0.0, 0.0, Lin), + kf(1.2, 0.0, 0.0, 0.0, Lin), + kf(1.45, 50.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "right_front_leg", + target: KfTarget::Pos, + frames: &[ + kf(0.0, 0.0, 0.0, -0.25, Lin), + kf(0.25, 0.0, 0.0, -0.5, Lin), + kf(0.55, 0.0, 1.0, -0.18, Lin), + kf(0.7, 0.0, 0.0, 0.0, Lin), + kf(0.95, 0.0, 0.0, -0.5, Lin), + kf(1.2, 0.0, 0.0, 0.0, Lin), + kf(1.45, 0.0, 0.0, -0.25, Lin), + ], + }, + KfChannel { + part: "left_front_leg", + target: KfTarget::Rot, + frames: &[ + kf(0.0, -50.0, 0.0, 0.0, Lin), + kf(0.25, 0.0, 0.0, 0.0, Lin), + kf(0.5, 0.0, 0.0, 0.0, Lin), + kf(0.75, 50.0, 0.0, 0.0, Lin), + kf(1.0, 50.0, 0.0, 0.0, Lin), + kf(1.3, -20.0, 0.0, 0.0, Lin), + kf(1.45, -50.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "left_front_leg", + target: KfTarget::Pos, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.25, 0.0, 0.0, -0.5, Lin), + kf(0.5, 0.0, 0.0, 0.0, Lin), + kf(1.0, 0.0, 0.0, -0.5, Lin), + kf(1.3, 0.0, 1.0, -0.18, Lin), + kf(1.45, 0.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "head", + target: KfTarget::Rot, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.25, 0.0, 0.0, -2.5, Lin), + kf(0.5, 0.0, 0.0, 0.0, Lin), + kf(0.7, 0.0, 0.0, 0.0, Lin), + kf(1.0, 0.0, 0.0, 2.5, Lin), + kf(1.25, 0.0, 0.0, 0.0, Lin), + kf(1.45, 0.0, 0.0, 0.0, Lin), + ], + }, + ], + } +}; + +static ARMADILLO_BABY_ROLL_OUT: KfAnim = { + use Kfi::{Cat, Lin}; + KfAnim { + length: 1.5, + looping: false, + channels: &[ + KfChannel { + part: "head", + target: KfTarget::Rot, + frames: &[ + kf(0.1, -50.0, 0.0, 0.0, Lin), + kf(0.15, 0.0, 0.0, 0.0, Lin), + kf(0.25, 0.0, 0.0, 0.0, Lin), + kf(0.4, 0.0, 0.0, 0.0, Lin), + kf(0.65, 0.0, 0.0, 0.0, Lin), + kf(0.7, 0.0, 0.0, 0.0, Lin), + kf(0.85, -2.5, 0.0, 0.0, Lin), + kf(0.9, 0.0, 0.0, 0.0, Lin), + kf(0.95, -7.5, 0.0, 0.0, Lin), + kf(1.05, 0.0, 0.0, 0.0, Lin), + kf(1.1, 7.5, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "head", + target: KfTarget::Pos, + frames: &[ + kf(0.0, 0.0, -1.9, 5.0, Lin), + kf(0.05, 0.0, -1.0, 0.2, Lin), + kf(0.1, 0.0, 0.0, 0.2, Lin), + kf(0.15, 0.0, 2.1, 1.2, Lin), + kf(0.25, 0.0, 0.03, 0.13, Lin), + kf(0.4, 0.0, 0.03, 0.13, Lin), + kf(0.65, 0.0, 0.03, 0.13, Lin), + kf(0.7, 0.0, 0.1, 0.2, Lin), + kf(0.75, 0.0, 3.1, 2.2, Lin), + kf(0.85, 0.0, 4.1, 2.2, Lin), + kf(0.9, 0.0, 0.1, 0.2, Lin), + kf(0.95, 0.0, 0.9, -0.8, Lin), + kf(1.05, 0.0, 0.9, 0.0, Lin), + kf(1.1, 0.0, 2.6, 0.2, Lin), + kf(1.15, 0.0, 2.4, 0.2, Lin), + kf(1.2, 0.0, 0.0, 0.2, Lin), + kf(1.25, 0.0, 0.0, 0.2, Lin), + kf(1.3, 0.0, 0.0, 0.2, Lin), + ], + }, + KfChannel { + part: "right_hind_leg", + target: KfTarget::Rot, + frames: &[ + kf(1.1, 0.0, 0.0, 0.0, Lin), + kf(1.3, 0.0, 0.0, 30.0, Lin), + kf(1.4, 0.0, 0.0, 0.0, Lin), + kf(1.45, 0.0, 0.0, 0.0, Lin), + kf(1.5, 0.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "right_hind_leg", + target: KfTarget::Pos, + frames: &[ + kf(1.1, 0.0, 3.0, -2.0, Lin), + kf(1.2, 0.0, 8.0, -2.0, Lin), + kf(1.3, -1.0, 3.0, 0.0, Lin), + kf(1.4, 0.0, 0.0, 0.0, Lin), + kf(1.45, 0.0, 0.0, 0.0, Lin), + kf(1.5, 0.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "left_hind_leg", + target: KfTarget::Rot, + frames: &[ + kf(1.1, 0.0, 0.0, 0.0, Lin), + kf(1.3, 0.0, 0.0, -30.0, Lin), + kf(1.4, 0.0, 0.0, 0.0, Lin), + kf(1.45, 0.0, 0.0, 0.0, Lin), + kf(1.5, 0.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "left_hind_leg", + target: KfTarget::Pos, + frames: &[ + kf(1.1, 0.0, 3.0, -2.0, Lin), + kf(1.2, 0.0, 8.0, -2.0, Lin), + kf(1.3, 1.0, 3.0, 0.0, Lin), + kf(1.35, 1.0, 3.0, 0.0, Lin), + kf(1.4, 0.0, 0.0, 0.0, Lin), + kf(1.45, 0.0, 0.0, 0.0, Lin), + kf(1.5, 0.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "right_front_leg", + target: KfTarget::Rot, + frames: &[ + kf(0.0, -90.0, 0.0, 0.0, Cat), + kf(0.05, -90.0, 0.0, 0.0, Cat), + kf(0.25, -45.0, 0.0, 0.0, Lin), + kf(0.55, -45.0, 0.0, 0.0, Lin), + kf(0.6, -92.5, 0.0, 0.0, Cat), + kf(1.1, -90.0, 0.0, 0.0, Cat), + kf(1.3, 0.0, 0.0, 30.0, Cat), + kf(1.4, 0.0, 0.0, 0.0, Cat), + kf(1.45, 0.0, 0.0, 0.0, Lin), + kf(1.5, 0.0, 0.0, 0.0, Cat), + ], + }, + KfChannel { + part: "right_front_leg", + target: KfTarget::Pos, + frames: &[ + kf(0.0, -1.0, 2.0, 2.0, Lin), + kf(0.05, -1.0, 2.0, 2.0, Cat), + kf(0.25, 0.0, 2.0, -1.0, Lin), + kf(0.55, 0.0, 2.0, -1.0, Lin), + kf(0.7, -1.0, 2.0, 2.63, Cat), + kf(1.1, -1.0, 2.0, 2.0, Lin), + kf(1.2, -1.0, 7.0, 2.0, Lin), + kf(1.3, -1.0, 3.0, 0.0, Lin), + kf(1.4, 0.0, 0.0, 0.0, Lin), + kf(1.45, 0.0, 0.0, 0.0, Lin), + kf(1.5, 0.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "left_front_leg", + target: KfTarget::Rot, + frames: &[ + kf(0.0, -90.0, 0.0, 0.0, Cat), + kf(0.05, -90.0, 0.0, 0.0, Cat), + kf(0.25, -45.0, 0.0, 0.0, Lin), + kf(0.55, -45.0, 0.0, 0.0, Lin), + kf(0.6, -87.5, 0.0, 0.0, Cat), + kf(1.1, -90.0, 0.0, 0.0, Cat), + kf(1.3, 0.0, 0.0, -30.0, Cat), + kf(1.4, 0.0, 0.0, 0.0, Cat), + kf(1.45, 0.0, 0.0, 0.0, Lin), + kf(1.5, 0.0, 0.0, 0.0, Cat), + ], + }, + KfChannel { + part: "left_front_leg", + target: KfTarget::Pos, + frames: &[ + kf(0.0, 1.0, 2.0, 2.0, Cat), + kf(0.05, 1.0, 2.0, 2.0, Cat), + kf(0.15, 1.0, 2.0, 0.0, Cat), + kf(0.25, 0.0, 2.0, -1.0, Lin), + kf(0.55, 0.0, 2.0, -1.0, Lin), + kf(0.7, 1.0, 2.0, 1.88, Cat), + kf(0.75, 1.0, 2.0, 2.67, Cat), + kf(1.1, 1.0, 2.0, 2.0, Cat), + kf(1.2, 1.0, 8.0, 2.0, Cat), + kf(1.25, 1.06, 5.06, 1.0, Cat), + kf(1.3, 1.0, 3.0, 0.0, Cat), + kf(1.4, 0.0, 0.0, 0.0, Cat), + kf(1.45, 0.0, 0.0, 0.0, Lin), + kf(1.5, 0.0, 0.0, 0.0, Cat), + ], + }, + KfChannel { + part: "body", + target: KfTarget::Pos, + frames: &[ + kf(1.1, 0.0, 0.0, 0.0, Lin), + kf(1.2, 0.0, 4.0, 0.0, Lin), + kf(1.25, 0.0, 5.0, 0.0, Lin), + kf(1.3, 0.0, 4.0, 0.0, Lin), + kf(1.4, 0.0, -1.0, 0.0, Lin), + kf(1.5, 0.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "left_ear", + target: KfTarget::Pos, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.15, 0.0, -1.4129, 0.0617, Lin), + kf(0.25, 0.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "right_ear", + target: KfTarget::Pos, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.15, 0.0, -1.4129, 0.0617, Lin), + kf(0.25, 0.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "cube", + target: KfTarget::Rot, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.05, 15.0, 0.0, 0.0, Lin), + kf(0.15, -7.5, 0.0, 0.0, Lin), + kf(0.25, 0.0, 0.0, 0.0, Lin), + kf(0.7, 0.0, 0.0, 0.0, Lin), + kf(0.75, -17.5, 0.0, 0.0, Lin), + kf(0.85, -25.0, 0.0, 0.0, Lin), + kf(0.9, 0.0, 0.0, 0.0, Lin), + kf(0.95, 12.5, 0.0, 0.0, Lin), + kf(1.05, 0.0, 0.0, 0.0, Lin), + kf(1.1, 0.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "cube", + target: KfTarget::Pos, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.05, 0.0, 1.6, 0.0, Lin), + kf(0.15, 0.0, 0.5, 0.0, Lin), + kf(0.25, 0.0, 0.0, 0.0, Lin), + kf(0.7, 0.0, 0.0, 0.0, Lin), + kf(0.75, 0.0, 1.2, 0.0, Lin), + kf(0.85, 0.0, 1.7, 0.0, Lin), + kf(0.9, 0.0, 0.0, 0.0, Lin), + kf(0.95, 0.0, 1.3, 0.0, Lin), + kf(1.05, 0.0, 0.0, 0.0, Lin), + kf(1.2, 0.0, 5.0, 0.0, Lin), + kf(1.25, 0.0, 8.0, 0.0, Lin), + kf(1.5, 0.0, 1.0, 0.0, Lin), + ], + }, + ], + } +}; + +static ARMADILLO_BABY_PEEK: KfAnim = { + use Kfi::{Cat, Lin}; + KfAnim { + length: 2.5, + looping: false, + channels: &[ + KfChannel { + part: "head", + target: KfTarget::Rot, + frames: &[ + kf(0.0, -70.0, 0.0, 0.0, Lin), + kf(0.15, -65.0, 0.0, 0.0, Lin), + kf(0.4, -50.0, 0.0, 0.0, Lin), + kf(0.5, 0.0, 0.0, 0.0, Lin), + kf(0.7, 0.0, 0.0, 0.0, Lin), + kf(0.9, -7.5, 0.0, 45.0, Cat), + kf(1.15, -7.5, 0.0, 45.0, Lin), + kf(1.3, -0.8639, -1.4959, -39.1287, Cat), + kf(1.6, -0.8639, -1.4959, -39.1287, Lin), + kf(1.75, 0.0, 0.0, 0.0, Lin), + kf(1.8, -25.0, 0.0, 0.0, Lin), + kf(1.85, -70.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "head", + target: KfTarget::Pos, + frames: &[ + kf(0.0, 0.0, -1.25, 4.0, Lin), + kf(0.1, 0.0, -1.0, 4.0, Lin), + kf(0.15, 0.0, -1.0, 4.0, Lin), + kf(0.25, 0.0, -1.0, 5.0, Lin), + kf(0.35, 0.0, 0.0, 0.2, Lin), + kf(0.4, 0.0, 0.0, 0.2, Lin), + kf(0.45, 0.0, 0.05, 0.7, Lin), + kf(0.5, 0.0, 1.1, 1.2, Lin), + kf(0.6, 0.0, 0.1, 0.8, Lin), + kf(0.7, 0.0, 0.1, 0.7, Lin), + kf(1.75, 0.0, 0.1, 0.7, Lin), + kf(1.8, 0.0, 0.1, 0.2, Lin), + kf(1.9, 0.0, -1.2, 3.53, Lin), + kf(1.95, 0.0, -1.2, 3.53, Lin), + kf(2.0, 0.0, -1.2, 3.53, Lin), + kf(2.15, 0.0, -0.9, 4.2, Lin), + kf(2.3, 0.0, -0.9, 3.4, Lin), + kf(2.5, 0.0, -1.9, 4.1, Lin), + ], + }, + KfChannel { + part: "right_hind_leg", + target: KfTarget::Pos, + frames: &[kf(0.0, 0.0, 3.0, -2.0, Lin)], + }, + KfChannel { + part: "left_hind_leg", + target: KfTarget::Pos, + frames: &[kf(0.0, 0.0, 3.0, -2.0, Lin)], + }, + KfChannel { + part: "right_front_leg", + target: KfTarget::Rot, + frames: &[ + kf(0.0, -90.0, 0.0, 0.0, Lin), + kf(0.6, -90.0, 0.0, 0.0, Cat), + kf(0.85, -45.0, 0.0, 0.0, Cat), + kf(1.0, -45.0, 0.0, 0.0, Cat), + kf(1.75, -45.0, 0.0, 0.0, Cat), + kf(1.8, -90.0, 0.0, 0.0, Cat), + kf(1.95, -90.0, 0.0, 0.0, Cat), + ], + }, + KfChannel { + part: "right_front_leg", + target: KfTarget::Pos, + frames: &[ + kf(0.0, -1.0, 2.0, 2.0, Lin), + kf(0.6, -1.0, 2.0, 2.0, Cat), + kf(0.65, -1.0, 2.0, 1.0, Cat), + kf(0.85, 0.0, 1.0, -0.5, Cat), + kf(1.0, 0.0, 1.0, -0.5, Cat), + kf(1.75, 0.0, 1.0, -0.5, Cat), + kf(1.95, 0.0, 1.0, 3.0, Cat), + kf(2.0, -1.0, 2.0, 3.0, Cat), + kf(2.15, -1.0, 3.0, 4.0, Cat), + ], + }, + KfChannel { + part: "left_front_leg", + target: KfTarget::Rot, + frames: &[ + kf(0.0, -90.0, 0.0, 0.0, Lin), + kf(0.65, -90.0, 0.0, 0.0, Cat), + kf(0.85, -45.0, 0.0, 0.0, Cat), + kf(1.0, -45.0, 0.0, 0.0, Cat), + kf(1.75, -45.0, 0.0, 0.0, Cat), + kf(1.8, -90.0, 0.0, 0.0, Cat), + kf(1.95, -90.0, 0.0, 0.0, Cat), + ], + }, + KfChannel { + part: "left_front_leg", + target: KfTarget::Pos, + frames: &[ + kf(0.0, 1.0, 2.0, 2.0, Lin), + kf(0.65, 1.0, 2.0, 2.0, Cat), + kf(0.85, 0.0, 1.0, -0.4, Cat), + kf(1.0, 0.0, 1.0, -0.4, Cat), + kf(1.75, 0.0, 1.0, -0.4, Cat), + kf(1.95, 1.0, 1.0, 2.0, Cat), + kf(2.0, 1.0, 2.0, 3.0, Cat), + kf(2.15, 1.0, 3.0, 4.0, Cat), + ], + }, + KfChannel { + part: "left_ear", + target: KfTarget::Rot, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.4, 0.0, 0.0, 0.0, Lin), + kf(0.5, -15.0, 0.0, 0.0, Lin), + kf(0.6, 0.0, 0.0, 0.0, Lin), + kf(1.8, 0.0, 0.0, 0.0, Lin), + kf(1.85, -10.0, 0.0, 0.0, Lin), + kf(2.5, 0.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "left_ear", + target: KfTarget::Pos, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.4, 0.0, -1.13, 1.05, Lin), + kf(0.5, 0.0, -1.4129, 0.0617, Lin), + kf(0.6, 0.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "right_ear", + target: KfTarget::Rot, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.4, 0.0, 0.0, 0.0, Lin), + kf(0.5, -15.0, 0.0, 0.0, Lin), + kf(0.6, 0.0, 0.0, 0.0, Lin), + kf(1.8, 0.0, 0.0, 0.0, Lin), + kf(1.85, -10.0, 0.0, 0.0, Lin), + kf(2.5, 0.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "right_ear", + target: KfTarget::Pos, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.4, 0.0, -1.13, 1.05, Lin), + kf(0.5, 0.0, -1.4129, 0.0617, Lin), + kf(0.6, 0.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "cube", + target: KfTarget::Rot, + frames: &[ + kf(0.0, 0.0, 0.0, 0.0, Lin), + kf(0.25, 0.0, 0.0, 0.0, Lin), + kf(0.35, 15.0, 0.0, 0.0, Lin), + kf(0.5, -7.5, 0.0, 0.0, Lin), + kf(0.6, 0.0, 0.0, 0.0, Lin), + kf(2.0, 0.0, 0.0, 0.0, Lin), + kf(2.05, -17.5, 0.0, 0.0, Lin), + kf(2.15, -25.0, 0.0, 0.0, Lin), + kf(2.25, 0.0, 0.0, 0.0, Lin), + kf(2.3, 12.5, 0.0, 0.0, Lin), + kf(2.5, 0.0, 0.0, 0.0, Lin), + ], + }, + KfChannel { + part: "cube", + target: KfTarget::Pos, + frames: &[ + kf(0.25, 0.0, 0.0, 0.0, Lin), + kf(0.35, 0.0, 1.6, 0.0, Lin), + kf(0.5, 0.0, 0.5, 0.0, Lin), + kf(0.6, 0.0, 0.0, 0.0, Lin), + kf(2.0, 0.0, 0.0, 0.0, Lin), + kf(2.05, 0.0, 1.2, 0.0, Lin), + kf(2.15, 0.0, 1.7, 0.0, Lin), + kf(2.25, 0.0, 0.0, 0.0, Lin), + kf(2.3, 0.0, 1.3, 0.0, Lin), + kf(2.5, 0.0, 0.0, 0.0, Lin), + ], + }, + ], + } +}; + +#[cfg(test)] +mod tests { + use super::*; + + /// `KeyframeAnimation.bake` throws on a channel naming a part the model + /// lacks; every table must resolve against its own mesh. + #[test] + fn every_channel_names_a_part_of_its_mesh() { + for (model, t) in [ + (bake_armadillo_model(), &ADULT), + (bake_baby_armadillo_model(), &BABY), + ] { + for anim in [t.walk, t.roll_out, t.roll_up, t.peek] { + for ch in anim.channels { + part_index(&model.parts, ch.part); + } + } + } + } + + /// In the shell the cube replaces the body's own cubes, hind legs and + /// tail; out of it only the cube hides. + #[test] + fn shell_swaps_body_for_cube() { + let model = bake_armadillo_model(); + let hidden = |hiding_in_shell| { + let a = ArmadilloAnims { + hiding_in_shell, + ..Default::default() + }; + let anim = compute_armadillo_anim(&model, 0.0, 0.0, 0.0, 0.0, false, &a); + let mut names: Vec<_> = anim + .scale + .iter() + .filter(|(_, s)| *s == Vec3::ZERO) + .map(|&(i, _)| model.parts[i].name.as_str()) + .collect(); + names.sort_unstable(); + names + }; + assert_eq!( + hidden(true), + [BODY_SHELL, "left_hind_leg", "right_hind_leg", "tail"] + ); + assert_eq!(hidden(false), ["cube"]); + } +} diff --git a/pomme-client/src/renderer/entity_model/goat.rs b/pomme-client/src/renderer/entity_model/goat.rs new file mode 100644 index 000000000..f280083ca --- /dev/null +++ b/pomme-client/src/renderer/entity_model/goat.rs @@ -0,0 +1,258 @@ +//! Goat: `GoatModel`/`BabyGoatModel` meshes and their `setupAnim` over +//! `QuadrupedModel`'s. + +// Vanilla's radian literals are kept verbatim, not swapped for `consts`. +#![allow(clippy::approx_constant)] + +use glam::Vec3; + +use super::{ + BakedEntityModel, PartAnim, bake_model, compute_quadruped_anim, mirrored, part_index, rotated, + vbox, vpart, +}; + +/// Vanilla `GoatModel.createBodyLayer`, 64x64. The head's `.mirror()` carries +/// over to the goatee (`CubeListBuilder` keeps the flag). +pub fn bake_goat_model() -> BakedEntityModel { + let parts = vec![ + vpart( + "head", + None, + Vec3::new(1.0, 14.0, 0.0), + vec![ + vbox((2, 61), (-6.0, -11.0, -10.0), (3.0, 2.0, 1.0)), + mirrored(vbox((2, 61), (2.0, -11.0, -10.0), (3.0, 2.0, 1.0))), + mirrored(vbox((23, 52), (-0.5, -3.0, -14.0), (0.0, 7.0, 5.0))), + ], + ), + vpart( + "left_horn", + Some(0), + Vec3::ZERO, + vec![vbox((12, 55), (-0.01, -16.0, -10.0), (2.0, 7.0, 2.0))], + ), + vpart( + "right_horn", + Some(0), + Vec3::ZERO, + vec![vbox((12, 55), (-2.99, -16.0, -10.0), (2.0, 7.0, 2.0))], + ), + rotated( + vpart( + "nose", + Some(0), + Vec3::new(0.0, -8.0, -8.0), + vec![vbox((34, 46), (-3.0, -4.0, -8.0), (5.0, 7.0, 10.0))], + ), + Vec3::new(0.9599, 0.0, 0.0), + ), + vpart( + "body", + None, + Vec3::new(0.0, 24.0, 0.0), + vec![ + vbox((1, 1), (-4.0, -17.0, -7.0), (9.0, 11.0, 16.0)), + vbox((0, 28), (-5.0, -18.0, -8.0), (11.0, 14.0, 11.0)), + ], + ), + vpart( + "left_hind_leg", + None, + Vec3::new(1.0, 14.0, 4.0), + vec![vbox((36, 29), (0.0, 4.0, 0.0), (3.0, 6.0, 3.0))], + ), + vpart( + "right_hind_leg", + None, + Vec3::new(-3.0, 14.0, 4.0), + vec![vbox((49, 29), (0.0, 4.0, 0.0), (3.0, 6.0, 3.0))], + ), + vpart( + "left_front_leg", + None, + Vec3::new(1.0, 14.0, -6.0), + vec![vbox((49, 2), (0.0, 0.0, 0.0), (3.0, 10.0, 3.0))], + ), + vpart( + "right_front_leg", + None, + Vec3::new(-3.0, 14.0, -6.0), + vec![vbox((35, 2), (0.0, 0.0, 0.0), (3.0, 10.0, 3.0))], + ), + ]; + bake_model(parts, 64, 64) +} + +/// Vanilla `BabyGoatModel.createBodyLayer`, 64x64. +pub fn bake_baby_goat_model() -> BakedEntityModel { + let leg = |name: &str, x: f32, z: f32, tex: (i32, i32)| { + vpart( + name, + None, + Vec3::new(x, 19.5, z), + vec![vbox(tex, (-1.0, -0.5, -1.0), (2.0, 5.0, 2.0))], + ) + }; + let horn_rot = Vec3::new(-0.3926991, 0.0, 0.0); + let parts = vec![ + leg("left_hind_leg", 1.5, 3.0, (29, 12)), + leg("right_hind_leg", -1.5, 3.0, (21, 12)), + leg("right_front_leg", -1.5, -2.0, (21, 5)), + leg("left_front_leg", 1.5, -2.0, (29, 5)), + vpart( + "body", + None, + Vec3::new(0.0, 17.8, 0.0), + vec![ + vbox((0, 10), (-3.0, -2.3, -4.5), (6.0, 5.0, 9.0)), + vbox((0, 24), (-2.5, -2.2, -4.0), (5.0, 4.0, 8.0)), + ], + ), + rotated( + vpart( + "head", + None, + Vec3::new(0.0, 15.5, -3.0), + vec![vbox((0, 0), (-2.0, -3.8126, -5.1548), (4.0, 4.0, 6.0))], + ), + Vec3::new(0.4363, 0.0, 0.0), + ), + rotated( + vpart( + "right_horn", + Some(5), + Vec3::new(-1.5, -1.5, -1.0), + vec![mirrored(vbox((24, 0), (0.0, -4.5, 0.0), (1.0, 2.0, 1.0)))], + ), + horn_rot, + ), + rotated( + vpart( + "left_horn", + Some(5), + Vec3::new(-1.5, -1.5, -1.0), + vec![mirrored(vbox((24, 0), (2.0, -4.5, 0.0), (1.0, 2.0, 1.0)))], + ), + horn_rot, + ), + rotated( + vpart( + "right_ear", + Some(5), + Vec3::new(-1.7, -2.3126, 0.1452), + vec![mirrored(vbox((0, 12), (-2.0, -0.5, -0.5), (2.0, 1.0, 1.0)))], + ), + Vec3::new(0.0, -0.5236, 0.0), + ), + rotated( + vpart( + "left_ear", + Some(5), + Vec3::new(1.7, -2.3126, 0.1452), + vec![vbox((0, 12), (0.0, -0.5, -0.5), (2.0, 1.0, 1.0))], + ), + Vec3::new(0.0, 0.5236, 0.0), + ), + vpart( + "HeadMain", + Some(5), + Vec3::new(0.0, -1.3126, -1.1548), + vec![vbox((0, 0), (-2.0, -2.5, -4.0), (4.0, 4.0, 6.0))], + ), + ]; + bake_model(parts, 64, 64) +} + +/// `GoatRenderState` (vanilla defaults: both horns present). +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct GoatAnims { + pub has_left_horn: bool, + pub has_right_horn: bool, + /// `Goat.getRammingXHeadRot`, radians. + pub ramming_x_head_rot: f32, +} + +impl Default for GoatAnims { + fn default() -> Self { + Self { + has_left_horn: true, + has_right_horn: true, + ramming_x_head_rot: 0.0, + } + } +} + +/// `BabyGoatModel.setupAnim`'s head pitch while not ramming (22.5 degrees). +const BABY_HEAD_X_ROT: f32 = 0.3926991; + +/// Vanilla `GoatModel.setupAnim` (and `BabyGoatModel`'s override) over +/// `QuadrupedModel.setupAnim`. A missing horn collapses to a zero scale. +pub fn compute_goat_anim( + model: &BakedEntityModel, + head_x_rot_deg: f32, + local_head_y_rot_deg: f32, + walk_pos: f32, + walk_speed: f32, + is_baby: bool, + g: &GoatAnims, +) -> PartAnim { + let mut anim = compute_quadruped_anim( + model, + head_x_rot_deg, + local_head_y_rot_deg, + walk_pos, + walk_speed, + 0.0, + None, + ); + let head_x = if g.ramming_x_head_rot != 0.0 { + Some(g.ramming_x_head_rot) + } else if is_baby { + Some(BABY_HEAD_X_ROT) + } else { + None + }; + if let Some(head_x) = head_x { + let head = part_index(&model.parts, "head"); + anim.rotation_mut(model, head).x = head_x; + } + for (name, present) in [ + ("left_horn", g.has_left_horn), + ("right_horn", g.has_right_horn), + ] { + if !present { + anim.scale + .push((part_index(&model.parts, name), Vec3::ZERO)); + } + } + anim +} + +#[cfg(test)] +mod tests { + use super::*; + + fn head_x(is_baby: bool, ramming: f32) -> f32 { + let model = if is_baby { + bake_baby_goat_model() + } else { + bake_goat_model() + }; + let g = GoatAnims { + ramming_x_head_rot: ramming, + ..Default::default() + }; + let anim = compute_goat_anim(&model, 10.0, 0.0, 0.0, 0.0, is_baby, &g); + anim.rotation_of(&model, "head").x + } + + /// Ramming overrides the look pitch; a baby not ramming pitches a fixed + /// 22.5 degrees whatever it looks at. + #[test] + fn ramming_and_baby_override_head_pitch() { + assert_eq!(head_x(false, 0.0), 10f32.to_radians()); + assert_eq!(head_x(false, 0.3), 0.3); + assert_eq!(head_x(true, 0.0), BABY_HEAD_X_ROT); + assert_eq!(head_x(true, 0.6), 0.6); + } +} diff --git a/pomme-client/src/renderer/pipelines/entity_renderer.rs b/pomme-client/src/renderer/pipelines/entity_renderer.rs index 070351f31..34645bed7 100644 --- a/pomme-client/src/renderer/pipelines/entity_renderer.rs +++ b/pomme-client/src/renderer/pipelines/entity_renderer.rs @@ -203,6 +203,8 @@ pub struct EntityRenderInfo { /// Axolotl animator factors and baby animation clocks. pub axolotl: entity_model::AxolotlAnims, pub panda: entity_model::PandaAnims, + pub armadillo: entity_model::ArmadilloAnims, + pub goat: entity_model::GoatAnims, pub parrot: entity_model::ParrotAnims, /// `DolphinRenderState.isMoving`. pub dolphin_moving: bool, @@ -292,6 +294,8 @@ impl Default for EntityRenderInfo { fox: entity_model::FoxAnims::default(), axolotl: entity_model::AxolotlAnims::default(), panda: entity_model::PandaAnims::default(), + armadillo: entity_model::ArmadilloAnims::default(), + goat: entity_model::GoatAnims::default(), parrot: entity_model::ParrotAnims::default(), dolphin_moving: false, turtle: entity_model::TurtleAnims::default(), @@ -979,6 +983,8 @@ enum AnimationType { PolarBear, Fox, Panda, + Armadillo, + Goat, Bee, Parrot, Dolphin, @@ -1215,6 +1221,10 @@ fn mob_definitions() -> Vec { const DOLPHIN_BABY_TEX: &[&[&str]] = tex_table!("dolphin" => "dolphin_baby"); const TURTLE_TEX: &[&[&str]] = tex_table!("turtle" => "turtle"); const TURTLE_BABY_TEX: &[&[&str]] = tex_table!("turtle" => "turtle_baby"); + const ARMADILLO_TEX: &[&[&str]] = tex_table!("armadillo" => "armadillo"); + const ARMADILLO_BABY_TEX: &[&[&str]] = tex_table!("armadillo" => "armadillo_baby"); + const GOAT_TEX: &[&[&str]] = tex_table!("goat" => "goat"); + const GOAT_BABY_TEX: &[&[&str]] = tex_table!("goat" => "goat_baby"); // Indexed by `Llama.Variant` id. const LLAMA_TEX: &[&[&str]] = tex_table!("llama" => "llama_creamy", "llama_white", "llama_brown", "llama_gray"); @@ -1366,6 +1376,34 @@ fn mob_definitions() -> Vec { adult_overlays: vec![], baby_overlays: vec![], }, + MobDef { + kind: EntityKind::Armadillo, + anim: AnimationType::Armadillo, + adult: vec![opaque( + entity_model::bake_armadillo_model(), + ARMADILLO_TEX, + 64, + )], + baby: Some(opaque( + entity_model::bake_baby_armadillo_model(), + ARMADILLO_BABY_TEX, + 64, + )), + adult_overlays: vec![], + baby_overlays: vec![], + }, + MobDef { + kind: EntityKind::Goat, + anim: AnimationType::Goat, + adult: vec![opaque(entity_model::bake_goat_model(), GOAT_TEX, 64)], + baby: Some(opaque( + entity_model::bake_baby_goat_model(), + GOAT_BABY_TEX, + 64, + )), + adult_overlays: vec![], + baby_overlays: vec![], + }, MobDef { kind: EntityKind::Axolotl, anim: AnimationType::Axolotl, @@ -2682,6 +2720,24 @@ impl EntityRenderer { info.bat_elapsed_secs, info.bat_resting, ), + AnimationType::Armadillo => entity_model::compute_armadillo_anim( + model, + info.head_x_rot_deg, + local_head_y, + info.walk_anim_pos, + info.walk_anim_speed, + info.is_baby, + &info.armadillo, + ), + AnimationType::Goat => entity_model::compute_goat_anim( + model, + info.head_x_rot_deg, + local_head_y, + info.walk_anim_pos, + info.walk_anim_speed, + info.is_baby, + &info.goat, + ), AnimationType::Axolotl => entity_model::compute_axolotl_anim( model, info.is_baby, @@ -3522,6 +3578,8 @@ pub(crate) fn standing_eye_height(kind: EntityKind, is_baby: bool) -> f32 { // Explicit `BABY_DIMENSIONS` eye heights. EntityKind::Fox | EntityKind::PolarBear if is_baby => 0.34375, EntityKind::Panda if is_baby => 0.28125, + EntityKind::Armadillo if is_baby => 0.21875, + EntityKind::Goat if is_baby => 0.59375, EntityKind::Axolotl | EntityKind::Dolphin if is_baby => 0.09375, EntityKind::Mooshroom if is_baby => 0.69, // Explicit `eyeHeight`s, halved; the 0.85 ratio would miss them. diff --git a/pomme-protocol/src/data/codec-fixtures-26.2.json b/pomme-protocol/src/data/codec-fixtures-26.2.json index 2bd44b00f..9b8bd1a81 100644 --- a/pomme-protocol/src/data/codec-fixtures-26.2.json +++ b/pomme-protocol/src/data/codec-fixtures-26.2.json @@ -26,6 +26,7 @@ "player_position/mixed_relative": "033ff80000000000004050000000000000c00000000000000000000000000000003fe0000000000000000000000000000042b400004120000000000112", "player_rotation/relative_yaw": "4234000001c1f0000000", "respawn/keep_all": "03146d696e6563726166743a7468655f6e6574686572fffffffffffffff90002010001146d696e6563726166743a7468655f6e65746865720000017fffffd046002003", + "set_entity_data/armadillo_state": "07122403ff", "set_entity_data/copper_golem": "07102602112503ff", "set_entity_data/sniffer_state": "07122305ff", "set_simulation_distance/twelve": "0c", diff --git a/pomme-protocol/src/entity_data.rs b/pomme-protocol/src/entity_data.rs index 15edf8635..8f732b085 100644 --- a/pomme-protocol/src/entity_data.rs +++ b/pomme-protocol/src/entity_data.rs @@ -142,8 +142,8 @@ macro_rules! serializers_26_2 { ]))), // SNIFFER_STATE: an idMapper VarInt, read like INT. S::Int, - // ARMADILLO_STATE - S::Skip(ENUM), + // ARMADILLO_STATE: an idMapper VarInt, read like INT. + S::Int, // COPPER_GOLEM_STATE and WEATHERING_COPPER_STATE: idMapper VarInts. S::Int, S::Int, @@ -335,6 +335,24 @@ mod tests { ); } + /// `Armadillo.ARMADILLO_STATE` (index 18) set to UNROLLING by vanilla's + /// encoder. + #[test] + fn armadillo_state_matches_vanilla_encoder() { + let payload = crate::vanilla_fixtures::case("set_entity_data/armadillo_state"); + let mut r = Reader::new(&payload); + assert_eq!(r.var_i32().unwrap(), 7); + let values = codec().read_entity_data(&mut r).expect("valid entity data"); + r.ensure_eof().unwrap(); + assert_eq!( + values, + [DataValue { + index: 18, + value: Value::Int(3), + }] + ); + } + #[test] fn decodes_an_item_stack() { let stone = id(ClientRegistry::Item, "stone"); diff --git a/tests/ingame/entities/armadillo.toml b/tests/ingame/entities/armadillo.toml new file mode 100644 index 000000000..70907f12b --- /dev/null +++ b/tests/ingame/entities/armadillo.toml @@ -0,0 +1,65 @@ +# Armadillos at rest, three-quarter view from above: an adult and a baby +# (its own mesh) standing, then an adult and a baby rolled up in the scared +# state, where the shell cube replaces the body and hind legs. + +[[step]] +wait_ms = 2000 + +[[step]] +chat = "/gamemode creative" + +[[step]] +chat = "/gamerule advance_time false" + +[[step]] +chat = "/time set 6000" + +[[step]] +deterministic = true + +# A pillar to look down from. +[[step]] +chat = "/fill 0 -60 0 0 -59 0 minecraft:stone" + +[[step]] +chat = "/tp @s 0.5 -58 0.5 0 40" + +[[step]] +expect = { player = { position = [0.5, -58.0, 0.5] } } + +[[step]] +chat = "/summon minecraft:armadillo -1.5 -60 3.5 {NoAI:1b,Rotation:[150f,0f]}" + +[[step]] +expect = { entity = "armadillo", baby = false } + +[[step]] +chat = "/summon minecraft:armadillo -0.25 -60 3.5 {NoAI:1b,Age:-24000,Rotation:[210f,0f]}" + +[[step]] +expect = { entity = "armadillo", baby = true } + +[[step]] +chat = "/summon minecraft:armadillo 1.25 -60 3.5 {NoAI:1b,state:\"scared\",Rotation:[150f,0f]}" + +[[step]] +chat = "/summon minecraft:armadillo 2.5 -60 3.5 {NoAI:1b,Age:-24000,state:\"scared\",Rotation:[210f,0f]}" + +[[step]] +look_at = [0.5, -59.8, 3.5] + +# Chat fades after 10 seconds. +[[step]] +wait_ms = 11000 + +[[step]] +screenshot = "armadillos" + +[[step]] +chat = "/kill @e[type=!player]" + +[[step]] +chat = "/fill 0 -60 0 0 -59 0 minecraft:air" + +[[step]] +chat = "/gamerule advance_time true" diff --git a/tests/ingame/entities/goat.toml b/tests/ingame/entities/goat.toml new file mode 100644 index 000000000..a0cb19fa3 --- /dev/null +++ b/tests/ingame/entities/goat.toml @@ -0,0 +1,62 @@ +# Goats at rest, three-quarter view from above: an adult with both horns, an +# adult that lost its left horn, and a baby (its own mesh, head pitched down +# 22.5 degrees). + +[[step]] +wait_ms = 2000 + +[[step]] +chat = "/gamemode creative" + +[[step]] +chat = "/gamerule advance_time false" + +[[step]] +chat = "/time set 6000" + +[[step]] +deterministic = true + +# A pillar to look down from. +[[step]] +chat = "/fill 0 -60 0 0 -58 0 minecraft:stone" + +[[step]] +chat = "/tp @s 0.5 -57 0.5 0 40" + +[[step]] +expect = { player = { position = [0.5, -57.0, 0.5] } } + +[[step]] +chat = "/summon minecraft:goat -1.5 -60 5.5 {NoAI:1b,Rotation:[150f,0f]}" + +[[step]] +expect = { entity = "goat", baby = false } + +[[step]] +chat = "/summon minecraft:goat 0.5 -60 6.5 {NoAI:1b,HasLeftHorn:0b,Rotation:[210f,0f]}" + +[[step]] +chat = "/summon minecraft:goat 2.5 -60 5 {NoAI:1b,Age:-24000,Rotation:[210f,0f]}" + +[[step]] +expect = { entity = "goat", baby = true } + +[[step]] +look_at = [0.5, -59.5, 5.5] + +# Chat fades after 10 seconds. +[[step]] +wait_ms = 11000 + +[[step]] +screenshot = "goats" + +[[step]] +chat = "/kill @e[type=!player]" + +[[step]] +chat = "/fill 0 -60 0 0 -58 0 minecraft:air" + +[[step]] +chat = "/gamerule advance_time true" diff --git a/tools/codec-oracle/CodecOracle.java b/tools/codec-oracle/CodecOracle.java index a5feff208..5c9216e24 100644 --- a/tools/codec-oracle/CodecOracle.java +++ b/tools/codec-oracle/CodecOracle.java @@ -53,6 +53,7 @@ import net.minecraft.world.clock.WorldClocks; import net.minecraft.world.entity.PositionMoveRotation; import net.minecraft.world.entity.Relative; +import net.minecraft.world.entity.animal.armadillo.Armadillo; import net.minecraft.world.entity.animal.golem.CopperGolemState; import net.minecraft.world.entity.animal.sniffer.Sniffer; import net.minecraft.world.item.equipment.trim.TrimPattern; @@ -109,6 +110,10 @@ public static void main(String[] args) throws Exception { EntityDataSerializers.WEATHERING_COPPER_STATE), WeatheringCopper.WeatherState.WEATHERED), SynchedEntityData.DataValue.create(new EntityDataAccessor<>(17, EntityDataSerializers.COPPER_GOLEM_STATE), CopperGolemState.DROPPING_ITEM)))); + encode("set_entity_data/armadillo_state", ClientboundSetEntityDataPacket.STREAM_CODEC::encode, + new ClientboundSetEntityDataPacket(7, List.of(SynchedEntityData.DataValue.create( + new EntityDataAccessor<>(18, EntityDataSerializers.ARMADILLO_STATE), + Armadillo.ArmadilloState.UNROLLING)))); JsonObject out = new JsonObject(); out.addProperty("version", SharedConstants.getCurrentVersion().name());