steel_core/entity/entities/mobs/ambient/
bat.rs1use std::f32::consts::PI;
4use std::sync::{Arc, Weak};
5
6use glam::DVec3;
7use rand::{Rng, RngExt as _};
8use simdnbt::borrow::NbtCompound as BorrowedNbtCompoundView;
9use simdnbt::owned::NbtCompound;
10use steel_macros::entity_behavior;
11use steel_math::wrap_degrees;
12use steel_registry::blocks::block_state_ext::BlockStateExt as _;
13use steel_registry::entity_type::{EntityDimensions, EntityTypeRef};
14use steel_registry::sound_event::SoundEventRef;
15use steel_registry::vanilla_block_tags::BlockTag;
16use steel_registry::vanilla_entity_data::BatEntityData;
17use steel_registry::{level_events, sound_events, vanilla_attributes};
18use steel_utils::locks::SyncMutex;
19use steel_utils::nbt::NbtValueInput as _;
20use steel_utils::{BlockPos, BlockStateId, DowncastType, DowncastTypeKey};
21
22use crate::chunk::heightmap::HeightmapType;
23use crate::entity::ai::targeting::TargetingConditions;
24use crate::entity::damage::DamageSource;
25use crate::entity::{
26 DefaultEntityPush, Entity, EntityBase, EntityBaseLoad, EntityMovementEmission, EntityPose,
27 EntitySpawnReason, EntitySyncedData, LivingEntity, LivingEntityBase, LivingTravelInput, Mob,
28 MobBase, SharedEntity, SpawnRule,
29};
30use crate::physics::MoveResult;
31use crate::world::{LevelReader, World, is_redstone_conductor};
32
33const FLAG_RESTING: i8 = 1;
34const BAT_RESTING_TARGETING_RANGE: f64 = 4.0;
35const DEFAULT_STEP_HEIGHT: f32 = 0.6;
36
37const RESTING_YAW_ROLL_CHANCE: i32 = 200;
39const RESTING_YAW_RANGE: i32 = 360;
40const RESTING_RETARGET_CHANCE: i32 = 30;
41const RESTING_SETTLE_CHANCE: i32 = 100;
42
43const TARGET_HORIZONTAL_RANGE: i32 = 7;
45const TARGET_VERTICAL_RANGE: i32 = 6;
46const TARGET_VERTICAL_OFFSET: f64 = 2.0;
47const TARGET_RECENTER_DISTANCE: f64 = 2.0;
48
49const STEER_TARGET_XZ: f64 = 0.5;
51const STEER_TARGET_Y: f32 = 0.7;
52const STEER_STEP: f32 = 0.1;
53const FLIGHT_FORWARD_INPUT: f32 = 0.5;
55
56const AIRBORNE_Y_DRAG: f64 = 0.6;
57
58#[entity_behavior(class = "Bat", spawn_rule)]
60pub struct BatEntity {
61 base: EntityBase,
62 entity_type: EntityTypeRef,
63 living_base: LivingEntityBase,
64 mob_base: MobBase,
65 entity_data: SyncMutex<BatEntityData>,
66 target_position: SyncMutex<Option<BlockPos>>,
67}
68
69unsafe impl DowncastType for BatEntity {
71 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/bat");
72}
73
74impl BatEntity {
75 pub(crate) const SPAWN_RULE: SpawnRule = Self::check_bat_spawn_rules;
76
77 pub(crate) fn check_bat_spawn_rules(
79 entity_type: EntityTypeRef,
80 level: &dyn LevelReader,
81 spawn_reason: EntitySpawnReason,
82 pos: BlockPos,
83 random: &mut dyn Rng,
84 ) -> bool {
85 if pos.y() >= level.height_at(HeightmapType::WorldSurface, pos.x(), pos.z())
86 || random.random::<bool>()
87 || i32::from(level.max_local_raw_brightness(pos, level.sky_darkening()))
88 > random.random_range(0..4)
89 {
90 return false;
91 }
92 level
93 .get_block_state(pos.below())
94 .get_block()
95 .has_tag(&BlockTag::BATS_SPAWNABLE_ON)
96 && <Self as Mob>::check_mob_spawn_rules(entity_type, level, spawn_reason, pos)
97 }
98
99 #[must_use]
101 pub fn new(entity_type: EntityTypeRef, id: i32, position: DVec3, world: Weak<World>) -> Self {
102 Self::new_with_base(
103 EntityBase::new(id, position, entity_type.dimensions, world),
104 entity_type,
105 )
106 }
107
108 #[must_use]
110 pub fn from_saved(entity_type: EntityTypeRef, load: EntityBaseLoad) -> Self {
111 Self::new_with_base(
112 EntityBase::from_load(load, entity_type.dimensions),
113 entity_type,
114 )
115 }
116
117 fn new_with_base(base: EntityBase, entity_type: EntityTypeRef) -> Self {
118 let living_base = LivingEntityBase::new(entity_type);
119 let mob_base = MobBase::new();
120 let mut entity_data = BatEntityData::new();
121 living_base.initialize_synced_data(&mut entity_data);
122 entity_data.id_flags.set(FLAG_RESTING);
123
124 Self {
125 base,
126 entity_type,
127 living_base,
128 mob_base,
129 entity_data: SyncMutex::new(entity_data),
130 target_position: SyncMutex::new(None),
131 }
132 }
133
134 #[must_use]
136 pub fn is_resting(&self) -> bool {
137 (self.entity_data.lock().id_flags.get() & FLAG_RESTING) != 0
138 }
139
140 pub fn set_resting(&self, resting: bool) {
142 let mut data = self.entity_data.lock();
143 let current = *data.id_flags.get();
144 let updated = if resting {
145 current | FLAG_RESTING
146 } else {
147 current & !FLAG_RESTING };
149 data.id_flags.set(updated);
150 }
151
152 fn update_dirty_mob_effect_entity_data(&self) {
153 if !self.living_base.take_effects_dirty() {
154 return;
155 }
156
157 let display = self.living_base.mob_effect_display_state();
158
159 {
160 let mut entity_data = self.entity_data.lock();
161 let living = entity_data.living_entity_mut();
162 living.effect_particles.set(display.particles);
163 living.effect_ambience.set(display.ambient);
164 }
165
166 self.entity_data.set_base_invisible_flag(display.invisible);
167 }
168
169 fn tick_resting_ai(&self, world: &World, pos: BlockPos, above: BlockPos) {
171 let silent = self.is_silent();
172 if is_redstone_conductor(world, world.get_block_state(above), pos) {
173 if rand::random_range(0..RESTING_YAW_ROLL_CHANCE) == 0 {
174 self.set_y_head_rot(rand::random_range(0..RESTING_YAW_RANGE) as f32);
175 }
176
177 let conditions =
178 TargetingConditions::for_non_combat().range(BAT_RESTING_TARGETING_RANGE);
179 let targeter: &dyn LivingEntity = self;
180 let player_near = world
181 .nearest_player(self.position(), BAT_RESTING_TARGETING_RANGE, |player| {
182 conditions.test(world, Some(targeter), player)
183 })
184 .is_some();
185 if !player_near {
186 return;
187 }
188 }
189
190 self.set_resting(false);
191 if !silent {
192 world.level_event(level_events::SOUND_BAT_LIFTOFF, pos, 0, None);
193 }
194 }
195
196 fn tick_flying_ai(&self, world: &World, above: BlockPos) {
198 let mut target_position = *self.target_position.lock();
199 if target_position.is_some_and(|target| {
200 !world.get_block_state(target).is_air() || target.y() <= world.get_min_y()
201 }) {
202 target_position = None;
203 }
204
205 let needs_new_target = target_position.is_none()
206 || rand::random_range(0..RESTING_RETARGET_CHANCE) == 0
207 || target_position.is_some_and(|target| {
208 target.closer_to_center_than(self.position(), TARGET_RECENTER_DISTANCE)
209 });
210 if needs_new_target {
211 target_position = Some(self.random_target_position());
212 }
213 *self.target_position.lock() = target_position;
214
215 let Some(target) = target_position else {
216 return;
217 };
218
219 let position = self.position();
220 let dx = f64::from(target.x()) + 0.5 - position.x;
221 let dy = f64::from(target.y()) + 0.1 - position.y;
222 let dz = f64::from(target.z()) + 0.5 - position.z;
223
224 let movement = self.velocity();
225 let steer_step = f64::from(STEER_STEP);
226 let new_movement = movement
227 + DVec3::new(
228 (dx.signum() * STEER_TARGET_XZ - movement.x) * steer_step,
229 (dy.signum() * f64::from(STEER_TARGET_Y) - movement.y) * steer_step,
230 (dz.signum() * STEER_TARGET_XZ - movement.z) * steer_step,
231 );
232 self.set_velocity(new_movement);
233
234 let y_rot_d = (new_movement.z.atan2(new_movement.x) * 180.0 / f64::from(PI)) as f32 - 90.0;
235 let (yaw, pitch) = self.rotation();
236 let rot_diff = wrap_degrees(y_rot_d - yaw);
237
238 let input = self.travel_input();
242 self.set_travel_input(LivingTravelInput::new(
243 input.sideways(),
244 input.vertical(),
245 FLIGHT_FORWARD_INPUT,
246 ));
247
248 self.set_rotation((yaw + rot_diff, pitch));
249
250 if rand::random_range(0..RESTING_SETTLE_CHANCE) == 0
251 && is_redstone_conductor(world, world.get_block_state(above), above)
252 {
253 self.set_resting(true);
254 }
255 }
256
257 fn random_target_position(&self) -> BlockPos {
259 let position = self.position();
260 BlockPos::containing(
261 position.x
262 + f64::from(
263 rand::random_range(0..TARGET_HORIZONTAL_RANGE)
264 - rand::random_range(0..TARGET_HORIZONTAL_RANGE),
265 ),
266 position.y + f64::from(rand::random_range(0..TARGET_VERTICAL_RANGE))
267 - TARGET_VERTICAL_OFFSET,
268 position.z
269 + f64::from(
270 rand::random_range(0..TARGET_HORIZONTAL_RANGE)
271 - rand::random_range(0..TARGET_HORIZONTAL_RANGE),
272 ),
273 )
274 }
275}
276
277impl DefaultEntityPush for BatEntity {}
278
279impl Entity for BatEntity {
280 fn is_hard_collision_relevant(&self) -> bool {
281 false
282 }
283
284 fn as_default_entity_push(&self) -> Option<&dyn DefaultEntityPush> {
285 Some(self)
286 }
287
288 fn base(&self) -> &EntityBase {
289 &self.base
290 }
291
292 fn entity_type(&self) -> EntityTypeRef {
293 self.entity_type
294 }
295
296 fn base_tick(&self) {
297 Mob::base_tick_mob(self);
298 }
299
300 fn tick(self: Arc<Self>) {
301 let entity: SharedEntity = Arc::<Self>::clone(&self);
302 self.tick_living_entity(&entity);
303
304 if self.is_resting() {
305 self.set_velocity(DVec3::ZERO);
306 let height = f64::from(self.base().dimensions().height);
307 let current = self.position();
308 let snapped = DVec3::new(current.x, current.y.floor() + 1.0 - height, current.z);
309 if snapped != current
310 && let Err(error) = self.try_set_position(snapped)
311 {
312 tracing::debug!(
313 entity_id = self.id(),
314 "bat resting position update failed: {error}"
315 );
316 }
317 } else {
318 let velocity = self.velocity();
319 self.set_velocity(DVec3::new(
320 velocity.x,
321 velocity.y * AIRBORNE_Y_DRAG,
322 velocity.z,
323 ));
324 }
325 }
326
327 fn dimensions_for_pose(&self, _pose: EntityPose) -> EntityDimensions {
328 let scale = LivingEntity::get_scale(self);
329 if self.entity_type.fixed {
330 self.entity_type.dimensions
331 } else {
332 self.entity_type.dimensions.scale(scale)
333 }
334 }
335
336 fn synced_data(&self) -> Option<&dyn EntitySyncedData> {
337 Some(&self.entity_data)
338 }
339
340 fn update_data_before_sync(&self) {
341 self.update_dirty_mob_effect_entity_data();
342 }
343
344 fn max_up_step(&self) -> f32 {
345 self.attributes()
346 .lock()
347 .get_value(vanilla_attributes::STEP_HEIGHT)
348 .unwrap_or(f64::from(DEFAULT_STEP_HEIGHT)) as f32
349 }
350
351 fn is_pushable(&self) -> bool {
352 false
353 }
354
355 fn is_ignoring_block_triggers(&self) -> bool {
356 true
357 }
358
359 fn movement_emission(&self) -> EntityMovementEmission {
360 EntityMovementEmission::Events
361 }
362
363 fn check_fall_damage(
364 self: Arc<Self>,
365 _vertical_movement: f64,
366 _on_ground: bool,
367 _on_state: BlockStateId,
368 _pos: BlockPos,
369 _world: &Arc<World>,
370 ) {
371 }
372
373 fn is_flapping(&self) -> bool {
374 !self.is_resting() && self.tick_count() % 10 == 0
375 }
376
377 fn hurt(&self, world: &World, source: &DamageSource, amount: f32) -> bool {
378 if !self.is_invulnerable_to(world, source) && self.is_resting() {
379 self.set_resting(false);
380 }
381 LivingEntity::hurt_server(self, world, source, amount)
382 }
383
384 fn save_additional(&self, nbt: &mut NbtCompound) {
385 self.save_mob(nbt);
386 nbt.insert("BatFlags", *self.entity_data.lock().id_flags.get());
387 }
388
389 fn load_additional(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
390 self.load_mob(nbt);
391 self.entity_data
392 .lock()
393 .id_flags
394 .set(nbt.byte_value("BatFlags").unwrap_or(0));
395 }
396}
397
398impl LivingEntity for BatEntity {
399 fn living_base(&self) -> &LivingEntityBase {
400 &self.living_base
401 }
402
403 fn get_health(&self) -> f32 {
404 *self.entity_data.lock().living_entity().health.get()
405 }
406
407 fn set_health(&self, health: f32) {
408 let max_health = self.get_max_health();
409 let clamped = health.clamp(0.0, max_health);
410 self.entity_data
411 .lock()
412 .living_entity_mut()
413 .health
414 .set(clamped);
415 }
416
417 fn sound_volume(&self) -> f32 {
418 0.1
419 }
420
421 fn voice_pitch(&self) -> f32 {
422 self.default_voice_pitch() * 0.95
423 }
424
425 fn hurt_sound(&self, _source: &DamageSource) -> Option<SoundEventRef> {
426 Some(&sound_events::ENTITY_BAT_HURT)
427 }
428
429 fn death_sound(&self) -> Option<SoundEventRef> {
430 Some(&sound_events::ENTITY_BAT_DEATH)
431 }
432
433 fn push_entities(&self) {}
434
435 fn server_ai_step(&self, entity: &SharedEntity) {
436 Mob::mob_server_ai_step(self, entity);
437 }
438
439 fn ai_step(&self, entity: &SharedEntity) -> Option<MoveResult> {
440 Mob::mob_ai_step(self, entity)
441 }
442}
443
444impl Mob for BatEntity {
445 fn mob_base(&self) -> &MobBase {
446 &self.mob_base
447 }
448
449 fn mob_can_be_leashed(&self) -> bool {
450 false
451 }
452
453 fn custom_server_ai_step(&self) {
454 let Some(world) = self.level() else {
455 return;
456 };
457 let pos = self.block_position();
458 let above = pos.above();
459 if self.is_resting() {
460 self.tick_resting_ai(&world, pos, above);
461 } else {
462 self.tick_flying_ai(&world, above);
463 }
464 }
465
466 fn ambient_sound(&self) -> Option<SoundEventRef> {
467 if self.is_resting() && rand::random_range(0..4) != 0 {
468 None
469 } else {
470 Some(&sound_events::ENTITY_BAT_AMBIENT)
471 }
472 }
473
474 fn mob_flags(&self) -> i8 {
475 *self.entity_data.lock().mob().mob_flags.get()
476 }
477
478 fn set_mob_flags(&self, flags: i8) {
479 self.entity_data.lock().mob_mut().mob_flags.set(flags);
480 }
481}
482
483#[cfg(test)]
484mod tests {
485 use super::BatEntity;
486 use crate::entity::{Entity, leash::Leashable};
487 use glam::DVec3;
488 use simdnbt::borrow::read_compound;
489 use simdnbt::owned::NbtCompound;
490 use std::io::Cursor;
491 use std::sync::Weak;
492 use steel_registry::{init_vanilla_registry, vanilla_entities};
493
494 #[test]
495 fn bat_starts_resting_and_round_trips_flags() {
496 init_vanilla_registry();
497
498 let bat = BatEntity::new(&vanilla_entities::BAT, 1, DVec3::ZERO, Weak::new());
499 assert!(bat.is_resting());
500 assert!(!bat.can_be_leashed());
501
502 bat.set_resting(false);
503 assert!(!bat.is_resting());
504
505 let mut nbt = NbtCompound::new();
506 bat.save_additional(&mut nbt);
507 assert_eq!(nbt.byte("BatFlags"), Some(0));
508
509 let loaded = BatEntity::new(&vanilla_entities::BAT, 2, DVec3::ZERO, Weak::new());
510 let mut bytes = Vec::new();
511 nbt.write(&mut bytes);
512 let borrowed = read_compound(&mut Cursor::new(&bytes))
513 .unwrap_or_else(|error| panic!("reborrow failed: {error}"));
514 loaded.load_additional((&borrowed).into());
515 assert!(!loaded.is_resting());
516 }
517
518 #[test]
519 fn bat_is_flapping_on_the_vanilla_tick_cadence() {
520 init_vanilla_registry();
521
522 let bat = BatEntity::new(&vanilla_entities::BAT, 1, DVec3::ZERO, Weak::new());
523 assert!(!bat.is_flapping(), "resting bats never flap");
524
525 bat.set_resting(false);
526 assert!(bat.is_flapping(), "tick count 0 is a multiple of 10");
527
528 bat.advance_tick_count();
529 assert!(!bat.is_flapping());
530 }
531}