fix: slimes

This commit is contained in:
Alexander Medvedev 2026-06-13 19:14:33 +02:00
parent 68bae5899e
commit 38a924e6ce
10 changed files with 935 additions and 89 deletions

View File

@ -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) .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. /// Generates a carver seed for cave and ravine generation.
/// ///
/// Carver seeds are used for terrain carving features like caves and ravines. /// Carver seeds are used for terrain carving features like caves and ravines.

View File

@ -288,6 +288,25 @@ impl TextComponentBase {
pub fn to_translated(self) -> Self { pub fn to_translated(self) -> Self {
// NOTE: Divide the translation into slices and inserts the substitutions. // NOTE: Divide the translation into slices and inserts the substitutions.
let component = match *self.content { 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 } => { TextContent::Custom { key, with, locale } => {
let translation = get_translation(&key, locale); let translation = get_translation(&key, locale);
let mut translation_parent = translation.clone(); let mut translation_parent = translation.clone();

View File

@ -1,6 +1,8 @@
use crate::entity::mob::Mob;
use pumpkin_util::math::subtract_angles; use pumpkin_util::math::subtract_angles;
pub mod look_control; pub mod look_control;
pub mod move_control;
pub trait Control: Send + Sync { pub trait Control: Send + Sync {
fn change_angle(&self, start: f32, end: f32, max_change: f32) -> f32 { fn change_angle(&self, start: f32, end: f32, max_change: f32) -> f32 {
@ -9,3 +11,7 @@ pub trait Control: Send + Sync {
start + j start + j
} }
} }
pub trait MoveControlTrait: Control {
fn tick(&mut self, mob: &dyn Mob);
}

View File

@ -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;
}
}

View File

@ -9,6 +9,7 @@ use crate::world::World;
use pumpkin_data::attributes::Attributes; use pumpkin_data::attributes::Attributes;
use pumpkin_data::entity::EntityType; use pumpkin_data::entity::EntityType;
use rand::RngExt; use rand::RngExt;
use std::future::Future;
use std::sync::Arc; use std::sync::Arc;
const DEFAULT_RECIPROCAL_CHANCE: i32 = 10; const DEFAULT_RECIPROCAL_CHANCE: i32 = 10;
@ -39,9 +40,11 @@ impl ActiveTargetGoal {
target_predicate.base_max_distance = mob target_predicate.base_max_distance = mob
.living_entity .living_entity
.get_attribute_value(&Attributes::FOLLOW_RANGE); .get_attribute_value(&Attributes::FOLLOW_RANGE);
if let Some(predicate) = predicate { if let Some(predicate) = predicate {
target_predicate.set_predicate(predicate); target_predicate.set_predicate(predicate);
} }
Self { Self {
track_target_goal, track_target_goal,
target: None, target: None,
@ -62,6 +65,7 @@ impl ActiveTargetGoal {
target_predicate.base_max_distance = mob target_predicate.base_max_distance = mob
.living_entity .living_entity
.get_attribute_value(&Attributes::FOLLOW_RANGE); .get_attribute_value(&Attributes::FOLLOW_RANGE);
Box::new(Self { Box::new(Self {
track_target_goal, track_target_goal,
target: None, target: None,
@ -71,16 +75,29 @@ impl ActiveTargetGoal {
}) })
} }
pub fn set_target(&mut self, target: Option<Arc<dyn EntityBase>>) {
self.target = target;
}
fn find_closest_target(&mut self, mob: &MobEntity) { 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(); 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 { if self.target_type == &EntityType::PLAYER {
let potential_player = world let potential_player = world
.get_closest_player( .get_closest_player(search_pos, follow_range)
mob.living_entity.entity.pos.load(),
mob.living_entity
.get_attribute_value(&Attributes::FOLLOW_RANGE),
)
.map(|p: Arc<Player>| p as Arc<dyn EntityBase>); .map(|p: Arc<Player>| p as Arc<dyn EntityBase>);
if let Some(potential_entity) = potential_player if let Some(potential_entity) = potential_player
&& let Some(living) = potential_entity.get_living_entity() && let Some(living) = potential_entity.get_living_entity()
&& self && self
@ -91,12 +108,9 @@ impl ActiveTargetGoal {
return; return;
} }
} else { } else {
let potential_entity = world.get_closest_entity( let potential_entity =
mob.living_entity.entity.pos.load(), world.get_closest_entity(search_pos, follow_range, Some(&[self.target_type]));
mob.living_entity
.get_attribute_value(&Attributes::FOLLOW_RANGE),
Some(&[self.target_type]),
);
if let Some(potential_entity) = potential_entity if let Some(potential_entity) = potential_entity
&& let Some(living) = potential_entity.get_living_entity() && let Some(living) = potential_entity.get_living_entity()
&& self && self
@ -123,6 +137,7 @@ impl Goal for ActiveTargetGoal {
self.target.is_some() self.target.is_some()
}) })
} }
fn should_continue<'a>(&'a self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> { fn should_continue<'a>(&'a self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> {
Box::pin(async { self.track_target_goal.should_continue(mob).await }) 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, ()> { fn start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
Box::pin(async { Box::pin(async {
mob.set_mob_target(self.target.clone()).await; mob.set_mob_target(self.target.clone()).await;
self.track_target_goal.start(mob).await; self.track_target_goal.start(mob).await;
}) })
} }

View File

@ -5,19 +5,19 @@ use crate::entity::living::LivingEntity;
use crate::entity::mob::Mob; use crate::entity::mob::Mob;
use pumpkin_data::attributes::Attributes; use pumpkin_data::attributes::Attributes;
use rand::RngExt; use rand::RngExt;
use std::sync::atomic::{AtomicI32, Ordering};
const UNSET: i32 = 0; const UNSET: i32 = 0;
const CAN_TRACK: i32 = 1; const CAN_TRACK: i32 = 1;
const CANNOT_TRACK: i32 = 2; const CANNOT_TRACK: i32 = 2;
#[expect(dead_code)]
pub struct TrackTargetGoal { pub struct TrackTargetGoal {
goal_control: Controls, goal_control: Controls,
check_visibility: bool, check_visibility: bool,
check_can_navigate: bool, check_can_navigate: bool,
can_navigate_flag: i32, can_navigate_flag: AtomicI32,
check_can_navigate_cooldown: i32, check_can_navigate_cooldown: AtomicI32,
time_without_visibility: i32, time_without_visibility: AtomicI32,
pub max_time_without_visibility: i32, pub max_time_without_visibility: i32,
target_predicate: TargetPredicate, target_predicate: TargetPredicate,
} }
@ -30,9 +30,9 @@ impl TrackTargetGoal {
goal_control: Controls::TARGET, goal_control: Controls::TARGET,
check_visibility, check_visibility,
check_can_navigate, check_can_navigate,
can_navigate_flag: UNSET, can_navigate_flag: AtomicI32::new(UNSET),
check_can_navigate_cooldown: 0, check_can_navigate_cooldown: AtomicI32::new(0),
time_without_visibility: 0, time_without_visibility: AtomicI32::new(0),
max_time_without_visibility: 60, max_time_without_visibility: 60,
target_predicate: TargetPredicate::create_attackable(), target_predicate: TargetPredicate::create_attackable(),
} }
@ -42,45 +42,57 @@ impl TrackTargetGoal {
Self::new(check_visibility, false) Self::new(check_visibility, false)
} }
fn can_navigate_to_entity(&mut self, mob: &dyn Mob, _target: &LivingEntity) -> bool { pub fn set_unseen_memory_ticks(mut self, ticks: i32) -> Self {
self.check_can_navigate_cooldown = to_goal_ticks(10 + mob.get_random().random_range(0..5)); 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 // TODO: after implementing path
false false
} }
/// Equivalent to Vanilla's `canAttack` check inside `TargetGoal`
pub fn can_track( pub fn can_track(
&mut self, &self,
mob: &dyn Mob, mob: &dyn Mob,
target: Option<&LivingEntity>, target: Option<&LivingEntity>,
target_predicate: &TargetPredicate, target_predicate: &TargetPredicate,
) -> bool { ) -> bool {
if target.is_none() { let Some(target) = target else {
return false; return false;
} };
let mob_entity = mob.get_mob_entity(); let mob_entity = mob.get_mob_entity();
let target = target.unwrap(); let world = mob_entity.living_entity.entity.world.load();
let world = mob_entity.living_entity.entity.world.load_full();
if !target_predicate.test(&world, Some(&mob_entity.living_entity), target) { if !target_predicate.test(&world, Some(&mob_entity.living_entity), target) {
return false; return false;
} }
// TODO: isInPositionTargetRange check
// TODO: isInPositionTargetRange (isWithinHome in Java) check
if self.check_can_navigate { if self.check_can_navigate {
self.check_can_navigate_cooldown -= 1; let cooldown = self
if self.check_can_navigate_cooldown <= 0 { .check_can_navigate_cooldown
self.can_navigate_flag = UNSET; .fetch_sub(1, Ordering::Relaxed)
- 1;
if cooldown <= 0 {
self.can_navigate_flag.store(UNSET, Ordering::Relaxed);
} }
if self.can_navigate_flag == UNSET { if self.can_navigate_flag.load(Ordering::Relaxed) == UNSET {
let value = if self.can_navigate_to_entity(mob, target) { let can_reach = self.can_navigate_to_entity(mob, target);
CAN_TRACK self.can_navigate_flag.store(
} else { if can_reach { CAN_TRACK } else { CANNOT_TRACK },
CANNOT_TRACK Ordering::Relaxed,
}; );
self.can_navigate_flag = value;
} }
if self.can_navigate_flag == CANNOT_TRACK { if self.can_navigate_flag.load(Ordering::Relaxed) == CANNOT_TRACK {
return false; return false;
} }
} }
@ -107,6 +119,12 @@ impl Goal for TrackTargetGoal {
return false; 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 let dist_sq = mob_entity
.living_entity .living_entity
.entity .entity
@ -118,21 +136,36 @@ impl Goal for TrackTargetGoal {
let follow_range = mob_entity let follow_range = mob_entity
.living_entity .living_entity
.get_attribute_value(&Attributes::FOLLOW_RANGE); .get_attribute_value(&Attributes::FOLLOW_RANGE);
if dist_sq > follow_range * follow_range { if dist_sq > follow_range * follow_range {
return false; 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 true
}) })
} }
fn start<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, ()> { fn start<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
Box::pin(async { Box::pin(async {
self.can_navigate_flag = 0; self.can_navigate_flag.store(UNSET, Ordering::Relaxed);
self.check_can_navigate_cooldown = 0; self.check_can_navigate_cooldown.store(0, Ordering::Relaxed);
self.time_without_visibility = 0; self.time_without_visibility.store(0, Ordering::Relaxed);
}) })
} }

View File

@ -22,4 +22,22 @@ impl Mob for MagmaCubeEntity {
fn get_mob_entity(&self) -> &MobEntity { fn get_mob_entity(&self) -> &MobEntity {
self.slime.get_mob_entity() self.slime.get_mob_entity()
} }
fn mob_tick<'a>(
&'a self,
caller: &'a Arc<dyn crate::entity::EntityBase>,
) -> 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::player::Player>,
) -> crate::entity::EntityBaseFuture<'a, ()> {
self.slime.mob_player_collision(player)
}
} }

