steel_core/entity/entities/mobs/passive/squid/
mod.rs1use 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;
61const 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#[entity_behavior(class = "Squid")]
73pub 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 x_body_rot_old: f32,
89 movement_vector: DVec3,
90 tentacle_speed: f32,
91 tentacle_movement: f32,
92}
93
94unsafe impl DowncastType for SquidEntity {
96 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/squid");
97}
98
99impl SquidEntity {
100 #[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 pub fn has_movement_vector(&self) -> bool {
111 self.state.lock().movement_vector.length_squared() > SQUID_MOVEMENT_THRESHOLD
112 }
113
114 pub fn set_movement_vector(&self, new_vec: DVec3) {
116 self.state.lock().movement_vector = new_vec;
117 }
118
119 pub fn movement_vector(&self) -> DVec3 {
121 self.state.lock().movement_vector
122 }
123
124 #[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 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 y * f64::from(living_entity.air_travel_vertical_friction(SQUID_AIR_DRAG)),
226 0.0,
227 ));
228 }
229 }
230
231 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;