steel_core/entity/entities/mobs/hostile/
endermite.rs1use 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#[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
47unsafe impl DowncastType for EndermiteEntity {
49 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/endermite");
50}
51
52impl EndermiteEntity {
53 pub(crate) const SPAWN_RULE: SpawnRule = Self::check_endermite_spawn_rules;
55
56 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 #[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 #[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 pub fn player_spawned(&self) -> bool {
122 *self.player_spawned.lock()
123 }
124
125 pub fn set_player_spawned(&self, player_spawned: bool) {
127 *self.player_spawned.lock() = player_spawned;
128 }
129
130 pub fn lifetime(&self) -> i32 {
132 *self.lifetime.lock()
133 }
134
135 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 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}