View File

@ -1,6 +1,8 @@
use super::{Entity, EntityBase, NBTStorage, ai::pathfinder::Navigator, living::LivingEntity}; use super::{Entity, EntityBase, NBTStorage, ai::pathfinder::Navigator, living::LivingEntity};
use crate::entity::EntityBaseFuture; use crate::entity::EntityBaseFuture;
use crate::entity::ai::control::MoveControlTrait;
use crate::entity::ai::control::look_control::LookControl; 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::ai::goal::goal_selector::GoalSelector;
use crate::entity::player::Player; use crate::entity::player::Player;
use crate::server::Server; use crate::server::Server;
@ -68,6 +70,7 @@ pub struct MobEntity {
pub navigator: std::sync::Mutex<Navigator>, pub navigator: std::sync::Mutex<Navigator>,
pub target: tokio::sync::Mutex<Option<Arc<dyn EntityBase>>>, pub target: tokio::sync::Mutex<Option<Arc<dyn EntityBase>>>,
pub look_control: std::sync::Mutex<LookControl>, pub look_control: std::sync::Mutex<LookControl>,
pub move_control: std::sync::Mutex<Box<dyn MoveControlTrait>>,
pub position_target: AtomicCell<BlockPos>, pub position_target: AtomicCell<BlockPos>,
pub position_target_range: AtomicI32, pub position_target_range: AtomicI32,
pub love_ticks: AtomicI32, pub love_ticks: AtomicI32,
@ -90,9 +93,7 @@ const NIGHT_START: i64 = 12542;
const NIGHT_END: i64 = 23459; const NIGHT_END: i64 = 23459;
impl MobEntity { impl MobEntity {
#[expect(dead_code)]
const AI_DISABLED_FLAG: u8 = 1; const AI_DISABLED_FLAG: u8 = 1;
#[expect(dead_code)]
const LEFT_HANDED_FLAG: u8 = 2; const LEFT_HANDED_FLAG: u8 = 2;
const ATTACKING_FLAG: u8 = 4; const ATTACKING_FLAG: u8 = 4;
@ -105,6 +106,7 @@ impl MobEntity {
navigator: std::sync::Mutex::new(Navigator::default()), navigator: std::sync::Mutex::new(Navigator::default()),
target: tokio::sync::Mutex::new(None), target: tokio::sync::Mutex::new(None),
look_control: std::sync::Mutex::new(LookControl::default()), 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: AtomicCell::new(BlockPos::ZERO),
position_target_range: AtomicI32::new(-1), position_target_range: AtomicI32::new(-1),
love_ticks: AtomicI32::new(0), love_ticks: AtomicI32::new(0),
@ -135,6 +137,26 @@ impl MobEntity {
self.set_mob_flag(Self::ATTACKING_FLAG, attacking); 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) { fn set_mob_flag(&self, flag: u8, value: bool) {
let old_b = self.mob_flags.load(Ordering::Relaxed); 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 target_hitbox = target.get_entity().bounding_box.load();
let attack_box_max = self.get_attack_box(max_range).await; if !self
.get_attack_box(max_range)
let intersects_max = attack_box_max.intersects(&target_hitbox); .await
.intersects(&target_hitbox)
if !intersects_max { {
return false; return false;
} }
if min_range > 0.0 { min_range <= 0.0
let attack_box_min = self.get_attack_box(min_range).await; || !self
if attack_box_min.intersects(&target_hitbox) { .get_attack_box(min_range)
return false; .await
} .intersects(&target_hitbox)
}
true
} }
pub fn is_dark_enough_to_spawn(world: &World, pos: &BlockPos, is_thundering: bool) -> bool { 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 }) Box::pin(async { false })
} }
fn mob_player_collision<'a>(&'a self, _player: &'a Arc<Player>) -> EntityBaseFuture<'a, ()> {
Box::pin(async {})
}
fn get_owner_uuid(&self) -> Option<Uuid> { fn get_owner_uuid(&self) -> Option<Uuid> {
None None
} }
@ -517,12 +540,17 @@ impl<T: Mob + Send + 'static> EntityBase for T {
*mob_entity.navigator.lock().unwrap() = navigator; *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(); let mut look_control = mob_entity.look_control.lock().unwrap();
look_control.tick(self); 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; mob_entity.living_entity.tick(caller, server).await;
self.post_tick().await; self.post_tick().await;
@ -608,6 +636,10 @@ impl<T: Mob + Send + 'static> EntityBase for T {
Box::pin(async move { self.mob_interact(player, item_stack).await }) Box::pin(async move { self.mob_interact(player, item_stack).await })
} }
fn on_player_collision<'a>(&'a self, player: &'a Arc<Player>) -> EntityBaseFuture<'a, ()> {
Box::pin(async move { self.mob_player_collision(player).await })
}
fn get_entity(&self) -> &Entity { fn get_entity(&self) -> &Entity {
&self.get_mob_entity().living_entity.entity &self.get_mob_entity().living_entity.entity
} }

