use pumpkin_data::item::Item; use pumpkin_data::meta_data_type::MetaDataType; use pumpkin_data::potion::Effect; use pumpkin_data::tag::{self, Taggable}; use pumpkin_data::tracked_data::{TrackedData, TrackedId}; 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::{ActorEventType, 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, NBTStorage, NBTStorageInit}; use crate::block::OnLandedUponArgs; 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::entity::{EntityBaseFuture, NbtFuture}; 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::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::{ CEntityStatus, CHurtAnimation, CSetPlayerInventory, CTakeItemEntity, CUpdateMobEffect, }; use pumpkin_protocol::{ codec::item_stack_seralizer::ItemStackSerializer, java::client::play::{CDamageEvent, CSetEquipment, Metadata}, }; use pumpkin_util::math::vector3::Vector3; use pumpkin_util::text::TextComponent; use rand::RngExt; use std::sync::RwLock; use tokio::sync::Mutex; use uuid::Uuid; /// 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: Mutex>, pub active_hand: 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: 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>, } impl LivingEntity { const USING_ITEM_FLAG: u8 = 1; const OFF_HAND_ACTIVE_FLAG: u8 = 2; #[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: Mutex::new(None), active_hand: Mutex::new(None), livings_flags: AtomicU8::new(0), active_effects: Mutex::new(HashMap::new()), entity_equipment: Arc::new(Mutex::new(EntityEquipment::new())), equipment_drop_chances: Arc::new(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: Vec<(i8, ItemStackSerializer)> = equipment .iter() .map(|(slot, stack)| { ( slot.discriminant(), ItemStackSerializer::from(stack.clone()), ) }) .collect(); self.entity.world.load().broadcast_packet_except( &[self.entity.entity_uuid], &CSetEquipment::new(self.entity_id().into(), equipment), ); } /// Picks up and Item entity or XP Orb pub fn pickup(&self, item: &Entity, stack_amount: u32) { let chunk_pos = self.entity.chunk_pos.load(); self.entity.world.load().broadcast_to_chunk_editioned_sync( chunk_pos, &CTakeItemEntity::new( item.entity_id.into(), self.entity.entity_id.into(), VarInt(stack_amount as i32), ), &CTakeItemActor::new( VarULong(item.entity_id as u64), VarULong(self.entity.entity_id as u64), ), ); } /// Sends the Hand animation to all others, used when Eating for example pub async fn set_active_hand(&self, hand: Hand, stack: ItemStack, duration: i32) { self.item_use_time.store(duration, Ordering::Relaxed); *self.item_in_use.lock().await = Some(stack); *self.active_hand.lock().await = 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 mut meta = pumpkin_protocol::bedrock::client::set_actor_data::EntityMetadata::new(); meta.set_flag( pumpkin_protocol::bedrock::client::set_actor_data::entity_data_key::FLAGS, pumpkin_protocol::bedrock::client::set_actor_data::entity_data_flag::USING_ITEM as u8, value, ); meta }); self.entity.send_meta_data( &[Metadata::new( TrackedData::LIVING_ENTITY_FLAGS, MetaDataType::BYTE, b, )], bedrock_meta.as_ref(), ); } pub async fn clear_active_hand(&self) { *self.item_in_use.lock().await = None; *self.active_hand.lock().await = None; self.item_use_time.store(0, Ordering::Relaxed); self.set_living_flag(Self::USING_ITEM_FLAG, false); } pub async fn is_blocking(&self) -> bool { let item_in_use = self.item_in_use.lock().await; 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); 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( TrackedData::HEALTH_ID, MetaDataType::FLOAT, 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 async 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], ) .await; // 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 async 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], ) .await; // Send absorption metadata for players (visual yellow hearts) if let Some(tracked_id) = self.player_absorption_id() { self.entity.send_meta_data( &[Metadata::new(tracked_id, MetaDataType::FLOAT, new_abs)], None, ); } } /// Returns the absorption ID for this (player) entity /// TODO: don't hardcode these here? fn player_absorption_id(&self) -> Option { (self.entity.entity_type == &EntityType::PLAYER).then_some(TrackedId { v1_21: 17u8, v1_21_2: 17u8, v1_21_4: 17u8, v1_21_5: 17u8, v1_21_6: 17u8, v1_21_7: 17u8, v1_21_9: 17u8, v1_21_11: 17u8, v26_1: 17u8, // ? v26_2: 17u8, }) } /// 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 async fn reset_effects_and_attributes(&self) { // Clear active effects and reset modified attributes let effects_to_remove: Vec<_> = { let lock = self.active_effects.lock().await; lock.keys().copied().collect() }; for effect_type in effects_to_remove { self.remove_effect(effect_type).await; } } pub const fn entity_id(&self) -> i32 { self.entity.entity_id } pub async fn add_effect(&self, effect: Effect) { // 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; if let Some(dyn_self) = self .entity .world .load() .get_entity_by_id(self.entity.entity_id) { dyn_self .damage(&*dyn_self, damage_amount, DamageType::MAGIC) .await; } } else { // Apply non-instant effects self.active_effects .lock() .await .insert(effect.effect_type, effect.clone()); // 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, ) .await; } } // 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).await; } // Apply invisible effect if effect.effect_type == &StatusEffect::INVISIBILITY { self.entity.set_invisible(true).await; } // Apply glowing effect if effect.effect_type == &StatusEffect::GLOWING { self.entity.set_glowing(true).await; } } // 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 packet = CUpdateMobEffect::new( self.entity.entity_id.into(), VarInt(i32::from(effect.effect_type.id)), effect.amplifier.into(), effect.duration.into(), flag, ); self.entity.world.load().broadcast_packet_all(&packet); } pub async fn remove_effect(&self, effect_type: &'static StatusEffect) -> bool { // Remove the effect let succeeded = self .active_effects .lock() .await .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) .await; } } // If absorption effect removed, clear current absorption amount and notify clients if effect_type == &StatusEffect::ABSORPTION { self.set_absorption(0.0).await; } // 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).await; } // If glowing effect removed, disable glowing if effect_type == &StatusEffect::GLOWING { self.entity.set_glowing(false).await; } succeeded } pub async fn has_effect(&self, effect: &'static StatusEffect) -> bool { let effects = self.active_effects.lock().await; effects.contains_key(&effect) } pub async fn get_effect(&self, effect: &'static StatusEffect) -> Option { let effects = self.active_effects.lock().await; effects.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 { let block_pos = self.entity.block_pos.load(); self.entity.world.load().get_block(&block_pos) == &Block::WATER } // Check if the entity is in powder snow pub fn is_in_powder_snow(&self) -> bool { let block_pos = self.entity.block_pos.load(); self.entity.world.load().get_block(&block_pos) == &Block::POWDER_SNOW } 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) } async fn get_effective_gravity(&self, caller: &Arc) -> f64 { let final_gravity = caller.get_gravity(); if self.entity.velocity.load().y <= 0.0 && self.has_effect(&StatusEffect::SLOW_FALLING).await { final_gravity.min(0.01) } else { final_gravity } } pub async 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, runtime_entity_id: pumpkin_protocol::codec::var_ulong::VarULong(entity_id as u64), data: 0.0, swing_source: None, }; world.broadcast_editioned(&je_packet, &be_packet).await; } async fn tick_movement<'a>(&'a self, server: &'a Server, caller: &'a Arc) { if self.jumping_cooldown.load(Relaxed) != 0 { self.jumping_cooldown.fetch_sub(1, Relaxed); } let should_swim_in_fluids = if let Some(player) = caller.get_player() { !player.is_flying().await } else { true }; 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().await; self.jumping_cooldown.store(10, SeqCst); } } else { self.jumping_cooldown.store(0, SeqCst); } if self.has_effect(&StatusEffect::SLOW_FALLING).await || self.has_effect(&StatusEffect::LEVITATION).await { 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).await; } else { // TODO: Gliding self.travel_in_air(caller).await; } // TODO: Apply Soul Speed boot durability when tick_block_underneath is implemented. //self.entity.tick_block_underneath(&caller); let suffocating = self.entity.tick_block_collisions(caller, server).await; if suffocating { self.damage(&**caller, 1.0, DamageType::IN_WALL).await; } } async fn travel_in_air<'a>(&'a self, caller: &'a Arc) { // applyMovementInput let effective_speed = self.get_attribute_value(&Attributes::MOVEMENT_SPEED); let (speed, friction) = if self.entity.on_ground.load(SeqCst) { // 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 = if let Some(player) = caller.get_player() { player.get_off_ground_speed().await } else { // TODO: If the passenger is a player, ogs = movement_speed * 0.1 0.02 }; (speed, 0.91) }; self.entity .update_velocity_from_input(self.movement_input.load(), speed); self.apply_climbing_speed(); self.make_move(caller).await; 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.as_ref()).await } 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).await; 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).await; // 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); } async fn travel_in_fluid<'a>(&'a self, caller: &'a Arc, 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).await; 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).await { friction = 0.96; } self.entity .update_velocity_from_input(movement_input, speed); self.make_move(caller).await; 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).await; 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; } } } async fn make_move<'a>(&'a self, caller: &'a Arc) { self.entity .move_entity(caller, self.entity.velocity.load()) .await; 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 } } async fn jump(&self) { let jump = self.get_jump_velocity(1.0).await; 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); } async 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).await { strength += 0.1 * f64::from(effect.amplifier + 1); } strength } pub async fn fall( &self, caller: Arc, 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.as_ref(), }) .await; } else { self.handle_fall_damage(&*caller, fall_distance, 1.0).await; } } 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 async fn handle_fall_damage( &self, caller: &dyn EntityBase, fall_distance: f32, damage_per_distance: f32, ) { if 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).await; // 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 async 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().await, cause.get_display_name().await, ], ) } else { TextComponent::translate_cross( format!("death.attack.{}", damage_type.message_id), format!("death.attack.{}", damage_type.message_id), [dyn_self.get_display_name().await], ) } } DeathMessageType::FallVariants => { //TODO TextComponent::translate_cross( translation::java::DEATH_FELL_ACCIDENT_GENERIC, translation::bedrock::DEATH_FELL_ACCIDENT_GENERIC, [dyn_self.get_display_name().await], ) } 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().await], )) .add_child(TextComponent::text("]")), } } pub async 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).await; // Plays the death sound world.send_entity_status( &self.entity, EntityStatus::Death, Some(ActorEventType::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) .await; 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().await; let is_thundering = world.is_thundering().await; 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()).await; // 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).await; } } self.entity.pose.store(EntityPose::Dying); self.drop_equipment(looting_level).await; // Broadcast death message if it's a player and the gamerule is enabled self.broadcast_death_message(&*dyn_self, damage_type, source, cause) .await; self.reset_effects_and_attributes().await; } } async 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().await; 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() .await .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).await; } } async 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).await; if let Some(server) = world.server.upgrade() { for player in server.get_all_players() { player.send_system_message(&death_message).await; } } } } async 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) .await; victim_player .set_stat( StatisticCategory::Custom, CustomStatistic::TimeSinceDeath as i32, 0, ) .await; if let Some(killer_entity) = cause.map(EntityBase::get_entity) { victim_player .increment_stat( StatisticCategory::KilledBy, killer_entity.entity_type.id as i32, 1, ) .await; } } 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, ) .await; } else { killer_player .increment_stat( StatisticCategory::Custom, CustomStatistic::MobKills as i32, 1, ) .await; 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, } ) .await; 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).await; } } if resource_name == "phantom" { killer_player.trigger_advancement(crate::entity::player::advancement::trigger::AdvancementTrigger::TwoBirdsOneArrow).await; } let held_item = killer_player.inventory().held_item().await; let is_crossbow = held_item.item.registry_key == "crossbow"; if is_crossbow { killer_player.trigger_advancement(crate::entity::player::advancement::trigger::AdvancementTrigger::Arbalistic).await; } } killer_player .increment_stat( StatisticCategory::Killed, self.entity.entity_type.id as i32, 1, ) .await; } } async 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).await; } } } async fn tick_effects(&self) { let mut effects_to_remove = Vec::new(); let mut effects_to_apply = Vec::new(); { let mut effects = self.active_effects.lock().await; 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 Self::should_apply_effect_tick(effect, tick_duration) { effects_to_apply.push((effect.effect_type, effect.amplifier)); } if effect.duration != -1 { effect.duration -= 1; } } } // Call the central removal function for each expired effect // This will now trigger your logs and absorption resets! for effect_type in effects_to_remove { self.remove_effect(effect_type).await; } for (effect_type, amplifier) in effects_to_apply { self.apply_effect_tick(effect_type, amplifier).await; } } /// Determines if an effect should apply its tick effect this frame /// Based on vanilla Minecraft's effect tick frequencies /// /// TODO: villager, beacon, and other effects. fn should_apply_effect_tick(effect: &pumpkin_data::potion::Effect, duration: i32) -> bool { let effect_type = effect.effect_type; if effect_type == &StatusEffect::REGENERATION { if duration <= 0 { return false; } let tick_rate = 50 >> effect.amplifier.min(4); duration % tick_rate == 0 } else if effect_type == &StatusEffect::POISON { if duration <= 0 { return false; } let tick_rate = 25 >> effect.amplifier.min(4); duration % tick_rate == 0 } else if effect_type == &StatusEffect::WITHER { if duration <= 0 { return false; } let tick_rate = 40 >> effect.amplifier.min(4); duration % tick_rate == 0 } else if effect_type == &StatusEffect::HUNGER { // Hunger every 20 ticks duration % 20 == 0 } else if effect_type == &StatusEffect::SATURATION { // Saturation every tick true } else { // Other effects that don't tick false } } /// Applies the actual effect to the entity /// This is called by `tick_effects` when an effect should trigger this tick async fn apply_effect_tick(&self, effect_type: &'static StatusEffect, amplifier: u8) { if effect_type == &StatusEffect::REGENERATION { let current_health = self.health.load(); let max_health = self.get_max_health(); if current_health < max_health && current_health > 0.0 { self.heal(1.0); } } else if effect_type == &StatusEffect::POISON { let current_health = self.health.load(); if current_health > 1.0 && let Some(dyn_self) = self .entity .world .load() .get_entity_by_id(self.entity.entity_id) { let damage_amount = (current_health - 1.0).min(1.0); if damage_amount > 0.0 { dyn_self .damage(&*dyn_self, damage_amount, DamageType::MAGIC) .await; } } } else if effect_type == &StatusEffect::WITHER { let damage_amount = 1.0; if let Some(dyn_self) = self .entity .world .load() .get_entity_by_id(self.entity.entity_id) { dyn_self .damage(&*dyn_self, damage_amount, DamageType::WITHER) .await; } } else if effect_type == &StatusEffect::HUNGER { let world = self.entity.world.load(); if let Some(entity) = world.get_entity_by_id(self.entity.entity_id) && let Some(player) = entity.get_player() { // Add exhaustion to trigger hunger decrease let exhaustion = 0.1 * (amplifier as f32 + 1.0); player.hunger_manager.add_exhaustion(exhaustion); } drop(world); } else if effect_type == &StatusEffect::SATURATION { let world = self.entity.world.load(); if let Some(entity) = world.get_entity_by_id(self.entity.entity_id) && let Some(player) = entity.get_player() { // Add hunger and saturation let hunger = amplifier + 1; player.hunger_manager.add_hunger(hunger); player.hunger_manager.add_saturation(hunger as f32 * 2.0); } } } /// Tries to use a totem of undying from the entity's hands. If successful, applies the totem effects and returns true. 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).await; // Clear the stack and use the totem of undying if stack.get_data_component::().is_some() { 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() .await .equipment .insert(slot, stack); } else { self.entity_equipment .lock() .await .equipment .insert(slot, stack); } self.set_health(1.0); self.entity.world.load().send_entity_status( &self.entity, EntityStatus::ProtectedFromDeath, Some(ActorEventType::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, }) .await; self.add_effect(Effect { effect_type: &StatusEffect::REGENERATION, duration: 900, amplifier: 1, ambient: false, show_particles: true, show_icon: true, blend: false, }) .await; self.add_effect(Effect { effect_type: &StatusEffect::FIRE_RESISTANCE, duration: 800, amplifier: 0, ambient: false, show_particles: true, show_icon: true, blend: false, }) .await; return true; } } false } async 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().await; 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), )) .await; } } } } if !equipment_updates.is_empty() { self.send_equipment_changes(&equipment_updates); } } pub async fn held_item(&self, caller: &dyn EntityBase) -> ItemStack { if let Some(player) = caller.get_player() { return player.inventory.held_item().await; } let equipment = self.entity_equipment.lock().await; equipment .equipment .get(&EquipmentSlot::MAIN_HAND) .cloned() .unwrap_or_else(|| ItemStack::EMPTY.clone()) } pub async fn get_stack_in_hand(&self, caller: &dyn EntityBase, hand: Hand) -> ItemStack { match hand { Hand::Left => self.off_hand_item(caller).await, Hand::Right => self.held_item(caller).await, } } /// getOffHandStack in source pub async fn off_hand_item(&self, caller: &dyn EntityBase) -> ItemStack { if let Some(player) = caller.get_player() { return player.inventory.off_hand_item().await; } let Some(slot) = self.equipment_slots.get(&PlayerInventory::OFF_HAND_SLOT) else { return ItemStack::EMPTY.clone(); }; let equipment = self.entity_equipment.lock().await; 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 async fn reset_state(&self) { self.entity.reset_state().await; // 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( TrackedData::HEALTH_ID, MetaDataType::FLOAT, max_health, )], None, ); self.reset_effects_and_attributes().await; // 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().await = 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); } /// Try to spawn silverfish when this entity is infested and hurt. async fn try_spawn_infested_silverfish(&self) { if !self.has_effect(&StatusEffect::INFESTED).await { return; } // Wither, ender dragon and silverfish are immune if self.entity.entity_type == &EntityType::WITHER || self.entity.entity_type == &EntityType::ENDER_DRAGON || self.entity.entity_type == &EntityType::SILVERFISH { return; } let world = self.entity.world.load(); // 10% chance if rand::rng().random::() <= 0.1 { let count = rand::rng().random_range(1..3); for _ in 0..count { // Spawn at center of entity let bbox = self.entity.bounding_box.load(); let center = Vector3::new( f64::midpoint(bbox.min.x, bbox.max.x), f64::midpoint(bbox.min.y, bbox.max.y), f64::midpoint(bbox.min.z, bbox.max.z), ); // Random direction let yaw_rad = self.entity.yaw.load().to_radians() as f64; let random_angle = rand::rng().random::() * std::f64::consts::PI - std::f64::consts::FRAC_PI_2; let angle = yaw_rad + random_angle; let speed = 0.3f64; let dx = -angle.sin() * speed; let dz = angle.cos() * speed; let dy = 0.1f64; // Spawn let silver = crate::entity::r#type::from_type( &EntityType::SILVERFISH, center, &world, Uuid::new_v4(), ); silver.get_entity().set_pos(center); silver.get_entity().velocity.store(Vector3::new(dx, dy, dz)); world.spawn_entity(silver).await; // Play sound world.play_sound(Sound::EntitySilverfishHurt, SoundCategory::Players, ¢er); } } } 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 NBTStorage for LivingEntity { fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> { Box::pin(async move { self.entity.write_nbt(nbt).await; 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("fall_distance", NbtTag::Float(fall_distance)); { let effects = self.active_effects.lock().await; if !effects.is_empty() { // Iterate effects and create Box<[NbtTag]> let mut effects_list = Vec::with_capacity(effects.len()); for effect in effects.values() { let mut effect_nbt = pumpkin_nbt::compound::NbtCompound::new(); effect.write_nbt(&mut effect_nbt).await; effects_list.push(NbtTag::Compound(effect_nbt)); } nbt.put("active_effects", NbtTag::List(effects_list)); } } //TODO: write equipment // todo more... }) } fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> { Box::pin(async { self.entity.read_nbt_non_mut(nbt).await; self.health.store(nbt.get_float("Health").unwrap_or(0.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("fall_distance").unwrap_or(0.0); if self.dead.load(Relaxed) { self.fall_distance.store(0.0); } else { self.fall_distance.store(fd); } { let mut active_effects = self.active_effects.lock().await; let nbt_effects = nbt.get_list("active_effects"); if let Some(nbt_effects) = nbt_effects { 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()).await { effect.blend = true; // TODO: change, is taken from effect give command active_effects.insert(effect.effect_type, effect); } else { warn!("Unable to read effect from nbt"); } } } } } }) // todo more... } } impl EntityBase for LivingEntity { #[allow(clippy::too_many_lines)] fn damage_with_context<'a>( &'a self, caller: &'a dyn EntityBase, amount: f32, damage_type: DamageType, position: Option>, source: Option<&'a dyn EntityBase>, cause: Option<&'a dyn EntityBase>, ) -> EntityBaseFuture<'a, bool> { Box::pin(async move { let mut amount = amount; // Check invulnerability before applying damage if self.entity.is_invulnerable_to(&damage_type).await { return false; } if self.health.load() <= 0.0 || self.dead.load(Relaxed) { return false; // Dying or dead } if amount < 0.0 { return false; } 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).await { 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().await; 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().await; 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), excluding bypasses_cooldown_protection and starvation damage let resistance_reduction = if !bypasses_cooldown_protection && damage_type != DamageType::STARVE { self.get_effect(&StatusEffect::RESISTANCE) .await .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, ) .await; } 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, ) .await; } } // Check for shield blocking if self.is_blocking().await && !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, ) .await; player.trigger_advancement(crate::entity::player::advancement::trigger::AdvancementTrigger::DeflectedDamage).await; } if let Some(attacker_player) = cause.and_then(|c| c.get_player()) { let held_item = attacker_player.inventory().held_item().await; let is_axe = held_item.is_axe(); if is_axe { let mut disable_chance = 0.25; let is_sprinting = attacker_player .living_entity .entity .sprinting .load(Ordering::Relaxed); if is_sprinting { disable_chance = 1.0; } if rand::random::() < disable_chance && let Some(victim_player) = caller.get_player() { victim_player .start_cooldown("minecraft:shield".to_string(), 100) .await; self.clear_active_hand().await; world.broadcast_packet_all(&CEntityStatus::new( self.entity.entity_id, 30, )); } } } let active_hand = self.active_hand.lock().await; 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().await; 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, ) .await; } 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().await; } } } 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 { entity_runtime_id: VarULong(entity_id as u64), event_type: ActorEventType::Hurt, event_data: VarInt(0), fire_at_position: None, }; world .broadcast_editioned( &CHurtAnimation::new(VarInt(entity_id), hurt_yaw), &hurt_event, ) .await; } 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, )); // Try to spawn infested silverfish self.try_spawn_infested_silverfish().await; 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, ) .await; } 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, ) .await; } if current_abs >= remaining { let new_abs = current_abs - remaining; self.set_absorption(new_abs).await; remaining = 0.0; } else { remaining -= current_abs; self.set_absorption(0.0).await; } // Track attacker for RevengeGoal (only after confirming damage) 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); } } // Apply remaining damage to health (clamped) 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); // Statistics updates if let Some(player) = caller.get_player() { player .increment_stat( StatisticCategory::Custom, CustomStatistic::DamageTaken as i32, (remaining * 10.0) as i32, ) .await; } 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, ) .await; } // Track attacker for RevengeGoal (only after confirming damage) 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); } } // Check if the entity died and isn't protected by a death protection mechanic (ex. totem of undying) if clamped_health <= 0.0 && (bypasses_cooldown_protection || !self.try_use_death_protector(caller).await) { self.on_death(damage_type, source, cause).await; } // Armor durability is based on incoming raw damage, not post-absorption remaining. // Armor loses floor(raw_damage / 4) durability, minimum 1. // Not applied when the source is in `#minecraft:bypasses_armor`. if damage_amount > 0.0 && !bypasses_armor_durability(&damage_type) { self.damage_armor_items(caller, damage_amount).await; } true }) } fn tick_in_void<'a>(&'a self, dyn_self: &'a dyn EntityBase) -> EntityBaseFuture<'a, ()> { Box::pin(async move { dyn_self .damage(dyn_self, 4.0, DamageType::OUT_OF_WORLD) .await; }) } fn get_gravity(&self) -> f64 { self.get_attribute_value(&Attributes::GRAVITY) } #[allow(clippy::too_many_lines)] fn tick<'a>( &'a self, caller: &'a Arc, server: &'a Server, ) -> EntityBaseFuture<'a, ()> { Box::pin(async move { self.entity.tick(caller, server).await; // 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(server, caller).await; // Vanilla-like order: freeze logic runs after movement/collisions. self.entity.tick_frozen(caller.as_ref()).await; } // 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 let Some(supporting) = supporting_pos { let world = self.entity.world.load(); let (block, state) = world.get_block_and_state(&supporting); world .block_registry .on_entity_step( block, &world, caller.as_ref() as &dyn EntityBase, &supporting, state, false, ) .await; // 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.as_ref() as &dyn EntityBase, &below_supporting, below_state, true, // below supporting block ) .await; } } self.tick_effects().await; // Current active item { let item_in_use = self.item_in_use.lock().await.clone(); if let Some(item) = item_in_use.as_ref() && self.item_use_time.fetch_sub(1, Ordering::Relaxed) <= 0 { // 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) .await; } self.apply_consumable_effects(item).await; // 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).await; 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), }) .await; // 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().await; 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).await; handled = true; } if !handled { // Check off-hand let mut off_hand = player.inventory.off_hand_item().await; 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) .await; handled = true; } } if !handled { // Use stored active_hand (as a fallback) let active_hand = *self.active_hand.lock().await; let hand_to_modify = active_hand.unwrap_or(Hand::Right); let mut item_stack = self .get_stack_in_hand(caller.as_ref(), hand_to_modify) .await; 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) .await; } 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) .await; } } self.clear_active_hand().await; } } 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_add(1, Relaxed); // 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(ActorEventType::Death), ); self.entity.remove().await; } } }) } 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 } fn as_nbt_storage(&self) -> &dyn NBTStorage { self } } impl LivingEntity { /// Applies data-driven `apply_effects` consume effects after an item completes use. /// Vanilla: `Consumable.onConsume` invokes every configured effect server-side. async fn apply_consumable_effects(&self, 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, }) .await; } } ConsumeEffect::ClearAllEffects => { self.reset_effects_and_attributes().await; } 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).await; } } } ConsumeEffect::TeleportRandomly(diameter) => { let center = self.entity.pos.load(); let radius = f64::from(*diameter) / 2.0; let target_x = center.x + (rand::random::() - 0.5) * radius * 2.0; let target_y = center.y + (rand::random::() - 0.5) * radius; let target_z = center.z + (rand::random::() - 0.5) * radius * 2.0; let pos = pumpkin_util::math::vector3::Vector3::new(target_x, target_y, target_z); let (yaw, pitch) = (self.entity.yaw.load(), self.entity.pitch.load()); let world = self.entity.world.load().clone(); self.entity.teleport(pos, Some(yaw), Some(pitch), world); } ConsumeEffect::PlaySound(_) => {} } } } } /// 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; #[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)); } } /// 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 ); } }