From 5a5bc19bbb7bea51b9ea9a85e3824590204df522 Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Thu, 13 Aug 2026 17:28:56 +0200 Subject: [PATCH] feat: add some more entity logic --- .../src/bedrock/server/actor_event.rs | 2 + .../src/java/client/play/entity_metadata.rs | 18 + .../src/block/entities/creaking_heart.rs | 49 +- crates/pumpkin/src/entity/ai/goal/beg.rs | 65 +- .../src/entity/ai/goal/follow_owner.rs | 139 +-- .../pumpkin/src/entity/ai/pathfinder/mod.rs | 8 + .../pumpkin/src/entity/decoration/display.rs | 796 ++++++++++++++++++ .../src/entity/decoration/item_frame.rs | 328 +++++++- crates/pumpkin/src/entity/decoration/mod.rs | 1 + crates/pumpkin/src/entity/interaction.rs | 313 +++++++ crates/pumpkin/src/entity/lightning.rs | 500 +++++++++++ crates/pumpkin/src/entity/living.rs | 7 +- crates/pumpkin/src/entity/marker.rs | 125 +++ crates/pumpkin/src/entity/mob/creaking.rs | 494 ++++++++++- crates/pumpkin/src/entity/mob/creeper.rs | 22 + crates/pumpkin/src/entity/mob/evoker.rs | 574 ++++++++++++- crates/pumpkin/src/entity/mob/mod.rs | 23 + crates/pumpkin/src/entity/mod.rs | 32 + .../pumpkin/src/entity/passive/armadillo.rs | 427 +++++++++- crates/pumpkin/src/entity/passive/cat.rs | 381 ++++++++- crates/pumpkin/src/entity/passive/chicken.rs | 6 +- .../src/entity/passive/copper_golem.rs | 316 +++++++ crates/pumpkin/src/entity/passive/cow.rs | 6 +- crates/pumpkin/src/entity/passive/fox.rs | 290 ++++++- crates/pumpkin/src/entity/passive/frog.rs | 152 +++- .../pumpkin/src/entity/passive/happy_ghast.rs | 249 ++++++ .../pumpkin/src/entity/passive/iron_golem.rs | 113 ++- crates/pumpkin/src/entity/passive/mod.rs | 3 + crates/pumpkin/src/entity/passive/nautilus.rs | 416 +++++++++ crates/pumpkin/src/entity/passive/ocelot.rs | 192 ++++- crates/pumpkin/src/entity/passive/pig.rs | 6 +- crates/pumpkin/src/entity/passive/sheep.rs | 33 +- crates/pumpkin/src/entity/passive/sniffer.rs | 413 ++++++++- crates/pumpkin/src/entity/passive/wolf.rs | 175 +++- .../src/entity/projectile/evoker_fangs.rs | 115 +++ .../pumpkin/src/entity/projectile/fireball.rs | 216 ++++- crates/pumpkin/src/entity/projectile/mod.rs | 1 + crates/pumpkin/src/entity/type.rs | 23 +- crates/pumpkin/src/world/mod.rs | 27 +- 39 files changed, 6811 insertions(+), 245 deletions(-) create mode 100644 crates/pumpkin/src/entity/decoration/display.rs create mode 100644 crates/pumpkin/src/entity/interaction.rs create mode 100644 crates/pumpkin/src/entity/lightning.rs create mode 100644 crates/pumpkin/src/entity/marker.rs create mode 100644 crates/pumpkin/src/entity/passive/copper_golem.rs create mode 100644 crates/pumpkin/src/entity/passive/happy_ghast.rs create mode 100644 crates/pumpkin/src/entity/passive/nautilus.rs create mode 100644 crates/pumpkin/src/entity/projectile/evoker_fangs.rs diff --git a/crates/pumpkin-protocol/src/bedrock/server/actor_event.rs b/crates/pumpkin-protocol/src/bedrock/server/actor_event.rs index 0afcbb007..caecbf2aa 100644 --- a/crates/pumpkin-protocol/src/bedrock/server/actor_event.rs +++ b/crates/pumpkin-protocol/src/bedrock/server/actor_event.rs @@ -39,6 +39,8 @@ pub enum ActorEventType { StartOfferFlower = 19, StopOfferFlower = 20, LoveHearts = 21, + TrustingFailed = 40, + TrustingSucceeded = 41, VillagerAngry = 22, VillagerHappy = 23, WitchHatMagic = 24, diff --git a/crates/pumpkin-protocol/src/java/client/play/entity_metadata.rs b/crates/pumpkin-protocol/src/java/client/play/entity_metadata.rs index 0c4c04c40..d4a142af0 100644 --- a/crates/pumpkin-protocol/src/java/client/play/entity_metadata.rs +++ b/crates/pumpkin-protocol/src/java/client/play/entity_metadata.rs @@ -283,6 +283,24 @@ impl MetadataSerializer for crate::codec::optional_int::OptionalInt { } } +impl MetadataSerializer for uuid::Uuid { + fn write_metadata(&self, writer: &mut impl std::io::Write) -> Result<(), WritingError> { + writer.write_uuid(self) + } +} + +impl MetadataSerializer for Option { + fn write_metadata(&self, writer: &mut impl std::io::Write) -> Result<(), WritingError> { + if let Some(uuid) = self { + writer.write_bool(true)?; + writer.write_uuid(uuid)?; + } else { + writer.write_bool(false)?; + } + Ok(()) + } +} + #[cfg(test)] mod tests { use std::io::{Cursor, Read}; diff --git a/crates/pumpkin/src/block/entities/creaking_heart.rs b/crates/pumpkin/src/block/entities/creaking_heart.rs index 21cd2937d..b707491ec 100644 --- a/crates/pumpkin/src/block/entities/creaking_heart.rs +++ b/crates/pumpkin/src/block/entities/creaking_heart.rs @@ -1,12 +1,19 @@ use std::pin::Pin; +use std::sync::Arc; +use crossbeam::atomic::AtomicCell; +use pumpkin_data::sound::{Sound, SoundCategory}; use pumpkin_nbt::compound::NbtCompound; use pumpkin_util::math::position::BlockPos; +use uuid::Uuid; + +use crate::world::World; use super::BlockEntity; pub struct CreakingHeartBlockEntity { pub position: BlockPos, + pub creaking_uuid: AtomicCell>, } impl BlockEntity for CreakingHeartBlockEntity { @@ -18,18 +25,28 @@ impl BlockEntity for CreakingHeartBlockEntity { self.position } - fn from_nbt(_nbt: &pumpkin_nbt::compound::NbtCompound, position: BlockPos) -> Self + fn from_nbt(nbt: &pumpkin_nbt::compound::NbtCompound, position: BlockPos) -> Self where Self: Sized, { - Self { position } + let creaking_uuid = nbt + .get_string("creaking_uuid") + .and_then(|uuid_str| Uuid::parse_str(uuid_str).ok()); + Self { + position, + creaking_uuid: AtomicCell::new(creaking_uuid), + } } fn write_nbt<'a>( &'a self, - _nbt: &'a mut NbtCompound, + nbt: &'a mut NbtCompound, ) -> Pin + Send + 'a>> { - Box::pin(async {}) + Box::pin(async move { + if let Some(uuid) = self.creaking_uuid.load() { + nbt.put_string("creaking_uuid", uuid.to_string()); + } + }) } fn as_any(&self) -> &dyn std::any::Any { @@ -39,8 +56,30 @@ impl BlockEntity for CreakingHeartBlockEntity { impl CreakingHeartBlockEntity { pub const ID: &'static str = "minecraft:creaking_heart"; + #[must_use] pub const fn new(position: BlockPos) -> Self { - Self { position } + Self { + position, + creaking_uuid: AtomicCell::new(None), + } + } + + pub fn is_protector(&self, creaking_uuid: Uuid) -> bool { + self.creaking_uuid + .load() + .is_none_or(|uuid| uuid == creaking_uuid) + } + + pub fn set_creaking_uuid(&self, uuid: Option) { + self.creaking_uuid.store(uuid); + } + + pub fn creaking_hurt(&self, world: &Arc) { + world.play_sound( + Sound::BlockCreakingHeartHurt, + SoundCategory::Blocks, + &self.position.to_f64(), + ); } } diff --git a/crates/pumpkin/src/entity/ai/goal/beg.rs b/crates/pumpkin/src/entity/ai/goal/beg.rs index dabd48930..cfa1abe48 100644 --- a/crates/pumpkin/src/entity/ai/goal/beg.rs +++ b/crates/pumpkin/src/entity/ai/goal/beg.rs @@ -4,41 +4,40 @@ use crate::entity::mob::Mob; use crate::entity::player::Player; use pumpkin_data::item::Item; use pumpkin_data::meta_data_type::MetaDataType; +use pumpkin_data::tag::{self, Taggable}; use pumpkin_data::tracked_data::TrackedData; use pumpkin_protocol::java::client::play::Metadata; use rand::RngExt; use std::sync::Arc; pub struct BegGoal { - beg_distance_sq: f64, - attractive_items: &'static [&'static Item], - timer: i32, - target: Option>, + look_distance_sq: f64, + look_time: i32, + player: Option>, } impl BegGoal { #[must_use] - pub fn new(beg_distance: f32, attractive_items: &'static [&'static Item]) -> Box { + pub fn new(look_distance: f32) -> Box { Box::new(Self { - beg_distance_sq: (beg_distance * beg_distance) as f64, - attractive_items, - timer: 0, - target: None, + look_distance_sq: f64::from(look_distance) * f64::from(look_distance), + look_time: 0, + player: None, }) } - fn is_attractive(&self, item: &Item) -> bool { - self.attractive_items.iter().any(|i| i.id == item.id) + fn is_interesting_item(item: &Item) -> bool { + item.id == Item::BONE.id || item.has_tag(&tag::Item::MINECRAFT_WOLF_FOOD) } - async fn is_player_holding_attractive(&self, player: &Player) -> bool { + async fn player_holding_interesting(&self, player: &Player) -> bool { let main_stack = player.inventory().held_item().await; - if main_stack.item_count > 0 && self.is_attractive(main_stack.item) { + if main_stack.item_count > 0 && Self::is_interesting_item(main_stack.item) { return true; } let off_stack = player.inventory().off_hand_item().await; - off_stack.item_count > 0 && self.is_attractive(off_stack.item) + off_stack.item_count > 0 && Self::is_interesting_item(off_stack.item) } fn distance_sq(mob: &dyn Mob, player: &Player) -> f64 { @@ -47,12 +46,12 @@ impl BegGoal { mob_pos.squared_distance_to_vec(&player_pos) } - fn set_begging(mob: &dyn Mob, begging: bool) { + fn set_is_interested(mob: &dyn Mob, value: bool) { mob.get_mob_entity().living_entity.entity.send_meta_data( &[Metadata::new( TrackedData::INTERESTED_ID, MetaDataType::BOOLEAN, - begging, + value, )], None, ); @@ -61,28 +60,28 @@ impl BegGoal { impl Goal for BegGoal { fn can_start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> { - Box::pin(async { + Box::pin(async move { let entity = &mob.get_mob_entity().living_entity.entity; let world = entity.world.load_full(); let pos = entity.pos.load(); - let radius = self.beg_distance_sq.sqrt(); + let radius = self.look_distance_sq.sqrt(); let Some(player) = world.get_closest_player(pos, radius) else { return false; }; - if !self.is_player_holding_attractive(&player).await { + if !self.player_holding_interesting(&player).await { return false; } - self.target = Some(player); + self.player = Some(player); true }) } fn should_continue<'a>(&'a self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> { - Box::pin(async { - let Some(player) = &self.target else { + Box::pin(async move { + let Some(player) = &self.player else { return false; }; @@ -90,32 +89,32 @@ impl Goal for BegGoal { return false; } - if Self::distance_sq(mob, player) > self.beg_distance_sq { + if Self::distance_sq(mob, player) > self.look_distance_sq { return false; } - self.timer > 0 && self.is_player_holding_attractive(player).await + self.look_time > 0 && self.player_holding_interesting(player).await }) } fn start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> { - Box::pin(async { - Self::set_begging(mob, true); + Box::pin(async move { + Self::set_is_interested(mob, true); let ticks = 40 + mob.get_random().random_range(0..40); - self.timer = self.get_tick_count(ticks); + self.look_time = self.get_tick_count(ticks); }) } fn stop<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> { - Box::pin(async { - Self::set_begging(mob, false); - self.target = None; + Box::pin(async move { + Self::set_is_interested(mob, false); + self.player = None; }) } fn tick<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> { - Box::pin(async { - if let Some(player) = &self.target { + Box::pin(async move { + if let Some(player) = &self.player { let player_pos = player.get_entity().get_eye_pos(); let mut look_control = mob .get_mob_entity() @@ -130,7 +129,7 @@ impl Goal for BegGoal { mob.get_max_look_pitch_change(), ); } - self.timer -= 1; + self.look_time -= 1; }) } diff --git a/crates/pumpkin/src/entity/ai/goal/follow_owner.rs b/crates/pumpkin/src/entity/ai/goal/follow_owner.rs index 73377204e..261a4e839 100644 --- a/crates/pumpkin/src/entity/ai/goal/follow_owner.rs +++ b/crates/pumpkin/src/entity/ai/goal/follow_owner.rs @@ -1,6 +1,7 @@ use super::{Controls, Goal, GoalFuture, to_goal_ticks}; use crate::entity::EntityBase; use crate::entity::ai::pathfinder::NavigatorGoal; +use crate::entity::ai::pathfinder::node::PathType; use crate::entity::mob::Mob; use crate::entity::player::Player; use pumpkin_util::math::position::BlockPos; @@ -11,27 +12,42 @@ use std::sync::Arc; const TELEPORT_DISTANCE_SQ: f64 = 144.0; pub struct FollowOwnerGoal { - speed: f64, - min_distance_sq: f64, - max_distance_sq: f64, - update_countdown: i32, + speed_modifier: f64, + start_distance_sq: f64, + stop_distance_sq: f64, + time_to_recalc_path: i32, owner: Option>, + old_water_cost: f32, } impl FollowOwnerGoal { #[must_use] - pub fn new(speed: f64, min_distance: f32, max_distance: f32) -> Box { + pub fn new(speed_modifier: f64, start_distance: f32, stop_distance: f32) -> Box { Box::new(Self { - speed, - min_distance_sq: (min_distance * min_distance) as f64, - max_distance_sq: (max_distance * max_distance) as f64, - update_countdown: 0, + speed_modifier, + start_distance_sq: f64::from(start_distance) * f64::from(start_distance), + stop_distance_sq: f64::from(stop_distance) * f64::from(stop_distance), + time_to_recalc_path: 0, owner: None, + old_water_cost: 0.0, }) } - fn can_follow(mob: &dyn Mob) -> bool { - !mob.is_sitting() + async fn unable_to_move_to_owner(mob: &dyn Mob, owner: Option<&Player>) -> bool { + if mob.is_sitting() { + return true; + } + let mob_entity = &mob.get_mob_entity().living_entity.entity; + if mob_entity.has_vehicle().await || mob_entity.is_leashed().await { + return true; + } + let Some(owner) = owner else { + return true; + }; + if owner.is_spectator() || !owner.living_entity.entity.is_alive() { + return true; + } + false } fn find_owner(mob: &dyn Mob) -> Option> { @@ -50,6 +66,11 @@ impl FollowOwnerGoal { mob_pos.squared_distance_to_vec(&owner_pos) } + fn should_try_teleport_to_owner(mob: &dyn Mob, owner: &Player) -> bool { + let dist_sq = Self::distance_to_owner_sq(mob, owner); + dist_sq >= TELEPORT_DISTANCE_SQ + } + fn try_teleport_to_owner(mob: &dyn Mob, owner: &Player) { let owner_pos = owner.living_entity.entity.pos.load(); let mob_entity = &mob.get_mob_entity().living_entity.entity; @@ -92,7 +113,7 @@ impl FollowOwnerGoal { mob_entity.teleport( Vector3::new( target_x as f64 + 0.5, - target_y as f64, + f64::from(target_y), target_z as f64 + 0.5, ), None, @@ -113,17 +134,17 @@ impl FollowOwnerGoal { impl Goal for FollowOwnerGoal { fn can_start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> { - Box::pin(async { - if !Self::can_follow(mob) { - return false; - } - + Box::pin(async move { let Some(owner) = Self::find_owner(mob) else { return false; }; + if Self::unable_to_move_to_owner(mob, Some(&owner)).await { + return false; + } + let dist_sq = Self::distance_to_owner_sq(mob, &owner); - if dist_sq < self.min_distance_sq { + if dist_sq < self.start_distance_sq { return false; } @@ -133,8 +154,20 @@ impl Goal for FollowOwnerGoal { } fn should_continue<'a>(&'a self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> { - Box::pin(async { - if !Self::can_follow(mob) { + Box::pin(async move { + let is_idle = { + let navigator = mob + .get_mob_entity() + .navigator + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + navigator.is_idle() + }; + if is_idle { + return false; + } + + if Self::unable_to_move_to_owner(mob, self.owner.as_deref()).await { return false; } @@ -142,40 +175,46 @@ impl Goal for FollowOwnerGoal { return false; }; - if owner.is_spectator() || !owner.living_entity.entity.is_alive() { - return false; - } - let dist_sq = Self::distance_to_owner_sq(mob, owner); - if dist_sq <= self.max_distance_sq { - return false; - } + dist_sq > self.stop_distance_sq + }) + } - let navigator = mob + fn start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> { + Box::pin(async move { + self.time_to_recalc_path = 0; + let mut navigator = mob .get_mob_entity() .navigator .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); - !navigator.is_idle() + self.old_water_cost = navigator.get_pathfinding_malus(PathType::Water); + navigator.set_pathfinding_malus(PathType::Water, 0.0); }) } - fn start<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, ()> { - Box::pin(async { - self.update_countdown = 0; + fn stop<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> { + Box::pin(async move { + self.owner = None; + let mut navigator = mob + .get_mob_entity() + .navigator + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + navigator.stop(); + navigator.set_pathfinding_malus(PathType::Water, self.old_water_cost); }) } fn tick<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> { - Box::pin(async { + Box::pin(async move { let Some(owner) = &self.owner else { return; }; - let dist_sq = Self::distance_to_owner_sq(mob, owner); - let should_teleport = dist_sq >= TELEPORT_DISTANCE_SQ; + let is_owner_far_away = Self::should_try_teleport_to_owner(mob, owner); - if !should_teleport { + if !is_owner_far_away { let mob_entity = mob.get_mob_entity(); let owner_eye_pos = owner.living_entity.entity.get_eye_pos(); let mut look_control = mob_entity @@ -189,14 +228,12 @@ impl Goal for FollowOwnerGoal { 10.0, mob.get_max_look_pitch_change(), ); - drop(look_control); } - self.update_countdown -= 1; - if self.update_countdown <= 0 { - self.update_countdown = to_goal_ticks(10); - - if should_teleport { + self.time_to_recalc_path -= 1; + if self.time_to_recalc_path <= 0 { + self.time_to_recalc_path = to_goal_ticks(10); + if is_owner_far_away { Self::try_teleport_to_owner(mob, owner); } else { let mob_pos = mob.get_mob_entity().living_entity.entity.pos.load(); @@ -206,24 +243,16 @@ impl Goal for FollowOwnerGoal { .navigator .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); - navigator.set_progress(NavigatorGoal::new(mob_pos, owner_pos, self.speed)); + navigator.set_progress(NavigatorGoal::new( + mob_pos, + owner_pos, + self.speed_modifier, + )); } } }) } - fn stop<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> { - Box::pin(async { - self.owner = None; - let mut navigator = mob - .get_mob_entity() - .navigator - .lock() - .unwrap_or_else(std::sync::PoisonError::into_inner); - navigator.stop(); - }) - } - fn should_run_every_tick(&self) -> bool { true } diff --git a/crates/pumpkin/src/entity/ai/pathfinder/mod.rs b/crates/pumpkin/src/entity/ai/pathfinder/mod.rs index a85274f13..b8e4c27f8 100644 --- a/crates/pumpkin/src/entity/ai/pathfinder/mod.rs +++ b/crates/pumpkin/src/entity/ai/pathfinder/mod.rs @@ -123,6 +123,14 @@ impl Navigator { self.path_type_overrides.insert(path_type, malus); } + #[must_use] + pub fn get_pathfinding_malus(&self, path_type: PathType) -> f32 { + self.path_type_overrides + .get(&path_type) + .copied() + .unwrap_or_else(|| path_type.get_malus()) + } + pub const fn set_mob_dimensions(&mut self, width: f32, height: f32) { self.mob_width = width; self.mob_height = height; diff --git a/crates/pumpkin/src/entity/decoration/display.rs b/crates/pumpkin/src/entity/decoration/display.rs new file mode 100644 index 000000000..9579f852d --- /dev/null +++ b/crates/pumpkin/src/entity/decoration/display.rs @@ -0,0 +1,796 @@ +use std::sync::{ + Arc, + atomic::{AtomicI8, AtomicI32, AtomicU8, Ordering}, +}; +use tokio::sync::Mutex; + +use pumpkin_data::{ + damage::DamageType, item_stack::ItemStack, meta_data_type::MetaDataType, + tracked_data::TrackedData, +}; +use pumpkin_nbt::{compound::NbtCompound, tag::NbtTag}; +use pumpkin_protocol::{ + codec::{item_stack_seralizer::ItemStackSerializer, var_int::VarInt}, + java::client::play::{Metadata, MetadataSerializer}, + ser::{NetworkWriteExt, WritingError}, +}; +use pumpkin_util::{math::vector3::Vector3, text::TextComponent}; + +use crate::{ + entity::{Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture, living::LivingEntity}, + server::Server, +}; + +#[derive(Clone, Copy, Debug)] +pub struct Vector3fSerializer(pub f32, pub f32, pub f32); + +impl MetadataSerializer for Vector3fSerializer { + fn write_metadata(&self, writer: &mut impl std::io::Write) -> Result<(), WritingError> { + writer.write_f32(self.0)?; + writer.write_f32(self.1)?; + writer.write_f32(self.2) + } +} + +#[derive(Clone, Copy, Debug)] +pub struct QuaternionfSerializer(pub f32, pub f32, pub f32, pub f32); + +impl MetadataSerializer for QuaternionfSerializer { + fn write_metadata(&self, writer: &mut impl std::io::Write) -> Result<(), WritingError> { + writer.write_f32(self.0)?; + writer.write_f32(self.1)?; + writer.write_f32(self.2)?; + writer.write_f32(self.3) + } +} + +pub struct DisplayEntity { + pub entity: Entity, + pub interpolation_start_delta_ticks: AtomicI32, + pub interpolation_duration: AtomicI32, + pub view_range: Mutex, + pub shadow_radius: Mutex, + pub shadow_strength: Mutex, + pub width: Mutex, + pub height: Mutex, + pub glow_color_override: AtomicI32, + pub billboard: AtomicU8, + pub brightness: AtomicI32, + pub translation: Mutex>, + pub scale: Mutex>, + pub left_rotation: Mutex<[f32; 4]>, + pub right_rotation: Mutex<[f32; 4]>, +} + +impl DisplayEntity { + pub fn new(entity: Entity) -> Self { + entity.no_clip.store(true, Ordering::Relaxed); + Self { + entity, + interpolation_start_delta_ticks: AtomicI32::new(0), + interpolation_duration: AtomicI32::new(0), + view_range: Mutex::new(1.0), + shadow_radius: Mutex::new(0.0), + shadow_strength: Mutex::new(1.0), + width: Mutex::new(0.0), + height: Mutex::new(0.0), + glow_color_override: AtomicI32::new(-1), + billboard: AtomicU8::new(0), + brightness: AtomicI32::new(-1), + translation: Mutex::new(Vector3::new(0.0, 0.0, 0.0)), + scale: Mutex::new(Vector3::new(1.0, 1.0, 1.0)), + left_rotation: Mutex::new([0.0, 0.0, 0.0, 1.0]), + right_rotation: Mutex::new([0.0, 0.0, 0.0, 1.0]), + } + } + + #[allow(clippy::too_many_lines)] + pub async fn init_display_data_tracker(&self) { + let view_range = *self.view_range.lock().await; + let shadow_radius = *self.shadow_radius.lock().await; + let shadow_strength = *self.shadow_strength.lock().await; + let width = *self.width.lock().await; + let height = *self.height.lock().await; + let translation = *self.translation.lock().await; + let scale = *self.scale.lock().await; + let left_rotation = *self.left_rotation.lock().await; + let right_rotation = *self.right_rotation.lock().await; + + self.entity.send_meta_data( + &[Metadata::new( + TrackedData::START_INTERPOLATION, + MetaDataType::INT, + VarInt(self.interpolation_start_delta_ticks.load(Ordering::Relaxed)), + )], + None, + ); + self.entity.send_meta_data( + &[Metadata::new( + TrackedData::INTERPOLATION_DURATION, + MetaDataType::INT, + VarInt(self.interpolation_duration.load(Ordering::Relaxed)), + )], + None, + ); + self.entity.send_meta_data( + &[Metadata::new( + TrackedData::TRANSLATION, + MetaDataType::VECTOR_3F, + Vector3fSerializer(translation.x, translation.y, translation.z), + )], + None, + ); + self.entity.send_meta_data( + &[Metadata::new( + TrackedData::SCALE, + MetaDataType::VECTOR_3F, + Vector3fSerializer(scale.x, scale.y, scale.z), + )], + None, + ); + self.entity.send_meta_data( + &[Metadata::new( + TrackedData::LEFT_ROTATION, + MetaDataType::QUATERNION_F, + QuaternionfSerializer( + left_rotation[0], + left_rotation[1], + left_rotation[2], + left_rotation[3], + ), + )], + None, + ); + self.entity.send_meta_data( + &[Metadata::new( + TrackedData::RIGHT_ROTATION, + MetaDataType::QUATERNION_F, + QuaternionfSerializer( + right_rotation[0], + right_rotation[1], + right_rotation[2], + right_rotation[3], + ), + )], + None, + ); + self.entity.send_meta_data( + &[Metadata::new( + TrackedData::BILLBOARD, + MetaDataType::BYTE, + self.billboard.load(Ordering::Relaxed), + )], + None, + ); + self.entity.send_meta_data( + &[Metadata::new( + TrackedData::BRIGHTNESS, + MetaDataType::INT, + VarInt(self.brightness.load(Ordering::Relaxed)), + )], + None, + ); + self.entity.send_meta_data( + &[Metadata::new( + TrackedData::VIEW_RANGE, + MetaDataType::FLOAT, + view_range, + )], + None, + ); + self.entity.send_meta_data( + &[Metadata::new( + TrackedData::SHADOW_RADIUS, + MetaDataType::FLOAT, + shadow_radius, + )], + None, + ); + self.entity.send_meta_data( + &[Metadata::new( + TrackedData::SHADOW_STRENGTH, + MetaDataType::FLOAT, + shadow_strength, + )], + None, + ); + self.entity.send_meta_data( + &[Metadata::new( + TrackedData::WIDTH, + MetaDataType::FLOAT, + width, + )], + None, + ); + self.entity.send_meta_data( + &[Metadata::new( + TrackedData::HEIGHT, + MetaDataType::FLOAT, + height, + )], + None, + ); + } + + pub async fn write_display_nbt(&self, nbt: &mut NbtCompound) { + self.entity.write_nbt(nbt).await; + nbt.put_int( + "interpolation_duration", + self.interpolation_duration.load(Ordering::Relaxed), + ); + nbt.put_int( + "start_interpolation", + self.interpolation_start_delta_ticks.load(Ordering::Relaxed), + ); + nbt.put_float("view_range", *self.view_range.lock().await); + nbt.put_float("shadow_radius", *self.shadow_radius.lock().await); + nbt.put_float("shadow_strength", *self.shadow_strength.lock().await); + nbt.put_float("width", *self.width.lock().await); + nbt.put_float("height", *self.height.lock().await); + nbt.put_int( + "glow_color_override", + self.glow_color_override.load(Ordering::Relaxed), + ); + + let billboard_str = match self.billboard.load(Ordering::Relaxed) { + 1 => "vertical", + 2 => "horizontal", + 3 => "center", + _ => "fixed", + }; + nbt.put_string("billboard", billboard_str.to_string()); + + let mut transform = NbtCompound::new(); + let translation = *self.translation.lock().await; + transform.put( + "translation", + NbtTag::List(vec![ + translation.x.into(), + translation.y.into(), + translation.z.into(), + ]), + ); + let scale = *self.scale.lock().await; + transform.put( + "scale", + NbtTag::List(vec![scale.x.into(), scale.y.into(), scale.z.into()]), + ); + let left_rot = *self.left_rotation.lock().await; + transform.put( + "left_rotation", + NbtTag::List(vec![ + left_rot[0].into(), + left_rot[1].into(), + left_rot[2].into(), + left_rot[3].into(), + ]), + ); + let right_rot = *self.right_rotation.lock().await; + transform.put( + "right_rotation", + NbtTag::List(vec![ + right_rot[0].into(), + right_rot[1].into(), + right_rot[2].into(), + right_rot[3].into(), + ]), + ); + nbt.put("transformation", NbtTag::Compound(transform)); + } + + pub async fn read_display_nbt(&self, nbt: &NbtCompound) { + self.entity.read_nbt_non_mut(nbt).await; + + if let Some(dur) = nbt.get_int("interpolation_duration") { + self.interpolation_duration.store(dur, Ordering::Relaxed); + } + if let Some(start) = nbt.get_int("start_interpolation") { + self.interpolation_start_delta_ticks + .store(start, Ordering::Relaxed); + } + if let Some(vr) = nbt.get_float("view_range") { + *self.view_range.lock().await = vr; + } + if let Some(sr) = nbt.get_float("shadow_radius") { + *self.shadow_radius.lock().await = sr; + } + if let Some(ss) = nbt.get_float("shadow_strength") { + *self.shadow_strength.lock().await = ss; + } + if let Some(w) = nbt.get_float("width") { + *self.width.lock().await = w; + } + if let Some(h) = nbt.get_float("height") { + *self.height.lock().await = h; + } + if let Some(glow) = nbt.get_int("glow_color_override") { + self.glow_color_override.store(glow, Ordering::Relaxed); + } + if let Some(bb) = nbt.get_string("billboard") { + let mode = match bb { + "vertical" => 1, + "horizontal" => 2, + "center" => 3, + _ => 0, + }; + self.billboard.store(mode, Ordering::Relaxed); + } + + if let Some(transform) = nbt.get_compound("transformation") { + if let Some(t_list) = transform.get_list("translation") + && t_list.len() >= 3 + { + let x = t_list[0].extract_float().unwrap_or(0.0); + let y = t_list[1].extract_float().unwrap_or(0.0); + let z = t_list[2].extract_float().unwrap_or(0.0); + *self.translation.lock().await = Vector3::new(x, y, z); + } + if let Some(s_list) = transform.get_list("scale") + && s_list.len() >= 3 + { + let x = s_list[0].extract_float().unwrap_or(1.0); + let y = s_list[1].extract_float().unwrap_or(1.0); + let z = s_list[2].extract_float().unwrap_or(1.0); + *self.scale.lock().await = Vector3::new(x, y, z); + } + if let Some(lr_list) = transform.get_list("left_rotation") + && lr_list.len() >= 4 + { + let x = lr_list[0].extract_float().unwrap_or(0.0); + let y = lr_list[1].extract_float().unwrap_or(0.0); + let z = lr_list[2].extract_float().unwrap_or(0.0); + let w = lr_list[3].extract_float().unwrap_or(1.0); + *self.left_rotation.lock().await = [x, y, z, w]; + } + if let Some(rr_list) = transform.get_list("right_rotation") + && rr_list.len() >= 4 + { + let x = rr_list[0].extract_float().unwrap_or(0.0); + let y = rr_list[1].extract_float().unwrap_or(0.0); + let z = rr_list[2].extract_float().unwrap_or(0.0); + let w = rr_list[3].extract_float().unwrap_or(1.0); + *self.right_rotation.lock().await = [x, y, z, w]; + } + } + } +} + +pub struct BlockDisplayEntity { + pub display: DisplayEntity, + pub block_state: AtomicI32, +} + +impl BlockDisplayEntity { + pub fn new(entity: Entity) -> Arc { + Arc::new(Self { + display: DisplayEntity::new(entity), + block_state: AtomicI32::new(0), + }) + } +} + +impl NBTStorage for BlockDisplayEntity { + fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> { + Box::pin(async move { + self.display.write_display_nbt(nbt).await; + nbt.put_int("block_state", self.block_state.load(Ordering::Relaxed)); + }) + } + + fn read_nbt<'a>(&'a mut self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> { + Box::pin(async move { + self.read_nbt_non_mut(nbt).await; + }) + } + + fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> { + Box::pin(async move { + self.display.read_display_nbt(nbt).await; + if let Some(state) = nbt.get_int("block_state") { + self.block_state.store(state, Ordering::Relaxed); + } + }) + } +} + +impl EntityBase for BlockDisplayEntity { + fn tick<'a>( + &'a self, + _caller: &'a Arc, + _server: &'a Server, + ) -> EntityBaseFuture<'a, ()> { + Box::pin(async move {}) + } + + fn init_data_tracker(&self) -> EntityBaseFuture<'_, ()> { + Box::pin(async move { + self.display.init_display_data_tracker().await; + self.display.entity.send_meta_data( + &[Metadata::new( + TrackedData::BLOCK_STATE, + MetaDataType::BLOCK_STATE, + VarInt(self.block_state.load(Ordering::Relaxed)), + )], + None, + ); + }) + } + + fn get_entity(&self) -> &Entity { + &self.display.entity + } + + fn get_living_entity(&self) -> Option<&LivingEntity> { + None + } + + fn as_nbt_storage(&self) -> &dyn NBTStorage { + self + } + + fn cast_any(&self) -> &dyn std::any::Any { + self + } + + fn is_pushable(&self) -> bool { + false + } + + fn is_pushed_by_fluids(&self) -> bool { + false + } + + fn can_hit(&self) -> bool { + false + } + + fn is_immune_to_explosion(&self) -> bool { + true + } + + fn damage_with_context<'a>( + &'a self, + _caller: &'a dyn EntityBase, + _amount: f32, + _damage_type: DamageType, + _position: Option>, + _source: Option<&'a dyn EntityBase>, + _cause: Option<&'a dyn EntityBase>, + ) -> EntityBaseFuture<'a, bool> { + Box::pin(async move { false }) + } +} + +pub struct ItemDisplayEntity { + pub display: DisplayEntity, + pub item_stack: Mutex, + pub item_display: AtomicU8, +} + +impl ItemDisplayEntity { + pub fn new(entity: Entity) -> Arc { + Arc::new(Self { + display: DisplayEntity::new(entity), + item_stack: Mutex::new(ItemStack::new(0, &pumpkin_data::item::Item::AIR)), + item_display: AtomicU8::new(0), + }) + } +} + +impl NBTStorage for ItemDisplayEntity { + fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> { + Box::pin(async move { + self.display.write_display_nbt(nbt).await; + let display_mode_str = match self.item_display.load(Ordering::Relaxed) { + 1 => "thirdperson_lefthand", + 2 => "thirdperson_righthand", + 3 => "firstperson_lefthand", + 4 => "firstperson_righthand", + 5 => "head", + 6 => "gui", + 7 => "ground", + 8 => "fixed", + _ => "none", + }; + nbt.put_string("item_display", display_mode_str.to_string()); + }) + } + + fn read_nbt<'a>(&'a mut self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> { + Box::pin(async move { + self.read_nbt_non_mut(nbt).await; + }) + } + + fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> { + Box::pin(async move { + self.display.read_display_nbt(nbt).await; + if let Some(mode_str) = nbt.get_string("item_display") { + let mode = match mode_str { + "thirdperson_lefthand" => 1, + "thirdperson_righthand" => 2, + "firstperson_lefthand" => 3, + "firstperson_righthand" => 4, + "head" => 5, + "gui" => 6, + "ground" => 7, + "fixed" => 8, + _ => 0, + }; + self.item_display.store(mode, Ordering::Relaxed); + } + }) + } +} + +impl EntityBase for ItemDisplayEntity { + fn tick<'a>( + &'a self, + _caller: &'a Arc, + _server: &'a Server, + ) -> EntityBaseFuture<'a, ()> { + Box::pin(async move {}) + } + + fn init_data_tracker(&self) -> EntityBaseFuture<'_, ()> { + Box::pin(async move { + self.display.init_display_data_tracker().await; + self.display.entity.send_meta_data( + &[Metadata::new( + TrackedData::ITEM, + MetaDataType::ITEM_STACK, + ItemStackSerializer::from(self.item_stack.lock().await.clone()), + )], + None, + ); + self.display.entity.send_meta_data( + &[Metadata::new( + TrackedData::ITEM_DISPLAY, + MetaDataType::BYTE, + self.item_display.load(Ordering::Relaxed), + )], + None, + ); + }) + } + + fn get_entity(&self) -> &Entity { + &self.display.entity + } + + fn get_living_entity(&self) -> Option<&LivingEntity> { + None + } + + fn as_nbt_storage(&self) -> &dyn NBTStorage { + self + } + + fn cast_any(&self) -> &dyn std::any::Any { + self + } + + fn is_pushable(&self) -> bool { + false + } + + fn is_pushed_by_fluids(&self) -> bool { + false + } + + fn can_hit(&self) -> bool { + false + } + + fn is_immune_to_explosion(&self) -> bool { + true + } + + fn damage_with_context<'a>( + &'a self, + _caller: &'a dyn EntityBase, + _amount: f32, + _damage_type: DamageType, + _position: Option>, + _source: Option<&'a dyn EntityBase>, + _cause: Option<&'a dyn EntityBase>, + ) -> EntityBaseFuture<'a, bool> { + Box::pin(async move { false }) + } +} + +pub struct TextDisplayEntity { + pub display: DisplayEntity, + pub text: Mutex, + pub line_width: AtomicI32, + pub background: AtomicI32, + pub text_opacity: AtomicI8, + pub flags: AtomicU8, +} + +impl TextDisplayEntity { + pub fn new(entity: Entity) -> Arc { + Arc::new(Self { + display: DisplayEntity::new(entity), + text: Mutex::new(TextComponent::text("")), + line_width: AtomicI32::new(200), + background: AtomicI32::new(1_073_741_824), + text_opacity: AtomicI8::new(-1), + flags: AtomicU8::new(0), + }) + } +} + +impl NBTStorage for TextDisplayEntity { + fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> { + Box::pin(async move { + self.display.write_display_nbt(nbt).await; + let text_json_res = pumpkin_util::serde_json::to_string(&*self.text.lock().await); + if let Ok(text_json) = text_json_res { + nbt.put_string("text", text_json); + } + nbt.put_int("line_width", self.line_width.load(Ordering::Relaxed)); + nbt.put_int("background", self.background.load(Ordering::Relaxed)); + nbt.put_byte("text_opacity", self.text_opacity.load(Ordering::Relaxed)); + + let flags = self.flags.load(Ordering::Relaxed); + nbt.put_bool("shadow", flags & 1 != 0); + nbt.put_bool("see_through", flags & 2 != 0); + nbt.put_bool("default_background", flags & 4 != 0); + let align_str = if flags & 8 != 0 { + "left" + } else if flags & 16 != 0 { + "right" + } else { + "center" + }; + nbt.put_string("alignment", align_str.to_string()); + }) + } + + fn read_nbt<'a>(&'a mut self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> { + Box::pin(async move { + self.read_nbt_non_mut(nbt).await; + }) + } + + fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> { + Box::pin(async move { + self.display.read_display_nbt(nbt).await; + if let Some(text_json) = nbt.get_string("text") + && let Ok(component) = pumpkin_util::serde_json::from_str(text_json) + { + *self.text.lock().await = component; + } + if let Some(lw) = nbt.get_int("line_width") { + self.line_width.store(lw, Ordering::Relaxed); + } + if let Some(bg) = nbt.get_int("background") { + self.background.store(bg, Ordering::Relaxed); + } + if let Some(opacity) = nbt.get_byte("text_opacity") { + self.text_opacity.store(opacity, Ordering::Relaxed); + } + + let mut flags = 0u8; + if nbt.get_bool("shadow").unwrap_or(false) { + flags |= 1; + } + if nbt.get_bool("see_through").unwrap_or(false) { + flags |= 2; + } + if nbt.get_bool("default_background").unwrap_or(false) { + flags |= 4; + } + if let Some(align) = nbt.get_string("alignment") { + match align { + "left" => flags |= 8, + "right" => flags |= 16, + _ => {} + } + } + self.flags.store(flags, Ordering::Relaxed); + }) + } +} + +impl EntityBase for TextDisplayEntity { + fn tick<'a>( + &'a self, + _caller: &'a Arc, + _server: &'a Server, + ) -> EntityBaseFuture<'a, ()> { + Box::pin(async move {}) + } + + fn init_data_tracker(&self) -> EntityBaseFuture<'_, ()> { + Box::pin(async move { + self.display.init_display_data_tracker().await; + let text = self.text.lock().await.clone(); + self.display.entity.send_meta_data( + &[Metadata::new( + TrackedData::TEXT, + MetaDataType::COMPONENT, + text, + )], + None, + ); + self.display.entity.send_meta_data( + &[Metadata::new( + TrackedData::LINE_WIDTH, + MetaDataType::INT, + VarInt(self.line_width.load(Ordering::Relaxed)), + )], + None, + ); + self.display.entity.send_meta_data( + &[Metadata::new( + TrackedData::BACKGROUND, + MetaDataType::INT, + VarInt(self.background.load(Ordering::Relaxed)), + )], + None, + ); + self.display.entity.send_meta_data( + &[Metadata::new( + TrackedData::TEXT_OPACITY, + MetaDataType::BYTE, + self.text_opacity.load(Ordering::Relaxed) as u8, + )], + None, + ); + self.display.entity.send_meta_data( + &[Metadata::new( + TrackedData::TEXT_DISPLAY_FLAGS, + MetaDataType::BYTE, + self.flags.load(Ordering::Relaxed), + )], + None, + ); + }) + } + + fn get_entity(&self) -> &Entity { + &self.display.entity + } + + fn get_living_entity(&self) -> Option<&LivingEntity> { + None + } + + fn as_nbt_storage(&self) -> &dyn NBTStorage { + self + } + + fn cast_any(&self) -> &dyn std::any::Any { + self + } + + fn is_pushable(&self) -> bool { + false + } + + fn is_pushed_by_fluids(&self) -> bool { + false + } + + fn can_hit(&self) -> bool { + false + } + + fn is_immune_to_explosion(&self) -> bool { + true + } + + fn damage_with_context<'a>( + &'a self, + _caller: &'a dyn EntityBase, + _amount: f32, + _damage_type: DamageType, + _position: Option>, + _source: Option<&'a dyn EntityBase>, + _cause: Option<&'a dyn EntityBase>, + ) -> EntityBaseFuture<'a, bool> { + Box::pin(async move { false }) + } +} diff --git a/crates/pumpkin/src/entity/decoration/item_frame.rs b/crates/pumpkin/src/entity/decoration/item_frame.rs index c35832c93..f76e96e30 100644 --- a/crates/pumpkin/src/entity/decoration/item_frame.rs +++ b/crates/pumpkin/src/entity/decoration/item_frame.rs @@ -1,13 +1,22 @@ +use std::sync::Arc; use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; +use crate::entity::player::Player; use crate::entity::{ Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture, living::LivingEntity, }; use crossbeam::atomic::AtomicCell; use pumpkin_data::BlockDirection; use pumpkin_data::damage::DamageType; +use pumpkin_data::entity::EntityType; +use pumpkin_data::item::Item; use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::meta_data_type::MetaDataType; +use pumpkin_data::sound::Sound; +use pumpkin_data::tracked_data::TrackedData; use pumpkin_nbt::compound::NbtCompound; +use pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer; +use pumpkin_protocol::java::client::play::{CSetEntityMetadata, Metadata}; use pumpkin_util::math::vector3::Vector3; use tokio::sync::Mutex; @@ -50,11 +59,167 @@ impl ItemFrameEntity { } } + pub const fn is_glow(&self) -> bool { + self.entity.entity_type.id == EntityType::GLOW_ITEM_FRAME.id + } + + pub const fn get_add_item_sound(&self) -> Sound { + if self.is_glow() { + Sound::EntityGlowItemFrameAddItem + } else { + Sound::EntityItemFrameAddItem + } + } + + pub const fn get_remove_item_sound(&self) -> Sound { + if self.is_glow() { + Sound::EntityGlowItemFrameRemoveItem + } else { + Sound::EntityItemFrameRemoveItem + } + } + + pub const fn get_rotate_item_sound(&self) -> Sound { + if self.is_glow() { + Sound::EntityGlowItemFrameRotateItem + } else { + Sound::EntityItemFrameRotateItem + } + } + + pub const fn get_break_sound(&self) -> Sound { + if self.is_glow() { + Sound::EntityGlowItemFrameBreak + } else { + Sound::EntityItemFrameBreak + } + } + + pub const fn get_place_sound(&self) -> Sound { + if self.is_glow() { + Sound::EntityGlowItemFramePlace + } else { + Sound::EntityItemFramePlace + } + } + pub fn get_facing(&self) -> BlockDirection { BlockDirection::from_index(self.facing.load(Ordering::Relaxed)) .unwrap_or(Self::DEFAULT_FACING) } + pub fn set_facing(&self, facing: BlockDirection) { + let index = facing.to_index(); + self.facing.store(index, Ordering::Relaxed); + self.entity.data.store(i32::from(index), Ordering::Relaxed); + } + + pub async fn get_item(&self) -> ItemStack { + self.item_stack.lock().await.clone() + } + + pub async fn set_item(&self, mut item_stack: ItemStack, update_neighbours: bool) { + if !item_stack.is_empty() { + item_stack.item_count = 1; + } + + let play_sound = !item_stack.is_empty(); + let item_serializer = ItemStackSerializer::from(item_stack.clone()); + *self.item_stack.lock().await = item_stack; + + self.entity.send_meta_data( + &[Metadata::new( + TrackedData::ITEM, + MetaDataType::ITEM_STACK, + &item_serializer, + )], + None, + ); + + if play_sound { + self.entity.play_sound(self.get_add_item_sound()); + } + + if update_neighbours { + let world = self.entity.world.load(); + let pos = self.entity.block_pos.load(); + world.update_neighbors(&pos, None).await; + } + } + + pub fn get_rotation(&self) -> u8 { + self.rotation.load(Ordering::Relaxed) % 8 + } + + pub async fn set_rotation(&self, rotation: u8, update_neighbours: bool) { + let rot = rotation % 8; + self.rotation.store(rot, Ordering::Relaxed); + + self.entity.send_meta_data( + &[Metadata::new( + TrackedData::ROTATION, + MetaDataType::INT, + rot as i32, + )], + None, + ); + + if update_neighbours { + let world = self.entity.world.load(); + let pos = self.entity.block_pos.load(); + world.update_neighbors(&pos, None).await; + } + } + + pub fn get_drop_chance(&self) -> f32 { + self.item_drop_chance.load() + } + + pub fn set_drop_chance(&self, chance: f32) { + self.item_drop_chance.store(chance); + } + + pub fn is_fixed(&self) -> bool { + self.fixed.load(Ordering::Relaxed) + } + + pub fn set_fixed(&self, fixed: bool) { + self.fixed.store(fixed, Ordering::Relaxed); + } + + pub fn is_invisible(&self) -> bool { + self.invisible.load(Ordering::Relaxed) + } + + pub fn set_invisible(&self, invisible: bool) { + self.invisible.store(invisible, Ordering::Relaxed); + } + + pub fn get_frame_item_stack(&self) -> ItemStack { + if self.is_glow() { + ItemStack::new(1, &Item::GLOW_ITEM_FRAME) + } else { + ItemStack::new(1, &Item::ITEM_FRAME) + } + } + + pub fn get_frame_item_stack_with_data(&self) -> ItemStack { + let mut stack = self.get_frame_item_stack(); + if let Some(custom_name) = self.entity.custom_name.load().as_ref().clone() { + stack.set_custom_name(custom_name.to_pretty_console()); + } + stack + } + + pub async fn get_pick_result(&self) -> ItemStack { + let framed_stack = self.get_item().await; + if framed_stack.is_empty() { + self.get_frame_item_stack_with_data() + } else { + framed_stack + } + } + /// The comparator signal this frame produces. /// /// Vanilla: `getItem().isEmpty() ? 0 : getRotation() % 8 + 1`. @@ -65,6 +230,41 @@ impl ItemFrameEntity { self.rotation.load(Ordering::Relaxed) % 8 + 1 } } + + pub async fn drop_item(&self, caused_by: Option<&dyn EntityBase>, with_frame: bool) { + if self.is_fixed() { + return; + } + + let item_stack = self.get_item().await; + self.set_item(ItemStack::EMPTY.clone(), true).await; + + let is_creative_player = caused_by.is_some_and(|s| { + s.cast_any() + .downcast_ref::() + .is_some_and(Player::is_creative) + }); + + if is_creative_player { + return; + } + + let world = self.entity.world.load(); + let pos = self.entity.block_pos.load(); + + if with_frame { + world + .drop_stack(&pos, self.get_frame_item_stack_with_data()) + .await; + } + + if !item_stack.is_empty() { + let drop_chance = self.item_drop_chance.load(); + if rand::random::() < drop_chance { + world.drop_stack(&pos, item_stack).await; + } + } + } } impl NBTStorage for ItemFrameEntity { @@ -77,8 +277,8 @@ impl NBTStorage for ItemFrameEntity { let mut item_compound = NbtCompound::new(); item.write_item_stack(&mut item_compound); nbt.put_compound("Item", item_compound); - nbt.put_float("ItemDropChance", self.item_drop_chance.load()); } + nbt.put_float("ItemDropChance", self.item_drop_chance.load()); nbt.put_byte("ItemRotation", self.rotation.load(Ordering::Relaxed) as i8); nbt.put_byte("Facing", self.facing.load(Ordering::Relaxed) as i8); nbt.put_bool("Invisible", self.invisible.load(Ordering::Relaxed)); @@ -124,19 +324,133 @@ impl EntityBase for ItemFrameEntity { None } + fn init_data_tracker(&self) -> EntityBaseFuture<'_, ()> { + Box::pin(async { + let item_serializer = ItemStackSerializer::from(self.item_stack.lock().await.clone()); + let rotation = self.get_rotation() as i32; + + self.entity.send_meta_data( + &[Metadata::new( + TrackedData::ITEM, + MetaDataType::ITEM_STACK, + &item_serializer, + )], + None, + ); + self.entity.send_meta_data( + &[Metadata::new( + TrackedData::ROTATION, + MetaDataType::INT, + rotation, + )], + None, + ); + }) + } + + fn send_java_spawn_packet<'a>( + &'a self, + client: &'a crate::net::java::JavaClient, + ) -> EntityBaseFuture<'a, ()> { + Box::pin(async move { + client + .enqueue_packet(&self.entity.create_spawn_packet()) + .await; + + let item_serializer = ItemStackSerializer::from(self.item_stack.lock().await.clone()); + let rotation = self.get_rotation() as i32; + + let mut data = Vec::new(); + let meta_item = + Metadata::new(TrackedData::ITEM, MetaDataType::ITEM_STACK, item_serializer); + let meta_rot = Metadata::new(TrackedData::ROTATION, MetaDataType::INT, rotation); + + let ver = client.version.load(); + if meta_item.write(&mut data, &ver).is_ok() && meta_rot.write(&mut data, &ver).is_ok() { + data.push(255); + client + .enqueue_packet(&CSetEntityMetadata::new( + self.entity.entity_id.into(), + data.into(), + )) + .await; + } + }) + } + + fn interact<'a>( + &'a self, + player: &'a Arc, + item_stack: &'a mut ItemStack, + ) -> EntityBaseFuture<'a, bool> { + Box::pin(async move { + if self.is_fixed() { + return false; + } + + let frame_has_item = !self.get_item().await.is_empty(); + let has_held_item = !item_stack.is_empty(); + + if frame_has_item { + let new_rot = self.get_rotation() + 1; + self.set_rotation(new_rot, true).await; + self.entity.play_sound(self.get_rotate_item_sound()); + true + } else if has_held_item && !self.entity.removed.load(Ordering::Relaxed) { + let mut new_stack = item_stack.clone(); + new_stack.item_count = 1; + self.set_item(new_stack, true).await; + + if !player.is_creative() { + item_stack.decrement(1); + } + true + } else { + false + } + }) + } + fn damage_with_context<'a>( &'a self, _caller: &'a dyn EntityBase, _amount: f32, - _damage_type: DamageType, + damage_type: DamageType, _position: Option>, - _source: Option<&'a dyn EntityBase>, + source: Option<&'a dyn EntityBase>, _cause: Option<&'a dyn EntityBase>, ) -> EntityBaseFuture<'a, bool> { - Box::pin(async { - // TODO: vanilla pops the displayed item first and only removes the - // frame itself when hit while empty; both should drop their items. - self.entity.remove().await; + Box::pin(async move { + let fixed = self.is_fixed(); + let is_creative_player = source.is_some_and(|s| { + s.cast_any() + .downcast_ref::() + .is_some_and(Player::is_creative) + }); + let bypasses_invuln = + damage_type == DamageType::OUT_OF_WORLD || damage_type == DamageType::GENERIC_KILL; + + if fixed { + if !bypasses_invuln && !is_creative_player { + return false; + } + self.drop_item(source, true).await; + self.entity.remove().await; + return true; + } + + let has_item = !self.get_item().await.is_empty(); + let is_explosion = + damage_type == DamageType::EXPLOSION || damage_type == DamageType::PLAYER_EXPLOSION; + + if !is_explosion && has_item { + self.drop_item(source, false).await; + self.entity.play_sound(self.get_remove_item_sound()); + } else { + self.drop_item(source, true).await; + self.entity.play_sound(self.get_break_sound()); + self.entity.remove().await; + } true }) } diff --git a/crates/pumpkin/src/entity/decoration/mod.rs b/crates/pumpkin/src/entity/decoration/mod.rs index 76ea13331..e24a82f46 100644 --- a/crates/pumpkin/src/entity/decoration/mod.rs +++ b/crates/pumpkin/src/entity/decoration/mod.rs @@ -1,4 +1,5 @@ pub mod armor_stand; +pub mod display; pub mod end_crystal; pub mod item_frame; pub mod leash_knot; diff --git a/crates/pumpkin/src/entity/interaction.rs b/crates/pumpkin/src/entity/interaction.rs new file mode 100644 index 000000000..5ce72823d --- /dev/null +++ b/crates/pumpkin/src/entity/interaction.rs @@ -0,0 +1,313 @@ +use std::sync::{ + Arc, + atomic::{AtomicBool, Ordering}, +}; +use tokio::sync::Mutex; +use uuid::Uuid; + +use pumpkin_data::{ + damage::DamageType, item_stack::ItemStack, meta_data_type::MetaDataType, + tracked_data::TrackedData, +}; +use pumpkin_nbt::{compound::NbtCompound, tag::NbtTag}; +use pumpkin_protocol::java::client::play::Metadata; +use pumpkin_util::math::{ + boundingbox::{BoundingBox, EntityDimensions}, + vector3::Vector3, +}; + +use crate::{ + entity::{ + Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture, living::LivingEntity, + player::Player, + }, + server::Server, +}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct PlayerAction { + pub player: Uuid, + pub timestamp: i64, +} + +impl PlayerAction { + #[must_use] + pub fn to_nbt(&self) -> NbtCompound { + let mut compound = NbtCompound::new(); + compound.put_uuid("player", self.player); + compound.put_long("timestamp", self.timestamp); + compound + } + + #[must_use] + pub fn from_nbt(compound: &NbtCompound) -> Option { + let player = compound.get_uuid("player")?; + let timestamp = compound.get_long("timestamp")?; + Some(Self { player, timestamp }) + } +} + +pub struct InteractionEntity { + pub entity: Entity, + pub width: Mutex, + pub height: Mutex, + pub response: AtomicBool, + pub attack: Mutex>, + pub interaction: Mutex>, +} + +impl InteractionEntity { + pub fn new(entity: Entity) -> Arc { + entity.no_clip.store(true, Ordering::Relaxed); + let width = 1.0; + let height = 1.0; + let dimensions = EntityDimensions::new(width, height, height * 0.85); + let pos = entity.pos.load(); + let aabb = BoundingBox::new_from_pos(pos.x, pos.y, pos.z, &dimensions); + entity.bounding_box.store(aabb); + + Arc::new(Self { + entity, + width: Mutex::new(width), + height: Mutex::new(height), + response: AtomicBool::new(false), + attack: Mutex::new(None), + interaction: Mutex::new(None), + }) + } + + pub async fn get_width(&self) -> f32 { + *self.width.lock().await + } + + pub async fn set_width(&self, width: f32) { + *self.width.lock().await = width; + self.update_dimensions().await; + self.entity.send_meta_data( + &[Metadata::new( + TrackedData::WIDTH, + MetaDataType::FLOAT, + width, + )], + None, + ); + } + + pub async fn get_height(&self) -> f32 { + *self.height.lock().await + } + + pub async fn set_height(&self, height: f32) { + *self.height.lock().await = height; + self.update_dimensions().await; + self.entity.send_meta_data( + &[Metadata::new( + TrackedData::HEIGHT, + MetaDataType::FLOAT, + height, + )], + None, + ); + } + + pub fn get_response(&self) -> bool { + self.response.load(Ordering::Relaxed) + } + + pub fn set_response(&self, response: bool) { + self.response.store(response, Ordering::Relaxed); + self.entity.send_meta_data( + &[Metadata::new( + TrackedData::RESPONSE, + MetaDataType::BOOLEAN, + response, + )], + None, + ); + } + + pub async fn update_dimensions(&self) { + let width = *self.width.lock().await; + let height = *self.height.lock().await; + let dimensions = EntityDimensions::new(width, height, height * 0.85); + let pos = self.entity.pos.load(); + let aabb = BoundingBox::new_from_pos(pos.x, pos.y, pos.z, &dimensions); + self.entity.bounding_box.store(aabb); + } + + pub async fn get_last_attacker(&self) -> Option { + *self.attack.lock().await + } + + pub async fn get_target(&self) -> Option { + *self.interaction.lock().await + } +} + +impl NBTStorage for InteractionEntity { + fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> { + Box::pin(async move { + self.entity.write_nbt(nbt).await; + nbt.put_float("width", *self.width.lock().await); + nbt.put_float("height", *self.height.lock().await); + nbt.put_bool("response", self.response.load(Ordering::Relaxed)); + + let attack = *self.attack.lock().await; + if let Some(attack) = attack { + nbt.put("attack", NbtTag::Compound(attack.to_nbt())); + } + + let interaction = *self.interaction.lock().await; + if let Some(interaction) = interaction { + nbt.put("interaction", NbtTag::Compound(interaction.to_nbt())); + } + }) + } + + fn read_nbt<'a>(&'a mut self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> { + Box::pin(async move { + self.read_nbt_non_mut(nbt).await; + }) + } + + fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> { + Box::pin(async move { + self.entity.read_nbt_non_mut(nbt).await; + + let width = nbt.get_float("width").unwrap_or(1.0); + let height = nbt.get_float("height").unwrap_or(1.0); + let response = nbt.get_bool("response").unwrap_or(false); + + *self.width.lock().await = width; + *self.height.lock().await = height; + self.response.store(response, Ordering::Relaxed); + self.update_dimensions().await; + + if let Some(attack_compound) = nbt.get_compound("attack") { + *self.attack.lock().await = PlayerAction::from_nbt(attack_compound); + } else { + *self.attack.lock().await = None; + } + + if let Some(interaction_compound) = nbt.get_compound("interaction") { + *self.interaction.lock().await = PlayerAction::from_nbt(interaction_compound); + } else { + *self.interaction.lock().await = None; + } + }) + } +} + +impl EntityBase for InteractionEntity { + fn tick<'a>( + &'a self, + _caller: &'a Arc, + _server: &'a Server, + ) -> EntityBaseFuture<'a, ()> { + Box::pin(async move {}) + } + + fn init_data_tracker(&self) -> EntityBaseFuture<'_, ()> { + Box::pin(async move { + let width = *self.width.lock().await; + let height = *self.height.lock().await; + let response = self.response.load(Ordering::Relaxed); + + self.entity.send_meta_data( + &[Metadata::new( + TrackedData::WIDTH, + MetaDataType::FLOAT, + width, + )], + None, + ); + self.entity.send_meta_data( + &[Metadata::new( + TrackedData::HEIGHT, + MetaDataType::FLOAT, + height, + )], + None, + ); + self.entity.send_meta_data( + &[Metadata::new( + TrackedData::RESPONSE, + MetaDataType::BOOLEAN, + response, + )], + None, + ); + }) + } + + fn get_entity(&self) -> &Entity { + &self.entity + } + + fn get_living_entity(&self) -> Option<&LivingEntity> { + None + } + + fn as_nbt_storage(&self) -> &dyn NBTStorage { + self + } + + fn cast_any(&self) -> &dyn std::any::Any { + self + } + + fn is_pushable(&self) -> bool { + false + } + + fn is_pushed_by_fluids(&self) -> bool { + false + } + + fn can_hit(&self) -> bool { + true + } + + fn is_immune_to_explosion(&self) -> bool { + true + } + + fn damage_with_context<'a>( + &'a self, + _caller: &'a dyn EntityBase, + _amount: f32, + _damage_type: DamageType, + _position: Option>, + source: Option<&'a dyn EntityBase>, + cause: Option<&'a dyn EntityBase>, + ) -> EntityBaseFuture<'a, bool> { + Box::pin(async move { + let player = source + .or(cause) + .and_then(|e| e.cast_any().downcast_ref::()); + if let Some(player) = player { + let timestamp = self.entity.world.load().level_time.lock().await.world_age as i64; + *self.attack.lock().await = Some(PlayerAction { + player: player.gameprofile.id, + timestamp, + }); + } + false + }) + } + + fn interact<'a>( + &'a self, + player: &'a Arc, + _item_stack: &'a mut ItemStack, + ) -> EntityBaseFuture<'a, bool> { + Box::pin(async move { + let timestamp = self.entity.world.load().level_time.lock().await.world_age as i64; + *self.interaction.lock().await = Some(PlayerAction { + player: player.gameprofile.id, + timestamp, + }); + true + }) + } +} diff --git a/crates/pumpkin/src/entity/lightning.rs b/crates/pumpkin/src/entity/lightning.rs new file mode 100644 index 000000000..8678e4ba1 --- /dev/null +++ b/crates/pumpkin/src/entity/lightning.rs @@ -0,0 +1,500 @@ +use std::collections::HashSet; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, AtomicI32, AtomicI64, Ordering}; +use tokio::sync::Mutex; + +use pumpkin_data::sound::{Sound, SoundCategory}; +use pumpkin_data::world::WorldEvent; +use pumpkin_data::{Block, BlockId, BlockStateId}; +use pumpkin_util::Difficulty; +use pumpkin_util::math::boundingbox::BoundingBox; +use pumpkin_util::math::position::BlockPos; +use pumpkin_util::math::vector3::Vector3; +use pumpkin_world::world::BlockFlags; +use rand::RngExt; + +use crate::block::blocks::fire::FireBlockBase; +use crate::entity::player::Player; +use crate::entity::{Entity, EntityBase, EntityBaseFuture, NBTStorage}; +use crate::server::Server; +use crate::world::World; + +pub struct LightningBoltEntity { + entity: Entity, + life: AtomicI32, + seed: AtomicI64, + flashes: AtomicI32, + visual_only: AtomicBool, + cause: Mutex>>, + hit_entities: Mutex>, + blocks_set_on_fire: AtomicI32, +} + +impl LightningBoltEntity { + pub fn new(entity: Entity) -> Self { + let seed = rand::rng().random::(); + let flashes = rand::rng().random_range(1..=3); + Self { + entity, + life: AtomicI32::new(2), + seed: AtomicI64::new(seed), + flashes: AtomicI32::new(flashes), + visual_only: AtomicBool::new(false), + cause: Mutex::new(None), + hit_entities: Mutex::new(HashSet::new()), + blocks_set_on_fire: AtomicI32::new(0), + } + } + + pub fn set_visual_only(&self, visual_only: bool) { + self.visual_only.store(visual_only, Ordering::Relaxed); + } + + pub fn is_visual_only(&self) -> bool { + self.visual_only.load(Ordering::Relaxed) + } + + pub async fn set_cause(&self, cause: Option>) { + *self.cause.lock().await = cause; + } + + pub async fn get_cause(&self) -> Option> { + self.cause.lock().await.clone() + } + + pub fn get_blocks_set_on_fire(&self) -> i32 { + self.blocks_set_on_fire.load(Ordering::Relaxed) + } + + pub fn get_seed(&self) -> i64 { + self.seed.load(Ordering::Relaxed) + } + + pub fn set_seed(&self, seed: i64) { + self.seed.store(seed, Ordering::Relaxed); + } + + fn get_strike_position(&self) -> BlockPos { + let pos = self.entity.pos.load(); + Vector3::new(pos.x, pos.y - 1.0e-6, pos.z).to_block_pos() + } + + async fn power_lightning_rod(&self, world: &Arc) { + let strike_pos = self.get_strike_position(); + let block = world.get_block(&strike_pos); + if block == &Block::LIGHTNING_ROD { + crate::block::blocks::redstone::lightning_rod::LightningRodBlock::trigger( + world, + &strike_pos, + ) + .await; + } + } + + async fn spawn_fire(&self, world: &Arc, additional_sources: i32) { + if self.visual_only.load(Ordering::Relaxed) { + return; + } + + let pos = self.entity.block_pos.load(); + + let try_place = |p: BlockPos| async move { + if world.get_block_state(&p).is_air() && FireBlockBase::can_place_at(world, &p) { + let fire_block = FireBlockBase::get_fire_type(world, &p); + world + .set_block_state(&p, fire_block.default_state.id, BlockFlags::NOTIFY_ALL) + .await; + self.blocks_set_on_fire.fetch_add(1, Ordering::Relaxed); + } + }; + + try_place(pos).await; + + for _ in 0..additional_sources { + let dx = rand::rng().random_range(-1..=1); + let dy = rand::rng().random_range(-1..=1); + let dz = rand::rng().random_range(-1..=1); + let nearby_pos = pos.offset(Vector3::new(dx, dy, dz)); + try_place(nearby_pos).await; + } + } + + async fn clear_copper_on_lightning_strike(&self, world: &Arc) { + let strike_pos = self.get_strike_position(); + let struck_state = world.get_block_state(&strike_pos); + let struck_block = struck_state.id.to_block(); + + let is_waxed = is_waxed_copper(struck_block.id); + let is_weathering = is_weathering_copper(struck_block.id); + + if is_weathering || is_waxed { + if is_weathering && let Some(first_block_id) = get_first_stage_copper(struck_block.id) { + let first_block = first_block_id.to_block(); + let offset = struck_state + .id + .as_u16() + .saturating_sub(struck_block.default_state.id.as_u16()); + let new_state_id = + BlockStateId::new(first_block.default_state.id.as_u16() + offset) + .unwrap_or(first_block.default_state.id); + world + .set_block_state(&strike_pos, new_state_id, BlockFlags::NOTIFY_ALL) + .await; + } + + let strikes_count = rand::rng().random_range(3..=5); + + for _ in 0..strikes_count { + let step_count = rand::rng().random_range(1..=8); + self.random_walk_cleaning_copper(world, &strike_pos, step_count) + .await; + } + } + } + + async fn random_walk_cleaning_copper( + &self, + world: &Arc, + original_strike_pos: &BlockPos, + step_count: i32, + ) { + let mut work_pos = *original_strike_pos; + + for _ in 0..step_count { + if let Some(next_pos) = self.random_step_cleaning_copper(world, &work_pos).await { + work_pos = next_pos; + } else { + break; + } + } + } + + async fn random_step_cleaning_copper( + &self, + world: &Arc, + pos: &BlockPos, + ) -> Option { + let candidates = random_in_cube(10, pos, 1); + for candidate in candidates { + let state = world.get_block_state(&candidate); + let block = state.id.to_block(); + + if is_weathering_copper(block.id) + && let Some(prev_block_id) = get_previous_stage_copper(block.id) + { + let prev_block = prev_block_id.to_block(); + let offset = state + .id + .as_u16() + .saturating_sub(block.default_state.id.as_u16()); + let new_state_id = BlockStateId::new(prev_block.default_state.id.as_u16() + offset) + .unwrap_or(prev_block.default_state.id); + world + .set_block_state(&candidate, new_state_id, BlockFlags::NOTIFY_ALL) + .await; + world.sync_world_event(WorldEvent::ParticlesElectricSpark, candidate, -1); + return Some(candidate); + } + } + None + } +} + +impl NBTStorage for LightningBoltEntity {} + +impl EntityBase for LightningBoltEntity { + fn tick<'a>( + &'a self, + _caller: &'a Arc, + _server: &'a Server, + ) -> EntityBaseFuture<'a, ()> { + Box::pin(async move { + let entity = &self.entity; + let life = self.life.load(Ordering::Relaxed); + + if life == 2 { + let world = entity.world.load(); + let pos = entity.pos.load(); + + let pitch_thunder = 0.8 + rand::rng().random::() * 0.2; + world.play_sound_fine( + Sound::EntityLightningBoltThunder, + SoundCategory::Weather, + &pos, + 10000.0, + pitch_thunder, + ); + let pitch_impact = 0.5 + rand::rng().random::() * 0.2; + world.play_sound_fine( + Sound::EntityLightningBoltImpact, + SoundCategory::Weather, + &pos, + 2.0, + pitch_impact, + ); + + let difficulty = world.level_info.load().difficulty; + if difficulty == Difficulty::Normal || difficulty == Difficulty::Hard { + self.spawn_fire(&world, 4).await; + } + + self.power_lightning_rod(&world).await; + self.clear_copper_on_lightning_strike(&world).await; + } + + let new_life = life - 1; + self.life.store(new_life, Ordering::Relaxed); + + if new_life < 0 { + let flashes = self.flashes.load(Ordering::Relaxed); + if flashes == 0 { + entity.remove().await; + return; + } else if new_life < -rand::rng().random_range(0..10) { + self.flashes.store(flashes - 1, Ordering::Relaxed); + self.life.store(1, Ordering::Relaxed); + self.seed.store(rand::random::(), Ordering::Relaxed); + let world = entity.world.load(); + self.spawn_fire(&world, 0).await; + } + } + + let current_life = self.life.load(Ordering::Relaxed); + if current_life >= 0 && !self.visual_only.load(Ordering::Relaxed) { + let world = entity.world.load(); + let pos = entity.pos.load(); + + let damage_box = BoundingBox::new( + Vector3::new(pos.x - 3.0, pos.y - 3.0, pos.z - 3.0), + Vector3::new(pos.x + 3.0, pos.y + 9.0, pos.z + 3.0), + ); + + let entities = world.get_all_at_box(&damage_box); + let mut hit_guard = self.hit_entities.lock().await; + + for hit_entity in entities { + if hit_entity.get_entity().entity_id == entity.entity_id { + continue; + } + let hit_id = hit_entity.get_entity().entity_id; + hit_entity + .on_lightning_strike(hit_entity.as_ref(), self) + .await; + hit_guard.insert(hit_id); + } + } + }) + } + + fn init_data_tracker(&self) -> EntityBaseFuture<'_, ()> { + Box::pin(async {}) + } + + fn get_entity(&self) -> &Entity { + &self.entity + } + + fn get_living_entity(&self) -> Option<&crate::entity::living::LivingEntity> { + None + } + + fn as_nbt_storage(&self) -> &dyn NBTStorage { + self + } + + fn cast_any(&self) -> &dyn std::any::Any { + self + } +} + +fn random_in_cube(count: usize, center: &BlockPos, radius: i32) -> Vec { + let mut list = Vec::with_capacity(count); + for _ in 0..count { + let dx = rand::rng().random_range(-radius..=radius); + let dy = rand::rng().random_range(-radius..=radius); + let dz = rand::rng().random_range(-radius..=radius); + list.push(center.offset(Vector3::new(dx, dy, dz))); + } + list +} + +#[must_use] +pub const fn is_waxed_copper(id: BlockId) -> bool { + matches!( + id, + BlockId::WAXED_COPPER_BLOCK + | BlockId::WAXED_EXPOSED_COPPER + | BlockId::WAXED_WEATHERED_COPPER + | BlockId::WAXED_OXIDIZED_COPPER + | BlockId::WAXED_CHISELED_COPPER + | BlockId::WAXED_EXPOSED_CHISELED_COPPER + | BlockId::WAXED_WEATHERED_CHISELED_COPPER + | BlockId::WAXED_OXIDIZED_CHISELED_COPPER + | BlockId::WAXED_COPPER_GRATE + | BlockId::WAXED_EXPOSED_COPPER_GRATE + | BlockId::WAXED_WEATHERED_COPPER_GRATE + | BlockId::WAXED_OXIDIZED_COPPER_GRATE + | BlockId::WAXED_CUT_COPPER + | BlockId::WAXED_EXPOSED_CUT_COPPER + | BlockId::WAXED_WEATHERED_CUT_COPPER + | BlockId::WAXED_OXIDIZED_CUT_COPPER + | BlockId::WAXED_CUT_COPPER_STAIRS + | BlockId::WAXED_EXPOSED_CUT_COPPER_STAIRS + | BlockId::WAXED_WEATHERED_CUT_COPPER_STAIRS + | BlockId::WAXED_OXIDIZED_CUT_COPPER_STAIRS + | BlockId::WAXED_CUT_COPPER_SLAB + | BlockId::WAXED_EXPOSED_CUT_COPPER_SLAB + | BlockId::WAXED_WEATHERED_CUT_COPPER_SLAB + | BlockId::WAXED_OXIDIZED_CUT_COPPER_SLAB + | BlockId::WAXED_COPPER_BULB + | BlockId::WAXED_EXPOSED_COPPER_BULB + | BlockId::WAXED_WEATHERED_COPPER_BULB + | BlockId::WAXED_OXIDIZED_COPPER_BULB + | BlockId::WAXED_COPPER_DOOR + | BlockId::WAXED_EXPOSED_COPPER_DOOR + | BlockId::WAXED_WEATHERED_COPPER_DOOR + | BlockId::WAXED_OXIDIZED_COPPER_DOOR + | BlockId::WAXED_COPPER_TRAPDOOR + | BlockId::WAXED_EXPOSED_COPPER_TRAPDOOR + | BlockId::WAXED_WEATHERED_COPPER_TRAPDOOR + | BlockId::WAXED_OXIDIZED_COPPER_TRAPDOOR + ) +} + +#[must_use] +pub const fn is_weathering_copper(id: BlockId) -> bool { + matches!( + id, + BlockId::COPPER_BLOCK + | BlockId::EXPOSED_COPPER + | BlockId::WEATHERED_COPPER + | BlockId::OXIDIZED_COPPER + | BlockId::CHISELED_COPPER + | BlockId::EXPOSED_CHISELED_COPPER + | BlockId::WEATHERED_CHISELED_COPPER + | BlockId::OXIDIZED_CHISELED_COPPER + | BlockId::COPPER_GRATE + | BlockId::EXPOSED_COPPER_GRATE + | BlockId::WEATHERED_COPPER_GRATE + | BlockId::OXIDIZED_COPPER_GRATE + | BlockId::CUT_COPPER + | BlockId::EXPOSED_CUT_COPPER + | BlockId::WEATHERED_CUT_COPPER + | BlockId::OXIDIZED_CUT_COPPER + | BlockId::CUT_COPPER_STAIRS + | BlockId::EXPOSED_CUT_COPPER_STAIRS + | BlockId::WEATHERED_CUT_COPPER_STAIRS + | BlockId::OXIDIZED_CUT_COPPER_STAIRS + | BlockId::CUT_COPPER_SLAB + | BlockId::EXPOSED_CUT_COPPER_SLAB + | BlockId::WEATHERED_CUT_COPPER_SLAB + | BlockId::OXIDIZED_CUT_COPPER_SLAB + | BlockId::COPPER_BULB + | BlockId::EXPOSED_COPPER_BULB + | BlockId::WEATHERED_COPPER_BULB + | BlockId::OXIDIZED_COPPER_BULB + | BlockId::COPPER_DOOR + | BlockId::EXPOSED_COPPER_DOOR + | BlockId::WEATHERED_COPPER_DOOR + | BlockId::OXIDIZED_COPPER_DOOR + | BlockId::COPPER_TRAPDOOR + | BlockId::EXPOSED_COPPER_TRAPDOOR + | BlockId::WEATHERED_COPPER_TRAPDOOR + | BlockId::OXIDIZED_COPPER_TRAPDOOR + ) +} + +#[must_use] +pub const fn get_first_stage_copper(id: BlockId) -> Option { + match id { + BlockId::COPPER_BLOCK + | BlockId::EXPOSED_COPPER + | BlockId::WEATHERED_COPPER + | BlockId::OXIDIZED_COPPER => Some(BlockId::COPPER_BLOCK), + + BlockId::CHISELED_COPPER + | BlockId::EXPOSED_CHISELED_COPPER + | BlockId::WEATHERED_CHISELED_COPPER + | BlockId::OXIDIZED_CHISELED_COPPER => Some(BlockId::CHISELED_COPPER), + + BlockId::COPPER_GRATE + | BlockId::EXPOSED_COPPER_GRATE + | BlockId::WEATHERED_COPPER_GRATE + | BlockId::OXIDIZED_COPPER_GRATE => Some(BlockId::COPPER_GRATE), + + BlockId::CUT_COPPER + | BlockId::EXPOSED_CUT_COPPER + | BlockId::WEATHERED_CUT_COPPER + | BlockId::OXIDIZED_CUT_COPPER => Some(BlockId::CUT_COPPER), + + BlockId::CUT_COPPER_STAIRS + | BlockId::EXPOSED_CUT_COPPER_STAIRS + | BlockId::WEATHERED_CUT_COPPER_STAIRS + | BlockId::OXIDIZED_CUT_COPPER_STAIRS => Some(BlockId::CUT_COPPER_STAIRS), + + BlockId::CUT_COPPER_SLAB + | BlockId::EXPOSED_CUT_COPPER_SLAB + | BlockId::WEATHERED_CUT_COPPER_SLAB + | BlockId::OXIDIZED_CUT_COPPER_SLAB => Some(BlockId::CUT_COPPER_SLAB), + + BlockId::COPPER_BULB + | BlockId::EXPOSED_COPPER_BULB + | BlockId::WEATHERED_COPPER_BULB + | BlockId::OXIDIZED_COPPER_BULB => Some(BlockId::COPPER_BULB), + + BlockId::COPPER_DOOR + | BlockId::EXPOSED_COPPER_DOOR + | BlockId::WEATHERED_COPPER_DOOR + | BlockId::OXIDIZED_COPPER_DOOR => Some(BlockId::COPPER_DOOR), + + BlockId::COPPER_TRAPDOOR + | BlockId::EXPOSED_COPPER_TRAPDOOR + | BlockId::WEATHERED_COPPER_TRAPDOOR + | BlockId::OXIDIZED_COPPER_TRAPDOOR => Some(BlockId::COPPER_TRAPDOOR), + + _ => None, + } +} + +#[must_use] +pub const fn get_previous_stage_copper(id: BlockId) -> Option { + match id { + BlockId::OXIDIZED_COPPER => Some(BlockId::WEATHERED_COPPER), + BlockId::WEATHERED_COPPER => Some(BlockId::EXPOSED_COPPER), + BlockId::EXPOSED_COPPER => Some(BlockId::COPPER_BLOCK), + + BlockId::OXIDIZED_CHISELED_COPPER => Some(BlockId::WEATHERED_CHISELED_COPPER), + BlockId::WEATHERED_CHISELED_COPPER => Some(BlockId::EXPOSED_CHISELED_COPPER), + BlockId::EXPOSED_CHISELED_COPPER => Some(BlockId::CHISELED_COPPER), + + BlockId::OXIDIZED_COPPER_GRATE => Some(BlockId::WEATHERED_COPPER_GRATE), + BlockId::WEATHERED_COPPER_GRATE => Some(BlockId::EXPOSED_COPPER_GRATE), + BlockId::EXPOSED_COPPER_GRATE => Some(BlockId::COPPER_GRATE), + + BlockId::OXIDIZED_CUT_COPPER => Some(BlockId::WEATHERED_CUT_COPPER), + BlockId::WEATHERED_CUT_COPPER => Some(BlockId::EXPOSED_CUT_COPPER), + BlockId::EXPOSED_CUT_COPPER => Some(BlockId::CUT_COPPER), + + BlockId::OXIDIZED_CUT_COPPER_STAIRS => Some(BlockId::WEATHERED_CUT_COPPER_STAIRS), + BlockId::WEATHERED_CUT_COPPER_STAIRS => Some(BlockId::EXPOSED_CUT_COPPER_STAIRS), + BlockId::EXPOSED_CUT_COPPER_STAIRS => Some(BlockId::CUT_COPPER_STAIRS), + + BlockId::OXIDIZED_CUT_COPPER_SLAB => Some(BlockId::WEATHERED_CUT_COPPER_SLAB), + BlockId::WEATHERED_CUT_COPPER_SLAB => Some(BlockId::EXPOSED_CUT_COPPER_SLAB), + BlockId::EXPOSED_CUT_COPPER_SLAB => Some(BlockId::CUT_COPPER_SLAB), + + BlockId::OXIDIZED_COPPER_BULB => Some(BlockId::WEATHERED_COPPER_BULB), + BlockId::WEATHERED_COPPER_BULB => Some(BlockId::EXPOSED_COPPER_BULB), + BlockId::EXPOSED_COPPER_BULB => Some(BlockId::COPPER_BULB), + + BlockId::OXIDIZED_COPPER_DOOR => Some(BlockId::WEATHERED_COPPER_DOOR), + BlockId::WEATHERED_COPPER_DOOR => Some(BlockId::EXPOSED_COPPER_DOOR), + BlockId::EXPOSED_COPPER_DOOR => Some(BlockId::COPPER_DOOR), + + BlockId::OXIDIZED_COPPER_TRAPDOOR => Some(BlockId::WEATHERED_COPPER_TRAPDOOR), + BlockId::WEATHERED_COPPER_TRAPDOOR => Some(BlockId::EXPOSED_COPPER_TRAPDOOR), + BlockId::EXPOSED_COPPER_TRAPDOOR => Some(BlockId::COPPER_TRAPDOOR), + + _ => None, + } +} diff --git a/crates/pumpkin/src/entity/living.rs b/crates/pumpkin/src/entity/living.rs index 25ebd42e6..bf7748f06 100644 --- a/crates/pumpkin/src/entity/living.rs +++ b/crates/pumpkin/src/entity/living.rs @@ -2106,7 +2106,7 @@ impl NBTStorage for LivingEntity { }; // Persist current absorption amount nbt.put("AbsorptionAmount", NbtTag::Float(self.absorption.load())); - nbt.put("fall_distance", NbtTag::Float(fall_distance)); + nbt.put("FallDistance", NbtTag::Float(fall_distance)); { let effects = self.active_effects.lock().await; if !effects.is_empty() { @@ -2138,7 +2138,10 @@ impl NBTStorage for LivingEntity { // Load fall distance, but if this entity is currently marked dead ensure we don't restore // a lethal fall distance that would immediately re-kill on spawn. - let fd = nbt.get_float("fall_distance").unwrap_or(0.0); + let fd = nbt + .get_float("FallDistance") + .or_else(|| nbt.get_float("fall_distance")) + .unwrap_or(0.0); if self.dead.load(Relaxed) { self.fall_distance.store(0.0); } else { diff --git a/crates/pumpkin/src/entity/marker.rs b/crates/pumpkin/src/entity/marker.rs new file mode 100644 index 000000000..7f0f3f4d9 --- /dev/null +++ b/crates/pumpkin/src/entity/marker.rs @@ -0,0 +1,125 @@ +use std::sync::{Arc, atomic::Ordering}; +use tokio::sync::Mutex; + +use crate::{ + entity::{Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture, living::LivingEntity}, + net::{bedrock::BedrockClient, java::JavaClient}, + server::Server, +}; +use pumpkin_data::damage::DamageType; +use pumpkin_nbt::{compound::NbtCompound, tag::NbtTag}; +use pumpkin_util::math::vector3::Vector3; + +pub struct MarkerEntity { + pub entity: Entity, + pub data: Mutex, +} + +impl MarkerEntity { + pub fn new(entity: Entity) -> Arc { + entity.no_clip.store(true, Ordering::Relaxed); + Arc::new(Self { + entity, + data: Mutex::new(NbtCompound::new()), + }) + } +} + +impl NBTStorage for MarkerEntity { + fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> { + Box::pin(async move { + self.entity.write_nbt(nbt).await; + let data = self.data.lock().await; + if !data.is_empty() { + nbt.put("data", NbtTag::Compound(data.clone())); + } + }) + } + + fn read_nbt<'a>(&'a mut self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> { + Box::pin(async move { + self.entity.read_nbt(nbt).await; + if let Some(data) = nbt.get_compound("data") { + *self.data.lock().await = data.clone(); + } + }) + } + + fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> { + Box::pin(async move { + self.entity.read_nbt_non_mut(nbt).await; + if let Some(data) = nbt.get_compound("data") { + *self.data.lock().await = data.clone(); + } + }) + } +} + +impl EntityBase for MarkerEntity { + fn tick<'a>( + &'a self, + _caller: &'a Arc, + _server: &'a Server, + ) -> EntityBaseFuture<'a, ()> { + Box::pin(async move {}) + } + + fn init_data_tracker(&self) -> EntityBaseFuture<'_, ()> { + Box::pin(async move {}) + } + + fn get_entity(&self) -> &Entity { + &self.entity + } + + fn get_living_entity(&self) -> Option<&LivingEntity> { + None + } + + fn as_nbt_storage(&self) -> &dyn NBTStorage { + self + } + + fn cast_any(&self) -> &dyn std::any::Any { + self + } + + fn is_pushable(&self) -> bool { + false + } + + fn is_pushed_by_fluids(&self) -> bool { + false + } + + fn can_hit(&self) -> bool { + false + } + + fn is_immune_to_explosion(&self) -> bool { + true + } + + fn damage_with_context<'a>( + &'a self, + _caller: &'a dyn EntityBase, + _amount: f32, + _damage_type: DamageType, + _position: Option>, + _source: Option<&'a dyn EntityBase>, + _cause: Option<&'a dyn EntityBase>, + ) -> EntityBaseFuture<'a, bool> { + Box::pin(async move { false }) + } + + fn send_java_spawn_packet<'a>(&'a self, _client: &'a JavaClient) -> EntityBaseFuture<'a, ()> { + Box::pin(async move {}) + } + + fn send_bedrock_spawn_packet<'a>( + &'a self, + _client: &'a BedrockClient, + ) -> EntityBaseFuture<'a, ()> { + Box::pin(async move {}) + } +} diff --git a/crates/pumpkin/src/entity/mob/creaking.rs b/crates/pumpkin/src/entity/mob/creaking.rs index 008e2597a..0f36aeac2 100644 --- a/crates/pumpkin/src/entity/mob/creaking.rs +++ b/crates/pumpkin/src/entity/mob/creaking.rs @@ -1,25 +1,112 @@ -use std::sync::{Arc, Weak}; +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::meta_data_type::MetaDataType; +use pumpkin_data::particle::Particle; +use pumpkin_data::sound::{Sound, SoundCategory}; +use pumpkin_data::tracked_data::TrackedData; +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, NBTStorage, + 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>, + 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 { let mob_entity = MobEntity::new(entity); - let creaking = Self { mob_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 = { let mob_arc: Arc = mob_arc.clone(); @@ -55,12 +142,411 @@ impl CreakingEntity { 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( + TrackedData::CAN_MOVE, + MetaDataType::BOOLEAN, + 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( + TrackedData::IS_ACTIVE, + MetaDataType::BOOLEAN, + 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( + TrackedData::IS_TEARING_DOWN, + MetaDataType::BOOLEAN, + true, + )], + None, + ); + } + + pub fn get_home_pos(&self) -> Option { + self.home_pos.load() + } + + pub fn set_home_pos(&self, pos: Option) { + 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( + TrackedData::HOME_POS, + MetaDataType::OPTIONAL_BLOCK_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) { + *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::() 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]) -> 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 {} +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) -> 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::() + .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::() + && 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 + }) + } } diff --git a/crates/pumpkin/src/entity/mob/creeper.rs b/crates/pumpkin/src/entity/mob/creeper.rs index aecf00056..8d2b9846a 100644 --- a/crates/pumpkin/src/entity/mob/creeper.rs +++ b/crates/pumpkin/src/entity/mob/creeper.rs @@ -163,6 +163,28 @@ impl Mob for CreeperEntity { &self.mob_entity } + fn mob_on_lightning_strike<'a>( + &'a self, + caller: &'a dyn EntityBase, + lightning: &'a crate::entity::lightning::LightningBoltEntity, + ) -> EntityBaseFuture<'a, ()> { + Box::pin(async move { + self.charged.store(true, Ordering::Relaxed); + self.mob_entity.living_entity.entity.send_meta_data( + &[Metadata::new( + TrackedData::CHARGED, + MetaDataType::BOOLEAN, + true, + )], + None, + ); + self.mob_entity + .living_entity + .on_lightning_strike(caller, lightning) + .await; + }) + } + fn mob_tick<'a>(&'a self, _caller: &'a Arc) -> EntityBaseFuture<'a, ()> { Box::pin(async move { let entity = &self.mob_entity.living_entity.entity; diff --git a/crates/pumpkin/src/entity/mob/evoker.rs b/crates/pumpkin/src/entity/mob/evoker.rs index 97932285a..61b8244db 100644 --- a/crates/pumpkin/src/entity/mob/evoker.rs +++ b/crates/pumpkin/src/entity/mob/evoker.rs @@ -1,28 +1,74 @@ +use std::sync::atomic::{AtomicI32, AtomicU8, Ordering}; use std::sync::{Arc, Weak}; +use tokio::sync::Mutex; +use uuid::Uuid; use pumpkin_data::entity::EntityType; +use pumpkin_data::meta_data_type::MetaDataType; +use pumpkin_data::sound::Sound; +use pumpkin_data::tracked_data::TrackedData; +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_protocol::java::client::play::Metadata; +use pumpkin_util::math::vector3::Vector3; use crate::entity::{ - Entity, NBTStorage, + Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture, ai::goal::{ - active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal, - look_at_entity::LookAtEntityGoal, swim::SwimGoal, wander_around::WanderAroundGoal, + Controls, Goal, GoalFuture, active_target::ActiveTargetGoal, + look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal, + wander_around::WanderAroundGoal, }, mob::{Mob, MobEntity}, + projectile::evoker_fangs::EvokerFangsEntity, + r#type::from_type, }; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[repr(u8)] +pub enum IllagerSpell { + None = 0, + SummonVex = 1, + Fangs = 2, + Wololo = 3, + Disappear = 4, + Blindness = 5, +} + +impl IllagerSpell { + #[must_use] + pub const fn from_u8(val: u8) -> Self { + match val { + 1 => Self::SummonVex, + 2 => Self::Fangs, + 3 => Self::Wololo, + 4 => Self::Disappear, + 5 => Self::Blindness, + _ => Self::None, + } + } +} + pub struct EvokerEntity { pub mob_entity: MobEntity, + spell_casting_tick_count: AtomicI32, + current_spell: AtomicU8, + wololo_target_id: Arc>>, } impl EvokerEntity { pub fn new(entity: Entity) -> Arc { let mob_entity = MobEntity::new(entity); - let evoker = Self { mob_entity }; + let evoker = Self { + mob_entity, + spell_casting_tick_count: AtomicI32::new(0), + current_spell: AtomicU8::new(IllagerSpell::None as u8), + wololo_target_id: Arc::new(Mutex::new(None)), + }; let mob_arc = Arc::new(evoker); - let mob_weak: Weak = { - let mob_arc: Arc = mob_arc.clone(); - Arc::downgrade(&mob_arc) + let mob_weak: Weak = Arc::downgrade(&mob_arc); + let mob_interface_weak: Weak = { + let mob_dyn: Arc = mob_arc.clone(); + Arc::downgrade(&mob_dyn) }; { @@ -33,13 +79,16 @@ impl EvokerEntity { .unwrap_or_else(std::sync::PoisonError::into_inner); goal_selector.add_goal(0, Box::new(SwimGoal::default())); - // Evokers have complex spell AI, but for now basic movement - goal_selector.add_goal(5, Box::new(WanderAroundGoal::new(1.0))); + goal_selector.add_goal(1, Box::new(EvokerCastingSpellGoal::new(mob_weak.clone()))); + goal_selector.add_goal(4, Box::new(EvokerSummonSpellGoal::new(mob_weak.clone()))); + goal_selector.add_goal(5, Box::new(EvokerAttackSpellGoal::new(mob_weak.clone()))); + goal_selector.add_goal(6, Box::new(EvokerWololoSpellGoal::new(mob_weak))); + goal_selector.add_goal(8, Box::new(WanderAroundGoal::new(0.6))); goal_selector.add_goal( - 6, - LookAtEntityGoal::with_default(mob_weak.clone(), &EntityType::PLAYER, 8.0), + 9, + LookAtEntityGoal::with_default(mob_interface_weak, &EntityType::PLAYER, 3.0), ); - goal_selector.add_goal(7, Box::new(RandomLookAroundGoal::default())); + goal_selector.add_goal(10, Box::new(RandomLookAroundGoal::default())); let mut target_selector = mob_arc .mob_entity @@ -62,12 +111,511 @@ impl EvokerEntity { mob_arc } + + pub fn is_casting_spell(&self) -> bool { + self.spell_casting_tick_count.load(Ordering::Relaxed) > 0 + } + + pub fn set_is_casting_spell(&self, spell: IllagerSpell) { + self.current_spell.store(spell as u8, Ordering::Relaxed); + let entity = &self.mob_entity.living_entity.entity; + entity.send_meta_data( + &[Metadata::new( + TrackedData::SPELL_CASTING_ID, + MetaDataType::BYTE, + spell as u8 as i8, + )], + None, + ); + } + + pub fn get_current_spell(&self) -> IllagerSpell { + IllagerSpell::from_u8(self.current_spell.load(Ordering::Relaxed)) + } + + pub fn get_spell_casting_time(&self) -> i32 { + self.spell_casting_tick_count.load(Ordering::Relaxed) + } + + pub fn set_spell_casting_time(&self, ticks: i32) { + self.spell_casting_tick_count + .store(ticks, Ordering::Relaxed); + } } -impl NBTStorage for EvokerEntity {} +impl NBTStorage for EvokerEntity { + fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> { + Box::pin(async move { + self.mob_entity.living_entity.entity.write_nbt(nbt).await; + nbt.put_int("SpellTicks", self.get_spell_casting_time()); + }) + } + + fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> { + Box::pin(async move { + self.mob_entity + .living_entity + .entity + .read_nbt_non_mut(nbt) + .await; + if let Some(ticks) = nbt.get_int("SpellTicks") { + self.set_spell_casting_time(ticks); + } + }) + } +} impl Mob for EvokerEntity { fn get_mob_entity(&self) -> &MobEntity { &self.mob_entity } + + fn mob_tick<'a>(&'a self, _caller: &'a Arc) -> EntityBaseFuture<'a, ()> { + Box::pin(async move { + let ticks = self.spell_casting_tick_count.load(Ordering::Relaxed); + if ticks > 0 { + self.spell_casting_tick_count + .store(ticks - 1, Ordering::Relaxed); + } + }) + } +} + +pub struct EvokerCastingSpellGoal { + evoker: Weak, +} + +impl EvokerCastingSpellGoal { + #[must_use] + pub const fn new(evoker: Weak) -> Self { + Self { evoker } + } +} + +impl Goal for EvokerCastingSpellGoal { + fn can_start<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, bool> { + Box::pin(async move { + let Some(evoker) = self.evoker.upgrade() else { + return false; + }; + evoker.is_casting_spell() + }) + } + + fn should_continue<'a>(&'a self, _mob: &'a dyn Mob) -> GoalFuture<'a, bool> { + Box::pin(async move { + let Some(evoker) = self.evoker.upgrade() else { + return false; + }; + evoker.is_casting_spell() + }) + } + + fn start<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, ()> { + Box::pin(async move { + if let Some(evoker) = self.evoker.upgrade() { + evoker + .mob_entity + .living_entity + .entity + .velocity + .store(Vector3::new(0.0, 0.0, 0.0)); + } + }) + } + + fn stop<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, ()> { + Box::pin(async move { + if let Some(evoker) = self.evoker.upgrade() { + evoker.set_is_casting_spell(IllagerSpell::None); + } + }) + } + + fn controls(&self) -> Controls { + Controls::MOVE | Controls::LOOK + } +} + +pub struct EvokerAttackSpellGoal { + evoker: Weak, + warmup_delay: i32, + next_attack_tick: i32, +} + +impl EvokerAttackSpellGoal { + #[must_use] + pub const fn new(evoker: Weak) -> Self { + Self { + evoker, + warmup_delay: 0, + next_attack_tick: 0, + } + } +} + +impl Goal for EvokerAttackSpellGoal { + fn can_start<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, bool> { + Box::pin(async move { + let Some(evoker) = self.evoker.upgrade() else { + return false; + }; + if evoker.is_casting_spell() { + return false; + } + let entity = &evoker.mob_entity.living_entity.entity; + if entity.age.load(Ordering::Relaxed) < self.next_attack_tick { + return false; + } + let target = evoker.mob_entity.target.lock().await.clone(); + target.is_some() + }) + } + + fn should_continue<'a>(&'a self, _mob: &'a dyn Mob) -> GoalFuture<'a, bool> { + Box::pin(async move { self.warmup_delay > 0 }) + } + + fn start<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, ()> { + Box::pin(async move { + self.warmup_delay = 20; + if let Some(evoker) = self.evoker.upgrade() { + evoker.set_spell_casting_time(40); + let age = evoker + .mob_entity + .living_entity + .entity + .age + .load(Ordering::Relaxed); + self.next_attack_tick = age + 100; + evoker.set_is_casting_spell(IllagerSpell::Fangs); + evoker + .mob_entity + .living_entity + .entity + .play_sound(Sound::EntityEvokerPrepareAttack); + } + }) + } + + fn tick<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, ()> { + Box::pin(async move { + self.warmup_delay -= 1; + if self.warmup_delay == 0 { + let Some(evoker) = self.evoker.upgrade() else { + return; + }; + let target = evoker.mob_entity.target.lock().await.clone(); + let Some(target) = target else { + return; + }; + + let evoker_ent = &evoker.mob_entity.living_entity.entity; + evoker_ent.play_sound(Sound::EntityEvokerCastSpell); + + let evoker_pos = evoker_ent.pos.load(); + let target_pos = target.get_entity().pos.load(); + + let dx = target_pos.x - evoker_pos.x; + let dz = target_pos.z - evoker_pos.z; + let angle_towards_target = (dz.atan2(dx)) as f32; + + let min_y = evoker_pos.y.min(target_pos.y); + let dist_sq = evoker_pos.squared_distance_to_vec(&target_pos); + let world = evoker_ent.world.load(); + let evoker_id = evoker_ent.entity_id; + + if dist_sq < 81.0 { + // Close range: concentric rings around Evoker + for i in 0..5 { + let angle = angle_towards_target + (i as f32) * std::f32::consts::PI * 0.4; + let spawn_x = evoker_pos.x + (angle.cos() as f64) * 1.5; + let spawn_z = evoker_pos.z + (angle.sin() as f64) * 1.5; + let pos = Vector3::new(spawn_x, min_y, spawn_z); + + let entity = Entity::from_uuid( + Uuid::new_v4(), + world.clone(), + pos, + &EntityType::EVOKER_FANGS, + ); + let fangs = + Arc::new(EvokerFangsEntity::new(entity, 0, angle, Some(evoker_id))); + world.spawn_entity(fangs).await; + } + + for i in 0..8 { + let angle = angle_towards_target + + (i as f32) * std::f32::consts::PI * 2.0 / 8.0 + + 1.256_637_1; + let spawn_x = evoker_pos.x + (angle.cos() as f64) * 2.5; + let spawn_z = evoker_pos.z + (angle.sin() as f64) * 2.5; + let pos = Vector3::new(spawn_x, min_y, spawn_z); + + let entity = Entity::from_uuid( + Uuid::new_v4(), + world.clone(), + pos, + &EntityType::EVOKER_FANGS, + ); + let fangs = + Arc::new(EvokerFangsEntity::new(entity, 3, angle, Some(evoker_id))); + world.spawn_entity(fangs).await; + } + } else { + // Long range: line of fangs towards target + for i in 0..16 { + let reach = 1.25 * ((i + 1) as f64); + let spawn_x = evoker_pos.x + (angle_towards_target.cos() as f64) * reach; + let spawn_z = evoker_pos.z + (angle_towards_target.sin() as f64) * reach; + let pos = Vector3::new(spawn_x, min_y, spawn_z); + + let entity = Entity::from_uuid( + Uuid::new_v4(), + world.clone(), + pos, + &EntityType::EVOKER_FANGS, + ); + let fangs = Arc::new(EvokerFangsEntity::new( + entity, + i as u32, + angle_towards_target, + Some(evoker_id), + )); + world.spawn_entity(fangs).await; + } + } + } + }) + } +} + +pub struct EvokerSummonSpellGoal { + evoker: Weak, + warmup_delay: i32, + next_attack_tick: i32, +} + +impl EvokerSummonSpellGoal { + #[must_use] + pub const fn new(evoker: Weak) -> Self { + Self { + evoker, + warmup_delay: 0, + next_attack_tick: 0, + } + } +} + +impl Goal for EvokerSummonSpellGoal { + fn can_start<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, bool> { + Box::pin(async move { + let Some(evoker) = self.evoker.upgrade() else { + return false; + }; + if evoker.is_casting_spell() { + return false; + } + let entity = &evoker.mob_entity.living_entity.entity; + if entity.age.load(Ordering::Relaxed) < self.next_attack_tick { + return false; + } + let target = evoker.mob_entity.target.lock().await.clone(); + if target.is_none() { + return false; + } + + // Count nearby Vexes + let bb = entity.bounding_box.load().expand(16.0, 16.0, 16.0); + let world = entity.world.load(); + let nearby = world.get_entities_at_box(&bb); + let vex_count = nearby + .iter() + .filter(|e| *e.get_entity().entity_type == EntityType::VEX) + .count(); + + let max_allowed = (rand::random::() % 8 + 1) as usize; + vex_count < max_allowed + }) + } + + fn should_continue<'a>(&'a self, _mob: &'a dyn Mob) -> GoalFuture<'a, bool> { + Box::pin(async move { self.warmup_delay > 0 }) + } + + fn start<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, ()> { + Box::pin(async move { + self.warmup_delay = 20; + if let Some(evoker) = self.evoker.upgrade() { + evoker.set_spell_casting_time(100); + let age = evoker + .mob_entity + .living_entity + .entity + .age + .load(Ordering::Relaxed); + self.next_attack_tick = age + 340; + evoker.set_is_casting_spell(IllagerSpell::SummonVex); + evoker + .mob_entity + .living_entity + .entity + .play_sound(Sound::EntityEvokerPrepareSummon); + } + }) + } + + fn tick<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, ()> { + Box::pin(async move { + self.warmup_delay -= 1; + if self.warmup_delay == 0 { + let Some(evoker) = self.evoker.upgrade() else { + return; + }; + let evoker_ent = &evoker.mob_entity.living_entity.entity; + evoker_ent.play_sound(Sound::EntityEvokerCastSpell); + + let world = evoker_ent.world.load(); + let evoker_pos = evoker_ent.pos.load(); + + for _ in 0..3 { + let offset_x = (rand::random::() % 5 - 2) as f64; + let offset_z = (rand::random::() % 5 - 2) as f64; + let spawn_pos = Vector3::new( + evoker_pos.x + offset_x, + evoker_pos.y + 1.0, + evoker_pos.z + offset_z, + ); + + let vex = from_type(&EntityType::VEX, spawn_pos, &world, Uuid::new_v4()); + world.spawn_entity(vex).await; + } + } + }) + } +} + +pub struct EvokerWololoSpellGoal { + evoker: Weak, + warmup_delay: i32, + next_attack_tick: i32, +} + +impl EvokerWololoSpellGoal { + #[must_use] + pub const fn new(evoker: Weak) -> Self { + Self { + evoker, + warmup_delay: 0, + next_attack_tick: 0, + } + } +} + +impl Goal for EvokerWololoSpellGoal { + fn can_start<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, bool> { + Box::pin(async move { + let Some(evoker) = self.evoker.upgrade() else { + return false; + }; + if evoker.is_casting_spell() { + return false; + } + let entity = &evoker.mob_entity.living_entity.entity; + if entity.age.load(Ordering::Relaxed) < self.next_attack_tick { + return false; + } + let target = evoker.mob_entity.target.lock().await.clone(); + if target.is_some() { + return false; + } + + // Find blue sheep within 16 blocks + let bb = entity.bounding_box.load().expand(16.0, 4.0, 16.0); + let world = entity.world.load(); + let candidates = world.get_entities_at_box(&bb); + + for cand in candidates { + if *cand.get_entity().entity_type == EntityType::SHEEP + && let Some(mob) = cand.get_mob() + && let Some(sheep) = mob.get_sheep() + { + // Blue color is 11 in Minecraft + if sheep.get_color() == 11 { + *evoker.wololo_target_id.lock().await = Some(cand.get_entity().entity_id); + return true; + } + } + } + + false + }) + } + + fn should_continue<'a>(&'a self, _mob: &'a dyn Mob) -> GoalFuture<'a, bool> { + Box::pin(async move { self.warmup_delay > 0 }) + } + + fn start<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, ()> { + Box::pin(async move { + self.warmup_delay = 40; + if let Some(evoker) = self.evoker.upgrade() { + evoker.set_spell_casting_time(60); + let age = evoker + .mob_entity + .living_entity + .entity + .age + .load(Ordering::Relaxed); + self.next_attack_tick = age + 140; + evoker.set_is_casting_spell(IllagerSpell::Wololo); + evoker + .mob_entity + .living_entity + .entity + .play_sound(Sound::EntityEvokerPrepareWololo); + } + }) + } + + fn stop<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, ()> { + Box::pin(async move { + if let Some(evoker) = self.evoker.upgrade() { + *evoker.wololo_target_id.lock().await = None; + } + }) + } + + fn tick<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, ()> { + Box::pin(async move { + self.warmup_delay -= 1; + if self.warmup_delay == 0 { + let Some(evoker) = self.evoker.upgrade() else { + return; + }; + let evoker_ent = &evoker.mob_entity.living_entity.entity; + evoker_ent.play_sound(Sound::EntityEvokerCastSpell); + + let target_id = *evoker.wololo_target_id.lock().await; + let Some(target_id) = target_id else { + return; + }; + + let world = evoker_ent.world.load(); + let bb = evoker_ent.bounding_box.load().expand(16.0, 4.0, 16.0); + let candidates = world.get_entities_at_box(&bb); + + for cand in candidates { + if cand.get_entity().entity_id == target_id + && let Some(mob) = cand.get_mob() + && let Some(sheep) = mob.get_sheep() + { + // Convert color to Red (14) + sheep.set_color(14); + break; + } + } + } + }) + } } diff --git a/crates/pumpkin/src/entity/mob/mod.rs b/crates/pumpkin/src/entity/mob/mod.rs index a31b1fb18..9aec5424c 100644 --- a/crates/pumpkin/src/entity/mob/mod.rs +++ b/crates/pumpkin/src/entity/mob/mod.rs @@ -623,12 +623,35 @@ pub trait Mob: EntityBase + Send + Sync { fn get_sheep(&self) -> Option<&crate::entity::passive::sheep::SheepEntity> { None } + + fn mob_on_lightning_strike<'a>( + &'a self, + caller: &'a dyn EntityBase, + lightning: &'a crate::entity::lightning::LightningBoltEntity, + ) -> EntityBaseFuture<'a, ()> { + Box::pin(async move { + self.get_mob_entity() + .living_entity + .on_lightning_strike(caller, lightning) + .await; + }) + } } impl EntityBase for T { fn get_mob(&self) -> Option<&dyn Mob> { Some(self) } + fn on_lightning_strike<'a>( + &'a self, + caller: &'a dyn EntityBase, + lightning: &'a crate::entity::lightning::LightningBoltEntity, + ) -> EntityBaseFuture<'a, ()> { + Box::pin(async move { + self.mob_on_lightning_strike(caller, lightning).await; + }) + } + fn get_item_steerable(&self) -> Option<&dyn crate::entity::item_steerable::ItemSteerable> { Mob::get_item_steerable(self) } diff --git a/crates/pumpkin/src/entity/mod.rs b/crates/pumpkin/src/entity/mod.rs index 9ba459c9f..7ee31cabe 100644 --- a/crates/pumpkin/src/entity/mod.rs +++ b/crates/pumpkin/src/entity/mod.rs @@ -91,9 +91,12 @@ pub mod effect; pub mod experience_orb; pub mod falling; pub mod hunger; +pub mod interaction; pub mod item; pub mod item_steerable; +pub mod lightning; pub mod living; +pub mod marker; pub mod mob; pub mod passive; pub mod player; @@ -103,6 +106,8 @@ pub mod tnt; pub mod r#type; pub mod vehicle; +pub use lightning::LightningBoltEntity; + mod combat; pub mod predicate; @@ -253,6 +258,28 @@ pub trait EntityBase: Send + Sync + NBTStorage + std::any::Any { }) } + fn on_lightning_strike<'a>( + &'a self, + caller: &'a dyn EntityBase, + lightning: &'a lightning::LightningBoltEntity, + ) -> EntityBaseFuture<'a, ()> { + Box::pin(async move { + if self.get_living_entity().is_some() { + self.set_on_fire_for(8.0); + let cause = lightning.get_cause().await; + self.damage_with_context( + caller, + 5.0, + DamageType::LIGHTNING_BOLT, + None, + Some(lightning), + cause.as_deref().map(|p| p as &dyn EntityBase), + ) + .await; + } + }) + } + fn is_spectator(&self) -> bool { false } @@ -3210,6 +3237,11 @@ impl Entity { vehicle.is_some() } + pub async fn is_leashed(&self) -> bool { + let leashed_to = self.leashed_to.lock().await; + leashed_to.is_some() + } + pub async fn add_passenger( &self, vehicle: Arc, diff --git a/crates/pumpkin/src/entity/passive/armadillo.rs b/crates/pumpkin/src/entity/passive/armadillo.rs index 72e1451bc..a63ffb6f0 100644 --- a/crates/pumpkin/src/entity/passive/armadillo.rs +++ b/crates/pumpkin/src/entity/passive/armadillo.rs @@ -1,27 +1,140 @@ -use std::sync::{Arc, Weak}; +use std::sync::{ + Arc, Weak, + atomic::{AtomicI32, AtomicU64, Ordering}, +}; +use pumpkin_data::damage::DamageType; use pumpkin_data::entity::EntityType; +use pumpkin_data::item::Item; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::meta_data_type::MetaDataType; +use pumpkin_data::sound::{Sound, SoundCategory}; +use pumpkin_data::tag::{self, Taggable}; +use pumpkin_data::tracked_data::TrackedData; +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_protocol::codec::var_int::VarInt; +use pumpkin_protocol::java::client::play::Metadata; use crate::entity::{ - Entity, NBTStorage, + Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture, + ageable::{AgeableData, AgeableMob}, ai::goal::{ + breed::BreedGoal, escape_danger::EscapeDangerGoal, follow_parent::FollowParentGoal, look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal, - wander_around::WanderAroundGoal, + tempt::TemptGoal, wander_around::WanderAroundGoal, }, + item::ItemEntity, mob::{Mob, MobEntity}, + passive::animal::Animal, + player::Player, }; +pub const ARMADILLO_FOOD: &[&Item] = &[&Item::SPIDER_EYE]; +pub const ARMADILLO_BABY_START_AGE: i32 = -48000; +pub const SCARE_CHECK_INTERVAL: i32 = 80; +pub const SCARE_DISTANCE_HORIZONTAL: f64 = 7.0; +pub const SCARE_DISTANCE_VERTICAL: f64 = 2.0; + +fn pick_next_scute_drop_time() -> i32 { + let rand_ticks = (rand::random::() % 6000) as i32; + rand_ticks + 6000 +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[repr(i32)] +pub enum ArmadilloState { + #[default] + Idle = 0, + Rolling = 1, + Scared = 2, + Unrolling = 3, +} + +impl ArmadilloState { + #[must_use] + pub const fn from_id(id: i32) -> Self { + match id { + 1 => Self::Rolling, + 2 => Self::Scared, + 3 => Self::Unrolling, + _ => Self::Idle, + } + } + + #[must_use] + pub const fn id(self) -> i32 { + self as i32 + } + + #[must_use] + pub const fn is_threatened(self) -> bool { + !matches!(self, Self::Idle) + } + + #[must_use] + pub const fn animation_duration(self) -> u64 { + match self { + Self::Idle => 0, + Self::Rolling => 10, + Self::Scared => 50, + Self::Unrolling => 30, + } + } + + #[must_use] + pub const fn should_hide_in_shell(self, ticks_in_state: u64) -> bool { + match self { + Self::Idle => false, + Self::Rolling => ticks_in_state > 5, + Self::Scared => true, + Self::Unrolling => ticks_in_state < 26, + } + } + + #[must_use] + pub const fn name(self) -> &'static str { + match self { + Self::Idle => "idle", + Self::Rolling => "rolling", + Self::Scared => "scared", + Self::Unrolling => "unrolling", + } + } + + #[must_use] + pub fn from_name(name: &str) -> Self { + match name { + "rolling" => Self::Rolling, + "scared" => Self::Scared, + "unrolling" => Self::Unrolling, + _ => Self::Idle, + } + } +} + /// Represents an Armadillo, a passive entity that can roll into a ball when threatened. /// /// Wiki: pub struct ArmadilloEntity { pub mob_entity: MobEntity, + pub ageable_data: AgeableData, + pub state: AtomicI32, + pub in_state_ticks: AtomicU64, + pub scute_time: AtomicI32, + pub danger_detected_recently_ticks: AtomicI32, } impl ArmadilloEntity { pub fn new(entity: Entity) -> Arc { let mob_entity = MobEntity::new(entity); - let armadillo = Self { mob_entity }; + let armadillo = Self { + mob_entity, + ageable_data: AgeableData::default(), + state: AtomicI32::new(ArmadilloState::Idle.id()), + in_state_ticks: AtomicU64::new(0), + scute_time: AtomicI32::new(pick_next_scute_drop_time()), + danger_detected_recently_ticks: AtomicI32::new(0), + }; let mob_arc = Arc::new(armadillo); let mob_weak: Weak = { let mob_arc: Arc = mob_arc.clone(); @@ -36,22 +149,320 @@ impl ArmadilloEntity { .unwrap_or_else(std::sync::PoisonError::into_inner); goal_selector.add_goal(0, Box::new(SwimGoal::default())); - goal_selector.add_goal(1, Box::new(WanderAroundGoal::new(1.0))); + goal_selector.add_goal(1, EscapeDangerGoal::new(2.0)); + goal_selector.add_goal(2, BreedGoal::new(1.0)); + goal_selector.add_goal(3, Box::new(TemptGoal::new(1.25, ARMADILLO_FOOD))); + goal_selector.add_goal(4, Box::new(FollowParentGoal::new(1.1))); + goal_selector.add_goal(5, Box::new(WanderAroundGoal::new(1.0))); goal_selector.add_goal( - 2, + 6, LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 6.0), ); - goal_selector.add_goal(3, Box::new(RandomLookAroundGoal::default())); + goal_selector.add_goal(7, Box::new(RandomLookAroundGoal::default())); }; mob_arc } + + #[must_use] + pub fn get_state(&self) -> ArmadilloState { + ArmadilloState::from_id(self.state.load(Ordering::Relaxed)) + } + + pub fn switch_to_state(&self, state: ArmadilloState) { + self.state.store(state.id(), Ordering::Relaxed); + self.in_state_ticks.store(0, Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + TrackedData::ARMADILLO_STATE, + MetaDataType::ARMADILLO_STATE, + VarInt(state.id()), + )], + None, + ); + } + + #[must_use] + pub fn is_scared(&self) -> bool { + self.get_state() != ArmadilloState::Idle + } + + #[must_use] + pub fn should_hide_in_shell(&self) -> bool { + self.get_state() + .should_hide_in_shell(self.in_state_ticks.load(Ordering::Relaxed)) + } + + #[must_use] + pub fn should_switch_to_scared_state(&self) -> bool { + self.get_state() == ArmadilloState::Rolling + && self.in_state_ticks.load(Ordering::Relaxed) + > ArmadilloState::Rolling.animation_duration() + } + + pub async fn can_stay_rolled_up(&self) -> bool { + !self.is_panicking() + && !self.mob_entity.living_entity.is_in_water() + && !self.get_entity().has_vehicle().await + } + + pub fn roll_up(&self) { + if !self.is_scared() { + self.mob_entity.reset_love_ticks(); + let entity = self.get_entity(); + let world = entity.world.load(); + world.play_sound( + Sound::EntityArmadilloRoll, + SoundCategory::Neutral, + &entity.pos.load(), + ); + self.switch_to_state(ArmadilloState::Rolling); + } + } + + pub fn roll_out(&self) { + if self.is_scared() { + let entity = self.get_entity(); + let world = entity.world.load(); + world.play_sound( + Sound::EntityArmadilloUnrollFinish, + SoundCategory::Neutral, + &entity.pos.load(), + ); + self.switch_to_state(ArmadilloState::Idle); + } + } + + pub fn brush_off_scute<'a>(&'a self, player: &'a Arc) -> EntityBaseFuture<'a, bool> { + Box::pin(async move { + if self.is_baby() { + return false; + } + let entity = self.get_entity(); + let world = entity.world.load(); + let pos = entity.pos.load(); + let item_entity = Arc::new(ItemEntity::new( + Entity::new(world.clone(), pos, &EntityType::ITEM), + ItemStack::new(1, &Item::ARMADILLO_SCUTE), + )); + world.spawn_entity(item_entity).await; + world.play_sound(Sound::EntityArmadilloBrush, SoundCategory::Neutral, &pos); + player.damage_held_item(16).await; + true + }) + } + + pub async fn is_scared_by(&self, living_entity: &dyn EntityBase) -> bool { + let entity = self.get_entity(); + let pos = entity.pos.load(); + let target_pos = living_entity.get_entity().pos.load(); + let dx = (pos.x - target_pos.x).abs(); + let dy = (pos.y - target_pos.y).abs(); + let dz = (pos.z - target_pos.z).abs(); + + if dx > SCARE_DISTANCE_HORIZONTAL + || dz > SCARE_DISTANCE_HORIZONTAL + || dy > SCARE_DISTANCE_VERTICAL + { + return false; + } + + let target_type = living_entity.get_entity().entity_type; + if target_type.has_tag(&tag::EntityType::MINECRAFT_UNDEAD) { + return true; + } + + if target_type == &EntityType::PLAYER { + let target_ent = living_entity.get_entity(); + if target_ent.is_sprinting() || target_ent.has_vehicle().await { + return true; + } + } + + false + } } -impl NBTStorage for ArmadilloEntity {} +impl AgeableMob for ArmadilloEntity { + fn get_ageable_data(&self) -> &AgeableData { + &self.ageable_data + } + + fn get_baby_start_age(&self) -> i32 { + ARMADILLO_BABY_START_AGE + } +} + +impl NBTStorage for ArmadilloEntity { + 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; + self.write_ageable_nbt(nbt); + self.write_animal_nbt(nbt); + nbt.put_string("state", self.get_state().name().to_string()); + nbt.put_int("scute_time", self.scute_time.load(Ordering::Relaxed)); + }) + } + + 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; + self.read_ageable_nbt(nbt); + self.read_animal_nbt(nbt); + if let Some(state_name) = nbt.get_string("state") { + self.switch_to_state(ArmadilloState::from_name(state_name)); + } else if let Some(state_id) = nbt.get_int("state") { + self.switch_to_state(ArmadilloState::from_id(state_id)); + } + if let Some(scute_time) = nbt.get_int("scute_time") { + self.scute_time.store(scute_time, Ordering::Relaxed); + } + }) + } +} + +impl Animal for ArmadilloEntity { + fn is_food(&self, item_stack: &ItemStack) -> bool { + item_stack + .item + .has_tag(&tag::Item::MINECRAFT_ARMADILLO_FOOD) + || ARMADILLO_FOOD.iter().any(|i| i.id == item_stack.item.id) + } +} impl Mob for ArmadilloEntity { fn get_mob_entity(&self) -> &MobEntity { &self.mob_entity } + + fn modify_incoming_damage(&self, amount: f32, _damage_type: DamageType) -> f32 { + if self.is_scared() { + (amount - 1.0).max(0.0) / 2.0 + } else { + amount + } + } + + fn on_damage<'a>( + &'a self, + _damage_type: DamageType, + source: Option<&'a dyn EntityBase>, + ) -> EntityBaseFuture<'a, ()> { + Box::pin(async move { + if self.get_entity().is_alive() + && let Some(src) = source + && src.get_entity().entity_type != &EntityType::ITEM + { + self.danger_detected_recently_ticks + .store(SCARE_CHECK_INTERVAL, Ordering::Relaxed); + if self.can_stay_rolled_up().await { + self.roll_up(); + } + } + }) + } + + fn mob_tick<'a>(&'a self, _caller: &'a Arc) -> EntityBaseFuture<'a, ()> { + Box::pin(async move { + self.ageable_ai_step(); + + self.in_state_ticks.fetch_add(1, Ordering::Relaxed); + let danger_ticks = self.danger_detected_recently_ticks.load(Ordering::Relaxed); + if danger_ticks > 0 { + self.danger_detected_recently_ticks + .store(danger_ticks - 1, Ordering::Relaxed); + } + + let entity = self.get_entity(); + let world = entity.world.load(); + + if entity.is_alive() && !self.is_baby() { + let scute_time = self.scute_time.fetch_sub(1, Ordering::Relaxed) - 1; + if scute_time <= 0 { + let pos = entity.pos.load(); + let item_entity = Arc::new(ItemEntity::new( + Entity::new(world.clone(), pos, &EntityType::ITEM), + ItemStack::new(1, &Item::ARMADILLO_SCUTE), + )); + world.spawn_entity(item_entity).await; + world.play_sound( + Sound::EntityArmadilloScuteDrop, + SoundCategory::Neutral, + &pos, + ); + self.scute_time + .store(pick_next_scute_drop_time(), Ordering::Relaxed); + } + } + + let state = self.get_state(); + let ticks_in_state = self.in_state_ticks.load(Ordering::Relaxed); + + match state { + ArmadilloState::Rolling => { + if ticks_in_state > ArmadilloState::Rolling.animation_duration() { + self.switch_to_state(ArmadilloState::Scared); + } + } + ArmadilloState::Scared => { + if !self.can_stay_rolled_up().await { + self.roll_out(); + } else if ticks_in_state > ArmadilloState::Scared.animation_duration() + && self.danger_detected_recently_ticks.load(Ordering::Relaxed) == 0 + { + self.switch_to_state(ArmadilloState::Unrolling); + } + } + ArmadilloState::Unrolling => { + if ticks_in_state > ArmadilloState::Unrolling.animation_duration() { + self.roll_out(); + } + } + ArmadilloState::Idle => {} + } + }) + } + + fn mob_init_data_tracker(&self) -> EntityBaseFuture<'_, ()> { + Box::pin(async move { + let entity = self.get_entity(); + let is_baby = entity.age.load(Ordering::Relaxed) < 0; + if is_baby { + entity.send_meta_data( + &[Metadata::new( + TrackedData::BABY_ID, + MetaDataType::BOOLEAN, + true, + )], + None, + ); + } + entity.send_meta_data( + &[Metadata::new( + TrackedData::ARMADILLO_STATE, + MetaDataType::ARMADILLO_STATE, + VarInt(self.get_state().id()), + )], + None, + ); + }) + } + + fn mob_interact<'a>( + &'a self, + player: &'a Arc, + item_stack: &'a mut ItemStack, + ) -> EntityBaseFuture<'a, bool> { + Box::pin(async move { + if item_stack.item == &Item::BRUSH && self.brush_off_scute(player).await { + return true; + } + if self.is_scared() { + return false; + } + self.animal_interact(player, item_stack, Sound::EntityArmadilloAmbient) + .await + }) + } } diff --git a/crates/pumpkin/src/entity/passive/cat.rs b/crates/pumpkin/src/entity/passive/cat.rs index b615b7db1..810849900 100644 --- a/crates/pumpkin/src/entity/passive/cat.rs +++ b/crates/pumpkin/src/entity/passive/cat.rs @@ -1,31 +1,87 @@ use std::sync::{ Arc, Weak, - atomic::{AtomicU8, Ordering}, + atomic::{AtomicBool, AtomicU8, Ordering}, }; -use pumpkin_data::entity::EntityType; +use crossbeam::atomic::AtomicCell; +use pumpkin_data::entity::{EntityStatus, EntityType}; use pumpkin_data::item::Item; +use pumpkin_data::item_stack::ItemStack; use pumpkin_data::meta_data_type::MetaDataType; +use pumpkin_data::tag::{self, Taggable}; use pumpkin_data::tracked_data::TrackedData; use pumpkin_nbt::compound::NbtCompound; +use pumpkin_protocol::bedrock::server::actor_event::ActorEventType; use pumpkin_protocol::codec::var_int::VarInt; use pumpkin_protocol::java::client::play::Metadata; +use rand::RngExt; +use uuid::Uuid; use crate::entity::{ Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture, ai::goal::{ - breed::BreedGoal, escape_danger::EscapeDangerGoal, follow_parent::FollowParentGoal, - look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal, - tempt::TemptGoal, wander_around::WanderAroundGoal, + active_target::ActiveTargetGoal, avoid_entity::AvoidEntityGoal, breed::BreedGoal, + escape_danger::EscapeDangerGoal, follow_owner::FollowOwnerGoal, + follow_parent::FollowParentGoal, look_around::RandomLookAroundGoal, + look_at_entity::LookAtEntityGoal, swim::SwimGoal, tempt::TemptGoal, + wander_around::WanderAroundGoal, }, mob::{Mob, MobEntity}, + passive::animal::Animal, + player::Player, }; const TEMPT_ITEMS: &[&Item] = &[&Item::COD, &Item::SALMON]; +fn get_dye_color_from_item(item: &Item) -> Option { + let key = item.registry_key; + if key.contains("white") { + Some(0) + } else if key.contains("orange") { + Some(1) + } else if key.contains("magenta") { + Some(2) + } else if key.contains("light_blue") { + Some(3) + } else if key.contains("yellow") { + Some(4) + } else if key.contains("lime") { + Some(5) + } else if key.contains("pink") { + Some(6) + } else if key.contains("light_gray") { + Some(8) + } else if key.contains("gray") { + Some(7) + } else if key.contains("cyan") { + Some(9) + } else if key.contains("purple") { + Some(10) + } else if key.contains("blue") { + Some(11) + } else if key.contains("brown") { + Some(12) + } else if key.contains("green") { + Some(13) + } else if key.contains("red") { + Some(14) + } else if key.contains("black") { + Some(15) + } else { + None + } +} + pub struct CatEntity { pub mob_entity: MobEntity, pub variant: AtomicU8, + pub sound_variant: AtomicU8, + pub collar_color: AtomicU8, + pub is_tame: AtomicBool, + pub is_sitting: AtomicBool, + pub is_lying: AtomicBool, + pub relax_state_one: AtomicBool, + pub owner: AtomicCell>, } impl CatEntity { @@ -33,7 +89,14 @@ impl CatEntity { let mob_entity = MobEntity::new(entity); let cat = Self { mob_entity, - variant: AtomicU8::new(9), // Default to tabby + variant: AtomicU8::new(1), // Default to black + sound_variant: AtomicU8::new(0), // Default to classic + collar_color: AtomicU8::new(14), // Default to red + is_tame: AtomicBool::new(false), + is_sitting: AtomicBool::new(false), + is_lying: AtomicBool::new(false), + relax_state_one: AtomicBool::new(false), + owner: AtomicCell::new(None), }; let mob_arc = Arc::new(cat); let mob_weak: Weak = { @@ -48,29 +111,177 @@ impl CatEntity { .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); + // Goal 1: SwimGoal (FloatGoal) goal_selector.add_goal(1, Box::new(SwimGoal::default())); + // Goal 1: TamableAnimalPanicGoal (EscapeDangerGoal) goal_selector.add_goal(1, EscapeDangerGoal::new(1.5)); - // goal_selector.add_goal(2, SitGoal::new(mob_arc.clone())); + // Goal 4: CatTemptGoal goal_selector.add_goal(4, Box::new(TemptGoal::new(0.6, TEMPT_ITEMS))); + // Goal 4: CatAvoidEntityGoal (when untamed) + goal_selector.add_goal( + 4, + Box::new(AvoidEntityGoal::new(&EntityType::PLAYER, 16.0, 0.8, 1.33)), + ); + // Goal 5: BreedGoal goal_selector.add_goal(5, BreedGoal::new(0.8)); - // goal_selector.add_goal(7, FollowOwnerGoal::new(1.0, 10.0, 5.0, false)); + // Goal 6: FollowOwnerGoal + goal_selector.add_goal(6, FollowOwnerGoal::new(1.0, 10.0, 5.0)); + // Goal 9: FollowParentGoal goal_selector.add_goal(9, Box::new(FollowParentGoal::new(0.8))); + // Goal 11: WanderAroundGoal goal_selector.add_goal(11, Box::new(WanderAroundGoal::new(0.8))); + // Goal 12: LookAtPlayerGoal goal_selector.add_goal( 12, LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 10.0), ); + // Goal 12: RandomLookAroundGoal goal_selector.add_goal(12, Box::new(RandomLookAroundGoal::default())); }; + { + let mut target_selector = mob_arc + .mob_entity + .target_selector + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + + // Target Goal 1: NonTameRandomTargetGoal for Rabbit and Turtle + target_selector.add_goal( + 1, + ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::RABBIT, false), + ); + target_selector.add_goal( + 1, + ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::TURTLE, false), + ); + }; + mob_arc } + + pub fn get_tame_flags(&self) -> u8 { + let mut flags = 0u8; + if self.is_sitting() { + flags |= 0x01; + } + if self.is_tame() { + flags |= 0x04; + } + flags + } + + pub fn is_tame(&self) -> bool { + self.is_tame.load(Ordering::Relaxed) + } + + pub fn is_sitting(&self) -> bool { + self.is_sitting.load(Ordering::Relaxed) + } + + pub fn is_lying(&self) -> bool { + self.is_lying.load(Ordering::Relaxed) + } + + pub fn is_relax_state_one(&self) -> bool { + self.relax_state_one.load(Ordering::Relaxed) + } + + pub fn get_collar_color(&self) -> u8 { + self.collar_color.load(Ordering::Relaxed) + } + + pub fn set_collar_color(&self, color: u8) { + self.collar_color.store(color, Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + TrackedData::COLLAR_COLOR, + MetaDataType::INT, + VarInt(color as i32), + )], + None, + ); + } + + pub fn set_sitting(&self, sitting: bool) { + self.is_sitting.store(sitting, Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + TrackedData::TAMEABLE_FLAGS, + MetaDataType::BYTE, + self.get_tame_flags(), + )], + None, + ); + } + + pub fn set_tame(&self, tame: bool, owner: Option) { + self.is_tame.store(tame, Ordering::Relaxed); + self.owner.store(owner); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + TrackedData::TAMEABLE_FLAGS, + MetaDataType::BYTE, + self.get_tame_flags(), + )], + None, + ); + entity.send_meta_data( + &[Metadata::new( + TrackedData::OWNER_UUID, + MetaDataType::OPTIONAL_UUID, + owner, + )], + None, + ); + } + + pub fn set_lying(&self, lying: bool) { + self.is_lying.store(lying, Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + TrackedData::IS_LYING, + MetaDataType::BOOLEAN, + lying, + )], + None, + ); + } + + pub fn set_relax_state_one(&self, relax: bool) { + self.relax_state_one.store(relax, Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + TrackedData::RELAX_STATE_ONE, + MetaDataType::BOOLEAN, + relax, + )], + None, + ); + } + + pub fn play_eating_sound(&self) { + let mob_entity = self.get_mob_entity(); + let entity = &mob_entity.living_entity.entity; + let world = entity.world.load(); + world.play_sound( + pumpkin_data::sound::Sound::EntityCatEat, + pumpkin_data::sound::SoundCategory::Neutral, + &entity.pos.load(), + ); + } } impl NBTStorage for CatEntity { fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> { Box::pin(async { self.mob_entity.living_entity.write_nbt(nbt).await; + self.write_animal_nbt(nbt); let variant_str = match self.variant.load(Ordering::Relaxed) { 0 => "minecraft:all_black", 1 => "minecraft:black", @@ -85,12 +296,23 @@ impl NBTStorage for CatEntity { _ => "minecraft:tabby", }; nbt.put_string("variant", variant_str.to_string()); + nbt.put_string("sound_variant", "minecraft:classic".to_string()); + nbt.put_byte( + "CollarColor", + self.collar_color.load(Ordering::Relaxed) as i8, + ); + nbt.put_bool("IsTame", self.is_tame.load(Ordering::Relaxed)); + nbt.put_bool("Sitting", self.is_sitting.load(Ordering::Relaxed)); + if let Some(owner) = self.owner.load() { + nbt.put_uuid("Owner", owner); + } }) } fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> { Box::pin(async { self.mob_entity.living_entity.read_nbt_non_mut(nbt).await; + self.read_animal_nbt(nbt); if let Some(variant_str) = nbt.get_string("variant") { let variant = match variant_str .strip_prefix("minecraft:") @@ -110,15 +332,44 @@ impl NBTStorage for CatEntity { }; self.variant.store(variant, Ordering::Relaxed); } + if let Some(collar) = nbt.get_byte("CollarColor") { + self.collar_color.store(collar as u8, Ordering::Relaxed); + } else if let Some(collar_int) = nbt.get_int("CollarColor") { + self.collar_color.store(collar_int as u8, Ordering::Relaxed); + } + if let Some(sitting) = nbt.get_bool("Sitting") { + self.is_sitting.store(sitting, Ordering::Relaxed); + } + if let Some(owner) = nbt.get_uuid("Owner") { + self.owner.store(Some(owner)); + self.is_tame.store(true, Ordering::Relaxed); + } else if let Some(is_tame) = nbt.get_bool("IsTame") { + self.is_tame.store(is_tame, Ordering::Relaxed); + } }) } } +impl Animal for CatEntity { + fn is_food(&self, item_stack: &ItemStack) -> bool { + let item = item_stack.get_item(); + item.has_tag(&tag::Item::MINECRAFT_CAT_FOOD) || item == &Item::COD || item == &Item::SALMON + } +} + impl Mob for CatEntity { fn get_mob_entity(&self) -> &MobEntity { &self.mob_entity } + fn get_owner_uuid(&self) -> Option { + self.owner.load() + } + + fn is_sitting(&self) -> bool { + self.is_sitting.load(Ordering::Relaxed) + } + fn mob_set_variant_name(&self, name: &str) { let variant = match name.strip_prefix("minecraft:").unwrap_or(name) { "all_black" => 0, @@ -150,6 +401,22 @@ impl Mob for CatEntity { None, ); } + entity.send_meta_data( + &[Metadata::new( + TrackedData::TAMEABLE_FLAGS, + MetaDataType::BYTE, + self.get_tame_flags(), + )], + None, + ); + entity.send_meta_data( + &[Metadata::new( + TrackedData::OWNER_UUID, + MetaDataType::OPTIONAL_UUID, + self.owner.load(), + )], + None, + ); entity.send_meta_data( &[Metadata::new( TrackedData::CAT_VARIANT, @@ -158,6 +425,104 @@ impl Mob for CatEntity { )], None, ); + entity.send_meta_data( + &[Metadata::new( + TrackedData::IS_LYING, + MetaDataType::BOOLEAN, + self.is_lying.load(Ordering::Relaxed), + )], + None, + ); + entity.send_meta_data( + &[Metadata::new( + TrackedData::RELAX_STATE_ONE, + MetaDataType::BOOLEAN, + self.relax_state_one.load(Ordering::Relaxed), + )], + None, + ); + entity.send_meta_data( + &[Metadata::new( + TrackedData::COLLAR_COLOR, + MetaDataType::INT, + VarInt(self.collar_color.load(Ordering::Relaxed) as i32), + )], + None, + ); + entity.send_meta_data( + &[Metadata::new( + TrackedData::SOUND_VARIANT, + MetaDataType::CAT_SOUND_VARIANT, + VarInt(self.sound_variant.load(Ordering::Relaxed) as i32), + )], + None, + ); + }) + } + + fn mob_interact<'a>( + &'a self, + player: &'a Arc, + item_stack: &'a mut ItemStack, + ) -> EntityBaseFuture<'a, bool> { + Box::pin(async move { + let item = item_stack.get_item(); + let is_food = self.is_food(item_stack); + + if self.is_tame() { + if self.get_owner_uuid() == Some(player.gameprofile.id) { + if item.has_tag(&tag::Item::MINECRAFT_CAT_COLLAR_DYES) + || item.has_tag(&tag::Item::C_DYES) + { + if let Some(color) = get_dye_color_from_item(item) + && color != self.get_collar_color() + { + self.set_collar_color(color); + item_stack.decrement_unless_creative(player.gamemode.load(), 1); + return true; + } + } else if is_food + && self.mob_entity.living_entity.health.load() + < self.mob_entity.living_entity.get_max_health() + { + item_stack.decrement_unless_creative(player.gamemode.load(), 1); + self.mob_entity.living_entity.heal(2.0); + self.play_eating_sound(); + return true; + } + + let parent_interaction = self.mob_entity.mob_interact(player, item_stack).await; + if !parent_interaction { + self.set_sitting(!self.is_sitting()); + return true; + } + return parent_interaction; + } + } else if is_food { + item_stack.decrement_unless_creative(player.gamemode.load(), 1); + self.play_eating_sound(); + + let mut rng = rand::rng(); + if rng.random_range(0..3) == 0 { + self.set_tame(true, Some(player.gameprofile.id)); + self.set_sitting(true); + self.get_entity().world.load().send_entity_status( + self.get_entity(), + EntityStatus::TamingSucceeded, + Some(ActorEventType::TamingSucceeded), + ); + } else { + self.get_entity().world.load().send_entity_status( + self.get_entity(), + EntityStatus::TamingFailed, + Some(ActorEventType::TamingFailed), + ); + } + + return true; + } + + self.mob_entity.mob_interact(player, item_stack).await }) } } diff --git a/crates/pumpkin/src/entity/passive/chicken.rs b/crates/pumpkin/src/entity/passive/chicken.rs index b0b807ed3..1479bc5d1 100644 --- a/crates/pumpkin/src/entity/passive/chicken.rs +++ b/crates/pumpkin/src/entity/passive/chicken.rs @@ -130,7 +130,11 @@ impl NBTStorage for ChickenEntity { impl super::animal::Animal for ChickenEntity { fn is_food(&self, item_stack: &ItemStack) -> bool { - TEMPT_ITEMS.iter().any(|i| i.id == item_stack.item.id) + use pumpkin_data::tag::Taggable; + item_stack + .item + .has_tag(&pumpkin_data::tag::Item::MINECRAFT_CHICKEN_FOOD) + || TEMPT_ITEMS.iter().any(|i| i.id == item_stack.item.id) } } diff --git a/crates/pumpkin/src/entity/passive/copper_golem.rs b/crates/pumpkin/src/entity/passive/copper_golem.rs new file mode 100644 index 000000000..cf191fdd0 --- /dev/null +++ b/crates/pumpkin/src/entity/passive/copper_golem.rs @@ -0,0 +1,316 @@ +use std::sync::{ + Arc, Weak, + atomic::{AtomicI32, AtomicI64, Ordering}, +}; + +use pumpkin_data::entity::EntityType; +use pumpkin_data::item::Item; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::meta_data_type::MetaDataType; +use pumpkin_data::sound::{Sound, SoundCategory}; +use pumpkin_data::tag::{self, Taggable}; +use pumpkin_data::tracked_data::TrackedData; +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_protocol::codec::var_int::VarInt; +use pumpkin_protocol::java::client::play::Metadata; + +use crate::entity::{ + Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture, + ai::goal::{ + look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal, + wander_around::WanderAroundGoal, + }, + mob::{Mob, MobEntity}, + player::Player, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[repr(i32)] +pub enum WeatherState { + #[default] + Unaffected = 0, + Exposed = 1, + Weathered = 2, + Oxidized = 3, +} + +impl WeatherState { + #[must_use] + pub const fn from_id(id: i32) -> Self { + match id { + 1 => Self::Exposed, + 2 => Self::Weathered, + 3 => Self::Oxidized, + _ => Self::Unaffected, + } + } + + #[must_use] + pub const fn id(self) -> i32 { + self as i32 + } + + #[must_use] + pub const fn next(self) -> Self { + match self { + Self::Unaffected => Self::Exposed, + Self::Exposed => Self::Weathered, + Self::Weathered | Self::Oxidized => Self::Oxidized, + } + } + + #[must_use] + pub const fn previous(self) -> Self { + match self { + Self::Oxidized => Self::Weathered, + Self::Weathered => Self::Exposed, + Self::Exposed | Self::Unaffected => Self::Unaffected, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[repr(i32)] +pub enum CopperGolemState { + #[default] + Idle = 0, + GettingItem = 1, + GettingNoItem = 2, + DroppingItem = 3, + DroppingNoItem = 4, +} + +impl CopperGolemState { + #[must_use] + pub const fn from_id(id: i32) -> Self { + match id { + 1 => Self::GettingItem, + 2 => Self::GettingNoItem, + 3 => Self::DroppingItem, + 4 => Self::DroppingNoItem, + _ => Self::Idle, + } + } + + #[must_use] + pub const fn id(self) -> i32 { + self as i32 + } +} + +/// Represents a Copper Golem, a passive creation mob made of copper blocks and lightning rod. +/// +/// Wiki: +pub struct CopperGolemEntity { + pub mob_entity: MobEntity, + pub weather_state: AtomicI32, + pub state: AtomicI32, + pub next_weathering_tick: AtomicI64, +} + +impl CopperGolemEntity { + pub fn new(entity: Entity) -> Arc { + let mob_entity = MobEntity::new(entity); + let golem = Self { + mob_entity, + weather_state: AtomicI32::new(WeatherState::Unaffected.id()), + state: AtomicI32::new(CopperGolemState::Idle.id()), + next_weathering_tick: AtomicI64::new(-1), + }; + let mob_arc = Arc::new(golem); + let mob_weak: Weak = { + let mob_arc: Arc = 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(1, Box::new(WanderAroundGoal::new(1.0))); + goal_selector.add_goal( + 2, + LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 6.0), + ); + goal_selector.add_goal(3, Box::new(RandomLookAroundGoal::default())); + }; + + mob_arc + } + + #[must_use] + pub fn get_weather_state(&self) -> WeatherState { + WeatherState::from_id(self.weather_state.load(Ordering::Relaxed)) + } + + pub fn set_weather_state(&self, state: WeatherState) { + self.weather_state.store(state.id(), Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + TrackedData::WEATHER_STATE, + MetaDataType::WEATHERING_COPPER_STATE, + VarInt(state.id()), + )], + None, + ); + } + + #[must_use] + pub fn get_state(&self) -> CopperGolemState { + CopperGolemState::from_id(self.state.load(Ordering::Relaxed)) + } + + pub fn set_state(&self, state: CopperGolemState) { + self.state.store(state.id(), Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + TrackedData::COPPER_GOLEM_STATE, + MetaDataType::COPPER_GOLEM_STATE, + VarInt(state.id()), + )], + None, + ); + } + + #[must_use] + pub fn hurt_sound(&self) -> Sound { + match self.get_weather_state() { + WeatherState::Unaffected | WeatherState::Exposed => Sound::EntityCopperGolemHurt, + WeatherState::Weathered => Sound::EntityCopperGolemWeatheredHurt, + WeatherState::Oxidized => Sound::EntityCopperGolemOxidizedHurt, + } + } + + #[must_use] + pub fn death_sound(&self) -> Sound { + match self.get_weather_state() { + WeatherState::Unaffected | WeatherState::Exposed => Sound::EntityCopperGolemDeath, + WeatherState::Weathered => Sound::EntityCopperGolemWeatheredDeath, + WeatherState::Oxidized => Sound::EntityCopperGolemOxidizedDeath, + } + } + + #[must_use] + pub fn step_sound(&self) -> Sound { + match self.get_weather_state() { + WeatherState::Unaffected | WeatherState::Exposed => Sound::EntityCopperGolemStep, + WeatherState::Weathered => Sound::EntityCopperGolemWeatheredStep, + WeatherState::Oxidized => Sound::EntityCopperGolemOxidizedStep, + } + } +} + +impl NBTStorage for CopperGolemEntity { + 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; + nbt.put_long( + "next_weather_age", + self.next_weathering_tick.load(Ordering::Relaxed), + ); + nbt.put_int("weather_state", self.get_weather_state().id()); + }) + } + + 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(next) = nbt.get_long("next_weather_age") { + self.next_weathering_tick.store(next, Ordering::Relaxed); + } + if let Some(state) = nbt.get_int("weather_state") { + self.set_weather_state(WeatherState::from_id(state)); + } + }) + } +} + +impl Mob for CopperGolemEntity { + fn get_mob_entity(&self) -> &MobEntity { + &self.mob_entity + } + + fn mob_on_lightning_strike<'a>( + &'a self, + caller: &'a dyn EntityBase, + lightning: &'a crate::entity::lightning::LightningBoltEntity, + ) -> EntityBaseFuture<'a, ()> { + Box::pin(async move { + self.set_weather_state(WeatherState::Unaffected); + self.mob_entity + .living_entity + .on_lightning_strike(caller, lightning) + .await; + }) + } + + fn mob_init_data_tracker(&self) -> EntityBaseFuture<'_, ()> { + Box::pin(async move { + let entity = self.get_entity(); + entity.send_meta_data( + &[ + Metadata::new( + TrackedData::WEATHER_STATE, + MetaDataType::WEATHERING_COPPER_STATE, + VarInt(self.get_weather_state().id()), + ), + Metadata::new( + TrackedData::COPPER_GOLEM_STATE, + MetaDataType::COPPER_GOLEM_STATE, + VarInt(self.get_state().id()), + ), + ], + None, + ); + }) + } + + fn mob_interact<'a>( + &'a self, + _player: &'a Arc, + item_stack: &'a mut ItemStack, + ) -> EntityBaseFuture<'a, bool> { + Box::pin(async move { + let entity = self.get_entity(); + let world = entity.world.load(); + + // Honeycomb waxing + if item_stack.item.id == Item::HONEYCOMB.id + && self.next_weathering_tick.load(Ordering::Relaxed) != -2 + { + self.next_weathering_tick.store(-2, Ordering::Relaxed); + let pos = entity.pos.load(); + world.play_sound(Sound::ItemHoneycombWaxOn, SoundCategory::Blocks, &pos); + return true; + } + + // Axe scraping + if item_stack.item.has_tag(&tag::Item::MINECRAFT_AXES) { + let current_next = self.next_weathering_tick.load(Ordering::Relaxed); + if current_next == -2 { + self.next_weathering_tick.store(-1, Ordering::Relaxed); + let pos = entity.pos.load(); + world.play_sound(Sound::ItemAxeScrape, SoundCategory::Blocks, &pos); + return true; + } + + let weather_state = self.get_weather_state(); + if weather_state != WeatherState::Unaffected { + self.set_weather_state(weather_state.previous()); + self.next_weathering_tick.store(-1, Ordering::Relaxed); + let pos = entity.pos.load(); + world.play_sound(Sound::ItemAxeScrape, SoundCategory::Blocks, &pos); + return true; + } + } + + false + }) + } +} diff --git a/crates/pumpkin/src/entity/passive/cow.rs b/crates/pumpkin/src/entity/passive/cow.rs index 8dbdd9b6e..8bda0f0c8 100644 --- a/crates/pumpkin/src/entity/passive/cow.rs +++ b/crates/pumpkin/src/entity/passive/cow.rs @@ -91,7 +91,11 @@ impl NBTStorage for CowEntity { impl super::animal::Animal for CowEntity { fn is_food(&self, item_stack: &ItemStack) -> bool { - TEMPT_ITEMS.iter().any(|i| i.id == item_stack.item.id) + use pumpkin_data::tag::Taggable; + item_stack + .item + .has_tag(&pumpkin_data::tag::Item::MINECRAFT_COW_FOOD) + || TEMPT_ITEMS.iter().any(|i| i.id == item_stack.item.id) } } diff --git a/crates/pumpkin/src/entity/passive/fox.rs b/crates/pumpkin/src/entity/passive/fox.rs index 75c4fb411..37f0497fb 100644 --- a/crates/pumpkin/src/entity/passive/fox.rs +++ b/crates/pumpkin/src/entity/passive/fox.rs @@ -1,30 +1,92 @@ -use std::sync::{Arc, Weak}; +use std::sync::{ + Arc, Weak, + atomic::{AtomicI32, AtomicU8, Ordering}, +}; -use pumpkin_data::{entity::EntityType, item::Item}; +use pumpkin_data::entity::EntityType; +use pumpkin_data::item::Item; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::meta_data_type::MetaDataType; +use pumpkin_data::sound::Sound; +use pumpkin_data::tag::{self, Taggable}; +use pumpkin_data::tracked_data::TrackedData; +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_protocol::codec::var_int::VarInt; +use pumpkin_protocol::java::client::play::Metadata; use crate::entity::{ - Entity, NBTStorage, + Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture, + ageable::{AgeableData, AgeableMob}, ai::goal::{ breed::BreedGoal, escape_danger::EscapeDangerGoal, follow_parent::FollowParentGoal, look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal, tempt::TemptGoal, wander_around::WanderAroundGoal, }, mob::{Mob, MobEntity}, + passive::animal::Animal, + player::Player, }; -const TEMPT_ITEMS: &[&Item] = &[&Item::SWEET_BERRIES, &Item::GLOW_BERRIES]; +pub const FOX_FOOD: &[&Item] = &[&Item::SWEET_BERRIES, &Item::GLOW_BERRIES]; -/// Represents a Fox, a passive nocturnal mob. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[repr(i32)] +pub enum FoxVariant { + #[default] + Red = 0, + Snow = 1, +} + +impl FoxVariant { + #[must_use] + pub const fn from_id(id: i32) -> Self { + match id { + 1 => Self::Snow, + _ => Self::Red, + } + } + + #[must_use] + pub const fn id(self) -> i32 { + self as i32 + } + + #[must_use] + pub const fn as_str(self) -> &'static str { + match self { + Self::Red => "red", + Self::Snow => "snow", + } + } + + #[must_use] + pub fn from_name(s: &str) -> Self { + match s { + "snow" | "minecraft:snow" => Self::Snow, + _ => Self::Red, + } + } +} + +/// Represents a Fox, a passive/neutral mob. /// /// Wiki: pub struct FoxEntity { pub mob_entity: MobEntity, + pub ageable_data: AgeableData, + pub variant: AtomicI32, + pub flags: AtomicU8, } impl FoxEntity { pub fn new(entity: Entity) -> Arc { let mob_entity = MobEntity::new(entity); - let fox = Self { mob_entity }; + let fox = Self { + mob_entity, + ageable_data: AgeableData::default(), + variant: AtomicI32::new(FoxVariant::Red.id()), + flags: AtomicU8::new(0), + }; let mob_arc = Arc::new(fox); let mob_weak: Weak = { let mob_arc: Arc = mob_arc.clone(); @@ -41,7 +103,7 @@ impl FoxEntity { goal_selector.add_goal(0, Box::new(SwimGoal::default())); goal_selector.add_goal(1, EscapeDangerGoal::new(1.5)); goal_selector.add_goal(2, BreedGoal::new(1.0)); - goal_selector.add_goal(3, Box::new(TemptGoal::new(1.2, TEMPT_ITEMS))); + goal_selector.add_goal(3, Box::new(TemptGoal::new(1.2, FOX_FOOD))); goal_selector.add_goal(4, Box::new(FollowParentGoal::new(1.1))); goal_selector.add_goal(6, Box::new(WanderAroundGoal::new(1.0))); goal_selector.add_goal( @@ -53,12 +115,224 @@ impl FoxEntity { mob_arc } + + #[must_use] + pub fn get_variant(&self) -> FoxVariant { + FoxVariant::from_id(self.variant.load(Ordering::Relaxed)) + } + + pub fn set_variant(&self, variant: FoxVariant) { + self.variant.store(variant.id(), Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + TrackedData::TYPE_ID, + MetaDataType::INT, + VarInt(variant.id()), + )], + None, + ); + } + + fn get_flag(&self, flag: u8) -> bool { + (self.flags.load(Ordering::Relaxed) & flag) != 0 + } + + fn set_flag(&self, flag: u8, value: bool) { + let current = self.flags.load(Ordering::Relaxed); + let new_flags = if value { + current | flag + } else { + current & !flag + }; + self.flags.store(new_flags, Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + TrackedData::FLAGS_ID, + MetaDataType::BYTE, + new_flags, + )], + None, + ); + } + + #[must_use] + pub fn is_sitting(&self) -> bool { + self.get_flag(1) + } + + pub fn set_sitting(&self, val: bool) { + self.set_flag(1, val); + } + + #[must_use] + pub fn is_crouching(&self) -> bool { + self.get_flag(4) + } + + pub fn set_crouching(&self, val: bool) { + self.set_flag(4, val); + } + + #[must_use] + pub fn is_interested(&self) -> bool { + self.get_flag(8) + } + + pub fn set_interested(&self, val: bool) { + self.set_flag(8, val); + } + + #[must_use] + pub fn is_pouncing(&self) -> bool { + self.get_flag(16) + } + + pub fn set_pouncing(&self, val: bool) { + self.set_flag(16, val); + } + + #[must_use] + pub fn is_sleeping(&self) -> bool { + self.get_flag(32) + } + + pub fn set_sleeping(&self, val: bool) { + self.set_flag(32, val); + } + + #[must_use] + pub fn is_faceplanted(&self) -> bool { + self.get_flag(64) + } + + pub fn set_faceplanted(&self, val: bool) { + self.set_flag(64, val); + } + + #[must_use] + pub fn is_defending(&self) -> bool { + self.get_flag(128) + } + + pub fn set_defending(&self, val: bool) { + self.set_flag(128, val); + } + + pub fn clear_states(&self) { + self.set_interested(false); + self.set_crouching(false); + self.set_sitting(false); + self.set_sleeping(false); + self.set_defending(false); + self.set_faceplanted(false); + } } -impl NBTStorage for FoxEntity {} +impl AgeableMob for FoxEntity { + fn get_ageable_data(&self) -> &AgeableData { + &self.ageable_data + } +} + +impl Animal for FoxEntity { + fn is_food(&self, item_stack: &ItemStack) -> bool { + item_stack.item.has_tag(&tag::Item::MINECRAFT_FOX_FOOD) + || FOX_FOOD.iter().any(|i| i.id == item_stack.item.id) + } +} + +impl NBTStorage for FoxEntity { + 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; + self.write_ageable_nbt(nbt); + self.write_animal_nbt(nbt); + nbt.put_string("Type", self.get_variant().as_str().to_string()); + nbt.put_bool("Sleeping", self.is_sleeping()); + nbt.put_bool("Sitting", self.is_sitting()); + nbt.put_bool("Crouching", self.is_crouching()); + }) + } + + 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; + self.read_ageable_nbt(nbt); + self.read_animal_nbt(nbt); + if let Some(variant_str) = nbt.get_string("Type") { + self.set_variant(FoxVariant::from_name(variant_str)); + } + if let Some(sleeping) = nbt.get_bool("Sleeping") { + self.set_sleeping(sleeping); + } + if let Some(sitting) = nbt.get_bool("Sitting") { + self.set_sitting(sitting); + } + if let Some(crouching) = nbt.get_bool("Crouching") { + self.set_crouching(crouching); + } + }) + } +} impl Mob for FoxEntity { fn get_mob_entity(&self) -> &MobEntity { &self.mob_entity } + + fn mob_set_variant_name(&self, name: &str) { + self.set_variant(FoxVariant::from_name(name)); + } + + fn mob_tick<'a>(&'a self, _caller: &'a Arc) -> EntityBaseFuture<'a, ()> { + Box::pin(async move { + self.ageable_ai_step(); + }) + } + + fn mob_init_data_tracker(&self) -> EntityBaseFuture<'_, ()> { + Box::pin(async move { + let entity = self.get_entity(); + let is_baby = entity.age.load(Ordering::Relaxed) < 0; + if is_baby { + entity.send_meta_data( + &[Metadata::new( + TrackedData::BABY_ID, + MetaDataType::BOOLEAN, + true, + )], + None, + ); + } + entity.send_meta_data( + &[Metadata::new( + TrackedData::TYPE_ID, + MetaDataType::INT, + VarInt(self.get_variant().id()), + )], + None, + ); + entity.send_meta_data( + &[Metadata::new( + TrackedData::FLAGS_ID, + MetaDataType::BYTE, + self.flags.load(Ordering::Relaxed), + )], + None, + ); + }) + } + + fn mob_interact<'a>( + &'a self, + player: &'a Arc, + item_stack: &'a mut ItemStack, + ) -> EntityBaseFuture<'a, bool> { + Box::pin(async move { + self.animal_interact(player, item_stack, Sound::EntityFoxAmbient) + .await + }) + } } diff --git a/crates/pumpkin/src/entity/passive/frog.rs b/crates/pumpkin/src/entity/passive/frog.rs index c715d71df..fbcdc31e6 100644 --- a/crates/pumpkin/src/entity/passive/frog.rs +++ b/crates/pumpkin/src/entity/passive/frog.rs @@ -1,10 +1,14 @@ use std::sync::{ Arc, Weak, - atomic::{AtomicU8, Ordering}, + atomic::{AtomicI32, Ordering}, }; use pumpkin_data::entity::EntityType; +use pumpkin_data::item::Item; +use pumpkin_data::item_stack::ItemStack; use pumpkin_data::meta_data_type::MetaDataType; +use pumpkin_data::sound::Sound; +use pumpkin_data::tag::{self, Taggable}; use pumpkin_data::tracked_data::TrackedData; use pumpkin_nbt::compound::NbtCompound; use pumpkin_protocol::codec::var_int::VarInt; @@ -12,19 +16,69 @@ use pumpkin_protocol::java::client::play::Metadata; use crate::entity::{ Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture, + ageable::{AgeableData, AgeableMob}, ai::goal::{ look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal, - wander_around::WanderAroundGoal, + tempt::TemptGoal, wander_around::WanderAroundGoal, }, mob::{Mob, MobEntity}, + passive::animal::Animal, + player::Player, }; -/// Represents a Frog, a passive mob that can eat small slimes and magma cubes. +pub const FROG_FOOD: &[&Item] = &[&Item::SLIME_BALL]; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[repr(i32)] +pub enum FrogVariant { + Cold = 0, + #[default] + Temperate = 1, + Warm = 2, +} + +impl FrogVariant { + #[must_use] + pub const fn from_id(id: i32) -> Self { + match id { + 0 => Self::Cold, + 2 => Self::Warm, + _ => Self::Temperate, + } + } + + #[must_use] + pub const fn id(self) -> i32 { + self as i32 + } + + #[must_use] + pub const fn as_str(self) -> &'static str { + match self { + Self::Cold => "minecraft:cold", + Self::Temperate => "minecraft:temperate", + Self::Warm => "minecraft:warm", + } + } + + #[must_use] + pub fn from_name(s: &str) -> Self { + match s { + "minecraft:cold" | "cold" => Self::Cold, + "minecraft:warm" | "warm" => Self::Warm, + _ => Self::Temperate, + } + } +} + +/// Represents a Frog, an amphibious mob that can eat small slimes and magma cubes. /// /// Wiki: pub struct FrogEntity { pub mob_entity: MobEntity, - pub variant: AtomicU8, + pub ageable_data: AgeableData, + pub variant: AtomicI32, + pub tongue_target_id: AtomicI32, } impl FrogEntity { @@ -32,7 +86,9 @@ impl FrogEntity { let mob_entity = MobEntity::new(entity); let frog = Self { mob_entity, - variant: AtomicU8::new(1), // Default to temperate + ageable_data: AgeableData::default(), + variant: AtomicI32::new(FrogVariant::Temperate.id()), + tongue_target_id: AtomicI32::new(-1), }; let mob_arc = Arc::new(frog); let mob_weak: Weak = { @@ -48,41 +104,67 @@ impl FrogEntity { .unwrap_or_else(std::sync::PoisonError::into_inner); goal_selector.add_goal(0, Box::new(SwimGoal::default())); - goal_selector.add_goal(1, Box::new(WanderAroundGoal::new(1.0))); + goal_selector.add_goal(1, Box::new(TemptGoal::new(1.0, FROG_FOOD))); + goal_selector.add_goal(2, Box::new(WanderAroundGoal::new(1.0))); goal_selector.add_goal( - 2, + 3, LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 6.0), ); - goal_selector.add_goal(3, Box::new(RandomLookAroundGoal::default())); + goal_selector.add_goal(4, Box::new(RandomLookAroundGoal::default())); }; mob_arc } + + #[must_use] + pub fn get_variant(&self) -> FrogVariant { + FrogVariant::from_id(self.variant.load(Ordering::Relaxed)) + } + + pub fn set_variant(&self, variant: FrogVariant) { + self.variant.store(variant.id(), Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + TrackedData::VARIANT, + MetaDataType::FROG_VARIANT, + VarInt(variant.id()), + )], + None, + ); + } +} + +impl AgeableMob for FrogEntity { + fn get_ageable_data(&self) -> &AgeableData { + &self.ageable_data + } +} + +impl Animal for FrogEntity { + fn is_food(&self, item_stack: &ItemStack) -> bool { + item_stack.item.has_tag(&tag::Item::MINECRAFT_FROG_FOOD) + || FROG_FOOD.iter().any(|i| i.id == item_stack.item.id) + } } impl NBTStorage for FrogEntity { fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> { - Box::pin(async { + Box::pin(async move { self.mob_entity.living_entity.write_nbt(nbt).await; - let variant_str = match self.variant.load(Ordering::Relaxed) { - 0 => "minecraft:cold", - 2 => "minecraft:warm", - _ => "minecraft:temperate", - }; - nbt.put_string("variant", variant_str.to_string()); + self.write_ageable_nbt(nbt); + self.write_animal_nbt(nbt); + nbt.put_string("variant", self.get_variant().as_str().to_string()); }) } fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> { - Box::pin(async { + Box::pin(async move { self.mob_entity.living_entity.read_nbt_non_mut(nbt).await; + self.read_ageable_nbt(nbt); + self.read_animal_nbt(nbt); if let Some(variant_str) = nbt.get_string("variant") { - let variant = match variant_str { - "minecraft:cold" | "cold" => 0, - "minecraft:warm" | "warm" => 2, - _ => 1, - }; - self.variant.store(variant, Ordering::Relaxed); + self.set_variant(FrogVariant::from_name(variant_str)); } }) } @@ -94,12 +176,13 @@ impl Mob for FrogEntity { } fn mob_set_variant_name(&self, name: &str) { - let variant = match name { - "minecraft:cold" | "cold" => 0, - "minecraft:warm" | "warm" => 2, - _ => 1, - }; - self.variant.store(variant, Ordering::Relaxed); + self.set_variant(FrogVariant::from_name(name)); + } + + fn mob_tick<'a>(&'a self, _caller: &'a Arc) -> EntityBaseFuture<'a, ()> { + Box::pin(async move { + self.ageable_ai_step(); + }) } fn mob_init_data_tracker(&self) -> EntityBaseFuture<'_, ()> { @@ -120,10 +203,21 @@ impl Mob for FrogEntity { &[Metadata::new( TrackedData::VARIANT, MetaDataType::FROG_VARIANT, - VarInt(self.variant.load(Ordering::Relaxed) as i32), + VarInt(self.get_variant().id()), )], None, ); }) } + + fn mob_interact<'a>( + &'a self, + player: &'a Arc, + item_stack: &'a mut ItemStack, + ) -> EntityBaseFuture<'a, bool> { + Box::pin(async move { + self.animal_interact(player, item_stack, Sound::EntityFrogAmbient) + .await + }) + } } diff --git a/crates/pumpkin/src/entity/passive/happy_ghast.rs b/crates/pumpkin/src/entity/passive/happy_ghast.rs new file mode 100644 index 000000000..b679495e6 --- /dev/null +++ b/crates/pumpkin/src/entity/passive/happy_ghast.rs @@ -0,0 +1,249 @@ +use std::sync::{ + Arc, Weak, + atomic::{AtomicBool, AtomicI32, Ordering}, +}; + +use pumpkin_data::entity::EntityType; +use pumpkin_data::item::Item; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::meta_data_type::MetaDataType; +use pumpkin_data::sound::Sound; +use pumpkin_data::tag::{self, Taggable}; +use pumpkin_data::tracked_data::TrackedData; +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_protocol::java::client::play::Metadata; + +use crate::entity::{ + Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture, + ageable::{AgeableData, AgeableMob}, + ai::goal::{ + look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal, + tempt::TemptGoal, wander_around::WanderAroundGoal, + }, + mob::{Mob, MobEntity}, + passive::animal::Animal, + player::Player, +}; + +pub const HAPPY_GHAST_FOOD: &[&Item] = &[&Item::SNOWBALL]; + +/// Represents a Happy Ghast, a passive flying mob. +/// +/// Wiki: +pub struct HappyGhastEntity { + pub mob_entity: MobEntity, + pub ageable_data: AgeableData, + pub server_still_timeout: AtomicI32, + pub leash_holder_time: AtomicI32, + pub is_leash_holder: AtomicBool, + pub stays_still: AtomicBool, +} + +impl HappyGhastEntity { + pub fn new(entity: Entity) -> Arc { + let mob_entity = MobEntity::new(entity); + let happy_ghast = Self { + mob_entity, + ageable_data: AgeableData::default(), + server_still_timeout: AtomicI32::new(0), + leash_holder_time: AtomicI32::new(0), + is_leash_holder: AtomicBool::new(false), + stays_still: AtomicBool::new(false), + }; + let mob_arc = Arc::new(happy_ghast); + let mob_weak: Weak = { + let mob_arc: Arc = 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(1, Box::new(TemptGoal::new(1.0, HAPPY_GHAST_FOOD))); + goal_selector.add_goal(2, Box::new(WanderAroundGoal::new(1.0))); + goal_selector.add_goal( + 3, + LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 6.0), + ); + goal_selector.add_goal(4, Box::new(RandomLookAroundGoal::default())); + }; + + mob_arc + } + + pub fn set_server_still_timeout(&self, timeout: i32) { + self.server_still_timeout.store(timeout, Ordering::Relaxed); + self.sync_stay_still_flag(); + } + + #[must_use] + pub fn is_on_still_timeout(&self) -> bool { + self.stays_still.load(Ordering::Relaxed) + || self.server_still_timeout.load(Ordering::Relaxed) > 0 + } + + fn set_leash_holder(&self, is_leash_holder: bool) { + self.is_leash_holder + .store(is_leash_holder, Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + TrackedData::IS_LEASH_HOLDER, + MetaDataType::BOOLEAN, + is_leash_holder, + )], + None, + ); + } + + fn sync_stay_still_flag(&self) { + let stays_still = self.server_still_timeout.load(Ordering::Relaxed) > 0; + self.stays_still.store(stays_still, Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + TrackedData::STAYS_STILL, + MetaDataType::BOOLEAN, + stays_still, + )], + None, + ); + } +} + +impl AgeableMob for HappyGhastEntity { + fn get_ageable_data(&self) -> &AgeableData { + &self.ageable_data + } +} + +impl NBTStorage for HappyGhastEntity { + 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; + self.write_ageable_nbt(nbt); + self.write_animal_nbt(nbt); + nbt.put_int( + "still_timeout", + self.server_still_timeout.load(Ordering::Relaxed), + ); + }) + } + + 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; + self.read_ageable_nbt(nbt); + self.read_animal_nbt(nbt); + if let Some(timeout) = nbt.get_int("still_timeout") { + self.set_server_still_timeout(timeout); + } + }) + } +} + +impl Animal for HappyGhastEntity { + fn is_food(&self, item_stack: &ItemStack) -> bool { + item_stack + .item + .has_tag(&tag::Item::MINECRAFT_HAPPY_GHAST_FOOD) + || HAPPY_GHAST_FOOD.iter().any(|i| i.id == item_stack.item.id) + } +} + +impl Mob for HappyGhastEntity { + fn get_mob_entity(&self) -> &MobEntity { + &self.mob_entity + } + + fn mob_tick<'a>(&'a self, _caller: &'a Arc) -> EntityBaseFuture<'a, ()> { + Box::pin(async move { + self.ageable_ai_step(); + + let leash_time = self.leash_holder_time.load(Ordering::Relaxed); + if leash_time > 0 { + self.leash_holder_time.fetch_sub(1, Ordering::Relaxed); + } + self.set_leash_holder(leash_time > 0); + + let still_timeout = self.server_still_timeout.load(Ordering::Relaxed); + if still_timeout > 0 { + let entity = self.get_entity(); + if entity.age.load(Ordering::Relaxed) > 60 { + self.server_still_timeout.fetch_sub(1, Ordering::Relaxed); + } + self.sync_stay_still_flag(); + } + + // Continuous healing + let entity = self.get_entity(); + if entity.is_alive() { + let living = &self.mob_entity.living_entity; + let current_health = living.health.load(); + let max_health = living.get_max_health(); + if current_health < max_health { + let world = entity.world.load(); + let ticks = world.level_time.lock().await.world_age; + let heal_interval = 600; + if ticks % heal_interval == 0 { + living.set_health(current_health + 1.0); + } + } + } + }) + } + + fn mob_init_data_tracker(&self) -> EntityBaseFuture<'_, ()> { + Box::pin(async move { + let entity = self.get_entity(); + let is_baby = entity.age.load(Ordering::Relaxed) < 0; + if is_baby { + entity.send_meta_data( + &[Metadata::new( + TrackedData::BABY_ID, + MetaDataType::BOOLEAN, + true, + )], + None, + ); + } + entity.send_meta_data( + &[ + Metadata::new( + TrackedData::IS_LEASH_HOLDER, + MetaDataType::BOOLEAN, + self.is_leash_holder.load(Ordering::Relaxed), + ), + Metadata::new( + TrackedData::STAYS_STILL, + MetaDataType::BOOLEAN, + self.stays_still.load(Ordering::Relaxed), + ), + ], + None, + ); + }) + } + + fn mob_interact<'a>( + &'a self, + player: &'a Arc, + item_stack: &'a mut ItemStack, + ) -> EntityBaseFuture<'a, bool> { + Box::pin(async move { + if self.is_baby() { + return self + .animal_interact(player, item_stack, Sound::EntityGhastlingAmbient) + .await; + } + + self.animal_interact(player, item_stack, Sound::EntityHappyGhastAmbient) + .await + }) + } +} diff --git a/crates/pumpkin/src/entity/passive/iron_golem.rs b/crates/pumpkin/src/entity/passive/iron_golem.rs index f51f507ab..02e70cdee 100644 --- a/crates/pumpkin/src/entity/passive/iron_golem.rs +++ b/crates/pumpkin/src/entity/passive/iron_golem.rs @@ -1,29 +1,48 @@ -use std::sync::{Arc, Weak}; +use std::sync::{ + Arc, Weak, + atomic::{AtomicBool, AtomicI32, Ordering}, +}; use pumpkin_data::entity::EntityType; +use pumpkin_data::item::Item; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::meta_data_type::MetaDataType; +use pumpkin_data::sound::{Sound, SoundCategory}; +use pumpkin_data::tracked_data::TrackedData; +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_protocol::java::client::play::Metadata; +use pumpkin_util::GameMode; use crate::entity::{ - Entity, NBTStorage, NbtFuture, + Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture, ai::goal::{ active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, revenge::RevengeGoal, wander_around::WanderAroundGoal, }, mob::{Mob, MobEntity}, + player::Player, }; -use pumpkin_nbt::compound::NbtCompound; /// Represents an Iron Golem, a powerful neutral mob that protects villagers and players. /// /// Wiki: pub struct IronGolemEntity { pub mob_entity: MobEntity, + pub player_created: AtomicBool, + pub attack_animation_tick: AtomicI32, + pub offer_flower_tick: AtomicI32, } impl IronGolemEntity { pub fn new(entity: Entity) -> Arc { let mob_entity = MobEntity::new(entity); - let iron_golem = Self { mob_entity }; + let iron_golem = Self { + mob_entity, + player_created: AtomicBool::new(false), + attack_animation_tick: AtomicI32::new(0), + offer_flower_tick: AtomicI32::new(0), + }; let mob_arc = Arc::new(iron_golem); let mob_weak: Weak = { let mob_arc: Arc = mob_arc.clone(); @@ -63,15 +82,42 @@ impl IronGolemEntity { mob_arc } + + #[must_use] + pub fn is_player_created(&self) -> bool { + self.player_created.load(Ordering::Relaxed) + } + + pub fn set_player_created(&self, value: bool) { + self.player_created.store(value, Ordering::Relaxed); + let entity = self.get_entity(); + let flag: u8 = u8::from(value); + entity.send_meta_data( + &[Metadata::new( + TrackedData::FLAGS_ID, + MetaDataType::BYTE, + flag, + )], + None, + ); + } } impl NBTStorage for IronGolemEntity { fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> { - self.mob_entity.living_entity.write_nbt(nbt) + Box::pin(async move { + self.mob_entity.living_entity.write_nbt(nbt).await; + nbt.put_bool("PlayerCreated", self.is_player_created()); + }) } fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> { - self.mob_entity.living_entity.read_nbt_non_mut(nbt) + Box::pin(async move { + self.mob_entity.living_entity.read_nbt_non_mut(nbt).await; + if let Some(created) = nbt.get_bool("PlayerCreated") { + self.set_player_created(created); + } + }) } } @@ -79,4 +125,59 @@ impl Mob for IronGolemEntity { fn get_mob_entity(&self) -> &MobEntity { &self.mob_entity } + + fn mob_tick<'a>(&'a self, _caller: &'a Arc) -> EntityBaseFuture<'a, ()> { + Box::pin(async move { + let attack_tick = self.attack_animation_tick.load(Ordering::Relaxed); + if attack_tick > 0 { + self.attack_animation_tick.fetch_sub(1, Ordering::Relaxed); + } + + let flower_tick = self.offer_flower_tick.load(Ordering::Relaxed); + if flower_tick > 0 { + self.offer_flower_tick.fetch_sub(1, Ordering::Relaxed); + } + }) + } + + fn mob_init_data_tracker(&self) -> EntityBaseFuture<'_, ()> { + Box::pin(async move { + let entity = self.get_entity(); + let flag: u8 = u8::from(self.is_player_created()); + entity.send_meta_data( + &[Metadata::new( + TrackedData::FLAGS_ID, + MetaDataType::BYTE, + flag, + )], + None, + ); + }) + } + + fn mob_interact<'a>( + &'a self, + player: &'a Arc, + item_stack: &'a mut ItemStack, + ) -> EntityBaseFuture<'a, bool> { + Box::pin(async move { + if item_stack.item.id == Item::IRON_INGOT.id { + let living = &self.mob_entity.living_entity; + let current_health = living.health.load(); + let max_health = living.get_max_health(); + if current_health < max_health { + living.set_health((current_health + 25.0).min(max_health)); + let entity = self.get_entity(); + let world = entity.world.load(); + let pos = entity.pos.load(); + world.play_sound(Sound::EntityIronGolemRepair, SoundCategory::Neutral, &pos); + if player.gamemode.load() != GameMode::Creative { + item_stack.item_count = item_stack.item_count.saturating_sub(1); + } + return true; + } + } + false + }) + } } diff --git a/crates/pumpkin/src/entity/passive/mod.rs b/crates/pumpkin/src/entity/passive/mod.rs index fb9417577..47779da37 100644 --- a/crates/pumpkin/src/entity/passive/mod.rs +++ b/crates/pumpkin/src/entity/passive/mod.rs @@ -7,6 +7,7 @@ pub mod camel; pub mod cat; pub mod chicken; pub mod cod; +pub mod copper_golem; pub mod cow; pub mod dolphin; pub mod donkey; @@ -14,11 +15,13 @@ pub mod fox; pub mod frog; pub mod glow_squid; pub mod goat; +pub mod happy_ghast; pub mod horse; pub mod iron_golem; pub mod llama; pub mod mooshroom; pub mod mule; +pub mod nautilus; pub mod ocelot; pub mod panda; pub mod parrot; diff --git a/crates/pumpkin/src/entity/passive/nautilus.rs b/crates/pumpkin/src/entity/passive/nautilus.rs new file mode 100644 index 000000000..c65d8a8a7 --- /dev/null +++ b/crates/pumpkin/src/entity/passive/nautilus.rs @@ -0,0 +1,416 @@ +use crossbeam::atomic::AtomicCell; +use std::sync::{ + Arc, + atomic::{AtomicBool, AtomicI32, Ordering}, +}; +use tokio::sync::Mutex; +use uuid::Uuid; + +use pumpkin_data::{ + effect::StatusEffect, + entity::EntityStatus, + item_stack::ItemStack, + meta_data_type::MetaDataType, + particle::Particle, + potion::Effect, + sound::{Sound, SoundCategory}, + tracked_data::TrackedData, +}; +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_protocol::java::client::play::Metadata; +use pumpkin_util::math::vector3::Vector3; + +use crate::entity::{ + Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture, + mob::{Mob, MobEntity}, + passive::animal::Animal, + player::Player, +}; + +pub struct NautilusEntity { + pub mob_entity: MobEntity, + pub is_tame: AtomicBool, + pub owner: AtomicCell>, + pub is_dashing: AtomicBool, + pub dash_cooldown: AtomicI32, + pub is_saddled: AtomicBool, + pub inventory: Mutex>, +} + +impl NautilusEntity { + pub fn new(entity: Entity) -> Arc { + let mob_entity = MobEntity::new(entity); + let nautilus = Self { + mob_entity, + is_tame: AtomicBool::new(false), + owner: AtomicCell::new(None), + is_dashing: AtomicBool::new(false), + dash_cooldown: AtomicI32::new(0), + is_saddled: AtomicBool::new(false), + inventory: Mutex::new(vec![ItemStack::new(0, &pumpkin_data::item::Item::AIR); 9]), + }; + + Arc::new(nautilus) + } + + pub fn is_dashing(&self) -> bool { + self.is_dashing.load(Ordering::Relaxed) + } + + pub fn set_dashing(&self, dashing: bool) { + self.is_dashing.store(dashing, Ordering::Relaxed); + self.mob_entity.living_entity.entity.send_meta_data( + &[Metadata::new( + TrackedData::DASH, + MetaDataType::BOOLEAN, + dashing, + )], + None, + ); + } + + pub fn is_tame(&self) -> bool { + self.is_tame.load(Ordering::Relaxed) + } + + pub fn set_tame(&self, tame: bool, owner: Option) { + self.is_tame.store(tame, Ordering::Relaxed); + self.owner.store(owner); + } + + pub fn get_ambient_sound(&self) -> Sound { + let is_baby = self + .mob_entity + .living_entity + .entity + .age + .load(Ordering::Relaxed) + < 0; + let is_water = self + .mob_entity + .living_entity + .entity + .touching_water + .load(Ordering::Relaxed); + if is_baby { + if is_water { + Sound::EntityBabyNautilusAmbient + } else { + Sound::EntityBabyNautilusAmbientLand + } + } else if is_water { + Sound::EntityNautilusAmbient + } else { + Sound::EntityNautilusAmbientLand + } + } + + pub fn get_hurt_sound(&self) -> Sound { + let is_baby = self + .mob_entity + .living_entity + .entity + .age + .load(Ordering::Relaxed) + < 0; + let is_water = self + .mob_entity + .living_entity + .entity + .touching_water + .load(Ordering::Relaxed); + if is_baby { + if is_water { + Sound::EntityBabyNautilusHurt + } else { + Sound::EntityBabyNautilusHurtLand + } + } else if is_water { + Sound::EntityNautilusHurt + } else { + Sound::EntityNautilusHurtLand + } + } + + pub fn get_death_sound(&self) -> Sound { + let is_baby = self + .mob_entity + .living_entity + .entity + .age + .load(Ordering::Relaxed) + < 0; + let is_water = self + .mob_entity + .living_entity + .entity + .touching_water + .load(Ordering::Relaxed); + if is_baby { + if is_water { + Sound::EntityBabyNautilusDeath + } else { + Sound::EntityBabyNautilusDeathLand + } + } else if is_water { + Sound::EntityNautilusDeath + } else { + Sound::EntityNautilusDeathLand + } + } + + pub fn get_dash_sound(&self) -> Sound { + let is_water = self + .mob_entity + .living_entity + .entity + .touching_water + .load(Ordering::Relaxed); + if is_water { + Sound::EntityNautilusDash + } else { + Sound::EntityNautilusDashLand + } + } + + pub fn get_dash_ready_sound(&self) -> Sound { + let is_water = self + .mob_entity + .living_entity + .entity + .touching_water + .load(Ordering::Relaxed); + if is_water { + Sound::EntityNautilusDashReady + } else { + Sound::EntityNautilusDashReadyLand + } + } + + pub fn get_eat_sound(&self) -> Sound { + let is_baby = self + .mob_entity + .living_entity + .entity + .age + .load(Ordering::Relaxed) + < 0; + if is_baby { + Sound::EntityBabyNautilusEat + } else { + Sound::EntityNautilusEat + } + } + + pub fn get_swim_sound(&self) -> Sound { + let is_baby = self + .mob_entity + .living_entity + .entity + .age + .load(Ordering::Relaxed) + < 0; + if is_baby { + Sound::EntityBabyNautilusSwim + } else { + Sound::EntityNautilusSwim + } + } +} + +impl NBTStorage for NautilusEntity { + 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; + self.write_animal_nbt(nbt); + nbt.put_bool("IsTame", self.is_tame.load(Ordering::Relaxed)); + nbt.put_bool("Saddled", self.is_saddled.load(Ordering::Relaxed)); + nbt.put_int("DashCooldown", self.dash_cooldown.load(Ordering::Relaxed)); + if let Some(owner) = self.owner.load() { + nbt.put_uuid("Owner", owner); + } + }) + } + + 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; + self.read_animal_nbt(nbt); + if let Some(is_tame) = nbt.get_bool("IsTame") { + self.is_tame.store(is_tame, Ordering::Relaxed); + } + if let Some(saddled) = nbt.get_bool("Saddled") { + self.is_saddled.store(saddled, Ordering::Relaxed); + } + if let Some(dash) = nbt.get_int("DashCooldown") { + self.dash_cooldown.store(dash, Ordering::Relaxed); + } + if let Some(owner) = nbt.get_uuid("Owner") { + self.owner.store(Some(owner)); + } + }) + } +} + +impl Animal for NautilusEntity { + fn is_food(&self, item_stack: &ItemStack) -> bool { + item_stack.item == &pumpkin_data::item::Item::NAUTILUS_SHELL + || item_stack.item == &pumpkin_data::item::Item::PUFFERFISH + || item_stack.item == &pumpkin_data::item::Item::COD + || item_stack.item == &pumpkin_data::item::Item::SALMON + || item_stack.item == &pumpkin_data::item::Item::TROPICAL_FISH + } +} + +impl Mob for NautilusEntity { + fn get_mob_entity(&self) -> &MobEntity { + &self.mob_entity + } + + fn mob_init_data_tracker(&self) -> EntityBaseFuture<'_, ()> { + Box::pin(async move { + self.mob_entity.living_entity.entity.send_meta_data( + &[Metadata::new( + TrackedData::DASH, + MetaDataType::BOOLEAN, + self.is_dashing(), + )], + None, + ); + }) + } + + fn mob_tick<'a>(&'a self, _caller: &'a Arc) -> EntityBaseFuture<'a, ()> { + Box::pin(async move { + let entity = &self.mob_entity.living_entity.entity; + + let passengers = entity.passengers.lock().await; + if let Some(passenger) = passengers.first() + && let Some(player) = passenger.cast_any().downcast_ref::() + { + let world = entity.world.load(); + let game_time = world.level_time.lock().await.world_age; + if game_time % 40 == 0 { + player + .living_entity + .add_effect(Effect { + effect_type: &StatusEffect::BREATH_OF_THE_NAUTILUS, + duration: 60, + amplifier: 0, + ambient: true, + show_particles: true, + show_icon: true, + blend: true, + }) + .await; + } + } + + if self.is_dashing() && self.dash_cooldown.load(Ordering::Relaxed) < 35 { + self.set_dashing(false); + } + + let cooldown = self.dash_cooldown.load(Ordering::Relaxed); + if cooldown > 0 { + let next = cooldown - 1; + self.dash_cooldown.store(next, Ordering::Relaxed); + if next == 0 { + let world = entity.world.load(); + world.play_sound( + self.get_dash_ready_sound(), + SoundCategory::Neutral, + &entity.pos.load(), + ); + } + } + + if entity.touching_water.load(Ordering::Relaxed) { + let velo = entity.velocity.load(); + let speed = velo.length(); + let prob = (speed * 2.0).clamp(0.15, 1.0); + if rand::random::() < prob { + let world = entity.world.load(); + let pos = entity.pos.load(); + world.spawn_particle( + pos + Vector3::new(0.0, 0.25, 0.0), + Vector3::new(0.4, 0.4, 0.4), + 0.5, + 2, + Particle::Bubble, + ); + } + } + }) + } + + fn mob_interact<'a>( + &'a self, + player: &'a Arc, + item_stack: &'a mut ItemStack, + ) -> EntityBaseFuture<'a, bool> { + Box::pin(async move { + let mob_entity = &self.mob_entity; + let entity = &mob_entity.living_entity.entity; + + if !self.is_tame() && self.is_food(item_stack) { + item_stack.decrement_unless_creative(player.gamemode.load(), 1); + if rand::random::().is_multiple_of(3) { + self.set_tame(true, Some(player.gameprofile.id)); + let world = entity.world.load(); + world.send_entity_status(entity, EntityStatus::TamingSucceeded, None); + } else { + let world = entity.world.load(); + world.send_entity_status(entity, EntityStatus::TamingFailed, None); + } + let world = entity.world.load(); + world.play_sound( + self.get_eat_sound(), + SoundCategory::Neutral, + &entity.pos.load(), + ); + return true; + } + + if self.is_tame() && !player.get_entity().is_sneaking() { + if !self.is_saddled.load(Ordering::Relaxed) + && item_stack.item == &pumpkin_data::item::Item::SADDLE + { + item_stack.decrement_unless_creative(player.gamemode.load(), 1); + self.is_saddled.store(true, Ordering::Relaxed); + let world = entity.world.load(); + world.play_sound( + Sound::ItemNautilusSaddleEquip, + SoundCategory::Neutral, + &entity.pos.load(), + ); + return true; + } + + let world = player.world(); + if let Some(vehicle) = world.get_entity_by_id(entity.entity_id) + && let Some(passenger) = world.get_player_by_id(player.entity_id()) + { + entity + .add_passenger(vehicle, passenger as Arc) + .await; + return true; + } + } + + self.animal_interact(player, item_stack, self.get_ambient_sound()) + .await + }) + } + + fn is_saddled(&self) -> bool { + self.is_saddled.load(Ordering::Relaxed) + } + + fn can_be_saddled(&self) -> bool { + self.mob_entity.living_entity.entity.is_alive() + } + + fn set_saddled(&self, saddled: bool) { + self.is_saddled.store(saddled, Ordering::Relaxed); + } +} diff --git a/crates/pumpkin/src/entity/passive/ocelot.rs b/crates/pumpkin/src/entity/passive/ocelot.rs index 9839829d3..b8cbc2cc8 100644 --- a/crates/pumpkin/src/entity/passive/ocelot.rs +++ b/crates/pumpkin/src/entity/passive/ocelot.rs @@ -1,27 +1,49 @@ -use std::sync::{Arc, Weak}; +use std::sync::{ + Arc, Weak, + atomic::{AtomicBool, Ordering}, +}; -use pumpkin_data::entity::EntityType; +use pumpkin_data::entity::{EntityStatus, EntityType}; +use pumpkin_data::item::Item; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::meta_data_type::MetaDataType; +use pumpkin_data::tag::{self, Taggable}; +use pumpkin_data::tracked_data::TrackedData; +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_protocol::bedrock::server::actor_event::ActorEventType; +use pumpkin_protocol::java::client::play::Metadata; +use rand::RngExt; use crate::entity::{ - Entity, NBTStorage, + Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture, ai::goal::{ + active_target::ActiveTargetGoal, avoid_entity::AvoidEntityGoal, breed::BreedGoal, + escape_danger::EscapeDangerGoal, follow_parent::FollowParentGoal, look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal, - wander_around::WanderAroundGoal, + tempt::TemptGoal, wander_around::WanderAroundGoal, }, mob::{Mob, MobEntity}, + passive::animal::Animal, + player::Player, }; +const TEMPT_ITEMS: &[&Item] = &[&Item::COD, &Item::SALMON]; + /// Represents an Ocelot, a shy passive mob found in jungles. /// /// Wiki: pub struct OcelotEntity { pub mob_entity: MobEntity, + pub is_trusting: AtomicBool, } impl OcelotEntity { pub fn new(entity: Entity) -> Arc { let mob_entity = MobEntity::new(entity); - let ocelot = Self { mob_entity }; + let ocelot = Self { + mob_entity, + is_trusting: AtomicBool::new(false), + }; let mob_arc = Arc::new(ocelot); let mob_weak: Weak = { let mob_arc: Arc = mob_arc.clone(); @@ -35,23 +57,171 @@ impl OcelotEntity { .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(WanderAroundGoal::new(1.0))); + // Goal 1: FloatGoal (SwimGoal) + goal_selector.add_goal(1, Box::new(SwimGoal::default())); + // Goal 1: PanicGoal (EscapeDangerGoal) + goal_selector.add_goal(1, EscapeDangerGoal::new(1.5)); + // Goal 3: OcelotTemptGoal + goal_selector.add_goal(3, Box::new(TemptGoal::new(0.6, TEMPT_ITEMS))); + // Goal 4: OcelotAvoidEntityGoal (when not trusting) goal_selector.add_goal( - 2, - LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 6.0), + 4, + Box::new(AvoidEntityGoal::new(&EntityType::PLAYER, 16.0, 0.8, 1.33)), + ); + // Goal 9: BreedGoal + goal_selector.add_goal(9, BreedGoal::new(0.8)); + // Goal 9: FollowParentGoal + goal_selector.add_goal(9, Box::new(FollowParentGoal::new(0.8))); + // Goal 10: WaterAvoidingRandomStrollGoal (WanderAroundGoal) + goal_selector.add_goal(10, Box::new(WanderAroundGoal::new(0.8))); + // Goal 11: LookAtPlayerGoal + goal_selector.add_goal( + 11, + LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 10.0), + ); + // Goal 11: RandomLookAroundGoal + goal_selector.add_goal(11, Box::new(RandomLookAroundGoal::default())); + }; + + { + let mut target_selector = mob_arc + .mob_entity + .target_selector + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + + // Target Goal 1: NearestAttackableTargetGoal for Chicken and Turtle + target_selector.add_goal( + 1, + ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::CHICKEN, false), + ); + target_selector.add_goal( + 1, + ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::TURTLE, false), ); - goal_selector.add_goal(3, Box::new(RandomLookAroundGoal::default())); }; mob_arc } + + pub fn is_trusting(&self) -> bool { + self.is_trusting.load(Ordering::Relaxed) + } + + pub fn set_trusting(&self, trusting: bool) { + self.is_trusting.store(trusting, Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + TrackedData::TRUSTING, + MetaDataType::BOOLEAN, + trusting, + )], + None, + ); + } } -impl NBTStorage for OcelotEntity {} +impl NBTStorage for OcelotEntity { + fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> { + Box::pin(async { + self.mob_entity.living_entity.write_nbt(nbt).await; + self.write_animal_nbt(nbt); + nbt.put_bool("Trusting", self.is_trusting.load(Ordering::Relaxed)); + }) + } + + fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> { + Box::pin(async { + self.mob_entity.living_entity.read_nbt_non_mut(nbt).await; + self.read_animal_nbt(nbt); + if let Some(trusting) = nbt.get_bool("Trusting") { + self.is_trusting.store(trusting, Ordering::Relaxed); + } + }) + } +} + +impl Animal for OcelotEntity { + fn is_food(&self, item_stack: &ItemStack) -> bool { + let item = item_stack.get_item(); + item.has_tag(&tag::Item::MINECRAFT_OCELOT_FOOD) + || item == &Item::COD + || item == &Item::SALMON + } +} impl Mob for OcelotEntity { fn get_mob_entity(&self) -> &MobEntity { &self.mob_entity } + + fn mob_init_data_tracker(&self) -> EntityBaseFuture<'_, ()> { + Box::pin(async move { + let entity = self.get_entity(); + let is_baby = entity.age.load(Ordering::Relaxed) < 0; + if is_baby { + entity.send_meta_data( + &[Metadata::new( + TrackedData::BABY_ID, + MetaDataType::BOOLEAN, + true, + )], + None, + ); + } + entity.send_meta_data( + &[Metadata::new( + TrackedData::TRUSTING, + MetaDataType::BOOLEAN, + self.is_trusting.load(Ordering::Relaxed), + )], + None, + ); + }) + } + + fn mob_interact<'a>( + &'a self, + player: &'a Arc, + item_stack: &'a mut ItemStack, + ) -> EntityBaseFuture<'a, bool> { + Box::pin(async move { + let is_food = self.is_food(item_stack); + let dist_sqr = self + .get_entity() + .pos + .load() + .squared_distance_to_vec(&player.get_entity().pos.load()); + + if !self.is_trusting() && is_food && dist_sqr < 9.0 { + item_stack.decrement_unless_creative(player.gamemode.load(), 1); + + let mut rng = rand::rng(); + if rng.random_range(0..3) == 0 { + self.set_trusting(true); + self.get_entity().world.load().send_entity_status( + self.get_entity(), + EntityStatus::TrustingSucceeded, + Some(ActorEventType::TrustingSucceeded), + ); + } else { + self.get_entity().world.load().send_entity_status( + self.get_entity(), + EntityStatus::TrustingFailed, + Some(ActorEventType::TrustingFailed), + ); + } + + return true; + } + + self.animal_interact( + player, + item_stack, + pumpkin_data::sound::Sound::EntityOcelotAmbient, + ) + .await + }) + } } diff --git a/crates/pumpkin/src/entity/passive/pig.rs b/crates/pumpkin/src/entity/passive/pig.rs index 3ea630646..16a43f03e 100644 --- a/crates/pumpkin/src/entity/passive/pig.rs +++ b/crates/pumpkin/src/entity/passive/pig.rs @@ -107,7 +107,11 @@ impl NBTStorage for PigEntity { impl super::animal::Animal for PigEntity { fn is_food(&self, item_stack: &ItemStack) -> bool { - PIG_FOOD.iter().any(|i| i.id == item_stack.item.id) + use pumpkin_data::tag::Taggable; + item_stack + .item + .has_tag(&pumpkin_data::tag::Item::MINECRAFT_PIG_FOOD) + || PIG_FOOD.iter().any(|i| i.id == item_stack.item.id) } } diff --git a/crates/pumpkin/src/entity/passive/sheep.rs b/crates/pumpkin/src/entity/passive/sheep.rs index f414a7113..e5bfcfe8d 100644 --- a/crates/pumpkin/src/entity/passive/sheep.rs +++ b/crates/pumpkin/src/entity/passive/sheep.rs @@ -11,6 +11,7 @@ use pumpkin_protocol::java::client::play::Metadata; use crate::entity::{ Entity, EntityBaseFuture, NBTStorage, NbtFuture, + ageable::AgeableMob, ai::goal::{ breed::BreedGoal, eat_grass::EatGrassGoal, escape_danger::EscapeDangerGoal, follow_parent::FollowParentGoal, look_around::RandomLookAroundGoal, @@ -18,6 +19,7 @@ use crate::entity::{ wander_around::WanderAroundGoal, }, mob::{Mob, MobEntity}, + passive::animal::Animal, player::Player, }; @@ -29,6 +31,7 @@ const TEMPT_ITEMS: &[&Item] = &[&Item::WHEAT]; pub struct SheepEntity { pub mob_entity: MobEntity, color_and_sheared: AtomicU8, + pub ageable_data: crate::entity::ageable::AgeableData, } impl SheepEntity { @@ -37,6 +40,7 @@ impl SheepEntity { let sheep = Self { mob_entity, color_and_sheared: AtomicU8::new(0), + ageable_data: crate::entity::ageable::AgeableData::default(), }; let mob_arc = Arc::new(sheep); let mob_weak: Weak = { @@ -107,10 +111,18 @@ impl SheepEntity { } } +impl crate::entity::ageable::AgeableMob for SheepEntity { + fn get_ageable_data(&self) -> &crate::entity::ageable::AgeableData { + &self.ageable_data + } +} + impl NBTStorage for SheepEntity { fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> { Box::pin(async { - self.mob_entity.living_entity.entity.write_nbt(nbt).await; + self.mob_entity.living_entity.write_nbt(nbt).await; + self.write_ageable_nbt(nbt); + self.write_animal_nbt(nbt); nbt.put_bool("Sheared", self.is_sheared()); nbt.put_byte("Color", self.get_color() as i8); }) @@ -118,12 +130,13 @@ impl NBTStorage for SheepEntity { fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> { Box::pin(async { - self.mob_entity - .living_entity - .entity - .read_nbt_non_mut(nbt) - .await; - let sheared = nbt.get_bool("Sheared").unwrap_or(false); + self.mob_entity.living_entity.read_nbt_non_mut(nbt).await; + self.read_ageable_nbt(nbt); + self.read_animal_nbt(nbt); + let sheared = nbt + .get_bool("Sheared") + .or_else(|| nbt.get_byte("Sheared").map(|b| b == 1)) + .unwrap_or(false); let color = nbt.get_byte("Color").unwrap_or(0) as u8; let byte = (color & 0x0F) | if sheared { 0x10 } else { 0 }; self.color_and_sheared.store(byte, Ordering::Relaxed); @@ -133,7 +146,11 @@ impl NBTStorage for SheepEntity { impl super::animal::Animal for SheepEntity { fn is_food(&self, item_stack: &ItemStack) -> bool { - TEMPT_ITEMS.iter().any(|i| i.id == item_stack.item.id) + use pumpkin_data::tag::Taggable; + item_stack + .item + .has_tag(&pumpkin_data::tag::Item::MINECRAFT_SHEEP_FOOD) + || TEMPT_ITEMS.iter().any(|i| i.id == item_stack.item.id) } } diff --git a/crates/pumpkin/src/entity/passive/sniffer.rs b/crates/pumpkin/src/entity/passive/sniffer.rs index cd2ead73f..e42d92f0f 100644 --- a/crates/pumpkin/src/entity/passive/sniffer.rs +++ b/crates/pumpkin/src/entity/passive/sniffer.rs @@ -1,24 +1,93 @@ -use std::sync::{Arc, Weak}; - -use pumpkin_data::entity::EntityType; - -use crate::entity::{ - Entity, NBTStorage, - ai::goal::{ - look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal, - wander_around::WanderAroundGoal, - }, - mob::{Mob, MobEntity}, +use std::sync::{ + Arc, Weak, + atomic::{AtomicI32, Ordering}, }; +use pumpkin_data::entity::{EntityStatus, EntityType}; +use pumpkin_data::item::Item; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::meta_data_type::MetaDataType; +use pumpkin_data::sound::{Sound, SoundCategory}; +use pumpkin_data::tag::{self, Taggable}; +use pumpkin_data::tracked_data::TrackedData; +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_protocol::codec::var_int::VarInt; +use pumpkin_protocol::java::client::play::Metadata; +use pumpkin_util::math::position::BlockPos; +use pumpkin_util::math::vector3::Vector3; + +use crate::entity::{ + Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture, + ageable::{AgeableData, AgeableMob}, + ai::goal::{ + breed::BreedGoal, follow_parent::FollowParentGoal, look_around::RandomLookAroundGoal, + look_at_entity::LookAtEntityGoal, swim::SwimGoal, tempt::TemptGoal, + wander_around::WanderAroundGoal, + }, + item::ItemEntity, + mob::{Mob, MobEntity}, + passive::animal::Animal, + player::Player, +}; + +pub const SNIFFER_FOOD: &[&Item] = &[&Item::TORCHFLOWER_SEEDS, &Item::PITCHER_POD]; +pub const SNIFFER_BABY_START_AGE: i32 = -48000; +pub const DIGGING_DROP_SEED_OFFSET_TICKS: i32 = 120; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[repr(i32)] +pub enum SnifferState { + #[default] + Idling = 0, + FeelingHappy = 1, + Scenting = 2, + Sniffing = 3, + Searching = 4, + Digging = 5, + Rising = 6, +} + +impl SnifferState { + #[must_use] + pub const fn from_id(id: i32) -> Self { + match id { + 1 => Self::FeelingHappy, + 2 => Self::Scenting, + 3 => Self::Sniffing, + 4 => Self::Searching, + 5 => Self::Digging, + 6 => Self::Rising, + _ => Self::Idling, + } + } + + #[must_use] + pub const fn id(self) -> i32 { + self as i32 + } +} + +/// Represents a Sniffer, a large passive mob that digs up ancient seeds. +/// +/// Wiki: pub struct SnifferEntity { pub mob_entity: MobEntity, + pub ageable_data: AgeableData, + pub state: AtomicI32, + pub drop_seed_at_tick: AtomicI32, + pub explored_positions: std::sync::Mutex>, } impl SnifferEntity { pub fn new(entity: Entity) -> Arc { let mob_entity = MobEntity::new(entity); - let sniffer = Self { mob_entity }; + let sniffer = Self { + mob_entity, + ageable_data: AgeableData::default(), + state: AtomicI32::new(SnifferState::Idling.id()), + drop_seed_at_tick: AtomicI32::new(0), + explored_positions: std::sync::Mutex::new(Vec::new()), + }; let mob_arc = Arc::new(sniffer); let mob_weak: Weak = { let mob_arc: Arc = mob_arc.clone(); @@ -33,22 +102,334 @@ impl SnifferEntity { .unwrap_or_else(std::sync::PoisonError::into_inner); goal_selector.add_goal(0, Box::new(SwimGoal::default())); - goal_selector.add_goal(1, Box::new(WanderAroundGoal::new(1.0))); + goal_selector.add_goal(1, BreedGoal::new(1.0)); + goal_selector.add_goal(2, Box::new(TemptGoal::new(1.2, SNIFFER_FOOD))); + goal_selector.add_goal(3, Box::new(FollowParentGoal::new(1.1))); + goal_selector.add_goal(4, Box::new(WanderAroundGoal::new(1.0))); goal_selector.add_goal( - 2, + 5, LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 6.0), ); - goal_selector.add_goal(3, Box::new(RandomLookAroundGoal::default())); + goal_selector.add_goal(6, Box::new(RandomLookAroundGoal::default())); }; mob_arc } + + #[must_use] + pub fn get_state(&self) -> SnifferState { + SnifferState::from_id(self.state.load(Ordering::Relaxed)) + } + + pub fn set_state(&self, state: SnifferState) { + self.state.store(state.id(), Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + TrackedData::STATE, + MetaDataType::SNIFFER_STATE, + VarInt(state.id()), + )], + None, + ); + } + + pub fn transition_to(&self, state: SnifferState) { + let entity = self.get_entity(); + let world = entity.world.load(); + let pos = entity.pos.load(); + + match state { + SnifferState::Idling => { + self.set_state(SnifferState::Idling); + } + SnifferState::FeelingHappy => { + world.play_sound(Sound::EntitySnifferHappy, SoundCategory::Neutral, &pos); + self.set_state(SnifferState::FeelingHappy); + } + SnifferState::Scenting => { + self.set_state(SnifferState::Scenting); + self.on_scenting_start(); + } + SnifferState::Sniffing => { + world.play_sound(Sound::EntitySnifferSniffing, SoundCategory::Neutral, &pos); + self.set_state(SnifferState::Sniffing); + } + SnifferState::Searching => { + self.set_state(SnifferState::Searching); + } + SnifferState::Digging => { + self.set_state(SnifferState::Digging); + self.on_digging_start(); + } + SnifferState::Rising => { + world.play_sound( + Sound::EntitySnifferDiggingStop, + SoundCategory::Neutral, + &pos, + ); + self.set_state(SnifferState::Rising); + } + } + } + + fn on_scenting_start(&self) { + let entity = self.get_entity(); + let world = entity.world.load(); + let pos = entity.pos.load(); + let pitch = if self.is_baby() { 1.3 } else { 1.0 }; + world.play_sound_fine( + Sound::EntitySnifferScenting, + SoundCategory::Neutral, + &pos, + 1.0, + pitch, + ); + } + + fn on_digging_start(&self) { + let entity = self.get_entity(); + let world = entity.world.load(); + let current_ticks = world.level_time.try_lock().map_or(0, |t| t.world_age); + let drop_tick = current_ticks as i32 + DIGGING_DROP_SEED_OFFSET_TICKS; + self.drop_seed_at_tick.store(drop_tick, Ordering::Relaxed); + entity.send_meta_data( + &[Metadata::new( + TrackedData::DROP_SEED_AT_TICK, + MetaDataType::INT, + VarInt(drop_tick), + )], + None, + ); + world.send_entity_status(entity, EntityStatus::SnifferDiggingSound, None); + } + + pub fn on_digging_complete(&self, success: bool) { + if success { + let head_block = self.get_head_block().down(); + self.store_explored_position(head_block); + } + } + + #[must_use] + pub fn get_head_position(&self) -> Vector3 { + let entity = self.get_entity(); + let pos = entity.pos.load(); + let yaw = f64::from(entity.yaw.load()); + let yaw_rad = yaw.to_radians(); + let forward = Vector3::new(-yaw_rad.sin(), 0.0, yaw_rad.cos()); + pos + forward * 2.25 + } + + #[must_use] + pub fn get_head_block(&self) -> BlockPos { + let head_pos = self.get_head_position(); + let entity = self.get_entity(); + let pos = entity.pos.load(); + BlockPos::floored(head_pos.x, pos.y + 0.2, head_pos.z) + } + + #[must_use] + pub fn can_dig(&self) -> bool { + let entity = self.get_entity(); + !self.is_panicking() + && !self.is_baby() + && !self.mob_entity.living_entity.is_in_water() + && entity.on_ground.load(Ordering::Relaxed) + && self.can_dig_at(self.get_head_block().down()) + } + + fn can_dig_at(&self, pos: BlockPos) -> bool { + let entity = self.get_entity(); + let world = entity.world.load(); + let block_state = world.get_block_state(&pos); + let block = pumpkin_data::Block::from_state_id(block_state.id); + + if !block.has_tag(&tag::Block::MINECRAFT_SNIFFER_DIGGABLE_BLOCK) { + return false; + } + + let explored = self + .explored_positions + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + !explored.contains(&pos) + } + + fn store_explored_position(&self, pos: BlockPos) { + let mut explored = self + .explored_positions + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + if explored.len() >= 20 { + explored.pop(); + } + explored.insert(0, pos); + } + + pub async fn drop_seed(&self) { + let entity = self.get_entity(); + let world = entity.world.load(); + let current_tick = world.level_time.lock().await.world_age as i32; + + if self.drop_seed_at_tick.load(Ordering::Relaxed) == current_tick { + let head_pos = self.get_head_position(); + let seed_item = if rand::random::() { + &Item::TORCHFLOWER_SEEDS + } else { + &Item::PITCHER_POD + }; + let item_stack = ItemStack::new(1, seed_item); + + let item_entity = Entity::new(world.clone(), head_pos, &EntityType::ITEM); + let item_arc = Arc::new(ItemEntity::new(item_entity, item_stack)); + world.spawn_entity(item_arc).await; + + world.play_sound( + Sound::EntitySnifferDropSeed, + SoundCategory::Neutral, + &head_pos, + ); + } + } + + pub async fn spawn_child_from_breeding(&self, partner: &dyn EntityBase) { + let entity = self.get_entity(); + let world = entity.world.load(); + let pos = entity.pos.load(); + + let item_stack = ItemStack::new(1, &Item::SNIFFER_EGG); + let egg_entity = Entity::new(world.clone(), pos, &EntityType::ITEM); + let egg_arc = Arc::new(ItemEntity::new(egg_entity, item_stack)); + world.spawn_entity(egg_arc).await; + + world.play_sound(Sound::BlockSnifferEggPlop, SoundCategory::Neutral, &pos); + + self.mob_entity.reset_love_ticks(); + self.mob_entity + .breeding_cooldown + .store(6000, Ordering::Relaxed); + partner.reset_love(); + partner.set_breeding_cooldown(6000); + } } -impl NBTStorage for SnifferEntity {} +impl AgeableMob for SnifferEntity { + fn get_ageable_data(&self) -> &AgeableData { + &self.ageable_data + } + + fn get_baby_start_age(&self) -> i32 { + SNIFFER_BABY_START_AGE + } +} + +impl NBTStorage for SnifferEntity { + 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; + self.write_ageable_nbt(nbt); + self.write_animal_nbt(nbt); + nbt.put_int("State", self.get_state().id()); + nbt.put_int( + "DropSeedAtTick", + self.drop_seed_at_tick.load(Ordering::Relaxed), + ); + }) + } + + 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; + self.read_ageable_nbt(nbt); + self.read_animal_nbt(nbt); + if let Some(state_id) = nbt.get_int("State") { + self.state.store(state_id, Ordering::Relaxed); + } + if let Some(drop_tick) = nbt.get_int("DropSeedAtTick") { + self.drop_seed_at_tick.store(drop_tick, Ordering::Relaxed); + } + }) + } +} + +impl Animal for SnifferEntity { + fn is_food(&self, item_stack: &ItemStack) -> bool { + item_stack.item.has_tag(&tag::Item::MINECRAFT_SNIFFER_FOOD) + || SNIFFER_FOOD.iter().any(|i| i.id == item_stack.item.id) + } +} impl Mob for SnifferEntity { fn get_mob_entity(&self) -> &MobEntity { &self.mob_entity } + + fn mob_tick<'a>(&'a self, _caller: &'a Arc) -> EntityBaseFuture<'a, ()> { + Box::pin(async move { + self.ageable_ai_step(); + let state = self.get_state(); + match state { + SnifferState::Searching => { + let entity = self.get_entity(); + let world = entity.world.load(); + let ticks = world.level_time.lock().await.world_age; + if ticks % 20 == 0 { + world.play_sound( + Sound::EntitySnifferSearching, + SoundCategory::Neutral, + &entity.pos.load(), + ); + } + } + SnifferState::Digging => { + self.drop_seed().await; + } + _ => {} + } + }) + } + + fn mob_init_data_tracker(&self) -> EntityBaseFuture<'_, ()> { + Box::pin(async move { + let entity = self.get_entity(); + let is_baby = entity.age.load(Ordering::Relaxed) < 0; + if is_baby { + entity.send_meta_data( + &[Metadata::new( + TrackedData::BABY_ID, + MetaDataType::BOOLEAN, + true, + )], + None, + ); + } + entity.send_meta_data( + &[ + Metadata::new( + TrackedData::STATE, + MetaDataType::SNIFFER_STATE, + VarInt(self.get_state().id()), + ), + Metadata::new( + TrackedData::DROP_SEED_AT_TICK, + MetaDataType::INT, + VarInt(self.drop_seed_at_tick.load(Ordering::Relaxed)), + ), + ], + None, + ); + }) + } + + fn mob_interact<'a>( + &'a self, + player: &'a Arc, + item_stack: &'a mut ItemStack, + ) -> EntityBaseFuture<'a, bool> { + Box::pin(async move { + self.animal_interact(player, item_stack, Sound::EntitySnifferEat) + .await + }) + } } diff --git a/crates/pumpkin/src/entity/passive/wolf.rs b/crates/pumpkin/src/entity/passive/wolf.rs index 5c481007f..6d78c1131 100644 --- a/crates/pumpkin/src/entity/passive/wolf.rs +++ b/crates/pumpkin/src/entity/passive/wolf.rs @@ -1,29 +1,43 @@ use std::sync::{ Arc, Weak, - atomic::{AtomicU8, Ordering}, + atomic::{AtomicBool, AtomicU8, Ordering}, }; +use crossbeam::atomic::AtomicCell; use pumpkin_data::entity::EntityType; use pumpkin_data::item::Item; +use pumpkin_data::item_stack::ItemStack; use pumpkin_data::meta_data_type::MetaDataType; +use pumpkin_data::tag::{self, Taggable}; use pumpkin_data::tracked_data::TrackedData; use pumpkin_nbt::compound::NbtCompound; use pumpkin_protocol::codec::var_int::VarInt; use pumpkin_protocol::java::client::play::Metadata; +use uuid::Uuid; use crate::entity::{ Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture, + ageable::AgeableMob, ai::goal::{ - beg::BegGoal, breed::BreedGoal, escape_danger::EscapeDangerGoal, + active_target::ActiveTargetGoal, avoid_entity::AvoidEntityGoal, beg::BegGoal, + breed::BreedGoal, escape_danger::EscapeDangerGoal, follow_owner::FollowOwnerGoal, follow_parent::FollowParentGoal, look_around::RandomLookAroundGoal, - look_at_entity::LookAtEntityGoal, swim::SwimGoal, wander_around::WanderAroundGoal, + look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, + owner_hurt_by_target::OwnerHurtByTargetGoal, owner_hurt_target::OwnerHurtTargetGoal, + revenge::RevengeGoal, swim::SwimGoal, wander_around::WanderAroundGoal, }, mob::{Mob, MobEntity}, + passive::animal::Animal, }; pub struct WolfEntity { pub mob_entity: MobEntity, pub variant: AtomicU8, + pub collar_color: AtomicU8, + pub is_tame: AtomicBool, + pub is_sitting: AtomicBool, + pub owner: AtomicCell>, + pub ageable_data: crate::entity::ageable::AgeableData, } impl WolfEntity { @@ -31,7 +45,12 @@ impl WolfEntity { let mob_entity = MobEntity::new(entity); let wolf = Self { mob_entity, - variant: AtomicU8::new(3), // Default to pale + variant: AtomicU8::new(3), // Default to pale + collar_color: AtomicU8::new(14), // Default to red + is_tame: AtomicBool::new(false), + is_sitting: AtomicBool::new(false), + owner: AtomicCell::new(None), + ageable_data: crate::entity::ageable::AgeableData::default(), }; let mob_arc = Arc::new(wolf); let mob_weak: Weak = { @@ -46,29 +65,108 @@ impl WolfEntity { .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); + // Goal selector (matching Vanilla registerGoals): + // 1: SwimGoal (FloatGoal) goal_selector.add_goal(1, Box::new(SwimGoal::default())); - // goal_selector.add_goal(2, SitGoal::new(mob_arc.clone())); - goal_selector.add_goal(4, EscapeDangerGoal::new(1.5)); - goal_selector.add_goal(5, BreedGoal::new(1.0)); - // goal_selector.add_goal(6, FollowOwnerGoal::new(1.0, 10.0, 2.0, false)); + // 1: EscapeDangerGoal (TamableAnimalPanicGoal) + goal_selector.add_goal(1, EscapeDangerGoal::new(1.5)); + // 3: Avoid Llama + goal_selector.add_goal( + 3, + Box::new(AvoidEntityGoal::new(&EntityType::LLAMA, 24.0, 1.5, 1.5)), + ); + // 5: MeleeAttackGoal + goal_selector.add_goal(5, Box::new(MeleeAttackGoal::new(1.0, true))); + // 6: FollowOwnerGoal + goal_selector.add_goal(6, FollowOwnerGoal::new(1.0, 10.0, 2.0)); + // 7: BreedGoal + goal_selector.add_goal(7, BreedGoal::new(1.0)); + // 8: FollowParentGoal & WanderAroundGoal goal_selector.add_goal(8, Box::new(FollowParentGoal::new(1.1))); - goal_selector.add_goal(9, BegGoal::new(8.0, &[&Item::BONE])); + goal_selector.add_goal(8, Box::new(WanderAroundGoal::new(1.0))); + // 9: BegGoal + goal_selector.add_goal(9, BegGoal::new(8.0)); + // 10: LookAtPlayer & RandomLookAround goal_selector.add_goal( 10, LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 8.0), ); goal_selector.add_goal(10, Box::new(RandomLookAroundGoal::default())); - goal_selector.add_goal(12, Box::new(WanderAroundGoal::new(1.0))); + }; + + { + let mut target_selector = mob_arc + .mob_entity + .target_selector + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + + // Target selector (matching Vanilla registerGoals): + // 1: OwnerHurtByTargetGoal + target_selector.add_goal(1, OwnerHurtByTargetGoal::new()); + // 2: OwnerHurtTargetGoal + target_selector.add_goal(2, OwnerHurtTargetGoal::new()); + // 3: HurtByTargetGoal (RevengeGoal) + target_selector.add_goal(3, Box::new(RevengeGoal::new(true))); + // 4: NearestAttackableTarget (Player) + target_selector.add_goal( + 4, + ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::PLAYER, true), + ); + // 5: NonTameRandomTarget (Sheep, Rabbit, Fox) + target_selector.add_goal( + 5, + ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::SHEEP, false), + ); + target_selector.add_goal( + 5, + ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::RABBIT, false), + ); + target_selector.add_goal( + 5, + ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::FOX, false), + ); + // 7: NearestAttackableTarget (Skeleton) + target_selector.add_goal( + 7, + ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::SKELETON, false), + ); }; mob_arc } + + pub fn get_tame_flags(&self) -> u8 { + let mut flags = 0u8; + if self.is_sitting.load(Ordering::Relaxed) { + flags |= 0x01; + } + if self.is_tame.load(Ordering::Relaxed) { + flags |= 0x04; + } + flags + } +} + +impl AgeableMob for WolfEntity { + fn get_ageable_data(&self) -> &crate::entity::ageable::AgeableData { + &self.ageable_data + } +} + +impl Animal for WolfEntity { + fn is_food(&self, item_stack: &ItemStack) -> bool { + let item = item_stack.get_item(); + item.has_tag(&tag::Item::MINECRAFT_WOLF_FOOD) || item == &Item::BONE + } } impl NBTStorage for WolfEntity { fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> { Box::pin(async { self.mob_entity.living_entity.write_nbt(nbt).await; + self.write_ageable_nbt(nbt); + self.write_animal_nbt(nbt); let variant_str = match self.variant.load(Ordering::Relaxed) { 0 => "minecraft:ashen", 1 => "minecraft:black", @@ -81,12 +179,23 @@ impl NBTStorage for WolfEntity { _ => "minecraft:pale", }; nbt.put_string("variant", variant_str.to_string()); + nbt.put_byte( + "CollarColor", + self.collar_color.load(Ordering::Relaxed) as i8, + ); + nbt.put_bool("IsTame", self.is_tame.load(Ordering::Relaxed)); + nbt.put_bool("Sitting", self.is_sitting.load(Ordering::Relaxed)); + if let Some(owner) = self.owner.load() { + nbt.put_uuid("Owner", owner); + } }) } fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> { Box::pin(async { self.mob_entity.living_entity.read_nbt_non_mut(nbt).await; + self.read_ageable_nbt(nbt); + self.read_animal_nbt(nbt); if let Some(variant_str) = nbt.get_string("variant") { let variant = match variant_str .strip_prefix("minecraft:") @@ -104,6 +213,20 @@ impl NBTStorage for WolfEntity { }; self.variant.store(variant, Ordering::Relaxed); } + if let Some(collar) = nbt.get_byte("CollarColor") { + self.collar_color.store(collar as u8, Ordering::Relaxed); + } else if let Some(collar_int) = nbt.get_int("CollarColor") { + self.collar_color.store(collar_int as u8, Ordering::Relaxed); + } + if let Some(sitting) = nbt.get_bool("Sitting") { + self.is_sitting.store(sitting, Ordering::Relaxed); + } + if let Some(owner) = nbt.get_uuid("Owner") { + self.owner.store(Some(owner)); + self.is_tame.store(true, Ordering::Relaxed); + } else if let Some(is_tame) = nbt.get_bool("IsTame") { + self.is_tame.store(is_tame, Ordering::Relaxed); + } }) } } @@ -113,6 +236,14 @@ impl Mob for WolfEntity { &self.mob_entity } + fn get_owner_uuid(&self) -> Option { + self.owner.load() + } + + fn is_sitting(&self) -> bool { + self.is_sitting.load(Ordering::Relaxed) + } + fn mob_set_variant_name(&self, name: &str) { let variant = match name.strip_prefix("minecraft:").unwrap_or(name) { "ashen" => 0, @@ -142,6 +273,22 @@ impl Mob for WolfEntity { None, ); } + entity.send_meta_data( + &[Metadata::new( + TrackedData::TAMEABLE_FLAGS, + MetaDataType::BYTE, + self.get_tame_flags(), + )], + None, + ); + entity.send_meta_data( + &[Metadata::new( + TrackedData::COLLAR_COLOR, + MetaDataType::INT, + VarInt(self.collar_color.load(Ordering::Relaxed) as i32), + )], + None, + ); entity.send_meta_data( &[Metadata::new( TrackedData::WOLF_VARIANT_ID, @@ -150,6 +297,14 @@ impl Mob for WolfEntity { )], None, ); + entity.send_meta_data( + &[Metadata::new( + TrackedData::OWNER_UUID, + MetaDataType::OPTIONAL_UUID, + self.owner.load(), + )], + None, + ); }) } } diff --git a/crates/pumpkin/src/entity/projectile/evoker_fangs.rs b/crates/pumpkin/src/entity/projectile/evoker_fangs.rs new file mode 100644 index 000000000..90e206044 --- /dev/null +++ b/crates/pumpkin/src/entity/projectile/evoker_fangs.rs @@ -0,0 +1,115 @@ +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; + +use pumpkin_data::damage::DamageType; +use pumpkin_data::sound::Sound; + +use crate::{ + entity::{Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture}, + server::Server, +}; + +pub struct EvokerFangsEntity { + pub entity: Entity, + pub warmup_ticks: AtomicU32, + pub life_ticks: AtomicU32, + pub owner_id: Option, + pub has_bitten: AtomicBool, +} + +impl EvokerFangsEntity { + #[must_use] + pub fn new(entity: Entity, warmup_ticks: u32, yaw: f32, owner_id: Option) -> Self { + entity.set_rotation(yaw, 0.0); + Self { + entity, + warmup_ticks: AtomicU32::new(warmup_ticks), + life_ticks: AtomicU32::new(0), + owner_id, + has_bitten: AtomicBool::new(false), + } + } +} + +impl NBTStorage for EvokerFangsEntity { + fn write_nbt<'a>( + &'a self, + nbt: &'a mut pumpkin_nbt::compound::NbtCompound, + ) -> NbtFuture<'a, ()> { + Box::pin(async move { + nbt.put_int("Warmup", self.warmup_ticks.load(Ordering::Relaxed) as i32); + }) + } + + fn read_nbt_non_mut<'a>( + &'a self, + nbt: &'a pumpkin_nbt::compound::NbtCompound, + ) -> NbtFuture<'a, ()> { + Box::pin(async move { + if let Some(warmup) = nbt.get_int("Warmup") { + self.warmup_ticks.store(warmup as u32, Ordering::Relaxed); + } + }) + } +} + +impl EntityBase for EvokerFangsEntity { + fn tick<'a>( + &'a self, + _caller: &'a Arc, + _server: &'a Server, + ) -> EntityBaseFuture<'a, ()> { + Box::pin(async move { + let entity = &self.entity; + let world = entity.world.load(); + + let warmup = self.warmup_ticks.load(Ordering::Relaxed); + let life = self.life_ticks.fetch_add(1, Ordering::Relaxed) + 1; + + if life >= warmup { + if !self.has_bitten.swap(true, Ordering::SeqCst) { + entity.play_sound(Sound::EntityEvokerFangsAttack); + + let bb = entity.bounding_box.load().expand(0.2, 0.2, 0.2); + let candidates = world.get_entities_at_box(&bb); + + for cand in candidates { + let cand_ent = cand.get_entity(); + if Some(cand_ent.entity_id) == self.owner_id { + continue; + } + + if cand_ent.entity_id != entity.entity_id { + let cand_clone = cand.clone(); + tokio::spawn(async move { + let _ = cand_clone + .damage(cand_clone.as_ref(), 6.0, DamageType::MAGIC) + .await; + }); + } + } + } + + if life > warmup + 20 { + entity.remove().await; + } + } + }) + } + + fn get_entity(&self) -> &Entity { + &self.entity + } + + fn get_living_entity(&self) -> Option<&crate::entity::living::LivingEntity> { + None + } + + fn as_nbt_storage(&self) -> &dyn NBTStorage { + self + } + + fn cast_any(&self) -> &dyn std::any::Any { + self + } +} diff --git a/crates/pumpkin/src/entity/projectile/fireball.rs b/crates/pumpkin/src/entity/projectile/fireball.rs index 678915050..d3f0ba91d 100644 --- a/crates/pumpkin/src/entity/projectile/fireball.rs +++ b/crates/pumpkin/src/entity/projectile/fireball.rs @@ -1,58 +1,243 @@ use std::sync::Arc; -use std::sync::atomic::AtomicBool; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use tokio::sync::RwLock; + +use pumpkin_data::item::Item; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::meta_data_type::MetaDataType; +use pumpkin_data::tracked_data::TrackedData; +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer; +use pumpkin_protocol::java::client::play::Metadata; +use pumpkin_util::math::atomic_f32::AtomicF32; +use pumpkin_util::math::vector3::Vector3; use crate::{ entity::{ - Entity, EntityBase, EntityBaseFuture, NBTStorage, + Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture, projectile::{ProjectileHit, ThrownItemEntity}, + projectile_deflection::ProjectileDeflectionType, }, server::Server, }; -const EXPLOSION_POWER: f32 = 1.0; -const GRAVITY: f64 = 0.0; +pub const MIN_CAMERA_DISTANCE_SQUARED: f32 = 12.25; +pub const INITIAL_ACCELERATION_POWER: f64 = 0.1; +pub const DEFLECTION_SCALE: f64 = 0.5; +pub const DEFAULT_EXPLOSION_POWER: f32 = 1.0; +pub const AIR_INERTIA: f64 = 0.95; +pub const WATER_INERTIA: f64 = 0.8; pub struct FireballEntity { pub thrown: ThrownItemEntity, - pub explosion_power: f32, + pub item_stack: RwLock, + pub acceleration_power: AtomicU64, + pub explosion_power: AtomicF32, } impl FireballEntity { #[must_use] - pub const fn new(entity: Entity) -> Self { + pub fn new(entity: Entity) -> Self { let thrown = ThrownItemEntity { entity, owner_id: None, collides_with_projectiles: false, has_hit: AtomicBool::new(false), - gravity: GRAVITY, + gravity: 0.0, }; Self { thrown, - explosion_power: EXPLOSION_POWER, + item_stack: RwLock::new(Self::get_default_item()), + acceleration_power: AtomicU64::new(INITIAL_ACCELERATION_POWER.to_bits()), + explosion_power: AtomicF32::new(DEFAULT_EXPLOSION_POWER), } } #[must_use] - pub fn new_shot(entity: Entity, shooter: &Entity) -> Self { - let thrown = ThrownItemEntity::new(entity, shooter, GRAVITY); + pub fn new_shot(entity: Entity, shooter: &Entity, direction: Vector3) -> Self { + let thrown = ThrownItemEntity::new(entity, shooter, 0.0); + let accel = INITIAL_ACCELERATION_POWER; + let vel = direction.normalize().multiply(accel, accel, accel); + thrown.entity.velocity.store(vel); + Self { thrown, - explosion_power: EXPLOSION_POWER, + item_stack: RwLock::new(Self::get_default_item()), + acceleration_power: AtomicU64::new(accel.to_bits()), + explosion_power: AtomicF32::new(DEFAULT_EXPLOSION_POWER), + } + } + + #[must_use] + pub fn new_directional( + entity: Entity, + direction: Vector3, + acceleration_power: f64, + ) -> Self { + let thrown = ThrownItemEntity { + entity, + owner_id: None, + collides_with_projectiles: false, + has_hit: AtomicBool::new(false), + gravity: 0.0, + }; + let vel = direction.normalize().multiply( + acceleration_power, + acceleration_power, + acceleration_power, + ); + thrown.entity.velocity.store(vel); + + Self { + thrown, + item_stack: RwLock::new(Self::get_default_item()), + acceleration_power: AtomicU64::new(acceleration_power.to_bits()), + explosion_power: AtomicF32::new(DEFAULT_EXPLOSION_POWER), + } + } + + #[must_use] + pub fn get_default_item() -> ItemStack { + ItemStack::new(1, &Item::FIRE_CHARGE) + } + + pub async fn get_item(&self) -> ItemStack { + self.item_stack.read().await.clone() + } + + pub async fn set_item(&self, source: ItemStack) { + let new_item = if source.item_count == 0 { + Self::get_default_item() + } else { + let mut item = source; + item.item_count = 1; + item + }; + *self.item_stack.write().await = new_item.clone(); + + self.get_entity().send_meta_data( + &[Metadata::new( + TrackedData::ITEM_STACK, + MetaDataType::ITEM_STACK, + &ItemStackSerializer::from(new_item), + )], + None, + ); + } + + pub fn get_acceleration_power(&self) -> f64 { + f64::from_bits(self.acceleration_power.load(Ordering::Relaxed)) + } + + pub fn set_acceleration_power(&self, power: f64) { + self.acceleration_power + .store(power.to_bits(), Ordering::Relaxed); + } + + pub fn get_explosion_power(&self) -> f32 { + self.explosion_power.load(Ordering::Relaxed) + } + + pub fn set_explosion_power(&self, power: f32) { + self.explosion_power.store(power, Ordering::Relaxed); + } + + pub fn on_deflection(&self, _deflection: &ProjectileDeflectionType, by_attack: bool) { + if by_attack { + self.set_acceleration_power(INITIAL_ACCELERATION_POWER); + } else { + let current = self.get_acceleration_power(); + self.set_acceleration_power(current * DEFLECTION_SCALE); + } + } + + pub fn should_render_at_sqr_distance(&self, distance_sqr: f64) -> bool { + if self.get_entity().age.load(Ordering::Relaxed) < 2 + && distance_sqr < f64::from(MIN_CAMERA_DISTANCE_SQUARED) + { + false + } else { + let bb_size = self + .get_entity() + .bounding_box + .load() + .get_average_side_length() + * 4.0; + let size = if bb_size.is_nan() { 4.0 } else { bb_size } * 64.0; + distance_sqr < size * size } } } -impl NBTStorage for FireballEntity {} +impl NBTStorage for FireballEntity { + fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> { + Box::pin(async move { + nbt.put_double("acceleration_power", self.get_acceleration_power()); + nbt.put_float("ExplosionPower", self.get_explosion_power()); + }) + } + + fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> { + Box::pin(async move { + if let Some(accel) = nbt + .get_double("acceleration_power") + .or_else(|| nbt.get_double("power")) + { + self.set_acceleration_power(accel); + } + if let Some(exp) = nbt.get_float("ExplosionPower") { + self.set_explosion_power(exp); + } + }) + } +} impl EntityBase for FireballEntity { + fn init_data_tracker(&self) -> EntityBaseFuture<'_, ()> { + Box::pin(async move { + let entity = self.get_entity(); + let stack = self.item_stack.read().await; + + entity.send_meta_data( + &[Metadata::new( + TrackedData::ITEM_STACK, + MetaDataType::ITEM_STACK, + &ItemStackSerializer::from(stack.clone()), + )], + None, + ); + }) + } + fn tick<'a>( &'a self, caller: &'a Arc, server: &'a Server, ) -> EntityBaseFuture<'a, ()> { - Box::pin(async move { self.thrown.process_tick(caller, server).await }) + Box::pin(async move { + let entity = self.get_entity(); + let mut velocity = entity.velocity.load(); + + let inertia = if entity.touching_water.load(Ordering::Relaxed) { + WATER_INERTIA + } else { + AIR_INERTIA + }; + + let accel = self.get_acceleration_power(); + let speed = velocity.length(); + if speed > 1e-6 { + let norm = velocity.normalize(); + velocity = norm + .multiply(accel, accel, accel) + .add(&velocity) + .multiply(inertia, inertia, inertia); + entity.velocity.store(velocity); + } + + self.thrown.process_tick(caller, server).await; + }) } fn get_entity(&self) -> &Entity { @@ -75,12 +260,10 @@ impl EntityBase for FireballEntity { Box::pin(async move { let world = self.get_entity().world.load(); - // Handle entity/block hit if let ProjectileHit::Entity { ref entity, .. } = hit { let entity_clone = entity.clone(); tokio::spawn(async move { - // Fireball does 6.0 damage in vanilla entity_clone.get_entity().set_on_fire_for(5.0); let _ = entity_clone .damage( @@ -93,8 +276,7 @@ impl EntityBase for FireballEntity { } let hit_pos = hit.hit_pos(); - // Explosion sets fire if mob griefing is enabled (assuming true for now) - world.explode(hit_pos, self.explosion_power).await; + world.explode(hit_pos, self.get_explosion_power()).await; }) } } diff --git a/crates/pumpkin/src/entity/projectile/mod.rs b/crates/pumpkin/src/entity/projectile/mod.rs index 1dd6e388e..916971dc5 100644 --- a/crates/pumpkin/src/entity/projectile/mod.rs +++ b/crates/pumpkin/src/entity/projectile/mod.rs @@ -12,6 +12,7 @@ use std::{ pub mod arrow; pub mod egg; pub mod ender_pearl; +pub mod evoker_fangs; pub mod eye_of_ender; pub mod fireball; pub mod firework_rocket; diff --git a/crates/pumpkin/src/entity/type.rs b/crates/pumpkin/src/entity/type.rs index 7af6e29db..0feb204f4 100644 --- a/crates/pumpkin/src/entity/type.rs +++ b/crates/pumpkin/src/entity/type.rs @@ -8,13 +8,19 @@ use uuid::Uuid; use crate::entity::boss::ender_dragon::EnderDragonEntity; use crate::entity::boss::wither::WitherEntity; use crate::entity::decoration::{ - armor_stand::ArmorStandEntity, end_crystal::EndCrystalEntity, item_frame::ItemFrameEntity, + armor_stand::ArmorStandEntity, + display::{BlockDisplayEntity, ItemDisplayEntity, TextDisplayEntity}, + end_crystal::EndCrystalEntity, + item_frame::ItemFrameEntity, painting::PaintingEntity, }; use crate::entity::experience_orb::ExperienceOrbEntity; use crate::entity::falling::FallingEntity; +use crate::entity::interaction::InteractionEntity; use crate::entity::item::ItemEntity; +use crate::entity::lightning::LightningBoltEntity; use crate::entity::living::LivingEntity; +use crate::entity::marker::MarkerEntity; use crate::entity::mob::bat::{self, BatEntity}; use crate::entity::mob::blaze::BlazeEntity; use crate::entity::mob::breeze::BreezeEntity; @@ -60,6 +66,7 @@ use crate::entity::passive::camel::CamelEntity; use crate::entity::passive::cat::CatEntity; use crate::entity::passive::chicken::ChickenEntity; use crate::entity::passive::cod::CodEntity; +use crate::entity::passive::copper_golem::CopperGolemEntity; use crate::entity::passive::cow::CowEntity; use crate::entity::passive::dolphin::DolphinEntity; use crate::entity::passive::donkey::DonkeyEntity; @@ -67,11 +74,13 @@ use crate::entity::passive::fox::FoxEntity; use crate::entity::passive::frog::FrogEntity; use crate::entity::passive::glow_squid::GlowSquidEntity; use crate::entity::passive::goat::GoatEntity; +use crate::entity::passive::happy_ghast::HappyGhastEntity; use crate::entity::passive::horse::HorseEntity; use crate::entity::passive::iron_golem::IronGolemEntity; use crate::entity::passive::llama::LlamaEntity; use crate::entity::passive::mooshroom::MooshroomEntity; use crate::entity::passive::mule::MuleEntity; +use crate::entity::passive::nautilus::NautilusEntity; use crate::entity::passive::ocelot::OcelotEntity; use crate::entity::passive::panda::PandaEntity; use crate::entity::passive::parrot::ParrotEntity; @@ -193,8 +202,10 @@ pub fn from_type( id if id == EntityType::AXOLOTL.id => AxolotlEntity::new(entity), id if id == EntityType::BEE.id => BeeEntity::new(entity), id if id == EntityType::CAMEL.id => CamelEntity::new(entity), + id if id == EntityType::COPPER_GOLEM.id => CopperGolemEntity::new(entity), id if id == EntityType::FROG.id => FrogEntity::new(entity), id if id == EntityType::GOAT.id => GoatEntity::new(entity), + id if id == EntityType::HAPPY_GHAST.id => HappyGhastEntity::new(entity), id if id == EntityType::MOOSHROOM.id => MooshroomEntity::new(entity), id if id == EntityType::OCELOT.id => OcelotEntity::new(entity), id if id == EntityType::PANDA.id => PandaEntity::new(entity), @@ -209,6 +220,7 @@ pub fn from_type( id if id == EntityType::TROPICAL_FISH.id => TropicalFishEntity::new(entity), id if id == EntityType::TADPOLE.id => TadpoleEntity::new(entity), id if id == EntityType::DOLPHIN.id => DolphinEntity::new(entity), + id if id == EntityType::NAUTILUS.id => NautilusEntity::new(entity), id if id == EntityType::SNOW_GOLEM.id => SnowGolemEntity::new(entity), id if id == EntityType::IRON_GOLEM.id => IronGolemEntity::new(entity), @@ -220,10 +232,16 @@ pub fn from_type( crate::entity::area_effect_cloud::AreaEffectCloudEntity::new(entity) } id if id == EntityType::ARMOR_STAND.id => Arc::new(ArmorStandEntity::new(entity)), + id if id == EntityType::BLOCK_DISPLAY.id => BlockDisplayEntity::new(entity), + id if id == EntityType::ITEM_DISPLAY.id => ItemDisplayEntity::new(entity), + id if id == EntityType::TEXT_DISPLAY.id => TextDisplayEntity::new(entity), id if id == EntityType::PAINTING.id => Arc::new(PaintingEntity::new(entity)), id if id == EntityType::ITEM_FRAME.id || id == EntityType::GLOW_ITEM_FRAME.id => { Arc::new(ItemFrameEntity::new(entity)) } + id if id == EntityType::EVOKER_FANGS.id => Arc::new( + crate::entity::projectile::evoker_fangs::EvokerFangsEntity::new(entity, 0, 0.0, None), + ), id if id == EntityType::END_CRYSTAL.id => Arc::new(EndCrystalEntity::new(entity)), id if id == EntityType::ENDER_PEARL.id => Arc::new(EnderPearlEntity::new(entity)), id if id == EntityType::SNOWBALL.id => Arc::new(SnowballEntity::new(entity)), @@ -277,6 +295,9 @@ pub fn from_type( }; Arc::new(WindChargeEntity::new_breeze(thrown)) } + id if id == EntityType::LIGHTNING_BOLT.id => Arc::new(LightningBoltEntity::new(entity)), + id if id == EntityType::MARKER.id => MarkerEntity::new(entity), + id if id == EntityType::INTERACTION.id => InteractionEntity::new(entity), id if id == EntityType::FIREWORK_ROCKET.id => Arc::new(FireworkRocketEntity::new(entity)), id if id == EntityType::SPLASH_POTION.id => Arc::new(SplashPotionEntity::new(entity)), id if id == EntityType::LINGERING_POTION.id => Arc::new(LingeringPotionEntity::new(entity)), diff --git a/crates/pumpkin/src/world/mod.rs b/crates/pumpkin/src/world/mod.rs index e030dee33..255608497 100644 --- a/crates/pumpkin/src/world/mod.rs +++ b/crates/pumpkin/src/world/mod.rs @@ -5011,15 +5011,40 @@ impl World { self.spawn_entity(item_entity).await; } - pub async fn strike_lightning(self: &Arc, pos: Vector3, _effect_only: bool) { + pub async fn strike_lightning(self: &Arc, pos: Vector3, effect_only: bool) { use pumpkin_data::entity::EntityType; use uuid::Uuid; + + let server_ref = self.server.upgrade(); + if let Some(server_ref) = server_ref { + let mut event = + crate::plugin::api::events::world::lightning_strike::LightningStrikeEvent::new( + pos, + effect_only, + ); + server_ref + .plugin_manager + .fire(&server_ref, &mut event) + .await; + if event.cancelled { + return; + } + } + let lightning = crate::entity::r#type::from_type( &EntityType::LIGHTNING_BOLT, pos, self, Uuid::new_v4(), ); + + if let Some(bolt) = lightning + .cast_any() + .downcast_ref::() + { + bolt.set_visual_only(effect_only); + } + self.spawn_entity(lightning).await; }