steel_core/entity/entities/mobs/hostile/
endermite.rs1use 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#[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
43unsafe impl DowncastType for EndermiteEntity {
45 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/endermite");
46}
47
48impl EndermiteEntity {
49 #[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 #[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 pub fn player_spawned(&self) -> bool {
103 *self.player_spawned.lock()
104 }
105
106 pub fn set_player_spawned(&self, player_spawned: bool) {
108 *self.player_spawned.lock() = player_spawned;
109 }
110
111 pub fn lifetime(&self) -> i32 {
113 *self.lifetime.lock()
114 }
115
116 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}