Pumpkin/crates/pumpkin/src/entity/mob/creaking.rs
2026-08-18 18:49:48 +02:00

547 lines
19 KiB
Rust

use std::sync::{
Arc, Weak,
atomic::{AtomicBool, AtomicI32, Ordering},
};
use crossbeam::atomic::AtomicCell;
use pumpkin_data::attributes::Attributes;
use pumpkin_data::damage::DamageType;
use pumpkin_data::data_component_impl::EquipmentSlot;
use pumpkin_data::entity::EntityType;
use pumpkin_data::item::Item;
use pumpkin_data::particle::Particle;
use pumpkin_data::sound::{Sound, SoundCategory};
use pumpkin_nbt::compound::NbtCompound;
use pumpkin_protocol::java::client::play::{CEntityStatus, Metadata};
use pumpkin_util::math::position::BlockPos;
use pumpkin_util::math::vector3::Vector3;
use crate::block::entities::creaking_heart::CreakingHeartBlockEntity;
use crate::entity::{
Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture,
ai::goal::{
active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal,
look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, swim::SwimGoal,
wander_around::WanderAroundGoal,
},
mob::{Mob, MobEntity},
player::Player,
};
pub const ATTACK_ANIMATION_DURATION: i32 = 15;
pub const MAX_HEALTH: f32 = 1.0;
pub const ATTACK_DAMAGE: f32 = 3.0;
pub const FOLLOW_RANGE: f32 = 32.0;
pub const ACTIVATION_RANGE_SQ: f64 = 144.0;
pub const ATTACK_INTERVAL: i32 = 40;
pub const MOVEMENT_SPEED_WHEN_FIGHTING: f32 = 0.4;
pub const SPEED_MULTIPLIER_WHEN_IDLING: f32 = 0.3;
pub const CREAKING_ORANGE: i32 = 16545810;
pub const CREAKING_GRAY: i32 = 6250335;
pub const INVULNERABILITY_ANIMATION_DURATION: i32 = 8;
pub const TWITCH_DEATH_DURATION: i32 = 45;
pub const MAX_PLAYER_STUCK_COUNTER: i32 = 4;
pub struct CreakingEntity {
pub mob_entity: MobEntity,
can_move: AtomicBool,
is_active: AtomicBool,
is_tearing_down: AtomicBool,
home_pos: AtomicCell<Option<BlockPos>>,
attack_animation_remaining_ticks: AtomicI32,
invulnerability_animation_remaining_ticks: AtomicI32,
eyes_glowing: AtomicBool,
next_flicker_time: AtomicI32,
player_stuck_counter: AtomicI32,
death_time: AtomicI32,
}
impl CreakingEntity {
pub fn new(entity: Entity) -> Arc<Self> {
let mob_entity = MobEntity::new(entity);
let creaking = Self {
mob_entity,
can_move: AtomicBool::new(true),
is_active: AtomicBool::new(false),
is_tearing_down: AtomicBool::new(false),
home_pos: AtomicCell::new(None),
attack_animation_remaining_ticks: AtomicI32::new(0),
invulnerability_animation_remaining_ticks: AtomicI32::new(0),
eyes_glowing: AtomicBool::new(false),
next_flicker_time: AtomicI32::new(0),
player_stuck_counter: AtomicI32::new(0),
death_time: AtomicI32::new(0),
};
// Initialize attributes
{
let mut attributes = creaking
.mob_entity
.living_entity
.attributes
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if let Some(health) = attributes.get_mut(&Attributes::MAX_HEALTH.id) {
health.base_value = f64::from(MAX_HEALTH);
health.dirty.store(true, Ordering::Relaxed);
}
if let Some(speed) = attributes.get_mut(&Attributes::MOVEMENT_SPEED.id) {
speed.base_value = f64::from(MOVEMENT_SPEED_WHEN_FIGHTING);
speed.dirty.store(true, Ordering::Relaxed);
}
if let Some(damage) = attributes.get_mut(&Attributes::ATTACK_DAMAGE.id) {
damage.base_value = f64::from(ATTACK_DAMAGE);
damage.dirty.store(true, Ordering::Relaxed);
}
if let Some(follow) = attributes.get_mut(&Attributes::FOLLOW_RANGE.id) {
follow.base_value = f64::from(FOLLOW_RANGE);
follow.dirty.store(true, Ordering::Relaxed);
}
if let Some(step) = attributes.get_mut(&Attributes::STEP_HEIGHT.id) {
step.base_value = 1.0625;
step.dirty.store(true, Ordering::Relaxed);
}
}
creaking.mob_entity.living_entity.health.store(MAX_HEALTH);
let mob_arc = Arc::new(creaking);
let mob_weak: Weak<dyn Mob> = {
let mob_arc: Arc<dyn Mob> = mob_arc.clone();
Arc::downgrade(&mob_arc)
};
{
let mut goal_selector = mob_arc
.mob_entity
.goals_selector
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
goal_selector.add_goal(0, Box::new(SwimGoal::default()));
goal_selector.add_goal(4, Box::new(MeleeAttackGoal::new(1.0, true)));
goal_selector.add_goal(5, Box::new(WanderAroundGoal::new(1.0)));
goal_selector.add_goal(
6,
LookAtEntityGoal::with_default(mob_weak.clone(), &EntityType::PLAYER, 8.0),
);
goal_selector.add_goal(7, Box::new(RandomLookAroundGoal::default()));
let mut target_selector = mob_arc
.mob_entity
.target_selector
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
target_selector.add_goal(
1,
ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::PLAYER, true),
);
};
mob_arc
}
pub fn set_transient(&self, pos: BlockPos) {
self.set_home_pos(Some(pos));
}
pub fn is_heart_bound(&self) -> bool {
self.get_home_pos().is_some()
}
pub fn can_move(&self) -> bool {
self.can_move.load(Ordering::Relaxed)
}
pub fn set_can_move(&self, can_move: bool) {
self.can_move.store(can_move, Ordering::Relaxed);
self.mob_entity.living_entity.entity.send_meta_data(
&[Metadata::new(
pumpkin_data::tracked_data::creaking::CAN_MOVE,
can_move,
)],
None,
);
}
pub fn is_active(&self) -> bool {
self.is_active.load(Ordering::Relaxed)
}
pub fn set_is_active(&self, active: bool) {
self.is_active.store(active, Ordering::Relaxed);
self.mob_entity.living_entity.entity.send_meta_data(
&[Metadata::new(
pumpkin_data::tracked_data::creaking::IS_ACTIVE,
active,
)],
None,
);
}
pub fn is_tearing_down(&self) -> bool {
self.is_tearing_down.load(Ordering::Relaxed)
}
pub fn set_tearing_down(&self) {
self.is_tearing_down.store(true, Ordering::Relaxed);
self.mob_entity.living_entity.entity.send_meta_data(
&[Metadata::new(
pumpkin_data::tracked_data::creaking::IS_TEARING_DOWN,
true,
)],
None,
);
}
pub fn get_home_pos(&self) -> Option<BlockPos> {
self.home_pos.load()
}
pub fn set_home_pos(&self, pos: Option<BlockPos>) {
self.home_pos.store(pos);
if let Some(p) = pos {
self.mob_entity.position_target.store(p);
self.mob_entity
.position_target_range
.store(32, Ordering::Relaxed);
} else {
self.mob_entity
.position_target_range
.store(-1, Ordering::Relaxed);
}
self.mob_entity.living_entity.entity.send_meta_data(
&[Metadata::new(
pumpkin_data::tracked_data::creaking::HOME_POS,
pos,
)],
None,
);
}
pub fn stop_in_place(&self) {
let entity = &self.mob_entity.living_entity.entity;
entity.velocity.store(Vector3::default());
if let Ok(mut navigator) = self.mob_entity.navigator.lock() {
navigator.stop();
}
}
pub async fn activate(&self, player: &Arc<Player>) {
*self.mob_entity.target.lock().await = Some(player.clone());
self.play_sound(Sound::EntityCreakingActivate);
self.set_is_active(true);
}
pub async fn deactivate(&self) {
*self.mob_entity.target.lock().await = None;
self.play_sound(Sound::EntityCreakingDeactivate);
self.set_is_active(false);
}
pub fn has_glowing_eyes(&self) -> bool {
self.eyes_glowing.load(Ordering::Relaxed)
}
pub fn check_eye_blink(&self) {
let death_time = self.death_time.load(Ordering::Relaxed);
let next_flicker = self.next_flicker_time.load(Ordering::Relaxed);
if death_time > next_flicker {
let glowing = self.eyes_glowing.load(Ordering::Relaxed);
let min_ticks = if glowing { 2 } else { (death_time / 4).max(1) };
let max_ticks = if glowing { 8 } else { (death_time / 2).max(2) };
let added_ticks =
(rand::random::<u32>() as i32 % (max_ticks - min_ticks + 1)).abs() + min_ticks;
self.next_flicker_time
.store(death_time + added_ticks, Ordering::Relaxed);
self.eyes_glowing.store(!glowing, Ordering::Relaxed);
}
}
pub fn player_is_stuck_in_you(&self, players: &[Arc<Player>]) -> bool {
let own_box = self.mob_entity.living_entity.entity.bounding_box.load();
for player in players {
let p = player.get_entity().get_eye_pos();
if p.x >= own_box.min.x
&& p.x <= own_box.max.x
&& p.y >= own_box.min.y
&& p.y <= own_box.max.y
&& p.z >= own_box.min.z
&& p.z <= own_box.max.z
{
let count = self.player_stuck_counter.fetch_add(1, Ordering::Relaxed) + 1;
return count > MAX_PLAYER_STUCK_COUNTER;
}
}
self.player_stuck_counter.store(0, Ordering::Relaxed);
false
}
pub fn is_looking_at_me(&self, player: &Player) -> bool {
let creaking_entity = &self.mob_entity.living_entity.entity;
let creaking_pos = creaking_entity.pos.load();
let eye_y = creaking_entity.get_eye_y();
let feet_y = creaking_pos.y + 0.5;
let mid_y = f64::midpoint(eye_y, creaking_pos.y);
let player_entity = player.get_entity();
let player_eye_pos = player_entity.get_eye_pos();
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 targets_y = [eye_y, feet_y, mid_y];
for target_y in targets_y {
let target = Vector3::new(creaking_pos.x, target_y, creaking_pos.z);
let dir = target - player_eye_pos;
let distance = (dir.x * dir.x + dir.y * dir.y + dir.z * dir.z).sqrt();
if distance > 0.001 {
let norm_dir = Vector3::new(dir.x / distance, dir.y / distance, dir.z / distance);
let dot = look_dir.dot(&norm_dir);
if dot > 1.0 - 0.5 / distance {
return true;
}
}
}
false
}
fn is_player_disguised(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()
{
return head_stack.item == &Item::CARVED_PUMPKIN;
}
false
}
pub async fn check_can_move(&self) -> bool {
let entity = &self.mob_entity.living_entity.entity;
let world = entity.world.load();
let pos = entity.pos.load();
let active = self.is_active();
let players = world.get_nearby_players(pos, 32.0);
if players.is_empty() {
if active {
self.deactivate().await;
}
return true;
}
let mut has_potential_target = false;
for player in players {
if player.living_entity.entity.is_alive()
&& player.gamemode.load() != pumpkin_util::GameMode::Creative
&& player.gamemode.load() != pumpkin_util::GameMode::Spectator
{
has_potential_target = true;
let is_disguised = Self::is_player_disguised(&player);
if (!active || !is_disguised) && self.is_looking_at_me(&player) {
if active {
return false;
}
let target_pos = player.get_entity().pos.load();
let dist_sq = pos.squared_distance_to(target_pos.x, target_pos.y, target_pos.z);
if dist_sq < ACTIVATION_RANGE_SQ {
self.activate(&player).await;
return false;
}
}
}
}
if !has_potential_target && active {
self.deactivate().await;
}
true
}
pub async fn tear_down(&self) {
let entity = &self.mob_entity.living_entity.entity;
let world = entity.world.load();
let pos = entity.pos.load();
world.spawn_particle(
pos,
Vector3::new(0.3, 0.3, 0.3),
0.0,
100,
Particle::BlockCrumble,
);
self.play_sound(Sound::EntityCreakingDeath);
entity.remove().await;
}
pub fn creaking_death_effects(&self) {
self.set_tearing_down();
self.play_sound(Sound::EntityCreakingTwitch);
}
fn play_sound(&self, sound: Sound) {
let entity = &self.mob_entity.living_entity.entity;
let world = entity.world.load();
world.play_sound_fine(sound, SoundCategory::Hostile, &entity.pos.load(), 1.0, 1.0);
}
}
impl NBTStorage for CreakingEntity {
fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
self.mob_entity.living_entity.write_nbt(nbt).await;
if let Some(pos) = self.get_home_pos() {
let mut sub = NbtCompound::new();
sub.put_int("x", pos.0.x);
sub.put_int("y", pos.0.y);
sub.put_int("z", pos.0.z);
nbt.put_compound("home_pos", sub);
}
})
}
fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
self.mob_entity.living_entity.read_nbt_non_mut(nbt).await;
if let Some(sub) = nbt.get_compound("home_pos")
&& let (Some(x), Some(y), Some(z)) =
(sub.get_int("x"), sub.get_int("y"), sub.get_int("z"))
{
let pos = BlockPos::new(x, y, z);
self.set_transient(pos);
}
})
}
}
impl Mob for CreakingEntity {
fn get_mob_entity(&self) -> &MobEntity {
&self.mob_entity
}
fn mob_tick<'a>(&'a self, _caller: &'a Arc<dyn EntityBase>) -> EntityBaseFuture<'a, ()> {
Box::pin(async move {
let entity = &self.mob_entity.living_entity.entity;
if !entity.is_alive() {
return;
}
if self
.invulnerability_animation_remaining_ticks
.load(Ordering::Relaxed)
> 0
{
self.invulnerability_animation_remaining_ticks
.fetch_sub(1, Ordering::Relaxed);
}
if self
.attack_animation_remaining_ticks
.load(Ordering::Relaxed)
> 0
{
self.attack_animation_remaining_ticks
.fetch_sub(1, Ordering::Relaxed);
}
let can_move = self.can_move();
let now_can_move = self.check_can_move().await;
if now_can_move != can_move {
let world = entity.world.load();
if now_can_move {
world.play_sound_fine(
Sound::EntityCreakingUnfreeze,
SoundCategory::Hostile,
&entity.pos.load(),
1.0,
1.0,
);
} else {
self.stop_in_place();
world.play_sound_fine(
Sound::EntityCreakingFreeze,
SoundCategory::Hostile,
&entity.pos.load(),
1.0,
1.0,
);
}
self.set_can_move(now_can_move);
}
// Home Creaking Heart check
if let Some(home_pos) = self.get_home_pos() {
let world = entity.world.load();
let has_protection = world.get_block_entity(&home_pos).is_some_and(|be| {
be.as_any()
.downcast_ref::<CreakingHeartBlockEntity>()
.is_some_and(|heart_be| heart_be.is_protector(entity.entity_uuid))
});
if !has_protection {
self.mob_entity.living_entity.health.store(0.0);
}
}
// Teardown / death tick
if self.is_heart_bound() && self.is_tearing_down() {
let death_time = self.death_time.fetch_add(1, Ordering::Relaxed) + 1;
if death_time > TWITCH_DEATH_DURATION && !entity.is_removed() {
self.tear_down().await;
}
}
})
}
fn pre_damage<'a>(
&'a self,
damage_type: DamageType,
_source: Option<&'a dyn EntityBase>,
) -> EntityBaseFuture<'a, bool> {
Box::pin(async move {
let entity = &self.mob_entity.living_entity.entity;
if self.is_heart_bound() && damage_type != DamageType::OUT_OF_WORLD {
if self
.invulnerability_animation_remaining_ticks
.load(Ordering::Relaxed)
<= 0
&& entity.is_alive()
{
self.invulnerability_animation_remaining_ticks
.store(8, Ordering::Relaxed);
let world = entity.world.load();
world.broadcast_to_chunk(
entity.chunk_pos.load(),
&CEntityStatus::new(entity.entity_id, 66),
);
if let Some(home_pos) = self.get_home_pos()
&& let Some(be) = world.get_block_entity(&home_pos)
&& let Some(heart_be) =
be.as_any().downcast_ref::<CreakingHeartBlockEntity>()
&& heart_be.is_protector(entity.entity_uuid)
{
heart_be.creaking_hurt(&world);
self.play_sound(Sound::EntityCreakingSway);
}
}
// Return false so heart-bound Creaking does not lose health from normal damage
return false;
}
true
})
}
}