View File

@ -1,21 +1,38 @@
use std::sync::atomic::Ordering; use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
use std::sync::{Arc, Weak}; use std::sync::{Arc, Weak};
use crossbeam::atomic::AtomicCell;
use pumpkin_data::attributes::Attributes;
use pumpkin_data::entity::EntityType; 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_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::{ use crate::entity::{
Entity, NBTStorage, NbtFuture, Entity, EntityBase, NBTStorage, NbtFuture,
ai::goal::{ ai::control::{Control, MoveControlTrait},
active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal, ai::goal::{Goal, GoalFuture, active_target::ActiveTargetGoal},
look_at_entity::LookAtEntityGoal, swim::SwimGoal, wander_around::WanderAroundGoal,
},
mob::{Mob, MobEntity}, mob::{Mob, MobEntity},
}; };
use crate::world::World;
use pumpkin_util::random::RandomImpl;
use rand::RngExt;
pub struct SlimeEntity { pub struct SlimeEntity {
entity: Arc<MobEntity>, entity: Arc<MobEntity>,
jump_delay: AtomicI32,
target_yaw: AtomicCell<f32>,
is_aggressive: AtomicBool,
was_on_ground: AtomicBool,
pub squish: AtomicCell<f32>,
pub target_squish: AtomicCell<f32>,
pub o_squish: AtomicCell<f32>,
speed_modifier: AtomicCell<f64>,
has_split: AtomicBool,
} }
impl SlimeEntity { impl SlimeEntity {
@ -23,34 +40,164 @@ impl SlimeEntity {
let mob_entity = MobEntity::new(entity); let mob_entity = MobEntity::new(entity);
let slime = Self { let slime = Self {
entity: Arc::new(mob_entity), 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_arc = Arc::new(slime);
let mob_weak: Weak<dyn Mob> = {
let mob_arc: Arc<dyn Mob> = 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 goal_selector = mob_arc.entity.goals_selector.lock().unwrap();
let mut target_selector = mob_arc.entity.target_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(1, Box::new(SlimeFloatGoal::new(mob_arc.clone())));
goal_selector.add_goal(5, Box::new(WanderAroundGoal::new(1.0))); goal_selector.add_goal(2, Box::new(SlimeAttackGoal::new(mob_arc.clone())));
goal_selector.add_goal( goal_selector.add_goal(3, Box::new(SlimeRandomDirectionGoal::new(mob_arc.clone())));
6, goal_selector.add_goal(5, Box::new(SlimeKeepOnJumpingGoal::new(mob_arc.clone())));
LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 6.0),
);
goal_selector.add_goal(6, Box::new(RandomLookAroundGoal::default()));
target_selector.add_goal( target_selector.add_goal(
1, 1,
ActiveTargetGoal::with_default(&mob_arc.entity, &EntityType::PLAYER, true), 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 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::<f32>() < 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 { pub(crate) const fn hurt_sound_for_size(size: i32) -> Sound {
if size == 1 { if size == 1 {
Sound::EntitySlimeHurtSmall Sound::EntitySlimeHurtSmall
@ -58,31 +205,66 @@ impl SlimeEntity {
Sound::EntitySlimeHurt 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 { impl NBTStorage for SlimeEntity {
fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> { fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move { Box::pin(async move {
self.entity.living_entity.write_nbt(nbt).await; self.entity.living_entity.write_nbt(nbt).await;
nbt.put_int( nbt.put_int("Size", self.get_size() - 1);
"Size", nbt.put_bool("wasOnGround", self.was_on_ground.load(Ordering::Relaxed));
self.entity
.living_entity
.entity
.data
.load(Ordering::Relaxed),
);
}) })
} }
fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> { fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move { Box::pin(async move {
self.entity.living_entity.read_nbt_non_mut(nbt).await; self.entity.living_entity.read_nbt_non_mut(nbt).await;
self.entity self.set_size(nbt.get_int("Size").unwrap_or(0) + 1, false);
.living_entity self.was_on_ground.store(
.entity nbt.get_bool("wasOnGround").unwrap_or(false),
.data Ordering::Relaxed,
.store(nbt.get_int("Size").unwrap_or(0), Ordering::Relaxed); );
}) })
} }
} }
@ -91,6 +273,373 @@ impl Mob for SlimeEntity {
fn get_mob_entity(&self) -> &MobEntity { fn get_mob_entity(&self) -> &MobEntity {
&self.entity &self.entity
} }
fn mob_tick<'a>(
&'a self,
_caller: &'a Arc<dyn EntityBase>,
) -> 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::player::Player>,
) -> 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<SlimeEntity>,
}
impl SlimeMoveControl {
pub fn new(slime: Weak<SlimeEntity>) -> 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<SlimeEntity>,
}
impl SlimeFloatGoal {
pub fn new(slime: Arc<SlimeEntity>) -> 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<SlimeEntity>,
grow_tired_timer: i32,
}
impl SlimeAttackGoal {
pub fn new(slime: Arc<SlimeEntity>) -> 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<SlimeEntity>,
chosen_degrees: f32,
next_randomize_time: i32,
}
impl SlimeRandomDirectionGoal {
pub fn new(slime: Arc<SlimeEntity>) -> 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<SlimeEntity>,
}
impl SlimeKeepOnJumpingGoal {
pub fn new(slime: Arc<SlimeEntity>) -> 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)] #[cfg(test)]

View File

@ -338,6 +338,9 @@ pub fn check_spawn_rules(
if id == EntityType::BAT.id { if id == EntityType::BAT.id {
return bat::BatEntity::check_bat_spawn_rules(world, pos); return bat::BatEntity::check_bat_spawn_rules(world, pos);
} }
if id == EntityType::SLIME.id {
return SlimeEntity::check_slime_spawn_rules(world, pos);
}
// TODO // TODO
true true