diff --git a/pumpkin-util/src/random/mod.rs b/pumpkin-util/src/random/mod.rs index 122e324bb..ce96a1ca8 100644 --- a/pumpkin-util/src/random/mod.rs +++ b/pumpkin-util/src/random/mod.rs @@ -144,6 +144,30 @@ pub fn get_region_seed(world_seed: u64, region_x: i32, region_z: i32, salt: u32) .wrapping_add(i64::from(salt) as u64) } +/// Generates a seed for slime chunk determination. +/// +/// This follows Minecraft's specific formula for identifying if a chunk +/// is a "slime chunk" where slimes can spawn regardless of light levels. +/// +/// # Arguments +/// - `x` – The X chunk coordinate. +/// - `z` – The Z chunk coordinate. +/// - `seed` – The world seed. +/// - `salt` – A salt value (default is 987234911). +/// +/// # Returns +/// A seed value to be used with a legacy random generator. +#[inline] +#[must_use] +pub fn seed_slime_chunk(x: i32, z: i32, seed: u64, salt: u64) -> u64 { + (seed + .wrapping_add((x.wrapping_mul(x).wrapping_mul(4_987_142)) as i64 as u64) + .wrapping_add((x.wrapping_mul(5_947_611)) as i64 as u64) + .wrapping_add((z.wrapping_mul(z) as i64).wrapping_mul(4_392_871) as u64) + .wrapping_add((z.wrapping_mul(389_711)) as i64 as u64)) + ^ salt +} + /// Generates a carver seed for cave and ravine generation. /// /// Carver seeds are used for terrain carving features like caves and ravines. diff --git a/pumpkin-util/src/text/mod.rs b/pumpkin-util/src/text/mod.rs index 9b47bb8fe..961bb3c4e 100644 --- a/pumpkin-util/src/text/mod.rs +++ b/pumpkin-util/src/text/mod.rs @@ -288,6 +288,25 @@ impl TextComponentBase { pub fn to_translated(self) -> Self { // NOTE: Divide the translation into slices and inserts the substitutions. let component = match *self.content { + TextContent::Translate { + translate, + bedrock_translate, + with, + } => { + let mut translated_with = vec![]; + for w in with { + translated_with.push(w.to_translated()); + } + Self { + content: Box::new(TextContent::Translate { + translate, + bedrock_translate, + with: translated_with, + }), + style: self.style, + extra: self.extra, + } + } TextContent::Custom { key, with, locale } => { let translation = get_translation(&key, locale); let mut translation_parent = translation.clone(); diff --git a/pumpkin/src/entity/ai/control/mod.rs b/pumpkin/src/entity/ai/control/mod.rs index 695885991..ccab9a02f 100644 --- a/pumpkin/src/entity/ai/control/mod.rs +++ b/pumpkin/src/entity/ai/control/mod.rs @@ -1,6 +1,8 @@ +use crate::entity::mob::Mob; use pumpkin_util::math::subtract_angles; pub mod look_control; +pub mod move_control; pub trait Control: Send + Sync { fn change_angle(&self, start: f32, end: f32, max_change: f32) -> f32 { @@ -9,3 +11,7 @@ pub trait Control: Send + Sync { start + j } } + +pub trait MoveControlTrait: Control { + fn tick(&mut self, mob: &dyn Mob); +} diff --git a/pumpkin/src/entity/ai/control/move_control.rs b/pumpkin/src/entity/ai/control/move_control.rs new file mode 100644 index 000000000..edcadfd2a --- /dev/null +++ b/pumpkin/src/entity/ai/control/move_control.rs @@ -0,0 +1,148 @@ +use crate::entity::ai::control::{Control, MoveControlTrait}; +use crate::entity::mob::Mob; +use pumpkin_data::attributes::Attributes; +use pumpkin_util::math::vector3::Vector3; +use std::sync::atomic::Ordering; + +#[derive(Default, Clone, Copy, PartialEq, Eq)] +pub enum Operation { + #[default] + Wait, + MoveTo, + Strafe, + Jumping, +} + +pub struct MoveControl { + pub wanted_x: f64, + pub wanted_y: f64, + pub wanted_z: f64, + pub speed_modifier: f64, + pub strafe_forwards: f32, + pub strafe_right: f32, + pub operation: Operation, +} + +impl Default for MoveControl { + fn default() -> Self { + Self { + wanted_x: 0.0, + wanted_y: 0.0, + wanted_z: 0.0, + speed_modifier: 0.0, + strafe_forwards: 0.0, + strafe_right: 0.0, + operation: Operation::Wait, + } + } +} + +impl Control for MoveControl {} + +impl MoveControlTrait for MoveControl { + fn tick(&mut self, mob: &dyn Mob) { + let mob_entity = mob.get_mob_entity(); + let living_entity = &mob_entity.living_entity; + let entity = &living_entity.entity; + + if self.operation == Operation::Strafe { + let movement_speed = living_entity.get_attribute_value(&Attributes::MOVEMENT_SPEED); + let speed_modified = (self.speed_modifier * movement_speed) as f32; + let xa = self.strafe_forwards; + let za = self.strafe_right; + let mut dist = xa.hypot(za); + if dist < 1.0 { + dist = 1.0; + } + + let dist_norm = speed_modified / dist; + let _xa_norm = xa * dist_norm; + let _za_norm = za * dist_norm; + + // TODO: is_walkable check + + living_entity.movement_input.store(Vector3::new( + self.strafe_right as f64, + 0.0, + self.strafe_forwards as f64, + )); + // Vanilla sets speed here too + self.operation = Operation::Wait; + } else if self.operation == Operation::MoveTo { + self.operation = Operation::Wait; + let pos = entity.pos.load(); + let xd = self.wanted_x - pos.x; + let zd = self.wanted_z - pos.z; + let yd = self.wanted_y - pos.y; + let dd = xd * xd + yd * yd + zd * zd; + + if dd < 2.5000003E-7 { + living_entity + .movement_input + .store(Vector3::new(0.0, 0.0, 0.0)); + return; + } + + let y_rot_d = (zd.atan2(xd).to_degrees() as f32) - 90.0; + entity + .yaw + .store(self.change_angle(entity.yaw.load(), y_rot_d, 90.0)); + + let movement_speed = living_entity.get_attribute_value(&Attributes::MOVEMENT_SPEED); + let speed = self.speed_modifier * movement_speed; + living_entity + .movement_input + .store(Vector3::new(0.0, 0.0, speed)); + + // TODO: Jump if needed (based on collision and height difference) + let step_height = living_entity.get_attribute_value(&Attributes::STEP_HEIGHT); + if yd > step_height + && xd * xd + zd * zd < 1.0f64.max(entity.entity_dimension.load().width as f64) + { + living_entity.jumping.store(true, Ordering::SeqCst); + self.operation = Operation::Jumping; + } + } else if self.operation == Operation::Jumping { + let movement_speed = living_entity.get_attribute_value(&Attributes::MOVEMENT_SPEED); + let speed = self.speed_modifier * movement_speed; + living_entity + .movement_input + .store(Vector3::new(0.0, 0.0, speed)); + + if entity.on_ground.load(Ordering::Relaxed) { + self.operation = Operation::Wait; + } + } else { + living_entity + .movement_input + .store(Vector3::new(0.0, 0.0, 0.0)); + } + } +} + +impl MoveControl { + pub fn has_wanted(&self) -> bool { + self.operation == Operation::MoveTo + } + + pub fn get_speed_modifier(&self) -> f64 { + self.speed_modifier + } + + pub fn set_wanted_position(&mut self, x: f64, y: f64, z: f64, speed_modifier: f64) { + self.wanted_x = x; + self.wanted_y = y; + self.wanted_z = z; + self.speed_modifier = speed_modifier; + if self.operation != Operation::Jumping { + self.operation = Operation::MoveTo; + } + } + + pub fn strafe(&mut self, forwards: f32, right: f32) { + self.operation = Operation::Strafe; + self.strafe_forwards = forwards; + self.strafe_right = right; + self.speed_modifier = 0.25; + } +} diff --git a/pumpkin/src/entity/ai/goal/active_target.rs b/pumpkin/src/entity/ai/goal/active_target.rs index 5afb2b989..845f67801 100644 --- a/pumpkin/src/entity/ai/goal/active_target.rs +++ b/pumpkin/src/entity/ai/goal/active_target.rs @@ -9,6 +9,7 @@ use crate::world::World; use pumpkin_data::attributes::Attributes; use pumpkin_data::entity::EntityType; use rand::RngExt; +use std::future::Future; use std::sync::Arc; const DEFAULT_RECIPROCAL_CHANCE: i32 = 10; @@ -39,9 +40,11 @@ impl ActiveTargetGoal { target_predicate.base_max_distance = mob .living_entity .get_attribute_value(&Attributes::FOLLOW_RANGE); + if let Some(predicate) = predicate { target_predicate.set_predicate(predicate); } + Self { track_target_goal, target: None, @@ -62,6 +65,7 @@ impl ActiveTargetGoal { target_predicate.base_max_distance = mob .living_entity .get_attribute_value(&Attributes::FOLLOW_RANGE); + Box::new(Self { track_target_goal, target: None, @@ -71,16 +75,29 @@ impl ActiveTargetGoal { }) } + pub fn set_target(&mut self, target: Option>) { + self.target = target; + } + fn find_closest_target(&mut self, mob: &MobEntity) { + let follow_range = mob + .living_entity + .get_attribute_value(&Attributes::FOLLOW_RANGE); + + // Vanilla updates the target conditions with the current follow distance on every search + self.target_predicate.base_max_distance = follow_range; + let world = mob.living_entity.entity.world.load(); + + // Vanilla searches using getEyeY(), so we offset the position by the eye height + let mut search_pos = mob.living_entity.entity.pos.load(); + search_pos.y += mob.living_entity.entity.entity_dimension.load().eye_height as f64; + if self.target_type == &EntityType::PLAYER { let potential_player = world - .get_closest_player( - mob.living_entity.entity.pos.load(), - mob.living_entity - .get_attribute_value(&Attributes::FOLLOW_RANGE), - ) + .get_closest_player(search_pos, follow_range) .map(|p: Arc| p as Arc); + if let Some(potential_entity) = potential_player && let Some(living) = potential_entity.get_living_entity() && self @@ -91,12 +108,9 @@ impl ActiveTargetGoal { return; } } else { - let potential_entity = world.get_closest_entity( - mob.living_entity.entity.pos.load(), - mob.living_entity - .get_attribute_value(&Attributes::FOLLOW_RANGE), - Some(&[self.target_type]), - ); + let potential_entity = + world.get_closest_entity(search_pos, follow_range, Some(&[self.target_type])); + if let Some(potential_entity) = potential_entity && let Some(living) = potential_entity.get_living_entity() && self @@ -123,6 +137,7 @@ impl Goal for ActiveTargetGoal { self.target.is_some() }) } + fn should_continue<'a>(&'a self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> { Box::pin(async { self.track_target_goal.should_continue(mob).await }) } @@ -130,7 +145,6 @@ impl Goal for ActiveTargetGoal { fn start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> { Box::pin(async { mob.set_mob_target(self.target.clone()).await; - self.track_target_goal.start(mob).await; }) } diff --git a/pumpkin/src/entity/ai/goal/track_target.rs b/pumpkin/src/entity/ai/goal/track_target.rs index 55f2d8d4b..a52454104 100644 --- a/pumpkin/src/entity/ai/goal/track_target.rs +++ b/pumpkin/src/entity/ai/goal/track_target.rs @@ -5,19 +5,19 @@ use crate::entity::living::LivingEntity; use crate::entity::mob::Mob; use pumpkin_data::attributes::Attributes; use rand::RngExt; +use std::sync::atomic::{AtomicI32, Ordering}; const UNSET: i32 = 0; const CAN_TRACK: i32 = 1; const CANNOT_TRACK: i32 = 2; -#[expect(dead_code)] pub struct TrackTargetGoal { goal_control: Controls, check_visibility: bool, check_can_navigate: bool, - can_navigate_flag: i32, - check_can_navigate_cooldown: i32, - time_without_visibility: i32, + can_navigate_flag: AtomicI32, + check_can_navigate_cooldown: AtomicI32, + time_without_visibility: AtomicI32, pub max_time_without_visibility: i32, target_predicate: TargetPredicate, } @@ -30,9 +30,9 @@ impl TrackTargetGoal { goal_control: Controls::TARGET, check_visibility, check_can_navigate, - can_navigate_flag: UNSET, - check_can_navigate_cooldown: 0, - time_without_visibility: 0, + can_navigate_flag: AtomicI32::new(UNSET), + check_can_navigate_cooldown: AtomicI32::new(0), + time_without_visibility: AtomicI32::new(0), max_time_without_visibility: 60, target_predicate: TargetPredicate::create_attackable(), } @@ -42,45 +42,57 @@ impl TrackTargetGoal { Self::new(check_visibility, false) } - fn can_navigate_to_entity(&mut self, mob: &dyn Mob, _target: &LivingEntity) -> bool { - self.check_can_navigate_cooldown = to_goal_ticks(10 + mob.get_random().random_range(0..5)); + pub fn set_unseen_memory_ticks(mut self, ticks: i32) -> Self { + self.max_time_without_visibility = ticks; + self + } + + fn can_navigate_to_entity(&self, mob: &dyn Mob, _target: &LivingEntity) -> bool { + let cooldown = to_goal_ticks(10 + mob.get_random().random_range(0..5)); + self.check_can_navigate_cooldown + .store(cooldown, Ordering::Relaxed); // TODO: after implementing path false } + /// Equivalent to Vanilla's `canAttack` check inside `TargetGoal` pub fn can_track( - &mut self, + &self, mob: &dyn Mob, target: Option<&LivingEntity>, target_predicate: &TargetPredicate, ) -> bool { - if target.is_none() { + let Some(target) = target else { return false; - } + }; + let mob_entity = mob.get_mob_entity(); - let target = target.unwrap(); - let world = mob_entity.living_entity.entity.world.load_full(); + let world = mob_entity.living_entity.entity.world.load(); + if !target_predicate.test(&world, Some(&mob_entity.living_entity), target) { return false; } - // TODO: isInPositionTargetRange check + + // TODO: isInPositionTargetRange (isWithinHome in Java) check if self.check_can_navigate { - self.check_can_navigate_cooldown -= 1; - if self.check_can_navigate_cooldown <= 0 { - self.can_navigate_flag = UNSET; + let cooldown = self + .check_can_navigate_cooldown + .fetch_sub(1, Ordering::Relaxed) + - 1; + if cooldown <= 0 { + self.can_navigate_flag.store(UNSET, Ordering::Relaxed); } - if self.can_navigate_flag == UNSET { - let value = if self.can_navigate_to_entity(mob, target) { - CAN_TRACK - } else { - CANNOT_TRACK - }; - self.can_navigate_flag = value; + if self.can_navigate_flag.load(Ordering::Relaxed) == UNSET { + let can_reach = self.can_navigate_to_entity(mob, target); + self.can_navigate_flag.store( + if can_reach { CAN_TRACK } else { CANNOT_TRACK }, + Ordering::Relaxed, + ); } - if self.can_navigate_flag == CANNOT_TRACK { + if self.can_navigate_flag.load(Ordering::Relaxed) == CANNOT_TRACK { return false; } } @@ -107,6 +119,12 @@ impl Goal for TrackTargetGoal { return false; } + if !self.can_track(mob, Some(target), &self.target_predicate) { + return false; + } + + // TODO: Team checks (return false if on the same team) + let dist_sq = mob_entity .living_entity .entity @@ -118,21 +136,36 @@ impl Goal for TrackTargetGoal { let follow_range = mob_entity .living_entity .get_attribute_value(&Attributes::FOLLOW_RANGE); + if dist_sq > follow_range * follow_range { return false; } - // TODO: Visibility timeout (check_visibility flag) + if self.check_visibility { + // TODO: mob.getSensing().hasLineOfSight(target) + let has_line_of_sight = true; + if has_line_of_sight { + self.time_without_visibility.store(0, Ordering::Relaxed); + } else { + let unseen_ticks = + self.time_without_visibility.fetch_add(1, Ordering::Relaxed) + 1; + if unseen_ticks > to_goal_ticks(self.max_time_without_visibility) { + return false; + } + } + } + + mob.set_mob_target(Some(target_base.clone())).await; true }) } fn start<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, ()> { Box::pin(async { - self.can_navigate_flag = 0; - self.check_can_navigate_cooldown = 0; - self.time_without_visibility = 0; + self.can_navigate_flag.store(UNSET, Ordering::Relaxed); + self.check_can_navigate_cooldown.store(0, Ordering::Relaxed); + self.time_without_visibility.store(0, Ordering::Relaxed); }) } diff --git a/pumpkin/src/entity/mob/magma_cube.rs b/pumpkin/src/entity/mob/magma_cube.rs index 0a3d068f4..ad94b520f 100644 --- a/pumpkin/src/entity/mob/magma_cube.rs +++ b/pumpkin/src/entity/mob/magma_cube.rs @@ -22,4 +22,22 @@ impl Mob for MagmaCubeEntity { fn get_mob_entity(&self) -> &MobEntity { self.slime.get_mob_entity() } + + fn mob_tick<'a>( + &'a self, + caller: &'a Arc, + ) -> crate::entity::EntityBaseFuture<'a, ()> { + self.slime.mob_tick(caller) + } + + fn post_tick(&self) -> crate::entity::EntityBaseFuture<'_, ()> { + self.slime.post_tick() + } + + fn mob_player_collision<'a>( + &'a self, + player: &'a Arc, + ) -> crate::entity::EntityBaseFuture<'a, ()> { + self.slime.mob_player_collision(player) + } } diff --git a/pumpkin/src/entity/mob/mod.rs b/pumpkin/src/entity/mob/mod.rs index 1c9e12d5c..918a2f640 100644 --- a/pumpkin/src/entity/mob/mod.rs +++ b/pumpkin/src/entity/mob/mod.rs @@ -1,6 +1,8 @@ use super::{Entity, EntityBase, NBTStorage, ai::pathfinder::Navigator, living::LivingEntity}; use crate::entity::EntityBaseFuture; +use crate::entity::ai::control::MoveControlTrait; use crate::entity::ai::control::look_control::LookControl; +use crate::entity::ai::control::move_control::MoveControl; use crate::entity::ai::goal::goal_selector::GoalSelector; use crate::entity::player::Player; use crate::server::Server; @@ -68,6 +70,7 @@ pub struct MobEntity { pub navigator: std::sync::Mutex, pub target: tokio::sync::Mutex>>, pub look_control: std::sync::Mutex, + pub move_control: std::sync::Mutex>, pub position_target: AtomicCell, pub position_target_range: AtomicI32, pub love_ticks: AtomicI32, @@ -90,9 +93,7 @@ const NIGHT_START: i64 = 12542; const NIGHT_END: i64 = 23459; impl MobEntity { - #[expect(dead_code)] const AI_DISABLED_FLAG: u8 = 1; - #[expect(dead_code)] const LEFT_HANDED_FLAG: u8 = 2; const ATTACKING_FLAG: u8 = 4; @@ -105,6 +106,7 @@ impl MobEntity { navigator: std::sync::Mutex::new(Navigator::default()), target: tokio::sync::Mutex::new(None), look_control: std::sync::Mutex::new(LookControl::default()), + move_control: std::sync::Mutex::new(Box::new(MoveControl::default())), position_target: AtomicCell::new(BlockPos::ZERO), position_target_range: AtomicI32::new(-1), love_ticks: AtomicI32::new(0), @@ -135,6 +137,26 @@ impl MobEntity { self.set_mob_flag(Self::ATTACKING_FLAG, attacking); } + pub fn is_attacking(&self) -> bool { + (self.mob_flags.load(Relaxed) & Self::ATTACKING_FLAG) != 0 + } + + pub fn set_left_handed(&self, left_handed: bool) { + self.set_mob_flag(Self::LEFT_HANDED_FLAG, left_handed); + } + + pub fn is_left_handed(&self) -> bool { + (self.mob_flags.load(Relaxed) & Self::LEFT_HANDED_FLAG) != 0 + } + + pub fn set_no_ai(&self, no_ai: bool) { + self.set_mob_flag(Self::AI_DISABLED_FLAG, no_ai); + } + + pub fn is_no_ai(&self) -> bool { + (self.mob_flags.load(Relaxed) & Self::AI_DISABLED_FLAG) != 0 + } + fn set_mob_flag(&self, flag: u8, value: bool) { let old_b = self.mob_flags.load(Ordering::Relaxed); @@ -178,22 +200,19 @@ impl MobEntity { let target_hitbox = target.get_entity().bounding_box.load(); - let attack_box_max = self.get_attack_box(max_range).await; - - let intersects_max = attack_box_max.intersects(&target_hitbox); - - if !intersects_max { + if !self + .get_attack_box(max_range) + .await + .intersects(&target_hitbox) + { return false; } - if min_range > 0.0 { - let attack_box_min = self.get_attack_box(min_range).await; - if attack_box_min.intersects(&target_hitbox) { - return false; - } - } - - true + min_range <= 0.0 + || !self + .get_attack_box(min_range) + .await + .intersects(&target_hitbox) } pub fn is_dark_enough_to_spawn(world: &World, pos: &BlockPos, is_thundering: bool) -> bool { @@ -445,6 +464,10 @@ pub trait Mob: EntityBase + Send + Sync { Box::pin(async { false }) } + fn mob_player_collision<'a>(&'a self, _player: &'a Arc) -> EntityBaseFuture<'a, ()> { + Box::pin(async {}) + } + fn get_owner_uuid(&self) -> Option { None } @@ -517,12 +540,17 @@ impl EntityBase for T { *mob_entity.navigator.lock().unwrap() = navigator; }; - // Look Control is synchronous, so we can just use a normal block + // Controllers are synchronous, so we can just use normal blocks { let mut look_control = mob_entity.look_control.lock().unwrap(); look_control.tick(self); }; + { + let mut move_control = mob_entity.move_control.lock().unwrap(); + move_control.tick(self); + }; + mob_entity.living_entity.tick(caller, server).await; self.post_tick().await; @@ -608,6 +636,10 @@ impl EntityBase for T { Box::pin(async move { self.mob_interact(player, item_stack).await }) } + fn on_player_collision<'a>(&'a self, player: &'a Arc) -> EntityBaseFuture<'a, ()> { + Box::pin(async move { self.mob_player_collision(player).await }) + } + fn get_entity(&self) -> &Entity { &self.get_mob_entity().living_entity.entity } diff --git a/pumpkin/src/entity/mob/slime.rs b/pumpkin/src/entity/mob/slime.rs index ed6ecf267..3ea60bc79 100644 --- a/pumpkin/src/entity/mob/slime.rs +++ b/pumpkin/src/entity/mob/slime.rs @@ -1,21 +1,38 @@ -use std::sync::atomic::Ordering; +use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; use std::sync::{Arc, Weak}; +use crossbeam::atomic::AtomicCell; +use pumpkin_data::attributes::Attributes; use pumpkin_data::entity::EntityType; -use pumpkin_data::sound::Sound; +use pumpkin_data::sound::{Sound, SoundCategory}; +use pumpkin_data::tag::Taggable; use pumpkin_nbt::compound::NbtCompound; +use pumpkin_util::Difficulty; +use pumpkin_util::math::boundingbox::{BoundingBox, EntityDimensions}; +use pumpkin_util::math::position::BlockPos; +use pumpkin_util::math::vector3::Vector3; use crate::entity::{ - Entity, NBTStorage, NbtFuture, - ai::goal::{ - active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal, - look_at_entity::LookAtEntityGoal, swim::SwimGoal, wander_around::WanderAroundGoal, - }, + Entity, EntityBase, NBTStorage, NbtFuture, + ai::control::{Control, MoveControlTrait}, + ai::goal::{Goal, GoalFuture, active_target::ActiveTargetGoal}, mob::{Mob, MobEntity}, }; +use crate::world::World; +use pumpkin_util::random::RandomImpl; +use rand::RngExt; pub struct SlimeEntity { entity: Arc, + jump_delay: AtomicI32, + target_yaw: AtomicCell, + is_aggressive: AtomicBool, + was_on_ground: AtomicBool, + pub squish: AtomicCell, + pub target_squish: AtomicCell, + pub o_squish: AtomicCell, + speed_modifier: AtomicCell, + has_split: AtomicBool, } impl SlimeEntity { @@ -23,34 +40,164 @@ impl SlimeEntity { let mob_entity = MobEntity::new(entity); let slime = Self { entity: Arc::new(mob_entity), + jump_delay: AtomicI32::new(0), + target_yaw: AtomicCell::new(0.0), + is_aggressive: AtomicBool::new(false), + was_on_ground: AtomicBool::new(false), + squish: AtomicCell::new(0.0), + target_squish: AtomicCell::new(0.0), + o_squish: AtomicCell::new(0.0), + speed_modifier: AtomicCell::new(0.0), + has_split: AtomicBool::new(false), }; let mob_arc = Arc::new(slime); - let mob_weak: Weak = { - let mob_arc: Arc = mob_arc.clone(); - Arc::downgrade(&mob_arc) - }; { + let mut move_control = mob_arc.entity.move_control.lock().unwrap(); + *move_control = Box::new(SlimeMoveControl::new(Arc::downgrade(&mob_arc))); + let mut goal_selector = mob_arc.entity.goals_selector.lock().unwrap(); let mut target_selector = mob_arc.entity.target_selector.lock().unwrap(); - goal_selector.add_goal(0, Box::new(SwimGoal::default())); - goal_selector.add_goal(5, Box::new(WanderAroundGoal::new(1.0))); - goal_selector.add_goal( - 6, - LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 6.0), - ); - goal_selector.add_goal(6, Box::new(RandomLookAroundGoal::default())); + goal_selector.add_goal(1, Box::new(SlimeFloatGoal::new(mob_arc.clone()))); + goal_selector.add_goal(2, Box::new(SlimeAttackGoal::new(mob_arc.clone()))); + goal_selector.add_goal(3, Box::new(SlimeRandomDirectionGoal::new(mob_arc.clone()))); + goal_selector.add_goal(5, Box::new(SlimeKeepOnJumpingGoal::new(mob_arc.clone()))); target_selector.add_goal( 1, ActiveTargetGoal::with_default(&mob_arc.entity, &EntityType::PLAYER, true), ); + target_selector.add_goal( + 3, + ActiveTargetGoal::with_default(&mob_arc.entity, &EntityType::IRON_GOLEM, true), + ); }; + mob_arc.randomize_size(); + mob_arc } + pub fn randomize_size(&self) { + let mut size_scale = rand::random_range(0..3); + if size_scale < 2 && rand::random_range(0.0..1.0) < 0.5 { + size_scale += 1; + } + let size = 1 << size_scale; + self.set_size(size, true); + } + + pub fn set_size(&self, size: i32, update_health: bool) { + let actual_size = size.clamp(1, 127); + let entity = &self.entity.living_entity.entity; + entity.data.store(actual_size, Ordering::Relaxed); + + // Update attributes + { + let mut attributes = self.entity.living_entity.attributes.write().unwrap(); + if let Some(health) = attributes.get_mut(&Attributes::MAX_HEALTH.id) { + health.base_value = (actual_size * actual_size) as f64; + health.dirty.store(true, Ordering::Relaxed); + } + if let Some(speed) = attributes.get_mut(&Attributes::MOVEMENT_SPEED.id) { + speed.base_value = (0.2 + 0.1 * actual_size as f32) as f64; + speed.dirty.store(true, Ordering::Relaxed); + } + if let Some(damage) = attributes.get_mut(&Attributes::ATTACK_DAMAGE.id) { + damage.base_value = actual_size as f64; + damage.dirty.store(true, Ordering::Relaxed); + } + } + + if update_health { + let max_health = self + .entity + .living_entity + .get_attribute_value(&Attributes::MAX_HEALTH) as f32; + self.entity.living_entity.health.store(max_health); + } + + // Refresh dimensions + let scaled_dimensions = EntityDimensions { + width: entity.entity_type.dimension[0] * actual_size as f32, + height: entity.entity_type.dimension[1] * actual_size as f32, + eye_height: entity.entity_type.eye_height * actual_size as f32, + }; + entity.entity_dimension.store(scaled_dimensions); + + let pos = entity.pos.load(); + let new_bb = BoundingBox::new_from_pos(pos.x, pos.y, pos.z, &scaled_dimensions); + entity.bounding_box.store(new_bb); + } + + pub fn get_size(&self) -> i32 { + self.entity + .living_entity + .entity + .data + .load(Ordering::Relaxed) + } + + pub fn is_tiny(&self) -> bool { + self.get_size() <= 1 + } + + pub fn check_slime_spawn_rules(world: &World, pos: &BlockPos) -> bool { + if world.level_info.load().difficulty == Difficulty::Peaceful { + return false; + } + + // TODO: check spawn reason. if it's spawner, we should return true if block below is valid + // For now, we assume natural spawning as that's what we are implementing. + + // Swamp/Surface Spawning + let biome = world.get_biome(pos); + if biome.has_tag(&pumpkin_data::tag::WorldgenBiome::MINECRAFT_ALLOWS_SURFACE_SLIME_SPAWNS) + && pos.0.y > 50 + && pos.0.y < 70 + { + let time = world.level_time.blocking_lock().time_of_day; + let moon_phase = (time / 24000) % 8; + let surface_slime_spawn_chance = Self::get_spawn_chance(moon_phase); + let mut rng = rand::rng(); + if rng.random::() < surface_slime_spawn_chance + && world.get_max_local_raw_brightness(pos) <= rng.random_range(0..8) + { + return true; + } + } + + // Slime Chunk Spawning + let chunk_pos = pos.chunk_position(); + let world_seed = world.level.seed.0; + let slime_seed = pumpkin_util::random::seed_slime_chunk( + chunk_pos.x, + chunk_pos.y, + world_seed, + 987_234_911, + ); + let mut slime_rand = pumpkin_util::random::legacy_rand::LegacyRand::from_seed(slime_seed); + + let mut rng = rand::rng(); + if rng.random_range(0..10) == 0 && slime_rand.next_bounded_i32(10) == 0 && pos.0.y < 40 { + return true; + } + + false + } + + fn get_spawn_chance(moon_phase: i64) -> f32 { + match moon_phase { + 0 => 1.0, + 1 | 7 => 0.75, + 2 | 6 => 0.5, + 3 | 5 => 0.25, + 4 => 0.0, + _ => 0.0, + } + } + pub(crate) const fn hurt_sound_for_size(size: i32) -> Sound { if size == 1 { Sound::EntitySlimeHurtSmall @@ -58,31 +205,66 @@ impl SlimeEntity { Sound::EntitySlimeHurt } } + + fn get_jump_delay(&self) -> i32 { + rand::random_range(10..30) + } + + fn rot_lerp(&self, start: f32, end: f32, max_step: f32) -> f32 { + let mut diff = (end - start).rem_euclid(360.0); + if diff > 180.0 { + diff -= 360.0; + } + start + diff.clamp(-max_step, max_step) + } + + fn do_play_jump_sound(&self) -> bool { + self.get_size() > 0 + } + + fn get_jump_sound(&self) -> Sound { + if self.is_tiny() { + Sound::EntitySlimeJumpSmall + } else { + Sound::EntitySlimeJump + } + } + + fn get_squish_sound(&self) -> Sound { + if self.is_tiny() { + Sound::EntitySlimeSquishSmall + } else { + Sound::EntitySlimeSquish + } + } + + fn get_sound_volume(&self) -> f32 { + 0.4 * self.get_size() as f32 + } + + fn get_sound_pitch(&self) -> f32 { + let pitch_adjuster = if self.is_tiny() { 1.4 } else { 0.8 }; + (rand::random_range(0.0..1.0) - rand::random_range(0.0..1.0)) * 0.2 + 1.0 * pitch_adjuster + } } impl NBTStorage for SlimeEntity { fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> { Box::pin(async move { self.entity.living_entity.write_nbt(nbt).await; - nbt.put_int( - "Size", - self.entity - .living_entity - .entity - .data - .load(Ordering::Relaxed), - ); + nbt.put_int("Size", self.get_size() - 1); + nbt.put_bool("wasOnGround", self.was_on_ground.load(Ordering::Relaxed)); }) } fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> { Box::pin(async move { self.entity.living_entity.read_nbt_non_mut(nbt).await; - self.entity - .living_entity - .entity - .data - .store(nbt.get_int("Size").unwrap_or(0), Ordering::Relaxed); + self.set_size(nbt.get_int("Size").unwrap_or(0) + 1, false); + self.was_on_ground.store( + nbt.get_bool("wasOnGround").unwrap_or(false), + Ordering::Relaxed, + ); }) } } @@ -91,6 +273,373 @@ impl Mob for SlimeEntity { fn get_mob_entity(&self) -> &MobEntity { &self.entity } + + fn mob_tick<'a>( + &'a self, + _caller: &'a Arc, + ) -> crate::entity::EntityBaseFuture<'a, ()> { + Box::pin(async move { + self.o_squish.store(self.squish.load()); + self.squish + .store(self.squish.load() + (self.target_squish.load() - self.squish.load()) * 0.5); + + let on_ground = self + .entity + .living_entity + .entity + .on_ground + .load(Ordering::Relaxed); + let was_on_ground = self.was_on_ground.load(Ordering::Relaxed); + + if on_ground && !was_on_ground { + // TODO: particles + + let world = self.entity.living_entity.entity.world.load(); + world.play_sound_fine( + self.get_squish_sound(), + SoundCategory::Hostile, + &self.entity.living_entity.entity.pos.load(), + self.get_sound_volume(), + ((rand::random_range(0.0..1.0) - rand::random_range(0.0..1.0)) * 0.2 + 1.0) + / 0.8, + ); + + self.target_squish.store(-0.5); + } else if !on_ground && was_on_ground { + self.target_squish.store(1.0); + } + + self.was_on_ground.store(on_ground, Ordering::Relaxed); + self.target_squish.store(self.target_squish.load() * 0.6); + + self.is_aggressive.store(false, Ordering::Relaxed); + self.speed_modifier.store(0.0); + }) + } + + fn mob_player_collision<'a>( + &'a self, + player: &'a Arc, + ) -> crate::entity::EntityBaseFuture<'a, ()> { + Box::pin(async move { + if !self.is_tiny() { + // dealDamage + self.entity.try_attack(self, &**player).await; + } + }) + } + + fn post_tick(&self) -> crate::entity::EntityBaseFuture<'_, ()> { + Box::pin(async move { + if self.entity.living_entity.dead.load(Ordering::Relaxed) + && self.get_size() > 1 + && self + .has_split + .compare_exchange(false, true, Ordering::Relaxed, Ordering::Relaxed) + .is_ok() + { + let size = self.get_size(); + let world = self.entity.living_entity.entity.world.load(); + let pos = self.entity.living_entity.entity.pos.load(); + let half_size = size / 2; + let count = 2 + rand::random_range(0..3); + + let width = self + .entity + .living_entity + .entity + .entity_dimension + .load() + .width; + let xz_offset = width / 4.0; + + for i in 0..count { + let xd = ((i % 2) as f32 - 0.5) * xz_offset; + let zd = ((i / 2) as f32 - 0.5) * xz_offset; + + let new_pos = pumpkin_util::math::vector3::Vector3::new( + pos.x + xd as f64, + pos.y + 0.5, + pos.z + zd as f64, + ); + let new_entity = Entity::new(world.clone(), new_pos, &EntityType::SLIME); + let slime = SlimeEntity::new(new_entity); + slime.set_size(half_size, true); + slime + .entity + .living_entity + .entity + .yaw + .store(rand::random_range(0.0..360.0)); + world.spawn_entity(slime).await; + } + } + }) + } +} + +pub struct SlimeMoveControl { + slime: Weak, +} + +impl SlimeMoveControl { + pub fn new(slime: Weak) -> Self { + Self { slime } + } +} + +impl Control for SlimeMoveControl {} + +impl MoveControlTrait for SlimeMoveControl { + fn tick(&mut self, mob: &dyn Mob) { + let Some(slime) = self.slime.upgrade() else { + return; + }; + let mob_entity = mob.get_mob_entity(); + let living_entity = &mob_entity.living_entity; + let entity = &living_entity.entity; + + let current_yaw = entity.yaw.load(); + let new_yaw = slime.rot_lerp(current_yaw, slime.target_yaw.load(), 90.0); + entity.yaw.store(new_yaw); + entity.head_yaw.store(new_yaw); + entity.body_yaw.store(new_yaw); + + let speed_modifier = slime.speed_modifier.load(); + let mut movement_input = Vector3::new(0.0, 0.0, 0.0); + + let on_ground = entity.on_ground.load(Ordering::Relaxed); + + if on_ground { + if speed_modifier > 0.0 { + let current_delay = slime.jump_delay.load(Ordering::Relaxed); + if current_delay <= 0 { + // Start jump + let mut next_delay = slime.get_jump_delay(); + if slime.is_aggressive.load(Ordering::Relaxed) { + next_delay /= 3; + } + slime.jump_delay.store(next_delay, Ordering::Relaxed); + living_entity.jumping.store(true, Ordering::SeqCst); + if slime.do_play_jump_sound() { + let world = entity.world.load(); + world.play_sound_fine( + slime.get_jump_sound(), + SoundCategory::Hostile, + &entity.pos.load(), + slime.get_sound_volume(), + slime.get_sound_pitch(), + ); + } + movement_input.z = speed_modifier; + } else { + slime.jump_delay.store(current_delay - 1, Ordering::Relaxed); + living_entity.jumping.store(false, Ordering::SeqCst); + } + } else { + living_entity.jumping.store(false, Ordering::SeqCst); + } + } else { + // In air: move forward but don't "jump" again + if speed_modifier > 0.0 { + movement_input.z = speed_modifier; + } + living_entity.jumping.store(false, Ordering::SeqCst); + } + living_entity.movement_input.store(movement_input); + } +} + +pub struct SlimeFloatGoal { + slime: Arc, +} + +impl SlimeFloatGoal { + pub fn new(slime: Arc) -> Self { + Self { slime } + } +} + +impl Goal for SlimeFloatGoal { + fn can_start<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, bool> { + Box::pin(async move { + let entity = &self.slime.entity.living_entity.entity; + entity.touching_water.load(Ordering::Relaxed) + || entity.touching_lava.load(Ordering::Relaxed) + }) + } + + fn tick<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, ()> { + Box::pin(async move { + if rand::random_range(0.0..1.0) < 0.8 { + self.slime + .entity + .living_entity + .jumping + .store(true, Ordering::SeqCst); + } + self.slime.speed_modifier.store(1.2); + }) + } + + fn should_run_every_tick(&self) -> bool { + true + } + + fn controls(&self) -> crate::entity::ai::goal::Controls { + crate::entity::ai::goal::Controls::JUMP | crate::entity::ai::goal::Controls::MOVE + } +} + +pub struct SlimeAttackGoal { + slime: Arc, + grow_tired_timer: i32, +} + +impl SlimeAttackGoal { + pub fn new(slime: Arc) -> Self { + Self { + slime, + grow_tired_timer: 0, + } + } +} + +impl Goal for SlimeAttackGoal { + fn can_start<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, bool> { + Box::pin(async move { + let target = self.slime.entity.target.lock().await; + target.is_some() + }) + } + + fn start<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, ()> { + Box::pin(async move { + self.grow_tired_timer = 300; + }) + } + + fn should_continue<'a>(&'a self, _mob: &'a dyn Mob) -> GoalFuture<'a, bool> { + Box::pin(async move { + let target = self.slime.entity.target.lock().await; + target.is_some() && self.grow_tired_timer > 0 + }) + } + + fn tick<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, ()> { + Box::pin(async move { + self.grow_tired_timer -= 1; + let target_guard = self.slime.entity.target.lock().await; + if let Some(target) = target_guard.as_ref() { + let pos = target.get_entity().pos.load(); + let my_pos = self.slime.entity.living_entity.entity.pos.load(); + let dx = pos.x - my_pos.x; + let dz = pos.z - my_pos.z; + let yaw = dx.atan2(dz).to_degrees() as f32; + self.slime.target_yaw.store(yaw); + } + self.slime.is_aggressive.store(true, Ordering::Relaxed); + }) + } + + fn should_run_every_tick(&self) -> bool { + true + } + + fn controls(&self) -> crate::entity::ai::goal::Controls { + crate::entity::ai::goal::Controls::LOOK + } +} + +pub struct SlimeRandomDirectionGoal { + slime: Arc, + chosen_degrees: f32, + next_randomize_time: i32, +} + +impl SlimeRandomDirectionGoal { + pub fn new(slime: Arc) -> Self { + Self { + slime, + chosen_degrees: 0.0, + next_randomize_time: 0, + } + } +} + +impl Goal for SlimeRandomDirectionGoal { + fn can_start<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, bool> { + Box::pin(async move { + let target = self.slime.entity.target.lock().await; + target.is_none() + && (self + .slime + .entity + .living_entity + .entity + .on_ground + .load(Ordering::Relaxed) + || self + .slime + .entity + .living_entity + .entity + .touching_water + .load(Ordering::Relaxed) + || self + .slime + .entity + .living_entity + .entity + .touching_lava + .load(Ordering::Relaxed)) + }) + } + + fn tick<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, ()> { + Box::pin(async move { + self.next_randomize_time -= 1; + if self.next_randomize_time <= 0 { + self.next_randomize_time = rand::random_range(40..100); + self.chosen_degrees = rand::random_range(0.0..360.0); + } + self.slime.target_yaw.store(self.chosen_degrees); + self.slime.is_aggressive.store(false, Ordering::Relaxed); + }) + } + + fn controls(&self) -> crate::entity::ai::goal::Controls { + crate::entity::ai::goal::Controls::LOOK + } +} + +pub struct SlimeKeepOnJumpingGoal { + slime: Arc, +} + +impl SlimeKeepOnJumpingGoal { + pub fn new(slime: Arc) -> Self { + Self { slime } + } +} + +impl Goal for SlimeKeepOnJumpingGoal { + fn can_start<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, bool> { + Box::pin(async move { + let vehicle = self.slime.entity.living_entity.entity.vehicle.lock().await; + vehicle.is_none() + }) + } + + fn tick<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, ()> { + Box::pin(async move { + self.slime.speed_modifier.store(1.0); + }) + } + + fn controls(&self) -> crate::entity::ai::goal::Controls { + crate::entity::ai::goal::Controls::JUMP | crate::entity::ai::goal::Controls::MOVE + } } #[cfg(test)] diff --git a/pumpkin/src/entity/type.rs b/pumpkin/src/entity/type.rs index 07e4074fc..a5dc7d61b 100644 --- a/pumpkin/src/entity/type.rs +++ b/pumpkin/src/entity/type.rs @@ -338,6 +338,9 @@ pub fn check_spawn_rules( if id == EntityType::BAT.id { return bat::BatEntity::check_bat_spawn_rules(world, pos); } + if id == EntityType::SLIME.id { + return SlimeEntity::check_slime_spawn_rules(world, pos); + } // TODO true