diff --git a/pomme-client/src/app/core.rs b/pomme-client/src/app/core.rs index 2b176192..e9e5d388 100644 --- a/pomme-client/src/app/core.rs +++ b/pomme-client/src/app/core.rs @@ -1085,15 +1085,12 @@ impl AppCore { mesh.z_size, ); } - NetworkEvent::EntityBabyFlag { id, is_baby } => { - game.entity_store.set_baby(id, is_baby); + NetworkEvent::EntityData { id, index, value } => { + game.entity_store.apply_entity_data(id, index, value); } NetworkEvent::EntityPose { id, is_crouching } => { game.entity_store.set_crouching(id, is_crouching); } - NetworkEvent::SheepWoolData { id, color, sheared } => { - game.entity_store.set_sheep_wool(id, color, sheared); - } NetworkEvent::SheepEatStart { id } => { game.entity_store.start_sheep_eat(id); } @@ -1115,12 +1112,6 @@ impl AppCore { NetworkEvent::EntityVariant { id, kind, variant } => { game.entity_store.set_variant(id, kind, variant); } - NetworkEvent::MobFlag { id, flag, value } => { - game.entity_store.set_mob_flag(id, flag, value); - } - NetworkEvent::SlimeSize { id, size } => { - game.entity_store.set_slime_size(id, size); - } NetworkEvent::VillagerData { id, kind, @@ -1130,15 +1121,9 @@ impl AppCore { game.entity_store .set_villager_data(id, kind, profession, level); } - NetworkEvent::VillagerUnhappy { id, counter } => { - game.entity_store.set_villager_unhappy(id, counter); - } NetworkEvent::EntityCustomName { id, name } => { game.entity_store.set_custom_name(id, name); } - NetworkEvent::EntityAggressive { id, aggressive } => { - game.entity_store.set_aggressive(id, aggressive); - } NetworkEvent::EntitySwing { id } => { game.entity_store.start_swing(id); } diff --git a/pomme-client/src/app/phases/in_game.rs b/pomme-client/src/app/phases/in_game.rs index e1aa3097..b6bfe72a 100644 --- a/pomme-client/src/app/phases/in_game.rs +++ b/pomme-client/src/app/phases/in_game.rs @@ -2270,6 +2270,7 @@ pub fn update_game( flap: extras.flap, flap_speed: extras.flap_speed, is_creepy: e.is_creepy, + is_converting: e.is_converting, // TODO: derive from the main-hand item (vanilla // `isHoldingItem`) once mob equipment tracking lands. is_holding_item: e.witch_drinking, @@ -2319,6 +2320,7 @@ pub fn update_game( flap: 0.0, flap_speed: 0.0, is_creepy: false, + is_converting: false, is_holding_item: false, nose_wobble_speed: 0.0, body_transform: None, @@ -2821,9 +2823,15 @@ fn entity_extras(entity_id: i32, e: &crate::entity::LivingEntity, alpha: f32) -> ..Default::default() }, EntityKind::Sheep => sheep_extras(entity_id, e, alpha), - EntityKind::Villager => villager_extras(e), - // Spider eyes overlay is always visible (slot 0). - EntityKind::Spider => EntityExtras { + EntityKind::Villager => villager_like_extras(e, &VILLAGER_TYPE_HAT), + EntityKind::ZombieVillager => villager_like_extras(e, &ZOMBIE_VILLAGER_TYPE_HAT), + EntityKind::Bogged => EntityExtras { + overlay_tints: SLOT0_TINTS, + variant_index: e.is_sheared as u32, + ..Default::default() + }, + // Always-visible slot-0 overlay (spider eyes, drowned/stray clothing). + EntityKind::Spider | EntityKind::Drowned | EntityKind::Stray => EntityExtras { overlay_tints: SLOT0_TINTS, ..Default::default() }, @@ -2916,6 +2924,8 @@ fn sheep_extras(entity_id: i32, e: &crate::entity::LivingEntity, alpha: f32) -> /// `.png.mcmeta` files under `textures/entity/villager/` (hardcoded — no /// resource-pack support). 0 = none, 1 = partial, 2 = full. const VILLAGER_TYPE_HAT: [u8; 7] = [2, 0, 0, 0, 2, 0, 0]; // desert, snow = full +// `zombie_villager/type/` ships no `.mcmeta` files at all. +const ZOMBIE_VILLAGER_TYPE_HAT: [u8; 7] = [0; 7]; const VILLAGER_PROFESSION_HAT: [u8; 15] = [ 0, // none 0, // armorer @@ -2936,14 +2946,15 @@ const VILLAGER_PROFESSION_HAT: [u8; 15] = [ /// Overlay slots: 0 = biome type (full model), 1 = biome type (no-hat model), /// 2 = profession, 3 = profession level. Mirrors vanilla -/// `VillagerProfessionLayer.submit`. -fn villager_extras(e: &crate::entity::LivingEntity) -> EntityExtras { +/// `VillagerProfessionLayer.submit`, shared by villager and zombie villager +/// (which differ only in their type-hat `.mcmeta` tables). +fn villager_like_extras(e: &crate::entity::LivingEntity, type_hat_table: &[u8; 7]) -> EntityExtras { use crate::entity::villager::VillagerProfession; let kind = e.villager_kind as usize; let profession = e.villager_profession as usize; - let type_hat = VILLAGER_TYPE_HAT[kind]; + let type_hat = type_hat_table[kind]; let prof_hat = VILLAGER_PROFESSION_HAT[profession]; let type_hat_visible = prof_hat == 0 || (prof_hat == 1 && type_hat != 2); diff --git a/pomme-client/src/entity/mod.rs b/pomme-client/src/entity/mod.rs index 5517139e..91677f65 100644 --- a/pomme-client/src/entity/mod.rs +++ b/pomme-client/src/entity/mod.rs @@ -14,13 +14,35 @@ use crate::physics::collision::resolve_collision; use crate::world::block::{FluidKind, fluid}; use crate::world::chunk::ChunkStore; -/// Kind-gated boolean mob states; each flag belongs to one mob kind and -/// [`EntityStore::set_mob_flag`] drops writes for a mismatched entity. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum MobFlag { - CreeperPowered, - EndermanCreepy, - WitchDrinking, +/// A scalar synched-entity-data value, forwarded raw from the wire; +/// [`EntityStore::apply_entity_data`] gives it meaning per (kind, index). +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum MetaValue { + Bool(bool), + Int(i32), + Byte(u8), + Float(f32), + Long(i64), +} + +/// Kinds whose entity-data index 16 is the baby flag: `AgeableMob` +/// descendants plus the zombie family (which defines its own baby flag at +/// the same index). NOT baby at 16: Bogged (sheared), Skeleton (stray +/// conversion), Witch (Raider celebrating), fish (from-bucket). +fn is_baby_kind(kind: EntityKind) -> bool { + matches!( + kind, + EntityKind::Pig + | EntityKind::Cow + | EntityKind::Sheep + | EntityKind::Chicken + | EntityKind::Villager + | EntityKind::Slime + | EntityKind::Zombie + | EntityKind::Husk + | EntityKind::Drowned + | EntityKind::ZombieVillager + ) } const INTERPOLATION_STEPS: i32 = 3; @@ -48,9 +70,11 @@ pub struct LivingEntity { pub is_crouching: bool, pub on_ground: bool, pub wool_color: Option, + /// Sheep wool shorn / bogged mushrooms shorn. pub is_sheared: bool, - /// Registry/wire variant slot; meaning is per-kind (pool index for - /// cow/chicken). Normalized in `EntityStore::set_variant`. + /// Registry/wire variant slot; meaning is per-kind. Holder-backed values + /// are pre-resolved to pool indices by the net handler; raw-int kinds are + /// normalized in `EntityStore::apply_entity_data`. pub variant: u32, /// Chicken wing-flap state (vanilla `Chicken.aiStep`): `flap` is the /// unbounded wing-cycle phase, `flap_speed` the 0..1 amplitude. @@ -66,6 +90,8 @@ pub struct LivingEntity { /// Enderman screaming flag — raises the head and jitters the render /// position. pub is_creepy: bool, + /// Zombie-family conversion (drowning / villager cure) — body-yaw shake. + pub is_converting: bool, /// Witch drinking flag — swings the nose down toward the potion. pub witch_drinking: bool, pub villager_kind: VillagerKind, @@ -138,6 +164,7 @@ impl LivingEntity { prev_squish: 0.0, slime_size: 1, is_creepy: false, + is_converting: false, witch_drinking: false, villager_kind: VillagerKind::default(), villager_profession: VillagerProfession::default(), @@ -619,9 +646,43 @@ impl EntityStore { } } - pub fn set_baby(&mut self, id: i32, is_baby: bool) { - if let Some(entity) = self.living.get_mut(&id) { - entity.is_baby = is_baby; + /// Resolves a raw synched-entity-data scalar per (kind, index), the + /// direct analogue of vanilla's per-class `onSyncedDataUpdated`. Index + /// arithmetic follows the registration chain: `Entity` 0-7, + /// `LivingEntity` 8-14, `Mob` 15, `AgeableMob` 16 baby + 17 age-locked, + /// first subclass field 18. Where an index moved between supported + /// versions both spots are accepted (the other index carries an + /// incompatible type or kind on each version). + pub fn apply_entity_data(&mut self, id: i32, index: u8, value: MetaValue) { + use MetaValue::{Bool, Byte, Int}; + let Some(entity) = self.living.get_mut(&id) else { + return; + }; + match (entity.entity_type, index, value) { + // Mob flags byte: bit 0x04 = aggressive. + (_, 15, Byte(f)) => entity.aggressive = f & 0x04 != 0, + (k, 16, Bool(b)) if is_baby_kind(k) => entity.is_baby = b, + // Skeleton: powder-snow stray conversion; drives the vanilla + // `isShaking` body jitter. + (EntityKind::Skeleton, 16, Bool(b)) => entity.is_converting = b, + (EntityKind::Bogged, 16, Bool(b)) => entity.is_sheared = b, + // Slime size: 16 on 1.21.9-26.1.x, 18 since Slime joined + // AgeableMob in 26.2. + (EntityKind::Slime, 16 | 18, Int(s)) => entity.slime_size = s.clamp(1, 127) as u8, + // Sheep wool byte (low nibble = DyeColor, bit 0x10 = sheared): + // 17 on 1.21.9-26.1.x, 18 on 26.2. + (EntityKind::Sheep, 17 | 18, Byte(w)) => { + entity.wool_color = Some(w & 0x0F); + entity.is_sheared = w & 0x10 != 0; + } + (EntityKind::Creeper, 17, Bool(b)) => entity.powered = b, + (EntityKind::Enderman, 17, Bool(b)) => entity.is_creepy = b, + (EntityKind::Witch, 17, Bool(b)) => entity.witch_drinking = b, + // Zombie-family underwater conversion / zombie villager curing. + (EntityKind::Zombie | EntityKind::Husk | EntityKind::Drowned, 18, Bool(b)) + | (EntityKind::ZombieVillager, 19, Bool(b)) => entity.is_converting = b, + (EntityKind::Villager, 18, Int(c)) => entity.unhappy_counter = c, + _ => {} } } @@ -631,15 +692,6 @@ impl EntityStore { } } - pub fn set_sheep_wool(&mut self, id: i32, color: u8, sheared: bool) { - if let Some(entity) = self.living.get_mut(&id) - && entity.entity_type == EntityKind::Sheep - { - entity.wool_color = Some(color); - entity.is_sheared = sheared; - } - } - /// `kind` is the mob the emitting handler arm resolved the value for; /// metadata indices are overloaded across kinds, so a mismatched entity /// ignores the write. @@ -651,29 +703,6 @@ impl EntityStore { } } - /// Applies a [`MobFlag`] write, dropping it when the entity isn't the - /// flag's mob (metadata indices are overloaded across kinds, so the net - /// handler emits every candidate flag for an ambiguous boolean). - pub fn set_mob_flag(&mut self, id: i32, flag: MobFlag, value: bool) { - let Some(entity) = self.living.get_mut(&id) else { - return; - }; - match (flag, entity.entity_type) { - (MobFlag::CreeperPowered, EntityKind::Creeper) => entity.powered = value, - (MobFlag::EndermanCreepy, EntityKind::Enderman) => entity.is_creepy = value, - (MobFlag::WitchDrinking, EntityKind::Witch) => entity.witch_drinking = value, - _ => {} - } - } - - pub fn set_slime_size(&mut self, id: i32, size: i32) { - if let Some(entity) = self.living.get_mut(&id) - && entity.entity_type == EntityKind::Slime - { - entity.slime_size = size.clamp(1, 127) as u8; - } - } - pub fn set_villager_data( &mut self, id: i32, @@ -682,7 +711,10 @@ impl EntityStore { level: u32, ) { if let Some(entity) = self.living.get_mut(&id) - && entity.entity_type == EntityKind::Villager + && matches!( + entity.entity_type, + EntityKind::Villager | EntityKind::ZombieVillager + ) { entity.villager_kind = kind; entity.villager_profession = profession; @@ -690,14 +722,6 @@ impl EntityStore { } } - pub fn set_villager_unhappy(&mut self, id: i32, counter: i32) { - if let Some(entity) = self.living.get_mut(&id) - && entity.entity_type == EntityKind::Villager - { - entity.unhappy_counter = counter; - } - } - pub fn start_sheep_eat(&mut self, id: i32) { if let Some(entity) = self.living.get_mut(&id) && entity.entity_type == EntityKind::Sheep @@ -720,12 +744,6 @@ impl EntityStore { } } - pub fn set_aggressive(&mut self, id: i32, aggressive: bool) { - if let Some(entity) = self.living.get_mut(&id) { - entity.aggressive = aggressive; - } - } - /// Begins an arm swing (server `Animate` packet). Restarts when idle or /// past the halfway point (vanilla `LivingEntity.swing`); `swing_time` /// counts down, so that is `swing_time <= SWING_DURATION / 2`. @@ -846,5 +864,10 @@ pub fn is_living_mob(kind: &EntityKind) -> bool { | EntityKind::Enderman | EntityKind::Slime | EntityKind::Witch + | EntityKind::Husk + | EntityKind::Drowned + | EntityKind::ZombieVillager + | EntityKind::Stray + | EntityKind::Bogged ) } diff --git a/pomme-client/src/net/handler.rs b/pomme-client/src/net/handler.rs index 76aad7de..2eea53cf 100644 --- a/pomme-client/src/net/handler.rs +++ b/pomme-client/src/net/handler.rs @@ -8,7 +8,7 @@ use crossbeam_channel::Sender; use super::NetworkEvent; use super::commands::{CommandTree, SharedCommandTree}; use super::sender::PacketSender; -use crate::entity::MobFlag; +use crate::entity::MetaValue; use crate::entity::components::Position; use crate::renderer::pipelines::entity_renderer::{CHICKEN_VARIANT_ORDER, COW_VARIANT_ORDER}; use crate::ui::text::format_text_spans; @@ -455,67 +455,34 @@ pub fn handle_game_packet( is_crouching: matches!(pose, azalea_entity::Pose::Crouching), }); } - if item.index == 16 - && let azalea_entity::EntityDataValue::Boolean(is_baby) = &item.value - { - let _ = event_tx.try_send(NetworkEvent::EntityBabyFlag { + // Scalar values are forwarded raw; the store resolves their + // meaning per (kind, index) like vanilla `onSyncedDataUpdated` + // (`EntityStore::apply_entity_data`). + let scalar = match &item.value { + azalea_entity::EntityDataValue::Boolean(v) => Some(MetaValue::Bool(*v)), + azalea_entity::EntityDataValue::Int(v) => Some(MetaValue::Int(*v)), + azalea_entity::EntityDataValue::Byte(v) => Some(MetaValue::Byte(*v)), + azalea_entity::EntityDataValue::Float(v) => Some(MetaValue::Float(*v)), + azalea_entity::EntityDataValue::Long(v) => Some(MetaValue::Long(*v)), + _ => None, + }; + if let Some(value) = scalar { + let _ = event_tx.try_send(NetworkEvent::EntityData { id: p.id.0, - is_baby: *is_baby, + index: item.index, + value, }); } - // Index 16 (Int) = player score / slime size (Slime's - // DATA_ID_SIZE sits at 16 on 1.21.9-26.1.x; 26.2 moved it to - // 18 when Slime gained AgeableMob's fields). Emit both; - // consumers filter by entity type. - if item.index == 16 + // Index 18 (Int) on players = score (16 on 1.21.9-26.1.x, + // before Avatar took 15/16). Kind-blind; the consumer applies + // it only to the local player. + if (item.index == 16 || item.index == 18) && let azalea_entity::EntityDataValue::Int(score) = &item.value { let _ = event_tx.try_send(NetworkEvent::PlayerScore { entity_id: p.id.0, score: *score, }); - let _ = event_tx.try_send(NetworkEvent::SlimeSize { - id: p.id.0, - size: *score, - }); - } - // Index 15 = mob flags byte (AbstractInsentient): bit 0x04 = aggressive. - if item.index == 15 - && let azalea_entity::EntityDataValue::Byte(flags) = &item.value - { - let _ = event_tx.try_send(NetworkEvent::EntityAggressive { - id: p.id.0, - aggressive: (*flags & 0x04) != 0, - }); - } - // Index 17 = sheep wool/sheared byte (low nibble = DyeColor, bit 4 = sheared). - // Emit unconditionally; consumer filters by entity type. - if item.index == 17 - && let azalea_entity::EntityDataValue::Byte(packed) = &item.value - { - let _ = event_tx.try_send(NetworkEvent::SheepWoolData { - id: p.id.0, - color: *packed & 0x0F, - sheared: (*packed & 0x10) != 0, - }); - } - // Index 17 (Boolean) = creeper powered / enderman creepy / witch - // drinking. Emit every candidate; the store applies only the - // flag matching the entity's kind. - if item.index == 17 - && let azalea_entity::EntityDataValue::Boolean(flag) = &item.value - { - for f in [ - MobFlag::CreeperPowered, - MobFlag::EndermanCreepy, - MobFlag::WitchDrinking, - ] { - let _ = event_tx.try_send(NetworkEvent::MobFlag { - id: p.id.0, - flag: f, - value: *flag, - }); - } } // Index 2 = custom name (Optional); needed for jeb_ sheep detection. if item.index == 2 @@ -528,42 +495,27 @@ pub fn handle_game_packet( if item.index == 18 && let azalea_entity::EntityDataValue::CowVariant(variant) = &item.value { - use azalea_registry::DataRegistry; let _ = event_tx.try_send(variant_event( registry_holder, p.id.0, EntityKind::Cow, - variant.protocol_id(), + variant, )); } // Index 18 on chickens = ChickenVariant Holder. if item.index == 18 && let azalea_entity::EntityDataValue::ChickenVariant(variant) = &item.value { - use azalea_registry::DataRegistry; let _ = event_tx.try_send(variant_event( registry_holder, p.id.0, EntityKind::Chicken, - variant.protocol_id(), + variant, )); } - // Index 18 (Int) = villager unhappy counter / slime size. Emit - // both; consumers filter by entity type. - if item.index == 18 - && let azalea_entity::EntityDataValue::Int(value) = &item.value - { - let _ = event_tx.try_send(NetworkEvent::VillagerUnhappy { - id: p.id.0, - counter: *value, - }); - let _ = event_tx.try_send(NetworkEvent::SlimeSize { - id: p.id.0, - size: *value, - }); - } - // Index 19 on villagers = VillagerData (type/profession/level). - if item.index == 19 + // Index 19 on villagers / 20 on zombie villagers = VillagerData + // (type/profession/level). + if (item.index == 19 || item.index == 20) && let azalea_entity::EntityDataValue::VillagerData(data) = &item.value { let _ = event_tx.try_send(NetworkEvent::VillagerData { @@ -771,12 +723,12 @@ fn variant_event( registry_holder: &RegistryHolder, id: i32, kind: EntityKind, - protocol_id: u32, + holder: &impl azalea_registry::DataRegistry, ) -> NetworkEvent { NetworkEvent::EntityVariant { id, kind, - variant: variant_index(registry_holder, kind, protocol_id), + variant: variant_index(registry_holder, kind, holder.protocol_id()), } } diff --git a/pomme-client/src/net/mod.rs b/pomme-client/src/net/mod.rs index 9c105c9e..f3725a72 100644 --- a/pomme-client/src/net/mod.rs +++ b/pomme-client/src/net/mod.rs @@ -17,7 +17,7 @@ use azalea_registry::builtin::{BlockEntityKind, EntityKind}; use glam::DVec3; use simdnbt::owned::NbtCompound; -use crate::entity::MobFlag; +use crate::entity::MetaValue; use crate::entity::components::Position; use crate::entity::villager::{VillagerKind, VillagerProfession}; @@ -288,19 +288,18 @@ pub enum NetworkEvent { id: i32, head_y_rot_deg: f32, }, - EntityBabyFlag { + /// A raw scalar entity-data value; `EntityStore::apply_entity_data` + /// resolves its meaning per (kind, index) like vanilla's + /// `onSyncedDataUpdated`. + EntityData { id: i32, - is_baby: bool, + index: u8, + value: MetaValue, }, EntityPose { id: i32, is_crouching: bool, }, - SheepWoolData { - id: i32, - color: u8, - sheared: bool, - }, SheepEatStart { id: i32, }, @@ -308,45 +307,23 @@ pub enum NetworkEvent { FinishUseItem { id: i32, }, - /// Registry/wire variant slot; meaning is per-kind (pool index for - /// cow/chicken). Per-kind normalization lives in - /// `EntityStore::set_variant`; `kind` is the mob the emitting arm - /// resolved for, guarding overloaded metadata indices. + /// Registry/wire variant slot; meaning is per-kind. `kind` is the mob + /// the emitting arm resolved for, guarding overloaded metadata indices. EntityVariant { id: i32, kind: EntityKind, variant: u32, }, - /// Kind-gated boolean mob state; the store drops writes whose entity - /// kind doesn't match the flag's mob (metadata indices are overloaded - /// across kinds). - MobFlag { - id: i32, - flag: MobFlag, - value: bool, - }, - SlimeSize { - id: i32, - size: i32, - }, VillagerData { id: i32, kind: VillagerKind, profession: VillagerProfession, level: u32, }, - VillagerUnhappy { - id: i32, - counter: i32, - }, EntityCustomName { id: i32, name: Option, }, - EntityAggressive { - id: i32, - aggressive: bool, - }, EntitySwing { id: i32, }, diff --git a/pomme-client/src/renderer/entity_model.rs b/pomme-client/src/renderer/entity_model.rs index a746ce5c..3c5a6ef1 100644 --- a/pomme-client/src/renderer/entity_model.rs +++ b/pomme-client/src/renderer/entity_model.rs @@ -39,6 +39,16 @@ fn quadruped_legs( ] } +/// Index of the named part, panicking at bake time if the mesh changed +/// shape — positional indexing into a shared parts builder goes stale +/// silently. +fn part_index(parts: &[EntityPart], name: &str) -> usize { + parts + .iter() + .position(|p| p.name == name) + .unwrap_or_else(|| panic!("mesh has a {name} part")) +} + /// Mirror a cube's geometry across x=0 WITHOUT flipping UVs (e.g. chicken /// wings and legs share one un-mirrored texture — vanilla quirk). Pair with /// `mirror: true` where vanilla's `.mirror()` UV flip is also wanted. @@ -132,6 +142,12 @@ impl BakedEntityModel { break; } } + // Scaled roots (`bake_scaled`) pre-bake `offset * factor`, so an + // anim translation must scale too; children inherit the scale + // through the parent matrix instead. No-op at the default 1.0. + if part.parent.is_none() { + extra_translation *= self.part_scales.get(i).copied().unwrap_or(1.0); + } let pivot = part.offset + extra_translation; let offset = match self.convention { @@ -561,31 +577,251 @@ pub fn bake_zombie_model() -> BakedEntityModel { bake_model(zombie_parts(), 64, 64) } -/// Baby zombie: adult mesh transformed per vanilla `BabyModelTransform` — head -/// scales 0.75, body/limbs 0.5, each pivot shifted so the feet stay grounded. -/// Per-part scaling is geometry-only, so UVs are untouched. +/// Vanilla `CubeDeformation` inflate applied mesh-wide (vanilla rebuilds +/// layer meshes at `createMesh(g)`): every cube grows by `g` on top of its +/// own deformation. +fn inflate(parts: &mut [EntityPart], g: f32) { + for part in parts { + for cube in &mut part.cubes { + cube.deformation += g; + } + } +} + +/// Vanilla `BabyZombieModel.createBodyLayer(g)`: a dedicated 64x64 baby mesh +/// with its own UVs, not a scaled adult. `g` inflates everything but the +/// head, whose second cube is a fixed 0.25 overlay. +fn baby_zombie_parts(g: f32) -> Vec { + let limb = |name: &str, pivot: Vec3, uv: (u32, u32), origin_y: f32, h: f32| { + vpart( + name, + None, + pivot, + vec![ModelCube { + deformation: g, + ..vbox(uv, (-1.0, origin_y, -1.0), (2.0, h, 2.0)) + }], + ) + }; + vec![ + vpart( + "body", + None, + Vec3::new(0.0, 17.5, 0.0), + vec![ModelCube { + deformation: g, + ..vbox((16, 16), (-2.0, -2.5, -1.0), (4.0, 5.0, 2.0)) + }], + ), + vpart( + "head", + None, + Vec3::new(0.0, 15.25, 0.0), + vec![ + vbox((3, 3), (-3.0, -6.25, -3.0), (6.0, 6.0, 6.0)), + ModelCube { + deformation: 0.25, + ..vbox((35, 3), (-3.0, -6.15, -3.0), (6.0, 6.0, 6.0)) + }, + ], + ), + limb("right_arm", Vec3::new(-3.0, 15.5, 0.0), (36, 16), -0.5, 5.0), + limb("left_arm", Vec3::new(3.0, 15.5, 0.0), (28, 16), -0.5, 5.0), + limb("right_leg", Vec3::new(-1.0, 20.0, 0.0), (8, 16), 0.0, 4.0), + limb("left_leg", Vec3::new(1.0, 20.0, 0.0), (0, 16), 0.0, 4.0), + ] +} + pub fn bake_baby_zombie_model() -> BakedEntityModel { - const HEAD_SCALE: f32 = 0.75; - const BODY_SCALE: f32 = 0.5; + bake_model(baby_zombie_parts(0.0), 64, 64) +} + +/// Husk: the zombie mesh under vanilla's `MeshTransformer.scaling(1.0625)` +/// (`ModelLayers.HUSK`). The baby husk is NOT scaled. +pub fn bake_husk_model() -> BakedEntityModel { + bake_scaled(zombie_parts(), 1.0625, 64) +} + +/// Vanilla `DrownedModel.createBodyLayer(g)`: the zombie mesh with the left +/// arm and leg given their own UV regions instead of mirrored ones. `g` 0.25 +/// is the clothing layer. +// TODO: swim pose and body pitch (`DrownedModel.setupAnim` swimAmount path) +// once a swim_amount ramp from the pose metadata exists. +pub fn bake_drowned_model(g: f32) -> BakedEntityModel { let mut parts = zombie_parts(); - let mut scales = Vec::with_capacity(parts.len()); - for part in &mut parts { - let (scale, lift) = if part.name == "head" { - (HEAD_SCALE, 16.0) - } else { - (BODY_SCALE, 24.0) - }; - part.offset = (part.offset + Vec3::new(0.0, lift, 0.0)) * scale; - scales.push(scale); + // Vanilla gives the drowned's left limbs their own UV regions. + let left_arm = part_index(&parts, "left_arm"); + parts[left_arm].cubes = vec![vbox((32, 48), (-1.0, -2.0, -2.0), (4.0, 12.0, 4.0))]; + let left_leg = part_index(&parts, "left_leg"); + parts[left_leg].cubes = vec![vbox((16, 48), (-2.0, 0.0, -2.0), (4.0, 12.0, 4.0))]; + inflate(&mut parts, g); + bake_model(parts, 64, 64) +} + +/// Vanilla `BabyDrownedModel` delegates to `BabyZombieModel` — zombie UVs, +/// not the drowned left-limb remap. +pub fn bake_baby_drowned_outer_model() -> BakedEntityModel { + bake_model(baby_zombie_parts(0.25), 64, 64) +} + +/// Vanilla `ZombieVillagerModel`: villager-shaped 10-tall head (the nose is a +/// cube inside the head part) and jacketed body on zombie-animated limbs, +/// 64x64 sheet. NOT villager-scaled (`LayerDefinitions` applies no +/// `villagerLikeScale` here). +fn zombie_villager_parts() -> Vec { + vec![ + vpart( + "head", + None, + Vec3::ZERO, + vec![ + vbox((0, 0), (-4.0, -10.0, -4.0), (8.0, 10.0, 8.0)), + // Nose. + vbox((24, 0), (-1.0, -3.0, -6.0), (2.0, 4.0, 2.0)), + ], + ), + vpart( + "hat", + Some(0), + Vec3::ZERO, + vec![ModelCube { + deformation: 0.5, + ..vbox((32, 0), (-4.0, -10.0, -4.0), (8.0, 10.0, 8.0)) + }], + ), + EntityPart { + default_rotation: Vec3::new(-std::f32::consts::FRAC_PI_2, 0.0, 0.0), + ..vpart( + "hat_rim", + Some(1), + Vec3::ZERO, + vec![vbox((30, 47), (-8.0, -8.0, -6.0), (16.0, 16.0, 1.0))], + ) + }, + vpart( + "body", + None, + Vec3::ZERO, + vec![ + vbox((16, 20), (-4.0, 0.0, -3.0), (8.0, 12.0, 6.0)), + // Jacket. + ModelCube { + deformation: 0.05, + ..vbox((0, 38), (-4.0, 0.0, -3.0), (8.0, 20.0, 6.0)) + }, + ], + ), + vpart( + "right_arm", + None, + Vec3::new(-5.0, 2.0, 0.0), + vec![vbox((44, 22), (-3.0, -2.0, -2.0), (4.0, 12.0, 4.0))], + ), + vpart( + "left_arm", + None, + Vec3::new(5.0, 2.0, 0.0), + vec![ModelCube { + mirror: true, + ..vbox((44, 22), (-1.0, -2.0, -2.0), (4.0, 12.0, 4.0)) + }], + ), + vpart( + "right_leg", + None, + Vec3::new(-2.0, 12.0, 0.0), + vec![vbox((0, 22), (-2.0, 0.0, -2.0), (4.0, 12.0, 4.0))], + ), + vpart( + "left_leg", + None, + Vec3::new(2.0, 12.0, 0.0), + vec![ModelCube { + mirror: true, + ..vbox((0, 22), (-2.0, 0.0, -2.0), (4.0, 12.0, 4.0)) + }], + ), + ] +} + +pub fn bake_zombie_villager_model(no_hat: bool) -> BakedEntityModel { + let mut parts = zombie_villager_parts(); + if no_hat { + clear_head_subtree(&mut parts); } - let mut model = bake_model(parts, 64, 64); - model.part_scales = scales; - model + bake_model(parts, 64, 64) +} + +/// Vanilla `BabyZombieVillagerModel`: hand-authored 64x64 baby mesh with real +/// arm/leg parts (humanoid-animated, unlike the crossed-arm baby villager). +/// The hat_rim hangs off the head, not the hat. +fn baby_zombie_villager_parts() -> Vec { + let limb = |name: &str, pivot: Vec3, uv: (u32, u32), h: f32| { + vpart( + name, + None, + pivot, + vec![vbox(uv, (-1.0, -0.5, -1.0), (2.0, h, 2.0))], + ) + }; + vec![ + vpart( + "body", + None, + Vec3::new(0.0, 18.75, 0.0), + vec![ + vbox((0, 15), (-2.0, -2.75, -1.5), (4.0, 5.0, 3.0)), + ModelCube { + deformation: 0.1, + ..vbox((16, 22), (-2.0, -2.75, -1.5), (4.0, 6.0, 3.0)) + }, + ], + ), + vpart( + "head", + None, + Vec3::new(0.0, 16.0, 0.0), + vec![vbox((0, 0), (-4.0, -8.0, -3.5), (8.0, 8.0, 7.0))], + ), + vpart( + "hat", + Some(1), + Vec3::new(0.0, -4.0, 0.0), + vec![ModelCube { + deformation: 0.3, + ..vbox((0, 31), (-4.0, -4.0, -3.5), (8.0, 8.0, 7.0)) + }], + ), + vpart( + "hat_rim", + Some(1), + Vec3::new(0.0, -4.5, 0.0), + vec![vbox((0, 46), (-7.0, -0.5, -6.0), (14.0, 1.0, 12.0))], + ), + vpart( + "nose", + Some(1), + Vec3::new(0.0, -1.0, -4.0), + vec![vbox((23, 0), (-1.0, -1.0, -0.5), (2.0, 2.0, 1.0))], + ), + limb("right_arm", Vec3::new(-3.0, 15.5, 0.0), (24, 15), 5.0), + limb("left_arm", Vec3::new(3.0, 15.5, 0.0), (16, 15), 5.0), + limb("right_leg", Vec3::new(-1.0, 21.5, 0.0), (8, 23), 3.0), + limb("left_leg", Vec3::new(1.0, 21.5, 0.0), (0, 23), 3.0), + ] +} + +pub fn bake_baby_zombie_villager_model(no_hat: bool) -> BakedEntityModel { + let mut parts = baby_zombie_villager_parts(); + if no_hat { + clear_head_subtree(&mut parts); + } + bake_model(parts, 64, 64) } /// Skeleton: humanoid layout with thin 2×12×2 limbs, 64×32 sheet /// (`SkeletonModel.createDefaultSkeletonMesh`). -pub fn bake_skeleton_model() -> BakedEntityModel { +fn skeleton_parts() -> Vec { let arm = ModelCube { origin: Vec3::new(-1.0, -2.0, -1.0), size: Vec3::new(2.0, 12.0, 2.0), @@ -600,7 +836,93 @@ pub fn bake_skeleton_model() -> BakedEntityModel { deformation: 0.0, mirror: false, }; - bake_model(humanoid_parts(arm, leg, 2.0), 64, 32) + humanoid_parts(arm, leg, 2.0) +} + +pub fn bake_skeleton_model() -> BakedEntityModel { + bake_model(skeleton_parts(), 64, 32) +} + +/// Stray/bogged clothing (`SkeletonClothingLayer`): the thick humanoid mesh +/// inflated by `g`, worn over the thin skeleton bones. 64×32 sheet. +fn skeleton_clothing_parts(g: f32) -> Vec { + let mut parts = zombie_parts(); + inflate(&mut parts, g); + parts +} + +pub fn bake_skeleton_clothing_model(g: f32) -> BakedEntityModel { + bake_model(skeleton_clothing_parts(g), 64, 32) +} + +/// The six mushroom quads on a bogged's head, three crossed pairs at 45/135 +/// degrees (vanilla `BoggedModel`; the empty `mushrooms` container part is +/// flattened away, angle literals are pi/4, 3pi/4 and -pi/2 rounded to f32). +/// Sheared keeps the parts with no cubes so every bogged model shares one +/// part order. +fn mushroom_parts(sheared: bool) -> Vec { + use std::f32::consts::{FRAC_PI_2, FRAC_PI_4}; + // (name, first index, texOffs, origin y, pivot, laid flat on the back) + let pairs = [ + ( + "red_mushroom", + 1, + (50, 16), + -3.0, + Vec3::new(3.0, -8.0, 3.0), + false, + ), + ( + "brown_mushroom", + 1, + (50, 22), + -3.0, + Vec3::new(-3.0, -8.0, -3.0), + false, + ), + ( + "brown_mushroom", + 3, + (50, 28), + -4.0, + Vec3::new(-2.0, -1.0, 4.0), + true, + ), + ]; + let mut parts = Vec::with_capacity(6); + for (name, first, uv, origin_y, pivot, flat) in pairs { + for (i, angle) in [FRAC_PI_4, 3.0 * FRAC_PI_4].into_iter().enumerate() { + let rot = if flat { + Vec3::new(-FRAC_PI_2, 0.0, angle) + } else { + Vec3::new(0.0, angle, 0.0) + }; + let cubes = if sheared { + vec![] + } else { + vec![vbox(uv, (-3.0, origin_y, 0.0), (6.0, 4.0, 0.0))] + }; + parts.push(EntityPart { + default_rotation: rot, + ..vpart(&format!("{name}_{}", first + i), Some(0), pivot, cubes) + }); + } + } + parts +} + +pub fn bake_bogged_model(sheared: bool) -> BakedEntityModel { + let mut parts = skeleton_parts(); + parts.extend(mushroom_parts(sheared)); + bake_model(parts, 64, 32) +} + +/// Bogged clothing padded with the empty mushroom parts (overlay part order +/// must match the base's). +pub fn bake_bogged_clothing_model() -> BakedEntityModel { + let mut parts = skeleton_clothing_parts(0.2); + parts.extend(mushroom_parts(true)); + bake_model(parts, 64, 32) } /// Creeper: head + upright body + four legs, animated as a quadruped @@ -1463,18 +1785,18 @@ fn clear_head_subtree(parts: &mut [EntityPart]) { /// grounded (`PartPose.scaled(f).translated(0, 24.016 * (1 - f), 0)`). const VILLAGER_SCALE: f32 = 0.9375; -/// Bakes with `VILLAGER_SCALE` applied to the roots only: the transform chain -/// propagates a root's scale to child pivots and geometry like vanilla's pose -/// stack (children would double-scale). -fn bake_villager_like(mut parts: Vec, tex_h: u32) -> BakedEntityModel { +/// Bakes with vanilla's `MeshTransformer.scaling(factor)` applied to the +/// roots only: the transform chain propagates a root's scale to child pivots +/// and geometry like vanilla's pose stack (children would double-scale). +fn bake_scaled(mut parts: Vec, factor: f32, tex_h: u32) -> BakedEntityModel { let mut scales = Vec::with_capacity(parts.len()); for part in parts.iter_mut() { let is_root = part.parent.is_none(); if is_root { - part.offset = part.offset * VILLAGER_SCALE - + Vec3::new(0.0, MODEL_REBASE_Y * (1.0 - VILLAGER_SCALE), 0.0); + part.offset = + part.offset * factor + Vec3::new(0.0, MODEL_REBASE_Y * (1.0 - factor), 0.0); } - scales.push(if is_root { VILLAGER_SCALE } else { 1.0 }); + scales.push(if is_root { factor } else { 1.0 }); } let mut model = bake_model(parts, 64, tex_h); model.part_scales = scales; @@ -1486,7 +1808,7 @@ pub fn bake_villager_model(no_hat: bool) -> BakedEntityModel { if no_hat { clear_head_subtree(&mut parts); } - bake_villager_like(parts, 64) + bake_scaled(parts, VILLAGER_SCALE, 64) } pub fn bake_baby_villager_model(no_hat: bool) -> BakedEntityModel { @@ -1603,22 +1925,16 @@ pub fn bake_slime_outer_model() -> BakedEntityModel { /// its witch.png region is fully transparent, so its cubes are dropped. fn witch_parts() -> Vec { let mut parts = villager_parts(); - let index_of = |parts: &[EntityPart], name: &str| { - parts - .iter() - .position(|p| p.name == name) - .unwrap_or_else(|| panic!("villager mesh has a {name}")) - }; - let head = index_of(&parts, "head"); - let hat = index_of(&parts, "hat"); - let nose = index_of(&parts, "nose"); + let head = part_index(&parts, "head"); + let hat = part_index(&parts, "hat"); + let nose = part_index(&parts, "nose"); parts[hat] = vpart( "hat", Some(head), Vec3::new(-5.0, -10.03125, -5.0), vec![vbox((0, 64), (0.0, 0.0, 0.0), (10.0, 2.0, 10.0))], ); - let hat_rim = index_of(&parts, "hat_rim"); + let hat_rim = part_index(&parts, "hat_rim"); parts[hat_rim].cubes.clear(); // The cone stacks parent hat -> hat2 -> hat3 -> hat4; the appended parts // land at indices n, n+1, n+2. @@ -1669,7 +1985,7 @@ fn witch_parts() -> Vec { } pub fn bake_witch_model() -> BakedEntityModel { - bake_villager_like(witch_parts(), 128) + bake_scaled(witch_parts(), VILLAGER_SCALE, 128) } pub fn compute_humanoid_anim( diff --git a/pomme-client/src/renderer/pipelines/entity_renderer.rs b/pomme-client/src/renderer/pipelines/entity_renderer.rs index 13ed7197..2eda723e 100644 --- a/pomme-client/src/renderer/pipelines/entity_renderer.rs +++ b/pomme-client/src/renderer/pipelines/entity_renderer.rs @@ -59,6 +59,8 @@ pub struct EntityRenderInfo { pub flap_speed: f32, /// Enderman screaming state — raises the head. pub is_creepy: bool, + /// Zombie-family conversion — shakes the whole body. + pub is_converting: bool, /// Witch drinking. Driven by the using-item metadata flag rather than /// vanilla's `isHoldingItem` (main-hand item check) — pomme tracks no /// mob equipment; the two only diverge for command-equipped witches. @@ -285,6 +287,35 @@ struct MobDef { } fn mob_definitions() -> Vec { + // One single-fallback texture entry per name under an entity texture dir. + macro_rules! tex_table { + ($dir:expr => $($name:literal),+ $(,)?) => { + &[$(&[concat!("minecraft/textures/entity/", $dir, "/", $name, ".png")]),+] + }; + } + // The villager and zombie-villager overlay dirs ship identical + // registry-ordered file names; each list is written once here and both + // mobs' tables expand from it. Types index by the builtin VillagerKind + // registry order, professions by VillagerProfession order minus "none" + // (which has no texture), levels by profession level 1-5 minus one. + macro_rules! villager_type_table { + ($dir:expr) => { + tex_table!($dir => "desert", "jungle", "plains", "savanna", "snow", "swamp", "taiga") + }; + } + macro_rules! villager_profession_table { + ($dir:expr) => { + tex_table!($dir => "armorer", "butcher", "cartographer", "cleric", "farmer", + "fisherman", "fletcher", "leatherworker", "librarian", "mason", "nitwit", + "shepherd", "toolsmith", "weaponsmith") + }; + } + macro_rules! villager_level_table { + ($dir:expr) => { + tex_table!($dir => "stone", "iron", "gold", "emerald", "diamond") + }; + } + const PIG_ADULT_TEX: &[&[&str]] = &[&[ "minecraft/textures/entity/pig/pig_temperate.png", "minecraft/textures/entity/pig/temperate_pig.png", @@ -318,77 +349,51 @@ fn mob_definitions() -> Vec { &["minecraft/textures/entity/chicken/chicken_warm_baby.png"], &["minecraft/textures/entity/chicken/chicken_cold_baby.png"], ]; - const SHEEP_ADULT_TEX: &[&[&str]] = &[&["minecraft/textures/entity/sheep/sheep.png"]]; - const SHEEP_BABY_TEX: &[&[&str]] = &[&["minecraft/textures/entity/sheep/sheep_baby.png"]]; - const SHEEP_WOOL_UNDERCOAT_TEX: &[&[&str]] = - &[&["minecraft/textures/entity/sheep/sheep_wool_undercoat.png"]]; - const SHEEP_WOOL_TEX: &[&[&str]] = &[&["minecraft/textures/entity/sheep/sheep_wool.png"]]; - const SHEEP_BABY_WOOL_TEX: &[&[&str]] = - &[&["minecraft/textures/entity/sheep/sheep_wool_baby.png"]]; - const PLAYER_TEX: &[&[&str]] = &[&["minecraft/textures/entity/player/wide/steve.png"]]; - const ZOMBIE_TEX: &[&[&str]] = &[&["minecraft/textures/entity/zombie/zombie.png"]]; - const SKELETON_TEX: &[&[&str]] = &[&["minecraft/textures/entity/skeleton/skeleton.png"]]; - const CREEPER_TEX: &[&[&str]] = &[&["minecraft/textures/entity/creeper/creeper.png"]]; - const CREEPER_ARMOR_TEX: &[&[&str]] = - &[&["minecraft/textures/entity/creeper/creeper_armor.png"]]; - const SPIDER_TEX: &[&[&str]] = &[&["minecraft/textures/entity/spider/spider.png"]]; - const SPIDER_EYES_TEX: &[&[&str]] = &[&["minecraft/textures/entity/spider/spider_eyes.png"]]; - const ENDERMAN_TEX: &[&[&str]] = &[&["minecraft/textures/entity/enderman/enderman.png"]]; - const ENDERMAN_EYES_TEX: &[&[&str]] = - &[&["minecraft/textures/entity/enderman/enderman_eyes.png"]]; - const SLIME_TEX: &[&[&str]] = &[&["minecraft/textures/entity/slime/slime.png"]]; + const SHEEP_ADULT_TEX: &[&[&str]] = tex_table!("sheep" => "sheep"); + const SHEEP_BABY_TEX: &[&[&str]] = tex_table!("sheep" => "sheep_baby"); + const SHEEP_WOOL_UNDERCOAT_TEX: &[&[&str]] = tex_table!("sheep" => "sheep_wool_undercoat"); + const SHEEP_WOOL_TEX: &[&[&str]] = tex_table!("sheep" => "sheep_wool"); + const SHEEP_BABY_WOOL_TEX: &[&[&str]] = tex_table!("sheep" => "sheep_wool_baby"); + const PLAYER_TEX: &[&[&str]] = tex_table!("player/wide" => "steve"); + const ZOMBIE_TEX: &[&[&str]] = tex_table!("zombie" => "zombie"); + const ZOMBIE_BABY_TEX: &[&[&str]] = tex_table!("zombie" => "zombie_baby"); + const HUSK_TEX: &[&[&str]] = tex_table!("zombie" => "husk"); + const HUSK_BABY_TEX: &[&[&str]] = tex_table!("zombie" => "husk_baby"); + const DROWNED_TEX: &[&[&str]] = tex_table!("zombie" => "drowned"); + const DROWNED_BABY_TEX: &[&[&str]] = tex_table!("zombie" => "drowned_baby"); + const DROWNED_OUTER_TEX: &[&[&str]] = tex_table!("zombie" => "drowned_outer_layer"); + const DROWNED_OUTER_BABY_TEX: &[&[&str]] = tex_table!("zombie" => "drowned_outer_layer_baby"); + const ZOMBIE_VILLAGER_TEX: &[&[&str]] = tex_table!("zombie_villager" => "zombie_villager"); + const ZOMBIE_VILLAGER_BABY_TEX: &[&[&str]] = + tex_table!("zombie_villager" => "zombie_villager_baby"); + const ZOMBIE_VILLAGER_TYPE_TEX: &[&[&str]] = villager_type_table!("zombie_villager/type"); + const ZOMBIE_VILLAGER_BABY_TYPE_TEX: &[&[&str]] = villager_type_table!("zombie_villager/baby"); + const ZOMBIE_VILLAGER_PROFESSION_TEX: &[&[&str]] = + villager_profession_table!("zombie_villager/profession"); + const ZOMBIE_VILLAGER_LEVEL_TEX: &[&[&str]] = + villager_level_table!("zombie_villager/profession_level"); + const SKELETON_TEX: &[&[&str]] = tex_table!("skeleton" => "skeleton"); + const STRAY_TEX: &[&[&str]] = tex_table!("skeleton" => "stray"); + const STRAY_OVERLAY_TEX: &[&[&str]] = tex_table!("skeleton" => "stray_overlay"); + const BOGGED_TEX: &[&[&str]] = tex_table!("skeleton" => "bogged"); + const BOGGED_OVERLAY_TEX: &[&[&str]] = tex_table!("skeleton" => "bogged_overlay"); + const CREEPER_TEX: &[&[&str]] = tex_table!("creeper" => "creeper"); + const CREEPER_ARMOR_TEX: &[&[&str]] = tex_table!("creeper" => "creeper_armor"); + const SPIDER_TEX: &[&[&str]] = tex_table!("spider" => "spider"); + const SPIDER_EYES_TEX: &[&[&str]] = tex_table!("spider" => "spider_eyes"); + const ENDERMAN_TEX: &[&[&str]] = tex_table!("enderman" => "enderman"); + const ENDERMAN_EYES_TEX: &[&[&str]] = tex_table!("enderman" => "enderman_eyes"); + const SLIME_TEX: &[&[&str]] = tex_table!("slime" => "slime"); const WITCH_TEX: &[&[&str]] = &[&[ "minecraft/textures/entity/witch/witch.png", "minecraft/textures/entity/witch.png", ]]; - const VILLAGER_TEX: &[&[&str]] = &[&["minecraft/textures/entity/villager/villager.png"]]; - const VILLAGER_BABY_TEX: &[&[&str]] = - &[&["minecraft/textures/entity/villager/villager_baby.png"]]; - // Indexed by the builtin VillagerKind registry order. - const VILLAGER_TYPE_TEX: &[&[&str]] = &[ - &["minecraft/textures/entity/villager/type/desert.png"], - &["minecraft/textures/entity/villager/type/jungle.png"], - &["minecraft/textures/entity/villager/type/plains.png"], - &["minecraft/textures/entity/villager/type/savanna.png"], - &["minecraft/textures/entity/villager/type/snow.png"], - &["minecraft/textures/entity/villager/type/swamp.png"], - &["minecraft/textures/entity/villager/type/taiga.png"], - ]; - const VILLAGER_BABY_TYPE_TEX: &[&[&str]] = &[ - &["minecraft/textures/entity/villager/baby/desert.png"], - &["minecraft/textures/entity/villager/baby/jungle.png"], - &["minecraft/textures/entity/villager/baby/plains.png"], - &["minecraft/textures/entity/villager/baby/savanna.png"], - &["minecraft/textures/entity/villager/baby/snow.png"], - &["minecraft/textures/entity/villager/baby/swamp.png"], - &["minecraft/textures/entity/villager/baby/taiga.png"], - ]; - // Indexed by VillagerProfession registry order minus one ("none" has no - // texture). - const VILLAGER_PROFESSION_TEX: &[&[&str]] = &[ - &["minecraft/textures/entity/villager/profession/armorer.png"], - &["minecraft/textures/entity/villager/profession/butcher.png"], - &["minecraft/textures/entity/villager/profession/cartographer.png"], - &["minecraft/textures/entity/villager/profession/cleric.png"], - &["minecraft/textures/entity/villager/profession/farmer.png"], - &["minecraft/textures/entity/villager/profession/fisherman.png"], - &["minecraft/textures/entity/villager/profession/fletcher.png"], - &["minecraft/textures/entity/villager/profession/leatherworker.png"], - &["minecraft/textures/entity/villager/profession/librarian.png"], - &["minecraft/textures/entity/villager/profession/mason.png"], - &["minecraft/textures/entity/villager/profession/nitwit.png"], - &["minecraft/textures/entity/villager/profession/shepherd.png"], - &["minecraft/textures/entity/villager/profession/toolsmith.png"], - &["minecraft/textures/entity/villager/profession/weaponsmith.png"], - ]; - // Indexed by profession level 1-5 minus one. - const VILLAGER_LEVEL_TEX: &[&[&str]] = &[ - &["minecraft/textures/entity/villager/profession_level/stone.png"], - &["minecraft/textures/entity/villager/profession_level/iron.png"], - &["minecraft/textures/entity/villager/profession_level/gold.png"], - &["minecraft/textures/entity/villager/profession_level/emerald.png"], - &["minecraft/textures/entity/villager/profession_level/diamond.png"], - ]; + const VILLAGER_TEX: &[&[&str]] = tex_table!("villager" => "villager"); + const VILLAGER_BABY_TEX: &[&[&str]] = tex_table!("villager" => "villager_baby"); + const VILLAGER_TYPE_TEX: &[&[&str]] = villager_type_table!("villager/type"); + const VILLAGER_BABY_TYPE_TEX: &[&[&str]] = villager_type_table!("villager/baby"); + const VILLAGER_PROFESSION_TEX: &[&[&str]] = villager_profession_table!("villager/profession"); + const VILLAGER_LEVEL_TEX: &[&[&str]] = villager_level_table!("villager/profession_level"); // Base and baby models, plus opaque overlays (sheep wool), are all Opaque. fn opaque( @@ -404,6 +409,38 @@ fn mob_definitions() -> Vec { } } + // Cutout layers over a villager-like base skin (vanilla + // `VillagerProfessionLayer`, shared by villager and zombie villager): + // slot 0 = biome type, slot 1 = biome type on the no-hat model (used when + // the profession texture brings its own hat), slot 2 = profession, slot 3 + // = profession level badge. entity_extras gates slot 0 xor 1 and picks + // each slot's texture variant. The `bake` parameter takes `no_hat`. + fn villager_like_overlays( + bake: fn(bool) -> BakedEntityModel, + type_tex: &'static [&'static [&'static str]], + profession_tex: &'static [&'static [&'static str]], + level_tex: &'static [&'static [&'static str]], + ) -> Vec { + // Slots 0/2/3 share one bake of the hatted model. + let hatted = bake(false); + vec![ + opaque(hatted.clone(), type_tex, 64), + opaque(bake(true), type_tex, 64), + opaque(hatted.clone(), profession_tex, 64), + opaque(hatted, level_tex, 64), + ] + } + + fn villager_like_baby_overlays( + bake: fn(bool) -> BakedEntityModel, + type_tex: &'static [&'static [&'static str]], + ) -> Vec { + vec![ + opaque(bake(false), type_tex, 64), + opaque(bake(true), type_tex, 64), + ] + } + vec![ MobDef { kind: EntityKind::Pig, @@ -494,12 +531,72 @@ fn mob_definitions() -> Vec { adult: vec![opaque(entity_model::bake_zombie_model(), ZOMBIE_TEX, 64)], baby: Some(opaque( entity_model::bake_baby_zombie_model(), - ZOMBIE_TEX, + ZOMBIE_BABY_TEX, + 64, + )), + adult_overlays: vec![], + baby_overlays: vec![], + }, + MobDef { + kind: EntityKind::Husk, + anim: AnimationType::Zombie, + adult: vec![opaque(entity_model::bake_husk_model(), HUSK_TEX, 64)], + baby: Some(opaque( + entity_model::bake_baby_zombie_model(), + HUSK_BABY_TEX, 64, )), adult_overlays: vec![], baby_overlays: vec![], }, + MobDef { + kind: EntityKind::Drowned, + anim: AnimationType::Zombie, + adult: vec![opaque( + entity_model::bake_drowned_model(0.0), + DROWNED_TEX, + 64, + )], + baby: Some(opaque( + entity_model::bake_baby_zombie_model(), + DROWNED_BABY_TEX, + 64, + )), + adult_overlays: vec![opaque( + entity_model::bake_drowned_model(0.25), + DROWNED_OUTER_TEX, + 64, + )], + baby_overlays: vec![opaque( + entity_model::bake_baby_drowned_outer_model(), + DROWNED_OUTER_BABY_TEX, + 64, + )], + }, + MobDef { + kind: EntityKind::ZombieVillager, + anim: AnimationType::Zombie, + adult: vec![opaque( + entity_model::bake_zombie_villager_model(false), + ZOMBIE_VILLAGER_TEX, + 64, + )], + baby: Some(opaque( + entity_model::bake_baby_zombie_villager_model(false), + ZOMBIE_VILLAGER_BABY_TEX, + 64, + )), + adult_overlays: villager_like_overlays( + entity_model::bake_zombie_villager_model, + ZOMBIE_VILLAGER_TYPE_TEX, + ZOMBIE_VILLAGER_PROFESSION_TEX, + ZOMBIE_VILLAGER_LEVEL_TEX, + ), + baby_overlays: villager_like_baby_overlays( + entity_model::bake_baby_zombie_villager_model, + ZOMBIE_VILLAGER_BABY_TYPE_TEX, + ), + }, MobDef { kind: EntityKind::Skeleton, anim: AnimationType::Skeleton, @@ -512,6 +609,37 @@ fn mob_definitions() -> Vec { adult_overlays: vec![], baby_overlays: vec![], }, + MobDef { + kind: EntityKind::Stray, + anim: AnimationType::Skeleton, + adult: vec![opaque(entity_model::bake_skeleton_model(), STRAY_TEX, 64)], + baby: None, + adult_overlays: vec![opaque( + entity_model::bake_skeleton_clothing_model(0.25), + STRAY_OVERLAY_TEX, + 64, + )], + baby_overlays: vec![], + }, + MobDef { + kind: EntityKind::Bogged, + anim: AnimationType::Skeleton, + // Variant 0 = mushrooms, 1 = sheared (empty mushroom parts). + // TODO: replace with a per-part visibility mask (vanilla + // `mushrooms.visible = !isSheared`) instead of a second baked + // model; would also drop the cubeless overlay padding. + adult: vec![ + opaque(entity_model::bake_bogged_model(false), BOGGED_TEX, 64), + opaque(entity_model::bake_bogged_model(true), BOGGED_TEX, 64), + ], + baby: None, + adult_overlays: vec![opaque( + entity_model::bake_bogged_clothing_model(), + BOGGED_OVERLAY_TEX, + 64, + )], + baby_overlays: vec![], + }, MobDef { kind: EntityKind::Creeper, anim: AnimationType::Quadruped, @@ -540,47 +668,18 @@ fn mob_definitions() -> Vec { VILLAGER_BABY_TEX, 64, )), - // Cutout layers over the base skin (vanilla `VillagerProfessionLayer`): - // slot 0 = biome type, slot 1 = biome type on the no-hat model (used - // when the profession texture brings its own hat), slot 2 = - // profession, slot 3 = profession level badge. entity_extras gates - // slot 0 xor 1 and picks each slot's texture variant. // TODO: CustomHeadLayer (worn head items) and CrossedArmsItemLayer // (held item) need a held-item layer first. - adult_overlays: vec![ - opaque( - entity_model::bake_villager_model(false), - VILLAGER_TYPE_TEX, - 64, - ), - opaque( - entity_model::bake_villager_model(true), - VILLAGER_TYPE_TEX, - 64, - ), - opaque( - entity_model::bake_villager_model(false), - VILLAGER_PROFESSION_TEX, - 64, - ), - opaque( - entity_model::bake_villager_model(false), - VILLAGER_LEVEL_TEX, - 64, - ), - ], - baby_overlays: vec![ - opaque( - entity_model::bake_baby_villager_model(false), - VILLAGER_BABY_TYPE_TEX, - 64, - ), - opaque( - entity_model::bake_baby_villager_model(true), - VILLAGER_BABY_TYPE_TEX, - 64, - ), - ], + adult_overlays: villager_like_overlays( + entity_model::bake_villager_model, + VILLAGER_TYPE_TEX, + VILLAGER_PROFESSION_TEX, + VILLAGER_LEVEL_TEX, + ), + baby_overlays: villager_like_baby_overlays( + entity_model::bake_baby_villager_model, + VILLAGER_BABY_TYPE_TEX, + ), }, MobDef { kind: EntityKind::Spider, @@ -1035,8 +1134,17 @@ impl EntityRenderer { /// The translation is anchor-relative, subtracted in f64 (see /// `Camera::anchor`). fn entity_matrix(info: &EntityRenderInfo, anchor: glam::DVec3) -> glam::Mat4 { + let mut body_y_rot_deg = info.body_y_rot_deg; + if info.is_converting { + // Vanilla `setupRotations` isShaking: a per-tick body-yaw jitter. + // The addend is a radians-magnitude value applied to degrees — + // vanilla's own unit mixing, ported literally (~±1.26 degrees). + // Applied here, after the head-vs-body split, so the head shakes + // with the body like vanilla. + body_y_rot_deg += (info.age_in_ticks.floor() * 3.25).cos() * std::f32::consts::PI * 0.4; + } let base = glam::Mat4::from_translation((*info.position - anchor).as_vec3()) - * glam::Mat4::from_rotation_y((180.0 - info.body_y_rot_deg).to_radians()); + * glam::Mat4::from_rotation_y((180.0 - body_y_rot_deg).to_radians()); // body_transform sits before the parts (whose root transforms carry // the convention's X flip), matching vanilla's setupRotations order. info.body_transform.map_or(base, |m| base * m) @@ -1074,11 +1182,16 @@ impl EntityRenderer { let variant = entry.base_variant(info.is_baby, self.effective_variant_index(info)); let entity_mat = Self::entity_matrix(info, anchor); let anim = self.compute_anim(entry.anim, &variant.model, info); + // Computed once per entity and shared with the overlay draws: + // overlays match the base's part order AND pivots (only cube + // geometry differs), and cubeless padding parts are never + // drawn, so their transforms are unused. + let part_transforms = variant.model.compute_part_transforms(&anim); vis.push(VisEntity { info, entry, entity_mat, - anim, + part_transforms, }); } if vis.is_empty() { @@ -1340,13 +1453,13 @@ fn create_camera_sets( (sets, buffers, allocations) } -/// A culled, drawable entity with its world transform and animation -/// precomputed. +/// A culled, drawable entity with its world transform and per-part +/// animation matrices precomputed (shared by the base and overlay draws). struct VisEntity<'a> { info: &'a EntityRenderInfo, entry: &'a MobEntry, entity_mat: glam::Mat4, - anim: entity_model::PartAnim, + part_transforms: Vec, } /// One instanced (variant, part) draw: a run of `instance_count` instances from @@ -1389,18 +1502,13 @@ impl<'a> VariantGroups<'a> { fn emit(&self, vis: &[VisEntity], instances: &mut Vec) -> Vec { let mut records = Vec::new(); for (variant, texture_set, members) in &self.groups { - // Part transforms differ per entity (animation), so compute per member. - let pts: Vec> = members - .iter() - .map(|(vi, ..)| variant.model.compute_part_transforms(&vis[*vi].anim)) - .collect(); for (p, (start, part_count)) in variant.model.part_ranges.iter().enumerate() { if *part_count == 0 { continue; } let first_instance = instances.len() as u32; - for (k, (vi, tint, overlay, uv)) in members.iter().enumerate() { - let model = vis[*vi].entity_mat * pts[k][p]; + for (vi, tint, overlay, uv) in members.iter() { + let model = vis[*vi].entity_mat * vis[*vi].part_transforms[p]; instances.push(EntityInstance { model: model.to_cols_array_2d(), tint: *tint, @@ -1468,21 +1576,36 @@ const ANIM_MARGIN: f32 = 0.5; /// Vanilla (width, height) hitbox per supported mob, scaled for babies; used to /// build the cull bounding sphere. fn entity_bounds(kind: EntityKind, is_baby: bool) -> (f32, f32) { + // Vanilla babies declare explicit BABY_DIMENSIONS rather than a scale; + // list kinds whose constant isn't the half-scale the fallback below + // assumes. Every new baby mob must be checked against its class. + if is_baby { + match kind { + EntityKind::Chicken => return (0.3, 0.4), + EntityKind::Zombie + | EntityKind::Husk + | EntityKind::Drowned + | EntityKind::ZombieVillager + | EntityKind::Villager => return (0.49, 0.98), + _ => {} + } + } let (w, h) = match kind { EntityKind::Pig => (0.9, 0.9), EntityKind::Cow => (0.9, 1.4), - // Vanilla Chicken.BABY_DIMENSIONS is an explicit 0.3x0.4, not half scale. - EntityKind::Chicken if is_baby => return (0.3, 0.4), EntityKind::Chicken => (0.4, 0.7), EntityKind::Sheep => (0.9, 1.3), - EntityKind::Zombie => (0.6, 1.95), - EntityKind::Skeleton => (0.6, 1.99), + EntityKind::Zombie + | EntityKind::Husk + | EntityKind::Drowned + | EntityKind::ZombieVillager + | EntityKind::Villager + | EntityKind::Witch => (0.6, 1.95), + EntityKind::Skeleton | EntityKind::Stray | EntityKind::Bogged => (0.6, 1.99), EntityKind::Creeper => (0.6, 1.7), EntityKind::Spider => (1.4, 0.9), - EntityKind::Villager => (0.6, 1.95), EntityKind::Enderman => (0.6, 2.9), EntityKind::Slime => (0.52, 0.52), - EntityKind::Witch => (0.6, 1.95), EntityKind::Player => (0.6, 1.8), _ => (1.0, 1.0), }; @@ -1551,6 +1674,9 @@ fn assert_part_order_matches(base: &[MobVariant], overlays: &[Vec]) } } +// TODO: share one vertex buffer + model per distinct mesh across texture +// variants (a zombie villager's 33 texture variants clone 2 meshes), and +// batch the per-texture one-time upload submits into one fence wait. #[allow(clippy::too_many_arguments)] fn build_variants( device: &vk::Device,