fix: slimes
This commit is contained in:
parent
68bae5899e
commit
38a924e6ce
@ -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.
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
148
pumpkin/src/entity/ai/control/move_control.rs
Normal file
148
pumpkin/src/entity/ai/control/move_control.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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<Arc<dyn EntityBase>>) {
|
||||
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<Player>| p as Arc<dyn EntityBase>);
|
||||
|
||||
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;
|
||||
})
|
||||
}
|
||||
|
||||
@ -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);
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -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<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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<Navigator>,
|
||||
pub target: tokio::sync::Mutex<Option<Arc<dyn EntityBase>>>,
|
||||
pub look_control: std::sync::Mutex<LookControl>,
|
||||
pub move_control: std::sync::Mutex<Box<dyn MoveControlTrait>>,
|
||||
pub position_target: AtomicCell<BlockPos>,
|
||||
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<Player>) -> EntityBaseFuture<'a, ()> {
|
||||
Box::pin(async {})
|
||||
}
|
||||
|
||||
fn get_owner_uuid(&self) -> Option<Uuid> {
|
||||
None
|
||||
}
|
||||
@ -517,12 +540,17 @@ impl<T: Mob + Send + 'static> 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<T: Mob + Send + 'static> EntityBase for T {
|
||||
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 {
|
||||
&self.get_mob_entity().living_entity.entity
|
||||
}
|
||||
|
||||
@ -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<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 {
|
||||
@ -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<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 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::<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 {
|
||||
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<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)]
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user