use pumpkin_data::item::Item; use pumpkin_data::particle::Particle; use pumpkin_data::potion::Effect; use pumpkin_data::tag::{self, Taggable}; use pumpkin_data::tracked_data; use pumpkin_inventory::build_equipment_slots; use pumpkin_inventory::player::player_inventory::PlayerInventory; use pumpkin_inventory::screen_handler::InventoryPlayer; use pumpkin_protocol::bedrock::client::take_item_actor::CTakeItemActor; use pumpkin_protocol::bedrock::server::actor_event::{ActorEventID, SActorEvent}; use pumpkin_protocol::codec::var_ulong::VarULong; use pumpkin_util::GameMode; use pumpkin_util::Hand; use pumpkin_util::math::position::BlockPos; use std::sync::Arc; use std::sync::atomic::Ordering; use std::sync::atomic::{ AtomicBool, AtomicU8, Ordering::{Relaxed, SeqCst}, }; use std::{collections::HashMap, sync::atomic::AtomicI32}; use tracing::warn; use super::experience_orb::ExperienceOrbEntity; use super::{Entity, EntityBase, NBTStorageInit}; use crate::block::OnLandedUponArgs; use crate::entity::NBTStorage; use crate::entity::attributes::AttributeInstance; use crate::entity::attributes::Modifier; use crate::entity::attributes::ModifierOperation; use crate::entity::combat::knockback_after_resistance; use crate::entity::mob::equipment::DEFAULT_EQUIPMENT_DROP_CHANCE; use crate::entity::mob::slime::SlimeEntity; use crate::entity::player::statistics::{CustomStatistic, StatisticCategory}; use crate::server::Server; use crate::world::loot::{LootContextParameters, LootTableExt}; use crossbeam::atomic::AtomicCell; use pumpkin_data::attributes::Attributes; use pumpkin_data::damage::DeathMessageType; use pumpkin_data::data_component_impl::Operation; use pumpkin_data::data_component_impl::food::{ConsumableImpl, ConsumeEffect}; use pumpkin_data::data_component_impl::{ AttributeModifiersImpl, BlocksAttacksImpl, DeathProtectionImpl, EnchantmentsImpl, EquipmentSlot, EquippableImpl, FoodImpl, }; use pumpkin_data::effect::StatusEffect; use pumpkin_data::entity::{EntityPose, EntityStatus, EntityType}; use pumpkin_data::fluid::Fluid; use pumpkin_data::item_stack::{DamageResult, ItemStack}; use pumpkin_data::sound::SoundCategory; use pumpkin_data::{Block, Enchantment, translation}; use pumpkin_data::{damage::DamageType, sound::Sound}; use pumpkin_inventory::entity_equipment::EntityEquipment; use pumpkin_nbt::compound::NbtCompound; use pumpkin_nbt::tag::NbtTag; use pumpkin_protocol::codec::var_int::VarInt; use pumpkin_protocol::java::client::play::{ CHurtAnimation, CSetPlayerInventory, CTakeItemEntity, CUpdateMobEffect, }; use pumpkin_protocol::{ codec::item_stack_seralizer::ItemStackSerializer, java::client::play::{CDamageEvent, CSetEquipment, Metadata, MetadataSerializer}, ser::{NetworkWriteExt, WritingError}, }; use pumpkin_util::math::boundingbox::BoundingBox; use pumpkin_util::math::vector3::Vector3; use pumpkin_util::text::TextComponent; use rand::RngExt; use std::sync::RwLock; /// Represents a living entity within the game world. /// /// This struct encapsulates the core properties and behaviors of living entities, including players, mobs, and other creatures. pub struct LivingEntity { /// The underlying entity object, providing basic entity information and functionality. pub entity: Entity, /// Tracks the remaining time until the entity can regenerate health. pub hurt_cooldown: AtomicI32, /// Stores the amount of damage the entity last received. pub last_damage_taken: AtomicCell, /// The current health level of the entity. pub health: AtomicCell, /// The current absorption (yellow hearts) on the entity. pub absorption: AtomicCell, pub item_use_time: AtomicI32, pub item_in_use: std::sync::Mutex>, pub active_hand: std::sync::Mutex>, pub death_time: AtomicU8, /// Indicates whether the entity is dead. (`on_death` called) pub dead: AtomicBool, /// The distance the entity has been falling. pub fall_distance: AtomicCell, pub active_effects: std::sync::Mutex>, pub entity_equipment: Arc>, pub equipment_drop_chances: Arc>>, pub movement_input: AtomicCell>, pub equipment_slots: Arc>, pub jumping: AtomicBool, pub jumping_cooldown: AtomicU8, pub climbing: AtomicBool, /// The position where the entity was last climbing, used for death messages pub climbing_pos: AtomicCell>, /// The entity ID of the entity that last attacked this living entity. pub last_attacker_id: AtomicI32, /// The tick at which this entity was last attacked (entity age). pub last_attacked_time: AtomicI32, /// The entity ID of the entity this living entity last attacked. pub last_attacking_id: AtomicI32, /// The tick at which this entity last attacked something (entity age). pub last_attack_time: AtomicI32, water_movement_speed_multiplier: f32, livings_flags: AtomicU8, /// The attributes of the entity pub attributes: RwLock>, } struct EffectParticle { particle_id: VarInt, color: i32, } struct EffectParticles(Vec); impl MetadataSerializer for EffectParticles { fn write_metadata( &self, writer: &mut impl std::io::Write, _version: &pumpkin_util::version::JavaMinecraftVersion, ) -> Result<(), WritingError> { let count = i32::try_from(self.0.len()) .map_err(|_| WritingError::Message("Too many effect particles".into()))?; writer.write_var_int(&VarInt(count))?; for particle in &self.0 { writer.write_var_int(&particle.particle_id)?; writer.write_i32(particle.color)?; } Ok(()) } } impl EffectParticle { const fn from_effect(effect: &Effect) -> Self { Self { particle_id: VarInt(Particle::EntityEffect as i32), color: (((if effect.ambient { 38 } else { 255 }) as u32) << 24 | effect.effect_type.color as u32) as i32, } } } impl LivingEntity { const USING_ITEM_FLAG: u8 = 1; const OFF_HAND_ACTIVE_FLAG: u8 = 2; const RANDOM_TELEPORT_ATTEMPTS: usize = 16; #[expect(dead_code)] const USING_RIPTIDE_FLAG: u8 = 4; const PREVENT_AREA_FALL_DAMAGE_BLOCKS: [&'static Block; 4] = [ &Block::COBWEB, &Block::LADDER, &Block::POWDER_SNOW, &Block::SLIME_BLOCK, ]; fn hurt_sound_for_entity(entity_type: &'static EntityType) -> Sound { entity_type.hurt_sound.unwrap_or(Sound::EntityGenericHurt) } pub fn new(entity: Entity) -> Self { let water_movement_speed_multiplier = if entity.entity_type == &EntityType::POLAR_BEAR { 0.98 } else if entity.entity_type == &EntityType::SKELETON_HORSE { 0.96 } else { 0.8 }; let mut max_health: f32 = 20.0; // Overridden by attribute base below Self { // Populate local attribute instances from the default registry and get initial vars attributes: { let mut m = std::collections::HashMap::new(); for (attr, base) in entity.entity_type.attributes { if attr.id == Attributes::MAX_HEALTH.id { max_health = *base as f32; } m.insert(attr.id, AttributeInstance::new(*base)); } std::sync::RwLock::new(m) }, health: AtomicCell::new(max_health), // Initial health value from attributes entity, hurt_cooldown: AtomicI32::new(0), last_damage_taken: AtomicCell::new(0.0), absorption: AtomicCell::new(0.0), fall_distance: AtomicCell::new(0.0), death_time: AtomicU8::new(0), dead: AtomicBool::new(false), item_use_time: AtomicI32::new(0), item_in_use: std::sync::Mutex::new(None), active_hand: std::sync::Mutex::new(None), livings_flags: AtomicU8::new(0), active_effects: std::sync::Mutex::new(HashMap::new()), entity_equipment: Arc::new(std::sync::Mutex::new(EntityEquipment::new())), equipment_drop_chances: Arc::new(std::sync::Mutex::new(HashMap::new())), equipment_slots: Arc::new(build_equipment_slots()), jumping: AtomicBool::new(false), jumping_cooldown: AtomicU8::new(0), climbing: AtomicBool::new(false), climbing_pos: AtomicCell::new(None), last_attacker_id: AtomicI32::new(0), last_attacked_time: AtomicI32::new(0), last_attacking_id: AtomicI32::new(0), last_attack_time: AtomicI32::new(0), movement_input: AtomicCell::new(Vector3::default()), water_movement_speed_multiplier, } } pub fn send_equipment_changes(&self, equipment: &[(EquipmentSlot, ItemStack)]) { if equipment.is_empty() { return; } let equipment_java: Vec<(i8, ItemStackSerializer)> = equipment .iter() .map(|(slot, stack)| { ( slot.discriminant(), ItemStackSerializer::from(stack.clone()), ) }) .collect(); let je_packet = CSetEquipment::new(self.entity_id().into(), equipment_java); let mut sent_editioned = false; for (slot, stack) in equipment { if *slot == EquipmentSlot::MAIN_HAND || *slot == EquipmentSlot::OFF_HAND { let window_id = if *slot == EquipmentSlot::OFF_HAND { 120 } else { 0 }; let be_packet = pumpkin_protocol::bedrock::client::CMobEquipment { target_runtime_id: (self.entity_id() as u64).into(), item: pumpkin_protocol::bedrock::network_item::NetworkItemStackDescriptor::from( stack, ), slot: 0, selected_slot: 0, container_id: window_id, }; self.entity.world.load().broadcast_packet_except_editioned( &[self.entity.entity_uuid], &je_packet, &be_packet, ); sent_editioned = true; } } if !sent_editioned { self.entity .world .load() .broadcast_packet_except(&[self.entity.entity_uuid], &je_packet); } } /// Picks up an Item entity or XP Orb pub fn pickup(&self, item: &Entity, stack_amount: u32) { let mut pickup_event = crate::plugin::api::events::entity::entity_pickup_item::EntityPickupItemEvent::new( self.entity.entity_id, item.entity_type.id.to_string(), stack_amount as u8, ); if let Some(server) = self.entity.world.load().server.upgrade() { server .plugin_manager .fire_blocking(&server, &mut pickup_event); if pickup_event.cancelled { return; } } let chunk_pos = self.entity.chunk_pos.load(); self.entity.world.load().broadcast_to_chunk_editioned( chunk_pos, &CTakeItemEntity::new( item.entity_id.into(), self.entity.entity_id.into(), VarInt(stack_amount as i32), ), &CTakeItemActor { item_runtime_id: VarULong(item.entity_id as u64), actor_runtime_id: VarULong(self.entity.entity_id as u64), }, ); } /// Sends the Hand animation to all others, used when Eating for example pub fn set_active_hand(&self, hand: Hand, stack: ItemStack, duration: i32) { self.item_use_time.store(duration, Ordering::Relaxed); *self .item_in_use .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(stack); *self .active_hand .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(hand); self.set_living_flag(Self::USING_ITEM_FLAG, true); self.set_living_flag(Self::OFF_HAND_ACTIVE_FLAG, hand == Hand::Left); } fn set_living_flag(&self, flag: u8, value: bool) { let index = flag; let mut b = self.livings_flags.load(Ordering::Relaxed); if value { b |= index; } else { b &= !index; } self.livings_flags.store(b, Ordering::Relaxed); let bedrock_meta = (flag == Self::USING_ITEM_FLAG).then(|| { let index = pumpkin_protocol::bedrock::client::set_actor_data::entity_data_flag::USING_ITEM; let mask = 1i64 << index; if value { self.entity.bedrock_flags.fetch_or(mask, Ordering::Relaxed); } else { self.entity .bedrock_flags .fetch_and(!mask, Ordering::Relaxed); } let mut meta = pumpkin_protocol::bedrock::client::set_actor_data::SyncedActorDataList::new(); meta.set( pumpkin_protocol::bedrock::client::set_actor_data::entity_data_key::FLAGS, pumpkin_protocol::bedrock::client::set_actor_data::MetadataValue::Int64( self.entity.bedrock_flags.load(Ordering::Relaxed), ), ); meta }); self.entity.send_meta_data( &[Metadata::new( tracked_data::living_entity::DATA_LIVING_ENTITY_FLAGS, b, )], bedrock_meta.as_ref(), ); } pub fn clear_active_hand(&self) { *self .item_in_use .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) = None; *self .active_hand .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) = None; self.item_use_time.store(0, Ordering::Relaxed); self.set_living_flag(Self::USING_ITEM_FLAG, false); } pub fn is_blocking(&self) -> bool { let item_in_use = self .item_in_use .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); if let Some(item) = item_in_use.as_ref() && item.get_data_component::().is_some() { let use_time = self.item_use_time.load(Ordering::Relaxed); let required_time = if let Some(dyn_self) = self .entity .world .load() .get_entity_by_id(self.entity.entity_id) && let Some(player) = dyn_self .cast_any() .downcast_ref::() && matches!( player.client.as_ref(), crate::net::ClientPlatform::Bedrock(_) ) { 0 } else { 5 }; return item.get_max_use_time() - use_time >= required_time; } false } pub fn heal(&self, additional_health: f32) { assert!(additional_health > 0.0); let mut event = crate::plugin::api::events::entity::entity_regain_health::EntityRegainHealthEvent::new( self.entity.entity_id, additional_health, ); if let Some(server) = self.entity.world.load().server.upgrade() { server.plugin_manager.fire_blocking(&server, &mut event); if event.cancelled { return; } } self.set_health(self.health.load() + additional_health); } pub fn set_health(&self, health: f32) { // Clamp to [0, max_health] let max_health = self.get_max_health(); let clamped = health.max(0.0).min(max_health); self.health.store(clamped); // tell everyone entities health changed self.entity.send_meta_data( &[Metadata::new( tracked_data::living_entity::DATA_HEALTH_ID, clamped, )], None, ); } /// Returns the current maximum health for this entity pub fn get_max_health(&self) -> f32 { self.get_attribute_value(&Attributes::MAX_HEALTH) as f32 } /// Sets the maximum health for this entity pub fn set_max_health(&self, max_health: f32) { // Update base attribute self.set_attribute_base(&Attributes::MAX_HEALTH, max_health as f64); // Broadcast the attribute change crate::entity::attributes::send_attribute_updates_for_living( self, vec![Attributes::MAX_HEALTH], ); // Clamp current health to new max if needed and send metadata update let current_health = self.health.load(); if current_health > max_health { self.set_health(max_health); } } /// Returns the current absorption amount for this entity (yellow hearts) pub fn get_absorption(&self) -> f32 { self.absorption.load() } /// Sets the current absorption amount for this entity (yellow hearts) pub fn set_absorption(&self, new_abs: f32) { // Must be at least 0 let new_abs = new_abs.max(0.0); // Set local state self.absorption.store(new_abs); // Broadcast attribute update for max_absorption so clients receive // the updated absorption value via the attribute packet. crate::entity::attributes::send_attribute_updates_for_living( self, vec![Attributes::MAX_ABSORPTION], ); // Send absorption metadata for players (visual yellow hearts) if self.entity.entity_type == &EntityType::PLAYER { self.entity.send_meta_data( &[Metadata::new( tracked_data::player::DATA_PLAYER_ABSORPTION_ID, new_abs, )], None, ); } } /// Convenience helper to mutate an attribute instance. Automatically inserts /// a new instance populated from the registry base if needed. pub fn update_attribute( &self, attribute: &Attributes, f: F, ) { let mut map = self .attributes .write() .unwrap_or_else(std::sync::PoisonError::into_inner); let inst = map.entry(attribute.id).or_insert_with(|| { let base = self .entity .entity_type .attributes .iter() .find(|a| a.0.id == attribute.id) .map_or_else( || { tracing::warn!( "Entity type {:?} has no base value for attribute {:?}; falling back to default {}", self.entity.entity_type, attribute.id, attribute.default_value, ); attribute.default_value }, |a| a.1, ); AttributeInstance::new(base) }); f(inst); inst.dirty.store(true, Ordering::Relaxed); } /// Returns the computed value for `attribute` using the local instance, falling back /// to `attribute.default_value` if no local instance exists. pub fn get_attribute_value(&self, attribute: &Attributes) -> f64 { let map = self .attributes .read() .unwrap_or_else(std::sync::PoisonError::into_inner); map.get(&attribute.id) .map_or(attribute.default_value, AttributeInstance::value) } /// Returns the base attribute value for `attribute` for this entity's type. pub fn get_attribute_base(&self, attribute: &Attributes) -> f64 { // Check the local base value first (could be modified) let map = self .attributes .read() .unwrap_or_else(std::sync::PoisonError::into_inner); if let Some(instance) = map.get(&attribute.id) { return instance.base_value; } // Fall back to registry base value if no local instance exists self.entity .entity_type .attributes .iter() .find(|a| a.0.id == attribute.id) .map_or(attribute.default_value, |a| a.1) } /// Update or insert the base value for an attribute on this entity. /// If the attribute doesn't exist locally yet, it will be inserted. pub fn set_attribute_base(&self, attribute: &Attributes, new_base: f64) { let mut map = self .attributes .write() .unwrap_or_else(std::sync::PoisonError::into_inner); if let Some(inst) = map.get_mut(&attribute.id) { inst.base_value = new_base; inst.dirty.store(true, Ordering::Relaxed); } else { let ai = AttributeInstance::new(new_base); ai.dirty.store(true, Ordering::Relaxed); map.insert(attribute.id, ai); } } pub fn reset_effects_and_attributes(&self) { // Clear active effects and reset modified attributes let effects_to_remove: Vec<_> = { let lock = self .active_effects .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); lock.keys().copied().collect() }; for effect_type in effects_to_remove { self.remove_effect(effect_type); } } pub const fn entity_id(&self) -> i32 { self.entity.entity_id } #[expect(clippy::too_many_lines)] pub fn add_effect(&self, effect: Effect) { let mut effect_event = crate::plugin::api::events::entity::entity_potion_effect::EntityPotionEffectEvent::new( self.entity.entity_id, effect.effect_type.translation_key.to_string(), effect.duration, effect.amplifier, ); if let Some(server) = self.entity.world.load().server.upgrade() { server .plugin_manager .fire_blocking(&server, &mut effect_event); } if effect_event.cancelled { return; } // Apply instant effects immediately before storing if effect.effect_type == &StatusEffect::INSTANT_HEALTH { let heal_amount = 4.0 * (1 << effect.amplifier) as f32; self.heal(heal_amount); } else if effect.effect_type == &StatusEffect::INSTANT_DAMAGE { let damage_amount = 6.0 * (1 << effect.amplifier) as f32; let dyn_self = self .entity .world .load() .get_entity_by_id(self.entity.entity_id); if let Some(dyn_self) = dyn_self { let _ = dyn_self.damage(&*dyn_self, damage_amount, DamageType::MAGIC); } } else { // Apply non-instant effects // Effects that modify attributes (ex. speed) should also update the // entity's attribute instances (server-side) and then notify clients. if !effect.effect_type.attribute_modifiers.is_empty() { // Apply each attribute modifier into the local AttributeInstance for m in effect.effect_type.attribute_modifiers { let id = m.id.to_string(); let op = match m.operation { Operation::AddValue => ModifierOperation::Add, Operation::AddMultipliedBase => ModifierOperation::MultiplyBase, Operation::AddMultipliedTotal => ModifierOperation::MultiplyTotal, }; let scaled_amount = m.base_value * (f64::from(effect.amplifier) + 1.); let mod_inst = Modifier { id, amount: scaled_amount, operation: op, }; self.update_attribute(m.attribute, |inst| { inst.add_or_replace_modifier(mod_inst.clone()); }); } // Recompute packet modifiers from active effects for each affected attribute let mut touched_attrs: Vec = Vec::new(); for m in effect.effect_type.attribute_modifiers { if !touched_attrs.iter().any(|a| a.id == m.attribute.id) { touched_attrs.push(m.attribute.clone()); } } if !touched_attrs.is_empty() { crate::entity::attributes::send_attribute_updates_for_living( self, touched_attrs, ); } } // Apply absorption effect (+4 absorption per level) if effect.effect_type == &StatusEffect::ABSORPTION { let added = 4.0 * (effect.amplifier as f32 + 1.0); let max_abs = self.get_attribute_value(&Attributes::MAX_ABSORPTION) as f32; let new_abs = (self.absorption.load() + added).min(max_abs); self.set_absorption(new_abs); } // Apply invisible effect if effect.effect_type == &StatusEffect::INVISIBILITY { self.entity.set_invisible(true); } // Apply glowing effect if effect.effect_type == &StatusEffect::GLOWING { self.entity.set_glowing(true); } } // Broadcast effect to nearby players let mut flag: i8 = 0; if effect.ambient { flag |= 1; } if effect.show_particles { flag |= 2; } if effect.show_icon { flag |= 4; } if effect.blend { flag |= 8; } let je_packet = CUpdateMobEffect::new( self.entity.entity_id.into(), VarInt(i32::from(effect.effect_type.id)), effect.amplifier.into(), effect.duration.into(), flag, ); let be_packet = pumpkin_protocol::bedrock::client::CMobEffect { target_runtime_id: VarULong(self.entity.entity_id as u64), event_id: pumpkin_protocol::bedrock::client::CMobEffect::EVENT_ADD, effect_id: VarInt(effect.effect_type.to_bedrock_id()), effect_amplifier: VarInt(i32::from(effect.amplifier)), show_particles: effect.show_particles, effect_duration_ticks: VarInt(effect.duration), tick: VarULong(0), ambient: effect.ambient, }; let chunk_pos = self.entity.chunk_pos.load(); self.entity .world .load() .broadcast_to_chunk_editioned(chunk_pos, &je_packet, &be_packet); if effect.effect_type != &StatusEffect::INSTANT_HEALTH && effect.effect_type != &StatusEffect::INSTANT_DAMAGE { self.active_effects .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .insert(effect.effect_type, effect); } self.sync_effect_particles(); } fn sync_effect_particles(&self) { let effects = self .active_effects .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); let has_effects = !effects.is_empty(); let particles = EffectParticles( effects .values() .filter(|effect| effect.show_particles) .map(EffectParticle::from_effect) .collect(), ); let ambient = effects .values() .filter(|effect| effect.show_particles) .all(|effect| effect.ambient); drop(effects); self.entity.send_meta_data( &[Metadata::new( tracked_data::living_entity::EFFECT_PARTICLES, particles, )], None, ); if has_effects { self.entity.send_meta_data( &[Metadata::new( tracked_data::living_entity::EFFECT_AMBIENCE_ID, ambient, )], None, ); } } pub fn remove_effect(&self, effect_type: &'static StatusEffect) -> bool { // Remove the effect let succeeded = self .active_effects .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .remove(&effect_type) .is_some(); // Broadcast effect removal self.entity .world .load() .send_remove_mob_effect(&self.entity, effect_type); // Remove attribute modifiers, if any if !effect_type.attribute_modifiers.is_empty() { let mut touched_attrs = Vec::new(); for m in effect_type.attribute_modifiers { let id = m.id.to_string(); // Clean local server state self.update_attribute(m.attribute, |inst| { inst.remove_modifier(&id); }); // Track unique attributes for the packet update if !touched_attrs .iter() .any(|a: &Attributes| a.id == m.attribute.id) { touched_attrs.push(m.attribute.clone()); } } // Sync the clean state to the client if !touched_attrs.is_empty() { crate::entity::attributes::send_attribute_updates_for_living(self, touched_attrs); } } // If absorption effect removed, clear current absorption amount and notify clients if effect_type == &StatusEffect::ABSORPTION { self.set_absorption(0.0); } // If health boost effect removed, clamp current health to new max and notify clients if effect_type == &StatusEffect::HEALTH_BOOST { let new_max = self.get_max_health(); if self.health.load() > new_max { // Update local health and send both health and absorption metadata together self.set_health(new_max.max(0.0)); } } // If invisible effect removed, disable invisibility if effect_type == &StatusEffect::INVISIBILITY { self.entity.set_invisible(false); } // If glowing effect removed, disable glowing if effect_type == &StatusEffect::GLOWING { self.entity.set_glowing(false); } if succeeded { self.sync_effect_particles(); } succeeded } pub fn has_effect(&self, effect: &'static StatusEffect) -> bool { self.active_effects .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .contains_key(&effect) } pub fn get_effect(&self, effect: &'static StatusEffect) -> Option { self.active_effects .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .get(&effect) .cloned() } pub fn is_in_fall_damage_resetting(&self) -> (bool, &Block) { let block_pos = self.entity.block_pos.load(); let block = self.entity.world.load().get_block(&block_pos); ( block.has_tag(&tag::Block::MINECRAFT_FALL_DAMAGE_RESETTING), block, ) } // Check if the entity is in water pub fn is_in_water(&self) -> bool { self.entity.touching_water.load(Ordering::Relaxed) } // Check if the entity is in powder snow pub fn is_in_powder_snow(&self) -> bool { self.entity.is_in_powder_snow.load(Ordering::Relaxed) } pub fn should_prevent_fall_damage(&self) -> bool { let (prevents, block) = self.is_in_fall_damage_resetting(); if block == &Block::SCAFFOLDING && !self.entity.is_sneaking() { return false; } if block == &Block::WATER { return true; } if self.entity.entity_type == &EntityType::PLAYER { if block == &Block::END_GATEWAY || block == &Block::END_PORTAL { return true; } if block == &Block::NETHER_PORTAL { let world = self.entity.world.load(); let level_info = world.level_info.load(); return level_info.game_rules.players_nether_portal_default_delay == 0; } } prevents } pub fn should_prevent_fall_damage_in_area(&self) -> bool { let world = self.entity.world.load(); let block_pos = self.entity.block_pos.load().down(); let entity_pos = self.entity.pos.load(); let min = BlockPos(Vector3::new( block_pos.0.x - 1, block_pos.0.y, block_pos.0.z - 1, )); let max = BlockPos(Vector3::new( block_pos.0.x + 1, block_pos.0.y, block_pos.0.z + 1, )); let pos_iter = BlockPos::iterate(min, max); // FIXME: it seems the java server checks all blocks around with a raycast and check if miss or hit, // then added to a collision checker to handle in the tick handler for pos in pos_iter { let block = world.get_block(&pos); if Self::PREVENT_AREA_FALL_DAMAGE_BLOCKS.contains(&block) { let block_center = Vector3::new( f64::from(pos.0.x) + 0.5, f64::from(pos.0.y) + 0.5, f64::from(pos.0.z) + 0.5, ); let distance = entity_pos.squared_distance_to_vec(&block_center); // Fetch safe fall distance from attribute let safe_distance = self.get_attribute_value(&Attributes::SAFE_FALL_DISTANCE); return distance.sqrt() <= safe_distance * safe_distance; } } false } pub fn is_immune_to_fall_damage(&self) -> bool { self.entity .entity_type .has_tag(&tag::EntityType::MINECRAFT_FALL_DAMAGE_IMMUNE) } fn get_effective_gravity(&self, caller: &dyn EntityBase) -> f64 { let final_gravity = caller.get_gravity(); if self.entity.velocity.load().y <= 0.0 && self.has_effect(&StatusEffect::SLOW_FALLING) { final_gravity.min(0.01) } else { final_gravity } } pub fn swing_hand(&self) { let world = self.entity.world.load(); let entity_id = self.entity_id(); let je_packet = pumpkin_protocol::java::client::play::CEntityAnimation::new( entity_id.into(), pumpkin_protocol::java::client::play::Animation::SwingMainArm, ); let be_packet = pumpkin_protocol::bedrock::server::animate::SAnimate { action: pumpkin_protocol::bedrock::server::animate::AnimateAction::SwingArm, target_actor_runtime_id: pumpkin_protocol::codec::var_ulong::VarULong(entity_id as u64), data: 0.0, swing_source: None, }; world.broadcast_editioned(&je_packet, &be_packet); } fn tick_movement(&self, caller: &dyn EntityBase) { if self.jumping_cooldown.load(Relaxed) != 0 { self.jumping_cooldown.fetch_sub(1, Relaxed); } let should_swim_in_fluids = caller.get_player().is_none_or(|player| !player.is_flying()); self.entity.check_zero_velo(); let mut movement_input = self.movement_input.load(); movement_input.x *= 0.98; movement_input.z *= 0.98; self.movement_input.store(movement_input); // TODO: Tick AI if self.jumping.load(SeqCst) && should_swim_in_fluids { let in_lava = self.entity.touching_lava.load(SeqCst); let in_water = self.entity.touching_water.load(SeqCst); let fluid_height = if in_lava { self.entity.lava_height.load() } else { self.entity.water_height.load() }; let swim_height = self.get_swim_height(); let on_ground = self.entity.on_ground.load(SeqCst); if (in_water || in_lava) && (!on_ground || fluid_height > swim_height) { // Swim upward let mut velo = self.entity.velocity.load(); velo.y += 0.04; self.entity.velocity.store(velo); } else if (on_ground || in_water && fluid_height <= swim_height) && self.jumping_cooldown.load(SeqCst) == 0 { self.jump(); self.jumping_cooldown.store(10, SeqCst); } } else { self.jumping_cooldown.store(0, SeqCst); } if self.has_effect(&StatusEffect::SLOW_FALLING) || self.has_effect(&StatusEffect::LEVITATION) { self.fall_distance.store(0.0); } let touching_water = self.entity.touching_water.load(SeqCst); // Strider is the only entity that has canWalkOnFluid = false if (touching_water || self.entity.touching_lava.load(SeqCst)) && should_swim_in_fluids && self.entity.entity_type != &EntityType::STRIDER { self.travel_in_fluid(caller, touching_water); } else { // TODO: Gliding self.travel_in_air(caller); } let suffocating = self.entity.tick_block_collisions(caller); if suffocating { caller.damage(caller, 1.0, DamageType::IN_WALL); } } fn travel_in_air(&self, caller: &dyn EntityBase) { // applyMovementInput let effective_speed = self.get_attribute_value(&Attributes::MOVEMENT_SPEED); let (speed, friction) = if self.entity.on_ground.load(Relaxed) { // getVelocityAffectingPos let slipperiness = f64::from( self.entity .get_block_with_y_offset(0.500_001) .1 .slipperiness, ); let speed = effective_speed * 0.216_000_02 / (slipperiness * slipperiness * slipperiness); (speed, slipperiness * 0.91) } else { let speed = caller .get_player() .map_or(0.02, super::player::Player::get_off_ground_speed); (speed, 0.91) }; self.entity .update_velocity_from_input(self.movement_input.load(), speed); self.apply_climbing_speed(); self.make_move(caller); let mut velo = self.entity.velocity.load(); let can_powder_snow_climb = if self.entity.was_in_powder_snow.load(Relaxed) { crate::block::blocks::powder_snow::can_entity_walk_on_powder_snow(caller) } else { false }; if (self.entity.horizontal_collision.load(SeqCst) || self.jumping.load(SeqCst)) && (self.climbing.load(Relaxed) || can_powder_snow_climb) { velo.y = 0.2; } let levitation = self.get_effect(&StatusEffect::LEVITATION); if let Some(lev) = levitation { velo.y += 0.05f64.mul_add(f64::from(lev.amplifier + 1), -velo.y) * 0.2; } else { velo.y -= self.get_effective_gravity(caller); // TODO: If world is not loaded: replace effective gravity with: // if below world's bottom y then -0.1, else 0.0 } // If entity has no drag: store velo and return velo.x *= friction; velo.z *= friction; velo.y *= caller.get_y_velocity_drag().unwrap_or_else(|| { if caller.is_flutterer() { friction } else { 0.98 } }); self.entity.velocity.store(velo); } fn travel_in_fluid(&self, caller: &dyn EntityBase, water: bool) { let movement_input = self.movement_input.load(); let falling = self.entity.velocity.load().y <= 0.0; let gravity = self.get_effective_gravity(caller); let effective_speed = self.get_attribute_value(&Attributes::MOVEMENT_SPEED); if water { let mut friction = if self.entity.sprinting.load(Relaxed) { 0.9 } else { f64::from(self.water_movement_speed_multiplier) }; let mut speed = 0.02; // Apply water movement efficiency attribute let mut water_movement_efficiency = self.get_attribute_value(&Attributes::WATER_MOVEMENT_EFFICIENCY); if water_movement_efficiency > 0.0 { if !self.entity.on_ground.load(SeqCst) { water_movement_efficiency *= 0.5; } friction += (0.546_000_06 - friction) * water_movement_efficiency; speed += (effective_speed - speed) * water_movement_efficiency; } if self.has_effect(&StatusEffect::DOLPHINS_GRACE) { friction = 0.96; } self.entity .update_velocity_from_input(movement_input, speed); self.make_move(caller); let mut velo = self.entity.velocity.load(); if self.entity.horizontal_collision.load(SeqCst) && self.climbing.load(Relaxed) { velo.y = 0.2; } velo = velo.multiply(friction, 0.8, friction); self.apply_fluid_moving_speed(&mut velo.y, gravity, falling); self.entity.velocity.store(velo); } else { self.entity.update_velocity_from_input(movement_input, 0.02); self.make_move(caller); let mut velo = self.entity.velocity.load(); if self.entity.lava_height.load() <= self.get_swim_height() { velo.x *= 0.5; velo.z *= 0.5; velo.y *= 0.8; self.apply_fluid_moving_speed(&mut velo.y, gravity, falling); } else { velo = velo * 0.5; } if gravity != 0.0 { velo.y -= gravity / 4.0; // Negative gravity = buoyancy } self.entity.velocity.store(velo); } let mut velo = self.entity.velocity.load(); if self.entity.horizontal_collision.load(SeqCst) && !self .entity .world .load() .check_fluid_collision(self.entity.bounding_box.load().shift(velo)) { velo.y = 0.3; self.entity.velocity.store(velo); } } fn apply_fluid_moving_speed(&self, dy: &mut f64, gravity: f64, falling: bool) { if gravity != 0.0 && !self.entity.sprinting.load(Relaxed) { if falling && (*dy - 0.005).abs() >= 0.003 && (*dy - gravity / 16.0).abs() < 0.003 { *dy = -0.003; } else { *dy -= gravity / 16.0; } } } fn make_move(&self, caller: &dyn EntityBase) { self.entity.move_entity(caller, self.entity.velocity.load()); self.check_climbing(); } fn check_climbing(&self) { // If spectator: return false // TODO // let mut pos = self.entity.block_pos.load(); // let world = self.entity.world.read().await; // let (block, state) = world.get_block_and_state(&pos); // let name = block.properties(state.id).map(|props| props.name()); // if let Some(name) = name { // if name == "LadderLikeProperties" // || name == "ScaffoldingLikeProperties" // || name == "CaveVinesLikeProperties" // || name == "CaveVinesPlantLikeProperties" // { // self.climbing.store(true, Relaxed); // self.climbing_pos.store(Some(pos)); // return; // } // if name == "OakTrapdoorLikeProperties" { // let trapdoor = OakTrapdoorLikeProperties::from_state_id(state.id, &block); // pos.0.y -= 1; // let (down_block, down_state) = world.get_block_and_state(&pos); // let is_ladder = down_block // .properties(down_state.id) // .is_some_and(|down_props| down_props.name() == "LadderLikeProperties"); // if is_ladder { // let ladder = LadderLikeProperties::from_state_id(down_state.id, &down_block); // if trapdoor.r#facing == ladder.r#facing { // self.climbing.store(true, Relaxed); // self.climbing_pos.store(Some(pos)); // return; // } // } // } // } self.climbing.store(false, Relaxed); if self.entity.on_ground.load(SeqCst) { self.climbing_pos.store(None); } } fn apply_climbing_speed(&self) { if self.climbing.load(Relaxed) { self.fall_distance.store(0.0); let mut velo = self.entity.velocity.load(); let pos = 0.15; let neg = -0.15; if velo.x < neg { velo.x = neg; } else if velo.x > pos { velo.x = pos; } if velo.z < neg { velo.z = neg; } else if velo.z > pos { velo.z = pos; } velo.y = velo.y.max(neg); // TODO // if velo.y < 0.0 // && self.entity.entity_type == &EntityType::PLAYER // && self.entity.sneaking.load(Relaxed) // { // let block = self // .entity // .world // .read() // .await // .get_block(&self.entity.block_pos.load()) // .await; // if let Some(props) = block.properties(block.default_state.id) { // if props.name() == "ScaffoldingLikeProperties" { // velo.y = 0.0; // } // } // } self.entity.velocity.store(velo); } } pub fn get_swim_height(&self) -> f64 { let eye_height = self.entity.get_eye_height(); if self.entity.entity_type == &EntityType::BREEZE { eye_height } else if eye_height < 0.4 { 0.0 } else { 0.4 } } fn jump(&self) { let jump = self.get_jump_velocity(1.0); if jump <= 1.0e-5 { return; } let mut velo = self.entity.velocity.load(); velo.y = jump.max(velo.y); if self.entity.sprinting.load(Relaxed) { let yaw = f64::from(self.entity.yaw.load()).to_radians(); velo.x -= yaw.sin() * 0.2; velo.z += yaw.cos() * 0.2; } self.entity.velocity.store(velo); self.entity.velocity_dirty.store(true, SeqCst); } fn get_jump_velocity(&self, mut strength: f64) -> f64 { strength *= self.get_attribute_value(&Attributes::JUMP_STRENGTH); strength *= f64::from(self.entity.get_jump_velocity_multiplier()); if let Some(effect) = self.get_effect(&StatusEffect::JUMP_BOOST) { strength += 0.1 * f64::from(effect.amplifier + 1); } strength } pub fn fall( &self, caller: &dyn EntityBase, height_difference: f64, ground: bool, dont_damage: bool, ) { if ground { let fall_distance = self.fall_distance.swap(0.0); if fall_distance <= 0.0 || dont_damage || self.should_prevent_fall_damage() || self.should_prevent_fall_damage_in_area() || self.is_immune_to_fall_damage() { return; } let world = self.entity.world.load(); let block = world.get_block(&self.entity.get_pos_with_y_offset(0.2).0); let pumpkin_block = world.block_registry.get_pumpkin_block(block.id); if let Some(pumpkin_block) = pumpkin_block { pumpkin_block.on_landed_upon(OnLandedUponArgs { world: &world, fall_distance, entity: caller, }); } else { self.handle_fall_damage(caller, fall_distance, 1.0); } } else if height_difference < 0.0 { let new_fall_distance = if !self.should_prevent_fall_damage() && !self.should_prevent_fall_damage_in_area() { let distance = self.fall_distance.load(); distance - (height_difference as f32) } else { 0f32 }; self.fall_distance.store(new_fall_distance); } } pub fn handle_fall_damage( &self, caller: &dyn EntityBase, fall_distance: f32, damage_per_distance: f32, ) { let may_fly = caller.get_player().is_some_and(|player| { player .abilities .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .allow_flying }); if may_fly || self.is_immune_to_fall_damage() { return; } // Fetches the safe fall distance attribute let safe_fall_distance = self.get_attribute_value(&Attributes::SAFE_FALL_DISTANCE) as f32; let unsafe_fall_distance = fall_distance + 1.0E-6 - safe_fall_distance; let damage = (unsafe_fall_distance * damage_per_distance).floor(); if damage > 0.0 { let check_damage = self.damage(caller, damage, DamageType::FALL); // Fall if check_damage { self.entity .play_sound(Self::get_fall_sound(fall_distance as i32)); } } } const fn get_fall_sound(distance: i32) -> Sound { if distance > 4 { Sound::EntityGenericBigFall } else { Sound::EntityGenericSmallFall } } pub fn get_death_message( dyn_self: &dyn EntityBase, damage_type: DamageType, source: Option<&dyn EntityBase>, cause: Option<&dyn EntityBase>, ) -> TextComponent { match damage_type.death_message_type { DeathMessageType::Default => { if let Some(cause) = cause && source.is_some() { TextComponent::translate_cross( format!("death.attack.{}.player", damage_type.message_id), format!("death.attack.{}.player", damage_type.message_id), [dyn_self.get_display_name(), cause.get_display_name()], ) } else { TextComponent::translate_cross( format!("death.attack.{}", damage_type.message_id), format!("death.attack.{}", damage_type.message_id), [dyn_self.get_display_name()], ) } } DeathMessageType::FallVariants => { //TODO TextComponent::translate_cross( translation::java::DEATH_FELL_ACCIDENT_GENERIC, translation::bedrock::DEATH_FELL_ACCIDENT_GENERIC, [dyn_self.get_display_name()], ) } DeathMessageType::IntentionalGameDesign => TextComponent::text("[") .add_child(TextComponent::translate_cross( format!("death.attack.{}.message", damage_type.message_id), format!("death.attack.{}.message", damage_type.message_id), [dyn_self.get_display_name()], )) .add_child(TextComponent::text("]")), } } pub fn on_death( &self, damage_type: DamageType, source: Option<&dyn EntityBase>, cause: Option<&dyn EntityBase>, ) { let world = self.entity.world.load(); let Some(dyn_self) = world.get_entity_by_id(self.entity.entity_id) else { return; }; if self .dead .compare_exchange(false, true, Relaxed, Relaxed) .is_ok() { self.movement_input.store(Vector3::default()); self.jumping.store(false, Relaxed); // Statistics updates self.update_death_stats(&*dyn_self, cause); // Plays the death sound world.send_entity_status(&self.entity, EntityStatus::Death, Some(ActorEventID::Death)); let looting_level; let tool = if let Some(cause_ent) = cause { if let Some(player) = cause_ent .cast_any() .downcast_ref::() { let hand_stack = player .inventory() .get_stack_in_hand(pumpkin_util::Hand::Right); looting_level = hand_stack .get_enchantment_level(&Enchantment::LOOTING) .max(0) as u32; (!hand_stack.is_empty()).then(|| hand_stack.clone()) } else { looting_level = 0; None } } else { looting_level = 0; None }; let is_raining = world.is_raining(); let is_thundering = world.is_thundering(); let params = LootContextParameters { killed_by_player: cause.map(|c| c.get_entity().entity_type == &EntityType::PLAYER), this_entity: Some(self.entity.entity_type), killer_entity: cause.map(|c| c.get_entity().entity_type), direct_killer_entity: source.map(|s| s.get_entity().entity_type), position: Some(self.entity.pos.load()), world_time: world.level_info.load().day_time as u64, damage_type: Some(damage_type), tool, is_raining: Some(is_raining), is_thundering: Some(is_thundering), is_on_fire: Some( self.entity .fire_ticks .load(std::sync::atomic::Ordering::Relaxed) > 0, ), ..Default::default() }; // Drop loot self.drop_loot(params.clone()); // Award experience if params.killed_by_player.unwrap_or(false) && world.level_info.load().game_rules.mob_drops { let amount = dyn_self.get_experience_reward(cause); if amount > 0 { ExperienceOrbEntity::spawn(&world, self.entity.pos.load(), amount); } } self.entity.pose.store(EntityPose::Dying); self.drop_equipment(looting_level); // Broadcast death message if it's a player and the gamerule is enabled self.broadcast_death_message(&*dyn_self, damage_type, source, cause); // Trigger on_mob_death for active status effects let active_effects_vec: Vec<_> = { let effects = self .active_effects .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); effects .values() .map(|e| (e.effect_type, e.amplifier)) .collect() }; for (effect_type, amplifier) in active_effects_vec { if let Some(mob_effect) = crate::entity::effect::get_mob_effect(effect_type) { mob_effect.on_mob_death(self, amplifier, &damage_type); } } self.reset_effects_and_attributes(); } } fn drop_equipment(&self, looting_level: u32) { let world = self.entity.world.load(); let block_pos = self.entity.block_pos.load(); let drop_chances = self .equipment_drop_chances .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); let slots_to_drop: Vec = { let mut slots: Vec<_> = self.equipment_slots.values().cloned().collect(); slots.push(EquipmentSlot::MAIN_HAND); slots }; for slot in &slots_to_drop { let mut chance = drop_chances .get(slot) .copied() .unwrap_or(DEFAULT_EQUIPMENT_DROP_CHANCE); // Vanilla approximation: EnchantmentHelper.processEquipmentDropChance // adds lootingLevel * 0.01 to the per-slot equipment drop chance. chance += looting_level as f32 * 0.01; chance = chance.min(1.0); if rand::random::() >= chance { continue; } let mut item = self .entity_equipment .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .equipment .remove(slot) .unwrap_or_else(|| ItemStack::EMPTY.clone()); if item.is_empty() { continue; } // Vanilla approximation: Mob.dropCustomDeathLoot applies random // damage to dropped equipment using two chained random calls: // setDamageValue(maxDamage - random.nextInt(1 + random.nextInt(max(maxDamage - 3, 1)))) if let Some(max_damage) = item.get_max_damage() { let mut rng = rand::rng(); let inner = rng.random_range(0..(max_damage - 3).max(1)); let outer = rng.random_range(0..=inner); item.set_damage((max_damage - outer).max(0)); } world.drop_stack(&block_pos, item); } } fn broadcast_death_message( &self, dyn_self: &dyn EntityBase, damage_type: DamageType, source: Option<&dyn EntityBase>, cause: Option<&dyn EntityBase>, ) { let world = self.entity.world.load(); let show_death_messages = { world.level_info.load().game_rules.show_death_messages }; if self.entity.entity_type == &EntityType::PLAYER && show_death_messages { //TODO: KillCredit let death_message = Self::get_death_message(dyn_self, damage_type, source, cause); if let Some(server) = world.server.upgrade() { for player in server.get_all_players() { player.send_system_message(&death_message); } } } } fn update_death_stats(&self, dyn_self: &dyn EntityBase, cause: Option<&dyn EntityBase>) { if let Some(victim_player) = dyn_self.get_player() { victim_player.increment_stat( StatisticCategory::Custom, CustomStatistic::Deaths as i32, 1, ); victim_player.set_stat( StatisticCategory::Custom, CustomStatistic::TimeSinceDeath as i32, 0, ); if let Some(killer_entity) = cause.map(EntityBase::get_entity) { victim_player.increment_stat( StatisticCategory::KilledBy, killer_entity.entity_type.id as i32, 1, ); } } if let Some(killer_player) = cause.and_then(|c| c.get_player()) { if dyn_self.get_player().is_some() { killer_player.increment_stat( StatisticCategory::Custom, CustomStatistic::PlayerKills as i32, 1, ); } else { killer_player.increment_stat( StatisticCategory::Custom, CustomStatistic::MobKills as i32, 1, ); let resource_name = self.entity.entity_type.resource_name; let criterion_key = format!("minecraft:{resource_name}"); killer_player.trigger_advancement( crate::entity::player::advancement::trigger::AdvancementTrigger::PlayerKilledEntity { entity_type_resource: criterion_key, }, ); if resource_name == "skeleton" { let distance_sq = killer_player .position() .squared_distance_to_vec(&self.entity.pos.load()); if distance_sq >= 2500.0 { killer_player.trigger_advancement(crate::entity::player::advancement::trigger::AdvancementTrigger::SniperDuel); } } if resource_name == "phantom" { killer_player.trigger_advancement(crate::entity::player::advancement::trigger::AdvancementTrigger::TwoBirdsOneArrow); } let held_item = killer_player.inventory().held_item(); let is_crossbow = held_item.item.registry_key == "crossbow"; if is_crossbow { killer_player.trigger_advancement( crate::entity::player::advancement::trigger::AdvancementTrigger::Arbalistic, ); } } killer_player.increment_stat( StatisticCategory::Killed, self.entity.entity_type.id as i32, 1, ); } } fn drop_loot(&self, params: LootContextParameters) { if let Some(loot_table) = &self.get_entity().entity_type.loot_table { let pos = self.entity.block_pos.load(); for stack in loot_table.get_loot(params) { self.entity.world.load().drop_stack(&pos, stack); } } } fn tick_effects(&self) { let mut effects_to_remove = Vec::new(); let mut effects_to_apply = Vec::new(); { let Ok(mut effects) = self.active_effects.try_lock() else { return; }; let entity_age = self.entity.age.load(Relaxed); for effect in effects.values_mut() { if effect.duration == 0 { effects_to_remove.push(effect.effect_type); continue; } let tick_duration = if effect.duration == -1 { entity_age } else { effect.duration }; if let Some(mob_effect) = crate::entity::effect::get_mob_effect(effect.effect_type) && mob_effect.should_apply_effect_tick(tick_duration, effect.amplifier) { effects_to_apply.push((mob_effect, effect.amplifier)); } if effect.duration != -1 { effect.duration -= 1; } } } // Call the central removal function for each expired effect for effect_type in effects_to_remove { self.remove_effect(effect_type); } for (mob_effect, amplifier) in effects_to_apply { mob_effect.apply_effect_tick(self, amplifier); } } /// Tries to use a totem of undying from the entity's hands. If successful, applies the totem effects and returns true. #[allow(dead_code)] async fn try_use_death_protector(&self, caller: &dyn EntityBase) -> bool { for hand in Hand::all() { let mut stack = self.get_stack_in_hand(caller, hand); // Clear the stack and use the totem of undying if stack.get_data_component::().is_some() { let mut resurrect_event = crate::plugin::api::events::entity::entity_resurrect::EntityResurrectEvent::new( self.entity.entity_id, ); if let Some(server) = self.entity.world.load().server.upgrade() { server .plugin_manager .fire(&server, &mut resurrect_event) .await; } if resurrect_event.cancelled { return false; } stack.clear(); let slot = match hand { Hand::Right => EquipmentSlot::MAIN_HAND, Hand::Left => EquipmentSlot::OFF_HAND, }; if let Some(player) = caller.get_player() { player .inventory() .entity_equipment .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .equipment .insert(slot, stack); } else { self.entity_equipment .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .equipment .insert(slot, stack); } self.set_health(1.0); self.entity.world.load().send_entity_status( &self.entity, EntityStatus::ProtectedFromDeath, Some(ActorEventID::InstantDeath), ); // Set Absorption, Regeneration, and Fire Resistance effects self.add_effect(Effect { effect_type: &StatusEffect::ABSORPTION, duration: 100, amplifier: 1, ambient: false, show_particles: true, show_icon: true, blend: false, }); self.add_effect(Effect { effect_type: &StatusEffect::REGENERATION, duration: 900, amplifier: 1, ambient: false, show_particles: true, show_icon: true, blend: false, }); self.add_effect(Effect { effect_type: &StatusEffect::FIRE_RESISTANCE, duration: 800, amplifier: 0, ambient: false, show_particles: true, show_icon: true, blend: false, }); return true; } } false } #[allow(dead_code)] fn damage_armor_items(&self, caller: &dyn EntityBase, damage_amount: f32) { // Formula: armor loses floor(incoming_damage / 4) durability, minimum 1. let armor_damage = (damage_amount / 4.0).floor().max(1.0) as i32; let mut equipment_updates = Vec::new(); // TODO: Falling anvil/stalactite should only damage the helmet slot. // TODO: Implement DAMAGE_RESISTANT component checks (e.g. netherite vs fire). let armor_slots: Vec<(usize, ItemStack, EquipmentSlot)> = { let equipment_lock = self .entity_equipment .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); self.equipment_slots .iter() .filter(|(_, slot)| slot.is_armor_slot()) .filter_map(|(index, slot)| { equipment_lock .equipment .get(slot) .cloned() .map(|stack| (*index, stack, slot.clone())) }) .collect() }; for (slot_index, mut stack, slot) in armor_slots { if stack.is_empty() { continue; } let takes_damage = stack .get_data_component::() .is_none_or(|equippable| equippable.damage_on_hurt); if takes_damage { let slot_result = stack.damage_item(armor_damage); if slot_result != pumpkin_data::item_stack::DamageResult::Untouched { if slot_result == pumpkin_data::item_stack::DamageResult::Broken { let world = self.entity.world.load(); world.send_entity_status( &self.entity, super::equipment_break_status(&slot), None, ); } equipment_updates.push((slot.clone(), stack.clone())); if let Some(player) = caller.get_player() { player.enqueue_slot_set_packet(&CSetPlayerInventory::new( (slot_index as i32).into(), &ItemStackSerializer::from(stack), )); } } } } if !equipment_updates.is_empty() { self.send_equipment_changes(&equipment_updates); } } pub fn held_item(&self, caller: &dyn EntityBase) -> ItemStack { if let Some(player) = caller.get_player() { return player.inventory.held_item(); } let equipment = self .entity_equipment .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); equipment .equipment .get(&EquipmentSlot::MAIN_HAND) .cloned() .unwrap_or_else(|| ItemStack::EMPTY.clone()) } pub fn get_stack_in_hand(&self, caller: &dyn EntityBase, hand: Hand) -> ItemStack { match hand { Hand::Left => self.off_hand_item(caller), Hand::Right => self.held_item(caller), } } /// getOffHandStack in source pub fn off_hand_item(&self, caller: &dyn EntityBase) -> ItemStack { if let Some(player) = caller.get_player() { return player.inventory.off_hand_item(); } let Some(slot) = self.equipment_slots.get(&PlayerInventory::OFF_HAND_SLOT) else { return ItemStack::EMPTY.clone(); }; let equipment = self .entity_equipment .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); equipment .equipment .get(slot) .cloned() .unwrap_or_else(|| ItemStack::EMPTY.clone()) } pub fn can_take_damage(&self) -> bool { !self.entity.invulnerable.load(Ordering::Relaxed) && self.is_part_of_game() } pub fn is_part_of_game(&self) -> bool { !self.is_spectator() && self.entity.is_alive() } pub fn reset_state(&self) { self.entity.reset_state(); // Restore to maximum health for this entity type let max_health = self.get_max_health(); self.set_health(max_health); // Clear any absorption self.absorption.store(0.0); // Send health metadata self.entity.send_meta_data( &[Metadata::new( tracked_data::living_entity::DATA_HEALTH_ID, max_health, )], None, ); self.reset_effects_and_attributes(); // Give a short grace period of invulnerability after respawn self.hurt_cooldown.store(20, Relaxed); self.last_damage_taken.store(0f32); self.entity.portal_cooldown.store(0, Relaxed); *self .entity .portal_manager .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) = None; // Clear fall/fire state self.fall_distance.store(0f32); self.death_time.store(0, Relaxed); self.entity.extinguish(); self.entity.fire_ticks.store(0, Relaxed); // Clear velocity and movement input to remove persisted momentum self.entity.velocity.store(Vector3::default()); self.entity.velocity_dirty.store(true, SeqCst); self.movement_input.store(Vector3::default()); self.jumping.store(false, Relaxed); // If this LivingEntity corresponds to a Player, reset their hunger manager let world = self.entity.world.load(); if let Some(player) = world.get_player_by_id(self.entity.entity_id) { player.hunger_manager.restart(); } self.dead.store(false, Relaxed); } pub fn is_player(&self) -> bool { let world = self.entity.world.load(); world.get_player_by_id(self.entity.entity_id).is_some() } pub fn get_movement(&self) -> Vector3 { self.entity.movement.load() } fn hurt_sound(&self) -> Sound { if self.entity.entity_type == &EntityType::SLIME { SlimeEntity::hurt_sound_for_size(self.entity.data.load(Relaxed)) } else { Self::hurt_sound_for_entity(self.entity.entity_type) } } } impl LivingEntity { pub fn write_living_nbt(&self, nbt: &mut NbtCompound) { nbt.put("Health", NbtTag::Float(self.health.load())); // Avoid persisting a lethal fall distance when the entity is dead to prevent death loops let fall_distance = if self.dead.load(Relaxed) { 0.0 } else { self.fall_distance.load() }; // Persist current absorption amount nbt.put("AbsorptionAmount", NbtTag::Float(self.absorption.load())); nbt.put("FallDistance", NbtTag::Float(fall_distance)); nbt.put_short("HurtTime", self.hurt_cooldown.load(Relaxed).max(0) as i16); nbt.put_short("DeathTime", i16::from(self.death_time.load(Relaxed))); nbt.put_bool("FallFlying", self.entity.is_fall_flying()); { let effects_vec: Vec = { let effects = self .active_effects .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); effects.values().cloned().collect() }; if !effects_vec.is_empty() { // Iterate effects and create Box<[NbtTag]> let mut effects_list = Vec::with_capacity(effects_vec.len()); for effect in effects_vec { let mut effect_nbt = pumpkin_nbt::compound::NbtCompound::new(); effect.write_nbt(&mut effect_nbt); effects_list.push(NbtTag::Compound(effect_nbt)); } nbt.put("active_effects", NbtTag::List(effects_list)); } } //TODO: write equipment // todo more... } pub fn read_living_nbt_non_mut(&self, nbt: &NbtCompound) { self.health.store(nbt.get_float("Health").unwrap_or(20.0)); // Clamp any persisted absorption to the entity's configured max let raw_abs = nbt.get_float("AbsorptionAmount").unwrap_or(0.0); let max_abs = self.get_attribute_value(&Attributes::MAX_ABSORPTION) as f32; let clamped_abs = raw_abs.max(0.0).min(max_abs); self.absorption.store(clamped_abs); // Load fall distance, but if this entity is currently marked dead ensure we don't restore // a lethal fall distance that would immediately re-kill on spawn. let fd = nbt .get_float("FallDistance") .or_else(|| nbt.get_float("fall_distance")) .unwrap_or(0.0); if self.dead.load(Relaxed) { self.fall_distance.store(0.0); } else { self.fall_distance.store(fd); } if let Some(hurt_time) = nbt.get_short("HurtTime") { self.hurt_cooldown.store(i32::from(hurt_time), Relaxed); } if let Some(death_time) = nbt.get_short("DeathTime") { self.death_time.store(death_time as u8, Relaxed); } self.entity .fall_flying .store(nbt.get_bool("FallFlying").unwrap_or(false), Relaxed); { let nbt_effects = nbt.get_list("active_effects"); if let Some(nbt_effects) = nbt_effects { let mut read_effects = Vec::new(); for effect in nbt_effects { if let NbtTag::Compound(effect_nbt) = effect { if let Some(mut effect) = Effect::create_from_nbt(&mut effect_nbt.clone()) { effect.blend = true; // TODO: change, is taken from effect give command read_effects.push(effect); } else { warn!("Unable to read effect from nbt"); } } } if !read_effects.is_empty() { let mut active_effects = self .active_effects .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); for effect in read_effects { active_effects.insert(effect.effect_type, effect); } } } } // todo more... } #[allow(clippy::too_many_lines)] pub fn damage_with_context( &self, caller: &dyn EntityBase, amount: f32, damage_type: DamageType, position: Option>, source: Option<&dyn EntityBase>, cause: Option<&dyn EntityBase>, ) -> bool { let mut amount = amount; // Check invulnerability before applying damage if self.entity.is_invulnerable_to(&damage_type) { return false; } if self.health.load() <= 0.0 || self.dead.load(Relaxed) { return false; // Dying or dead } if amount < 0.0 { return false; } let mut damage_event = crate::plugin::api::events::entity::entity_damage::EntityDamageEvent::new( self.entity.entity_id, damage_type, amount, ); if let Some(server) = self.entity.world.load().server.upgrade() { server .plugin_manager .fire_blocking(&server, &mut damage_event); } if damage_event.cancelled { return false; } amount = damage_event.damage; let world = self.entity.world.load(); let is_fire_damage = damage_type == DamageType::IN_FIRE || damage_type == DamageType::ON_FIRE || damage_type == DamageType::LAVA || damage_type == DamageType::HOT_FLOOR; // Fire damage can be prevented by either game rules or fire resistance if is_fire_damage { // Check game rule for fire damage (only for players) if self.entity.entity_type == &EntityType::PLAYER && !world.level_info.load().game_rules.fire_damage { return false; } // Check for fire resistance effect if self.has_effect(&StatusEffect::FIRE_RESISTANCE) { return false; } } // Vanilla parity: entities in FREEZE_HURTS_EXTRA_TYPES take 5x freezing damage. if damage_type == DamageType::FREEZE && self .entity .entity_type .has_tag(&tag::EntityType::MINECRAFT_FREEZE_HURTS_EXTRA_TYPES) { amount *= 5.0; } // These damage types bypass the hurt cooldown and death protection let bypasses_cooldown_protection = damage_type == DamageType::GENERIC_KILL || damage_type == DamageType::OUT_OF_WORLD; let mut damage_after_armor = amount; if !bypasses_armor_durability(&damage_type) { let mut armor = 0.0f32; let mut toughness = 0.0f32; { let equipment_lock = self .entity_equipment .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); for slot in [ EquipmentSlot::HEAD, EquipmentSlot::CHEST, EquipmentSlot::LEGS, EquipmentSlot::FEET, ] { if let Some(stack) = equipment_lock.equipment.get(&slot) && !stack.is_empty() && let Some(modifiers) = stack.get_data_component::() { for modifier in modifiers.attribute_modifiers.iter() { if modifier.r#type == &Attributes::ARMOR { armor += modifier.amount as f32; } else if modifier.r#type == &Attributes::ARMOR_TOUGHNESS { toughness += modifier.amount as f32; } } } } } let value = 2.0f32 + toughness / 4.0; let clamped_armor = (armor - damage_after_armor / value) .max(armor / 5.0) .min(20.0); damage_after_armor *= 1.0 - clamped_armor / 25.0; } let mut damage_after_enchantments = damage_after_armor; if damage_type != DamageType::OUT_OF_WORLD { let mut epf = 0i32; { let equipment_lock = self .entity_equipment .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); for slot in [ EquipmentSlot::HEAD, EquipmentSlot::CHEST, EquipmentSlot::LEGS, EquipmentSlot::FEET, ] { if let Some(stack) = equipment_lock.equipment.get(&slot) && !stack.is_empty() && let Some(enchantments) = stack.get_data_component::() { for (enchantment, level) in enchantments.enchantment.iter() { let mut factor = 0; let enc = *enchantment; if enc == &Enchantment::PROTECTION { if damage_type != DamageType::DROWN && damage_type != DamageType::STARVE && damage_type != DamageType::GENERIC_KILL { factor = *level; } } else if enc == &Enchantment::FIRE_PROTECTION { if is_fire_damage { factor = *level * 2; } } else if enc == &Enchantment::BLAST_PROTECTION { if damage_type == DamageType::EXPLOSION || damage_type == DamageType::PLAYER_EXPLOSION { factor = *level * 2; } } else if enc == &Enchantment::PROJECTILE_PROTECTION { if damage_type == DamageType::ARROW || damage_type == DamageType::MOB_PROJECTILE || damage_type == DamageType::THROWN { factor = (*level) * 2; } } else if enc == &Enchantment::FEATHER_FALLING && damage_type == DamageType::FALL { factor = (*level) * 4; } epf += factor; } } } } epf = epf.min(20); if epf > 0 { damage_after_enchantments *= 1.0 - (epf as f32 * 0.04); } } // Apply Resistance effect reduction (20% per level) let resistance_reduction = if !bypasses_cooldown_protection && damage_type != DamageType::STARVE { self.get_effect(&StatusEffect::RESISTANCE) .map_or(0.0, |e| 0.2 * (e.amplifier + 1) as f32) } else { 0.0 }; // Total damage after reductions let effective_amount = damage_after_enchantments * (1.0 - resistance_reduction); if resistance_reduction > 0.0 { let resisted = damage_after_enchantments * resistance_reduction; if let Some(player) = caller.get_player() { player.increment_stat( StatisticCategory::Custom, CustomStatistic::DamageResisted as i32, (resisted * 10.0) as i32, ); } if let Some(attacker_player) = cause.and_then(|c| c.get_player()) { attacker_player.increment_stat( StatisticCategory::Custom, CustomStatistic::DamageDealtResisted as i32, (resisted * 10.0) as i32, ); } } // Check for shield blocking if self.is_blocking() && !damage_type.has_tag(&tag::DamageType::MINECRAFT_BYPASSES_SHIELD) && let Some(pos) = position { let player_pos = self.entity.pos.load(); let look_vec = Vector3::rotation_vector(0.0, self.entity.yaw.load() as f64); let mut source_to_player = (player_pos - pos).normalize(); source_to_player.y = 0.0; if source_to_player.dot(&look_vec) < 0.0 { world.play_sound(Sound::ItemShieldBlock, SoundCategory::Players, &player_pos); if let Some(player) = caller.get_player() { player.increment_stat( StatisticCategory::Custom, CustomStatistic::DamageBlockedByShield as i32, (effective_amount * 10.0) as i32, ); } let active_hand = self .active_hand .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); if let Some(hand) = *active_hand { let slot = if hand == Hand::Left { EquipmentSlot::MAIN_HAND } else { EquipmentSlot::OFF_HAND }; let mut equipment_guard = self .entity_equipment .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); if let Some(stack) = equipment_guard.equipment.get_mut(&slot) { let durability_damage = (amount / 1.0).floor().max(1.0) as i32; if stack.damage_item(durability_damage) == DamageResult::Broken { if let Some(player) = caller.get_player() { player.increment_stat( StatisticCategory::Broken, stack.item.id as i32, 1, ); } world.send_entity_status( &self.entity, crate::entity::equipment_break_status(&slot), None, ); *stack = ItemStack::EMPTY.clone(); let broken_stack = stack.clone(); drop(equipment_guard); self.send_equipment_changes(&[(slot, broken_stack)]); self.clear_active_hand(); } } } return false; } } // Apply hurt cooldown logic let last_damage = self.last_damage_taken.load(); let (damage_amount, play_sound) = if self.hurt_cooldown.load(Relaxed) > 10 && !bypasses_cooldown_protection { if effective_amount <= last_damage { return false; } (effective_amount - last_damage, false) } else { self.hurt_cooldown.store(20, Relaxed); (effective_amount, true) }; // Finalize state self.last_damage_taken.store(amount); let damage_amount = damage_amount.max(0.0); let Some(server) = world.server.upgrade() else { return false; }; let config = &server.advanced_config.pvp; if config.hurt_animation { let entity_id = self.entity.entity_id; let hurt_yaw = source.map_or(0.0, |source| { let src = source.get_entity().pos.load(); let tgt = self.entity.pos.load(); (src.z - tgt.z).atan2(src.x - tgt.x).to_degrees() as f32 - self.entity.yaw.load() }); let hurt_event = SActorEvent { target_runtime_id: VarULong(entity_id as u64), event_id: ActorEventID::Hurt, data: VarInt(0), fire_at_position: None, }; world.broadcast_to_chunk( self.entity.chunk_pos.load(), &CHurtAnimation::new(entity_id.into(), hurt_yaw), ); world.broadcast_to_chunk_bedrock(self.entity.chunk_pos.load(), &hurt_event); } world.broadcast_packet_all(&CDamageEvent::new( self.entity.entity_id.into(), damage_type.id.into(), source.map(|e| e.get_entity().entity_id.into()), cause.map(|e| e.get_entity().entity_id.into()), position, )); if play_sound { world.play_sound( self.hurt_sound(), SoundCategory::Players, &self.entity.pos.load(), ); if let Some(source) = source { let source_pos = source.get_entity().pos.load(); let target_pos = self.entity.pos.load(); let dx = source_pos.x - target_pos.x; let dz = source_pos.z - target_pos.z; let resistance = self.get_attribute_value(&Attributes::KNOCKBACK_RESISTANCE); self.entity .apply_knockback(knockback_after_resistance(0.4, resistance), dx, dz); self.entity.send_velocity(); } } // Consume absorption first, then apply remaining damage to health let mut remaining = damage_amount; let current_abs = self.absorption.load(); if current_abs > 0.0 { let absorbed = current_abs.min(remaining); if let Some(player) = caller.get_player() { player.increment_stat( StatisticCategory::Custom, CustomStatistic::DamageAbsorbed as i32, (absorbed * 10.0) as i32, ); } if let Some(attacker_player) = cause.and_then(|c| c.get_player()) { attacker_player.increment_stat( StatisticCategory::Custom, CustomStatistic::DamageDealtAbsorbed as i32, (absorbed * 10.0) as i32, ); } if current_abs >= remaining { let new_abs = current_abs - remaining; self.set_absorption(new_abs); remaining = 0.0; } else { remaining -= current_abs; self.set_absorption(0.0); } if let Some(attacker) = cause.or(source) { self.last_attacker_id .store(attacker.get_entity().entity_id, Relaxed); self.last_attacked_time .store(self.entity.age.load(Relaxed), Relaxed); } } let max_h = self.get_max_health(); let new_health = self.health.load() - remaining; let clamped_health = new_health.max(0.0).min(max_h); if remaining > 0.0 { self.set_health(clamped_health); if let Some(player) = caller.get_player() { player.increment_stat( StatisticCategory::Custom, CustomStatistic::DamageTaken as i32, (remaining * 10.0) as i32, ); } if let Some(attacker_player) = cause.and_then(|c| c.get_player()) { attacker_player.increment_stat( StatisticCategory::Custom, CustomStatistic::DamageDealt as i32, (remaining * 10.0) as i32, ); } if let Some(attacker) = cause.or(source) { self.last_attacker_id .store(attacker.get_entity().entity_id, Relaxed); self.last_attacked_time .store(self.entity.age.load(Relaxed), Relaxed); } } if clamped_health <= 0.0 { let mut death_event = crate::plugin::api::events::entity::entity_death::EntityDeathEvent::new( self.entity.entity_id, 0, ); if let Some(server) = world.server.upgrade() { server .plugin_manager .fire_blocking(&server, &mut death_event); } world.send_entity_status( &self.entity, pumpkin_data::entity::EntityStatus::Death, None, ); } true } pub fn damage(&self, caller: &dyn EntityBase, amount: f32, damage_type: DamageType) -> bool { self.damage_with_context(caller, amount, damage_type, None, None, None) } } impl EntityBase for LivingEntity { fn damage_with_context( &self, caller: &dyn EntityBase, amount: f32, damage_type: DamageType, position: Option>, source: Option<&dyn EntityBase>, cause: Option<&dyn EntityBase>, ) -> bool { self.damage_with_context(caller, amount, damage_type, position, source, cause) } fn tick_in_void(&self, dyn_self: &dyn EntityBase) { dyn_self.damage(dyn_self, 4.0, DamageType::OUT_OF_WORLD); } fn get_gravity(&self) -> f64 { self.get_attribute_value(&Attributes::GRAVITY) } #[allow(clippy::too_many_lines)] fn tick(&self, caller: &dyn EntityBase, server: &Server) { self.entity.tick(caller, server); // Only tick movement if the entity is alive. This prevents a dead "corpse" // from continuing to be simulated (accumulating fall_distance/velocity). // We allow movement during death animation (20 ticks) so knockback is applied. let is_alive = !self.dead.load(Relaxed) && self.health.load() > 0.0; let in_death_animation = self.health.load() <= 0.0 && self.death_time.load(Relaxed) < 20; if is_alive || (in_death_animation && self.entity.entity_type != &EntityType::PLAYER) { self.tick_movement(caller); // Vanilla-like order: freeze logic runs after movement/collisions. self.entity.tick_frozen(caller); } // TODO let player = caller.get_player(); let is_player = player.is_some(); if !is_player { self.entity.send_pos_rot(); } // Fetch supporting blocks for players or other entities let supporting_pos = caller.get_player().map_or_else( || self.entity.get_supporting_block_pos(), super::player::Player::get_supporting_block_pos, ); // Notify the block under the entity each tick if a supporting block position is found if self.entity.is_affected_by_blocks() && let Some(supporting) = supporting_pos { let world = self.entity.world.load_full(); let (block, state) = world.get_block_and_state(&supporting); world .block_registry .on_entity_step(block, &world, caller, &supporting, state, false); // Check slightly below supporting_pos for additional supporting blocks (blocks under carpets and the like) if !block.is_solid() { let below_supporting = supporting.down(); let (below_block, below_state) = world.get_block_and_state(&below_supporting); // If block is not air, notify it as well world.block_registry.on_entity_step( below_block, &world, caller, &below_supporting, below_state, true, // below supporting block ); } } self.tick_effects(); // Current active item if self.item_use_time.load(Ordering::Relaxed) > 0 && self.item_use_time.fetch_sub(1, Ordering::Relaxed) <= 1 { let item_in_use = self .item_in_use .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .clone(); if let Some(item) = item_in_use.as_ref() { // Consume item let mut is_potion = false; if let Some(food) = item.get_data_component::() && let Some(player) = caller.get_player() { player .hunger_manager .eat(player, food.nutrition as u8, food.saturation); } self.apply_consumable_effects(caller, item); // Handle potion consumption if item .get_data_component::() .is_some() { let effects = crate::item::potion::PotionContents::read_potion_effects(item); crate::item::potion::PotionContents::apply_effects_to( self, effects, 1.0, crate::item::potion::PotionApplicationSource::Normal, ); is_potion = true; } if let Some(player) = caller.get_player() { player.trigger_advancement( crate::entity::player::advancement::trigger::AdvancementTrigger::ConsumeItem { item_id: format!("minecraft:{}", item.item.registry_key), }, ); // Prefer modifying the exact stack that matches the consumed item: // 1) selected hotbar (held_item) // 2) off-hand // 3) fallback to active_hand if the above didn't match let mut handled = false; // Check main hand (hotbar selected) let mut held = player.inventory.held_item(); if held.are_items_and_components_equal(item) { if is_potion { if player.gamemode.load() != GameMode::Creative { held.decrement(1); if held.is_empty() { held = ItemStack::new(1, &Item::GLASS_BOTTLE); } } } else { held.decrement_unless_creative(player.gamemode.load(), 1); } player.inventory.set_held_item(held); handled = true; } if !handled { // Check off-hand let mut off_hand = player.inventory.off_hand_item(); if off_hand.are_items_and_components_equal(item) { if is_potion { if player.gamemode.load() != GameMode::Creative { off_hand.decrement(1); if off_hand.is_empty() { off_hand = ItemStack::new(1, &Item::GLASS_BOTTLE); } } } else { off_hand.decrement_unless_creative(player.gamemode.load(), 1); } player.inventory.set_stack_in_hand(Hand::Left, off_hand); handled = true; } } if !handled { // Use stored active_hand (as a fallback) let active_hand = *self .active_hand .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); let hand_to_modify = active_hand.unwrap_or(Hand::Right); let mut item_stack = self.get_stack_in_hand(caller, hand_to_modify); if is_potion { if player.gamemode.load() != GameMode::Creative { item_stack.decrement(1); if item_stack.is_empty() { item_stack = ItemStack::new(1, &Item::GLASS_BOTTLE); } } } else { item_stack.decrement_unless_creative(player.gamemode.load(), 1); } player .inventory .set_stack_in_hand(hand_to_modify, item_stack); } if let Some(cooldown) = item.get_use_cooldown() { let group = cooldown .cooldown_group .clone() .unwrap_or_else(|| item.item.registry_key.to_string()); player.start_cooldown(group, (cooldown.seconds * 20.0) as i32); } } self.clear_active_hand(); } } if self.hurt_cooldown.load(Relaxed) > 0 { self.hurt_cooldown.fetch_sub(1, Relaxed); } if self.health.load() <= 0.0 { let time = self .death_time .fetch_update(Relaxed, Relaxed, |time| Some(time.saturating_add(1))) .unwrap_or_else(|time| time) .saturating_add(1); // Players remain part of the world until their client requests a // respawn. Removing one here breaks reconnecting while dead. if self.entity.entity_type == &EntityType::PLAYER { return; } // Only send death particles once (on the exact tick death_time reaches 20) // and then remove the entity, preventing entity_event spam. if time == 20 && !self.entity.removed.swap(true, Ordering::Relaxed) { self.entity.world.load().send_entity_status( &self.entity, EntityStatus::Death, Some(ActorEventID::Death), ); self.entity.remove(); } } } fn get_entity(&self) -> &Entity { &self.entity } fn get_living_entity(&self) -> Option<&LivingEntity> { Some(self) } fn is_pushable(&self) -> bool { self.health.load() > 0.0 && !self.dead.load(Relaxed) } fn cast_any(&self) -> &dyn std::any::Any { self } } impl LivingEntity { /// Applies data-driven `apply_effects` consume effects after an item completes use. /// Vanilla: `Consumable.onConsume` invokes every configured effect server-side. fn apply_consumable_effects(&self, caller: &dyn EntityBase, item: &ItemStack) { let Some(consumable) = item.get_data_component::() else { return; }; for consume_effect in consumable.effects.iter() { match consume_effect { ConsumeEffect::ApplyEffects((effects, probability)) => { if !consume_effect_probability_applies(*probability, rand::random()) { continue; } for effect in effects.iter() { let Some(effect_type) = StatusEffect::from_minecraft_name(&effect.effect_id) else { continue; }; let Ok(amplifier) = u8::try_from(effect.amplifier) else { continue; }; self.add_effect(Effect { effect_type, duration: effect.duration, amplifier, ambient: effect.ambient, show_particles: effect.show_particles, show_icon: effect.show_icon, blend: false, }); } } ConsumeEffect::ClearAllEffects => { self.reset_effects_and_attributes(); } ConsumeEffect::RemoveEffects(idset) => { if let pumpkin_data::data_component_impl::IDSet::IDs(ids) = idset { for effect_type in ids.iter() { self.remove_effect(effect_type); } } } ConsumeEffect::TeleportRandomly(diameter) => { // Java Edition dismounts the consumer before random teleport attempts. let vehicle = caller .get_entity() .vehicle .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .clone(); if let Some(vehicle) = vehicle { vehicle .get_entity() .remove_passenger_sync(caller.get_entity().entity_id); if caller.get_entity().has_vehicle() { continue; } } let center = self.entity.pos.load(); let Some(pos) = self.find_random_teleport_target(*diameter) else { continue; }; let (yaw, pitch) = (self.entity.yaw.load(), self.entity.pitch.load()); let world = self.entity.world.load_full(); caller.teleport(pos, Some(yaw), Some(pitch), world.clone()); let destination = self.entity.pos.load(); if destination != center { self.fall_distance.store(0.0); // Vanilla broadcasts entity event 46 (teleport particles) on success. world.send_entity_status(&self.entity, EntityStatus::Teleport, None); world.emit_game_event("teleport", center); world.play_sound( Sound::ItemChorusFruitTeleport, SoundCategory::Players, &destination, ); } } ConsumeEffect::PlaySound(_) => {} } } } fn find_random_teleport_target(&self, diameter: f32) -> Option> { let center = self.entity.pos.load(); let world = self.entity.world.load(); let bottom_y = world.get_bottom_y(); let top_y = world.get_top_y(); let dimensions = self.entity.entity_dimension.load(); let mut rng = rand::rng(); 'attempts: for _ in 0..Self::RANDOM_TELEPORT_ATTEMPTS { let target_x = random_teleport_coordinate(center.x, diameter, rng.random()); let target_z = random_teleport_coordinate(center.z, diameter, rng.random()); let sampled_y = random_teleport_coordinate(center.y, diameter, rng.random()) .clamp(f64::from(bottom_y + 1), f64::from(top_y)); let mut block_y = sampled_y.floor() as i32; let block_x = target_x.floor() as i32; let block_z = target_z.floor() as i32; loop { if block_y <= bottom_y { continue 'attempts; } let below = BlockPos::new(block_x, block_y - 1, block_z); let Some(below_state) = world.get_block_state_if_loaded(&below) else { continue 'attempts; }; if below_state.is_solid() { break; } block_y -= 1; } let target = Vector3::new(target_x, f64::from(block_y), target_z); let bounding_box = BoundingBox::new_from_pos(target.x, target.y, target.z, &dimensions); for block_pos in BlockPos::iterate(bounding_box.min_block_pos(), bounding_box.max_block_pos()) { if world.get_block_state_if_loaded(&block_pos).is_none() || world.get_fluid(&block_pos).id != Fluid::EMPTY.id { continue 'attempts; } } if world.is_space_empty(bounding_box) { return Some(target); } } None } } fn random_teleport_coordinate(center: f64, diameter: f32, random: f64) -> f64 { center + (random - 0.5) * f64::from(diameter) } /// Mirrors vanilla's strict `random < probability` consume-effect gate. const fn consume_effect_probability_applies(probability: f32, random: f32) -> bool { random < probability } #[cfg(test)] mod consumable_effect_tests { use super::{consume_effect_probability_applies, random_teleport_coordinate}; #[test] fn consumable_effect_probability_matches_vanilla_strict_threshold() { assert!(!consume_effect_probability_applies(0.0, 0.0)); assert!(consume_effect_probability_applies(1.0, 0.999)); assert!(consume_effect_probability_applies(0.5, 0.499)); assert!(!consume_effect_probability_applies(0.5, 0.5)); } #[test] fn random_teleport_coordinate_uses_full_diameter() { assert_eq!(random_teleport_coordinate(10.0, 16.0, 0.0), 2.0); assert_eq!(random_teleport_coordinate(10.0, 16.0, 0.5), 10.0); assert_eq!(random_teleport_coordinate(10.0, 16.0, 1.0), 18.0); } } /// Returns `true` if `damage_type` is in `#minecraft:bypasses_armor` (1.21.11). /// These sources bypass armor entirely (fall, drown, freeze, etc.). pub(crate) const fn bypasses_armor_durability(damage_type: &DamageType) -> bool { // Bitmask lookup: O(1) with two instructions (shift + AND), no array scan. // DamageType IDs can exceed 31; use u64 for sufficient range. // TODO: Make data-driven once the data pack system can handle it without performance regressions. // Compile-time assertions: ensure all bypassing types fit in u64 bitmask. const _: () = assert!( DamageType::FALL.id < 64 && DamageType::FLY_INTO_WALL.id < 64 && DamageType::ON_FIRE.id < 64 && DamageType::IN_WALL.id < 64 && DamageType::CRAMMING.id < 64 && DamageType::DROWN.id < 64 && DamageType::GENERIC.id < 64 && DamageType::WITHER.id < 64 && DamageType::DRAGON_BREATH.id < 64 && DamageType::STARVE.id < 64 && DamageType::ENDER_PEARL.id < 64 && DamageType::FREEZE.id < 64 && DamageType::STALAGMITE.id < 64 && DamageType::MAGIC.id < 64 && DamageType::INDIRECT_MAGIC.id < 64 && DamageType::OUT_OF_WORLD.id < 64 && DamageType::GENERIC_KILL.id < 64 && DamageType::SONIC_BOOM.id < 64 && DamageType::OUTSIDE_BORDER.id < 64, "One or more bypass DamageType IDs exceed u64 bitmask width (>= 64)" ); const BYPASS_MASK: u64 = (1u64 << DamageType::FALL.id) | (1u64 << DamageType::FLY_INTO_WALL.id) | (1u64 << DamageType::ON_FIRE.id) | (1u64 << DamageType::IN_WALL.id) | (1u64 << DamageType::CRAMMING.id) | (1u64 << DamageType::DROWN.id) | (1u64 << DamageType::GENERIC.id) | (1u64 << DamageType::WITHER.id) | (1u64 << DamageType::DRAGON_BREATH.id) | (1u64 << DamageType::STARVE.id) | (1u64 << DamageType::ENDER_PEARL.id) | (1u64 << DamageType::FREEZE.id) | (1u64 << DamageType::STALAGMITE.id) | (1u64 << DamageType::MAGIC.id) | (1u64 << DamageType::INDIRECT_MAGIC.id) | (1u64 << DamageType::OUT_OF_WORLD.id) | (1u64 << DamageType::GENERIC_KILL.id) | (1u64 << DamageType::SONIC_BOOM.id) | (1u64 << DamageType::OUTSIDE_BORDER.id); (damage_type.id < 64) && ((BYPASS_MASK >> damage_type.id) & 1 == 1) } #[cfg(test)] mod tests { use super::*; // ── bypasses_armor_durability ───────────────────────────────────── /// Every member of `minecraft:bypasses_armor` (1.21.11) must return `true`. #[test] fn bypasses_armor_durability_returns_true_for_tag_members() { // Exact contents of the minecraft:bypasses_armor tag in 1.21.11. let bypassing: &[DamageType] = &[ DamageType::ON_FIRE, DamageType::IN_WALL, DamageType::CRAMMING, DamageType::DROWN, DamageType::FLY_INTO_WALL, DamageType::GENERIC, DamageType::WITHER, DamageType::DRAGON_BREATH, DamageType::STARVE, DamageType::FALL, DamageType::ENDER_PEARL, DamageType::FREEZE, DamageType::STALAGMITE, DamageType::MAGIC, DamageType::INDIRECT_MAGIC, DamageType::OUT_OF_WORLD, DamageType::GENERIC_KILL, DamageType::SONIC_BOOM, DamageType::OUTSIDE_BORDER, ]; for dt in bypassing { assert!( bypasses_armor_durability(dt), "{} should bypass armor durability", dt.message_id ); } } /// Physical/combat damage types must NOT bypass armor durability. #[test] fn bypasses_armor_durability_returns_false_for_physical_sources() { let physical: &[DamageType] = &[ DamageType::MOB_ATTACK, DamageType::PLAYER_ATTACK, DamageType::ARROW, DamageType::CACTUS, DamageType::SWEET_BERRY_BUSH, DamageType::LAVA, DamageType::EXPLOSION, DamageType::PLAYER_EXPLOSION, DamageType::LIGHTNING_BOLT, DamageType::FIREBALL, DamageType::THORNS, DamageType::TRIDENT, ]; for dt in physical { assert!( !bypasses_armor_durability(dt), "{} should NOT bypass armor durability", dt.message_id ); } } #[test] fn hurt_sound_for_entity_uses_zombie_family_sounds() { let cases = [ (&EntityType::ZOMBIE, Sound::EntityZombieHurt), (&EntityType::DROWNED, Sound::EntityDrownedHurt), (&EntityType::HUSK, Sound::EntityHuskHurt), ( &EntityType::ZOMBIE_VILLAGER, Sound::EntityZombieVillagerHurt, ), ]; for (entity_type, expected) in cases { assert_eq!(LivingEntity::hurt_sound_for_entity(entity_type), expected); } } #[test] fn hurt_sound_for_entity_uses_enderman_hurt_sound() { assert_eq!( LivingEntity::hurt_sound_for_entity(&EntityType::ENDERMAN), Sound::EntityEndermanHurt ); } #[test] fn hurt_sound_for_entity_uses_skeleton_family_sounds() { let cases = [ (&EntityType::SKELETON, Sound::EntitySkeletonHurt), (&EntityType::BOGGED, Sound::EntityBoggedHurt), (&EntityType::PARCHED, Sound::EntityParchedHurt), ( &EntityType::WITHER_SKELETON, Sound::EntityWitherSkeletonHurt, ), (&EntityType::STRAY, Sound::EntityStrayHurt), ]; for (entity_type, expected) in cases { assert_eq!(LivingEntity::hurt_sound_for_entity(entity_type), expected); } } #[test] fn hurt_sound_for_entity_defaults_to_generic_hurt() { assert_eq!( LivingEntity::hurt_sound_for_entity(&EntityType::CREEPER), Sound::EntityGenericHurt ); } #[test] fn regeneration_particle_metadata_uses_vanilla_argb_color() { let effect = Effect { effect_type: &StatusEffect::REGENERATION, duration: 200, amplifier: 0, ambient: false, show_particles: true, show_icon: true, blend: false, }; let metadata = Metadata::new( tracked_data::living_entity::EFFECT_PARTICLES, EffectParticles(vec![EffectParticle::from_effect(&effect)]), ); let mut bytes = Vec::new(); metadata .write( &mut bytes, &pumpkin_util::version::JavaMinecraftVersion::V_26_2, ) .unwrap(); assert_eq!(bytes, [10, 17, 1, 28, 0xff, 0xcd, 0x5c, 0xab]); } }