Skip to main content

steel_core/entity/entities/mobs/passive/squid/
mod.rs

1use std::{
2    f64::consts::{PI, TAU},
3    sync::{Arc, Weak},
4};
5
6use glam::DVec3;
7use simdnbt::{borrow, owned};
8use steel_macros::entity_behavior;
9use steel_math::{angle::DEGREE_90, vector};
10use steel_registry::{
11    entity_data::{EntityPose, ParticleData},
12    entity_type::{EntityAttachments, EntityDimensions, EntityTypeRef},
13    sound_event::SoundEventRef,
14    sound_events, vanilla_attributes, vanilla_damage_types,
15    vanilla_entity_data::SquidEntityData,
16    vanilla_mob_effects::LEVITATION,
17    vanilla_particle_types,
18};
19use steel_utils::{
20    DowncastType, DowncastTypeKey, entity_events, locks::SyncMutex, types::InteractionHand,
21};
22
23use crate::{
24    behavior::InteractionResult,
25    entity::{
26        AgeableMob, AgeableMobBase, Entity, EntityBase, EntityBaseLoad, EntitySyncedData,
27        LivingEntity, LivingEntityBase, Mob, MobBase, PathfinderMob, SharedEntity,
28        ai::goal::SquidRandomMovementGoal,
29    },
30    physics::{MoveResult, MoverType},
31    world::World,
32};
33use crate::{
34    entity::{
35        EntityMovementEmission, EntitySpawnReason, SpawnGroupData,
36        ai::{goal::SquidFleeGoal, path::PathType},
37        damage::DamageSource,
38    },
39    player::Player,
40};
41
42const SQUID_DEFAULT_HEALTH: f64 = 10.0;
43
44const SQUID_BABY_WIDTH: f32 = 0.5;
45const SQUID_BABY_HEIGHT: f32 = 0.5;
46const SQUID_BABY_EYE_HEIGHT: f32 = 0.37;
47
48const SQUID_BABY_DIMENSIONS: EntityDimensions = EntityDimensions::new_with_attachments(
49    SQUID_BABY_WIDTH,
50    SQUID_BABY_HEIGHT,
51    SQUID_BABY_EYE_HEIGHT,
52    EntityAttachments::fallback(),
53);
54
55const SQUID_AIR_DRAG: f32 = 0.98;
56const SQUID_GRAVITY: f64 = 0.08;
57const SQUID_SOUND_VOLUME: f32 = 0.4;
58const INK_PARTICLE_COUNT: u32 = 30;
59const ADULT_POSITION_SCALE: f32 = 0.3;
60const BABY_POSITION_SCALE: f32 = 0.1;
61/// Const version of Vanilla `AgeableWaterCreature.getAmbientSoundInterval` function.
62const SQUID_AMBIENT_SOUND_INTERVAL: i32 = 120;
63
64const SQUID_SQUIRT_SOUND: SoundEventRef = &sound_events::ENTITY_SQUID_SQUIRT;
65const SQUID_MOVEMENT_THRESHOLD: f64 = 1.0e-5;
66const SQUID_TENTICLE_SCALE_MOVEMENT_THRESHOLD: f32 = 7.5;
67
68// TODO(ageable-water-creature): vanilla `Squid extends AgeableWaterCreature`.
69// Still missing from that layer: `checkSpawnObstruction`.
70// Functions `getAmbientSoundInterval` and `getBaseExperienceReward` sit on this
71// entity until the trait exists; `Dolphin` is the only other consumer.
72#[entity_behavior(class = "Squid")]
73/// Vanilla Squid entity
74pub struct SquidEntity {
75    base: EntityBase,
76    entity_type: EntityTypeRef,
77    living_base: LivingEntityBase,
78    mob_base: MobBase,
79    ageable_base: AgeableMobBase,
80    entity_data: SyncMutex<SquidEntityData>,
81
82    state: SyncMutex<SquidState>,
83}
84
85pub struct SquidState {
86    x_body_rot: f32,
87    // Required for inking on fleeing.
88    x_body_rot_old: f32,
89    movement_vector: DVec3,
90    tentacle_speed: f32,
91    tentacle_movement: f32,
92}
93
94// SAFETY: This key is owned by Steel and uniquely identifies `SquidEntity`.
95unsafe impl DowncastType for SquidEntity {
96    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/squid");
97}
98
99impl SquidEntity {
100    /// Creates a new Squid entity
101    #[must_use]
102    pub fn new(entity_type: EntityTypeRef, id: i32, position: DVec3, world: Weak<World>) -> Self {
103        Self::new_with_base(
104            EntityBase::new(id, position, entity_type.dimensions, world),
105            entity_type,
106        )
107    }
108
109    /// Checks if the squid has a movement vector.
110    pub fn has_movement_vector(&self) -> bool {
111        self.state.lock().movement_vector.length_squared() > SQUID_MOVEMENT_THRESHOLD
112    }
113
114    /// Sets the squid's movement vector
115    pub fn set_movement_vector(&self, new_vec: DVec3) {
116        self.state.lock().movement_vector = new_vec;
117    }
118
119    /// Returns the squid's movement vector
120    pub fn movement_vector(&self) -> DVec3 {
121        self.state.lock().movement_vector
122    }
123
124    /// Recreates a squid from saved entity state
125    #[must_use]
126    pub fn from_saved(entity_type: EntityTypeRef, load: EntityBaseLoad) -> Self {
127        Self::new_with_base(
128            EntityBase::from_load(load, entity_type.dimensions),
129            entity_type,
130        )
131    }
132
133    /// Vanilla `Squid.rotateVector`, squid-local space into world space.
134    fn rotate_vector(&self, vector: DVec3) -> DVec3 {
135        let x_rot = self.state.lock().x_body_rot_old.to_radians();
136        let y_rot = -self.living_rotation_state().y_body_rot_o().to_radians();
137
138        vector::y_rot(vector::x_rot(vector, x_rot), y_rot)
139    }
140
141    fn spawn_ink(&self) {
142        let Some(world) = self.level() else {
143            return;
144        };
145
146        self.make_sound(Some(self::SQUID_SQUIRT_SOUND));
147
148        let position = self.position() + self.rotate_vector(DVec3::new(0.0, -1.0, 0.0));
149        let particle_position = position + DVec3::new(0.0, 0.5, 0.0);
150        let particle = ParticleData::simple(&vanilla_particle_types::SQUID_INK);
151        let position_scale = if AgeableMob::is_baby(self) {
152            BABY_POSITION_SCALE
153        } else {
154            ADULT_POSITION_SCALE
155        };
156
157        for _ in 0..INK_PARTICLE_COUNT {
158            let direction = self.rotate_vector(DVec3::new(
159                rand::random_range(-0.3..0.3),
160                -1.0,
161                rand::random_range(-0.3..0.3),
162            ));
163            let offset = direction * f64::from(position_scale + rand::random::<f32>() * 2.0_f32);
164            world.send_particles(particle.clone(), particle_position, 0, offset, 0.1);
165        }
166    }
167
168    fn tick_squid_movement(&self) {
169        let (tentacle_movement, movement_vector, animation_sync) = {
170            let mut state = self.state.lock();
171
172            state.tentacle_movement += state.tentacle_speed;
173
174            let animation_sync = f64::from(state.tentacle_movement) > TAU;
175
176            if animation_sync {
177                state.tentacle_movement -= TAU as f32;
178
179                if rand::random_range(0..10) == 0 {
180                    state.tentacle_speed = 1.0 / (rand::random::<f32>() + 1.0) * 0.2;
181                }
182            }
183
184            (
185                state.tentacle_movement,
186                state.movement_vector,
187                animation_sync,
188            )
189        };
190
191        if animation_sync {
192            self.broadcast_entity_event(entity_events::EntityStatus::SquidAnimSynch);
193        }
194
195        if self.is_in_water() {
196            if tentacle_movement < PI as f32 {
197                let tentacle_scale = tentacle_movement / PI as f32;
198
199                if tentacle_scale > SQUID_TENTICLE_SCALE_MOVEMENT_THRESHOLD {
200                    self.set_velocity(DVec3::new(
201                        movement_vector.x,
202                        movement_vector.y,
203                        movement_vector.z,
204                    ));
205                }
206            } else {
207                self.set_velocity(self.velocity() * 0.9);
208            }
209            return;
210        }
211
212        let velocity = self.velocity();
213        let y = self
214            .mob_effect(LEVITATION)
215            .map_or(velocity.y - self.get_gravity(), |effect| {
216                0.05 * f64::from(effect.amplifier() + 1)
217            });
218
219        if let Some(living_entity) = self.as_living_entity() {
220            self.set_velocity(DVec3::new(
221                0.0,
222                // TODO: depending on how `LivingEntity.air_travel_vertical_friction` implements `FlyingAnimal`'s
223                // different base friction value, the input value may change here (e.g. replace with
224                // a const value added to `LivingEntity`).
225                y * f64::from(living_entity.air_travel_vertical_friction(SQUID_AIR_DRAG)),
226                0.0,
227            ));
228        }
229    }
230
231    /// Copies the body pitch to vanilla `xBodyRotO`, which
232    /// [`Self::rotate_vector`] reads. `yBodyRotO` is advanced by the base tick.
233    fn advance_body_rotation(&self) {
234        let mut state = self.state.lock();
235        state.x_body_rot_old = state.x_body_rot;
236    }
237
238    fn update_squid_rotation(&self) {
239        let mut state = self.state.lock();
240
241        if self.is_in_water() {
242            let movement = self.velocity();
243            let horizontal = movement.x.hypot(movement.z);
244
245            let y_body_rot = self.living_rotation_state().y_body_rot();
246            let y_body_rot = y_body_rot
247                + (-(movement.x.atan2(movement.z) as f32).to_degrees() - y_body_rot) * 0.1;
248
249            self.set_y_body_rot(y_body_rot);
250            self.set_rotation((y_body_rot, self.rotation().1));
251            state.x_body_rot +=
252                (-(horizontal.atan2(movement.y) as f32).to_degrees() - state.x_body_rot) * 0.1;
253        } else {
254            state.x_body_rot += (-DEGREE_90 - state.x_body_rot) * 0.02;
255        }
256    }
257
258    fn handle_air_supply(&self, pre_tick_air_supply: i32) {
259        if LivingEntity::is_alive(self) && !self.is_in_water() {
260            self.set_air_supply(pre_tick_air_supply - 1);
261            if self.should_take_drowning_damage() {
262                self.set_air_supply(0);
263                if let Some(world) = self.level() {
264                    self.hurt(
265                        &world,
266                        &DamageSource::environment(&vanilla_damage_types::DROWN),
267                        2.0,
268                    );
269                }
270            }
271        } else {
272            self.set_air_supply(300);
273        }
274    }
275
276    fn new_with_base(base: EntityBase, entity_type: EntityTypeRef) -> Self {
277        let living_base = LivingEntityBase::new(entity_type);
278        living_base
279            .attributes()
280            .lock()
281            .set_base_value(vanilla_attributes::MAX_HEALTH, SQUID_DEFAULT_HEALTH);
282
283        let mob_base = MobBase::new();
284        mob_base
285            .pathfinding_malus()
286            .lock()
287            .set(PathType::Water, 0.0);
288
289        let ageable_base = AgeableMobBase::new();
290
291        let mut entity_data = SquidEntityData::new();
292        living_base.initialize_synced_data(&mut entity_data);
293
294        {
295            let mut goal_selector = mob_base.goal_selector().lock();
296            goal_selector.add_goal(0, SquidRandomMovementGoal::new());
297            goal_selector.add_goal(1, SquidFleeGoal::new());
298        }
299
300        Self {
301            base,
302            entity_type,
303            living_base,
304            mob_base,
305            ageable_base,
306            entity_data: SyncMutex::new(entity_data),
307            state: SyncMutex::new(SquidState {
308                x_body_rot: 0.0,
309                x_body_rot_old: 0.0,
310                movement_vector: DVec3::ZERO,
311                tentacle_speed: 1.0 / (rand::random::<f32>() + 1.0) * 0.2,
312                tentacle_movement: 0.0,
313            }),
314        }
315    }
316}
317
318impl Entity for SquidEntity {
319    fn base(&self) -> &EntityBase {
320        &self.base
321    }
322
323    fn hurt(&self, world: &World, source: &DamageSource, amount: f32) -> bool {
324        let hurt = LivingEntity::hurt_server(self, world, source, amount);
325
326        if hurt && self.last_hurt_by_mob().is_some() {
327            self.spawn_ink();
328            return true;
329        }
330
331        false
332    }
333
334    fn movement_emission(&self) -> EntityMovementEmission {
335        EntityMovementEmission::Events
336    }
337
338    fn entity_type(&self) -> EntityTypeRef {
339        self.entity_type
340    }
341
342    fn base_tick(&self) {
343        let air_supply = self.air_supply();
344        Mob::base_tick_mob(self);
345        self.handle_air_supply(air_supply);
346    }
347
348    fn get_default_gravity(&self) -> f64 {
349        SQUID_GRAVITY
350    }
351
352    fn dimensions_for_pose(&self, _pose: EntityPose) -> EntityDimensions {
353        let scale = LivingEntity::get_scale(self);
354        if AgeableMob::is_baby(self) {
355            SQUID_BABY_DIMENSIONS.scale(scale)
356        } else if self.entity_type.fixed {
357            self.entity_type.dimensions
358        } else {
359            self.entity_type.dimensions.scale(scale)
360        }
361    }
362
363    fn synced_data(&self) -> Option<&dyn EntitySyncedData> {
364        Some(&self.entity_data)
365    }
366
367    fn save_additional(&self, nbt: &mut owned::NbtCompound) {
368        self.save_mob(nbt);
369        self.save_ageable_mob(nbt);
370    }
371
372    fn load_additional(&self, nbt: borrow::NbtCompound<'_, '_>) {
373        self.load_mob(nbt);
374        self.load_ageable_mob(nbt);
375    }
376}
377
378impl LivingEntity for SquidEntity {
379    fn living_base(&self) -> &LivingEntityBase {
380        &self.living_base
381    }
382
383    fn sound_volume(&self) -> f32 {
384        SQUID_SOUND_VOLUME
385    }
386
387    fn get_health(&self) -> f32 {
388        *self.entity_data.lock().living_entity().health.get()
389    }
390
391    fn set_health(&self, health: f32) {
392        let max_health = self.get_max_health();
393        let clamped = health.clamp(0.0, max_health);
394        self.entity_data
395            .lock()
396            .living_entity_mut()
397            .health
398            .set(clamped);
399    }
400
401    fn travel(&self, entity: &SharedEntity, _input: DVec3) -> Option<MoveResult> {
402        Arc::clone(entity).move_entity(MoverType::SelfMovement, self.velocity())
403    }
404
405    fn server_ai_step(&self, entity: &SharedEntity) {
406        Mob::mob_server_ai_step(self, entity);
407    }
408
409    fn ai_step(&self, entity: &SharedEntity) -> Option<MoveResult> {
410        let result = Mob::mob_ai_step(self, entity);
411
412        self.advance_body_rotation();
413        self.tick_squid_movement();
414        self.update_squid_rotation();
415
416        AgeableMob::tick_ageable_mob(self);
417        result
418    }
419
420    fn hurt_sound(&self, _source: &DamageSource) -> Option<SoundEventRef> {
421        Some(&sound_events::ENTITY_SQUID_HURT)
422    }
423
424    fn death_sound(&self) -> Option<SoundEventRef> {
425        Some(&sound_events::ENTITY_SQUID_DEATH)
426    }
427
428    fn base_experience_reward(&self) -> i32 {
429        rand::random_range(1..4)
430    }
431}
432
433impl AgeableMob for SquidEntity {
434    fn ageable_base(&self) -> &AgeableMobBase {
435        &self.ageable_base
436    }
437
438    fn is_age_locked(&self) -> bool {
439        *self.entity_data.lock().ageable_mob().age_locked.get()
440    }
441
442    fn set_age_locked(&self, age_locked: bool) {
443        self.entity_data
444            .lock()
445            .ageable_mob_mut()
446            .age_locked
447            .set(age_locked);
448    }
449
450    fn set_synced_baby(&self, baby: bool) {
451        self.entity_data.lock().ageable_mob_mut().baby.set(baby);
452    }
453}
454
455impl Mob for SquidEntity {
456    fn mob_base(&self) -> &MobBase {
457        &self.mob_base
458    }
459
460    fn mob_interact(&self, player: &Player, hand: InteractionHand) -> InteractionResult {
461        AgeableMob::mob_interact_ageable(self, player, hand)
462    }
463
464    fn tick_goal_selectors(&self, entity: &SharedEntity) {
465        PathfinderMob::tick_pathfinder_goal_selectors(self, entity);
466    }
467
468    fn tick_path_navigation(&self) {
469        PathfinderMob::tick_pathfinder_path_navigation(self);
470    }
471
472    fn mob_flags(&self) -> i8 {
473        *self.entity_data.lock().mob().mob_flags.get()
474    }
475
476    fn set_mob_flags(&self, flags: i8) {
477        self.entity_data.lock().mob_mut().mob_flags.set(flags);
478    }
479
480    fn ambient_sound(&self) -> Option<SoundEventRef> {
481        Some(&sound_events::ENTITY_SQUID_AMBIENT)
482    }
483
484    fn ambient_sound_interval(&self) -> i32 {
485        SQUID_AMBIENT_SOUND_INTERVAL
486    }
487
488    fn finalize_spawn(
489        &self,
490        world: &Arc<World>,
491        spawn_reason: EntitySpawnReason,
492        group_data: Option<SpawnGroupData>,
493    ) -> Option<SpawnGroupData> {
494        self.finalize_spawn_ageable_mob(world, spawn_reason, group_data)
495    }
496}
497
498impl PathfinderMob for SquidEntity {}
499
500#[cfg(test)]
501mod tests;