Pumpkin/crates/pumpkin/src/entity/mob/enderman.rs
2026-08-26 16:45:10 +02:00

472 lines
16 KiB
Rust

use std::sync::{
Arc, Weak,
atomic::{AtomicBool, Ordering},
};
use crate::entity::attributes::Modifier;
use crate::entity::attributes::ModifierOperation;
use pumpkin_data::{BlockStateId, attributes::Attributes};
use crossbeam::atomic::AtomicCell;
use pumpkin_data::{
damage::DamageType,
data_component_impl::EquipmentSlot,
entity::EntityType,
item::Item,
particle::Particle,
sound::{Sound, SoundCategory},
tag,
tag::Taggable,
};
use pumpkin_nbt::compound::NbtCompound;
use pumpkin_protocol::{
codec::var_int::VarInt,
java::client::play::{CEntityPositionSync, Metadata},
};
use pumpkin_util::math::{boundingbox::BoundingBox, position::BlockPos, vector3::Vector3};
use rand::RngExt;
use crate::entity::{
Entity, EntityBase,
ai::{
goal::{
active_target::ActiveTargetGoal, chase_player::ChasePlayerGoal,
look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal,
melee_attack::MeleeAttackGoal, pick_up_block::PickUpBlockGoal,
place_block::PlaceBlockGoal, revenge::RevengeGoal, swim::SwimGoal,
teleport_towards_player::TeleportTowardsPlayerGoal, wander_around::WanderAroundGoal,
},
pathfinder::node::PathType,
},
mob::{Mob, MobEntity},
player::Player,
};
const SPEED_BOOST: f64 = 0.15;
const ENDERMAN_SPEED_BOOST_ID: &str = "minecraft:attacking";
pub const ENDERMAN_EYE_HEIGHT: f64 = 2.55;
pub const ENDERMAN_BODY_Y_OFFSET: f64 = 1.45;
pub const PLAYER_EYE_HEIGHT: f64 = 1.62;
fn is_projectile_damage(dt: DamageType) -> bool {
let (names, _) = pumpkin_data::tag::DamageType::MINECRAFT_IS_PROJECTILE;
names.contains(&dt.message_id)
}
pub struct EndermanEntity {
pub mob_entity: MobEntity,
carried_block: AtomicCell<Option<BlockStateId>>,
angry: AtomicBool,
provoked: AtomicBool,
speed_boosted: AtomicBool,
}
impl EndermanEntity {
pub fn new(entity: Entity) -> Arc<Self> {
let mob_entity = MobEntity::new(entity);
let entity = Self {
mob_entity,
carried_block: AtomicCell::new(None),
angry: AtomicBool::new(false),
provoked: AtomicBool::new(false),
speed_boosted: AtomicBool::new(false),
};
let mob_arc = Arc::new(entity);
let mob_weak: Weak<dyn Mob> = {
let mob_arc: Arc<dyn Mob> = mob_arc.clone();
Arc::downgrade(&mob_arc)
};
let mut navigator = mob_arc
.mob_entity
.navigator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
navigator.set_mob_dimensions(0.6, 2.9);
navigator.set_pathfinding_malus(PathType::Water, -1.0);
drop(navigator);
{
let mut goal_selector = mob_arc
.mob_entity
.goals_selector
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let mut target_selector = mob_arc
.mob_entity
.target_selector
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
goal_selector.add_goal(0, Box::new(SwimGoal::default()));
goal_selector.add_goal(1, Box::new(ChasePlayerGoal::new(mob_arc.clone())));
goal_selector.add_goal(2, Box::new(MeleeAttackGoal::new(1.0, false)));
goal_selector.add_goal(7, Box::new(WanderAroundGoal::new(1.0)));
goal_selector.add_goal(
8,
LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 8.0),
);
goal_selector.add_goal(8, Box::new(RandomLookAroundGoal::default()));
goal_selector.add_goal(10, Box::new(PlaceBlockGoal::new(mob_arc.clone())));
goal_selector.add_goal(11, Box::new(PickUpBlockGoal::new(mob_arc.clone())));
target_selector.add_goal(1, Box::new(TeleportTowardsPlayerGoal::new(mob_arc.clone())));
target_selector.add_goal(2, Box::new(RevengeGoal::new(true)));
target_selector.add_goal(
3,
ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::ENDERMITE, true),
);
};
mob_arc
}
pub fn teleport_randomly(&self) -> bool {
let entity = &self.mob_entity.living_entity.entity;
let pos = entity.pos.load();
let (x, y, z) = {
let mut rng = self.get_random();
(
pos.x + (rng.random_range(0.0..1.0) - 0.5) * 64.0,
pos.y + (rng.random_range(0i32..64) - 32) as f64,
pos.z + (rng.random_range(0.0..1.0) - 0.5) * 64.0,
)
};
self.teleport_to(x, y, z)
}
pub fn teleport_towards(&self, target: &dyn EntityBase) -> bool {
let entity = &self.mob_entity.living_entity.entity;
let pos = entity.pos.load();
let target_pos = target.get_entity().pos.load();
let dx = pos.x - target_pos.x;
let dy = (pos.y + ENDERMAN_BODY_Y_OFFSET) - (target_pos.y + PLAYER_EYE_HEIGHT);
let dz = pos.z - target_pos.z;
let dist = (dx * dx + dy * dy + dz * dz).sqrt();
if dist < 1e-6 {
return false;
}
let nx = dx / dist;
let ny = dy / dist;
let nz = dz / dist;
let (x, y, z) = {
let mut rng = self.get_random();
(
pos.x + (rng.random_range(0.0..1.0) - 0.5) * 8.0 - nx * 16.0,
pos.y + (rng.random_range(0i32..16) - 8) as f64 - ny * 16.0,
pos.z + (rng.random_range(0.0..1.0) - 0.5) * 8.0 - nz * 16.0,
)
};
self.teleport_to(x, y, z)
}
pub fn teleport_to(&self, x: f64, y: f64, z: f64) -> bool {
let entity = &self.mob_entity.living_entity.entity;
let origin = entity.pos.load();
let world = entity.world.load();
let min_y = f64::from(world.dimension.min_y);
let max_y = f64::from(world.dimension.min_y + world.dimension.height - 1);
let mut target_y = y.clamp(min_y, max_y);
let block_x = x.floor() as i32;
let mut block_y = target_y.floor() as i32;
let block_z = z.floor() as i32;
let mut found_ground = false;
loop {
let below_pos = BlockPos::new(block_x, block_y - 1, block_z);
let below_state = world.get_block_state(&below_pos);
if below_state.is_solid() {
found_ground = true;
break;
}
if block_y <= world.dimension.min_y {
break;
}
block_y -= 1;
target_y = block_y as f64;
}
if !found_ground {
return false;
}
let dest_pos = BlockPos::new(block_x, block_y, block_z);
let dest_fluid = world.get_fluid(&dest_pos);
if dest_fluid.has_tag(&tag::Fluid::MINECRAFT_WATER) {
return false;
}
let half_width = 0.3;
let height = 2.9;
let bb = BoundingBox::new(
Vector3::new(x - half_width, target_y, z - half_width),
Vector3::new(x + half_width, target_y + height, z + half_width),
);
if !world.is_space_empty(bb) {
return false;
}
let new_pos = Vector3::new(x, target_y, z);
for pos in &[origin, new_pos] {
world.spawn_particle(
*pos,
Vector3::new(0.0, 0.0, 0.0),
0.0,
128,
Particle::Portal,
);
world.play_sound(Sound::EntityEndermanTeleport, SoundCategory::Hostile, pos);
}
if let Some(server) = world.server.upgrade() {
let mut event =
crate::plugin::api::events::entity::entity_teleport::EntityTeleportEvent::new(
entity.entity_id,
origin,
new_pos,
);
server.plugin_manager.fire_blocking(&server, &mut event);
if event.cancelled {
return false;
}
}
entity.set_pos(new_pos);
let chunk_pos = entity.chunk_pos.load();
world.broadcast_to_chunk(
chunk_pos,
&CEntityPositionSync::new(
entity.entity_id.into(),
new_pos,
Vector3::new(0.0, 0.0, 0.0),
entity.yaw.load(),
entity.pitch.load(),
entity.on_ground.load(Ordering::Relaxed),
),
);
self.mob_entity
.navigator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.stop();
true
}
pub fn set_target(&self, target: Option<Arc<dyn EntityBase>>) {
let is_some = target.is_some();
let mut mob_target = self
.mob_entity
.target
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*mob_target = target;
drop(mob_target);
if is_some {
self.set_angry(true);
// Use attribute modifier instead of direct speed arithmetic
if !self.speed_boosted.swap(true, Ordering::Relaxed) {
let living = &self.mob_entity.living_entity;
let modifier = Modifier {
id: ENDERMAN_SPEED_BOOST_ID.to_string(),
amount: SPEED_BOOST,
operation: ModifierOperation::Add,
};
living.update_attribute(&Attributes::MOVEMENT_SPEED, |inst| {
inst.add_or_replace_modifier(modifier);
});
crate::entity::attributes::send_attribute_updates_for_living(
living,
vec![Attributes::MOVEMENT_SPEED],
);
}
} else {
self.set_angry(false);
self.set_provoked(false);
if self.speed_boosted.swap(false, Ordering::Relaxed) {
let living = &self.mob_entity.living_entity;
living.update_attribute(&Attributes::MOVEMENT_SPEED, |inst| {
inst.remove_modifier(ENDERMAN_SPEED_BOOST_ID);
});
crate::entity::attributes::send_attribute_updates_for_living(
living,
vec![Attributes::MOVEMENT_SPEED],
);
}
}
}
pub fn set_angry(&self, angry: bool) {
self.angry.store(angry, Ordering::Relaxed);
self.mob_entity.living_entity.entity.send_meta_data(
&[Metadata::new(
pumpkin_data::tracked_data::enderman::CREEPY,
angry,
)],
None,
);
}
pub fn is_angry(&self) -> bool {
self.angry.load(Ordering::Relaxed)
}
pub fn set_provoked(&self, provoked: bool) {
self.provoked.store(provoked, Ordering::Relaxed);
self.mob_entity.living_entity.entity.send_meta_data(
&[Metadata::new(
pumpkin_data::tracked_data::enderman::STARED_AT,
provoked,
)],
None,
);
}
pub fn set_carried_block(&self, block_state: Option<BlockStateId>) {
self.carried_block.store(block_state);
let value = block_state.map_or(VarInt(0), |id| VarInt(id.as_u16() as i32));
self.mob_entity.living_entity.entity.send_meta_data(
&[Metadata::new(
pumpkin_data::tracked_data::enderman::CARRY_STATE,
value,
)],
None,
);
}
pub fn get_carried_block(&self) -> Option<BlockStateId> {
self.carried_block.load()
}
pub fn is_player_staring(&self, player: &Player) -> bool {
let equipment = player.living_entity.entity_equipment.try_lock();
if let Ok(equipment) = equipment
&& let Some(head_stack) = equipment.equipment.get(&EquipmentSlot::HEAD)
&& !head_stack.is_empty()
&& head_stack.item == &Item::CARVED_PUMPKIN
{
return false;
}
let entity = &self.mob_entity.living_entity.entity;
let enderman_pos = entity.pos.load();
let enderman_eye_y = enderman_pos.y + ENDERMAN_EYE_HEIGHT;
let player_entity = player.get_entity();
let player_pos = player_entity.pos.load();
let player_eye_y = player_pos.y + PLAYER_EYE_HEIGHT;
let pitch = player_entity.pitch.load().to_radians();
let yaw = -player_entity.yaw.load().to_radians();
let cos_pitch = pitch.cos();
let look_dir = Vector3::new(
(yaw.sin() * cos_pitch) as f64,
(-pitch.sin()) as f64,
(yaw.cos() * cos_pitch) as f64,
);
let dx = enderman_pos.x - player_pos.x;
let dy = enderman_eye_y - player_eye_y;
let dz = enderman_pos.z - player_pos.z;
let distance = (dx * dx + dy * dy + dz * dz).sqrt();
if distance < 0.1 {
return false;
}
let dir_x = dx / distance;
let dir_y = dy / distance;
let dir_z = dz / distance;
let dot = look_dir.x * dir_x + look_dir.y * dir_y + look_dir.z * dir_z;
if dot <= 1.0 - 0.025 / distance {
return false;
}
let enderman_eye_pos = Vector3::new(enderman_pos.x, enderman_eye_y, enderman_pos.z);
let player_eye_pos = Vector3::new(player_pos.x, player_eye_y, player_pos.z);
let world = entity.world.load();
world
.raycast(enderman_eye_pos, player_eye_pos, |block_pos, w| {
let state = w.get_block_state(block_pos);
state.is_solid()
})
.is_none()
}
}
impl Mob for EndermanEntity {
fn mob_write_nbt(&self, nbt: &mut NbtCompound) {
if let Some(block_state) = self.carried_block.load() {
nbt.put_int("carriedBlockState", block_state.as_u16() as i32);
}
}
fn mob_read_nbt(&self, nbt: &NbtCompound) {
if let Some(block_state) = nbt.get_int("carriedBlockState") {
self.set_carried_block(BlockStateId::new(block_state as u16));
}
}
fn get_mob_entity(&self) -> &MobEntity {
&self.mob_entity
}
fn set_mob_target(&self, target: Option<Arc<dyn EntityBase>>) {
self.set_target(target);
}
// TODO: sunlight avoidance, carried block drop on death, angerable system, ambient sound override
fn mob_tick<'a>(&'a self, caller: &'a Arc<dyn EntityBase>) {
let entity = &self.mob_entity.living_entity.entity;
if !entity.is_alive() {
return;
}
let world = entity.world.load();
let raining_at_feet = world.is_raining_at(&entity.block_pos.load());
let raining_at_head = world.is_raining_at(&entity.bounding_box.load().max_block_pos());
if entity.touching_water.load(Ordering::SeqCst) || raining_at_feet || raining_at_head {
caller.damage(caller.as_ref(), 1.0, DamageType::DROWN);
}
}
fn pre_damage(&self, damage_type: DamageType, _source: Option<&dyn EntityBase>) -> bool {
let is_projectile = is_projectile_damage(damage_type);
if is_projectile {
for _ in 0..64 {
if self.teleport_randomly() {
return false;
}
}
}
true
}
fn on_damage(&self, _damage_type: DamageType, source: Option<&dyn EntityBase>) {
if source.is_some_and(|s| s.get_living_entity().is_some()) {
return;
}
let should_teleport = self.get_random().random_range(0..10) != 0;
if should_teleport {
self.teleport_randomly();
}
}
}