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 simdnbt::borrow::NbtCompound as BorrowedNbtCompoundView;
6use simdnbt::owned::NbtCompound;
7use steel_macros::entity_behavior;
8use steel_protocol::packets::game::SoundSource;
9use steel_registry::entity_type::{EntityDimensions, EntityTypeRef};
10use steel_registry::sound_event::SoundEventRef;
11use steel_registry::vanilla_entity_data::EndermiteEntityData;
12use steel_registry::{sound_events, vanilla_attributes};
13use steel_utils::locks::SyncMutex;
14use steel_utils::{BlockPos, BlockStateId, DowncastType, DowncastTypeKey};
15
16use crate::entity::ai::goal::{
17    ClimbOnTopOfPowderSnowGoal, FloatGoal, HurtByTargetGoal, LookAtPlayerGoal, MeleeAttackGoal,
18    NearestAttackableTargetGoal, RandomLookAroundGoal, WaterAvoidingRandomStrollGoal,
19};
20use crate::entity::damage::DamageSource;
21use crate::entity::{
22    Entity, EntityBase, EntityBaseLoad, EntityPose, EntitySyncedData, LivingEntity,
23    LivingEntityBase, Mob, MobBase, PathfinderMob, RemovalReason,
24};
25use crate::physics::MoveResult;
26use crate::world::World;
27
28const DEFAULT_STEP_HEIGHT: f32 = 0.6;
29const MAX_LIFETIME: i32 = 2400;
30
31/// A hostile endermite entity.
32#[entity_behavior(class = "Endermite")]
33pub struct EndermiteEntity {
34    base: EntityBase,
35    entity_type: EntityTypeRef,
36    living_base: LivingEntityBase,
37    mob_base: MobBase,
38    entity_data: SyncMutex<EndermiteEntityData>,
39    lifetime: SyncMutex<i32>,
40    player_spawned: SyncMutex<bool>,
41}
42
43// SAFETY: The owner-scoped type key uniquely identifies EndermiteEntity.
44unsafe impl DowncastType for EndermiteEntity {
45    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/endermite");
46}
47
48impl EndermiteEntity {
49    /// Creates a new endermite entity instance.
50    #[must_use]
51    pub fn new(entity_type: EntityTypeRef, id: i32, position: DVec3, world: Weak<World>) -> Self {
52        Self::new_with_base(
53            EntityBase::new(id, position, entity_type.dimensions, world),
54            entity_type,
55        )
56    }
57
58    /// Loads a saved endermite entity.
59    #[must_use]
60    pub fn from_saved(entity_type: EntityTypeRef, load: EntityBaseLoad) -> Self {
61        Self::new_with_base(
62            EntityBase::from_load(load, entity_type.dimensions),
63            entity_type,
64        )
65    }
66
67    fn new_with_base(base: EntityBase, entity_type: EntityTypeRef) -> Self {
68        let living_base = LivingEntityBase::new(entity_type);
69        let mob_base = MobBase::new();
70        let mut entity_data = EndermiteEntityData::new();
71        living_base.initialize_synced_data(&mut entity_data);
72
73        {
74            let mut goal_selector = mob_base.goal_selector().lock();
75            goal_selector.add_goal(1, FloatGoal::new(&mob_base));
76            goal_selector.add_goal(1, ClimbOnTopOfPowderSnowGoal::new());
77            goal_selector.add_goal(2, MeleeAttackGoal::new(1.0, false));
78            goal_selector.add_goal(3, WaterAvoidingRandomStrollGoal::new(1.0));
79            goal_selector.add_goal(7, LookAtPlayerGoal::new(8.0));
80            goal_selector.add_goal(8, RandomLookAroundGoal::new());
81
82            let mut target_selector = mob_base.target_selector().lock();
83            target_selector.add_goal(1, HurtByTargetGoal::new().set_alert_others(empty()));
84            target_selector.add_goal(
85                2,
86                NearestAttackableTargetGoal::new_for_players(true, |_, _| true),
87            );
88        }
89
90        Self {
91            base,
92            entity_type,
93            living_base,
94            mob_base,
95            entity_data: SyncMutex::new(entity_data),
96            lifetime: SyncMutex::new(0),
97            player_spawned: SyncMutex::new(false),
98        }
99    }
100
101    /// Returns true if the endermite was spawned by a player.
102    pub fn player_spawned(&self) -> bool {
103        *self.player_spawned.lock()
104    }
105
106    /// Sets whether the endermite was spawned by a player.
107    pub fn set_player_spawned(&self, player_spawned: bool) {
108        *self.player_spawned.lock() = player_spawned;
109    }
110
111    /// Returns the endermite's lifetime in ticks.
112    pub fn lifetime(&self) -> i32 {
113        *self.lifetime.lock()
114    }
115
116    /// Sets the endermite's lifetime in ticks.
117    pub fn set_lifetime(&self, lifetime: i32) {
118        *self.lifetime.lock() = lifetime;
119    }
120
121    fn update_dirty_mob_effect_entity_data(&self) {
122        if !self.living_base.take_effects_dirty() {
123            return;
124        }
125
126        let display = self.living_base.mob_effect_display_state();
127
128        {
129            let mut entity_data = self.entity_data.lock();
130            let living = entity_data.living_entity_mut();
131            living.effect_particles.set(display.particles);
132            living.effect_ambience.set(display.ambient);
133        }
134
135        self.entity_data.set_base_invisible_flag(display.invisible);
136    }
137}
138
139impl Entity for EndermiteEntity {
140    fn base(&self) -> &EntityBase {
141        &self.base
142    }
143
144    fn entity_type(&self) -> EntityTypeRef {
145        self.entity_type
146    }
147
148    fn base_tick(&self) {
149        Mob::base_tick_mob(self);
150    }
151
152    fn dimensions_for_pose(&self, _pose: EntityPose) -> EntityDimensions {
153        let scale = LivingEntity::get_scale(self);
154        if self.entity_type.fixed {
155            self.entity_type.dimensions
156        } else {
157            self.entity_type.dimensions.scale(scale)
158        }
159    }
160
161    fn synced_data(&self) -> Option<&dyn EntitySyncedData> {
162        Some(&self.entity_data)
163    }
164
165    fn update_data_before_sync(&self) {
166        self.update_dirty_mob_effect_entity_data();
167    }
168
169    fn max_up_step(&self) -> f32 {
170        self.attributes()
171            .lock()
172            .get_value(vanilla_attributes::STEP_HEIGHT)
173            .unwrap_or(f64::from(DEFAULT_STEP_HEIGHT)) as f32
174    }
175
176    fn sound_source(&self) -> SoundSource {
177        SoundSource::Hostile
178    }
179
180    fn play_step_sound(&self, _pos: BlockPos, _block_state: BlockStateId) {
181        self.play_sound(&sound_events::ENTITY_ENDERMITE_STEP, 0.15, 1.0);
182    }
183
184    fn save_additional(&self, nbt: &mut NbtCompound) {
185        self.save_mob(nbt);
186        nbt.insert("Lifetime", *self.lifetime.lock());
187        nbt.insert("PlayerSpawned", i8::from(*self.player_spawned.lock()));
188    }
189
190    fn load_additional(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
191        self.load_mob(nbt);
192        *self.lifetime.lock() = nbt.int("Lifetime").unwrap_or(0);
193        *self.player_spawned.lock() = nbt.byte("PlayerSpawned").is_some_and(|b| b != 0);
194    }
195}
196
197impl LivingEntity for EndermiteEntity {
198    fn living_base(&self) -> &LivingEntityBase {
199        &self.living_base
200    }
201
202    fn get_health(&self) -> f32 {
203        *self.entity_data.lock().living_entity().health.get()
204    }
205
206    fn set_health(&self, health: f32) {
207        let max_health = self.get_max_health();
208        let clamped = health.clamp(0.0, max_health);
209        self.entity_data
210            .lock()
211            .living_entity_mut()
212            .health
213            .set(clamped);
214    }
215
216    fn sound_volume(&self) -> f32 {
217        0.4
218    }
219
220    fn hurt_sound(&self, _source: &DamageSource) -> Option<SoundEventRef> {
221        Some(&sound_events::ENTITY_ENDERMITE_HURT)
222    }
223
224    fn death_sound(&self) -> Option<SoundEventRef> {
225        Some(&sound_events::ENTITY_ENDERMITE_DEATH)
226    }
227
228    fn server_ai_step(&self) {
229        Mob::mob_server_ai_step(self);
230    }
231
232    fn ai_step(&self) -> Option<MoveResult> {
233        let result = self.default_ai_step();
234        if self.level().is_some() && !self.is_persistence_required() {
235            let mut lifetime = self.lifetime.lock();
236            *lifetime += 1;
237            if *lifetime >= MAX_LIFETIME {
238                self.set_removed(RemovalReason::Discarded);
239            }
240        }
241        result
242    }
243}
244
245impl Mob for EndermiteEntity {
246    fn mob_base(&self) -> &MobBase {
247        &self.mob_base
248    }
249
250    fn tick_goal_selectors(&self) {
251        PathfinderMob::tick_pathfinder_goal_selectors(self);
252    }
253
254    fn tick_path_navigation(&self) {
255        PathfinderMob::tick_pathfinder_path_navigation(self);
256    }
257
258    fn ambient_sound(&self) -> Option<SoundEventRef> {
259        Some(&sound_events::ENTITY_ENDERMITE_AMBIENT)
260    }
261
262    fn mob_flags(&self) -> i8 {
263        *self.entity_data.lock().mob().mob_flags.get()
264    }
265
266    fn set_mob_flags(&self, flags: i8) {
267        self.entity_data.lock().mob_mut().mob_flags.set(flags);
268    }
269}
270
271impl PathfinderMob for EndermiteEntity {}
272
273#[cfg(test)]
274mod tests {
275    use super::EndermiteEntity;
276    use crate::entity::Entity;
277    use glam::DVec3;
278    use simdnbt::borrow::read_compound;
279    use simdnbt::owned::NbtCompound;
280    use std::io::Cursor;
281    use std::sync::Weak;
282    use steel_registry::{init_vanilla_registry, vanilla_entities};
283
284    #[test]
285    fn endermite_nbt_round_trip() {
286        init_vanilla_registry();
287
288        let endermite =
289            EndermiteEntity::new(&vanilla_entities::ENDERMITE, 1, DVec3::ZERO, Weak::new());
290
291        assert!(!endermite.player_spawned());
292        assert_eq!(endermite.lifetime(), 0);
293
294        endermite.set_player_spawned(true);
295        endermite.set_lifetime(1234);
296        assert!(endermite.player_spawned());
297        assert_eq!(endermite.lifetime(), 1234);
298
299        let mut nbt = NbtCompound::new();
300        endermite.save_additional(&mut nbt);
301
302        assert_eq!(nbt.int("Lifetime"), Some(1234));
303        assert_eq!(nbt.byte("PlayerSpawned"), Some(1));
304
305        let loaded =
306            EndermiteEntity::new(&vanilla_entities::ENDERMITE, 2, DVec3::ZERO, Weak::new());
307        let mut bytes = Vec::new();
308        nbt.write(&mut bytes);
309        let borrowed = read_compound(&mut Cursor::new(&bytes))
310            .unwrap_or_else(|error| panic!("reborrow failed: {error}"));
311        loaded.load_additional((&borrowed).into());
312
313        assert!(loaded.player_spawned());
314        assert_eq!(loaded.lifetime(), 1234);
315    }
316}