Skip to main content

steel_core/entity/entities/mobs/hostile/
endermite.rs

1use std::iter::empty;
2use std::sync::Weak;
3
4use glam::DVec3;
5use rand::Rng;
6use simdnbt::borrow::NbtCompound as BorrowedNbtCompoundView;
7use simdnbt::owned::NbtCompound;
8use steel_macros::entity_behavior;
9use steel_protocol::packets::game::SoundSource;
10use steel_registry::entity_type::{EntityDimensions, EntityTypeRef};
11use steel_registry::sound_event::SoundEventRef;
12use steel_registry::vanilla_entity_data::EndermiteEntityData;
13use steel_registry::{sound_events, vanilla_attributes};
14use steel_utils::locks::SyncMutex;
15use steel_utils::nbt::NbtValueInput as _;
16use steel_utils::{BlockPos, BlockStateId, DowncastType, DowncastTypeKey};
17
18use crate::entity::ai::goal::{
19    ClimbOnTopOfPowderSnowGoal, FloatGoal, HurtByTargetGoal, LookAtPlayerGoal, MeleeAttackGoal,
20    NearestAttackableTargetGoal, RandomLookAroundGoal, WaterAvoidingRandomStrollGoal,
21};
22use crate::entity::damage::DamageSource;
23use crate::entity::{
24    DefaultEntityPush, Entity, EntityBase, EntityBaseLoad, EntityMovementEmission, EntityPose,
25    EntitySpawnReason, EntitySyncedData, LivingEntity, LivingEntityBase, Mob, MobBase, Monster,
26    PathfinderMob, RemovalReason, SharedEntity, SpawnRule, sync_dirty_mob_effects,
27};
28use crate::physics::MoveResult;
29use crate::world::{LevelReader, World};
30
31const DEFAULT_STEP_HEIGHT: f32 = 0.6;
32const MAX_LIFETIME: i32 = 2400;
33const XP_REWARD: i32 = 3;
34
35/// A hostile endermite entity.
36#[entity_behavior(class = "Endermite", spawn_rule)]
37pub struct EndermiteEntity {
38    base: EntityBase,
39    entity_type: EntityTypeRef,
40    living_base: LivingEntityBase,
41    mob_base: MobBase,
42    entity_data: SyncMutex<EndermiteEntityData>,
43    lifetime: SyncMutex<i32>,
44    player_spawned: SyncMutex<bool>,
45}
46
47// SAFETY: The owner-scoped type key uniquely identifies EndermiteEntity.
48unsafe impl DowncastType for EndermiteEntity {
49    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/endermite");
50}
51
52impl EndermiteEntity {
53    /// Vanilla `SpawnPlacements` predicate: `Endermite.checkEndermiteSpawnRules`.
54    pub(crate) const SPAWN_RULE: SpawnRule = Self::check_endermite_spawn_rules;
55
56    /// Checks the Endermite placement predicate for the spawner-only dispatcher.
57    pub(crate) fn check_endermite_spawn_rules(
58        _entity_type: EntityTypeRef,
59        _level: &dyn LevelReader,
60        spawn_reason: EntitySpawnReason,
61        _pos: BlockPos,
62        _random: &mut dyn Rng,
63    ) -> bool {
64        spawn_reason.is_spawner()
65    }
66
67    /// Creates a new endermite entity instance.
68    #[must_use]
69    pub fn new(entity_type: EntityTypeRef, id: i32, position: DVec3, world: Weak<World>) -> Self {
70        Self::new_with_base(
71            EntityBase::new(id, position, entity_type.dimensions, world),
72            entity_type,
73        )
74    }
75
76    /// Loads a saved endermite entity.
77    #[must_use]
78    pub fn from_saved(entity_type: EntityTypeRef, load: EntityBaseLoad) -> Self {
79        Self::new_with_base(
80            EntityBase::from_load(load, entity_type.dimensions),
81            entity_type,
82        )
83    }
84
85    fn new_with_base(base: EntityBase, entity_type: EntityTypeRef) -> Self {
86        let living_base = LivingEntityBase::new(entity_type);
87        let mob_base = MobBase::new();
88        mob_base.set_xp_reward(XP_REWARD);
89        let mut entity_data = EndermiteEntityData::new();
90        living_base.initialize_synced_data(&mut entity_data);
91
92        {
93            let mut goal_selector = mob_base.goal_selector().lock();
94            goal_selector.add_goal(1, FloatGoal::new(&mob_base));
95            goal_selector.add_goal(1, ClimbOnTopOfPowderSnowGoal::new());
96            goal_selector.add_goal(2, MeleeAttackGoal::new(1.0, false));
97            goal_selector.add_goal(3, WaterAvoidingRandomStrollGoal::new(1.0));
98            goal_selector.add_goal(7, LookAtPlayerGoal::new(8.0));
99            goal_selector.add_goal(8, RandomLookAroundGoal::new());
100
101            let mut target_selector = mob_base.target_selector().lock();
102            target_selector.add_goal(1, HurtByTargetGoal::new().set_alert_others(empty()));
103            target_selector.add_goal(
104                2,
105                NearestAttackableTargetGoal::new_for_players(true, |_, _| true),
106            );
107        }
108
109        Self {
110            base,
111            entity_type,
112            living_base,
113            mob_base,
114            entity_data: SyncMutex::new(entity_data),
115            lifetime: SyncMutex::new(0),
116            player_spawned: SyncMutex::new(false),
117        }
118    }
119
120    /// Returns true if the endermite was spawned by a player.
121    pub fn player_spawned(&self) -> bool {
122        *self.player_spawned.lock()
123    }
124
125    /// Sets whether the endermite was spawned by a player.
126    pub fn set_player_spawned(&self, player_spawned: bool) {
127        *self.player_spawned.lock() = player_spawned;
128    }
129
130    /// Returns the endermite's lifetime in ticks.
131    pub fn lifetime(&self) -> i32 {
132        *self.lifetime.lock()
133    }
134
135    /// Sets the endermite's lifetime in ticks.
136    pub fn set_lifetime(&self, lifetime: i32) {
137        *self.lifetime.lock() = lifetime;
138    }
139
140    fn update_dirty_mob_effect_entity_data(&self) {
141        sync_dirty_mob_effects(&self.living_base, &self.entity_data);
142    }
143}
144
145impl DefaultEntityPush for EndermiteEntity {}
146
147impl Entity for EndermiteEntity {
148    fn is_hard_collision_relevant(&self) -> bool {
149        false
150    }
151
152    fn as_default_entity_push(&self) -> Option<&dyn DefaultEntityPush> {
153        Some(self)
154    }
155
156    fn base(&self) -> &EntityBase {
157        &self.base
158    }
159
160    fn entity_type(&self) -> EntityTypeRef {
161        self.entity_type
162    }
163
164    fn movement_emission(&self) -> EntityMovementEmission {
165        EntityMovementEmission::Events
166    }
167
168    fn base_tick(&self) {
169        Mob::base_tick_mob(self);
170    }
171
172    fn dimensions_for_pose(&self, _pose: EntityPose) -> EntityDimensions {
173        let scale = LivingEntity::get_scale(self);
174        if self.entity_type.fixed {
175            self.entity_type.dimensions
176        } else {
177            self.entity_type.dimensions.scale(scale)
178        }
179    }
180
181    fn synced_data(&self) -> Option<&dyn EntitySyncedData> {
182        Some(&self.entity_data)
183    }
184
185    fn update_data_before_sync(&self) {
186        self.update_dirty_mob_effect_entity_data();
187    }
188
189    fn max_up_step(&self) -> f32 {
190        self.attributes()
191            .lock()
192            .get_value(vanilla_attributes::STEP_HEIGHT)
193            .unwrap_or(f64::from(DEFAULT_STEP_HEIGHT)) as f32
194    }
195
196    fn sound_source(&self) -> SoundSource {
197        Monster::sound_source_monster(self)
198    }
199
200    fn fall_sounds(&self) -> (SoundEventRef, SoundEventRef) {
201        Monster::fall_sounds_monster(self)
202    }
203
204    fn swim_sound(&self) -> SoundEventRef {
205        Monster::swim_sound_monster(self)
206    }
207
208    fn swim_splash_sound(&self) -> SoundEventRef {
209        Monster::swim_splash_sound_monster(self)
210    }
211
212    fn play_step_sound(&self, _pos: BlockPos, _block_state: BlockStateId) {
213        self.play_sound(&sound_events::ENTITY_ENDERMITE_STEP, 0.15, 1.0);
214    }
215
216    fn save_additional(&self, nbt: &mut NbtCompound) {
217        self.save_mob(nbt);
218        nbt.insert("Lifetime", *self.lifetime.lock());
219        nbt.insert("PlayerSpawned", i8::from(*self.player_spawned.lock()));
220    }
221
222    fn load_additional(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
223        self.load_mob(nbt);
224        *self.lifetime.lock() = nbt.int_value("Lifetime").unwrap_or(0);
225        *self.player_spawned.lock() = nbt.byte_value("PlayerSpawned").is_some_and(|b| b != 0);
226    }
227}
228
229impl LivingEntity for EndermiteEntity {
230    fn living_base(&self) -> &LivingEntityBase {
231        &self.living_base
232    }
233
234    fn get_health(&self) -> f32 {
235        *self.entity_data.lock().living_entity().health.get()
236    }
237
238    fn set_health(&self, health: f32) {
239        let max_health = self.get_max_health();
240        let clamped = health.clamp(0.0, max_health);
241        self.entity_data
242            .lock()
243            .living_entity_mut()
244            .health
245            .set(clamped);
246    }
247
248    fn sound_volume(&self) -> f32 {
249        0.4
250    }
251
252    fn hurt_sound(&self, _source: &DamageSource) -> Option<SoundEventRef> {
253        Some(&sound_events::ENTITY_ENDERMITE_HURT)
254    }
255
256    fn death_sound(&self) -> Option<SoundEventRef> {
257        Some(&sound_events::ENTITY_ENDERMITE_DEATH)
258    }
259
260    fn should_drop_experience(&self) -> bool {
261        Monster::should_drop_experience_monster(self)
262    }
263
264    fn should_drop_loot(&self, world: &World) -> bool {
265        Monster::should_drop_loot_monster(self, world)
266    }
267
268    fn server_ai_step(&self, entity: &SharedEntity) {
269        Mob::mob_server_ai_step(self, entity);
270    }
271
272    fn ai_step(&self, entity: &SharedEntity) -> Option<MoveResult> {
273        let result = Monster::monster_ai_step(self, entity);
274        // Persistence pauses aging but does not bypass the lifetime cap.
275        if self.level().is_some() {
276            let discard = {
277                let mut lifetime = self.lifetime.lock();
278                if !self.is_persistence_required() {
279                    *lifetime += 1;
280                }
281                *lifetime >= MAX_LIFETIME
282            };
283            if discard {
284                self.set_removed(RemovalReason::Discarded);
285            }
286        }
287        result
288    }
289}
290
291impl Mob for EndermiteEntity {
292    fn mob_base(&self) -> &MobBase {
293        &self.mob_base
294    }
295
296    fn tick_goal_selectors(&self, entity: &SharedEntity) {
297        PathfinderMob::tick_pathfinder_goal_selectors(self, entity);
298    }
299
300    fn tick_path_navigation(&self) {
301        PathfinderMob::tick_pathfinder_path_navigation(self);
302    }
303
304    fn ambient_sound(&self) -> Option<SoundEventRef> {
305        Some(&sound_events::ENTITY_ENDERMITE_AMBIENT)
306    }
307
308    fn mob_flags(&self) -> i8 {
309        *self.entity_data.lock().mob().mob_flags.get()
310    }
311
312    fn set_mob_flags(&self, flags: i8) {
313        self.entity_data.lock().mob_mut().mob_flags.set(flags);
314    }
315}
316
317impl Monster for EndermiteEntity {}
318
319impl PathfinderMob for EndermiteEntity {}
320
321#[cfg(test)]
322mod tests {
323    use std::io::Cursor;
324    use std::sync::{Arc, Weak};
325
326    use glam::DVec3;
327    use simdnbt::borrow::read_compound;
328    use simdnbt::owned::NbtCompound;
329    use steel_registry::{init_vanilla_registry, vanilla_entities};
330    use steel_utils::{ChunkPos, Downcast as _};
331
332    use super::{EndermiteEntity, MAX_LIFETIME};
333    use crate::entity::{Entity, LivingEntity, Mob, SharedEntity, next_entity_id};
334    use crate::test_support::{fresh_test_world, insert_ready_full_chunk};
335
336    #[test]
337    fn endermite_discards_after_max_lifetime_unless_persistence_required() {
338        init_vanilla_registry();
339        let world_fixture = fresh_test_world("endermite_lifetime");
340        let world = &world_fixture.world;
341        insert_ready_full_chunk(world, ChunkPos::new(0, 0));
342
343        let endermite: SharedEntity = Arc::new(EndermiteEntity::new(
344            &vanilla_entities::ENDERMITE,
345            next_entity_id(),
346            DVec3::new(8.0, 65.0, 8.0),
347            Arc::downgrade(world),
348        ));
349        world
350            .try_add_entity(Arc::clone(&endermite))
351            .expect("test endermite should attach to the loaded chunk");
352        let endermite_entity = endermite;
353        let endermite = endermite_entity
354            .downcast_ref::<EndermiteEntity>()
355            .expect("endermite is concrete");
356        endermite.set_lifetime(MAX_LIFETIME - 2);
357        endermite.ai_step(&endermite_entity);
358        assert!(
359            !endermite.is_removed(),
360            "lifetime below the cap keeps the mob alive"
361        );
362        endermite.ai_step(&endermite_entity);
363        assert!(endermite.is_removed(), "reaching the cap discards the mob");
364
365        let persistent: SharedEntity = Arc::new(EndermiteEntity::new(
366            &vanilla_entities::ENDERMITE,
367            next_entity_id(),
368            DVec3::new(9.0, 65.0, 8.0),
369            Arc::downgrade(world),
370        ));
371        world
372            .try_add_entity(Arc::clone(&persistent))
373            .expect("test endermite should attach to the loaded chunk");
374        let persistent_entity = persistent;
375        let persistent = persistent_entity
376            .downcast_ref::<EndermiteEntity>()
377            .expect("endermite is concrete");
378        persistent.set_persistence_required();
379        persistent.set_lifetime(MAX_LIFETIME - 1);
380        persistent.ai_step(&persistent_entity);
381        assert!(
382            !persistent.is_removed(),
383            "persistence-required endermites must not expire"
384        );
385    }
386
387    #[test]
388    fn endermite_nbt_round_trip() {
389        init_vanilla_registry();
390
391        let endermite =
392            EndermiteEntity::new(&vanilla_entities::ENDERMITE, 1, DVec3::ZERO, Weak::new());
393
394        assert!(!endermite.player_spawned());
395        assert_eq!(endermite.lifetime(), 0);
396
397        endermite.set_player_spawned(true);
398        endermite.set_lifetime(1234);
399        assert!(endermite.player_spawned());
400        assert_eq!(endermite.lifetime(), 1234);
401
402        let mut nbt = NbtCompound::new();
403        endermite.save_additional(&mut nbt);
404
405        assert_eq!(nbt.int("Lifetime"), Some(1234));
406        assert_eq!(nbt.byte("PlayerSpawned"), Some(1));
407
408        let loaded =
409            EndermiteEntity::new(&vanilla_entities::ENDERMITE, 2, DVec3::ZERO, Weak::new());
410        let mut bytes = Vec::new();
411        nbt.write(&mut bytes);
412        let borrowed = read_compound(&mut Cursor::new(&bytes))
413            .unwrap_or_else(|error| panic!("reborrow failed: {error}"));
414        loaded.load_additional((&borrowed).into());
415
416        assert!(loaded.player_spawned());
417        assert_eq!(loaded.lifetime(), 1234);
418    }
419}