Pumpkin/crates/pumpkin/src/entity/living.rs
Alexander Medvedev 6eb02f65b9 chore: more fixes
2026-08-28 13:31:28 +02:00

3229 lines
118 KiB
Rust

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<f32>,
/// The current health level of the entity.
pub health: AtomicCell<f32>,
/// The current absorption (yellow hearts) on the entity.
pub absorption: AtomicCell<f32>,
pub item_use_time: AtomicI32,
pub item_in_use: std::sync::Mutex<Option<ItemStack>>,
pub active_hand: std::sync::Mutex<Option<Hand>>,
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<f32>,
pub active_effects: std::sync::Mutex<HashMap<&'static StatusEffect, Effect>>,
pub entity_equipment: Arc<std::sync::Mutex<EntityEquipment>>,
pub equipment_drop_chances: Arc<std::sync::Mutex<HashMap<EquipmentSlot, f32>>>,
pub movement_input: AtomicCell<Vector3<f64>>,
pub equipment_slots: Arc<HashMap<usize, EquipmentSlot>>,
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<Option<BlockPos>>,
/// 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<HashMap<u8, AttributeInstance>>,
}
struct EffectParticle {
particle_id: VarInt,
color: i32,
}
struct EffectParticles(Vec<EffectParticle>);
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::<BlocksAttacksImpl>().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::<crate::entity::player::Player>()
&& 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<F: FnOnce(&mut AttributeInstance)>(
&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<pumpkin_data::attributes::Attributes> = 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<Effect> {
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::<crate::entity::player::Player>()
{
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<EquipmentSlot> = {
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::<f32>() >= 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::<DeathProtectionImpl>().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::<EquippableImpl>()
.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<f64> {
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<pumpkin_data::potion::Effect> = {
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<Vector3<f64>>,
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::<AttributeModifiersImpl>()
{
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::<EnchantmentsImpl>()
{
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<Vector3<f64>>,
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::<FoodImpl>()
&& 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::<pumpkin_data::data_component_impl::PotionContentsImpl>()
.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::<ConsumableImpl>() 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<Vector3<f64>> {
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]);
}
}