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<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: Mutex<Option<ItemStack>>,
    pub active_hand: 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: Mutex<HashMap<&'static StatusEffect, Effect>>,
    pub entity_equipment: Arc<Mutex<EntityEquipment>>,
    pub equipment_drop_chances: Arc<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>>,
}

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_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::new(
                    self.entity_id() as u64,
                    pumpkin_protocol::bedrock::network_item::NetworkItemStackDescriptor::from(
                        stack,
                    ),
                    0,
                    0,
                    window_id,
                );
                self.entity
                    .world
                    .load()
                    .broadcast_packet_except_editioned_sync(
                        &[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 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::<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);
        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<TrackedId> {
        (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<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 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;
            let dyn_self = self
                .entity
                .world
                .load()
                .get_entity_by_id(self.entity.entity_id);
            if let Some(dyn_self) = dyn_self {
                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<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,
                    )
                    .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<Effect> {
        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<dyn EntityBase>) -> 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<dyn EntityBase>) {
        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<dyn EntityBase>) {
        // 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<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).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<dyn EntityBase>) {
        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<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.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::<crate::entity::player::Player>()
                {
                    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<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()
                .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;
            let dyn_self = self
                .entity
                .world
                .load()
                .get_entity_by_id(self.entity.entity_id);
            if let Some(dyn_self) = dyn_self {
                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::<DeathProtectionImpl>().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::<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),
                            ))
                            .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::<f32>() <= 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::<f64>() * 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, &center);
            }
        }
    }

    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 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<Vector3<f64>>,
        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::<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().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::<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), 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::<f32>() < 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<dyn EntityBase>,
        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::<FoodImpl>()
                        && 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::<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).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::<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,
                        })
                        .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::<f64>() - 0.5) * radius * 2.0;
                    let target_y = center.y + (rand::random::<f64>() - 0.5) * radius;
                    let target_z = center.z + (rand::random::<f64>() - 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
        );
    }
}
