Skip to main content

steel_core/entity/living_base/
mod.rs

1//! Shared fields for all living entities.
2//!
3//! Mirrors the runtime fields that vanilla defines on `LivingEntity` (and
4//! `Entity` for `invulnerableTime`). Entities that implement `LivingEntity`
5//! embed this struct and expose it via `LivingEntity::living_base()`, just like
6//! `EntityBase` is used for core `Entity` fields.
7
8use std::{array, mem, sync::Arc};
9
10use glam::DVec3;
11use rustc_hash::FxHashMap;
12use simdnbt::owned::{NbtCompound, NbtTag};
13use steel_protocol::packets::game::{CRemoveMobEffect, CUpdateMobEffect, MobEffectPacketFlags};
14use steel_registry::RegistryEntry;
15use steel_registry::attribute::AttributeRef;
16use steel_registry::entity_data::ParticleList;
17use steel_registry::entity_type::EntityTypeRef;
18use steel_registry::item_stack::ItemStack;
19use steel_registry::mob_effect::MobEffectRef;
20use steel_registry::vanilla_attributes;
21use steel_registry::vanilla_entity_data::VanillaLivingEntityData;
22use steel_registry::{vanilla_damage_types, vanilla_mob_effects};
23use steel_utils::locks::{IntoShared, Shared, SyncMutex};
24use steel_utils::types::InteractionHand;
25use steel_utils::{BlockPos, Identifier};
26use uuid::Uuid;
27
28use crate::entity::attribute::{AttributeMap, AttributeModifier, AttributeModifierOperation};
29use crate::entity::damage::DamageSource;
30use crate::entity::{LivingEntity, SharedEntity, WeakEntity};
31use crate::inventory::equipment::{EntityEquipment, EquipmentSlot, OwnedEntityEquipment};
32use crate::world::World;
33
34/// Duration in ticks of the death animation before entity removal.
35pub const DEATH_DURATION: i32 = 20;
36/// Vanilla default `SwingAnimation` duration in ticks.
37pub const DEFAULT_SWING_DURATION: i32 = 6;
38const INFINITE_EFFECT_DURATION: i32 = -1;
39const MIN_EFFECT_AMPLIFIER: i32 = 0;
40const MAX_EFFECT_AMPLIFIER: i32 = 255;
41const SPRINT_SPEED_MODIFIER_AMOUNT: f64 = 0.3;
42const POST_IMPULSE_GRACE_TICKS: i32 = 40;
43
44/// Runtime mob-effect state.
45///
46/// Mirrors vanilla `MobEffectInstance` state that affects server-side living
47/// physics and client synchronization.
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct MobEffectInstance {
50    effect: MobEffectRef,
51    duration: i32,
52    amplifier: i32,
53    ambient: bool,
54    visible: bool,
55    show_icon: bool,
56    hidden_effect: Option<Box<MobEffectInstance>>,
57}
58
59/// Active mob-effect state stored on a living entity.
60pub type ActiveMobEffect = MobEffectInstance;
61
62impl MobEffectInstance {
63    /// Creates infinite active mob-effect state for internal physics tests and hooks.
64    #[must_use]
65    pub const fn new(effect: MobEffectRef, amplifier: i32) -> Self {
66        Self::with_duration(effect, INFINITE_EFFECT_DURATION, amplifier)
67    }
68
69    /// Creates active mob-effect state with vanilla default visibility flags.
70    #[must_use]
71    pub const fn with_duration(effect: MobEffectRef, duration: i32, amplifier: i32) -> Self {
72        Self {
73            effect,
74            duration,
75            amplifier: clamp_effect_amplifier(amplifier),
76            ambient: false,
77            visible: true,
78            show_icon: true,
79            hidden_effect: None,
80        }
81    }
82
83    /// Sets whether this effect is ambient.
84    #[must_use]
85    pub const fn with_ambient(mut self, ambient: bool) -> Self {
86        self.ambient = ambient;
87        self
88    }
89
90    /// Sets whether this effect should show particles.
91    #[must_use]
92    pub const fn with_visible(mut self, visible: bool) -> Self {
93        self.visible = visible;
94        self
95    }
96
97    /// Sets whether this effect should show its inventory icon.
98    #[must_use]
99    pub const fn with_show_icon(mut self, show_icon: bool) -> Self {
100        self.show_icon = show_icon;
101        self
102    }
103
104    /// Returns the mob effect.
105    #[must_use]
106    pub const fn effect(&self) -> MobEffectRef {
107        self.effect
108    }
109
110    /// Returns vanilla `MobEffectInstance.getDuration()`.
111    #[must_use]
112    pub const fn duration(&self) -> i32 {
113        self.duration
114    }
115
116    /// Returns vanilla `MobEffectInstance.getAmplifier()`.
117    #[must_use]
118    pub const fn amplifier(&self) -> i32 {
119        self.amplifier
120    }
121
122    /// Returns vanilla `MobEffectInstance.isAmbient()`.
123    #[must_use]
124    pub const fn is_ambient(&self) -> bool {
125        self.ambient
126    }
127
128    /// Returns vanilla `MobEffectInstance.isVisible()`.
129    #[must_use]
130    pub const fn is_visible(&self) -> bool {
131        self.visible
132    }
133
134    /// Returns vanilla `MobEffectInstance.showIcon()`.
135    #[must_use]
136    pub const fn show_icon(&self) -> bool {
137        self.show_icon
138    }
139
140    /// Returns whether this effect uses vanilla's infinite-duration sentinel.
141    #[must_use]
142    pub const fn is_infinite_duration(&self) -> bool {
143        self.duration == INFINITE_EFFECT_DURATION
144    }
145
146    /// Serializes this effect with vanilla's `MobEffectInstance.CODEC` shape.
147    #[must_use]
148    pub(crate) fn to_vanilla_nbt(&self) -> NbtCompound {
149        let mut nbt = self.details_to_vanilla_nbt();
150        nbt.insert("id", self.effect.key.to_string());
151        nbt
152    }
153
154    fn details_to_vanilla_nbt(&self) -> NbtCompound {
155        let mut nbt = NbtCompound::new();
156        if self.amplifier != 0 {
157            nbt.insert("amplifier", NbtTag::Byte(self.amplifier as i8));
158        }
159        if self.duration != 0 {
160            nbt.insert("duration", self.duration);
161        }
162        if self.ambient {
163            nbt.insert("ambient", NbtTag::Byte(1));
164        }
165        if !self.visible {
166            nbt.insert("show_particles", NbtTag::Byte(0));
167        }
168        nbt.insert("show_icon", NbtTag::Byte(i8::from(self.show_icon)));
169        if let Some(hidden_effect) = self.hidden_effect.as_deref() {
170            nbt.insert(
171                "hidden_effect",
172                NbtTag::Compound(hidden_effect.details_to_vanilla_nbt()),
173            );
174        }
175        nbt
176    }
177
178    #[must_use]
179    pub(crate) const fn has_remaining_duration(&self) -> bool {
180        self.is_infinite_duration() || self.duration > 0
181    }
182
183    #[must_use]
184    pub(crate) fn should_apply_effect_tick_this_tick(&self, entity_tick_count: i32) -> bool {
185        let tick_count = if self.is_infinite_duration() {
186            entity_tick_count
187        } else {
188            self.duration
189        };
190
191        if self.effect == vanilla_mob_effects::WITHER {
192            let interval = 40_i32.wrapping_shr(self.amplifier as u32);
193            return interval <= 0 || tick_count % interval == 0;
194        }
195
196        // TODO: Add the remaining vanilla effect schedules as their gameplay systems land.
197        false
198    }
199
200    pub(crate) fn apply_effect_tick<E: LivingEntity + ?Sized>(
201        &self,
202        world: &World,
203        entity: &E,
204    ) -> bool {
205        if self.effect == vanilla_mob_effects::WITHER {
206            entity.hurt(
207                world,
208                &DamageSource::environment(&vanilla_damage_types::WITHER),
209                1.0,
210            );
211        }
212
213        // Vanilla effect ticks return whether the effect remains active. Wither
214        // always returns true, and unimplemented effect ticks are not scheduled.
215        true
216    }
217
218    #[must_use]
219    const fn is_shorter_duration_than(&self, other: &Self) -> bool {
220        !self.is_infinite_duration()
221            && (self.duration < other.duration || other.is_infinite_duration())
222    }
223
224    /// Merges another instance of the same effect into this instance.
225    ///
226    /// Mirrors vanilla `MobEffectInstance.update`.
227    pub fn update(&mut self, take_over: Self) -> bool {
228        let mut changed = false;
229        let take_over_ambient = take_over.ambient;
230        let take_over_visible = take_over.visible;
231        let take_over_show_icon = take_over.show_icon;
232        if take_over.amplifier > self.amplifier {
233            if take_over.is_shorter_duration_than(self) {
234                let previous_hidden_effect = self.hidden_effect.take();
235                let mut hidden = self.clone();
236                hidden.hidden_effect = previous_hidden_effect;
237                self.hidden_effect = Some(Box::new(hidden));
238            }
239
240            self.amplifier = take_over.amplifier;
241            self.duration = take_over.duration;
242            changed = true;
243        } else if self.is_shorter_duration_than(&take_over) {
244            if take_over.amplifier == self.amplifier {
245                self.duration = take_over.duration;
246                changed = true;
247            } else if let Some(hidden_effect) = &mut self.hidden_effect {
248                hidden_effect.update(take_over);
249            } else {
250                self.hidden_effect = Some(Box::new(take_over));
251            }
252        }
253
254        if (!take_over_ambient && self.ambient) || changed {
255            self.ambient = take_over_ambient;
256            changed = true;
257        }
258
259        if take_over_visible != self.visible {
260            self.visible = take_over_visible;
261            changed = true;
262        }
263
264        if take_over_show_icon != self.show_icon {
265            self.show_icon = take_over_show_icon;
266            changed = true;
267        }
268
269        changed
270    }
271
272    fn tick_duration(&mut self) -> MobEffectTickResult {
273        if !self.has_remaining_duration() {
274            return MobEffectTickResult::Expired;
275        }
276
277        self.tick_down_duration();
278        if self.downgrade_to_hidden_effect() {
279            return MobEffectTickResult::Active { downgraded: true };
280        }
281        if self.has_remaining_duration() {
282            MobEffectTickResult::Active { downgraded: false }
283        } else {
284            MobEffectTickResult::Expired
285        }
286    }
287
288    fn tick_down_duration(&mut self) {
289        if let Some(hidden_effect) = &mut self.hidden_effect {
290            hidden_effect.tick_down_duration();
291        }
292
293        if !self.is_infinite_duration() && self.duration != 0 {
294            self.duration -= 1;
295        }
296    }
297
298    fn downgrade_to_hidden_effect(&mut self) -> bool {
299        if self.duration != 0 {
300            return false;
301        }
302
303        let Some(hidden_effect) = self.hidden_effect.take() else {
304            return false;
305        };
306        let MobEffectInstance {
307            duration,
308            amplifier,
309            ambient,
310            visible,
311            show_icon,
312            hidden_effect,
313            ..
314        } = *hidden_effect;
315        self.duration = duration;
316        self.amplifier = amplifier;
317        self.ambient = ambient;
318        self.visible = visible;
319        self.show_icon = show_icon;
320        self.hidden_effect = hidden_effect;
321        true
322    }
323}
324
325const fn clamp_effect_amplifier(amplifier: i32) -> i32 {
326    if amplifier < MIN_EFFECT_AMPLIFIER {
327        MIN_EFFECT_AMPLIFIER
328    } else if amplifier > MAX_EFFECT_AMPLIFIER {
329        MAX_EFFECT_AMPLIFIER
330    } else {
331        amplifier
332    }
333}
334
335enum MobEffectTickResult {
336    Active { downgraded: bool },
337    Expired,
338}
339
340/// A queued mob-effect packet change produced by living effect state.
341#[derive(Debug, Clone, PartialEq, Eq)]
342pub enum MobEffectSyncChange {
343    /// Add or update a mob effect.
344    Update {
345        /// The active effect instance to encode.
346        effect: MobEffectInstance,
347        /// Whether the owner-client packet should use vanilla's blend flag.
348        blend_for_self: bool,
349    },
350    /// Remove a mob effect.
351    Remove {
352        /// The effect type to remove.
353        effect: MobEffectRef,
354    },
355}
356
357impl MobEffectSyncChange {
358    /// Builds the clientbound packet for a concrete recipient.
359    #[must_use]
360    pub fn packet(&self, entity_id: i32, is_self_recipient: bool) -> MobEffectSyncPacket {
361        match self {
362            Self::Update {
363                effect,
364                blend_for_self,
365            } => MobEffectSyncPacket::Update(CUpdateMobEffect::new(
366                entity_id,
367                effect.effect,
368                effect.amplifier,
369                effect.duration,
370                MobEffectPacketFlags {
371                    ambient: effect.ambient,
372                    visible: effect.visible,
373                    show_icon: effect.show_icon,
374                    blend: *blend_for_self && is_self_recipient,
375                },
376            )),
377            Self::Remove { effect } => {
378                MobEffectSyncPacket::Remove(CRemoveMobEffect::new(entity_id, effect))
379            }
380        }
381    }
382}
383
384/// Concrete mob-effect packet ready to send to a player connection.
385#[derive(Debug, Clone)]
386pub enum MobEffectSyncPacket {
387    /// Add/update mob-effect packet.
388    Update(CUpdateMobEffect),
389    /// Remove mob-effect packet.
390    Remove(CRemoveMobEffect),
391}
392
393/// Synchronized living entity-data values derived from active mob effects.
394#[derive(Debug, Clone, PartialEq)]
395pub struct MobEffectDisplayState {
396    /// Visible effect particles for `LivingEntity.DATA_EFFECT_PARTICLES`.
397    pub particles: ParticleList,
398    /// Whether all active effects are ambient.
399    pub ambient: bool,
400    /// Whether the shared invisible flag should be set by active effects.
401    pub invisible: bool,
402    /// Whether the shared glowing flag should be set by active effects.
403    pub glowing: bool,
404}
405
406/// Movement input stored on vanilla `LivingEntity`.
407///
408/// Vanilla names these fields `xxa`, `yya`, and `zza`; Steel uses axis names
409/// so AI/pathfinding code can set intent without carrying obfuscated names.
410#[derive(Debug, Clone, Copy, PartialEq)]
411pub struct LivingTravelInput {
412    sideways: f32,
413    vertical: f32,
414    forward: f32,
415}
416
417impl LivingTravelInput {
418    /// No travel input.
419    pub const ZERO: Self = Self::new(0.0, 0.0, 0.0);
420
421    /// Creates living travel input.
422    #[must_use]
423    pub const fn new(sideways: f32, vertical: f32, forward: f32) -> Self {
424        Self {
425            sideways,
426            vertical,
427            forward,
428        }
429    }
430
431    /// Returns sideways movement input.
432    #[must_use]
433    pub const fn sideways(self) -> f32 {
434        self.sideways
435    }
436
437    /// Returns vertical movement input.
438    #[must_use]
439    pub const fn vertical(self) -> f32 {
440        self.vertical
441    }
442
443    /// Returns forward movement input.
444    #[must_use]
445    pub const fn forward(self) -> f32 {
446        self.forward
447    }
448
449    /// Returns input after vanilla `LivingEntity.applyInput()` damping.
450    #[must_use]
451    pub const fn dampened(self) -> Self {
452        Self {
453            sideways: self.sideways * 0.98,
454            vertical: self.vertical,
455            forward: self.forward * 0.98,
456        }
457    }
458}
459
460/// Vanilla living-entity body/head rotation state.
461#[derive(Debug, Clone, Copy, PartialEq)]
462pub struct LivingRotationState {
463    y_body_rot: f32,
464    y_body_rot_o: f32,
465    y_head_rot: f32,
466    y_head_rot_o: f32,
467}
468
469impl LivingRotationState {
470    /// Creates default living rotation state.
471    #[must_use]
472    pub const fn new() -> Self {
473        Self {
474            y_body_rot: 0.0,
475            y_body_rot_o: 0.0,
476            y_head_rot: 0.0,
477            y_head_rot_o: 0.0,
478        }
479    }
480
481    /// Returns vanilla `yBodyRot`.
482    #[must_use]
483    pub const fn y_body_rot(self) -> f32 {
484        self.y_body_rot
485    }
486
487    /// Returns vanilla `yBodyRotO`.
488    #[must_use]
489    pub const fn y_body_rot_o(self) -> f32 {
490        self.y_body_rot_o
491    }
492
493    /// Returns vanilla `yHeadRot`.
494    #[must_use]
495    pub const fn y_head_rot(self) -> f32 {
496        self.y_head_rot
497    }
498
499    /// Returns vanilla `yHeadRotO`.
500    #[must_use]
501    pub const fn y_head_rot_o(self) -> f32 {
502        self.y_head_rot_o
503    }
504}
505
506impl Default for LivingRotationState {
507    fn default() -> Self {
508        Self::new()
509    }
510}
511
512/// Vanilla arm-swing animation state stored on `LivingEntity`.
513#[derive(Debug, Clone, Copy, PartialEq)]
514pub struct LivingSwingState {
515    swinging: bool,
516    swinging_arm: Option<InteractionHand>,
517    swing_time: i32,
518    old_attack_anim: f32,
519    attack_anim: f32,
520}
521
522impl LivingSwingState {
523    /// Creates empty vanilla swing animation state.
524    #[must_use]
525    pub const fn new() -> Self {
526        Self {
527            swinging: false,
528            swinging_arm: None,
529            swing_time: 0,
530            old_attack_anim: 0.0,
531            attack_anim: 0.0,
532        }
533    }
534
535    /// Returns vanilla `LivingEntity.swinging`.
536    #[must_use]
537    pub const fn swinging(self) -> bool {
538        self.swinging
539    }
540
541    /// Returns vanilla `LivingEntity.swingingArm`.
542    #[must_use]
543    pub const fn swinging_arm(self) -> Option<InteractionHand> {
544        self.swinging_arm
545    }
546
547    /// Returns vanilla `LivingEntity.swingTime`.
548    #[must_use]
549    pub const fn swing_time(self) -> i32 {
550        self.swing_time
551    }
552
553    /// Returns vanilla `LivingEntity.oAttackAnim`.
554    #[must_use]
555    pub const fn old_attack_anim(self) -> f32 {
556        self.old_attack_anim
557    }
558
559    /// Returns vanilla `LivingEntity.attackAnim`.
560    #[must_use]
561    pub const fn attack_anim(self) -> f32 {
562        self.attack_anim
563    }
564}
565
566impl Default for LivingSwingState {
567    fn default() -> Self {
568        Self::new()
569    }
570}
571
572#[derive(Debug, Clone)]
573struct LivingEntityState {
574    effects_dirty: bool,
575    death_processed: bool,
576    invulnerable_time: i32,
577    last_hurt: f32,
578    last_hurt_by_player: Option<Uuid>,
579    last_hurt_by_player_memory_time: i32,
580    last_hurt_by_mob: Option<WeakEntity>,
581    last_hurt_by_mob_timestamp: i32,
582    last_hurt_mob: Option<WeakEntity>,
583    last_hurt_mob_timestamp: i32,
584    last_damage_source: Option<DamageSource>,
585    last_damage_stamp: i64,
586    absorption_amount: f32,
587    skip_drop_experience: bool,
588    death_time: i32,
589    speed: f32,
590    current_impulse_impact_pos: Option<DVec3>,
591    current_impulse_context_reset_grace_time: i32,
592    fall_flying: bool,
593    fall_flying_ticks: i32,
594    sprinting: bool,
595    sleeping_pos: Option<BlockPos>,
596    last_climbable_pos: Option<BlockPos>,
597    discard_friction: bool,
598    jumping: bool,
599    travel_input: LivingTravelInput,
600    rotation: LivingRotationState,
601    swing: LivingSwingState,
602    no_jump_delay: i32,
603    no_action_time: i32,
604}
605
606impl LivingEntityState {
607    const fn new(speed: f32) -> Self {
608        Self {
609            effects_dirty: false,
610            death_processed: false,
611            invulnerable_time: 0,
612            last_hurt: 0.0,
613            last_hurt_by_player: None,
614            last_hurt_by_player_memory_time: 0,
615            last_hurt_by_mob: None,
616            last_hurt_by_mob_timestamp: 0,
617            last_hurt_mob: None,
618            last_hurt_mob_timestamp: 0,
619            last_damage_source: None,
620            last_damage_stamp: 0,
621            absorption_amount: 0.0,
622            skip_drop_experience: false,
623            death_time: 0,
624            speed,
625            current_impulse_impact_pos: None,
626            current_impulse_context_reset_grace_time: 0,
627            fall_flying: false,
628            fall_flying_ticks: 0,
629            sprinting: false,
630            sleeping_pos: None,
631            last_climbable_pos: None,
632            discard_friction: false,
633            jumping: false,
634            travel_input: LivingTravelInput::ZERO,
635            rotation: LivingRotationState::new(),
636            swing: LivingSwingState::new(),
637            no_jump_delay: 0,
638            no_action_time: 0,
639        }
640    }
641
642    const fn reset_death_state(&mut self) {
643        self.death_processed = false;
644        self.death_time = 0;
645        self.invulnerable_time = 0;
646        self.last_hurt = 0.0;
647        self.absorption_amount = 0.0;
648        self.skip_drop_experience = false;
649    }
650}
651
652/// Common runtime fields shared by all living entities.
653///
654/// **Deviation from vanilla:** Vanilla calls this guard `LivingEntity.dead`,
655/// but it means death side effects have been processed, not health is zero.
656/// `ServerPlayer.die()` does NOT call `super.die()` and never sets that field.
657/// Steel uses this guard for players too because it reuses the same `Player`
658/// instance; health remains the source of truth for dead-or-dying checks such
659/// as client respawn requests.
660pub struct LivingEntityBase {
661    state: SyncMutex<LivingEntityState>,
662    attributes: SyncMutex<AttributeMap>,
663    active_mob_effects: SyncMutex<FxHashMap<MobEffectRef, ActiveMobEffect>>,
664    dirty_mob_effects: SyncMutex<Vec<MobEffectSyncChange>>,
665    equipment: Shared<dyn EntityEquipment>,
666    last_equipment_items: SyncMutex<[ItemStack; EquipmentSlot::ALL.len()]>,
667    pending_equipment_changes: SyncMutex<[Option<ItemStack>; EquipmentSlot::ALL.len()]>,
668    equipment_attribute_modifiers:
669        SyncMutex<[Vec<EquipmentAttributeModifierKey>; EquipmentSlot::ALL.len()]>,
670}
671
672#[derive(Debug)]
673struct EquipmentAttributeModifierKey {
674    attribute: AttributeRef,
675    id: Identifier,
676}
677
678impl LivingEntityBase {
679    /// Creates living runtime state from an entity type's default attributes.
680    #[must_use]
681    pub fn new(entity_type: EntityTypeRef) -> Self {
682        Self::with_attributes(AttributeMap::new_for_entity(entity_type))
683    }
684
685    /// Creates living runtime state from an explicit attribute map.
686    #[must_use]
687    pub fn with_attributes(attributes: AttributeMap) -> Self {
688        let equipment: Shared<dyn EntityEquipment> = OwnedEntityEquipment::new().into_shared();
689        Self::with_attributes_and_equipment(attributes, equipment)
690    }
691
692    /// Creates living runtime state with an explicit canonical equipment backing.
693    #[must_use]
694    pub fn with_equipment(
695        entity_type: EntityTypeRef,
696        equipment: Shared<dyn EntityEquipment>,
697    ) -> Self {
698        Self::with_attributes_and_equipment(AttributeMap::new_for_entity(entity_type), equipment)
699    }
700
701    fn with_attributes_and_equipment(
702        attributes: AttributeMap,
703        equipment: Shared<dyn EntityEquipment>,
704    ) -> Self {
705        let speed = attributes.required_value(vanilla_attributes::MOVEMENT_SPEED) as f32;
706
707        Self {
708            state: SyncMutex::new(LivingEntityState::new(speed)),
709            attributes: SyncMutex::new(attributes),
710            active_mob_effects: SyncMutex::new(FxHashMap::default()),
711            dirty_mob_effects: SyncMutex::new(Vec::new()),
712            equipment,
713            last_equipment_items: SyncMutex::new(array::from_fn(|_| ItemStack::empty())),
714            pending_equipment_changes: SyncMutex::new(array::from_fn(|_| None)),
715            equipment_attribute_modifiers: SyncMutex::new(array::from_fn(|_| Vec::new())),
716        }
717    }
718
719    /// Returns this entity's attribute map.
720    #[inline]
721    pub const fn attributes(&self) -> &SyncMutex<AttributeMap> {
722        &self.attributes
723    }
724
725    /// Applies vanilla constructor-time synced-data mutations for living entities.
726    ///
727    /// Vanilla defines `DATA_HEALTH_ID` as `1.0F`, then `LivingEntity` constructs
728    /// its attribute map and calls `setHealth(getMaxHealth())`.
729    pub fn initialize_synced_data<T: VanillaLivingEntityData>(&self, entity_data: &mut T) {
730        let max_health = self
731            .attributes
732            .lock()
733            .required_value(vanilla_attributes::MAX_HEALTH) as f32;
734        entity_data.living_entity_mut().health.set(max_health);
735    }
736
737    /// Returns vanilla `LivingEntity.equipment` storage.
738    #[inline]
739    pub const fn equipment(&self) -> &Shared<dyn EntityEquipment> {
740        &self.equipment
741    }
742
743    /// Collects equipment changes against Vanilla's previous-tick snapshots.
744    pub fn collect_equipment_changes(&self) -> Vec<(EquipmentSlot, ItemStack, ItemStack)> {
745        let current_items: [ItemStack; EquipmentSlot::ALL.len()] = {
746            let equipment = self.equipment.lock();
747            array::from_fn(|index| equipment.get_ref(EquipmentSlot::ALL[index]).clone())
748        };
749        let mut last_items = self.last_equipment_items.lock();
750        let mut changes = Vec::new();
751
752        for slot in EquipmentSlot::ALL {
753            let index = slot.index();
754            if ItemStack::matches(&last_items[index], &current_items[index]) {
755                continue;
756            }
757            let previous = mem::replace(&mut last_items[index], current_items[index].clone());
758            changes.push((slot, previous, current_items[index].clone()));
759        }
760        changes
761    }
762
763    /// Coalesces detected equipment changes until entity tracking sends them.
764    pub fn queue_equipment_changes(
765        &self,
766        changes: impl IntoIterator<Item = (EquipmentSlot, ItemStack)>,
767    ) {
768        let mut pending = self.pending_equipment_changes.lock();
769        for (slot, item_stack) in changes {
770            pending[slot.index()] = Some(item_stack);
771        }
772    }
773
774    /// Drains equipment changes detected by the living tick.
775    pub fn drain_equipment_changes(&self) -> Vec<(EquipmentSlot, ItemStack)> {
776        let mut pending = self.pending_equipment_changes.lock();
777        EquipmentSlot::ALL
778            .into_iter()
779            .filter_map(|slot| pending[slot.index()].take().map(|item| (slot, item)))
780            .collect()
781    }
782
783    /// Returns vanilla living body/head rotation state.
784    #[must_use]
785    pub fn rotation_state(&self) -> LivingRotationState {
786        self.state.lock().rotation
787    }
788
789    /// Returns vanilla arm-swing animation state.
790    #[must_use]
791    pub fn swing_state(&self) -> LivingSwingState {
792        self.state.lock().swing
793    }
794
795    /// Returns vanilla `yBodyRot`.
796    #[must_use]
797    pub fn y_body_rot(&self) -> f32 {
798        self.state.lock().rotation.y_body_rot
799    }
800
801    /// Sets vanilla `yBodyRot`.
802    pub fn set_y_body_rot(&self, y_body_rot: f32) {
803        self.state.lock().rotation.y_body_rot = y_body_rot;
804    }
805
806    /// Returns vanilla `yHeadRot`.
807    #[must_use]
808    pub fn y_head_rot(&self) -> f32 {
809        self.state.lock().rotation.y_head_rot
810    }
811
812    /// Sets vanilla `yHeadRot`.
813    pub fn set_y_head_rot(&self, y_head_rot: f32) {
814        self.state.lock().rotation.y_head_rot = y_head_rot;
815    }
816
817    /// Copies current living head/body rotations to their old-rotation fields.
818    pub fn advance_rotation_for_base_tick(&self) {
819        let mut state = self.state.lock();
820        state.rotation.y_head_rot_o = state.rotation.y_head_rot;
821        state.rotation.y_body_rot_o = state.rotation.y_body_rot;
822    }
823
824    /// Copies current attack animation to vanilla `oAttackAnim`.
825    pub fn advance_attack_animation_for_base_tick(&self) {
826        let mut state = self.state.lock();
827        state.swing.old_attack_anim = state.swing.attack_anim;
828    }
829
830    /// Starts vanilla `LivingEntity.swing` state if the swing gate allows it.
831    pub fn start_swing(&self, hand: InteractionHand, current_swing_duration: i32) -> bool {
832        let mut state = self.state.lock();
833        let swing = &mut state.swing;
834        if swing.swinging && swing.swing_time < current_swing_duration / 2 && swing.swing_time >= 0
835        {
836            return false;
837        }
838
839        swing.swing_time = -1;
840        swing.swinging = true;
841        swing.swinging_arm = Some(hand);
842        true
843    }
844
845    /// Updates vanilla `LivingEntity.swingTime` and `attackAnim`.
846    pub fn update_swing_time(&self, current_swing_duration: i32) {
847        let mut state = self.state.lock();
848        let swing = &mut state.swing;
849        if swing.swinging {
850            swing.swing_time += 1;
851            if swing.swing_time >= current_swing_duration {
852                swing.swing_time = 0;
853                swing.swinging = false;
854            }
855        } else {
856            swing.swing_time = 0;
857        }
858
859        swing.attack_anim = swing.swing_time as f32 / current_swing_duration as f32;
860    }
861
862    /// Returns vanilla `LivingEntity.absorptionAmount` for non-player living entities.
863    #[must_use]
864    pub fn absorption_amount(&self) -> f32 {
865        self.state.lock().absorption_amount
866    }
867
868    /// Sets vanilla `LivingEntity.absorptionAmount` for non-player living entities.
869    pub fn set_absorption_amount(&self, amount: f32) {
870        let max_absorption = self
871            .attributes
872            .lock()
873            .required_value(vanilla_attributes::MAX_ABSORPTION) as f32;
874        self.state.lock().absorption_amount = amount.clamp(0.0, max_absorption);
875    }
876
877    /// Runs vanilla `LivingEntity.skipDropExperience`.
878    pub fn skip_drop_experience(&self) {
879        self.state.lock().skip_drop_experience = true;
880    }
881
882    /// Returns vanilla `LivingEntity.wasExperienceConsumed`.
883    #[must_use]
884    pub fn was_experience_consumed(&self) -> bool {
885        self.state.lock().skip_drop_experience
886    }
887
888    /// Returns vanilla `LivingEntity.noActionTime`.
889    #[must_use]
890    pub fn no_action_time(&self) -> i32 {
891        self.state.lock().no_action_time
892    }
893
894    /// Sets vanilla `LivingEntity.noActionTime`.
895    pub fn set_no_action_time(&self, no_action_time: i32) {
896        self.state.lock().no_action_time = no_action_time;
897    }
898
899    /// Increments vanilla `LivingEntity.noActionTime` by one tick.
900    pub fn increment_no_action_time(&self) {
901        self.state.lock().no_action_time += 1;
902    }
903
904    /// Refreshes transient item attribute modifiers for an equipment slot.
905    pub fn refresh_equipment_attribute_modifiers(
906        &self,
907        slot: EquipmentSlot,
908        item_stack: &ItemStack,
909    ) {
910        let slot_index = slot.index();
911        let mut attributes = self.attributes.lock();
912        let mut installed_modifiers = self.equipment_attribute_modifiers.lock();
913
914        for key in installed_modifiers[slot_index].drain(..) {
915            attributes.remove_modifier(key.attribute, &key.id);
916        }
917
918        if item_stack.is_empty() || item_stack.is_broken() {
919            return;
920        }
921
922        let Some(modifiers) = item_stack.get_attribute_modifiers() else {
923            return;
924        };
925
926        for entry in modifiers.for_slot(slot) {
927            for (index, keys) in installed_modifiers.iter_mut().enumerate() {
928                if index == slot_index {
929                    continue;
930                }
931                keys.retain(|key| key.attribute.key != entry.attribute.key || key.id != entry.id);
932            }
933
934            attributes.remove_modifier(entry.attribute, &entry.id);
935            if attributes.add_modifier(
936                entry.attribute,
937                AttributeModifier {
938                    id: entry.id.clone(),
939                    amount: entry.amount,
940                    operation: entry.operation,
941                },
942                false,
943            ) {
944                installed_modifiers[slot_index].push(EquipmentAttributeModifierKey {
945                    attribute: entry.attribute,
946                    id: entry.id.clone(),
947                });
948            }
949        }
950    }
951
952    /// Returns whether this living entity has an active vanilla mob effect.
953    #[must_use]
954    pub fn has_mob_effect(&self, effect: MobEffectRef) -> bool {
955        self.active_mob_effects.lock().contains_key(&effect)
956    }
957
958    /// Returns active vanilla mob-effect state.
959    #[must_use]
960    pub fn mob_effect(&self, effect: MobEffectRef) -> Option<ActiveMobEffect> {
961        self.active_mob_effects.lock().get(&effect).cloned()
962    }
963
964    /// Returns all active vanilla mob effects.
965    #[must_use]
966    pub fn active_mob_effects(&self) -> Vec<ActiveMobEffect> {
967        self.active_mob_effects.lock().values().cloned().collect()
968    }
969
970    /// Adds or updates active vanilla mob-effect state.
971    pub fn add_mob_effect(&self, effect: MobEffectInstance) -> bool {
972        let effect_key = effect.effect;
973        let mut existing_effect = None;
974        let mut changed_effect = None;
975        {
976            let mut effects = self.active_mob_effects.lock();
977            if let Some(current) = effects.get_mut(&effect_key) {
978                if current.update(effect) {
979                    changed_effect = Some(current.clone());
980                }
981            } else {
982                effects.insert(effect_key, effect.clone());
983                existing_effect = Some(effect);
984            }
985        }
986
987        if let Some(effect) = existing_effect {
988            self.add_effect_attribute_modifiers(&effect);
989            self.mark_effects_dirty();
990            self.queue_mob_effect_sync(MobEffectSyncChange::Update {
991                effect,
992                blend_for_self: true,
993            });
994            return true;
995        }
996
997        if let Some(effect) = changed_effect {
998            self.refresh_effect_attribute_modifiers(&effect);
999            self.mark_effects_dirty();
1000            self.queue_mob_effect_sync(MobEffectSyncChange::Update {
1001                effect,
1002                blend_for_self: false,
1003            });
1004            return true;
1005        }
1006
1007        false
1008    }
1009
1010    /// Sets active vanilla mob-effect state.
1011    pub fn set_mob_effect(&self, effect: MobEffectRef, amplifier: i32) {
1012        self.add_mob_effect(MobEffectInstance::new(effect, amplifier));
1013    }
1014
1015    /// Sets the presence of a vanilla mob effect.
1016    pub fn set_mob_effect_active(&self, effect: MobEffectRef, active: bool) {
1017        if active {
1018            self.set_mob_effect(effect, 0);
1019        } else {
1020            self.remove_mob_effect(effect);
1021        }
1022    }
1023
1024    /// Removes active vanilla mob-effect state.
1025    pub fn remove_mob_effect(&self, effect: MobEffectRef) -> bool {
1026        let removed = self.active_mob_effects.lock().remove(&effect);
1027        let Some(removed) = removed else {
1028            return false;
1029        };
1030
1031        self.remove_effect_attribute_modifiers(removed.effect);
1032        self.mark_effects_dirty();
1033        self.queue_mob_effect_sync(MobEffectSyncChange::Remove { effect });
1034        true
1035    }
1036
1037    /// Ticks one active mob-effect duration after its server behavior has run.
1038    pub(super) fn tick_mob_effect_duration(&self, effect_key: MobEffectRef) {
1039        let (updated, removed) = {
1040            let mut effects = self.active_mob_effects.lock();
1041            let Some(effect) = effects.get_mut(&effect_key) else {
1042                return;
1043            };
1044            match effect.tick_duration() {
1045                MobEffectTickResult::Active { downgraded } => {
1046                    let updated =
1047                        (downgraded || effect.duration() % 600 == 0).then(|| effect.clone());
1048                    (updated, None)
1049                }
1050                MobEffectTickResult::Expired => (None, effects.remove(&effect_key)),
1051            }
1052        };
1053
1054        if let Some(effect) = updated {
1055            self.refresh_effect_attribute_modifiers(&effect);
1056            self.mark_effects_dirty();
1057            self.queue_mob_effect_sync(MobEffectSyncChange::Update {
1058                effect,
1059                blend_for_self: false,
1060            });
1061        }
1062
1063        if let Some(effect) = removed {
1064            self.remove_effect_attribute_modifiers(effect.effect);
1065            self.mark_effects_dirty();
1066            self.queue_mob_effect_sync(MobEffectSyncChange::Remove {
1067                effect: effect.effect,
1068            });
1069        }
1070    }
1071
1072    /// Drains pending mob-effect packet changes.
1073    pub fn drain_dirty_mob_effects(&self) -> Vec<MobEffectSyncChange> {
1074        self.dirty_mob_effects.lock().drain(..).collect()
1075    }
1076
1077    /// Returns whether synchronized effect entity data should be recomputed.
1078    pub fn take_effects_dirty(&self) -> bool {
1079        let mut state = self.state.lock();
1080        let dirty = state.effects_dirty;
1081        state.effects_dirty = false;
1082        dirty
1083    }
1084
1085    /// Builds the synchronized living effect particle/glow/invisibility state.
1086    pub fn mob_effect_display_state(&self) -> MobEffectDisplayState {
1087        let mut effects = self
1088            .active_mob_effects
1089            .lock()
1090            .values()
1091            .cloned()
1092            .collect::<Vec<_>>();
1093        effects.sort_by_key(|effect| effect.effect.try_id().unwrap_or(usize::MAX));
1094
1095        let particles = effects
1096            .iter()
1097            .filter(|effect| effect.is_visible())
1098            .map(|effect| effect.effect.create_particle_options(effect.ambient))
1099            .collect();
1100
1101        MobEffectDisplayState {
1102            particles: ParticleList { particles },
1103            ambient: !effects.is_empty() && effects.iter().all(MobEffectInstance::is_ambient),
1104            invisible: effects
1105                .iter()
1106                .any(|effect| effect.effect == vanilla_mob_effects::INVISIBILITY),
1107            glowing: effects
1108                .iter()
1109                .any(|effect| effect.effect == vanilla_mob_effects::GLOWING),
1110        }
1111    }
1112
1113    fn add_effect_attribute_modifiers(&self, effect: &MobEffectInstance) {
1114        let mut attributes = self.attributes.lock();
1115        for modifier in effect.effect.attribute_modifiers {
1116            attributes.remove_modifier(modifier.attribute, &modifier.id);
1117            attributes.add_modifier(
1118                modifier.attribute,
1119                AttributeModifier {
1120                    id: modifier.id.clone(),
1121                    amount: modifier.amount * f64::from(effect.amplifier + 1),
1122                    operation: modifier.operation,
1123                },
1124                false,
1125            );
1126        }
1127    }
1128
1129    fn refresh_effect_attribute_modifiers(&self, effect: &MobEffectInstance) {
1130        self.remove_effect_attribute_modifiers(effect.effect);
1131        self.add_effect_attribute_modifiers(effect);
1132    }
1133
1134    fn remove_effect_attribute_modifiers(&self, effect: MobEffectRef) {
1135        let mut attributes = self.attributes.lock();
1136        for modifier in effect.attribute_modifiers {
1137            attributes.remove_modifier(modifier.attribute, &modifier.id);
1138        }
1139    }
1140
1141    fn queue_mob_effect_sync(&self, change: MobEffectSyncChange) {
1142        self.dirty_mob_effects.lock().push(change);
1143    }
1144
1145    /// Marks synchronized effect visibility data for recomputation.
1146    pub(crate) fn mark_effects_dirty(&self) {
1147        self.state.lock().effects_dirty = true;
1148    }
1149
1150    /// Gets the cached movement speed used by living movement code.
1151    #[inline]
1152    pub fn speed(&self) -> f32 {
1153        self.state.lock().speed
1154    }
1155
1156    /// Sets the cached movement speed used by living movement code.
1157    #[inline]
1158    pub fn set_speed(&self, speed: f32) {
1159        self.state.lock().speed = speed;
1160    }
1161
1162    /// Refreshes the cached movement speed from the `MOVEMENT_SPEED` attribute.
1163    pub fn refresh_speed_from_attributes(&self) {
1164        if let Some(speed) = self
1165            .attributes
1166            .lock()
1167            .get_value(vanilla_attributes::MOVEMENT_SPEED)
1168        {
1169            self.state.lock().speed = speed as f32;
1170        }
1171    }
1172
1173    /// Applies vanilla post-impulse movement validation grace.
1174    pub fn apply_post_impulse_grace_time(&self, ticks: i32) {
1175        let mut state = self.state.lock();
1176        state.current_impulse_context_reset_grace_time =
1177            state.current_impulse_context_reset_grace_time.max(ticks);
1178    }
1179
1180    /// Mirrors vanilla `LivingEntity.setIgnoreFallDamageFromCurrentImpulse`.
1181    pub fn set_ignore_fall_damage_from_current_impulse(
1182        &self,
1183        ignore_fall_damage: bool,
1184        new_impulse_impact_pos: DVec3,
1185    ) {
1186        let mut state = self.state.lock();
1187        if ignore_fall_damage {
1188            state.current_impulse_context_reset_grace_time = state
1189                .current_impulse_context_reset_grace_time
1190                .max(POST_IMPULSE_GRACE_TICKS);
1191            state.current_impulse_impact_pos = Some(new_impulse_impact_pos);
1192        } else {
1193            state.current_impulse_context_reset_grace_time = 0;
1194        }
1195    }
1196
1197    /// Returns vanilla `LivingEntity.currentImpulseImpactPos`.
1198    #[must_use]
1199    pub fn current_impulse_impact_pos(&self) -> Option<DVec3> {
1200        self.state.lock().current_impulse_impact_pos
1201    }
1202
1203    /// Returns vanilla `LivingEntity.currentImpulseContextResetGraceTime`.
1204    #[must_use]
1205    pub fn current_impulse_context_reset_grace_time(&self) -> i32 {
1206        self.state.lock().current_impulse_context_reset_grace_time
1207    }
1208
1209    /// Returns vanilla `LivingEntity.isIgnoringFallDamageFromCurrentImpulse`.
1210    #[must_use]
1211    pub fn is_ignoring_fall_damage_from_current_impulse(&self) -> bool {
1212        self.state.lock().current_impulse_impact_pos.is_some()
1213    }
1214
1215    /// Mirrors vanilla `LivingEntity.tryResetCurrentImpulseContext`.
1216    pub fn try_reset_current_impulse_context(&self) {
1217        let mut state = self.state.lock();
1218        if state.current_impulse_context_reset_grace_time == 0 {
1219            state.current_impulse_impact_pos = None;
1220        }
1221    }
1222
1223    /// Mirrors vanilla `LivingEntity.resetCurrentImpulseContext`.
1224    pub fn reset_current_impulse_context(&self) {
1225        let mut state = self.state.lock();
1226        state.current_impulse_context_reset_grace_time = 0;
1227        state.current_impulse_impact_pos = None;
1228    }
1229
1230    /// Returns whether movement validation is inside post-impulse grace.
1231    #[must_use]
1232    pub fn is_in_post_impulse_grace_time(&self) -> bool {
1233        self.state.lock().current_impulse_context_reset_grace_time > 0
1234    }
1235
1236    /// Decrements post-impulse grace once per living-entity tick.
1237    pub fn tick_post_impulse_grace_time(&self) {
1238        let mut state = self.state.lock();
1239        if state.current_impulse_context_reset_grace_time > 0 {
1240            state.current_impulse_context_reset_grace_time -= 1;
1241        }
1242    }
1243
1244    /// Returns whether this living entity is currently fall flying.
1245    #[must_use]
1246    pub fn is_fall_flying(&self) -> bool {
1247        self.state.lock().fall_flying
1248    }
1249
1250    /// Sets the vanilla living-entity fall-flying state.
1251    pub fn set_fall_flying(&self, fall_flying: bool) {
1252        self.state.lock().fall_flying = fall_flying;
1253    }
1254
1255    /// Returns vanilla `LivingEntity.fallFlyTicks`.
1256    #[must_use]
1257    pub fn fall_flying_ticks(&self) -> i32 {
1258        self.state.lock().fall_flying_ticks
1259    }
1260
1261    /// Ticks vanilla `LivingEntity.fallFlyTicks`.
1262    pub fn tick_fall_flying_state(&self, fall_flying: bool) {
1263        let mut state = self.state.lock();
1264        if fall_flying {
1265            state.fall_flying_ticks = state.fall_flying_ticks.wrapping_add(1);
1266        } else {
1267            state.fall_flying_ticks = 0;
1268        }
1269    }
1270
1271    /// Returns whether this living entity is sprinting.
1272    #[must_use]
1273    pub fn is_sprinting(&self) -> bool {
1274        self.state.lock().sprinting
1275    }
1276
1277    /// Sets the vanilla living-entity sprinting state and movement-speed modifier.
1278    pub fn set_sprinting(&self, sprinting: bool) {
1279        self.state.lock().sprinting = sprinting;
1280
1281        let mut attributes = self.attributes.lock();
1282        if sprinting {
1283            attributes.add_modifier(
1284                vanilla_attributes::MOVEMENT_SPEED,
1285                AttributeModifier {
1286                    id: Identifier::vanilla_static("sprinting"),
1287                    amount: SPRINT_SPEED_MODIFIER_AMOUNT,
1288                    operation: AttributeModifierOperation::AddMultipliedTotal,
1289                },
1290                false,
1291            );
1292        } else {
1293            attributes.remove_modifier(
1294                vanilla_attributes::MOVEMENT_SPEED,
1295                &Identifier::vanilla_static("sprinting"),
1296            );
1297        }
1298    }
1299
1300    /// Returns the bed position that makes this living entity sleeping.
1301    #[must_use]
1302    pub fn sleeping_pos(&self) -> Option<BlockPos> {
1303        self.state.lock().sleeping_pos
1304    }
1305
1306    /// Sets the vanilla living-entity sleeping position.
1307    pub fn set_sleeping_pos(&self, bed_position: BlockPos) {
1308        self.state.lock().sleeping_pos = Some(bed_position);
1309    }
1310
1311    /// Clears the vanilla living-entity sleeping position.
1312    pub fn clear_sleeping_pos(&self) {
1313        self.state.lock().sleeping_pos = None;
1314    }
1315
1316    /// Returns whether this living entity has a sleeping position.
1317    #[must_use]
1318    pub fn is_sleeping(&self) -> bool {
1319        self.sleeping_pos().is_some()
1320    }
1321
1322    /// Returns the last climbable block position this living entity touched.
1323    #[must_use]
1324    pub fn last_climbable_pos(&self) -> Option<BlockPos> {
1325        self.state.lock().last_climbable_pos
1326    }
1327
1328    /// Records the last climbable block position this living entity touched.
1329    pub fn set_last_climbable_pos(&self, pos: BlockPos) {
1330        self.state.lock().last_climbable_pos = Some(pos);
1331    }
1332
1333    /// Returns whether vanilla living travel should skip friction damping.
1334    #[must_use]
1335    pub fn should_discard_friction(&self) -> bool {
1336        self.state.lock().discard_friction
1337    }
1338
1339    /// Sets whether vanilla living travel should skip friction damping.
1340    pub fn set_discard_friction(&self, discard_friction: bool) {
1341        self.state.lock().discard_friction = discard_friction;
1342    }
1343
1344    /// Returns whether this living entity is applying jump input.
1345    #[must_use]
1346    pub fn is_jumping(&self) -> bool {
1347        self.state.lock().jumping
1348    }
1349
1350    /// Sets whether this living entity is applying jump input.
1351    pub fn set_jumping(&self, jumping: bool) {
1352        self.state.lock().jumping = jumping;
1353    }
1354
1355    /// Returns vanilla living travel input.
1356    #[must_use]
1357    pub fn travel_input(&self) -> LivingTravelInput {
1358        self.state.lock().travel_input
1359    }
1360
1361    /// Sets vanilla living travel input.
1362    pub fn set_travel_input(&self, input: LivingTravelInput) {
1363        self.state.lock().travel_input = input;
1364    }
1365
1366    /// Applies vanilla `LivingEntity.applyInput()` damping to travel input.
1367    pub fn dampen_travel_input(&self) {
1368        let mut state = self.state.lock();
1369        state.travel_input = state.travel_input.dampened();
1370    }
1371
1372    /// Returns vanilla jump cooldown ticks.
1373    #[must_use]
1374    pub fn no_jump_delay(&self) -> i32 {
1375        self.state.lock().no_jump_delay
1376    }
1377
1378    /// Sets vanilla jump cooldown ticks.
1379    pub fn set_no_jump_delay(&self, ticks: i32) {
1380        self.state.lock().no_jump_delay = ticks;
1381    }
1382
1383    /// Decrements vanilla jump cooldown once per living AI step.
1384    pub fn tick_no_jump_delay(&self) {
1385        let mut state = self.state.lock();
1386        if state.no_jump_delay > 0 {
1387            state.no_jump_delay -= 1;
1388        }
1389    }
1390
1391    /// Calculates vanilla living-entity fall damage.
1392    #[must_use]
1393    pub fn calculate_fall_damage(
1394        fall_distance: f64,
1395        damage_modifier: f32,
1396        safe_fall_distance: f64,
1397        fall_damage_multiplier: f64,
1398    ) -> i32 {
1399        ((fall_distance + 1.0e-6 - safe_fall_distance)
1400            * f64::from(damage_modifier)
1401            * fall_damage_multiplier)
1402            .floor() as i32
1403    }
1404
1405    /// Decrements remaining invulnerability ticks by one if any are active.
1406    pub fn decrement_invulnerable_time(&self) {
1407        let mut state = self.state.lock();
1408        if state.invulnerable_time > 0 {
1409            state.invulnerable_time -= 1;
1410        }
1411    }
1412
1413    /// Applies vanilla hurt cooldown bookkeeping.
1414    ///
1415    /// Returns `None` when damage should be ignored because death was already
1416    /// processed or the amount did not exceed the active invulnerability frame.
1417    pub fn apply_damage_cooldown(
1418        &self,
1419        amount: f32,
1420        bypasses_cooldown: bool,
1421    ) -> Option<(bool, f32)> {
1422        let mut state = self.state.lock();
1423        if state.death_processed {
1424            return None;
1425        }
1426
1427        if state.invulnerable_time > 10 && !bypasses_cooldown {
1428            if amount <= state.last_hurt {
1429                return None;
1430            }
1431            let effective = amount - state.last_hurt;
1432            state.last_hurt = amount;
1433            Some((false, effective))
1434        } else {
1435            state.last_hurt = amount;
1436            state.invulnerable_time = 20;
1437            Some((true, amount))
1438        }
1439    }
1440
1441    /// Records vanilla `LivingEntity.lastDamageSource` after successful damage.
1442    pub fn record_last_damage_source(&self, source: &DamageSource, game_time: i64) {
1443        let mut state = self.state.lock();
1444        state.last_damage_source = Some(source.clone());
1445        state.last_damage_stamp = game_time;
1446    }
1447
1448    /// Returns vanilla `LivingEntity.getLastDamageSource()`.
1449    pub fn last_damage_source(&self, game_time: i64) -> Option<DamageSource> {
1450        let mut state = self.state.lock();
1451        if game_time - state.last_damage_stamp > 40 {
1452            state.last_damage_source = None;
1453        }
1454        state.last_damage_source.clone()
1455    }
1456
1457    /// Sets vanilla `LivingEntity.lastHurtByPlayer` and memory time.
1458    pub fn set_last_hurt_by_player(&self, player_uuid: Uuid, time_to_remember: i32) {
1459        let mut state = self.state.lock();
1460        state.last_hurt_by_player = Some(player_uuid);
1461        state.last_hurt_by_player_memory_time = time_to_remember;
1462    }
1463
1464    /// Returns vanilla `LivingEntity.lastHurtByPlayerMemoryTime`.
1465    #[must_use]
1466    pub fn last_hurt_by_player_memory_time(&self) -> i32 {
1467        self.state.lock().last_hurt_by_player_memory_time
1468    }
1469
1470    /// Returns the remembered player UUID, if present.
1471    #[must_use]
1472    pub fn last_hurt_by_player_uuid(&self) -> Option<Uuid> {
1473        self.state.lock().last_hurt_by_player
1474    }
1475
1476    /// Returns vanilla `LivingEntity.lastHurtByMob`, if still resolvable.
1477    #[must_use]
1478    pub fn last_hurt_by_mob(&self) -> Option<SharedEntity> {
1479        let mut state = self.state.lock();
1480        living_entity_from_weak(&mut state.last_hurt_by_mob)
1481    }
1482
1483    /// Returns vanilla `LivingEntity.lastHurtByMobTimestamp`.
1484    #[must_use]
1485    pub fn last_hurt_by_mob_timestamp(&self) -> i32 {
1486        self.state.lock().last_hurt_by_mob_timestamp
1487    }
1488
1489    /// Sets vanilla `LivingEntity.lastHurtByMob` and timestamp.
1490    pub fn set_last_hurt_by_mob(&self, target: Option<&SharedEntity>, tick_count: i32) {
1491        let mut state = self.state.lock();
1492        state.last_hurt_by_mob = weak_living_entity(target);
1493        state.last_hurt_by_mob_timestamp = tick_count;
1494    }
1495
1496    /// Returns vanilla `LivingEntity.lastHurtMob`, if still resolvable.
1497    #[must_use]
1498    pub fn last_hurt_mob(&self) -> Option<SharedEntity> {
1499        let mut state = self.state.lock();
1500        living_entity_from_weak(&mut state.last_hurt_mob)
1501    }
1502
1503    /// Returns vanilla `LivingEntity.lastHurtMobTimestamp`.
1504    #[must_use]
1505    pub fn last_hurt_mob_timestamp(&self) -> i32 {
1506        self.state.lock().last_hurt_mob_timestamp
1507    }
1508
1509    /// Sets vanilla `LivingEntity.lastHurtMob` and timestamp.
1510    pub fn set_last_hurt_mob(&self, target: Option<&SharedEntity>, tick_count: i32) {
1511        let mut state = self.state.lock();
1512        state.last_hurt_mob = weak_living_entity(target);
1513        state.last_hurt_mob_timestamp = tick_count;
1514    }
1515
1516    /// Ticks vanilla last-hurt-by-player memory.
1517    pub fn tick_last_hurt_by_player_memory(&self) {
1518        let mut state = self.state.lock();
1519        if state.last_hurt_by_player_memory_time > 0 {
1520            state.last_hurt_by_player_memory_time -= 1;
1521        } else {
1522            state.last_hurt_by_player = None;
1523        }
1524    }
1525
1526    /// Ticks vanilla living combat-memory cleanup.
1527    pub fn tick_living_combat_memory(&self, tick_count: i32) {
1528        if self
1529            .last_hurt_mob()
1530            .is_some_and(|target| living_is_dead(&target))
1531        {
1532            self.set_last_hurt_mob(None, tick_count);
1533        }
1534
1535        let Some(hurt_by) = self.last_hurt_by_mob() else {
1536            return;
1537        };
1538        if living_is_dead(&hurt_by) || tick_count - self.last_hurt_by_mob_timestamp() > 100 {
1539            self.set_last_hurt_by_mob(None, tick_count);
1540        }
1541    }
1542
1543    /// Marks death side effects as processed.
1544    ///
1545    /// Returns `false` if they were already processed.
1546    pub fn mark_death_processed(&self) -> bool {
1547        let mut state = self.state.lock();
1548        if state.death_processed {
1549            return false;
1550        }
1551        state.death_processed = true;
1552        true
1553    }
1554
1555    /// Increments death animation time by 1 and returns the new value.
1556    #[inline]
1557    pub fn increment_death_time(&self) -> i32 {
1558        let mut state = self.state.lock();
1559        state.death_time += 1;
1560        state.death_time
1561    }
1562
1563    /// Returns vanilla `LivingEntity.deathTime`.
1564    #[must_use]
1565    pub fn death_time(&self) -> i32 {
1566        self.state.lock().death_time
1567    }
1568
1569    /// Resets all death-related state back to alive defaults.
1570    #[inline]
1571    pub fn reset_death_state(&self) {
1572        self.state.lock().reset_death_state();
1573    }
1574
1575    /// Resets state that vanilla gets from constructing a fresh living player for death respawn.
1576    pub fn reset_for_player_respawn(&self) {
1577        self.set_sprinting(false);
1578
1579        // Vanilla respawns with a newly constructed `LivingEntity`, whose
1580        // equipment snapshots and related runtime bookkeeping start empty.
1581        // Steel reuses the same `Player`, so reset those fields explicitly.
1582        *self.last_equipment_items.lock() = array::from_fn(|_| ItemStack::empty());
1583        *self.pending_equipment_changes.lock() = array::from_fn(|_| None);
1584        {
1585            let mut attributes = self.attributes.lock();
1586            let mut installed_modifiers = self.equipment_attribute_modifiers.lock();
1587            for modifiers in installed_modifiers.iter_mut() {
1588                for key in modifiers.drain(..) {
1589                    attributes.remove_modifier(key.attribute, &key.id);
1590                }
1591            }
1592        }
1593
1594        let removed_effects = {
1595            let mut effects = self.active_mob_effects.lock();
1596            let removed_effects = effects.keys().copied().collect::<Vec<_>>();
1597            effects.clear();
1598            removed_effects
1599        };
1600
1601        for effect in removed_effects.iter().copied() {
1602            self.remove_effect_attribute_modifiers(effect);
1603        }
1604
1605        {
1606            let mut dirty_effects = self.dirty_mob_effects.lock();
1607            dirty_effects.clear();
1608            dirty_effects.extend(
1609                removed_effects
1610                    .into_iter()
1611                    .map(|effect| MobEffectSyncChange::Remove { effect }),
1612            );
1613        }
1614
1615        let speed = self
1616            .attributes
1617            .lock()
1618            .required_value(vanilla_attributes::MOVEMENT_SPEED) as f32;
1619
1620        let mut state = self.state.lock();
1621        *state = LivingEntityState::new(speed);
1622        state.effects_dirty = true;
1623    }
1624}
1625
1626fn weak_living_entity(target: Option<&SharedEntity>) -> Option<WeakEntity> {
1627    let target = target?;
1628    target.is_living_entity().then(|| Arc::downgrade(target))
1629}
1630
1631fn living_entity_from_weak(entity: &mut Option<WeakEntity>) -> Option<SharedEntity> {
1632    let Some(upgraded) = entity.as_ref().and_then(WeakEntity::upgrade) else {
1633        *entity = None;
1634        return None;
1635    };
1636    if !upgraded.is_living_entity() {
1637        *entity = None;
1638        return None;
1639    }
1640    Some(upgraded)
1641}
1642
1643fn living_is_dead(entity: &SharedEntity) -> bool {
1644    entity
1645        .as_living_entity()
1646        .is_none_or(|living| !LivingEntity::is_alive(living))
1647}
1648
1649#[cfg(test)]
1650mod tests;