diff --git a/crates/pumpkin-protocol/src/java/client/config/add_resource_pack.rs b/crates/pumpkin-protocol/src/java/client/config/add_resource_pack.rs index 4e4cead49..07b26fed9 100644 --- a/crates/pumpkin-protocol/src/java/client/config/add_resource_pack.rs +++ b/crates/pumpkin-protocol/src/java/client/config/add_resource_pack.rs @@ -42,7 +42,9 @@ impl ClientPacket for CConfigAddResourcePack<'_> { mut write: impl std::io::Write, version: &JavaMinecraftVersion, ) -> Result<(), crate::ser::WritingError> { - write.write_uuid(self.uuid)?; + if *version >= JavaMinecraftVersion::V_1_20_3 { + write.write_uuid(self.uuid)?; + } write.write_string(self.url)?; write.write_string(self.hash)?; write.write_bool(self.forced)?; diff --git a/crates/pumpkin-protocol/src/java/client/play/add_resource_pack.rs b/crates/pumpkin-protocol/src/java/client/play/add_resource_pack.rs index 382f5477a..4a5d73852 100644 --- a/crates/pumpkin-protocol/src/java/client/play/add_resource_pack.rs +++ b/crates/pumpkin-protocol/src/java/client/play/add_resource_pack.rs @@ -41,15 +41,19 @@ impl ClientPacket for CAddResourcePack<'_> { mut write: impl std::io::Write, version: &JavaMinecraftVersion, ) -> Result<(), crate::ser::WritingError> { - write.write_uuid(self.uuid)?; + if *version >= JavaMinecraftVersion::V_1_20_3 { + write.write_uuid(self.uuid)?; + } write.write_string(self.url)?; write.write_string(self.hash)?; - write.write_bool(self.forced)?; - if let Some(prompt) = &self.prompt_message { - write.write_bool(true)?; - write.write_component(prompt, version)?; - } else { - write.write_bool(false)?; + if *version >= JavaMinecraftVersion::V_1_17 { + write.write_bool(self.forced)?; + if let Some(prompt) = &self.prompt_message { + write.write_bool(true)?; + write.write_component(prompt, version)?; + } else { + write.write_bool(false)?; + } } Ok(()) } diff --git a/crates/pumpkin-protocol/src/java/client/play/command_suggestions.rs b/crates/pumpkin-protocol/src/java/client/play/command_suggestions.rs index 1c15c12c1..25e9168df 100644 --- a/crates/pumpkin-protocol/src/java/client/play/command_suggestions.rs +++ b/crates/pumpkin-protocol/src/java/client/play/command_suggestions.rs @@ -81,12 +81,19 @@ impl ClientPacket for CCommandSuggestions { mut write: impl std::io::Write, version: &JavaMinecraftVersion, ) -> Result<(), crate::ser::WritingError> { - write.write_var_int(&self.id)?; - write.write_var_int(&self.start)?; - write.write_var_int(&self.length)?; - write.write_var_int(&VarInt(self.matches.len() as i32))?; - for match_ in &self.matches { - match_.write(&mut write, version)?; + if *version >= JavaMinecraftVersion::V_1_13 { + write.write_var_int(&self.id)?; + write.write_var_int(&self.start)?; + write.write_var_int(&self.length)?; + write.write_var_int(&VarInt(self.matches.len() as i32))?; + for match_ in &self.matches { + match_.write(&mut write, version)?; + } + } else { + write.write_var_int(&VarInt(self.matches.len() as i32))?; + for match_ in &self.matches { + write.write_string(&match_.suggestion)?; + } } Ok(()) } diff --git a/crates/pumpkin-protocol/src/java/client/play/place_ghost_recipe.rs b/crates/pumpkin-protocol/src/java/client/play/place_ghost_recipe.rs index bf79454d4..9750c281f 100644 --- a/crates/pumpkin-protocol/src/java/client/play/place_ghost_recipe.rs +++ b/crates/pumpkin-protocol/src/java/client/play/place_ghost_recipe.rs @@ -24,9 +24,9 @@ impl ClientPacket for CPlaceGhostRecipe<'_> { fn write_packet_data( &self, mut write: impl std::io::Write, - _version: &JavaMinecraftVersion, + version: &JavaMinecraftVersion, ) -> Result<(), crate::ser::WritingError> { - write.write_u8(self.window_id)?; + write.write_container_id(&crate::VarInt(i32::from(self.window_id)), version)?; write.write_string(self.recipe_id)?; Ok(()) } diff --git a/crates/pumpkin-protocol/src/java/client/play/player_rotation.rs b/crates/pumpkin-protocol/src/java/client/play/player_rotation.rs index d5fd46107..10d08e177 100644 --- a/crates/pumpkin-protocol/src/java/client/play/player_rotation.rs +++ b/crates/pumpkin-protocol/src/java/client/play/player_rotation.rs @@ -21,10 +21,16 @@ impl ClientPacket for CPlayerRotation { fn write_packet_data( &self, mut write: impl std::io::Write, - _version: &JavaMinecraftVersion, + version: &JavaMinecraftVersion, ) -> Result<(), crate::ser::WritingError> { write.write_f32_be(self.yaw)?; + if *version >= JavaMinecraftVersion::V_1_21_9 { + write.write_bool(false)?; + } write.write_f32_be(self.pitch)?; + if *version >= JavaMinecraftVersion::V_1_21_9 { + write.write_bool(false)?; + } Ok(()) } } diff --git a/crates/pumpkin-protocol/src/java/client/play/projectile_power.rs b/crates/pumpkin-protocol/src/java/client/play/projectile_power.rs index 7b314fc8b..aa488e594 100644 --- a/crates/pumpkin-protocol/src/java/client/play/projectile_power.rs +++ b/crates/pumpkin-protocol/src/java/client/play/projectile_power.rs @@ -28,12 +28,14 @@ impl ClientPacket for CProjectilePower { fn write_packet_data( &self, mut write: impl std::io::Write, - _version: &JavaMinecraftVersion, + version: &JavaMinecraftVersion, ) -> Result<(), crate::ser::WritingError> { write.write_var_int(&self.entity_id)?; write.write_f64_be(self.x_power)?; - write.write_f64_be(self.y_power)?; - write.write_f64_be(self.z_power)?; + if *version < JavaMinecraftVersion::V_1_21 { + write.write_f64_be(self.y_power)?; + write.write_f64_be(self.z_power)?; + } Ok(()) } } diff --git a/crates/pumpkin-protocol/src/java/client/play/remove_mob_effect.rs b/crates/pumpkin-protocol/src/java/client/play/remove_mob_effect.rs index 45b234e8e..67eaf95e9 100644 --- a/crates/pumpkin-protocol/src/java/client/play/remove_mob_effect.rs +++ b/crates/pumpkin-protocol/src/java/client/play/remove_mob_effect.rs @@ -26,10 +26,19 @@ impl ClientPacket for CRemoveMobEffect { fn write_packet_data( &self, mut write: impl std::io::Write, - _version: &JavaMinecraftVersion, + version: &JavaMinecraftVersion, ) -> Result<(), crate::ser::WritingError> { - write.write_var_int(&self.entity_id)?; - write.write_var_int(&self.effect_id)?; + if *version <= JavaMinecraftVersion::V_1_7_6 { + write.write_i32_be(self.entity_id.0)?; + } else { + write.write_var_int(&self.entity_id)?; + } + + if *version >= JavaMinecraftVersion::V_1_18_2 { + write.write_var_int(&self.effect_id)?; + } else { + write.write_u8(self.effect_id.0 as u8)?; + } Ok(()) } } diff --git a/crates/pumpkin-protocol/src/java/client/play/server_data.rs b/crates/pumpkin-protocol/src/java/client/play/server_data.rs index 8befd0099..a76400ceb 100644 --- a/crates/pumpkin-protocol/src/java/client/play/server_data.rs +++ b/crates/pumpkin-protocol/src/java/client/play/server_data.rs @@ -24,12 +24,36 @@ impl ClientPacket for CServerData<'_> { mut write: impl std::io::Write, version: &JavaMinecraftVersion, ) -> Result<(), crate::ser::WritingError> { - write.write_component(self.motd, version)?; - if let Some(icon) = self.icon_base64 { - write.write_bool(true)?; - write.write_string(icon)?; + if *version >= JavaMinecraftVersion::V_1_19_4 { + write.write_component(self.motd, version)?; + if let Some(icon) = self.icon_base64 { + write.write_bool(true)?; + let raw_b64 = icon.strip_prefix("data:image/png;base64,").unwrap_or(icon); + if let Ok(bytes) = pumpkin_util::jwt::decode_b64_standard(raw_b64) { + write.write_slice(&bytes)?; + } else { + write.write_slice(&[])?; + } + } else { + write.write_bool(false)?; + } } else { - write.write_bool(false)?; + write.write_bool(true)?; + write.write_component(self.motd, version)?; + if let Some(icon) = self.icon_base64 { + write.write_bool(true)?; + write.write_string(icon)?; + } else { + write.write_bool(false)?; + } + if *version < JavaMinecraftVersion::V_1_19_3 { + write.write_bool(false)?; + } + if *version >= JavaMinecraftVersion::V_1_19_1 + && *version < JavaMinecraftVersion::V_1_20_5 + { + write.write_bool(false)?; + } } Ok(()) } diff --git a/crates/pumpkin-protocol/src/java/client/play/set_held_item.rs b/crates/pumpkin-protocol/src/java/client/play/set_held_item.rs index f9e2bd5b7..23e4d847f 100644 --- a/crates/pumpkin-protocol/src/java/client/play/set_held_item.rs +++ b/crates/pumpkin-protocol/src/java/client/play/set_held_item.rs @@ -20,9 +20,13 @@ impl ClientPacket for CSetSelectedSlot { fn write_packet_data( &self, mut write: impl std::io::Write, - _version: &JavaMinecraftVersion, + version: &JavaMinecraftVersion, ) -> Result<(), crate::ser::WritingError> { - write.write_i8(self.slot)?; + if *version >= JavaMinecraftVersion::V_1_21_4 { + write.write_var_int(&crate::VarInt(i32::from(self.slot)))?; + } else { + write.write_i8(self.slot)?; + } Ok(()) } } diff --git a/crates/pumpkin-protocol/src/java/client/play/update_mob_effect.rs b/crates/pumpkin-protocol/src/java/client/play/update_mob_effect.rs index 0d231f356..b1c67eb8d 100644 --- a/crates/pumpkin-protocol/src/java/client/play/update_mob_effect.rs +++ b/crates/pumpkin-protocol/src/java/client/play/update_mob_effect.rs @@ -38,13 +38,40 @@ impl ClientPacket for CUpdateMobEffect { fn write_packet_data( &self, mut write: impl std::io::Write, - _version: &JavaMinecraftVersion, + version: &JavaMinecraftVersion, ) -> Result<(), crate::ser::WritingError> { - write.write_var_int(&self.entity_id)?; - write.write_var_int(&self.effect_id)?; - write.write_var_int(&self.amplifier)?; - write.write_var_int(&self.duration)?; - write.write_i8(self.flags)?; + if *version <= JavaMinecraftVersion::V_1_7_6 { + write.write_i32_be(self.entity_id.0)?; + } else { + write.write_var_int(&self.entity_id)?; + } + + if *version >= JavaMinecraftVersion::V_1_18_2 { + write.write_var_int(&self.effect_id)?; + } else { + write.write_u8(self.effect_id.0 as u8)?; + } + + if *version >= JavaMinecraftVersion::V_1_20_5 { + write.write_var_int(&self.amplifier)?; + } else { + write.write_i8(self.amplifier.0 as i8)?; + } + + if *version <= JavaMinecraftVersion::V_1_7_6 { + write.write_i16_be(self.duration.0 as i16)?; + } else { + write.write_var_int(&self.duration)?; + } + + if *version > JavaMinecraftVersion::V_1_7_6 { + write.write_i8(self.flags)?; + } + + if *version >= JavaMinecraftVersion::V_1_19 && *version < JavaMinecraftVersion::V_1_20_5 { + write.write_bool(false)?; + } + Ok(()) } } diff --git a/crates/pumpkin-protocol/src/java/client/play/update_objectives.rs b/crates/pumpkin-protocol/src/java/client/play/update_objectives.rs index c1c6be478..820d26023 100644 --- a/crates/pumpkin-protocol/src/java/client/play/update_objectives.rs +++ b/crates/pumpkin-protocol/src/java/client/play/update_objectives.rs @@ -45,27 +45,38 @@ impl ClientPacket for CUpdateObjectives { write.write_string(&self.objective_name)?; write.write_u8(self.mode)?; if self.mode == 0 || self.mode == 2 { - write.write_component(&self.display_name, version)?; - write.write_var_int(&self.render_type)?; - write.write_option(&self.number_format, |p, v| { - match v { - NumberFormat::Blank => p.write_var_int(&VarInt(0)), - NumberFormat::Styled(_style) => { - p.write_var_int(&VarInt(1))?; - let comp = pumpkin_nbt::compound::NbtCompound::new(); - // Write style properties if any - let bytes = pumpkin_nbt::Nbt::from(comp).write_unnamed(); - p.write_all(&bytes).map_err(WritingError::IoError) - } - NumberFormat::Fixed(text_component) => { - p.write_var_int(&VarInt(2))?; - p.write_component(text_component, version) - } + if *version < JavaMinecraftVersion::V_1_13 { + write.write_string(&self.display_name.clone().to_pretty_console())?; + let render_str = if self.render_type.0 == 1 { + "hearts" + } else { + "integer" + }; + write.write_string(render_str)?; + } else { + write.write_component(&self.display_name, version)?; + write.write_var_int(&self.render_type)?; + if *version >= JavaMinecraftVersion::V_1_20_3 { + write.write_option(&self.number_format, |p, v| { + match v { + NumberFormat::Blank => p.write_var_int(&VarInt(0)), + NumberFormat::Styled(_style) => { + p.write_var_int(&VarInt(1))?; + let comp = pumpkin_nbt::compound::NbtCompound::new(); + // Write style properties if any + let bytes = pumpkin_nbt::Nbt::from(comp).write_unnamed(); + p.write_all(&bytes).map_err(WritingError::IoError) + } + NumberFormat::Fixed(text_component) => { + p.write_var_int(&VarInt(2))?; + p.write_component(text_component, version) + } + } + })?; } - }) - } else { - Ok(()) + } } + Ok(()) } } diff --git a/crates/pumpkin-protocol/src/java/client/play/update_score.rs b/crates/pumpkin-protocol/src/java/client/play/update_score.rs index 0679b841f..455f4dcd2 100644 --- a/crates/pumpkin-protocol/src/java/client/play/update_score.rs +++ b/crates/pumpkin-protocol/src/java/client/play/update_score.rs @@ -66,9 +66,52 @@ impl ClientPacket for CUpdateScore { version: &pumpkin_util::version::JavaMinecraftVersion, ) -> Result<(), WritingError> { write.write_string(&self.entity_name)?; - write.write_string(&self.objective_name)?; - write.write_var_int(&self.value)?; - write.write_option(&self.display_name, |w, t| w.write_component(t, version))?; - write.write_option(&self.number_format, |w, n| n.write(w)) + if *version >= pumpkin_util::version::JavaMinecraftVersion::V_1_20_3 { + write.write_string(&self.objective_name)?; + write.write_var_int(&self.value)?; + write.write_option(&self.display_name, |w, t| w.write_component(t, version))?; + write.write_option(&self.number_format, |w, n| n.write(w)) + } else if *version <= pumpkin_util::version::JavaMinecraftVersion::V_1_7_6 { + write.write_u8(0)?; + write.write_string(&self.objective_name)?; + write.write_i32_be(self.value.0) + } else { + write.write_u8(0)?; + write.write_string(&self.objective_name)?; + write.write_var_int(&self.value) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use pumpkin_util::version::JavaMinecraftVersion; + + #[test] + fn update_score_serialization() { + let packet = CUpdateScore::new("Alex".into(), "kills".into(), VarInt(42), None, None); + + // Modern 1.20.3+ + let mut buf_modern = Vec::new(); + packet + .write_packet_data(&mut buf_modern, &JavaMinecraftVersion::V_1_20_3) + .unwrap(); + + // 1.8 - 1.20.2 + let mut buf_legacy = Vec::new(); + packet + .write_packet_data(&mut buf_legacy, &JavaMinecraftVersion::V_1_8) + .unwrap(); + + // 1.7.6 + let mut buf_v1_7 = Vec::new(); + packet + .write_packet_data(&mut buf_v1_7, &JavaMinecraftVersion::V_1_7_6) + .unwrap(); + + assert!(!buf_modern.is_empty()); + assert!(!buf_legacy.is_empty()); + assert!(!buf_v1_7.is_empty()); } } diff --git a/crates/pumpkin-protocol/src/java/server/config/resource_pack_response.rs b/crates/pumpkin-protocol/src/java/server/config/resource_pack_response.rs index aef4fb2f9..50fa3c758 100644 --- a/crates/pumpkin-protocol/src/java/server/config/resource_pack_response.rs +++ b/crates/pumpkin-protocol/src/java/server/config/resource_pack_response.rs @@ -3,7 +3,7 @@ use pumpkin_macros::java_packet; use crate::{ ServerPacket, - ser::{NetworkReadExt, ReadingError}, + ser::{NetworkReadExt, NetworkReadSliceExt, ReadingError}, }; use pumpkin_util::version::JavaMinecraftVersion; @@ -34,11 +34,17 @@ pub struct SConfigResourcePack { } impl<'a> ServerPacket<'a> for SConfigResourcePack { - fn read(bytebuf: &mut &'a [u8], _version: &JavaMinecraftVersion) -> Result { - Ok(Self { - uuid: bytebuf.get_uuid()?, - result: bytebuf.get_var_int()?, - }) + fn read(bytebuf: &mut &'a [u8], version: &JavaMinecraftVersion) -> Result { + let uuid = if *version >= JavaMinecraftVersion::V_1_20_3 { + bytebuf.get_uuid()? + } else { + uuid::Uuid::nil() + }; + if *version < JavaMinecraftVersion::V_1_10 { + let _hash = bytebuf.get_str_bounded_borrowed(40)?; + } + let result = bytebuf.get_var_int()?; + Ok(Self { uuid, result }) } } diff --git a/crates/pumpkin-protocol/src/java/server/play/command_suggestion.rs b/crates/pumpkin-protocol/src/java/server/play/command_suggestion.rs index 7cba4c1a9..33cdd122b 100644 --- a/crates/pumpkin-protocol/src/java/server/play/command_suggestion.rs +++ b/crates/pumpkin-protocol/src/java/server/play/command_suggestion.rs @@ -16,11 +16,28 @@ pub struct SCommandSuggestion<'a> { } impl<'a> ServerPacket<'a> for SCommandSuggestion<'a> { - fn read(bytebuf: &mut &'a [u8], _version: &JavaMinecraftVersion) -> Result { - Ok(Self { - id: bytebuf.get_var_int()?, - command: bytebuf.get_str_borrowed()?, - }) + fn read(bytebuf: &mut &'a [u8], version: &JavaMinecraftVersion) -> Result { + if *version >= JavaMinecraftVersion::V_1_13 { + Ok(Self { + id: bytebuf.get_var_int()?, + command: bytebuf.get_str_borrowed()?, + }) + } else { + let command = bytebuf.get_str_borrowed()?; + if *version >= JavaMinecraftVersion::V_1_9 { + let _assume_command = bytebuf.get_bool()?; + } + if *version >= JavaMinecraftVersion::V_1_8 { + let has_pos = bytebuf.get_bool()?; + if has_pos { + let _ = bytebuf.get_block_pos(version)?; + } + } + Ok(Self { + id: VarInt(0), + command, + }) + } } } diff --git a/crates/pumpkin-protocol/src/java/server/play/edit_book.rs b/crates/pumpkin-protocol/src/java/server/play/edit_book.rs index e61c8059e..9b8080d6c 100644 --- a/crates/pumpkin-protocol/src/java/server/play/edit_book.rs +++ b/crates/pumpkin-protocol/src/java/server/play/edit_book.rs @@ -16,21 +16,49 @@ pub struct SEditBook<'a> { } impl<'a> ServerPacket<'a> for SEditBook<'a> { - fn read(read: &mut &'a [u8], _version: &JavaMinecraftVersion) -> Result { - let slot = read.get_var_int()?; - let count = read.get_var_int()?.0 as usize; - let count = count.min(100); - let mut pages = Vec::with_capacity(count); - for _ in 0..count { - pages.push(read.get_str_bounded_borrowed(1024)?); - } - let has_title = read.get_bool()?; - let title = if has_title { - Some(read.get_str_bounded_borrowed(128)?) + fn read(read: &mut &'a [u8], version: &JavaMinecraftVersion) -> Result { + if *version >= JavaMinecraftVersion::V_1_17_1 { + let slot = read.get_var_int()?; + let count = read.get_var_int()?.0 as usize; + let max_pages = if *version >= JavaMinecraftVersion::V_1_21_2 { + 100 + } else { + 200 + }; + let count = count.min(max_pages); + let char_limit = if *version >= JavaMinecraftVersion::V_1_21_2 { + 1024 + } else { + 8192 + }; + let mut pages = Vec::with_capacity(count); + for _ in 0..count { + pages.push(read.get_str_bounded_borrowed(char_limit)?); + } + let has_title = read.get_bool()?; + let title_limit = if *version >= JavaMinecraftVersion::V_1_21_2 { + 32 + } else { + 128 + }; + let title = if has_title { + Some(read.get_str_bounded_borrowed(title_limit)?) + } else { + None + }; + Ok(Self { slot, pages, title }) } else { - None - }; - Ok(Self { slot, pages, title }) + let _item = crate::codec::item_stack_seralizer::ItemStackSerializer::read_with_version( + read, version, + )?; + let _signing = read.get_bool()?; + let slot = read.get_var_int()?; + Ok(Self { + slot, + pages: Vec::new(), + title: None, + }) + } } } diff --git a/crates/pumpkin-protocol/src/java/server/play/place_recipe.rs b/crates/pumpkin-protocol/src/java/server/play/place_recipe.rs index 8d0dd3dba..35e5b456f 100644 --- a/crates/pumpkin-protocol/src/java/server/play/place_recipe.rs +++ b/crates/pumpkin-protocol/src/java/server/play/place_recipe.rs @@ -1,6 +1,6 @@ use crate::{ ServerPacket, - ser::{NetworkReadExt, ReadingError}, + ser::{NetworkReadExt, NetworkReadSliceExt, ReadingError}, }; use pumpkin_data::packet::serverbound::play::PLACE_RECIPE; use pumpkin_macros::java_packet; @@ -16,12 +16,35 @@ pub struct SPlaceRecipe { } impl<'a> ServerPacket<'a> for SPlaceRecipe { - fn read(bytebuf: &mut &'a [u8], _version: &JavaMinecraftVersion) -> Result { - Ok(Self { - container_id: bytebuf.get_i8()?, - recipe_display_id: bytebuf.get_var_int()?, - use_max_items: bytebuf.get_bool()?, - }) + fn read(bytebuf: &mut &'a [u8], version: &JavaMinecraftVersion) -> Result { + if *version >= JavaMinecraftVersion::V_1_21_2 { + let container_id = bytebuf.get_container_id(version)?.0 as i8; + let recipe_display_id = bytebuf.get_var_int()?; + let use_max_items = bytebuf.get_bool()?; + Ok(Self { + container_id, + recipe_display_id, + use_max_items, + }) + } else if *version >= JavaMinecraftVersion::V_1_13 { + let container_id = bytebuf.get_i8()?; + let _recipe_key = bytebuf.get_str_borrowed()?; + let use_max_items = bytebuf.get_bool()?; + Ok(Self { + container_id, + recipe_display_id: VarInt(0), + use_max_items, + }) + } else { + let container_id = bytebuf.get_i8()?; + let recipe_display_id = bytebuf.get_var_int()?; + let use_max_items = bytebuf.get_bool()?; + Ok(Self { + container_id, + recipe_display_id, + use_max_items, + }) + } } } diff --git a/crates/pumpkin-protocol/src/java/server/play/recipe_book_seen_recipe.rs b/crates/pumpkin-protocol/src/java/server/play/recipe_book_seen_recipe.rs index 2a3018c1c..1960eb886 100644 --- a/crates/pumpkin-protocol/src/java/server/play/recipe_book_seen_recipe.rs +++ b/crates/pumpkin-protocol/src/java/server/play/recipe_book_seen_recipe.rs @@ -1,6 +1,6 @@ use crate::{ ServerPacket, - ser::{NetworkReadExt, ReadingError}, + ser::{NetworkReadExt, NetworkReadSliceExt, ReadingError}, }; use pumpkin_data::packet::serverbound::play::RECIPE_BOOK_SEEN_RECIPE; use pumpkin_macros::java_packet; @@ -14,10 +14,17 @@ pub struct SRecipeBookSeenRecipe { } impl<'a> ServerPacket<'a> for SRecipeBookSeenRecipe { - fn read(bytebuf: &mut &'a [u8], _version: &JavaMinecraftVersion) -> Result { - Ok(Self { - recipe_display_id: bytebuf.get_var_int()?, - }) + fn read(bytebuf: &mut &'a [u8], version: &JavaMinecraftVersion) -> Result { + if *version >= JavaMinecraftVersion::V_1_21_2 { + Ok(Self { + recipe_display_id: bytebuf.get_var_int()?, + }) + } else { + let _recipe = bytebuf.get_str_borrowed()?; + Ok(Self { + recipe_display_id: VarInt(0), + }) + } } } diff --git a/crates/pumpkin-protocol/src/java/server/play/resource_pack_response.rs b/crates/pumpkin-protocol/src/java/server/play/resource_pack_response.rs index eddc1b9d1..ba5341999 100644 --- a/crates/pumpkin-protocol/src/java/server/play/resource_pack_response.rs +++ b/crates/pumpkin-protocol/src/java/server/play/resource_pack_response.rs @@ -4,7 +4,7 @@ use pumpkin_macros::java_packet; use crate::{ ServerPacket, codec::var_int::VarInt, - ser::{NetworkReadExt, ReadingError}, + ser::{NetworkReadExt, NetworkReadSliceExt, ReadingError}, }; use pumpkin_util::version::JavaMinecraftVersion; @@ -45,11 +45,17 @@ impl SPlayResourcePack { } impl<'a> ServerPacket<'a> for SPlayResourcePack { - fn read(bytebuf: &mut &'a [u8], _version: &JavaMinecraftVersion) -> Result { - Ok(Self { - uuid: bytebuf.get_uuid()?, - result: bytebuf.get_var_int()?, - }) + fn read(bytebuf: &mut &'a [u8], version: &JavaMinecraftVersion) -> Result { + let uuid = if *version >= JavaMinecraftVersion::V_1_20_3 { + bytebuf.get_uuid()? + } else { + uuid::Uuid::nil() + }; + if *version < JavaMinecraftVersion::V_1_10 { + let _hash = bytebuf.get_str_bounded_borrowed(40)?; + } + let result = bytebuf.get_var_int()?; + Ok(Self { uuid, result }) } } diff --git a/crates/pumpkin-protocol/src/java/server/play/set_beacon.rs b/crates/pumpkin-protocol/src/java/server/play/set_beacon.rs index 652b59cb5..75c70e00f 100644 --- a/crates/pumpkin-protocol/src/java/server/play/set_beacon.rs +++ b/crates/pumpkin-protocol/src/java/server/play/set_beacon.rs @@ -15,11 +15,22 @@ pub struct SSetBeacon { } impl<'a> ServerPacket<'a> for SSetBeacon { - fn read(bytebuf: &mut &'a [u8], _version: &JavaMinecraftVersion) -> Result { - Ok(Self { - primary_effect: bytebuf.get_option(NetworkReadExt::get_var_int)?, - secondary_effect: bytebuf.get_option(NetworkReadExt::get_var_int)?, - }) + fn read(bytebuf: &mut &'a [u8], version: &JavaMinecraftVersion) -> Result { + if *version >= JavaMinecraftVersion::V_1_19 { + Ok(Self { + primary_effect: bytebuf.get_option(NetworkReadExt::get_var_int)?, + secondary_effect: bytebuf.get_option(NetworkReadExt::get_var_int)?, + }) + } else { + let p = bytebuf.get_var_int()?; + let s = bytebuf.get_var_int()?; + let primary_effect = if p.0 == -1 { None } else { Some(p) }; + let secondary_effect = if s.0 == -1 { None } else { Some(s) }; + Ok(Self { + primary_effect, + secondary_effect, + }) + } } } diff --git a/crates/pumpkin-util/src/math/vector3.rs b/crates/pumpkin-util/src/math/vector3.rs index cf205a004..9e8399c27 100644 --- a/crates/pumpkin-util/src/math/vector3.rs +++ b/crates/pumpkin-util/src/math/vector3.rs @@ -474,6 +474,18 @@ impl Mul for Vector3 { } } +impl Div for Vector3 { + type Output = Self; + + fn div(self, scalar: T) -> Self { + Self { + x: self.x / scalar, + y: self.y / scalar, + z: self.z / scalar, + } + } +} + impl Add for Vector3 { type Output = Self; diff --git a/crates/pumpkin/src/block/blocks/anvil.rs b/crates/pumpkin/src/block/blocks/anvil.rs index e6417dbe6..6ecca285d 100644 --- a/crates/pumpkin/src/block/blocks/anvil.rs +++ b/crates/pumpkin/src/block/blocks/anvil.rs @@ -2,12 +2,12 @@ use crate::block::blocks::falling::FallingBlock; use crate::block::registry::BlockActionResult; use crate::block::{ BlockBehaviour, GetStateForNeighborUpdateArgs, NormalUseArgs, OnPlaceArgs, OnScheduledTickArgs, - PlacedArgs, + PathComputationType, PlacedArgs, }; use pumpkin_data::block_properties::WallTorchLikeProperties; use pumpkin_data::translation; -use pumpkin_data::{Block, BlockStateId}; +use pumpkin_data::{Block, BlockState, BlockStateId}; use pumpkin_inventory::anvil::AnvilScreenHandler; use pumpkin_inventory::player::player_inventory::PlayerInventory; use pumpkin_inventory::screen_handler::{ @@ -79,6 +79,10 @@ impl BlockBehaviour for AnvilBlock { ) -> BlockStateId { FallingBlock::get_state_for_neighbor_update(&FallingBlock, args) } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } struct AnvilScreenFactory; diff --git a/crates/pumpkin/src/block/blocks/bed.rs b/crates/pumpkin/src/block/blocks/bed.rs index 7758a0d50..df46b1f4b 100644 --- a/crates/pumpkin/src/block/blocks/bed.rs +++ b/crates/pumpkin/src/block/blocks/bed.rs @@ -1,12 +1,11 @@ use std::sync::Arc; use crate::block::entities::bed::BedBlockEntity; -use pumpkin_data::Block; -use pumpkin_data::BlockStateId; use pumpkin_data::block_properties::BedPart; use pumpkin_data::dimension::Dimension; use pumpkin_data::entity::EntityType; use pumpkin_data::translation; +use pumpkin_data::{Block, BlockState, BlockStateId}; use pumpkin_macros::pumpkin_block_from_tag; use pumpkin_util::GameMode; use pumpkin_util::math::position::BlockPos; @@ -18,7 +17,7 @@ use crate::block::bounce_entity_after_fall; use crate::block::registry::BlockActionResult; use crate::block::{ BlockBehaviour, BrokenArgs, CanPlaceAtArgs, NormalUseArgs, OnPlaceArgs, OnStateReplacedArgs, - PlacedArgs, PlayerPlacedArgs, + PathComputationType, PlacedArgs, PlayerPlacedArgs, }; use crate::entity::{Entity, EntityBase, player::Player}; use crate::world::World; @@ -188,6 +187,10 @@ impl BlockBehaviour for BedBlock { fn normal_use(&self, args: NormalUseArgs<'_>) -> BlockActionResult { Self::use_bed(args.world, args.player, args.block, args.position) } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } impl BedBlock { diff --git a/crates/pumpkin/src/block/blocks/brewing_stand.rs b/crates/pumpkin/src/block/blocks/brewing_stand.rs index 182f6f947..35f2c736d 100644 --- a/crates/pumpkin/src/block/blocks/brewing_stand.rs +++ b/crates/pumpkin/src/block/blocks/brewing_stand.rs @@ -1,13 +1,14 @@ use std::sync::Arc; use std::sync::Mutex; -use crate::block::{GetComparatorOutputArgs, PlacedArgs}; +use crate::block::{GetComparatorOutputArgs, PathComputationType, PlacedArgs}; use crate::block::{ registry::BlockActionResult, {BlockBehaviour, NormalUseArgs}, }; use crate::block::entities::brewing_stand::BrewingStandBlockEntity; +use pumpkin_data::BlockState; use pumpkin_data::translation; use pumpkin_inventory::player::player_inventory::PlayerInventory; use pumpkin_inventory::screen_handler::{ @@ -85,4 +86,8 @@ impl BlockBehaviour for BrewingStandBlock { None } } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } diff --git a/crates/pumpkin/src/block/blocks/cake.rs b/crates/pumpkin/src/block/blocks/cake.rs index 7b4771a1d..5d0daab1d 100644 --- a/crates/pumpkin/src/block/blocks/cake.rs +++ b/crates/pumpkin/src/block/blocks/cake.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use crate::{ block::{ BlockBehaviour, CanPlaceAtArgs, GetComparatorOutputArgs, GetStateForNeighborUpdateArgs, - NormalUseArgs, OnPlaceArgs, OnScheduledTickArgs, UseWithItemArgs, + NormalUseArgs, OnPlaceArgs, OnScheduledTickArgs, PathComputationType, UseWithItemArgs, blocks::candle_cakes::cake_from_candle, registry::BlockActionResult, }, entity::player::Player, @@ -11,7 +11,7 @@ use crate::{ }; use pumpkin_data::item::Item; use pumpkin_data::{ - Block, BlockStateId, + Block, BlockState, BlockStateId, block_properties::CakeLikeProperties, sound::{Sound, SoundCategory}, }; @@ -169,6 +169,10 @@ impl BlockBehaviour for CakeBlock { } } } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } fn can_place_at(world: &dyn BlockAccessor, position: &BlockPos) -> bool { diff --git a/crates/pumpkin/src/block/blocks/campfire.rs b/crates/pumpkin/src/block/blocks/campfire.rs index 0ae346890..21a80d0a5 100644 --- a/crates/pumpkin/src/block/blocks/campfire.rs +++ b/crates/pumpkin/src/block/blocks/campfire.rs @@ -1,6 +1,7 @@ use pumpkin_data::{ - Block, BlockDirection, BlockStateId, Enchantment, block_properties::CampfireLikeProperties, - damage::DamageType, data_component_impl::EquipmentSlot, effect::StatusEffect, fluid::Fluid, + Block, BlockDirection, BlockState, BlockStateId, Enchantment, + block_properties::CampfireLikeProperties, damage::DamageType, + data_component_impl::EquipmentSlot, effect::StatusEffect, fluid::Fluid, }; use pumpkin_macros::pumpkin_block_from_tag; use pumpkin_world::tick::TickPriority; @@ -9,7 +10,7 @@ use crate::block::entities::campfire::CampfireBlockEntity; use crate::{ block::{ BlockBehaviour, BlockIsReplacing, GetStateForNeighborUpdateArgs, OnEntityCollisionArgs, - OnPlaceArgs, PlacedArgs, + OnPlaceArgs, PathComputationType, PlacedArgs, }, entity::EntityBase, }; @@ -95,6 +96,10 @@ impl BlockBehaviour for CampfireBlock { props.to_state_id(args.block) } + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } + // TODO: onProjectileHit } diff --git a/crates/pumpkin/src/block/blocks/candle_cakes.rs b/crates/pumpkin/src/block/blocks/candle_cakes.rs index 7424b5a14..5204807fc 100644 --- a/crates/pumpkin/src/block/blocks/candle_cakes.rs +++ b/crates/pumpkin/src/block/blocks/candle_cakes.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use pumpkin_data::{Block, BlockStateId, item::Item, item_stack::ItemStack}; +use pumpkin_data::{Block, BlockState, BlockStateId, item::Item, item_stack::ItemStack}; use pumpkin_macros::pumpkin_block_from_tag; use pumpkin_util::{GameMode, math::position::BlockPos}; use pumpkin_world::{ @@ -11,7 +11,7 @@ use pumpkin_world::{ use crate::{ block::{ BlockBehaviour, GetStateForNeighborUpdateArgs, NormalUseArgs, OnScheduledTickArgs, - UseWithItemArgs, blocks::cake::CakeBlock, registry::BlockActionResult, + PathComputationType, UseWithItemArgs, blocks::cake::CakeBlock, registry::BlockActionResult, }, entity::player::Player, world::World, @@ -121,6 +121,10 @@ impl BlockBehaviour for CandleCakeBlock { } args.state_id } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } fn can_place_at(world: &dyn BlockAccessor, position: &BlockPos) -> bool { diff --git a/crates/pumpkin/src/block/blocks/cauldron.rs b/crates/pumpkin/src/block/blocks/cauldron.rs index fbf6ef742..85c0c95c3 100644 --- a/crates/pumpkin/src/block/blocks/cauldron.rs +++ b/crates/pumpkin/src/block/blocks/cauldron.rs @@ -1,13 +1,14 @@ use std::sync::Arc; use crate::block::registry::BlockActionResult; -use crate::block::{BlockBehaviour, BlockMetadata, GetComparatorOutputArgs, UseWithItemArgs}; -use pumpkin_data::Block; -use pumpkin_data::BlockId; +use crate::block::{ + BlockBehaviour, BlockMetadata, GetComparatorOutputArgs, PathComputationType, UseWithItemArgs, +}; use pumpkin_data::block_properties::WaterCauldronLikeProperties; use pumpkin_data::item::Item; use pumpkin_data::item_stack::ItemStack; use pumpkin_data::sound::{Sound, SoundCategory}; +use pumpkin_data::{Block, BlockId, BlockState}; use pumpkin_world::world::BlockFlags; pub struct CauldronBlock; @@ -216,4 +217,8 @@ impl BlockBehaviour for CauldronBlock { _ => Some(0), } } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } diff --git a/crates/pumpkin/src/block/blocks/chain.rs b/crates/pumpkin/src/block/blocks/chain.rs index a511ea90b..a37a7c2cf 100644 --- a/crates/pumpkin/src/block/blocks/chain.rs +++ b/crates/pumpkin/src/block/blocks/chain.rs @@ -1,7 +1,6 @@ -use crate::block::{BlockBehaviour, OnPlaceArgs}; -use pumpkin_data::BlockDirection; -use pumpkin_data::BlockStateId; +use crate::block::{BlockBehaviour, OnPlaceArgs, PathComputationType}; use pumpkin_data::block_properties::Axis; +use pumpkin_data::{BlockDirection, BlockState, BlockStateId}; use pumpkin_macros::pumpkin_block; #[pumpkin_block("minecraft:iron_chain")] @@ -20,4 +19,8 @@ impl BlockBehaviour for ChainBlock { props.to_state_id(args.block) } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } diff --git a/crates/pumpkin/src/block/blocks/chests.rs b/crates/pumpkin/src/block/blocks/chests.rs index 7bc0501f1..6f645f73b 100644 --- a/crates/pumpkin/src/block/blocks/chests.rs +++ b/crates/pumpkin/src/block/blocks/chests.rs @@ -22,17 +22,15 @@ use pumpkin_world::world::BlockFlags; use std::sync::Mutex; use crate::block::{ - BrokenArgs, EmitsRedstonePowerArgs, GetComparatorOutputArgs, GetRedstonePowerArgs, - NormalUseArgs, OnPlaceArgs, OnSyncedBlockEventArgs, PlacedArgs, PlayerPlacedArgs, - RandomTickArgs, + BlockBehaviour, BrokenArgs, EmitsRedstonePowerArgs, GetComparatorOutputArgs, + GetRedstonePowerArgs, NormalUseArgs, OnPlaceArgs, OnSyncedBlockEventArgs, PathComputationType, + PlacedArgs, PlayerPlacedArgs, RandomTickArgs, registry::BlockActionResult, }; use crate::entity::EntityBase; +use crate::entity::player::Player; use crate::world::World; use crate::world::loot::fill_chest_inventory; -use crate::{ - block::{BlockBehaviour, registry::BlockActionResult}, - entity::player::Player, -}; +use pumpkin_data::BlockState; struct ChestScreenFactory(Arc); @@ -297,6 +295,10 @@ impl BlockBehaviour for ChestBlock { fn get_comparator_output(&self, args: GetComparatorOutputArgs<'_>) -> Option { get_chest_comparator_output(&args) } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } /// Copper chests have the same behavior as wooden chests but also oxidize over time. @@ -389,6 +391,10 @@ impl BlockBehaviour for CopperChestBlock { fn get_comparator_output(&self, args: GetComparatorOutputArgs<'_>) -> Option { get_chest_comparator_output(&args) } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } /// Trapped chests have the same behavior as wooden chests but also emit redstone power based on viewer count. @@ -455,6 +461,10 @@ impl BlockBehaviour for TrappedChestBlock { fn get_comparator_output(&self, args: GetComparatorOutputArgs<'_>) -> Option { get_chest_comparator_output(&args) } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } fn compute_chest_props( diff --git a/crates/pumpkin/src/block/blocks/composter.rs b/crates/pumpkin/src/block/blocks/composter.rs index 2ceb8f75d..67339997b 100644 --- a/crates/pumpkin/src/block/blocks/composter.rs +++ b/crates/pumpkin/src/block/blocks/composter.rs @@ -3,13 +3,13 @@ use std::sync::Arc; use crate::{ block::{ BlockBehaviour, GetComparatorOutputArgs, NormalUseArgs, OnScheduledTickArgs, - UseWithItemArgs, registry::BlockActionResult, + PathComputationType, UseWithItemArgs, registry::BlockActionResult, }, entity::{Entity, item::ItemEntity}, world::World, }; use pumpkin_data::{ - Block, BlockStateId, block_properties::ComposterLikeProperties, + Block, BlockState, BlockStateId, block_properties::ComposterLikeProperties, composter_increase_chance::get_composter_increase_chance_from_item_id, entity::EntityType, item::Item, item_stack::ItemStack, world::WorldEvent, }; @@ -93,6 +93,10 @@ impl BlockBehaviour for ComposterBlock { Some(props.level) } } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } impl ComposterBlock { diff --git a/crates/pumpkin/src/block/blocks/conduit.rs b/crates/pumpkin/src/block/blocks/conduit.rs index abf8f34fc..4bd1a81fd 100644 --- a/crates/pumpkin/src/block/blocks/conduit.rs +++ b/crates/pumpkin/src/block/blocks/conduit.rs @@ -1,6 +1,6 @@ use crate::block::entities::conduit::ConduitBlockEntity; -use crate::block::{BlockBehaviour, OnPlaceArgs, PlacedArgs}; -use pumpkin_data::BlockStateId; +use crate::block::{BlockBehaviour, OnPlaceArgs, PathComputationType, PlacedArgs}; +use pumpkin_data::{BlockState, BlockStateId}; use pumpkin_macros::pumpkin_block; use std::sync::Arc; @@ -22,4 +22,8 @@ impl BlockBehaviour for ConduitBlock { args.world.add_block_entity(Arc::new(entity)); } } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } diff --git a/crates/pumpkin/src/block/blocks/decorated_pot.rs b/crates/pumpkin/src/block/blocks/decorated_pot.rs index 32f141cdc..6c80d82c5 100644 --- a/crates/pumpkin/src/block/blocks/decorated_pot.rs +++ b/crates/pumpkin/src/block/blocks/decorated_pot.rs @@ -1,17 +1,17 @@ use std::sync::Arc; -use pumpkin_data::BlockStateId; use pumpkin_data::block_properties::DecoratedPotLikeProperties; use pumpkin_data::item::Item; use pumpkin_data::item_stack::ItemStack; use pumpkin_data::sound::{Sound, SoundCategory}; +use pumpkin_data::{BlockState, BlockStateId}; use pumpkin_macros::pumpkin_block; use crate::block::entities::decorated_pot::DecoratedPotBlockEntity; use crate::block::registry::BlockActionResult; use crate::block::{ - BlockBehaviour, BrokenArgs, GetComparatorOutputArgs, NormalUseArgs, OnPlaceArgs, PlacedArgs, - UseWithItemArgs, + BlockBehaviour, BrokenArgs, GetComparatorOutputArgs, NormalUseArgs, OnPlaceArgs, + PathComputationType, PlacedArgs, UseWithItemArgs, }; #[pumpkin_block("minecraft:decorated_pot")] @@ -109,4 +109,8 @@ impl BlockBehaviour for DecoratedPotBlock { Some(0) } } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } diff --git a/crates/pumpkin/src/block/blocks/dirt_path.rs b/crates/pumpkin/src/block/blocks/dirt_path.rs index 4c70ff741..3bfc78d68 100644 --- a/crates/pumpkin/src/block/blocks/dirt_path.rs +++ b/crates/pumpkin/src/block/blocks/dirt_path.rs @@ -1,11 +1,8 @@ -use crate::block::BlockBehaviour; -use crate::block::CanPlaceAtArgs; -use crate::block::GetStateForNeighborUpdateArgs; -use crate::block::OnPlaceArgs; -use crate::block::OnScheduledTickArgs; -use pumpkin_data::Block; -use pumpkin_data::BlockDirection; -use pumpkin_data::BlockStateId; +use crate::block::{ + BlockBehaviour, CanPlaceAtArgs, GetStateForNeighborUpdateArgs, OnPlaceArgs, + OnScheduledTickArgs, PathComputationType, +}; +use pumpkin_data::{Block, BlockDirection, BlockState, BlockStateId}; use pumpkin_macros::pumpkin_block; use pumpkin_util::math::position::BlockPos; use pumpkin_world::tick::TickPriority; @@ -47,6 +44,10 @@ impl BlockBehaviour for DirtPathBlock { fn can_place_at(&self, args: CanPlaceAtArgs<'_>) -> bool { can_place_at(args.block_accessor, args.position) } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } fn can_place_at(world: &dyn BlockAccessor, block_pos: &BlockPos) -> bool { diff --git a/crates/pumpkin/src/block/blocks/doors.rs b/crates/pumpkin/src/block/blocks/doors.rs index 73534b78c..c38c4f9fd 100644 --- a/crates/pumpkin/src/block/blocks/doors.rs +++ b/crates/pumpkin/src/block/blocks/doors.rs @@ -7,7 +7,7 @@ use pumpkin_data::block_properties::HorizontalFacing; use pumpkin_data::sound::Sound; use pumpkin_data::sound::SoundCategory; use pumpkin_data::tag::Taggable; -use pumpkin_data::{Block, tag}; +use pumpkin_data::{Block, BlockState, tag}; use pumpkin_macros::pumpkin_block_from_tag; use pumpkin_util::math::position::BlockPos; use pumpkin_world::world::BlockAccessor; @@ -22,6 +22,7 @@ use crate::block::NormalUseArgs; use crate::block::OnNeighborUpdateArgs; use crate::block::OnPlaceArgs; use crate::block::OnStateReplacedArgs; +use crate::block::PathComputationType; use crate::block::PlacedArgs; use crate::block::blocks::redstone::block_receives_redstone_power; use crate::block::registry::BlockActionResult; @@ -391,6 +392,15 @@ impl BlockBehaviour for DoorBlock { } } } + + fn is_pathfindable(&self, state: &BlockState, computation_type: PathComputationType) -> bool { + match computation_type { + PathComputationType::Land | PathComputationType::Air => { + DoorProperties::from_state_id(state.id).open + } + PathComputationType::Water => false, + } + } } fn has_support(world: &dyn BlockAccessor, block_pos: &BlockPos) -> bool { diff --git a/crates/pumpkin/src/block/blocks/dragon_egg.rs b/crates/pumpkin/src/block/blocks/dragon_egg.rs index fc971556b..681080906 100644 --- a/crates/pumpkin/src/block/blocks/dragon_egg.rs +++ b/crates/pumpkin/src/block/blocks/dragon_egg.rs @@ -1,7 +1,10 @@ use crate::block::blocks::falling::FallingBlock; use crate::block::registry::BlockActionResult; -use crate::block::{BlockBehaviour, BrokenArgs, NormalUseArgs, PlacedArgs}; +use crate::block::{ + BlockBehaviour, BrokenArgs, NormalUseArgs, OnScheduledTickArgs, PathComputationType, PlacedArgs, +}; use crate::world::World; +use pumpkin_data::BlockState; use pumpkin_macros::pumpkin_block; use pumpkin_util::math::position::BlockPos; use pumpkin_world::tick::TickPriority; @@ -56,7 +59,11 @@ impl BlockBehaviour for DragonEggBlock { Self::teleport(args.world, args.position); } - fn on_scheduled_tick(&self, args: crate::block::OnScheduledTickArgs<'_>) { + fn on_scheduled_tick(&self, args: OnScheduledTickArgs<'_>) { FallingBlock::on_scheduled_tick(&FallingBlock, args); } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } diff --git a/crates/pumpkin/src/block/blocks/dripstone.rs b/crates/pumpkin/src/block/blocks/dripstone.rs index d6a2561c1..7cde7a56d 100644 --- a/crates/pumpkin/src/block/blocks/dripstone.rs +++ b/crates/pumpkin/src/block/blocks/dripstone.rs @@ -3,13 +3,13 @@ use std::sync::Arc; use crate::{ block::{ BlockBehaviour, BrokenArgs, CanPlaceAtArgs, GetStateForNeighborUpdateArgs, OnPlaceArgs, - PlacedArgs, + PathComputationType, PlacedArgs, }, entity::player::Player, world::World, }; use pumpkin_data::{ - Block, BlockDirection, BlockStateId, + Block, BlockDirection, BlockState, BlockStateId, block_properties::{PointedDripstoneLikeProperties, SpeleothemThickness, VerticalDirection}, }; use pumpkin_macros::pumpkin_block; @@ -114,6 +114,10 @@ impl BlockBehaviour for DripstoneBlock { } args.state_id } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } fn update_stalagmite(world: &Arc, stalagmite_len: u8, tip_pos: &BlockPos) { let block_above = world.get_block(&tip_pos.up()); diff --git a/crates/pumpkin/src/block/blocks/enchanting_table.rs b/crates/pumpkin/src/block/blocks/enchanting_table.rs index f4480cfaa..505ad4fa5 100644 --- a/crates/pumpkin/src/block/blocks/enchanting_table.rs +++ b/crates/pumpkin/src/block/blocks/enchanting_table.rs @@ -3,8 +3,8 @@ use std::sync::Mutex; use crate::block::entities::enchanting_table::EnchantingTableBlockEntity; use crate::block::registry::BlockActionResult; -use crate::block::{BlockBehaviour, NormalUseArgs, PlacedArgs}; -use pumpkin_data::{Block, BlockStateId, translation}; +use crate::block::{BlockBehaviour, NormalUseArgs, PathComputationType, PlacedArgs}; +use pumpkin_data::{Block, BlockState, BlockStateId, translation}; use pumpkin_inventory::enchanting::enchanting_screen_handler::EnchantingTableScreenHandler; use pumpkin_inventory::player::player_inventory::PlayerInventory; use pumpkin_inventory::screen_handler::{ @@ -79,6 +79,10 @@ impl BlockBehaviour for EnchantingTableBlock { ); BlockActionResult::Success } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } impl EnchantingTableBlock { diff --git a/crates/pumpkin/src/block/blocks/end_portal_frame.rs b/crates/pumpkin/src/block/blocks/end_portal_frame.rs index 283fe9546..f6dfcdf3f 100644 --- a/crates/pumpkin/src/block/blocks/end_portal_frame.rs +++ b/crates/pumpkin/src/block/blocks/end_portal_frame.rs @@ -1,8 +1,8 @@ -use pumpkin_data::BlockStateId; +use pumpkin_data::{BlockState, BlockStateId}; use pumpkin_macros::pumpkin_block; use crate::{ - block::{BlockBehaviour, OnPlaceArgs}, + block::{BlockBehaviour, OnPlaceArgs, PathComputationType}, entity::EntityBase, }; @@ -18,4 +18,8 @@ impl BlockBehaviour for EndPortalFrameBlock { end_portal_frame_props.to_state_id(args.block) } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } diff --git a/crates/pumpkin/src/block/blocks/end_rod.rs b/crates/pumpkin/src/block/blocks/end_rod.rs index ab7f657f6..9cde6aa0c 100644 --- a/crates/pumpkin/src/block/blocks/end_rod.rs +++ b/crates/pumpkin/src/block/blocks/end_rod.rs @@ -1,7 +1,6 @@ -use crate::block::{BlockBehaviour, OnPlaceArgs}; -use pumpkin_data::Block; -use pumpkin_data::BlockStateId; +use crate::block::{BlockBehaviour, OnPlaceArgs, PathComputationType}; use pumpkin_data::block_properties::EndRodLikeProperties; +use pumpkin_data::{Block, BlockState, BlockStateId}; use pumpkin_macros::pumpkin_block; #[pumpkin_block("minecraft:end_rod")] @@ -26,4 +25,8 @@ impl BlockBehaviour for EndRodBlock { props.to_state_id(args.block) } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } diff --git a/crates/pumpkin/src/block/blocks/ender_chest.rs b/crates/pumpkin/src/block/blocks/ender_chest.rs index a290ba910..1c9925b7c 100644 --- a/crates/pumpkin/src/block/blocks/ender_chest.rs +++ b/crates/pumpkin/src/block/blocks/ender_chest.rs @@ -3,13 +3,12 @@ use std::sync::Mutex; use crate::block::entities::ender_chest::EnderChestBlockEntity; use crate::block::{ - BlockBehaviour, NormalUseArgs, OnPlaceArgs, OnSyncedBlockEventArgs, PlacedArgs, - registry::BlockActionResult, + BlockBehaviour, NormalUseArgs, OnPlaceArgs, OnSyncedBlockEventArgs, PathComputationType, + PlacedArgs, registry::BlockActionResult, }; use crate::world::World; -use pumpkin_data::BlockStateId; use pumpkin_data::block_properties::LadderLikeProperties; -use pumpkin_data::translation; +use pumpkin_data::{BlockState, BlockStateId, translation}; use pumpkin_inventory::{ generic_container_screen_handler::create_generic_9x3, player::ender_chest_inventory::EnderChestInventory, @@ -112,6 +111,10 @@ impl BlockBehaviour for EnderChestBlock { let block_entity = EnderChestBlockEntity::new(*args.position); args.world.add_block_entity(Arc::new(block_entity)); } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } fn is_chest_blocked(world: &World, block_pos: &BlockPos) -> bool { diff --git a/crates/pumpkin/src/block/blocks/farmland.rs b/crates/pumpkin/src/block/blocks/farmland.rs index 81d5267e3..8e888dcfa 100644 --- a/crates/pumpkin/src/block/blocks/farmland.rs +++ b/crates/pumpkin/src/block/blocks/farmland.rs @@ -1,18 +1,14 @@ use std::sync::Arc; -use crate::block::BlockBehaviour; -use crate::block::CanPlaceAtArgs; -use crate::block::GetStateForNeighborUpdateArgs; -use crate::block::OnPlaceArgs; -use crate::block::OnScheduledTickArgs; -use crate::block::RandomTickArgs; +use crate::block::{ + BlockBehaviour, CanPlaceAtArgs, GetStateForNeighborUpdateArgs, OnPlaceArgs, + OnScheduledTickArgs, PathComputationType, RandomTickArgs, +}; use crate::world::World; -use pumpkin_data::Block; -use pumpkin_data::BlockDirection; -use pumpkin_data::BlockStateId; use pumpkin_data::block_properties::FarmlandLikeProperties; use pumpkin_data::tag; use pumpkin_data::tag::Taggable; +use pumpkin_data::{Block, BlockDirection, BlockState, BlockStateId}; use pumpkin_macros::pumpkin_block; use pumpkin_util::math::position::BlockPos; use pumpkin_util::math::vector3::Vector3; @@ -93,6 +89,10 @@ impl BlockBehaviour for FarmlandBlock { } } } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } fn can_place_at(world: &dyn BlockAccessor, block_pos: &BlockPos) -> bool { diff --git a/crates/pumpkin/src/block/blocks/fence_gates.rs b/crates/pumpkin/src/block/blocks/fence_gates.rs index 78d3034c3..36fd55276 100644 --- a/crates/pumpkin/src/block/blocks/fence_gates.rs +++ b/crates/pumpkin/src/block/blocks/fence_gates.rs @@ -3,16 +3,16 @@ use std::sync::Arc; use crate::block::blocks::redstone::block_receives_redstone_power; use crate::block::registry::BlockActionResult; use crate::block::{ - BlockBehaviour, GetStateForNeighborUpdateArgs, NormalUseArgs, OnNeighborUpdateArgs, OnPlaceArgs, + BlockBehaviour, GetStateForNeighborUpdateArgs, NormalUseArgs, OnNeighborUpdateArgs, + OnPlaceArgs, PathComputationType, }; use crate::entity::EntityBase; use crate::entity::player::Player; use crate::world::World; -use pumpkin_data::Block; -use pumpkin_data::BlockStateId; use pumpkin_data::sound::{Sound, SoundCategory}; use pumpkin_data::tag; use pumpkin_data::tag::Taggable; +use pumpkin_data::{Block, BlockState, BlockStateId}; use pumpkin_macros::pumpkin_block_from_tag; use pumpkin_util::math::position::BlockPos; use pumpkin_world::world::BlockFlags; @@ -134,6 +134,15 @@ impl BlockBehaviour for FenceGateBlock { ); } } + + fn is_pathfindable(&self, state: &BlockState, computation_type: PathComputationType) -> bool { + match computation_type { + PathComputationType::Land | PathComputationType::Air => { + FenceGateProperties::from_state_id(state.id).open + } + PathComputationType::Water => false, + } + } } fn is_in_wall(args: &GetStateForNeighborUpdateArgs<'_>) -> FenceGateProperties { diff --git a/crates/pumpkin/src/block/blocks/fences.rs b/crates/pumpkin/src/block/blocks/fences.rs index 170fb60f0..01fa31d3a 100644 --- a/crates/pumpkin/src/block/blocks/fences.rs +++ b/crates/pumpkin/src/block/blocks/fences.rs @@ -1,21 +1,16 @@ -use crate::block::GetStateForNeighborUpdateArgs; -use crate::block::OnPlaceArgs; -use pumpkin_data::BlockDirection; -use pumpkin_data::BlockState; -use pumpkin_data::BlockStateId; -use pumpkin_data::HorizontalFacingExt; +use crate::block::{ + BlockBehaviour, GetStateForNeighborUpdateArgs, OnPlaceArgs, PathComputationType, +}; +use crate::world::World; use pumpkin_data::block_properties::HorizontalFacing; use pumpkin_data::tag::Taggable; -use pumpkin_data::{Block, tag}; +use pumpkin_data::{Block, BlockDirection, BlockState, BlockStateId, HorizontalFacingExt, tag}; use pumpkin_macros::pumpkin_block_from_tag; use pumpkin_util::math::position::BlockPos; type FenceGateProperties = pumpkin_data::block_properties::OakFenceGateLikeProperties; type FenceProperties = pumpkin_data::block_properties::OakFenceLikeProperties; -use crate::block::BlockBehaviour; -use crate::world::World; - #[pumpkin_block_from_tag("minecraft:fences")] pub struct FenceBlock; @@ -34,6 +29,10 @@ impl BlockBehaviour for FenceBlock { let fence_props = FenceProperties::from_state_id(args.state_id); compute_fence_state(fence_props, args.world, args.block, args.position) } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } pub fn compute_fence_state( diff --git a/crates/pumpkin/src/block/blocks/flower_pots.rs b/crates/pumpkin/src/block/blocks/flower_pots.rs index 2c8b8027d..d40782e86 100644 --- a/crates/pumpkin/src/block/blocks/flower_pots.rs +++ b/crates/pumpkin/src/block/blocks/flower_pots.rs @@ -1,8 +1,8 @@ use crate::block::registry::BlockActionResult; -use crate::block::{BlockBehaviour, RandomTickArgs, UseWithItemArgs}; +use crate::block::{BlockBehaviour, PathComputationType, RandomTickArgs, UseWithItemArgs}; use pumpkin_data::dimension::Dimension; use pumpkin_data::flower_pot_transformations::get_potted_item; -use pumpkin_data::{Block, BlockId}; +use pumpkin_data::{Block, BlockId, BlockState}; use pumpkin_macros::pumpkin_block_from_tag; use pumpkin_world::world::BlockFlags; @@ -61,4 +61,8 @@ impl BlockBehaviour for FlowerPotBlock { ); } } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } diff --git a/crates/pumpkin/src/block/blocks/glass_panes.rs b/crates/pumpkin/src/block/blocks/glass_panes.rs index b9764b76d..29387ebc3 100644 --- a/crates/pumpkin/src/block/blocks/glass_panes.rs +++ b/crates/pumpkin/src/block/blocks/glass_panes.rs @@ -1,19 +1,15 @@ -use crate::block::GetStateForNeighborUpdateArgs; -use crate::block::OnPlaceArgs; -use pumpkin_data::BlockDirection; -use pumpkin_data::BlockStateId; -use pumpkin_data::HorizontalFacingExt; +use crate::block::{ + BlockBehaviour, GetStateForNeighborUpdateArgs, OnPlaceArgs, PathComputationType, +}; +use crate::world::World; use pumpkin_data::block_properties::HorizontalFacing; use pumpkin_data::tag::Taggable; -use pumpkin_data::{Block, tag}; +use pumpkin_data::{Block, BlockDirection, BlockState, BlockStateId, HorizontalFacingExt, tag}; use pumpkin_macros::pumpkin_block_from_tag; use pumpkin_util::math::position::BlockPos; type GlassPaneProperties = pumpkin_data::block_properties::OakFenceLikeProperties; -use crate::block::BlockBehaviour; -use crate::world::World; - #[pumpkin_block_from_tag("c:glass_panes")] pub struct GlassPaneBlock; @@ -32,6 +28,10 @@ impl BlockBehaviour for GlassPaneBlock { let pane_props = GlassPaneProperties::from_state_id(args.state_id); compute_pane_state(pane_props, args.world, args.block, args.position) } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } pub fn compute_pane_state( diff --git a/crates/pumpkin/src/block/blocks/grindstone.rs b/crates/pumpkin/src/block/blocks/grindstone.rs index 3aab87852..a2f7197f0 100644 --- a/crates/pumpkin/src/block/blocks/grindstone.rs +++ b/crates/pumpkin/src/block/blocks/grindstone.rs @@ -17,10 +17,12 @@ use pumpkin_util::text::TextComponent; use pumpkin_world::inventory::SimpleInventory; use pumpkin_world::world::BlockAccessor; -use crate::block::BlockBehaviour; -use crate::block::CanPlaceAtArgs; use crate::block::registry::BlockActionResult; -use crate::block::{GetStateForNeighborUpdateArgs, NormalUseArgs, OnPlaceArgs}; +use crate::block::{ + BlockBehaviour, CanPlaceAtArgs, GetStateForNeighborUpdateArgs, NormalUseArgs, OnPlaceArgs, + PathComputationType, +}; +use pumpkin_data::BlockState; use super::abstract_wall_mounting::WallMountedBlock; @@ -63,6 +65,10 @@ impl BlockBehaviour for GrindstoneBlock { ) -> BlockStateId { WallMountedBlock::get_state_for_neighbor_update(self, args) } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } impl WallMountedBlock for GrindstoneBlock { diff --git a/crates/pumpkin/src/block/blocks/hopper.rs b/crates/pumpkin/src/block/blocks/hopper.rs index 4296b3ed8..98ce4aa4b 100644 --- a/crates/pumpkin/src/block/blocks/hopper.rs +++ b/crates/pumpkin/src/block/blocks/hopper.rs @@ -2,7 +2,9 @@ use std::sync::Arc; use std::sync::Mutex; use crate::block::blocks::redstone::block_receives_redstone_power; -use crate::block::{GetComparatorOutputArgs, OnNeighborUpdateArgs, OnPlaceArgs, PlacedArgs}; +use crate::block::{ + GetComparatorOutputArgs, OnNeighborUpdateArgs, OnPlaceArgs, PathComputationType, PlacedArgs, +}; use crate::block::{ registry::BlockActionResult, {BlockBehaviour, NormalUseArgs}, @@ -10,9 +12,8 @@ use crate::block::{ use crate::world::World; use crate::block::entities::hopper::HopperBlockEntity; -use pumpkin_data::BlockStateId; use pumpkin_data::block_properties::FacingHopper; -use pumpkin_data::{Block, BlockDirection, translation}; +use pumpkin_data::{Block, BlockDirection, BlockState, BlockStateId, translation}; use pumpkin_inventory::generic_container_screen_handler::create_hopper; use pumpkin_inventory::player::player_inventory::PlayerInventory; use pumpkin_inventory::screen_handler::{ @@ -111,6 +112,10 @@ impl BlockBehaviour for HopperBlock { None } } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } fn check_powered_state(world: &Arc, pos: &BlockPos, state_id: BlockStateId, block: &Block) { diff --git a/crates/pumpkin/src/block/blocks/iron_bars.rs b/crates/pumpkin/src/block/blocks/iron_bars.rs index d45d2de74..88073101b 100644 --- a/crates/pumpkin/src/block/blocks/iron_bars.rs +++ b/crates/pumpkin/src/block/blocks/iron_bars.rs @@ -1,19 +1,15 @@ -use crate::block::GetStateForNeighborUpdateArgs; -use crate::block::OnPlaceArgs; -use pumpkin_data::BlockDirection; -use pumpkin_data::BlockStateId; -use pumpkin_data::HorizontalFacingExt; +use crate::block::{ + BlockBehaviour, GetStateForNeighborUpdateArgs, OnPlaceArgs, PathComputationType, +}; +use crate::world::World; use pumpkin_data::block_properties::HorizontalFacing; use pumpkin_data::tag::Taggable; -use pumpkin_data::{Block, tag}; +use pumpkin_data::{Block, BlockDirection, BlockState, BlockStateId, HorizontalFacingExt, tag}; use pumpkin_macros::pumpkin_block; use pumpkin_util::math::position::BlockPos; type IronBarsProperties = pumpkin_data::block_properties::OakFenceLikeProperties; -use crate::block::BlockBehaviour; -use crate::world::World; - #[pumpkin_block("minecraft:iron_bars")] pub struct IronBarsBlock; @@ -32,6 +28,10 @@ impl BlockBehaviour for IronBarsBlock { let bars_props = IronBarsProperties::from_state_id(args.state_id); compute_bars_state(bars_props, args.world, args.block, args.position) } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } pub fn compute_bars_state( diff --git a/crates/pumpkin/src/block/blocks/lanterns.rs b/crates/pumpkin/src/block/blocks/lanterns.rs index d33d9fcda..8b603ce6a 100644 --- a/crates/pumpkin/src/block/blocks/lanterns.rs +++ b/crates/pumpkin/src/block/blocks/lanterns.rs @@ -1,10 +1,10 @@ use crate::block::{ - BlockBehaviour, CanPlaceAtArgs, GetStateForNeighborUpdateArgs, OnPlaceArgs, OnScheduledTickArgs, + BlockBehaviour, CanPlaceAtArgs, GetStateForNeighborUpdateArgs, OnPlaceArgs, + OnScheduledTickArgs, PathComputationType, }; use crate::world::World; -use pumpkin_data::BlockStateId; use pumpkin_data::tag::Taggable; -use pumpkin_data::{BlockDirection, tag}; +use pumpkin_data::{BlockDirection, BlockState, BlockStateId, tag}; use pumpkin_macros::pumpkin_block_from_tag; use pumpkin_util::math::position::BlockPos; use pumpkin_world::tick::TickPriority; @@ -48,6 +48,10 @@ impl BlockBehaviour for LanternBlock { .break_block(args.position, None, BlockFlags::empty()); } } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } fn can_place_at(world: &World, position: &BlockPos) -> bool { diff --git a/crates/pumpkin/src/block/blocks/lectern.rs b/crates/pumpkin/src/block/blocks/lectern.rs index 2adab5209..01f118f1a 100644 --- a/crates/pumpkin/src/block/blocks/lectern.rs +++ b/crates/pumpkin/src/block/blocks/lectern.rs @@ -6,7 +6,7 @@ use crate::block::registry::BlockActionResult; use crate::block::{ BlockBehaviour, BrokenArgs, EmitsRedstonePowerArgs, GetComparatorOutputArgs, GetRedstonePowerArgs, NormalUseArgs, OnPlaceArgs, OnScheduledTickArgs, OnStateReplacedArgs, - PlacedArgs, UseWithItemArgs, + PathComputationType, PlacedArgs, UseWithItemArgs, }; use crate::entity::Entity; use crate::entity::item::ItemEntity; @@ -16,7 +16,7 @@ use pumpkin_data::entity::EntityType; use pumpkin_data::sound::{Sound, SoundCategory}; use pumpkin_data::tag::Taggable; use pumpkin_data::world::WorldEvent; -use pumpkin_data::{Block, BlockDirection, BlockStateId, tag, translation}; +use pumpkin_data::{Block, BlockDirection, BlockState, BlockStateId, tag, translation}; use pumpkin_inventory::lectern_screen_handler::{LecternController, LecternScreenHandler}; use pumpkin_inventory::player::player_inventory::PlayerInventory; use pumpkin_inventory::screen_handler::{ @@ -296,4 +296,8 @@ impl BlockBehaviour for LecternBlock { Some(0) } } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } diff --git a/crates/pumpkin/src/block/blocks/piston/piston.rs b/crates/pumpkin/src/block/blocks/piston/piston.rs index 142a55cc0..68f67a6ff 100644 --- a/crates/pumpkin/src/block/blocks/piston/piston.rs +++ b/crates/pumpkin/src/block/blocks/piston/piston.rs @@ -17,7 +17,7 @@ use rustc_hash::FxHashMap; use crate::{ block::{ BlockBehaviour, BlockMetadata, BrokenArgs, OnNeighborUpdateArgs, OnPlaceArgs, - OnSyncedBlockEventArgs, PlacedArgs, + OnSyncedBlockEventArgs, PathComputationType, PlacedArgs, blocks::{piston::piston_head::PistonHeadProperties, redstone::is_emitting_redstone_power}, }, world::World, @@ -121,6 +121,10 @@ impl BlockBehaviour for PistonBlock { let block = Block::from_id(block_id); Self::handle_synced_block_event(block, args.world, args.position, args.r#type, args.data) } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } impl PistonBlock { diff --git a/crates/pumpkin/src/block/blocks/piston/piston_extension.rs b/crates/pumpkin/src/block/blocks/piston/piston_extension.rs index 0a1418a6d..6e66d8a11 100644 --- a/crates/pumpkin/src/block/blocks/piston/piston_extension.rs +++ b/crates/pumpkin/src/block/blocks/piston/piston_extension.rs @@ -1,9 +1,8 @@ -use pumpkin_data::{Block, FacingExt}; +use pumpkin_data::{Block, BlockState, FacingExt}; use pumpkin_macros::pumpkin_block; use pumpkin_world::world::BlockFlags; -use crate::block::BlockBehaviour; -use crate::block::BrokenArgs; +use crate::block::{BlockBehaviour, BrokenArgs, PathComputationType}; use super::piston::PistonProps; @@ -29,4 +28,8 @@ impl BlockBehaviour for PistonExtensionBlock { } } } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } diff --git a/crates/pumpkin/src/block/blocks/piston/piston_head.rs b/crates/pumpkin/src/block/blocks/piston/piston_head.rs index a6c51fb5c..a17a519d9 100644 --- a/crates/pumpkin/src/block/blocks/piston/piston_head.rs +++ b/crates/pumpkin/src/block/blocks/piston/piston_head.rs @@ -1,11 +1,10 @@ use pumpkin_data::block_properties::Facing; -use pumpkin_data::{Block, FacingExt}; +use pumpkin_data::{Block, BlockState, FacingExt}; use pumpkin_macros::pumpkin_block; use pumpkin_world::world::BlockFlags; -use crate::block::BlockBehaviour; use crate::block::blocks::piston::piston::try_move; -use crate::block::{BrokenArgs, OnNeighborUpdateArgs}; +use crate::block::{BlockBehaviour, BrokenArgs, OnNeighborUpdateArgs, PathComputationType}; use super::piston::PistonProps; @@ -54,4 +53,8 @@ impl BlockBehaviour for PistonHeadBlock { } } } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } diff --git a/crates/pumpkin/src/block/blocks/plant/bamboo.rs b/crates/pumpkin/src/block/blocks/plant/bamboo.rs index 18e4ee804..49b3849f1 100644 --- a/crates/pumpkin/src/block/blocks/plant/bamboo.rs +++ b/crates/pumpkin/src/block/blocks/plant/bamboo.rs @@ -1,20 +1,19 @@ use std::sync::Arc; -use pumpkin_data::BlockStateId; use pumpkin_data::block_properties::{BambooLeaves, BambooLikeProperties}; use pumpkin_data::tag::Block::MINECRAFT_SUPPORTS_BAMBOO; use pumpkin_data::tag::Taggable; use pumpkin_data::tag::{self}; -use pumpkin_data::{Block, BlockDirection}; +use pumpkin_data::{Block, BlockDirection, BlockState, BlockStateId}; use pumpkin_macros::pumpkin_block; use pumpkin_util::math::position::BlockPos; use pumpkin_world::tick::TickPriority; use pumpkin_world::world::{BlockAccessor, BlockFlags}; use rand::RngExt; -use crate::block::{BlockBehaviour, CanPlaceAtArgs, blocks::plant::PlantBlockBase}; use crate::block::{ - GetStateForNeighborUpdateArgs, OnPlaceArgs, OnScheduledTickArgs, RandomTickArgs, + BlockBehaviour, CanPlaceAtArgs, GetStateForNeighborUpdateArgs, OnPlaceArgs, + OnScheduledTickArgs, PathComputationType, RandomTickArgs, blocks::plant::PlantBlockBase, }; use crate::world::World; @@ -111,6 +110,10 @@ impl BlockBehaviour for BambooBlock { update_leaves_and_grow(args.world, args.position); } } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } fn update_leaves_and_grow(world: &Arc, position: &BlockPos) { diff --git a/crates/pumpkin/src/block/blocks/plant/cactus.rs b/crates/pumpkin/src/block/blocks/plant/cactus.rs index aa741deb2..d530b4f26 100644 --- a/crates/pumpkin/src/block/blocks/plant/cactus.rs +++ b/crates/pumpkin/src/block/blocks/plant/cactus.rs @@ -1,8 +1,7 @@ -use pumpkin_data::BlockStateId; use pumpkin_data::block_properties::CactusLikeProperties; use pumpkin_data::damage::DamageType; use pumpkin_data::tag::Taggable; -use pumpkin_data::{Block, BlockDirection, tag}; +use pumpkin_data::{Block, BlockDirection, BlockState, BlockStateId, tag}; use pumpkin_macros::pumpkin_block; use pumpkin_util::math::position::BlockPos; use pumpkin_world::tick::TickPriority; @@ -11,7 +10,7 @@ use rand::RngExt; use crate::block::{ BlockBehaviour, CanPlaceAtArgs, GetStateForNeighborUpdateArgs, OnEntityCollisionArgs, - OnScheduledTickArgs, RandomTickArgs, + OnScheduledTickArgs, PathComputationType, RandomTickArgs, }; #[pumpkin_block("minecraft:cactus")] @@ -94,6 +93,10 @@ impl BlockBehaviour for CactusBlock { fn can_place_at(&self, args: CanPlaceAtArgs<'_>) -> bool { can_place_at(args.block_accessor, args.position) } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } fn can_place_at(world: &dyn BlockAccessor, block_pos: &BlockPos) -> bool { diff --git a/crates/pumpkin/src/block/blocks/plant/chorus_plant.rs b/crates/pumpkin/src/block/blocks/plant/chorus_plant.rs index d667488fa..25f98bfbe 100644 --- a/crates/pumpkin/src/block/blocks/plant/chorus_plant.rs +++ b/crates/pumpkin/src/block/blocks/plant/chorus_plant.rs @@ -1,5 +1,5 @@ use pumpkin_data::{ - Block, BlockDirection, BlockStateId, + Block, BlockDirection, BlockState, BlockStateId, block_properties::BrownMushroomBlockLikeProperties, tag::{self, Taggable}, }; @@ -11,7 +11,8 @@ use pumpkin_world::{ }; use crate::block::{ - BlockBehaviour, CanPlaceAtArgs, GetStateForNeighborUpdateArgs, OnPlaceArgs, OnScheduledTickArgs, + BlockBehaviour, CanPlaceAtArgs, GetStateForNeighborUpdateArgs, OnPlaceArgs, + OnScheduledTickArgs, PathComputationType, }; #[pumpkin_block("minecraft:chorus_plant")] @@ -65,6 +66,10 @@ impl BlockBehaviour for ChorusPlantBlock { .break_block(args.position, None, BlockFlags::empty()); } } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } #[must_use] diff --git a/crates/pumpkin/src/block/blocks/plant/cocoa.rs b/crates/pumpkin/src/block/blocks/plant/cocoa.rs index c29c6d95b..e0c27f283 100644 --- a/crates/pumpkin/src/block/blocks/plant/cocoa.rs +++ b/crates/pumpkin/src/block/blocks/plant/cocoa.rs @@ -9,7 +9,7 @@ use pumpkin_world::world::{BlockAccessor, BlockFlags}; use crate::block::{ BlockBehaviour, BonemealArgs, CanPlaceAtArgs, GetStateForNeighborUpdateArgs, OnPlaceArgs, - RandomTickArgs, + PathComputationType, RandomTickArgs, }; use crate::entity::EntityBase; @@ -137,4 +137,8 @@ impl BlockBehaviour for CocoaBlock { props.facing = mirror.mirror_horizontal(props.facing); BlockState::from_id(props.to_state_id(block)) } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } diff --git a/crates/pumpkin/src/block/blocks/plant/sea_pickles.rs b/crates/pumpkin/src/block/blocks/plant/sea_pickles.rs index cd843fc88..23a395c3f 100644 --- a/crates/pumpkin/src/block/blocks/plant/sea_pickles.rs +++ b/crates/pumpkin/src/block/blocks/plant/sea_pickles.rs @@ -3,14 +3,13 @@ use crate::block::blocks::plant::PlantBlockBase; use crate::block::registry::BlockActionResult; use crate::block::{ BlockBehaviour, CanPlaceAtArgs, CanUpdateAtArgs, GetStateForNeighborUpdateArgs, OnPlaceArgs, - UseWithItemArgs, + PathComputationType, UseWithItemArgs, }; use crate::entity::EntityBase; -use pumpkin_data::BlockStateId; use pumpkin_data::entity::EntityPose; use pumpkin_data::item::Item; use pumpkin_data::tag::Taggable; -use pumpkin_data::{Block, BlockDirection, tag}; +use pumpkin_data::{Block, BlockDirection, BlockState, BlockStateId, tag}; use pumpkin_macros::pumpkin_block; use pumpkin_util::math::position::BlockPos; use pumpkin_world::world::BlockFlags; @@ -130,6 +129,10 @@ impl BlockBehaviour for SeaPickleBlock { args.state_id, ) } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } impl PlantBlockBase for SeaPickleBlock {} diff --git a/crates/pumpkin/src/block/blocks/powder_snow.rs b/crates/pumpkin/src/block/blocks/powder_snow.rs index edf595d90..8d6c41bea 100644 --- a/crates/pumpkin/src/block/blocks/powder_snow.rs +++ b/crates/pumpkin/src/block/blocks/powder_snow.rs @@ -1,3 +1,4 @@ +use pumpkin_data::BlockState; use pumpkin_data::data_component_impl::EquipmentSlot; use pumpkin_data::entity::EntityType; use pumpkin_data::item::Item; @@ -9,7 +10,7 @@ use pumpkin_util::math::position::BlockPos; use pumpkin_util::math::vector3::Vector3; use pumpkin_world::world::BlockFlags; -use crate::block::{BlockBehaviour, OnEntityCollisionArgs, OnLandedUponArgs}; +use crate::block::{BlockBehaviour, OnEntityCollisionArgs, OnLandedUponArgs, PathComputationType}; use crate::entity::EntityBase; #[pumpkin_block("minecraft:powder_snow")] @@ -117,4 +118,8 @@ impl BlockBehaviour for PowderSnowBlock { entity.extinguish(); } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + true + } } diff --git a/crates/pumpkin/src/block/blocks/redstone/bell.rs b/crates/pumpkin/src/block/blocks/redstone/bell.rs index 7d74157b0..b2a9c5dfd 100644 --- a/crates/pumpkin/src/block/blocks/redstone/bell.rs +++ b/crates/pumpkin/src/block/blocks/redstone/bell.rs @@ -5,16 +5,15 @@ use crate::block::entities::bell::BellBlockEntity; use crate::block::registry::BlockActionResult; use crate::block::{ BlockBehaviour, BlockHitResult, BrokenArgs, CanPlaceAtArgs, NormalUseArgs, - OnNeighborUpdateArgs, OnPlaceArgs, PlacedArgs, + OnNeighborUpdateArgs, OnPlaceArgs, PathComputationType, PlacedArgs, }; use crate::world::World; -use pumpkin_data::BlockStateId; use pumpkin_data::block_properties::HorizontalFacing; use pumpkin_data::block_properties::{AttachFace, BellAttachment, BellLikeProperties}; use pumpkin_data::sound::Sound; use pumpkin_data::sound::SoundCategory; use pumpkin_data::tag::Taggable; -use pumpkin_data::{Block, BlockDirection}; +use pumpkin_data::{Block, BlockDirection, BlockState, BlockStateId}; use pumpkin_data::{HorizontalFacingExt, tag}; use pumpkin_macros::pumpkin_block; use pumpkin_util::math::position::BlockPos; @@ -215,4 +214,8 @@ impl BlockBehaviour for BellBlock { } } } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } diff --git a/crates/pumpkin/src/block/blocks/redstone/lightning_rod.rs b/crates/pumpkin/src/block/blocks/redstone/lightning_rod.rs index d29ef2454..688674e07 100644 --- a/crates/pumpkin/src/block/blocks/redstone/lightning_rod.rs +++ b/crates/pumpkin/src/block/blocks/redstone/lightning_rod.rs @@ -2,10 +2,11 @@ use std::sync::Arc; use crate::block::{ BlockBehaviour, EmitsRedstonePowerArgs, GetRedstonePowerArgs, OnPlaceArgs, OnScheduledTickArgs, + PathComputationType, }; use crate::world::World; use pumpkin_data::block_properties::LightningRodLikeProperties; -use pumpkin_data::{BlockStateId, FacingExt}; +use pumpkin_data::{BlockState, BlockStateId, FacingExt}; use pumpkin_macros::pumpkin_block; use pumpkin_util::math::position::BlockPos; use pumpkin_world::tick::TickPriority; @@ -75,4 +76,8 @@ impl BlockBehaviour for LightningRodBlock { ); } } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } diff --git a/crates/pumpkin/src/block/blocks/redstone/sculk_sensor.rs b/crates/pumpkin/src/block/blocks/redstone/sculk_sensor.rs index 331c7cca2..9ad3b7940 100644 --- a/crates/pumpkin/src/block/blocks/redstone/sculk_sensor.rs +++ b/crates/pumpkin/src/block/blocks/redstone/sculk_sensor.rs @@ -4,14 +4,14 @@ use crate::block::entities::calibrated_sculk_sensor::CalibratedSculkSensorBlockE use crate::block::entities::sculk_sensor::SculkSensorBlockEntity; use crate::block::{ BlockBehaviour, BlockMetadata, EmitsRedstonePowerArgs, GetComparatorOutputArgs, - GetRedstonePowerArgs, OnPlaceArgs, OnScheduledTickArgs, PlacedArgs, + GetRedstonePowerArgs, OnPlaceArgs, OnScheduledTickArgs, PathComputationType, PlacedArgs, }; use crate::world::World; use pumpkin_data::block_properties::{ CalibratedSculkSensorLikeProperties, HorizontalFacing, SculkSensorLikeProperties, SculkSensorPhase, }; -use pumpkin_data::{Block, BlockDirection, BlockId, BlockStateId}; +use pumpkin_data::{Block, BlockDirection, BlockId, BlockState, BlockStateId}; use pumpkin_util::math::position::BlockPos; use pumpkin_world::tick::TickPriority; use pumpkin_world::world::BlockFlags; @@ -224,4 +224,8 @@ impl BlockBehaviour for SculkSensorBlock { } } } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } diff --git a/crates/pumpkin/src/block/blocks/respawn_anchor.rs b/crates/pumpkin/src/block/blocks/respawn_anchor.rs index ca58f20e3..2189c767f 100644 --- a/crates/pumpkin/src/block/blocks/respawn_anchor.rs +++ b/crates/pumpkin/src/block/blocks/respawn_anchor.rs @@ -2,12 +2,12 @@ use pumpkin_data::block_properties::RespawnAnchorLikeProperties; use pumpkin_data::dimension::Dimension; use pumpkin_data::item::Item; use pumpkin_data::sound::{Sound, SoundCategory}; -use pumpkin_data::translation; +use pumpkin_data::{BlockState, translation}; use pumpkin_macros::pumpkin_block; use pumpkin_world::world::BlockFlags; use crate::block::registry::BlockActionResult; -use crate::block::{BlockBehaviour, NormalUseArgs, UseWithItemArgs}; +use crate::block::{BlockBehaviour, NormalUseArgs, PathComputationType, UseWithItemArgs}; use crate::entity::EntityBase; #[pumpkin_block("minecraft:respawn_anchor")] @@ -91,4 +91,8 @@ impl BlockBehaviour for RespawnAnchorBlock { BlockActionResult::SuccessServer } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } diff --git a/crates/pumpkin/src/block/blocks/shelf.rs b/crates/pumpkin/src/block/blocks/shelf.rs index 1bbe93aec..8608df957 100644 --- a/crates/pumpkin/src/block/blocks/shelf.rs +++ b/crates/pumpkin/src/block/blocks/shelf.rs @@ -1,9 +1,9 @@ -use pumpkin_data::BlockStateId; use pumpkin_data::block_properties::AcaciaShelfLikeProperties; +use pumpkin_data::{BlockState, BlockStateId}; use pumpkin_macros::pumpkin_block_from_tag; use crate::block::entities::shelf::ShelfBlockEntity; -use crate::block::{BlockBehaviour, OnPlaceArgs, PlacedArgs}; +use crate::block::{BlockBehaviour, OnPlaceArgs, PathComputationType, PlacedArgs}; use crate::entity::EntityBase; use std::sync::Arc; @@ -26,4 +26,8 @@ impl BlockBehaviour for ShelfBlock { args.world.add_block_entity(Arc::new(entity)); } } + + fn is_pathfindable(&self, state: &BlockState, computation_type: PathComputationType) -> bool { + computation_type == PathComputationType::Water && state.is_waterlogged() + } } diff --git a/crates/pumpkin/src/block/blocks/skull_block.rs b/crates/pumpkin/src/block/blocks/skull_block.rs index 63b03002a..5d749f700 100644 --- a/crates/pumpkin/src/block/blocks/skull_block.rs +++ b/crates/pumpkin/src/block/blocks/skull_block.rs @@ -1,11 +1,14 @@ use crate::block::blocks::redstone::block_receives_redstone_power; -use crate::block::{BlockBehaviour, BlockMetadata, OnNeighborUpdateArgs, OnPlaceArgs}; +use crate::block::entities::skull::SkullBlockEntity; +use crate::block::{ + BlockBehaviour, BlockMetadata, OnNeighborUpdateArgs, OnPlaceArgs, PathComputationType, + PlacedArgs, +}; use crate::entity::EntityBase; -use pumpkin_data::BlockId; -use pumpkin_data::BlockStateId; +use pumpkin_data::block_properties::SkeletonSkullLikeProperties; +use pumpkin_data::{BlockId, BlockState, BlockStateId}; use pumpkin_world::world::BlockFlags; - -type SkeletonSkullLikeProperties = pumpkin_data::block_properties::SkeletonSkullLikeProperties; +use std::sync::Arc; pub struct SkullBlock; @@ -23,10 +26,6 @@ impl BlockMetadata for SkullBlock { } } -use crate::block::PlacedArgs; -use crate::block::entities::skull::SkullBlockEntity; -use std::sync::Arc; - impl BlockBehaviour for SkullBlock { fn placed(&self, args: PlacedArgs<'_>) { { @@ -57,4 +56,8 @@ impl BlockBehaviour for SkullBlock { } } } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } diff --git a/crates/pumpkin/src/block/blocks/slabs.rs b/crates/pumpkin/src/block/blocks/slabs.rs index 0a0376af7..5bdc1313f 100644 --- a/crates/pumpkin/src/block/blocks/slabs.rs +++ b/crates/pumpkin/src/block/blocks/slabs.rs @@ -1,12 +1,10 @@ -use pumpkin_data::BlockDirection; -use pumpkin_data::BlockStateId; use pumpkin_data::block_properties::SlabType; +use pumpkin_data::{BlockDirection, BlockState, BlockStateId}; use pumpkin_macros::pumpkin_block_from_tag; -use crate::block::BlockBehaviour; -use crate::block::BlockIsReplacing; -use crate::block::CanUpdateAtArgs; -use crate::block::OnPlaceArgs; +use crate::block::{ + BlockBehaviour, BlockIsReplacing, CanUpdateAtArgs, OnPlaceArgs, PathComputationType, +}; type SlabProperties = pumpkin_data::block_properties::ResinBrickSlabLikeProperties; @@ -49,4 +47,11 @@ impl BlockBehaviour for SlabBlock { }, } } + + fn is_pathfindable(&self, state: &BlockState, computation_type: PathComputationType) -> bool { + match computation_type { + PathComputationType::Water => state.is_waterlogged(), + PathComputationType::Land | PathComputationType::Air => false, + } + } } diff --git a/crates/pumpkin/src/block/blocks/sniffer_egg.rs b/crates/pumpkin/src/block/blocks/sniffer_egg.rs index 2ab29fa5c..7de24800a 100644 --- a/crates/pumpkin/src/block/blocks/sniffer_egg.rs +++ b/crates/pumpkin/src/block/blocks/sniffer_egg.rs @@ -2,12 +2,14 @@ use pumpkin_data::block_properties::SnifferEggLikeProperties; use pumpkin_data::item::Item; use pumpkin_data::item_stack::ItemStack; use pumpkin_data::sound::{Sound, SoundCategory}; -use pumpkin_data::{BlockId, BlockStateId}; +use pumpkin_data::{BlockId, BlockState, BlockStateId}; use pumpkin_macros::pumpkin_block; use pumpkin_world::tick::TickPriority; use pumpkin_world::world::BlockFlags; -use crate::block::{BlockBehaviour, BrokenArgs, OnPlaceArgs, OnScheduledTickArgs, PlacedArgs}; +use crate::block::{ + BlockBehaviour, BrokenArgs, OnPlaceArgs, OnScheduledTickArgs, PathComputationType, PlacedArgs, +}; #[pumpkin_block("minecraft:sniffer_egg")] pub struct SnifferEggBlock; @@ -94,4 +96,8 @@ impl BlockBehaviour for SnifferEggBlock { .drop_stack(args.position, ItemStack::new(1, &Item::SNIFFER_EGG)); } } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } diff --git a/crates/pumpkin/src/block/blocks/snow.rs b/crates/pumpkin/src/block/blocks/snow.rs index 0cc2cb7a8..d93b93127 100644 --- a/crates/pumpkin/src/block/blocks/snow.rs +++ b/crates/pumpkin/src/block/blocks/snow.rs @@ -1,5 +1,7 @@ use pumpkin_data::tag::Taggable; -use pumpkin_data::{Block, BlockStateId, block_properties::SnowLikeProperties, item::Item, tag}; +use pumpkin_data::{ + Block, BlockState, BlockStateId, block_properties::SnowLikeProperties, item::Item, tag, +}; use pumpkin_macros::pumpkin_block; use pumpkin_util::math::position::BlockPos; use pumpkin_world::{ @@ -9,7 +11,7 @@ use pumpkin_world::{ use crate::block::{ BlockBehaviour, GetStateForNeighborUpdateArgs, OnPlaceArgs, OnScheduledTickArgs, - RandomTickArgs, UseWithItemArgs, registry::BlockActionResult, + PathComputationType, RandomTickArgs, UseWithItemArgs, registry::BlockActionResult, }; #[pumpkin_block("minecraft:snow")] @@ -90,6 +92,11 @@ impl BlockBehaviour for LayeredSnowBlock { } args.state_id } + + fn is_pathfindable(&self, state: &BlockState, computation_type: PathComputationType) -> bool { + computation_type == PathComputationType::Land + && SnowLikeProperties::from_state_id(state.id).layers < 5 + } } fn can_place_at(block_accessor: &dyn BlockAccessor, position: &BlockPos) -> bool { diff --git a/crates/pumpkin/src/block/blocks/stairs.rs b/crates/pumpkin/src/block/blocks/stairs.rs index 193f8e8a9..07d882907 100644 --- a/crates/pumpkin/src/block/blocks/stairs.rs +++ b/crates/pumpkin/src/block/blocks/stairs.rs @@ -8,9 +8,7 @@ use pumpkin_macros::pumpkin_block_from_tag; use pumpkin_util::math::position::BlockPos; use pumpkin_world::world::BlockFlags; -use crate::block::BlockBehaviour; -use crate::block::OnNeighborUpdateArgs; -use crate::block::OnPlaceArgs; +use crate::block::{BlockBehaviour, OnNeighborUpdateArgs, OnPlaceArgs, PathComputationType}; use crate::world::World; type StairsProperties = pumpkin_data::block_properties::OakStairsLikeProperties; @@ -117,6 +115,10 @@ impl BlockBehaviour for StairBlock { BlockState::from_id(state_id) } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } fn compute_stair_shape( diff --git a/crates/pumpkin/src/block/blocks/stonecutter.rs b/crates/pumpkin/src/block/blocks/stonecutter.rs index 29e39dd6d..92e3f9e60 100644 --- a/crates/pumpkin/src/block/blocks/stonecutter.rs +++ b/crates/pumpkin/src/block/blocks/stonecutter.rs @@ -1,7 +1,7 @@ use crate::block::registry::BlockActionResult; -use crate::block::{BlockBehaviour, NormalUseArgs}; +use crate::block::{BlockBehaviour, NormalUseArgs, PathComputationType}; -use pumpkin_data::translation; +use pumpkin_data::{BlockState, translation}; use pumpkin_inventory::player::player_inventory::PlayerInventory; use pumpkin_inventory::screen_handler::{ InventoryPlayer, ScreenHandlerFactory, SharedScreenHandler, @@ -28,6 +28,10 @@ impl BlockBehaviour for StonecutterBlock { BlockActionResult::Success } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } struct StonecutterScreenFactory; diff --git a/crates/pumpkin/src/block/blocks/trapdoor.rs b/crates/pumpkin/src/block/blocks/trapdoor.rs index f7a9b8bfe..84cde49d5 100644 --- a/crates/pumpkin/src/block/blocks/trapdoor.rs +++ b/crates/pumpkin/src/block/blocks/trapdoor.rs @@ -1,15 +1,16 @@ use crate::block::blocks::redstone::block_receives_redstone_power; use crate::block::registry::BlockActionResult; -use crate::block::{BlockBehaviour, NormalUseArgs, OnNeighborUpdateArgs, OnPlaceArgs}; +use crate::block::{ + BlockBehaviour, NormalUseArgs, OnNeighborUpdateArgs, OnPlaceArgs, PathComputationType, +}; use crate::entity::EntityBase; use crate::entity::player::Player; use crate::world::World; use pumpkin_data::BlockDirection; -use pumpkin_data::BlockStateId; use pumpkin_data::block_properties::Half; use pumpkin_data::sound::{Sound, SoundCategory}; use pumpkin_data::tag::Taggable; -use pumpkin_data::{Block, tag}; +use pumpkin_data::{Block, BlockState, BlockStateId, tag}; use pumpkin_macros::pumpkin_block_from_tag; use pumpkin_util::math::position::BlockPos; use pumpkin_world::world::BlockFlags; @@ -135,4 +136,12 @@ impl BlockBehaviour for TrapDoorBlock { ); } } + + fn is_pathfindable(&self, state: &BlockState, computation_type: PathComputationType) -> bool { + let props = TrapDoorProperties::from_state_id(state.id); + match computation_type { + PathComputationType::Land | PathComputationType::Air => props.open, + PathComputationType::Water => props.waterlogged, + } + } } diff --git a/crates/pumpkin/src/block/blocks/walls.rs b/crates/pumpkin/src/block/blocks/walls.rs index 1557cb873..950733de4 100644 --- a/crates/pumpkin/src/block/blocks/walls.rs +++ b/crates/pumpkin/src/block/blocks/walls.rs @@ -1,20 +1,17 @@ -use crate::block::GetStateForNeighborUpdateArgs; -use crate::block::OnPlaceArgs; -use pumpkin_data::BlockDirection; -use pumpkin_data::BlockState; -use pumpkin_data::BlockStateId; +use crate::block::{ + BlockBehaviour, GetStateForNeighborUpdateArgs, OnPlaceArgs, PathComputationType, +}; +use crate::world::World; use pumpkin_data::block_properties::EastWall; use pumpkin_data::block_properties::HorizontalFacing; use pumpkin_data::block_properties::NorthWall; use pumpkin_data::block_properties::SouthWall; use pumpkin_data::block_properties::WestWall; use pumpkin_data::tag::Taggable; -use pumpkin_data::{Block, tag}; +use pumpkin_data::{Block, BlockDirection, BlockState, BlockStateId, tag}; use pumpkin_macros::pumpkin_block_from_tag; use pumpkin_util::math::position::BlockPos; -use crate::block::BlockBehaviour; -use crate::world::World; type FenceGateProperties = pumpkin_data::block_properties::OakFenceGateLikeProperties; type FenceLikeProperties = pumpkin_data::block_properties::OakFenceLikeProperties; type WallProperties = pumpkin_data::block_properties::ResinBrickWallLikeProperties; @@ -37,6 +34,10 @@ impl BlockBehaviour for WallBlock { let wall_props = WallProperties::from_state_id(args.state_id); compute_wall_state(wall_props, args.world, args.block, args.position) } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } pub fn compute_wall_state( diff --git a/crates/pumpkin/src/block/blocks/weathering_copper.rs b/crates/pumpkin/src/block/blocks/weathering_copper.rs index 294550ac2..2f10d34d7 100644 --- a/crates/pumpkin/src/block/blocks/weathering_copper.rs +++ b/crates/pumpkin/src/block/blocks/weathering_copper.rs @@ -20,7 +20,7 @@ use crate::block::registry::BlockActionResult; use crate::block::{ BlockBehaviour, BlockMetadata, BrokenArgs, CanPlaceAtArgs, CanUpdateAtArgs, GetStateForNeighborUpdateArgs, NormalUseArgs, OnNeighborUpdateArgs, OnPlaceArgs, - OnStateReplacedArgs, PlacedArgs, RandomTickArgs, + OnStateReplacedArgs, PathComputationType, PlacedArgs, RandomTickArgs, }; use crate::world::World; @@ -710,6 +710,10 @@ impl BlockBehaviour for WeatheringCopperStairBlock { fn random_tick(&self, args: RandomTickArgs<'_>) { change_over_time(args.world, args.position, args.block); } + + fn is_pathfindable(&self, state: &BlockState, computation_type: PathComputationType) -> bool { + StairBlock.is_pathfindable(state, computation_type) + } } /// Weathering copper trapdoor blocks. @@ -768,6 +772,10 @@ impl BlockBehaviour for WeatheringCopperTrapDoorBlock { fn random_tick(&self, args: RandomTickArgs<'_>) { change_over_time(args.world, args.position, args.block); } + + fn is_pathfindable(&self, state: &BlockState, computation_type: PathComputationType) -> bool { + TrapDoorBlock.is_pathfindable(state, computation_type) + } } /// Weathering copper slab blocks. @@ -822,6 +830,10 @@ impl BlockBehaviour for WeatheringCopperSlabBlock { fn random_tick(&self, args: RandomTickArgs<'_>) { change_over_time(args.world, args.position, args.block); } + + fn is_pathfindable(&self, state: &BlockState, computation_type: PathComputationType) -> bool { + SlabBlock.is_pathfindable(state, computation_type) + } } /// Weathering copper door blocks. @@ -907,6 +919,10 @@ impl BlockBehaviour for WeatheringCopperDoorBlock { change_over_time(args.world, args.position, args.block); } } + + fn is_pathfindable(&self, state: &BlockState, computation_type: PathComputationType) -> bool { + DoorBlock.is_pathfindable(state, computation_type) + } } /// Weathering copper grate blocks. diff --git a/crates/pumpkin/src/block/blocks/wither_skull.rs b/crates/pumpkin/src/block/blocks/wither_skull.rs index 342635a6a..4764fed4e 100644 --- a/crates/pumpkin/src/block/blocks/wither_skull.rs +++ b/crates/pumpkin/src/block/blocks/wither_skull.rs @@ -1,9 +1,14 @@ -use pumpkin_data::{Block, BlockDirection, BlockStateId, entity::EntityType, world::WorldEvent}; +use pumpkin_data::{ + Block, BlockDirection, BlockState, BlockStateId, entity::EntityType, world::WorldEvent, +}; use pumpkin_macros::pumpkin_block; use pumpkin_world::world::BlockFlags; use crate::{ - block::{BlockBehaviour, OnPlaceArgs, PlacedArgs, blocks::skull_block::SkullBlock}, + block::{ + BlockBehaviour, OnPlaceArgs, PathComputationType, PlacedArgs, + blocks::skull_block::SkullBlock, + }, entity::{Entity, boss::wither::WitherEntity}, }; @@ -92,4 +97,8 @@ impl BlockBehaviour for WitherSkeletonSkullBlock { } } } + + fn is_pathfindable(&self, _state: &BlockState, _computation_type: PathComputationType) -> bool { + false + } } diff --git a/crates/pumpkin/src/block/mod.rs b/crates/pumpkin/src/block/mod.rs index 4fb15065e..9a287f4a4 100644 --- a/crates/pumpkin/src/block/mod.rs +++ b/crates/pumpkin/src/block/mod.rs @@ -1,3 +1,5 @@ +use pumpkin_data::fluid::Fluid; +use pumpkin_data::tag::{self, Taggable}; use pumpkin_data::{Block, BlockId, BlockState}; use pumpkin_data::BlockStateId; @@ -28,6 +30,13 @@ use pumpkin_util::math::boundingbox::BoundingBox; use pumpkin_util::math::vector3::Vector3; use pumpkin_world::world::{BlockAccessor, BlockFlags}; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum PathComputationType { + Land, + Water, + Air, +} + pub trait BlockMetadata { fn ids() -> Box<[BlockId]>; } @@ -185,6 +194,17 @@ pub trait BlockBehaviour: Send + Sync { ) -> &'static BlockState { block.rotate(state_id, rotation) } + + fn is_pathfindable(&self, state: &BlockState, computation_type: PathComputationType) -> bool { + match computation_type { + PathComputationType::Water => { + state.is_waterlogged() + || Fluid::from_state_id(state.id) + .is_some_and(|f| f.has_tag(&tag::Fluid::MINECRAFT_WATER)) + } + PathComputationType::Land | PathComputationType::Air => !state.is_full_cube(), + } + } } #[derive(Clone, Copy)] diff --git a/crates/pumpkin/src/block/registry.rs b/crates/pumpkin/src/block/registry.rs index f92c6d10d..26be0fe67 100644 --- a/crates/pumpkin/src/block/registry.rs +++ b/crates/pumpkin/src/block/registry.rs @@ -160,7 +160,8 @@ use crate::block::fluid::water::FlowingWater; use crate::block::{ BlockBehaviour, BlockHitResult, BlockMetadata, BonemealArgs, FluidMetadata, GetInsideCollisionShapeArgs, OnEntityCollisionArgs, OnEntityStepArgs, OnLandedUponArgs, - OnProjectileHitArgs, UpdateEntityMovementAfterFallOnArgs, stop_vertical_movement_after_fall, + OnProjectileHitArgs, PathComputationType, UpdateEntityMovementAfterFallOnArgs, + stop_vertical_movement_after_fall, }; use crate::entity::EntityBase; use crate::entity::player::Player; @@ -172,6 +173,7 @@ use pumpkin_data::data_component_impl::EquipmentSlot; use pumpkin_data::fluid::Fluid; use pumpkin_data::item::Item; use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::tag::{self, Taggable}; use pumpkin_data::{Block, BlockDirection, BlockId, BlockState}; use pumpkin_protocol::java::server::play::SUseItemOn; use pumpkin_util::math::boundingbox::BoundingBox; @@ -1385,4 +1387,24 @@ impl BlockRegistry { |pumpkin_block| pumpkin_block.rotate(block, state_id, rotation), ) } + + #[must_use] + pub fn is_pathfindable( + &self, + block: &Block, + state: &BlockState, + computation_type: PathComputationType, + ) -> bool { + self.get_pumpkin_block(block.id).map_or_else( + || match computation_type { + PathComputationType::Water => { + state.is_waterlogged() + || Fluid::from_state_id(state.id) + .is_some_and(|f| f.has_tag(&tag::Fluid::MINECRAFT_WATER)) + } + PathComputationType::Land | PathComputationType::Air => !state.is_full_cube(), + }, + |pumpkin_block| pumpkin_block.is_pathfindable(state, computation_type), + ) + } } diff --git a/crates/pumpkin/src/entity/ai/control/body_rotation_control.rs b/crates/pumpkin/src/entity/ai/control/body_rotation_control.rs new file mode 100644 index 000000000..aded0361f --- /dev/null +++ b/crates/pumpkin/src/entity/ai/control/body_rotation_control.rs @@ -0,0 +1,85 @@ +use crate::entity::ai::control::Control; +use crate::entity::mob::Mob; +use pumpkin_util::math::rotate_if_necessary; + +pub struct BodyRotationControl { + head_stable_time: i32, + last_stable_y_head_rot: f32, +} + +impl Default for BodyRotationControl { + fn default() -> Self { + Self::new() + } +} + +impl BodyRotationControl { + #[must_use] + pub const fn new() -> Self { + Self { + head_stable_time: 0, + last_stable_y_head_rot: 0.0, + } + } + + pub fn client_tick(&mut self, mob: &dyn Mob) { + let mob_entity = mob.get_mob_entity(); + let entity = &mob_entity.living_entity.entity; + + if Self::is_moving(mob) { + entity.body_yaw.store(entity.yaw.load()); + Self::rotate_head_if_necessary(mob); + self.last_stable_y_head_rot = entity.head_yaw.load(); + self.head_stable_time = 0; + } else if (entity.head_yaw.load() - self.last_stable_y_head_rot).abs() > 15.0 { + self.head_stable_time = 0; + self.last_stable_y_head_rot = entity.head_yaw.load(); + Self::rotate_body_if_necessary(mob); + } else { + self.head_stable_time += 1; + if self.head_stable_time > 10 { + self.rotate_head_towards_front(mob); + } + } + } + + fn rotate_body_if_necessary(mob: &dyn Mob) { + let entity = &mob.get_mob_entity().living_entity.entity; + let max_head_y_rot = mob.get_max_head_rotation(); + entity.body_yaw.store(rotate_if_necessary( + entity.body_yaw.load(), + entity.head_yaw.load(), + max_head_y_rot, + )); + } + + fn rotate_head_if_necessary(mob: &dyn Mob) { + let entity = &mob.get_mob_entity().living_entity.entity; + let max_head_y_rot = mob.get_max_head_rotation(); + entity.head_yaw.store(rotate_if_necessary( + entity.head_yaw.load(), + entity.body_yaw.load(), + max_head_y_rot, + )); + } + + fn rotate_head_towards_front(&self, mob: &dyn Mob) { + let time_since_starting = self.head_stable_time - 10; + let face_forward_fraction = (time_since_starting as f32 / 10.0).clamp(0.0, 1.0); + let angle_remaining = mob.get_max_head_rotation() * (1.0 - face_forward_fraction); + let entity = &mob.get_mob_entity().living_entity.entity; + entity.body_yaw.store(rotate_if_necessary( + entity.body_yaw.load(), + entity.head_yaw.load(), + angle_remaining, + )); + } + + fn is_moving(mob: &dyn Mob) -> bool { + let entity = &mob.get_mob_entity().living_entity.entity; + let vel = entity.velocity.load(); + vel.x * vel.x + vel.z * vel.z > 2.500_000_3e-7 + } +} + +impl Control for BodyRotationControl {} diff --git a/crates/pumpkin/src/entity/ai/control/flying_move_control.rs b/crates/pumpkin/src/entity/ai/control/flying_move_control.rs new file mode 100644 index 000000000..41fcb3e9c --- /dev/null +++ b/crates/pumpkin/src/entity/ai/control/flying_move_control.rs @@ -0,0 +1,113 @@ +use crate::entity::ai::control::move_control::Operation; +use crate::entity::ai::control::{Control, MoveControlTrait}; +use crate::entity::mob::Mob; +use pumpkin_data::attributes::Attributes; +use pumpkin_util::math::vector3::Vector3; +use std::sync::atomic::Ordering; + +pub struct FlyingMoveControl { + pub wanted_x: f64, + pub wanted_y: f64, + pub wanted_z: f64, + pub speed_modifier: f64, + pub max_turn: i32, + pub hovers_in_place: bool, + pub operation: Operation, +} + +impl FlyingMoveControl { + #[must_use] + pub const fn new(max_turn: i32, hovers_in_place: bool) -> Self { + Self { + wanted_x: 0.0, + wanted_y: 0.0, + wanted_z: 0.0, + speed_modifier: 0.0, + max_turn, + hovers_in_place, + operation: Operation::Wait, + } + } +} + +impl Control for FlyingMoveControl {} + +impl MoveControlTrait for FlyingMoveControl { + fn tick(&mut self, mob: &dyn Mob) { + let mob_entity = mob.get_mob_entity(); + let living_entity = &mob_entity.living_entity; + let entity = &living_entity.entity; + + if self.operation == Operation::MoveTo { + self.operation = Operation::Wait; + entity.set_has_no_gravity(true); + + let pos = entity.pos.load(); + let xd = self.wanted_x - pos.x; + let yd = self.wanted_y - pos.y; + let zd = self.wanted_z - pos.z; + let dd = xd * xd + yd * yd + zd * zd; + + if dd < 2.5000003E-7 { + living_entity + .movement_input + .store(Vector3::new(0.0, 0.0, 0.0)); + return; + } + + let y_rot_d = (zd.atan2(xd).to_degrees() as f32) - 90.0; + entity + .yaw + .store(self.change_angle(entity.yaw.load(), y_rot_d, 90.0)); + + let speed = if entity.on_ground.load(Ordering::Relaxed) { + let movement_speed = living_entity.get_attribute_value(&Attributes::MOVEMENT_SPEED); + (self.speed_modifier * movement_speed) as f32 + } else { + let flying_speed = living_entity.get_attribute_value(&Attributes::FLYING_SPEED); + (self.speed_modifier * flying_speed) as f32 + }; + + let sd = xd.hypot(zd); + if yd.abs() > 1.0E-5 || sd > 1.0E-5 { + let x_rot_d = -((yd.atan2(sd).to_degrees()) as f32); + entity.pitch.store(self.change_angle( + entity.pitch.load(), + x_rot_d, + self.max_turn as f32, + )); + let yya = if yd > 0.0 { + speed as f64 + } else { + -(speed as f64) + }; + living_entity + .movement_input + .store(Vector3::new(0.0, yya, speed as f64)); + } else { + living_entity + .movement_input + .store(Vector3::new(0.0, 0.0, speed as f64)); + } + } else { + if !self.hovers_in_place { + entity.set_has_no_gravity(false); + } + living_entity + .movement_input + .store(Vector3::new(0.0, 0.0, 0.0)); + } + } + + fn set_wanted_position(&mut self, x: f64, y: f64, z: f64, speed_modifier: f64) { + self.wanted_x = x; + self.wanted_y = y; + self.wanted_z = z; + self.speed_modifier = speed_modifier; + self.operation = Operation::MoveTo; + } + + fn has_wanted(&self) -> bool { + self.operation == Operation::MoveTo + } +} diff --git a/crates/pumpkin/src/entity/ai/control/jump_control.rs b/crates/pumpkin/src/entity/ai/control/jump_control.rs new file mode 100644 index 000000000..96f03a523 --- /dev/null +++ b/crates/pumpkin/src/entity/ai/control/jump_control.rs @@ -0,0 +1,24 @@ +use crate::entity::ai::control::Control; +use crate::entity::mob::Mob; +use std::sync::atomic::Ordering; + +#[derive(Default)] +pub struct JumpControl { + jump: bool, +} + +impl Control for JumpControl {} + +impl JumpControl { + pub const fn jump(&mut self) { + self.jump = true; + } + + pub fn tick(&mut self, mob: &dyn Mob) { + mob.get_mob_entity() + .living_entity + .jumping + .store(self.jump, Ordering::SeqCst); + self.jump = false; + } +} diff --git a/crates/pumpkin/src/entity/ai/control/mod.rs b/crates/pumpkin/src/entity/ai/control/mod.rs index 17e00f514..2ec530a7c 100644 --- a/crates/pumpkin/src/entity/ai/control/mod.rs +++ b/crates/pumpkin/src/entity/ai/control/mod.rs @@ -1,8 +1,13 @@ use crate::entity::mob::Mob; use pumpkin_util::math::subtract_angles; +pub mod body_rotation_control; +pub mod flying_move_control; +pub mod jump_control; pub mod look_control; pub mod move_control; +pub mod smooth_swimming_look_control; +pub mod smooth_swimming_move_control; pub trait Control: Send + Sync { fn change_angle(&self, start: f32, end: f32, max_change: f32) -> f32 { diff --git a/crates/pumpkin/src/entity/ai/control/smooth_swimming_look_control.rs b/crates/pumpkin/src/entity/ai/control/smooth_swimming_look_control.rs new file mode 100644 index 000000000..f353bdf3a --- /dev/null +++ b/crates/pumpkin/src/entity/ai/control/smooth_swimming_look_control.rs @@ -0,0 +1,113 @@ +use crate::entity::ai::control::Control; +use crate::entity::mob::Mob; +use pumpkin_util::math::vector3::Vector3; +use pumpkin_util::math::wrap_degrees; + +pub struct SmoothSwimmingLookControl { + pub max_yaw_change: f32, + pub max_pitch_change: f32, + pub look_at_timer: i32, + pub position: Vector3, + pub max_y_rot_from_center: i32, +} + +impl SmoothSwimmingLookControl { + #[must_use] + pub const fn new(max_y_rot_from_center: i32) -> Self { + Self { + max_yaw_change: 0.0, + max_pitch_change: 0.0, + look_at_timer: 0, + position: Vector3::new(0.0, 0.0, 0.0), + max_y_rot_from_center, + } + } + + pub const fn look_at_with_range( + &mut self, + x: f64, + y: f64, + z: f64, + max_yaw_change: f32, + max_pitch_change: f32, + ) { + self.position = Vector3::new(x, y, z); + self.max_yaw_change = max_yaw_change; + self.max_pitch_change = max_pitch_change; + self.look_at_timer = 2; + } + + pub fn tick(&mut self, mob: &dyn Mob) { + let mob_entity = mob.get_mob_entity(); + let entity = &mob_entity.living_entity.entity; + + if self.look_at_timer > 0 { + self.look_at_timer -= 1; + if let Some(yaw) = self.get_target_yaw(mob) { + entity.head_yaw.store(self.change_angle( + entity.head_yaw.load(), + yaw + 20.0, + self.max_yaw_change, + )); + } + if let Some(pitch) = self.get_target_pitch(mob) { + entity.set_pitch(self.change_angle( + entity.pitch.load(), + pitch + 10.0, + self.max_pitch_change, + )); + } + } else { + let is_idle = mob_entity + .navigator + .try_lock() + .is_ok_and(|navigator| navigator.is_idle()); + if is_idle { + entity.set_pitch(self.change_angle(entity.pitch.load(), 0.0, 5.0)); + } + entity.head_yaw.store(self.change_angle( + entity.head_yaw.load(), + entity.body_yaw.load(), + self.max_yaw_change, + )); + } + + let head_diff_body = wrap_degrees(entity.head_yaw.load() - entity.body_yaw.load()); + let max_rot = self.max_y_rot_from_center as f32; + if head_diff_body < -max_rot { + let body_yaw = entity.body_yaw.load(); + entity.body_yaw.store(body_yaw - 4.0); + } else if head_diff_body > max_rot { + let body_yaw = entity.body_yaw.load(); + entity.body_yaw.store(body_yaw + 4.0); + } + } + + fn get_target_pitch(&self, mob: &dyn Mob) -> Option { + let position = self.position; + let mob_position = mob.get_entity().pos.load(); + let d = position.x - mob_position.x; + let e = position.y - mob.get_entity().get_eye_y(); + let f = position.z - mob_position.z; + let g = d.hypot(f); + if e.abs() <= 1.0E-5 && g.abs() <= 1.0E-5 { + None + } else { + Some(-(e.atan2(g) as f32).to_degrees()) + } + } + + fn get_target_yaw(&self, mob: &dyn Mob) -> Option { + let position = self.position; + let mob_position = mob.get_entity().pos.load(); + let d = position.x - mob_position.x; + let e = position.z - mob_position.z; + if e.abs() <= 1.0E-5 && d.abs() <= 1.0E-5 { + None + } else { + Some((e.atan2(d) as f32).to_degrees() - 90.0) + } + } +} + +impl Control for SmoothSwimmingLookControl {} diff --git a/crates/pumpkin/src/entity/ai/control/smooth_swimming_move_control.rs b/crates/pumpkin/src/entity/ai/control/smooth_swimming_move_control.rs new file mode 100644 index 000000000..6b0c6ebc9 --- /dev/null +++ b/crates/pumpkin/src/entity/ai/control/smooth_swimming_move_control.rs @@ -0,0 +1,138 @@ +use crate::entity::ai::control::move_control::Operation; +use crate::entity::ai::control::{Control, MoveControlTrait}; +use crate::entity::mob::Mob; +use pumpkin_data::attributes::Attributes; +use pumpkin_util::math::vector3::Vector3; +use pumpkin_util::math::wrap_degrees; +use std::sync::atomic::Ordering; + +pub struct SmoothSwimmingMoveControl { + pub wanted_x: f64, + pub wanted_y: f64, + pub wanted_z: f64, + pub speed_modifier: f64, + pub max_turn_x: i32, + pub max_turn_y: i32, + pub in_water_speed_modifier: f32, + pub outside_water_speed_modifier: f32, + pub apply_gravity: bool, + pub operation: Operation, +} + +impl SmoothSwimmingMoveControl { + #[must_use] + pub const fn new( + max_turn_x: i32, + max_turn_y: i32, + in_water_speed_modifier: f32, + outside_water_speed_modifier: f32, + apply_gravity: bool, + ) -> Self { + Self { + wanted_x: 0.0, + wanted_y: 0.0, + wanted_z: 0.0, + speed_modifier: 0.0, + max_turn_x, + max_turn_y, + in_water_speed_modifier, + outside_water_speed_modifier, + apply_gravity, + operation: Operation::Wait, + } + } + + fn get_turning_speed_factor(left_to_turn: f32) -> f32 { + 1.0 - ((left_to_turn - 10.0) / 50.0).clamp(0.0, 1.0) + } +} + +impl Control for SmoothSwimmingMoveControl {} + +impl MoveControlTrait for SmoothSwimmingMoveControl { + fn tick(&mut self, mob: &dyn Mob) { + let mob_entity = mob.get_mob_entity(); + let living_entity = &mob_entity.living_entity; + let entity = &living_entity.entity; + + if self.apply_gravity && entity.touching_water.load(Ordering::Relaxed) { + let vel = entity.velocity.load(); + entity.set_velocity(Vector3::new(vel.x, vel.y + 0.005, vel.z)); + } + + let is_idle = mob_entity + .navigator + .try_lock() + .is_ok_and(|navigator| navigator.is_idle()); + + if self.operation == Operation::MoveTo && !is_idle { + let pos = entity.pos.load(); + let xd = self.wanted_x - pos.x; + let yd = self.wanted_y - pos.y; + let zd = self.wanted_z - pos.z; + let dd = xd * xd + yd * yd + zd * zd; + + if dd < 2.5000003E-7 { + living_entity + .movement_input + .store(Vector3::new(0.0, 0.0, 0.0)); + return; + } + + let y_rot_d = (zd.atan2(xd).to_degrees() as f32) - 90.0; + let current_yaw = entity.yaw.load(); + let new_yaw = self.change_angle(current_yaw, y_rot_d, self.max_turn_y as f32); + entity.yaw.store(new_yaw); + entity.body_yaw.store(new_yaw); + entity.head_yaw.store(new_yaw); + + let movement_speed = living_entity.get_attribute_value(&Attributes::MOVEMENT_SPEED); + let speed = (self.speed_modifier * movement_speed) as f32; + + if entity.touching_water.load(Ordering::Relaxed) { + let water_speed = speed * self.in_water_speed_modifier; + let sqrt = xd.hypot(zd); + if yd.abs() > 1.0E-5 || sqrt > 1.0E-5 { + let mut x_rot_d = -((yd.atan2(sqrt).to_degrees()) as f32); + x_rot_d = wrap_degrees(x_rot_d) + .clamp(-(self.max_turn_x as f32), self.max_turn_x as f32); + entity + .pitch + .store(self.change_angle(entity.pitch.load(), x_rot_d, 5.0)); + } + + let pitch_rad = entity.pitch.load().to_radians(); + let cos = pitch_rad.cos(); + let sin = pitch_rad.sin(); + living_entity.movement_input.store(Vector3::new( + 0.0, + -(sin * water_speed) as f64, + (cos * water_speed) as f64, + )); + } else { + let left_to_turn = wrap_degrees(entity.yaw.load() - y_rot_d).abs(); + let factor = Self::get_turning_speed_factor(left_to_turn); + let land_speed = speed * self.outside_water_speed_modifier * factor; + living_entity + .movement_input + .store(Vector3::new(0.0, 0.0, land_speed as f64)); + } + } else { + living_entity + .movement_input + .store(Vector3::new(0.0, 0.0, 0.0)); + } + } + + fn set_wanted_position(&mut self, x: f64, y: f64, z: f64, speed_modifier: f64) { + self.wanted_x = x; + self.wanted_y = y; + self.wanted_z = z; + self.speed_modifier = speed_modifier; + self.operation = Operation::MoveTo; + } + + fn has_wanted(&self) -> bool { + self.operation == Operation::MoveTo + } +} diff --git a/crates/pumpkin/src/entity/ai/goal/blaze_attack.rs b/crates/pumpkin/src/entity/ai/goal/blaze_attack.rs index 9ef281af9..97d87533d 100644 --- a/crates/pumpkin/src/entity/ai/goal/blaze_attack.rs +++ b/crates/pumpkin/src/entity/ai/goal/blaze_attack.rs @@ -1,6 +1,4 @@ use pumpkin_protocol::java::client::play::CWorldEvent; -use pumpkin_util::math::vector3::Vector3; -use rand::RngExt; use std::sync::Arc; use crate::entity::{ @@ -30,7 +28,6 @@ impl BlazeShootFireballGoal { } const fn get_follow_distance() -> f64 { - // TODO: use FOLLOW_RANGE 48.0 } } @@ -41,12 +38,7 @@ impl Goal for BlazeShootFireballGoal { return false; }; let target = blaze.entity.get_target(); - if target.is_some() { - // TODO: check is_alive - true - } else { - false - } + target.is_some_and(|t| t.get_entity().is_alive()) } fn should_continue(&self, _mob: &dyn Mob) -> bool { @@ -54,12 +46,7 @@ impl Goal for BlazeShootFireballGoal { return false; }; let target = blaze.entity.get_target(); - if target.is_some() { - // TODO: check is_alive - true - } else { - false - } + target.is_some_and(|t| t.get_entity().is_alive()) } fn start(&mut self, _mob: &dyn Mob) { @@ -89,7 +76,6 @@ impl Goal for BlazeShootFireballGoal { return; }; - // TODO: hasLineOfSight check let has_line_of_sight = true; if has_line_of_sight { @@ -114,15 +100,9 @@ impl Goal for BlazeShootFireballGoal { if self.attack_time <= 0 { self.attack_time = 20; - // TODO: doHurtTarget + blaze.entity.try_attack(&*blaze, &*target); } - - // TODO: set wanted position to target } else if distance_sq < Self::get_follow_distance().powi(2) && has_line_of_sight { - let target_y_offset = target_pos.y + 0.5; // roughly target.getY(0.5) - let blaze_y_offset = blaze_pos.y + 0.5; // roughly blaze.getY(0.5) - let yd = target_y_offset - blaze_y_offset; - if self.attack_time <= 0 { self.attack_step += 1; if self.attack_step == 1 { @@ -137,9 +117,6 @@ impl Goal for BlazeShootFireballGoal { } if self.attack_step > 1 { - let distance = distance_sq.sqrt(); - let sqd = distance.sqrt() * 0.5; - // play shoot sound let chunk_pos = blaze.entity.living_entity.entity.chunk_pos.load(); blaze .entity @@ -157,50 +134,33 @@ impl Goal for BlazeShootFireballGoal { ), ); - for _ in 0..1 { - // Vanilla loops 1 time - // Calculate spread - let _direction = { - let mut rng = rand::rng(); - let dir_x = (dx - 2.297 * sqd) - + rng.random_range(0.0..1.0) * (2.297 * sqd * 2.0); - let dir_z = (dz - 2.297 * sqd) - + rng.random_range(0.0..1.0) * (2.297 * sqd * 2.0); - Vector3::new(dir_x, yd, dir_z).normalize() - }; + let world = blaze.entity.living_entity.entity.world.load_full(); + let uuid = uuid::Uuid::new_v4(); - // Spawn SmallFireball - let world = blaze.entity.living_entity.entity.world.load_full(); - let uuid = uuid::Uuid::new_v4(); + let mut pos = blaze.entity.living_entity.entity.pos.load(); + pos.y += blaze.entity.living_entity.entity.get_eye_height() - 0.1; - let mut pos = blaze.entity.living_entity.entity.pos.load(); - pos.y += blaze.entity.living_entity.entity.get_eye_height() - 0.1; + let base_entity = Entity::from_uuid( + uuid, + world.clone(), + pos, + &pumpkin_data::entity::EntityType::SMALL_FIREBALL, + ); - let base_entity = Entity::from_uuid( - uuid, - world.clone(), - pos, - &pumpkin_data::entity::EntityType::SMALL_FIREBALL, - ); - - let fireball = SmallFireballEntity::new_shot( - base_entity, - &blaze.entity.living_entity.entity, - ); - world.spawn_entity(Arc::new(fireball)); - } + let fireball = SmallFireballEntity::new_shot( + base_entity, + &blaze.entity.living_entity.entity, + ); + world.spawn_entity(Arc::new(fireball)); } } - // Look at target blaze .entity .look_control .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .look_at_entity(&*blaze, &target); - } else if self.last_seen < 5 { - // TODO: set wanted position to target } } diff --git a/crates/pumpkin/src/entity/ai/goal/break_door.rs b/crates/pumpkin/src/entity/ai/goal/break_door.rs index 994efdfe1..a49793fae 100644 --- a/crates/pumpkin/src/entity/ai/goal/break_door.rs +++ b/crates/pumpkin/src/entity/ai/goal/break_door.rs @@ -178,42 +178,3 @@ impl Goal for BreakDoorGoal { Controls::empty() } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn door_break_time_default_and_custom() { - let default_goal = BreakDoorGoal::default(); - assert_eq!(default_goal.get_door_break_time(), 240); - - let custom_goal = - BreakDoorGoal::with_door_break_time(300, Arc::new(|d| d == Difficulty::Hard)); - assert_eq!(custom_goal.get_door_break_time(), 300); - - let low_goal = - BreakDoorGoal::with_door_break_time(100, Arc::new(|d| d == Difficulty::Hard)); - assert_eq!(low_goal.get_door_break_time(), 240); - } - - #[test] - fn valid_difficulty() { - let hard_only = BreakDoorGoal::new(Arc::new(|d| d == Difficulty::Hard)); - assert!(hard_only.is_valid_difficulty(Difficulty::Hard)); - assert!(!hard_only.is_valid_difficulty(Difficulty::Normal)); - assert!(!hard_only.is_valid_difficulty(Difficulty::Easy)); - assert!(!hard_only.is_valid_difficulty(Difficulty::Peaceful)); - } - - #[test] - fn break_progress_calculation() { - let break_time = 120; - let total_time = 240; - let progress = (break_time as f32 / total_time as f32 * 10.0) as i32; - assert_eq!(progress, 5); - - let end_progress = (240.0f32 / 240.0f32 * 10.0) as i32; - assert_eq!(end_progress, 10); - } -} diff --git a/crates/pumpkin/src/entity/ai/goal/door_interact.rs b/crates/pumpkin/src/entity/ai/goal/door_interact.rs index 600ad27de..40054833e 100644 --- a/crates/pumpkin/src/entity/ai/goal/door_interact.rs +++ b/crates/pumpkin/src/entity/ai/goal/door_interact.rs @@ -154,44 +154,3 @@ impl Goal for DoorInteractGoal { Controls::empty() } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn passed_when_crossing_door() { - let mut goal = DoorInteractGoal::new(); - goal.door_pos = BlockPos::new(10, 64, 10); - - // Simulate start: mob at (10.5, 64.0, 8.0), moving towards door at Z=10.5 - // door_open_dir = (10.5 - 10.5, 10.5 - 8.0) = (0.0, 2.5) - goal.door_open_dir_x = 0.0; - goal.door_open_dir_z = 2.5; - goal.passed = false; - - assert!(goal.can_continue_to_use()); - - // Still before door: mob at (10.5, 64.0, 9.5) -> new_door_dir = (0.0, 1.0) - // dot = 0*0 + 2.5*1.0 = 2.5 > 0 - let new_x = 0.0f32; - let new_z = 1.0f32; - let dot = goal.door_open_dir_x * new_x + goal.door_open_dir_z * new_z; - if dot < 0.0 { - goal.passed = true; - } - assert!(!goal.passed); - assert!(goal.can_continue_to_use()); - - // Crossed door: mob at (10.5, 64.0, 12.0) -> new_door_dir = (0.0, -1.5) - // dot = 0*0 + 2.5*(-1.5) = -3.75 < 0 - let new_x2 = 0.0f32; - let new_z2 = -1.5f32; - let dot2 = goal.door_open_dir_x * new_x2 + goal.door_open_dir_z * new_z2; - if dot2 < 0.0 { - goal.passed = true; - } - assert!(goal.passed); - assert!(!goal.can_continue_to_use()); - } -} diff --git a/crates/pumpkin/src/entity/ai/goal/flee_sun.rs b/crates/pumpkin/src/entity/ai/goal/flee_sun.rs new file mode 100644 index 000000000..074b45134 --- /dev/null +++ b/crates/pumpkin/src/entity/ai/goal/flee_sun.rs @@ -0,0 +1,118 @@ +use super::{Controls, Goal}; +use crate::entity::ai::pathfinder::NavigatorGoal; +use crate::entity::mob::Mob; +use pumpkin_data::data_component_impl::EquipmentSlot; +use pumpkin_util::math::position::BlockPos; +use pumpkin_util::math::vector3::Vector3; +use rand::RngExt; + +pub struct FleeSunGoal { + goal_control: Controls, + speed_modifier: f64, + wanted_x: f64, + wanted_y: f64, + wanted_z: f64, +} + +impl FleeSunGoal { + #[must_use] + pub const fn new(speed_modifier: f64) -> Self { + Self { + goal_control: Controls::MOVE, + speed_modifier, + wanted_x: 0.0, + wanted_y: 0.0, + wanted_z: 0.0, + } + } + + fn find_hide_pos(mob: &dyn Mob) -> Option> { + let mob_pos = mob.get_entity().block_pos.load(); + let world = mob.get_entity().world.load(); + let mut rng = mob.get_random(); + + for _ in 0..10 { + let offset_x = rng.random_range(-10..=10); + let offset_y = rng.random_range(-3..=3); + let offset_z = rng.random_range(-10..=10); + let check_pos = BlockPos::new( + mob_pos.0.x + offset_x, + mob_pos.0.y + offset_y, + mob_pos.0.z + offset_z, + ); + + let block_at = world.get_block_state(&check_pos); + let block_above = world.get_block_state(&check_pos.up()); + let block_below = world.get_block_state(&check_pos.down()); + + if !block_at.is_solid() && !block_above.is_solid() && block_below.is_solid() { + return Some(Vector3::new( + f64::from(check_pos.0.x) + 0.5, + f64::from(check_pos.0.y), + f64::from(check_pos.0.z) + 0.5, + )); + } + } + + None + } +} + +impl Goal for FleeSunGoal { + fn can_start(&mut self, mob: &dyn Mob) -> bool { + if mob.get_mob_entity().get_target().is_some() { + return false; + } + + if !mob.get_mob_entity().living_entity.entity.is_on_fire() { + return false; + } + + let has_helmet = mob + .get_mob_entity() + .living_entity + .entity_equipment + .try_lock() + .is_ok_and(|eq| !eq.get(&EquipmentSlot::HEAD).is_empty()); + + if has_helmet { + return false; + } + + if let Some(pos) = Self::find_hide_pos(mob) { + self.wanted_x = pos.x; + self.wanted_y = pos.y; + self.wanted_z = pos.z; + true + } else { + false + } + } + + fn should_continue(&self, mob: &dyn Mob) -> bool { + let is_idle = mob + .get_mob_entity() + .navigator + .try_lock() + .is_ok_and(|nav| nav.is_idle()); + !is_idle + } + + fn start(&mut self, mob: &dyn Mob) { + let mob_pos = mob.get_entity().pos.load(); + let mut navigator = mob + .get_mob_entity() + .navigator + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + navigator.set_progress(NavigatorGoal::new( + mob_pos, + Vector3::new(self.wanted_x, self.wanted_y, self.wanted_z), + self.speed_modifier, + )); + } + + fn controls(&self) -> Controls { + self.goal_control + } +} diff --git a/crates/pumpkin/src/entity/ai/goal/follow_mob.rs b/crates/pumpkin/src/entity/ai/goal/follow_mob.rs new file mode 100644 index 000000000..9f7b59f0c --- /dev/null +++ b/crates/pumpkin/src/entity/ai/goal/follow_mob.rs @@ -0,0 +1,131 @@ +use super::{Controls, Goal, to_goal_ticks}; +use crate::entity::EntityBase; +use crate::entity::ai::pathfinder::NavigatorGoal; +use crate::entity::mob::Mob; +use std::sync::Arc; + +pub struct FollowMobGoal { + goal_control: Controls, + following_mob: Option>, + speed_modifier: f64, + time_to_recalc_path: i32, + stop_distance_sq: f64, + area_size: f64, +} + +impl FollowMobGoal { + #[must_use] + pub fn new(speed_modifier: f64, stop_distance: f32, area_size: f32) -> Self { + Self { + goal_control: Controls::MOVE | Controls::LOOK, + following_mob: None, + speed_modifier, + time_to_recalc_path: 0, + stop_distance_sq: f64::from(stop_distance * stop_distance), + area_size: f64::from(area_size), + } + } +} + +impl Goal for FollowMobGoal { + fn can_start(&mut self, mob: &dyn Mob) -> bool { + let my_type = mob.get_entity().entity_type; + let my_uuid = mob.get_entity().entity_uuid; + let pos = mob.get_entity().pos.load(); + let world = mob.get_entity().world.load(); + + let nearby = world.get_nearby_entities(pos, self.area_size); + for candidate in nearby.values() { + let c_entity = candidate.get_entity(); + if c_entity.entity_uuid == my_uuid { + continue; + } + if c_entity.entity_type == my_type { + continue; + } + if candidate.get_living_entity().is_some() { + self.following_mob = Some(candidate.clone()); + return true; + } + } + + false + } + + fn should_continue(&self, mob: &dyn Mob) -> bool { + let Some(following) = &self.following_mob else { + return false; + }; + + if !following.get_entity().is_alive() { + return false; + } + + let is_idle = mob + .get_mob_entity() + .navigator + .try_lock() + .is_ok_and(|nav| nav.is_idle()); + + let mob_pos = mob.get_entity().pos.load(); + let follow_pos = following.get_entity().pos.load(); + let dist_sq = mob_pos.squared_distance_to_vec(&follow_pos); + + !is_idle && dist_sq > self.stop_distance_sq + } + + fn start(&mut self, _mob: &dyn Mob) { + self.time_to_recalc_path = 0; + } + + fn stop(&mut self, mob: &dyn Mob) { + self.following_mob = None; + let mut navigator = mob + .get_mob_entity() + .navigator + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + navigator.stop(); + } + + fn tick(&mut self, mob: &dyn Mob) { + let Some(following) = &self.following_mob else { + return; + }; + + let mob_entity = mob.get_mob_entity(); + let follow_pos = following.get_entity().pos.load(); + let mob_pos = mob.get_entity().pos.load(); + + mob_entity + .look_control + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .look_at_entity(mob, following); + + self.time_to_recalc_path -= 1; + if self.time_to_recalc_path <= 0 { + self.time_to_recalc_path = to_goal_ticks(10); + let dist_sq = mob_pos.squared_distance_to_vec(&follow_pos); + + let mut navigator = mob_entity + .navigator + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + + if dist_sq > self.stop_distance_sq { + navigator.set_progress(NavigatorGoal::new( + mob_pos, + follow_pos, + self.speed_modifier, + )); + } else { + navigator.stop(); + } + } + } + + fn controls(&self) -> Controls { + self.goal_control + } +} diff --git a/crates/pumpkin/src/entity/ai/goal/interact.rs b/crates/pumpkin/src/entity/ai/goal/interact.rs new file mode 100644 index 000000000..f1d4d1be9 --- /dev/null +++ b/crates/pumpkin/src/entity/ai/goal/interact.rs @@ -0,0 +1,53 @@ +use super::look_at_entity::LookAtEntityGoal; +use super::{Controls, Goal}; +use crate::entity::mob::Mob; +use pumpkin_data::entity::EntityType; +use std::sync::Weak; + +pub struct InteractGoal { + look_at_goal: LookAtEntityGoal, +} + +impl InteractGoal { + #[must_use] + pub fn new( + mob_weak: Weak, + target_type: &'static EntityType, + range: f32, + chance: f32, + ) -> Self { + Self { + look_at_goal: LookAtEntityGoal::new(mob_weak, target_type, range, chance, false), + } + } +} + +impl Goal for InteractGoal { + fn can_start(&mut self, mob: &dyn Mob) -> bool { + self.look_at_goal.can_start(mob) + } + + fn should_continue(&self, mob: &dyn Mob) -> bool { + self.look_at_goal.should_continue(mob) + } + + fn start(&mut self, mob: &dyn Mob) { + self.look_at_goal.start(mob); + } + + fn stop(&mut self, mob: &dyn Mob) { + self.look_at_goal.stop(mob); + } + + fn tick(&mut self, mob: &dyn Mob) { + self.look_at_goal.tick(mob); + } + + fn should_run_every_tick(&self) -> bool { + self.look_at_goal.should_run_every_tick() + } + + fn controls(&self) -> Controls { + Controls::LOOK | Controls::MOVE + } +} diff --git a/crates/pumpkin/src/entity/ai/goal/leap_at_target.rs b/crates/pumpkin/src/entity/ai/goal/leap_at_target.rs new file mode 100644 index 000000000..3df6cc144 --- /dev/null +++ b/crates/pumpkin/src/entity/ai/goal/leap_at_target.rs @@ -0,0 +1,101 @@ +use super::{Controls, Goal, to_goal_ticks}; +use crate::entity::EntityBase; +use crate::entity::mob::Mob; +use pumpkin_util::math::vector3::Vector3; +use rand::RngExt; +use std::sync::Arc; +use std::sync::atomic::Ordering; + +pub struct LeapAtTargetGoal { + goal_control: Controls, + yd: f32, + target: Option>, +} + +impl LeapAtTargetGoal { + #[must_use] + pub const fn new(yd: f32) -> Self { + Self { + goal_control: Controls::MOVE.union(Controls::JUMP), + yd, + target: None, + } + } +} + +impl Goal for LeapAtTargetGoal { + fn can_start(&mut self, mob: &dyn Mob) -> bool { + let mob_entity = mob.get_mob_entity(); + let entity = &mob_entity.living_entity.entity; + + if !entity.on_ground.load(Ordering::Relaxed) { + return false; + } + + let target = mob_entity.get_target(); + let Some(target) = target else { + return false; + }; + + if !target.get_entity().is_alive() { + return false; + } + + let mob_pos = entity.pos.load(); + let target_pos = target.get_entity().pos.load(); + let dist_sq = mob_pos.squared_distance_to_vec(&target_pos); + + if !(4.0..=16.0).contains(&dist_sq) { + return false; + } + + if mob.get_random().random_range(0..to_goal_ticks(5).max(1)) != 0 { + return false; + } + + self.target = Some(target); + true + } + + fn should_continue(&self, mob: &dyn Mob) -> bool { + let entity = &mob.get_mob_entity().living_entity.entity; + !entity.on_ground.load(Ordering::Relaxed) + } + + fn start(&mut self, mob: &dyn Mob) { + let Some(target) = &self.target else { + return; + }; + + let mob_entity = mob.get_mob_entity(); + let entity = &mob_entity.living_entity.entity; + let mob_pos = entity.pos.load(); + let target_pos = target.get_entity().pos.load(); + let movement = entity.velocity.load(); + + let dx = target_pos.x - mob_pos.x; + let dz = target_pos.z - mob_pos.z; + let dist_xz_sq = dx * dx + dz * dz; + + let delta = if dist_xz_sq > 1.0E-7 { + let dist_xz = dist_xz_sq.sqrt(); + Vector3::new( + (dx / dist_xz) * 0.4 + movement.x * 0.2, + f64::from(self.yd), + (dz / dist_xz) * 0.4 + movement.z * 0.2, + ) + } else { + Vector3::new(movement.x, f64::from(self.yd), movement.z) + }; + + entity.set_velocity(delta); + } + + fn stop(&mut self, _mob: &dyn Mob) { + self.target = None; + } + + fn controls(&self) -> Controls { + self.goal_control + } +} diff --git a/crates/pumpkin/src/entity/ai/goal/mod.rs b/crates/pumpkin/src/entity/ai/goal/mod.rs index a6327afe7..b93310bb3 100644 --- a/crates/pumpkin/src/entity/ai/goal/mod.rs +++ b/crates/pumpkin/src/entity/ai/goal/mod.rs @@ -15,14 +15,20 @@ pub mod destroy_egg; pub mod door_interact; pub mod eat_grass; pub mod escape_danger; +pub mod flee_sun; +pub mod follow_mob; pub mod follow_owner; pub mod follow_parent; pub mod goal_selector; +pub mod interact; +pub mod leap_at_target; pub mod look_around; pub mod look_at_entity; pub mod melee_attack; pub mod move_to_target_pos; +pub mod move_towards_restriction; pub mod move_towards_target; +pub mod ocelot_attack; pub mod offer_flower; pub mod open_door; pub mod owner_hurt_by_target; @@ -32,7 +38,10 @@ pub mod pick_up_block; pub mod place_block; pub mod ranged_attack; pub mod ranged_crossbow_attack; +pub mod restrict_sun; pub mod revenge; +pub mod run_around_like_crazy; +pub mod sit_when_ordered_to; pub mod step_and_destroy_block; pub mod swim; pub mod teleport_towards_player; @@ -40,7 +49,9 @@ pub mod tempt; pub(crate) mod track_target; pub mod trade_with_player; pub mod try_find_water; +pub mod use_item; pub mod wander_around; +pub mod water_avoiding_random_flying; pub mod work_at_job_site; pub mod zombie_attack; @@ -138,6 +149,11 @@ impl Controls { (self.0 & control.0) != 0 } + #[must_use] + pub const fn union(self, other: Self) -> Self { + Self(self.0 | other.0) + } + #[must_use] pub const fn idx(&self) -> usize { self.0.trailing_zeros() as usize diff --git a/crates/pumpkin/src/entity/ai/goal/move_towards_restriction.rs b/crates/pumpkin/src/entity/ai/goal/move_towards_restriction.rs new file mode 100644 index 000000000..622cfb013 --- /dev/null +++ b/crates/pumpkin/src/entity/ai/goal/move_towards_restriction.rs @@ -0,0 +1,114 @@ +use super::{Controls, Goal}; +use crate::entity::ai::pathfinder::NavigatorGoal; +use crate::entity::mob::Mob; +use pumpkin_util::math::position::BlockPos; +use pumpkin_util::math::vector3::Vector3; +use rand::RngExt; + +pub struct MoveTowardsRestrictionGoal { + goal_control: Controls, + speed_modifier: f64, + wanted_x: f64, + wanted_y: f64, + wanted_z: f64, +} + +impl MoveTowardsRestrictionGoal { + #[must_use] + pub const fn new(speed_modifier: f64) -> Self { + Self { + goal_control: Controls::MOVE, + speed_modifier, + wanted_x: 0.0, + wanted_y: 0.0, + wanted_z: 0.0, + } + } + + fn find_pos_towards(mob: &dyn Mob, target_pos: BlockPos) -> Option> { + let mob_pos = mob.get_entity().pos.load(); + let world = mob.get_entity().world.load(); + let mut rng = mob.get_random(); + + let dx = f64::from(target_pos.0.x) + 0.5 - mob_pos.x; + let dz = f64::from(target_pos.0.z) + 0.5 - mob_pos.z; + let base_angle = dz.atan2(dx) - std::f64::consts::FRAC_PI_2; + + for _ in 0..10 { + let angle = + base_angle + (2.0 * rng.random_range(0.0..1.0) - 1.0) * std::f64::consts::FRAC_PI_2; + let t = rng.random_range(0.0..1.0f64).sqrt(); + let dist = t * 16.0 * std::f64::consts::SQRT_2; + let step_x = -dist * angle.sin(); + let step_z = dist * angle.cos(); + let step_y = rng.random_range(-7..=7); + + let candidate = BlockPos::new( + (mob_pos.x + step_x) as i32, + (mob_pos.y + step_y as f64) as i32, + (mob_pos.z + step_z) as i32, + ); + + let block_at = world.get_block_state(&candidate); + let block_below = world.get_block_state(&candidate.down()); + + if !block_at.is_solid() && block_below.is_solid() { + return Some(Vector3::new( + f64::from(candidate.0.x) + 0.5, + f64::from(candidate.0.y), + f64::from(candidate.0.z) + 0.5, + )); + } + } + + None + } +} + +impl Goal for MoveTowardsRestrictionGoal { + fn can_start(&mut self, mob: &dyn Mob) -> bool { + let mob_entity = mob.get_mob_entity(); + let block_pos = mob.get_entity().block_pos.load(); + + if mob_entity.is_in_position_target_range_pos(&block_pos) { + return false; + } + + let home_pos = mob_entity.position_target.load(); + let Some(pos) = Self::find_pos_towards(mob, home_pos) else { + return false; + }; + + self.wanted_x = pos.x; + self.wanted_y = pos.y; + self.wanted_z = pos.z; + true + } + + fn should_continue(&self, mob: &dyn Mob) -> bool { + let is_idle = mob + .get_mob_entity() + .navigator + .try_lock() + .is_ok_and(|nav| nav.is_idle()); + !is_idle + } + + fn start(&mut self, mob: &dyn Mob) { + let mob_pos = mob.get_entity().pos.load(); + let mut navigator = mob + .get_mob_entity() + .navigator + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + navigator.set_progress(NavigatorGoal::new( + mob_pos, + Vector3::new(self.wanted_x, self.wanted_y, self.wanted_z), + self.speed_modifier, + )); + } + + fn controls(&self) -> Controls { + self.goal_control + } +} diff --git a/crates/pumpkin/src/entity/ai/goal/move_towards_target.rs b/crates/pumpkin/src/entity/ai/goal/move_towards_target.rs index 557fde256..c71821b5d 100644 --- a/crates/pumpkin/src/entity/ai/goal/move_towards_target.rs +++ b/crates/pumpkin/src/entity/ai/goal/move_towards_target.rs @@ -172,37 +172,3 @@ impl Goal for MoveTowardsTargetGoal { self.goal_control } } - -#[cfg(test)] -#[allow(clippy::unimplemented)] -mod tests { - use super::MoveTowardsTargetGoal; - use crate::entity::ai::goal::{Controls, Goal}; - - #[test] - fn initial_controls_and_state() { - let mut goal = MoveTowardsTargetGoal::new(1.0, 16.0); - assert_eq!(goal.controls(), Controls::MOVE); - assert!(goal.target.is_none()); - assert!(goal.wanted_pos.is_none()); - - goal.wanted_pos = Some(pumpkin_util::math::vector3::Vector3::new(1.0, 2.0, 3.0)); - assert!(goal.wanted_pos.is_some()); - goal.stop(&MockMob); - assert!(goal.target.is_none()); - assert!(goal.wanted_pos.is_none()); - } - - struct MockMob; - impl crate::entity::mob::Mob for MockMob { - fn get_mob_entity(&self) -> &crate::entity::mob::MobEntity { - unimplemented!() - } - fn mob_write_nbt(&self, _nbt: &mut pumpkin_nbt::compound::NbtCompound) { - unimplemented!() - } - fn mob_read_nbt(&self, _nbt: &pumpkin_nbt::compound::NbtCompound) { - unimplemented!() - } - } -} diff --git a/crates/pumpkin/src/entity/ai/goal/ocelot_attack.rs b/crates/pumpkin/src/entity/ai/goal/ocelot_attack.rs new file mode 100644 index 000000000..f1923e31f --- /dev/null +++ b/crates/pumpkin/src/entity/ai/goal/ocelot_attack.rs @@ -0,0 +1,129 @@ +use super::{Controls, Goal}; +use crate::entity::EntityBase; +use crate::entity::ai::pathfinder::NavigatorGoal; +use crate::entity::mob::Mob; +use std::sync::Arc; + +pub struct OcelotAttackGoal { + goal_control: Controls, + target: Option>, + attack_time: i32, +} + +impl Default for OcelotAttackGoal { + fn default() -> Self { + Self::new() + } +} + +impl OcelotAttackGoal { + #[must_use] + pub const fn new() -> Self { + Self { + goal_control: Controls::MOVE.union(Controls::LOOK), + target: None, + attack_time: 0, + } + } +} + +impl Goal for OcelotAttackGoal { + fn can_start(&mut self, mob: &dyn Mob) -> bool { + let target = mob.get_mob_entity().get_target(); + let Some(target) = target else { + return false; + }; + + if !target.get_entity().is_alive() { + return false; + } + + self.target = Some(target); + true + } + + fn should_continue(&self, mob: &dyn Mob) -> bool { + let Some(target) = &self.target else { + return false; + }; + + if !target.get_entity().is_alive() { + return false; + } + + let mob_pos = mob.get_entity().pos.load(); + let target_pos = target.get_entity().pos.load(); + let dist_sq = mob_pos.squared_distance_to_vec(&target_pos); + + if dist_sq > 225.0 { + return false; + } + + let is_idle = mob + .get_mob_entity() + .navigator + .try_lock() + .is_ok_and(|nav| nav.is_idle()); + + !is_idle || mob.get_mob_entity().get_target().is_some() + } + + fn stop(&mut self, mob: &dyn Mob) { + self.target = 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 + } + + fn tick(&mut self, mob: &dyn Mob) { + let Some(target) = &self.target else { + return; + }; + + let mob_entity = mob.get_mob_entity(); + mob_entity + .look_control + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .look_at_entity_with_range(target, 30.0, 30.0); + + let bb_width = mob.get_entity().entity_dimension.load().width; + let melee_radius_sq = (bb_width * 2.0) * (bb_width * 2.0); + let mob_pos = mob.get_entity().pos.load(); + let target_pos = target.get_entity().pos.load(); + let dist_sq = mob_pos.squared_distance_to_vec(&target_pos); + + let speed_modifier = if dist_sq > f64::from(melee_radius_sq) && dist_sq < 16.0 { + 1.33 + } else if dist_sq < 225.0 { + 0.6 + } else { + 0.8 + }; + + let mut navigator = mob_entity + .navigator + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + navigator.set_progress(NavigatorGoal::new(mob_pos, target_pos, speed_modifier)); + + self.attack_time = (self.attack_time - 1).max(0); + if dist_sq <= f64::from(melee_radius_sq) && self.attack_time <= 0 { + self.attack_time = 20; + mob.get_mob_entity().living_entity.swing_hand(); + mob.get_mob_entity() + .try_attack(mob.get_entity(), target.as_ref()); + } + } + + fn controls(&self) -> Controls { + self.goal_control + } +} diff --git a/crates/pumpkin/src/entity/ai/goal/open_door.rs b/crates/pumpkin/src/entity/ai/goal/open_door.rs index 83433c4f6..7f5ca0489 100644 --- a/crates/pumpkin/src/entity/ai/goal/open_door.rs +++ b/crates/pumpkin/src/entity/ai/goal/open_door.rs @@ -59,31 +59,3 @@ impl Goal for OpenDoorGoal { Controls::empty() } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn open_door_can_continue_to_use() { - let mut goal = OpenDoorGoal::new(true); - goal.forget_time = 20; - goal.door_interact_goal.passed = false; - - assert!(goal.close_door); - assert!(goal.forget_time > 0); - assert!(goal.door_interact_goal.can_continue_to_use()); - - // When forget_time expires - goal.forget_time = 0; - assert!( - !(goal.close_door - && goal.forget_time > 0 - && goal.door_interact_goal.can_continue_to_use()) - ); - - // When close_door is false - let goal_no_close = OpenDoorGoal::new(false); - assert!(!goal_no_close.close_door); - } -} diff --git a/crates/pumpkin/src/entity/ai/goal/owner_hurt_by_target.rs b/crates/pumpkin/src/entity/ai/goal/owner_hurt_by_target.rs index 30424887b..d5688988c 100644 --- a/crates/pumpkin/src/entity/ai/goal/owner_hurt_by_target.rs +++ b/crates/pumpkin/src/entity/ai/goal/owner_hurt_by_target.rs @@ -97,14 +97,3 @@ impl Goal for OwnerHurtByTargetGoal { Controls::TARGET } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn owner_hurt_by_target_goal_controls() { - let goal = OwnerHurtByTargetGoal::new(); - assert!(goal.controls().get(Controls::TARGET)); - } -} diff --git a/crates/pumpkin/src/entity/ai/goal/owner_hurt_target.rs b/crates/pumpkin/src/entity/ai/goal/owner_hurt_target.rs index 4bd3e6633..f47a7ab58 100644 --- a/crates/pumpkin/src/entity/ai/goal/owner_hurt_target.rs +++ b/crates/pumpkin/src/entity/ai/goal/owner_hurt_target.rs @@ -97,14 +97,3 @@ impl Goal for OwnerHurtTargetGoal { Controls::TARGET } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn owner_hurt_target_goal_controls() { - let goal = OwnerHurtTargetGoal::new(); - assert!(goal.controls().get(Controls::TARGET)); - } -} diff --git a/crates/pumpkin/src/entity/ai/goal/restrict_sun.rs b/crates/pumpkin/src/entity/ai/goal/restrict_sun.rs new file mode 100644 index 000000000..dd5405568 --- /dev/null +++ b/crates/pumpkin/src/entity/ai/goal/restrict_sun.rs @@ -0,0 +1,51 @@ +use super::{Controls, Goal}; +use crate::entity::mob::Mob; +use pumpkin_data::data_component_impl::EquipmentSlot; + +#[derive(Default)] +pub struct RestrictSunGoal; + +impl RestrictSunGoal { + #[must_use] + pub const fn new() -> Self { + Self + } +} + +impl Goal for RestrictSunGoal { + fn can_start(&mut self, mob: &dyn Mob) -> bool { + let has_helmet = mob + .get_mob_entity() + .living_entity + .entity_equipment + .try_lock() + .is_ok_and(|eq| !eq.get(&EquipmentSlot::HEAD).is_empty()); + + if has_helmet { + return false; + } + + let time = mob.get_entity().world.load().get_world_age() % 24000; + time < 12000 + } + + fn should_continue(&self, mob: &dyn Mob) -> bool { + let has_helmet = mob + .get_mob_entity() + .living_entity + .entity_equipment + .try_lock() + .is_ok_and(|eq| !eq.get(&EquipmentSlot::HEAD).is_empty()); + + if has_helmet { + return false; + } + + let time = mob.get_entity().world.load().get_world_age() % 24000; + time < 12000 + } + + fn controls(&self) -> Controls { + Controls::empty() + } +} diff --git a/crates/pumpkin/src/entity/ai/goal/run_around_like_crazy.rs b/crates/pumpkin/src/entity/ai/goal/run_around_like_crazy.rs new file mode 100644 index 000000000..f3704b7f5 --- /dev/null +++ b/crates/pumpkin/src/entity/ai/goal/run_around_like_crazy.rs @@ -0,0 +1,102 @@ +use super::{Controls, Goal}; +use crate::entity::ai::pathfinder::NavigatorGoal; +use crate::entity::mob::Mob; +use pumpkin_util::math::position::BlockPos; +use pumpkin_util::math::vector3::Vector3; +use rand::RngExt; + +pub struct RunAroundLikeCrazyGoal { + goal_control: Controls, + speed_modifier: f64, + pos_x: f64, + pos_y: f64, + pos_z: f64, +} + +impl RunAroundLikeCrazyGoal { + #[must_use] + pub const fn new(speed_modifier: f64) -> Self { + Self { + goal_control: Controls::MOVE, + speed_modifier, + pos_x: 0.0, + pos_y: 0.0, + pos_z: 0.0, + } + } + + fn find_random_pos(mob: &dyn Mob) -> Option> { + let mob_pos = mob.get_entity().pos.load(); + let world = mob.get_entity().world.load(); + let mut rng = mob.get_random(); + + for _ in 0..10 { + let dx = rng.random_range(-5.0..=5.0); + let dy = rng.random_range(-4.0..=4.0); + let dz = rng.random_range(-5.0..=5.0); + + let check_pos = BlockPos::new( + (mob_pos.x + dx) as i32, + (mob_pos.y + dy) as i32, + (mob_pos.z + dz) as i32, + ); + + let block = world.get_block_state(&check_pos); + let block_below = world.get_block_state(&check_pos.down()); + if !block.is_solid() && block_below.is_solid() { + return Some(Vector3::new( + f64::from(check_pos.0.x) + 0.5, + f64::from(check_pos.0.y), + f64::from(check_pos.0.z) + 0.5, + )); + } + } + + None + } +} + +impl Goal for RunAroundLikeCrazyGoal { + fn can_start(&mut self, mob: &dyn Mob) -> bool { + if mob.is_tamed() || !mob.get_entity().has_passengers() { + return false; + } + + let Some(pos) = Self::find_random_pos(mob) else { + return false; + }; + + self.pos_x = pos.x; + self.pos_y = pos.y; + self.pos_z = pos.z; + true + } + + fn should_continue(&self, mob: &dyn Mob) -> bool { + let is_idle = mob + .get_mob_entity() + .navigator + .try_lock() + .is_ok_and(|nav| nav.is_idle()); + + !mob.is_tamed() && !is_idle && mob.get_entity().has_passengers() + } + + fn start(&mut self, mob: &dyn Mob) { + let mob_pos = mob.get_entity().pos.load(); + let mut navigator = mob + .get_mob_entity() + .navigator + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + navigator.set_progress(NavigatorGoal::new( + mob_pos, + Vector3::new(self.pos_x, self.pos_y, self.pos_z), + self.speed_modifier, + )); + } + + fn controls(&self) -> Controls { + self.goal_control + } +} diff --git a/crates/pumpkin/src/entity/ai/goal/sit_when_ordered_to.rs b/crates/pumpkin/src/entity/ai/goal/sit_when_ordered_to.rs new file mode 100644 index 000000000..cd5b5937e --- /dev/null +++ b/crates/pumpkin/src/entity/ai/goal/sit_when_ordered_to.rs @@ -0,0 +1,53 @@ +use super::{Controls, Goal}; +use crate::entity::mob::Mob; +use std::sync::atomic::Ordering; + +#[derive(Default)] +pub struct SitWhenOrderedToGoal { + goal_control: Controls, +} + +impl SitWhenOrderedToGoal { + #[must_use] + pub const fn new() -> Self { + Self { + goal_control: Controls::MOVE.union(Controls::JUMP), + } + } +} + +impl Goal for SitWhenOrderedToGoal { + fn can_start(&mut self, mob: &dyn Mob) -> bool { + if !mob.is_sitting() && !mob.is_tamed() { + return false; + } + + let entity = &mob.get_mob_entity().living_entity.entity; + if entity.touching_water.load(Ordering::Relaxed) { + return false; + } + + if !entity.on_ground.load(Ordering::Relaxed) { + return false; + } + + mob.is_sitting() + } + + fn should_continue(&self, mob: &dyn Mob) -> bool { + mob.is_sitting() + } + + fn start(&mut self, mob: &dyn Mob) { + let mut navigator = mob + .get_mob_entity() + .navigator + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + navigator.stop(); + } + + fn controls(&self) -> Controls { + self.goal_control + } +} diff --git a/crates/pumpkin/src/entity/ai/goal/track_target.rs b/crates/pumpkin/src/entity/ai/goal/track_target.rs index 3d4dd4e4f..114cc081f 100644 --- a/crates/pumpkin/src/entity/ai/goal/track_target.rs +++ b/crates/pumpkin/src/entity/ai/goal/track_target.rs @@ -183,28 +183,3 @@ impl Goal for TrackTargetGoal { self.goal_control } } - -#[cfg(test)] -mod tests { - use super::{TrackTargetGoal, to_goal_ticks}; - use std::sync::atomic::Ordering; - - #[test] - fn forgets_unseen_target_after_vanilla_memory_window() { - let goal = TrackTargetGoal::with_default(true); - let memory_ticks = to_goal_ticks(goal.max_time_without_visibility); - - for _ in 0..memory_ticks { - assert!(goal.remembers_visible_target(false)); - } - assert!(!goal.remembers_visible_target(false)); - } - - #[test] - fn seeing_target_resets_unseen_memory() { - let goal = TrackTargetGoal::with_default(true); - assert!(goal.remembers_visible_target(false)); - assert!(goal.remembers_visible_target(true)); - assert_eq!(goal.time_without_visibility.load(Ordering::Relaxed), 0); - } -} diff --git a/crates/pumpkin/src/entity/ai/goal/use_item.rs b/crates/pumpkin/src/entity/ai/goal/use_item.rs new file mode 100644 index 000000000..8f769bb92 --- /dev/null +++ b/crates/pumpkin/src/entity/ai/goal/use_item.rs @@ -0,0 +1,82 @@ +use super::{Controls, Goal}; +use crate::entity::mob::Mob; +use pumpkin_data::data_component_impl::EquipmentSlot; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::sound::Sound; +use pumpkin_util::Hand; +use std::sync::Arc; + +pub type CanUsePredicate = Arc bool + Send + Sync>; + +pub struct UseItemGoal { + goal_control: Controls, + item: ItemStack, + finish_sound: Option, + can_use_fn: CanUsePredicate, +} + +impl UseItemGoal { + #[must_use] + pub fn new(item: ItemStack, finish_sound: Option, can_use_fn: CanUsePredicate) -> Self { + Self { + goal_control: Controls::empty(), + item, + finish_sound, + can_use_fn, + } + } +} + +impl Goal for UseItemGoal { + fn can_start(&mut self, mob: &dyn Mob) -> bool { + (self.can_use_fn)(mob) + } + + fn should_continue(&self, mob: &dyn Mob) -> bool { + mob.get_mob_entity() + .living_entity + .active_hand + .lock() + .is_ok_and(|hand| hand.is_some()) + } + + fn start(&mut self, mob: &dyn Mob) { + if let Ok(mut eq) = mob + .get_mob_entity() + .living_entity + .entity_equipment + .try_lock() + { + eq.put(&EquipmentSlot::MAIN_HAND, self.item.clone()); + } + mob.get_mob_entity().living_entity.set_active_hand( + Hand::Right, + self.item.clone(), + i32::MAX, + ); + } + + fn stop(&mut self, mob: &dyn Mob) { + if let Ok(mut eq) = mob + .get_mob_entity() + .living_entity + .entity_equipment + .try_lock() + { + eq.put(&EquipmentSlot::MAIN_HAND, ItemStack::EMPTY.clone()); + } + mob.get_mob_entity().living_entity.clear_active_hand(); + if let Some(sound) = self.finish_sound { + let pos = mob.get_entity().pos.load(); + mob.get_entity().world.load().play_sound( + sound, + pumpkin_data::sound::SoundCategory::Hostile, + &pos, + ); + } + } + + fn controls(&self) -> Controls { + self.goal_control + } +} diff --git a/crates/pumpkin/src/entity/ai/goal/water_avoiding_random_flying.rs b/crates/pumpkin/src/entity/ai/goal/water_avoiding_random_flying.rs new file mode 100644 index 000000000..94b1bb633 --- /dev/null +++ b/crates/pumpkin/src/entity/ai/goal/water_avoiding_random_flying.rs @@ -0,0 +1,90 @@ +use super::{Controls, Goal, to_goal_ticks}; +use crate::entity::ai::pathfinder::NavigatorGoal; +use crate::entity::mob::Mob; +use pumpkin_util::math::position::BlockPos; +use pumpkin_util::math::vector3::Vector3; +use rand::RngExt; + +pub struct WaterAvoidingRandomFlyingGoal { + goal_control: Controls, + speed: f64, + target: Option>, + chance: i32, +} + +impl WaterAvoidingRandomFlyingGoal { + #[must_use] + pub const fn new(speed: f64) -> Self { + Self { + goal_control: Controls::MOVE, + speed, + target: None, + chance: to_goal_ticks(120), + } + } + + fn find_air_target(mob: &dyn Mob) -> Option> { + let mob_pos = mob.get_entity().pos.load(); + let world = mob.get_entity().world.load(); + let mut rng = mob.get_random(); + + for _ in 0..10 { + let dx = rng.random_range(-8.0..=8.0); + let dy = rng.random_range(-4.0..=7.0); + let dz = rng.random_range(-8.0..=8.0); + + let check_pos = BlockPos::new( + (mob_pos.x + dx) as i32, + (mob_pos.y + dy) as i32, + (mob_pos.z + dz) as i32, + ); + + let block = world.get_block_state(&check_pos); + if !block.is_solid() && !block.is_liquid() { + return Some(Vector3::new(mob_pos.x + dx, mob_pos.y + dy, mob_pos.z + dz)); + } + } + + None + } +} + +impl Goal for WaterAvoidingRandomFlyingGoal { + fn can_start(&mut self, mob: &dyn Mob) -> bool { + if mob.get_random().random_range(0..self.chance) != 0 { + return false; + } + + self.target = Self::find_air_target(mob); + self.target.is_some() + } + + fn should_continue(&self, mob: &dyn Mob) -> bool { + let is_idle = mob + .get_mob_entity() + .navigator + .try_lock() + .is_ok_and(|nav| nav.is_idle()); + !is_idle + } + + fn start(&mut self, mob: &dyn Mob) { + if let Some(target) = self.target { + let mob_pos = mob.get_entity().pos.load(); + let mut navigator = mob + .get_mob_entity() + .navigator + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + navigator.set_progress(NavigatorGoal::new(mob_pos, target, self.speed)); + } + } + + fn stop(&mut self, _mob: &dyn Mob) { + self.target = None; + } + + fn controls(&self) -> Controls { + self.goal_control + } +} diff --git a/crates/pumpkin/src/entity/ai/pathfinder/amphibious_node_evaluator.rs b/crates/pumpkin/src/entity/ai/pathfinder/amphibious_node_evaluator.rs new file mode 100644 index 000000000..f794000af --- /dev/null +++ b/crates/pumpkin/src/entity/ai/pathfinder/amphibious_node_evaluator.rs @@ -0,0 +1,194 @@ +use pumpkin_data::BlockDirection; +use pumpkin_util::math::{position::BlockPos, vector3::Vector3}; + +use crate::entity::ai::pathfinder::{ + node::{Node, PathType, Target}, + node_evaluator::{MobData, NodeEvaluator}, + pathfinding_context::PathfindingContext, + walk_node_evaluator::WalkNodeEvaluator, +}; + +pub struct AmphibiousNodeEvaluator { + pub walk: WalkNodeEvaluator, + prefers_shallow_swimming: bool, + old_walkable_cost: f32, + old_water_border_cost: f32, +} + +impl AmphibiousNodeEvaluator { + #[must_use] + pub fn new(prefers_shallow_swimming: bool) -> Self { + let mut walk = WalkNodeEvaluator::new(); + walk.is_amphibious = true; + Self { + walk, + prefers_shallow_swimming, + old_walkable_cost: 0.0, + old_water_border_cost: 0.0, + } + } + + fn is_vertical_neighbor_valid(vertical_node: Option<&Node>, pos: &Node) -> bool { + WalkNodeEvaluator::is_neighbor_valid(vertical_node, pos) + && vertical_node.is_some_and(|n| n.path_type == PathType::Water) + } +} + +impl NodeEvaluator for AmphibiousNodeEvaluator { + fn prepare(&mut self, context: PathfindingContext, mut mob_data: MobData) { + mob_data.set_pathfinding_malus(PathType::Water, 0.0); + self.old_walkable_cost = mob_data.get_pathfinding_malus(PathType::Walkable); + mob_data.set_pathfinding_malus(PathType::Walkable, 6.0); + self.old_water_border_cost = mob_data.get_pathfinding_malus(PathType::WaterBorder); + mob_data.set_pathfinding_malus(PathType::WaterBorder, 4.0); + self.walk.prepare(context, mob_data); + } + + fn done(&mut self) { + if let Some(ref mut mob) = self.walk.base.mob_data { + mob.set_pathfinding_malus(PathType::Walkable, self.old_walkable_cost); + mob.set_pathfinding_malus(PathType::WaterBorder, self.old_water_border_cost); + } + self.walk.done(); + } + + fn get_start(&mut self) -> Option { + let mob_data = self.walk.base.mob_data?; + if mob_data.is_in_water { + let half_width = f64::from(mob_data.width) / 2.0; + let min_x = (mob_data.position.x - half_width).floor() as i32; + let min_y = (mob_data.position.y + 0.5).floor() as i32; + let min_z = (mob_data.position.z - half_width).floor() as i32; + Some(self.walk.get_start_node(Vector3::new(min_x, min_y, min_z))) + } else { + self.walk.get_start() + } + } + + fn get_target(&mut self, pos: BlockPos) -> Target { + Target::new(self.walk.base.get_node(pos)) + } + + fn get_neighbors(&mut self, current: &Node, out: &mut Vec) { + self.walk.get_neighbors(current, out); + let block_path_type_above = self + .walk + .get_cached_path_type(current.pos.0.add_raw(0, 1, 0)); + let block_path_type_current = self.walk.get_cached_path_type(current.pos.0); + + let jump_size = if self.walk.get_mob_penalty(block_path_type_above) >= 0.0 + && block_path_type_current != PathType::StickyHoney + { + self.walk.get_mob_jump_height().floor() as i32 + } else { + 0 + }; + + let pos_height = self.walk.get_floor_level(¤t.pos); + let up_node = self.walk.find_accepted_node( + current.pos.0.x, + current.pos.0.y + 1, + current.pos.0.z, + (jump_size - 1).max(0), + pos_height, + BlockDirection::Up, + block_path_type_current, + ); + let down_node = self.walk.find_accepted_node( + current.pos.0.x, + current.pos.0.y - 1, + current.pos.0.z, + jump_size, + pos_height, + BlockDirection::Down, + block_path_type_current, + ); + + if Self::is_vertical_neighbor_valid(up_node.as_ref(), current) + && let Some(n) = up_node + { + out.push(n); + } + + if Self::is_vertical_neighbor_valid(down_node.as_ref(), current) + && block_path_type_current != PathType::Trapdoor + && let Some(n) = down_node + { + out.push(n); + } + + let sea_level = self.walk.base.mob_data.map_or(63, |d| d.sea_level); + for neighbor in out.iter_mut() { + if neighbor.path_type == PathType::Water + && self.prefers_shallow_swimming + && neighbor.pos.0.y < sea_level - 10 + { + neighbor.cost_malus += 1.0; + } + } + } + + fn get_path_type_of_mob( + &mut self, + context: &mut PathfindingContext, + pos: Vector3, + mob_data: &MobData, + ) -> PathType { + self.walk.get_path_type_of_mob(context, pos, mob_data) + } + + fn get_path_type(&mut self, context: &mut PathfindingContext, pos: Vector3) -> PathType { + let block_path_type = context.get_path_type_from_state(pos); + if block_path_type == PathType::Water { + for dir in [ + BlockDirection::Down, + BlockDirection::Up, + BlockDirection::North, + BlockDirection::South, + BlockDirection::West, + BlockDirection::East, + ] { + let neighbor_pos = pos + dir.to_offset(); + let path_type = context.get_path_type_from_state(neighbor_pos); + if path_type == PathType::Blocked { + return PathType::WaterBorder; + } + } + PathType::Water + } else { + self.walk.get_path_type(context, pos) + } + } + + fn set_can_pass_doors(&mut self, can_pass: bool) { + self.walk.set_can_pass_doors(can_pass); + } + + fn set_can_open_doors(&mut self, can_open: bool) { + self.walk.set_can_open_doors(can_open); + } + + fn set_can_float(&mut self, can_float: bool) { + self.walk.set_can_float(can_float); + } + + fn set_can_walk_over_fences(&mut self, can_walk: bool) { + self.walk.set_can_walk_over_fences(can_walk); + } + + fn can_pass_doors(&self) -> bool { + self.walk.can_pass_doors() + } + + fn can_open_doors(&self) -> bool { + self.walk.can_open_doors() + } + + fn can_float(&self) -> bool { + self.walk.can_float() + } + + fn can_walk_over_fences(&self) -> bool { + self.walk.can_walk_over_fences() + } +} diff --git a/crates/pumpkin/src/entity/ai/pathfinder/fly_node_evaluator.rs b/crates/pumpkin/src/entity/ai/pathfinder/fly_node_evaluator.rs new file mode 100644 index 000000000..0e90f4495 --- /dev/null +++ b/crates/pumpkin/src/entity/ai/pathfinder/fly_node_evaluator.rs @@ -0,0 +1,562 @@ +use pumpkin_util::math::{position::BlockPos, vector3::Vector3}; +use rustc_hash::FxHashMap; + +use crate::entity::ai::pathfinder::{ + node::{Node, PathType, Target}, + node_evaluator::{MobData, NodeEvaluator}, + pathfinding_context::PathfindingContext, + walk_node_evaluator::WalkNodeEvaluator, +}; + +pub struct FlyNodeEvaluator { + pub walk: WalkNodeEvaluator, + path_types_cache: FxHashMap, PathType>, +} + +impl FlyNodeEvaluator { + #[must_use] + pub fn new() -> Self { + Self { + walk: WalkNodeEvaluator::new(), + path_types_cache: FxHashMap::default(), + } + } + + pub fn get_cached_path_type(&mut self, pos: Vector3) -> PathType { + if let Some(&cached) = self.path_types_cache.get(&pos) { + return cached; + } + + let path_type = if let Some(mut ctx) = self.walk.base.context.take() + && let Some(mob_data) = self.walk.base.mob_data + { + let res = self.get_path_type_of_mob(&mut ctx, pos, &mob_data); + self.walk.base.context = Some(ctx); + res + } else { + PathType::Blocked + }; + + self.path_types_cache.insert(pos, path_type); + path_type + } + + pub fn can_start_at(&mut self, pos: Vector3) -> bool { + let path_type = self.get_cached_path_type(pos); + self.walk.get_mob_penalty(path_type) >= 0.0 + } + + fn find_accepted_node(&mut self, x: i32, y: i32, z: i32) -> Option { + let path_type = self.get_cached_path_type(Vector3::new(x, y, z)); + let path_cost = self.walk.get_mob_penalty(path_type); + (path_cost >= 0.0).then(|| { + let mut best = self.walk.base.get_node(BlockPos::new(x, y, z)); + best.path_type = path_type; + best.cost_malus = best.cost_malus.max(path_cost); + if path_type == PathType::Walkable { + best.cost_malus += 1.0; + } + best + }) + } + + const fn has_malus(node: Option<&Node>) -> bool { + if let Some(n) = node { + n.cost_malus >= 0.0 + } else { + false + } + } + + const fn is_open(node: Option<&Node>) -> bool { + if let Some(n) = node { !n.closed } else { false } + } +} + +impl NodeEvaluator for FlyNodeEvaluator { + fn prepare(&mut self, context: PathfindingContext, mob_data: MobData) { + self.walk.prepare(context, mob_data); + self.path_types_cache.clear(); + } + + fn done(&mut self) { + self.path_types_cache.clear(); + self.walk.done(); + } + + fn get_start(&mut self) -> Option { + let mob_data = self.walk.base.mob_data?; + let mob_x = mob_data.position.x; + let mob_y = mob_data.position.y; + let mob_z = mob_data.position.z; + let block_x = mob_x.floor() as i32; + let block_z = mob_z.floor() as i32; + + let start_y = if self.walk.base.can_float && mob_data.is_in_water { + let mut y = mob_y.floor() as i32; + if let Some(ref ctx) = self.walk.base.context { + while ctx.is_water(&BlockPos::new(block_x, y + 1, block_z)) { + y += 1; + } + } + y + } else { + (mob_y + 0.5).floor() as i32 + }; + + let start_pos = Vector3::new(block_x, start_y, block_z); + if !self.can_start_at(start_pos) { + let is_small_mob = mob_data.width < 1.0 || mob_data.height < 1.0; + if is_small_mob { + let x_pad = (1.1 - f64::from(mob_data.width)).max(0.0); + let y_pad = (1.1 - f64::from(mob_data.height)).max(0.0); + let z_pad = (1.1 - f64::from(mob_data.width)).max(0.0); + let half_w = f64::from(mob_data.width) / 2.0; + let min_x = (mob_x - half_w - x_pad).floor() as i32; + let max_x = (mob_x + half_w + x_pad).floor() as i32; + let min_y = (mob_y - y_pad).floor() as i32; + let max_y = (mob_y + f64::from(mob_data.height) + y_pad).floor() as i32; + let min_z = (mob_z - half_w - z_pad).floor() as i32; + let max_z = (mob_z + half_w + z_pad).floor() as i32; + + let mut candidate_count = 0; + for cx in min_x..=max_x { + for cy in min_y..=max_y { + for cz in min_z..=max_z { + let candidate = Vector3::new(cx, cy, cz); + if self.can_start_at(candidate) { + return Some(self.walk.get_start_node(candidate)); + } + candidate_count += 1; + if candidate_count >= 10 { + break; + } + } + if candidate_count >= 10 { + break; + } + } + if candidate_count >= 10 { + break; + } + } + } else { + let half_width = f64::from(mob_data.width) / 2.0; + let min_x = (mob_x - half_width).floor() as i32; + let max_x = (mob_x + half_width).floor() as i32; + let min_z = (mob_z - half_width).floor() as i32; + let max_z = (mob_z + half_width).floor() as i32; + + for candidate in [ + Vector3::new(min_x, start_y, min_z), + Vector3::new(min_x, start_y, max_z), + Vector3::new(max_x, start_y, min_z), + Vector3::new(max_x, start_y, max_z), + ] { + if self.can_start_at(candidate) { + return Some(self.walk.get_start_node(candidate)); + } + } + } + } + + Some(self.walk.get_start_node(start_pos)) + } + + fn get_target(&mut self, pos: BlockPos) -> Target { + Target::new(self.walk.base.get_node(pos)) + } + + #[expect(clippy::too_many_lines)] + fn get_neighbors(&mut self, current: &Node, out: &mut Vec) { + let x = current.pos.0.x; + let y = current.pos.0.y; + let z = current.pos.0.z; + + let south = self.find_accepted_node(x, y, z + 1); + if Self::is_open(south.as_ref()) + && let Some(n) = south + { + out.push(n); + } + + let west = self.find_accepted_node(x - 1, y, z); + if Self::is_open(west.as_ref()) + && let Some(n) = west + { + out.push(n); + } + + let east = self.find_accepted_node(x + 1, y, z); + if Self::is_open(east.as_ref()) + && let Some(n) = east + { + out.push(n); + } + + let north = self.find_accepted_node(x, y, z - 1); + if Self::is_open(north.as_ref()) + && let Some(n) = north + { + out.push(n); + } + + let up = self.find_accepted_node(x, y + 1, z); + if Self::is_open(up.as_ref()) + && let Some(n) = up + { + out.push(n); + } + + let down = self.find_accepted_node(x, y - 1, z); + if Self::is_open(down.as_ref()) + && let Some(n) = down + { + out.push(n); + } + + let south_up = self.find_accepted_node(x, y + 1, z + 1); + if Self::is_open(south_up.as_ref()) + && Self::has_malus(south.as_ref()) + && Self::has_malus(up.as_ref()) + && let Some(n) = south_up + { + out.push(n); + } + + let west_up = self.find_accepted_node(x - 1, y + 1, z); + if Self::is_open(west_up.as_ref()) + && Self::has_malus(west.as_ref()) + && Self::has_malus(up.as_ref()) + && let Some(n) = west_up + { + out.push(n); + } + + let east_up = self.find_accepted_node(x + 1, y + 1, z); + if Self::is_open(east_up.as_ref()) + && Self::has_malus(east.as_ref()) + && Self::has_malus(up.as_ref()) + && let Some(n) = east_up + { + out.push(n); + } + + let north_up = self.find_accepted_node(x, y + 1, z - 1); + if Self::is_open(north_up.as_ref()) + && Self::has_malus(north.as_ref()) + && Self::has_malus(up.as_ref()) + && let Some(n) = north_up + { + out.push(n); + } + + let south_down = self.find_accepted_node(x, y - 1, z + 1); + if Self::is_open(south_down.as_ref()) + && Self::has_malus(south.as_ref()) + && Self::has_malus(down.as_ref()) + && let Some(n) = south_down + { + out.push(n); + } + + let west_down = self.find_accepted_node(x - 1, y - 1, z); + if Self::is_open(west_down.as_ref()) + && Self::has_malus(west.as_ref()) + && Self::has_malus(down.as_ref()) + && let Some(n) = west_down + { + out.push(n); + } + + let east_down = self.find_accepted_node(x + 1, y - 1, z); + if Self::is_open(east_down.as_ref()) + && Self::has_malus(east.as_ref()) + && Self::has_malus(down.as_ref()) + && let Some(n) = east_down + { + out.push(n); + } + + let north_down = self.find_accepted_node(x, y - 1, z - 1); + if Self::is_open(north_down.as_ref()) + && Self::has_malus(north.as_ref()) + && Self::has_malus(down.as_ref()) + && let Some(n) = north_down + { + out.push(n); + } + + let north_east = self.find_accepted_node(x + 1, y, z - 1); + if Self::is_open(north_east.as_ref()) + && Self::has_malus(north.as_ref()) + && Self::has_malus(east.as_ref()) + && let Some(n) = north_east + { + out.push(n); + } + + let south_east = self.find_accepted_node(x + 1, y, z + 1); + if Self::is_open(south_east.as_ref()) + && Self::has_malus(south.as_ref()) + && Self::has_malus(east.as_ref()) + && let Some(n) = south_east + { + out.push(n); + } + + let north_west = self.find_accepted_node(x - 1, y, z - 1); + if Self::is_open(north_west.as_ref()) + && Self::has_malus(north.as_ref()) + && Self::has_malus(west.as_ref()) + && let Some(n) = north_west + { + out.push(n); + } + + let south_west = self.find_accepted_node(x - 1, y, z + 1); + if Self::is_open(south_west.as_ref()) + && Self::has_malus(south.as_ref()) + && Self::has_malus(west.as_ref()) + && let Some(n) = south_west + { + out.push(n); + } + + let north_east_up = self.find_accepted_node(x + 1, y + 1, z - 1); + if Self::is_open(north_east_up.as_ref()) + && Self::has_malus(north_east.as_ref()) + && Self::has_malus(north.as_ref()) + && Self::has_malus(east.as_ref()) + && Self::has_malus(up.as_ref()) + && Self::has_malus(north_up.as_ref()) + && Self::has_malus(east_up.as_ref()) + && let Some(n) = north_east_up + { + out.push(n); + } + + let south_east_up = self.find_accepted_node(x + 1, y + 1, z + 1); + if Self::is_open(south_east_up.as_ref()) + && Self::has_malus(south_east.as_ref()) + && Self::has_malus(south.as_ref()) + && Self::has_malus(east.as_ref()) + && Self::has_malus(up.as_ref()) + && Self::has_malus(south_up.as_ref()) + && Self::has_malus(east_up.as_ref()) + && let Some(n) = south_east_up + { + out.push(n); + } + + let north_west_up = self.find_accepted_node(x - 1, y + 1, z - 1); + if Self::is_open(north_west_up.as_ref()) + && Self::has_malus(north_west.as_ref()) + && Self::has_malus(north.as_ref()) + && Self::has_malus(west.as_ref()) + && Self::has_malus(up.as_ref()) + && Self::has_malus(north_up.as_ref()) + && Self::has_malus(west_up.as_ref()) + && let Some(n) = north_west_up + { + out.push(n); + } + + let south_west_up = self.find_accepted_node(x - 1, y + 1, z + 1); + if Self::is_open(south_west_up.as_ref()) + && Self::has_malus(south_west.as_ref()) + && Self::has_malus(south.as_ref()) + && Self::has_malus(west.as_ref()) + && Self::has_malus(up.as_ref()) + && Self::has_malus(south_up.as_ref()) + && Self::has_malus(west_up.as_ref()) + && let Some(n) = south_west_up + { + out.push(n); + } + + let north_east_down = self.find_accepted_node(x + 1, y - 1, z - 1); + if Self::is_open(north_east_down.as_ref()) + && Self::has_malus(north_east.as_ref()) + && Self::has_malus(north.as_ref()) + && Self::has_malus(east.as_ref()) + && Self::has_malus(down.as_ref()) + && Self::has_malus(north_down.as_ref()) + && Self::has_malus(east_down.as_ref()) + && let Some(n) = north_east_down + { + out.push(n); + } + + let south_east_down = self.find_accepted_node(x + 1, y - 1, z + 1); + if Self::is_open(south_east_down.as_ref()) + && Self::has_malus(south_east.as_ref()) + && Self::has_malus(south.as_ref()) + && Self::has_malus(east.as_ref()) + && Self::has_malus(down.as_ref()) + && Self::has_malus(south_down.as_ref()) + && Self::has_malus(east_down.as_ref()) + && let Some(n) = south_east_down + { + out.push(n); + } + + let north_west_down = self.find_accepted_node(x - 1, y - 1, z - 1); + if Self::is_open(north_west_down.as_ref()) + && Self::has_malus(north_west.as_ref()) + && Self::has_malus(north.as_ref()) + && Self::has_malus(west.as_ref()) + && Self::has_malus(down.as_ref()) + && Self::has_malus(north_down.as_ref()) + && Self::has_malus(west_down.as_ref()) + && let Some(n) = north_west_down + { + out.push(n); + } + + let south_west_down = self.find_accepted_node(x - 1, y - 1, z + 1); + if Self::is_open(south_west_down.as_ref()) + && Self::has_malus(south_west.as_ref()) + && Self::has_malus(south.as_ref()) + && Self::has_malus(west.as_ref()) + && Self::has_malus(down.as_ref()) + && Self::has_malus(south_down.as_ref()) + && Self::has_malus(west_down.as_ref()) + && let Some(n) = south_west_down + { + out.push(n); + } + } + + fn get_path_type_of_mob( + &mut self, + context: &mut PathfindingContext, + pos: Vector3, + mob_data: &MobData, + ) -> PathType { + let block_types = self + .walk + .get_path_type_within_mob_bb(context, pos.x, pos.y, pos.z, mob_data); + if block_types.len() == 1 { + return block_types[0]; + } + + if block_types.contains(&PathType::Fence) { + return PathType::Fence; + } + + if block_types.contains(&PathType::UnpassableRail) { + return PathType::UnpassableRail; + } + + let mut highest_malus_path_type_within_bb = PathType::Blocked; + let mut highest_malus_within_bb = + mob_data.get_pathfinding_malus(highest_malus_path_type_within_bb); + + for &path_type in &block_types { + let malus = mob_data.get_pathfinding_malus(path_type); + if malus < 0.0 { + return path_type; + } + + if malus >= highest_malus_within_bb { + highest_malus_within_bb = malus; + highest_malus_path_type_within_bb = path_type; + } + } + + let current_node_path_type = self.get_path_type(context, pos); + let is_large_mob = self.walk.base.entity_width > 1; + if is_large_mob { + let is_current_node_cheaper = + mob_data.get_pathfinding_malus(current_node_path_type) < highest_malus_within_bb; + let cap_malus_due_to_cheap_node = is_current_node_cheaper + && mob_data.get_pathfinding_malus(PathType::BigMobsCloseToDanger) + < highest_malus_within_bb; + if cap_malus_due_to_cheap_node { + PathType::BigMobsCloseToDanger + } else { + highest_malus_path_type_within_bb + } + } else if current_node_path_type == PathType::Open + && highest_malus_path_type_within_bb != PathType::Open + && highest_malus_within_bb == 0.0 + { + PathType::Open + } else { + highest_malus_path_type_within_bb + } + } + + fn get_path_type(&mut self, context: &mut PathfindingContext, pos: Vector3) -> PathType { + let mut block_path_type = context.get_path_type_from_state(pos); + if block_path_type == PathType::Open && pos.y > context.min_y() { + let below_pos = Vector3::new(pos.x, pos.y - 1, pos.z); + let below_type = context.get_path_type_from_state(below_pos); + if below_type == PathType::DamageFire || below_type == PathType::Lava { + block_path_type = PathType::DamageFire; + } else if below_type == PathType::DamageOther { + block_path_type = PathType::DamageOther; + } else if below_type == PathType::Cocoa { + block_path_type = PathType::Cocoa; + } else if below_type == PathType::Fence { + if below_pos != context.mob_position() { + block_path_type = PathType::Fence; + } + } else { + block_path_type = if below_type != PathType::Walkable + && below_type != PathType::Open + && below_type != PathType::Water + { + PathType::Walkable + } else { + PathType::Open + }; + } + } + + if block_path_type == PathType::Walkable || block_path_type == PathType::Open { + block_path_type = context.get_node_type_from_neighbors(pos, block_path_type); + } + + block_path_type + } + + fn set_can_pass_doors(&mut self, can_pass: bool) { + self.walk.set_can_pass_doors(can_pass); + } + + fn set_can_open_doors(&mut self, can_open: bool) { + self.walk.set_can_open_doors(can_open); + } + + fn set_can_float(&mut self, can_float: bool) { + self.walk.set_can_float(can_float); + } + + fn set_can_walk_over_fences(&mut self, can_walk: bool) { + self.walk.set_can_walk_over_fences(can_walk); + } + + fn can_pass_doors(&self) -> bool { + self.walk.can_pass_doors() + } + + fn can_open_doors(&self) -> bool { + self.walk.can_open_doors() + } + + fn can_float(&self) -> bool { + self.walk.can_float() + } + + fn can_walk_over_fences(&self) -> bool { + self.walk.can_walk_over_fences() + } +} + +impl Default for FlyNodeEvaluator { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/pumpkin/src/entity/ai/pathfinder/mod.rs b/crates/pumpkin/src/entity/ai/pathfinder/mod.rs index eea1d6b2f..a27644ef6 100644 --- a/crates/pumpkin/src/entity/ai/pathfinder/mod.rs +++ b/crates/pumpkin/src/entity/ai/pathfinder/mod.rs @@ -1,28 +1,259 @@ +use pumpkin_util::math::position::BlockPos; use pumpkin_util::math::vector3::Vector3; +use pumpkin_util::math::wrap_degrees; use crate::entity::living::LivingEntity; +use crate::world::World; +use crate::entity::ai::pathfinder::amphibious_node_evaluator::AmphibiousNodeEvaluator; use crate::entity::ai::pathfinder::binary_heap::BinaryHeap; -use crate::entity::ai::pathfinder::node::Coordinate; +use crate::entity::ai::pathfinder::fly_node_evaluator::FlyNodeEvaluator; use crate::entity::ai::pathfinder::node::Node; use crate::entity::ai::pathfinder::node::PathType; use crate::entity::ai::pathfinder::node_evaluator::{MobData, NodeEvaluator}; use crate::entity::ai::pathfinder::path::Path; use crate::entity::ai::pathfinder::pathfinding_context::PathfindingContext; +use crate::entity::ai::pathfinder::swim_node_evaluator::SwimNodeEvaluator; use crate::entity::ai::pathfinder::walk_node_evaluator::WalkNodeEvaluator; use pumpkin_data::attributes::Attributes; -use pumpkin_util::math::wrap_degrees; use rustc_hash::{FxHashMap, FxHashSet}; +use std::ops::{Deref, DerefMut}; use std::sync::atomic::{AtomicBool, Ordering}; +pub mod amphibious_node_evaluator; pub mod binary_heap; +pub mod fly_node_evaluator; pub mod node; pub mod node_evaluator; pub mod path; pub mod path_type_cache; pub mod pathfinding_context; +pub mod swim_node_evaluator; pub mod walk_node_evaluator; +const MAX_ITERS: usize = 560; +const TARGET_DISTANCE_MULTIPLIER: f32 = 1.5; +const NODE_REACH_Y: f64 = 1.0; +const MAX_YAW_TURN_PER_TICK: f32 = 90.0; + +pub struct PathFinder { + max_visited_nodes: usize, + open_set: BinaryHeap, + neighbors_buf: Vec, + all_nodes: FxHashMap, Node>, +} + +impl PathFinder { + #[must_use] + pub fn new(max_visited_nodes: usize) -> Self { + Self { + max_visited_nodes, + open_set: BinaryHeap::new(), + neighbors_buf: Vec::with_capacity(32), + all_nodes: FxHashMap::default(), + } + } + + pub const fn set_max_visited_nodes(&mut self, max_visited_nodes: usize) { + self.max_visited_nodes = max_visited_nodes; + } + + #[must_use] + pub const fn get_max_visited_nodes(&self) -> usize { + self.max_visited_nodes + } + + pub fn find_path_single( + &mut self, + evaluator: &mut EvaluatorKind, + target: BlockPos, + max_path_length: f32, + reach_range: i32, + max_visited_nodes_multiplier: f32, + ) -> Option { + self.find_path( + evaluator, + &[target], + max_path_length, + reach_range, + max_visited_nodes_multiplier, + ) + } + + #[expect(clippy::too_many_lines)] + pub fn find_path( + &mut self, + evaluator: &mut EvaluatorKind, + targets: &[BlockPos], + max_path_length: f32, + reach_range: i32, + max_visited_nodes_multiplier: f32, + ) -> Option { + if targets.is_empty() { + return None; + } + + let mut from = evaluator.get_start()?; + + let mut target_entries: Vec<(crate::entity::ai::pathfinder::node::Target, BlockPos)> = + targets + .iter() + .map(|&pos| (evaluator.get_target(pos), pos)) + .collect(); + + self.all_nodes.clear(); + self.open_set.clear(); + + from.g = 0.0; + from.h = Self::get_best_h(&from, &mut target_entries); + from.f = from.h; + from.walked_dist = 0.0; + from.came_from = None; + from.closed = false; + + self.all_nodes.insert(from.pos.0, from); + self.open_set.insert(from); + + let mut count = 0usize; + let mut reached_targets: Vec = Vec::new(); + let max_visited_nodes_adjusted = + (self.max_visited_nodes as f32 * max_visited_nodes_multiplier) as usize; + + while !self.open_set.is_empty() { + count += 1; + if count >= max_visited_nodes_adjusted { + break; + } + + let Some(mut current) = self.open_set.pop() else { + break; + }; + + current.closed = true; + self.all_nodes.insert(current.pos.0, current); + + for (idx, (target, _)) in target_entries.iter_mut().enumerate() { + if current.distance_manhattan_node(&target.node) <= reach_range as f32 { + target.set_reached(); + target.update_best(0.0, ¤t); + if !reached_targets.contains(&idx) { + reached_targets.push(idx); + } + } + } + + if !reached_targets.is_empty() { + break; + } + + if current.distance_to_node(&from) < max_path_length { + self.neighbors_buf.clear(); + evaluator.get_neighbors(¤t, &mut self.neighbors_buf); + + for mut neighbor in self.neighbors_buf.drain(..) { + let distance = current.distance_to_node(&neighbor); + neighbor.walked_dist = current.walked_dist + distance; + let tentative_g = current.g + distance + neighbor.cost_malus; + + let in_open = self.open_set.contains(&neighbor); + let is_better = if in_open { + self.open_set + .get_node(&neighbor) + .is_some_and(|existing| tentative_g < existing.g) + } else { + true + }; + + if neighbor.walked_dist < max_path_length && is_better { + neighbor.came_from = Some(current.pos.0); + neighbor.g = tentative_g; + neighbor.h = Self::get_best_h(&neighbor, &mut target_entries) * 1.5; + neighbor.f = neighbor.g + neighbor.h; + + if in_open { + self.open_set.update_node(&neighbor, neighbor); + } else { + self.open_set.insert(neighbor); + } + self.all_nodes.insert(neighbor.pos.0, neighbor); + } + } + } + } + + for node in self.open_set.drain() { + self.all_nodes.entry(node.pos.0).or_insert(node); + } + + if reached_targets.is_empty() { + target_entries + .into_iter() + .filter_map(|(target, target_pos)| { + target.get_best_node().map(|best| { + Self::reconstruct_path(&best, target_pos, false, &self.all_nodes) + }) + }) + .min_by(|a, b| { + a.get_dist_to_target() + .total_cmp(&b.get_dist_to_target()) + .then_with(|| a.get_node_count().cmp(&b.get_node_count())) + }) + } else { + reached_targets + .into_iter() + .filter_map(|idx| { + let (target, target_pos) = &target_entries[idx]; + target.get_best_node().map(|best| { + Self::reconstruct_path(&best, *target_pos, true, &self.all_nodes) + }) + }) + .min_by_key(Path::get_node_count) + } + } + + fn reconstruct_path( + closest: &Node, + target: BlockPos, + reached: bool, + all_nodes: &FxHashMap, Node>, + ) -> Path { + let mut nodes = Vec::new(); + let mut current = *closest; + nodes.push(current); + let mut visited = FxHashSet::default(); + visited.insert(current.pos.0); + + while let Some(prev_pos) = current.came_from { + if prev_pos == current.pos.0 || !visited.insert(prev_pos) { + break; + } + if let Some(prev_node) = all_nodes.get(&prev_pos) { + nodes.push(*prev_node); + current = *prev_node; + } else { + break; + } + } + + nodes.reverse(); + Path::new(nodes, target, reached) + } + + fn get_best_h( + from: &Node, + targets: &mut [(crate::entity::ai::pathfinder::node::Target, BlockPos)], + ) -> f32 { + let mut best_h = f32::MAX; + for (target, _) in targets.iter_mut() { + let h = from.distance_to_node(&target.node); + target.update_best(h, from); + best_h = best_h.min(h); + } + best_h + } +} + +#[derive(Clone, Copy, Debug)] pub struct NavigatorGoal { pub current_progress: Vector3, pub destination: Vector3, @@ -44,34 +275,211 @@ impl NavigatorGoal { } } -pub struct Navigator { - current_goal: Option, - evaluator: WalkNodeEvaluator, - current_path: Option, - // Stuck detection - ticks_on_current_node: u32, - last_node_index: usize, - total_ticks: u32, - path_start_pos: Option>, - path_type_overrides: FxHashMap, - mob_width: f32, - mob_height: f32, - // Smart re-pathing cooldown - repath_cooldown: u32, - // Reusable allocations to avoid per-pathfind heap allocations - open_set: BinaryHeap, - neighbors_buf: Vec, - /// Thread-safe status check to avoid deadlocks when components (like `LookControl`) need to - /// check navigation status. +pub enum EvaluatorKind { + Walk(WalkNodeEvaluator), + Fly(FlyNodeEvaluator), + Swim(SwimNodeEvaluator), + Amphibious(AmphibiousNodeEvaluator), +} + +impl EvaluatorKind { + pub fn prepare(&mut self, context: PathfindingContext, mob_data: MobData) { + match self { + Self::Walk(e) => e.prepare(context, mob_data), + Self::Fly(e) => e.prepare(context, mob_data), + Self::Swim(e) => e.prepare(context, mob_data), + Self::Amphibious(e) => e.prepare(context, mob_data), + } + } + + pub fn done(&mut self) { + match self { + Self::Walk(e) => e.done(), + Self::Fly(e) => e.done(), + Self::Swim(e) => e.done(), + Self::Amphibious(e) => e.done(), + } + } + + pub fn get_start(&mut self) -> Option { + match self { + Self::Walk(e) => e.get_start(), + Self::Fly(e) => e.get_start(), + Self::Swim(e) => e.get_start(), + Self::Amphibious(e) => e.get_start(), + } + } + + pub fn get_target(&mut self, pos: BlockPos) -> crate::entity::ai::pathfinder::node::Target { + match self { + Self::Walk(e) => e.get_target(pos), + Self::Fly(e) => e.get_target(pos), + Self::Swim(e) => e.get_target(pos), + Self::Amphibious(e) => e.get_target(pos), + } + } + + pub fn get_neighbors(&mut self, current: &Node, out: &mut Vec) { + match self { + Self::Walk(e) => e.get_neighbors(current, out), + Self::Fly(e) => e.get_neighbors(current, out), + Self::Swim(e) => e.get_neighbors(current, out), + Self::Amphibious(e) => e.get_neighbors(current, out), + } + } + + pub fn set_can_float(&mut self, can_float: bool) { + match self { + Self::Walk(e) => e.set_can_float(can_float), + Self::Fly(e) => e.set_can_float(can_float), + Self::Swim(e) => e.set_can_float(can_float), + Self::Amphibious(e) => e.set_can_float(can_float), + } + } + + pub fn set_can_open_doors(&mut self, can_open: bool) { + match self { + Self::Walk(e) => e.set_can_open_doors(can_open), + Self::Fly(e) => e.set_can_open_doors(can_open), + Self::Swim(e) => e.set_can_open_doors(can_open), + Self::Amphibious(e) => e.set_can_open_doors(can_open), + } + } + + pub fn set_can_pass_doors(&mut self, can_pass: bool) { + match self { + Self::Walk(e) => e.set_can_pass_doors(can_pass), + Self::Fly(e) => e.set_can_pass_doors(can_pass), + Self::Swim(e) => e.set_can_pass_doors(can_pass), + Self::Amphibious(e) => e.set_can_pass_doors(can_pass), + } + } + + pub fn set_can_walk_over_fences(&mut self, can_walk: bool) { + match self { + Self::Walk(e) => e.set_can_walk_over_fences(can_walk), + Self::Fly(e) => e.set_can_walk_over_fences(can_walk), + Self::Swim(e) => e.set_can_walk_over_fences(can_walk), + Self::Amphibious(e) => e.set_can_walk_over_fences(can_walk), + } + } +} + +pub trait PathNavigationTrait: Send + Sync { + fn set_progress(&mut self, goal: NavigatorGoal); + fn set_speed(&mut self, speed: f64); + fn stop(&mut self); + fn is_idle(&self) -> bool; + fn is_done(&self) -> bool; + fn is_in_progress(&self) -> bool; + fn is_stuck(&self) -> bool; + fn get_path(&self) -> Option<&Path>; + fn get_path_mut(&mut self) -> Option<&mut Path>; + fn set_pathfinding_malus(&mut self, path_type: PathType, malus: f32); + fn get_pathfinding_malus(&self, path_type: PathType) -> f32; + fn set_mob_dimensions(&mut self, width: f32, height: f32); + fn can_reach_within( + &mut self, + entity: &LivingEntity, + destination: Vector3, + distance: f32, + ) -> bool; + fn tick(&mut self, entity: &LivingEntity); + fn move_to_coords(&mut self, x: f64, y: f64, z: f64, speed: f64, entity: &LivingEntity) + -> bool; + fn move_to_pos(&mut self, pos: BlockPos, speed: f64, entity: &LivingEntity) -> bool; + fn move_to_entity(&mut self, target: &LivingEntity, speed: f64, entity: &LivingEntity) -> bool; + fn move_to_path(&mut self, path: Option, speed: f64, entity: &LivingEntity) -> bool; + fn create_path( + &mut self, + entity: &LivingEntity, + destination: Vector3, + reach_range: i32, + ) -> Option; + fn recompute_path(&mut self, entity: &LivingEntity); + fn set_avoid_sun(&mut self, avoid_sun: bool); + fn set_can_walk_over_fences(&mut self, can_walk: bool); + fn set_can_open_doors(&mut self, can_open: bool); + fn set_can_pass_doors(&mut self, can_pass: bool); + fn set_can_float(&mut self, can_float: bool); + fn can_float(&self) -> bool; + fn can_navigate_ground(&self) -> bool; + fn set_required_path_length(&mut self, length: f32); + fn set_max_visited_nodes_multiplier(&mut self, multiplier: f32); + fn reset_max_visited_nodes_multiplier(&mut self); + fn get_target_pos(&self) -> Option; + fn can_path_to_targets_below_surface(&self) -> bool; + fn set_can_path_to_targets_below_surface(&mut self, can_path: bool); +} + +pub struct PathNavigation { + pub current_goal: Option, + pub evaluator: EvaluatorKind, + pub path: Option, + pub speed_modifier: f64, + pub tick_count: u32, + pub last_stuck_check: u32, + pub last_stuck_check_pos: Vector3, + pub timeout_cached_node: Vector3, + pub timeout_timer: u64, + pub last_timeout_check: u64, + pub timeout_limit: f64, + pub max_distance_to_waypoint: f32, + pub has_delayed_recomputation: bool, + pub time_last_recompute: u64, + pub target_pos: Option, + pub reach_range: i32, + pub max_visited_nodes_multiplier: f32, + pub is_stuck: bool, + pub required_path_length: f32, + pub ticks_on_current_node: u32, + pub last_node_index: usize, + pub total_ticks: u32, + pub path_start_pos: Option>, + pub path_type_overrides: FxHashMap, + pub mob_width: f32, + pub mob_height: f32, + pub repath_cooldown: u32, + pub can_float: bool, + pub can_walk_over_fences: bool, + pub can_open_doors: bool, + pub can_pass_doors: bool, + pub avoid_sun: bool, + pub can_path_to_targets_below_surface: bool, + pub open_set: BinaryHeap, + pub neighbors_buf: Vec, pub is_idle: AtomicBool, } -impl Default for Navigator { +impl Default for PathNavigation { fn default() -> Self { + Self::new(EvaluatorKind::Walk(WalkNodeEvaluator::default())) + } +} + +impl PathNavigation { + #[must_use] + pub fn new(evaluator: EvaluatorKind) -> Self { Self { current_goal: None, - evaluator: WalkNodeEvaluator::default(), - current_path: None, + evaluator, + path: None, + speed_modifier: 1.0, + tick_count: 0, + last_stuck_check: 0, + last_stuck_check_pos: Vector3::new(0.0, 0.0, 0.0), + timeout_cached_node: Vector3::new(0, 0, 0), + timeout_timer: 0, + last_timeout_check: 0, + timeout_limit: 0.0, + max_distance_to_waypoint: 0.5, + has_delayed_recomputation: false, + time_last_recompute: 0, + target_pos: None, + reach_range: 1, + max_visited_nodes_multiplier: 1.0, + is_stuck: false, + required_path_length: 16.0, ticks_on_current_node: 0, last_node_index: 0, total_ticks: 0, @@ -80,31 +488,27 @@ impl Default for Navigator { mob_width: 0.6, mob_height: 1.95, repath_cooldown: 0, + can_float: false, + can_walk_over_fences: false, + can_open_doors: false, + can_pass_doors: true, + avoid_sun: false, + can_path_to_targets_below_surface: false, open_set: BinaryHeap::new(), neighbors_buf: Vec::new(), is_idle: AtomicBool::new(true), } } -} -// If I counted correctly this should be equal to the number of iters that vanilla does for -// a zombie (yes, vanilla does a different number of iterations based on the mob and some -// other things) -// TODO: Calculate from mob attributes like in vanilla -const MAX_ITERS: usize = 560; -const TARGET_DISTANCE_MULTIPLIER: f32 = 1.5; -const NODE_REACH_XZ: f64 = 0.5; -const NODE_REACH_Y: f64 = 1.0; -const MAX_YAW_TURN_PER_TICK: f32 = 90.0; - -impl Navigator { pub fn set_progress(&mut self, goal: NavigatorGoal) { self.is_idle.store(false, Ordering::Relaxed); + self.speed_modifier = goal.speed; self.current_goal = Some(goal); - self.current_path = None; + self.path = None; } pub const fn set_speed(&mut self, speed: f64) { + self.speed_modifier = speed; if let Some(goal) = &mut self.current_goal { goal.speed = speed; } @@ -113,13 +517,14 @@ impl Navigator { pub fn stop(&mut self) { self.is_idle.store(true, Ordering::Relaxed); self.current_goal = None; - self.current_path = None; + self.path = None; self.ticks_on_current_node = 0; self.total_ticks = 0; self.path_start_pos = None; + self.reset_stuck_timeout(); } - fn finish_navigation(&mut self, entity: &LivingEntity) { + pub fn finish_navigation(&mut self, entity: &LivingEntity) { self.stop(); entity.movement_input.store(Vector3::new(0.0, 0.0, 0.0)); entity.jumping.store(false, Ordering::Relaxed); @@ -148,12 +553,22 @@ impl Navigator { destination: Vector3, distance: f32, ) -> bool { - self.compute_path(entity, destination) + self.compute_path(entity, destination, 1) .is_some_and(|path| path.can_reach() || path.get_dist_to_target() <= distance) } + fn mob_max_follow_range(&self, entity: &LivingEntity) -> f32 { + let follow_range = entity.get_attribute_value(&Attributes::FOLLOW_RANGE) as f32; + follow_range.max(self.required_path_length) + } + #[allow(clippy::too_many_lines)] - fn compute_path(&mut self, entity: &LivingEntity, destination: Vector3) -> Option { + pub fn compute_path( + &mut self, + entity: &LivingEntity, + destination: Vector3, + reach_range: i32, + ) -> Option { let start_pos_f = entity.entity.pos.load(); let start_block_vec = start_pos_f.to_i32(); let mob_position = Vector3::new(start_block_vec.x, start_block_vec.y, start_block_vec.z); @@ -161,54 +576,61 @@ impl Navigator { let context = PathfindingContext::new(mob_position, entity.entity.world.load_full()); let mut mob_data = MobData::new(start_pos_f, self.mob_width, self.mob_height, 1.0); mob_data.on_ground = entity.entity.on_ground.load(Ordering::Relaxed); + mob_data.can_swim = self.can_float; + mob_data.set_pathfinding_malus(PathType::DangerFire, 16.0); mob_data.set_pathfinding_malus(PathType::DamageFire, -1.0); - mob_data.set_pathfinding_malus(PathType::Water, 8.0); + mob_data.set_pathfinding_malus(PathType::Water, if self.can_float { 0.0 } else { 8.0 }); mob_data.set_pathfinding_malus(PathType::Lava, -1.0); mob_data.set_pathfinding_malus(PathType::DangerOther, 8.0); - // Apply per-mob pathfinding malus overrides for (&path_type, &malus) in &self.path_type_overrides { mob_data.set_pathfinding_malus(path_type, malus); } + self.evaluator.set_can_float(self.can_float); + self.evaluator.set_can_open_doors(self.can_open_doors); + self.evaluator.set_can_pass_doors(self.can_pass_doors); + self.evaluator + .set_can_walk_over_fences(self.can_walk_over_fences); self.evaluator.prepare(context, mob_data); let mut start_node = self.evaluator.get_start()?; - let mut target = self.evaluator.get_target(destination.to_block_pos()); start_node.g = 0.0; let start_dist = start_node.distance(&target); target.update_best(start_dist, &start_node); - // Start node uses raw distance (no 1.5x multiplier - that's only for neighbors) start_node.h = start_dist; start_node.f = start_node.h; start_node.walked_dist = 0.0; start_node.came_from = None; let start_pos = start_node.pos.0; - - // Map to store closed nodes for path reconstruction let mut closed_set: FxHashMap, Node> = FxHashMap::default(); - // Reuse the navigator's open_set and neighbors_buf self.open_set.clear(); self.open_set.insert(start_node); let mut iterations = 0usize; let mut reached = false; + let max_iters = ((self.mob_max_follow_range(entity) + * 16.0 + * self.max_visited_nodes_multiplier) as usize) + .clamp(100, 2048) + .max(MAX_ITERS); while !self.open_set.is_empty() { iterations += 1; - if iterations >= MAX_ITERS { + if iterations >= max_iters { break; } let Some(current) = self.open_set.pop() else { break; }; - if current.distance_manhattan(&target) < 1.0 { + + if current.distance_manhattan(&target) <= reach_range as f32 { target.reached = true; reached = true; target.update_best(0.0, ¤t); @@ -216,15 +638,12 @@ impl Navigator { break; } - let euclidean_from_start = { - let dx = (current.pos.0.x - start_pos.x) as f32; - let dy = (current.pos.0.y - start_pos.y) as f32; - let dz = (current.pos.0.z - start_pos.z) as f32; - (dx * dx + dy * dy + dz * dz).sqrt() - }; - - let follow_range = entity.get_attribute_value(&Attributes::FOLLOW_RANGE) as f32; - if euclidean_from_start >= follow_range { + let dx = (current.pos.0.x - start_pos.x) as f32; + let dy = (current.pos.0.y - start_pos.y) as f32; + let dz = (current.pos.0.z - start_pos.z) as f32; + let euclidean = (dx * dx + dy * dy + dz * dz).sqrt(); + let follow_range = self.mob_max_follow_range(entity); + if euclidean >= follow_range { closed_set.insert(current.pos.0, current); continue; } @@ -264,7 +683,8 @@ impl Navigator { closed_set.insert(current.pos.0, current); } - // Also store any remaining open set nodes for path reconstruction + self.evaluator.done(); + for node in self.open_set.drain() { closed_set.entry(node.pos.0).or_insert(node); } @@ -278,7 +698,7 @@ impl Navigator { while let Some(node) = closed_set.get(¤t_pos) { if let Some(prev_pos) = node.came_from { if prev_pos == current_pos || !visited.insert(prev_pos) { - break; // Self-reference or cycle detected + break; } if let Some(&prev_node) = closed_set.get(&prev_pos) { path_nodes.push(prev_node); @@ -292,38 +712,284 @@ impl Navigator { } path_nodes.reverse(); - let path_target = target.node.pos.0; + let path_target = target.node.pos; return Some(Path::new(path_nodes, path_target, reached)); } None } - fn needs_new_path(&self, goal: &NavigatorGoal) -> bool { - if self.current_path.is_none() { + pub fn needs_new_path(&self, goal: &NavigatorGoal) -> bool { + if self.path.is_none() { return true; } if self.repath_cooldown > 0 { return false; } - self.current_path.as_ref().is_some_and(|p| { + self.path.as_ref().is_some_and(|p| { let path_target = p.get_target(); let goal_target = goal.destination.to_i32(); - let dx = f64::from(path_target.x - goal_target.x); - let dy = f64::from(path_target.y - goal_target.y); - let dz = f64::from(path_target.z - goal_target.z); + let dx = f64::from(path_target.0.x - goal_target.x); + let dy = f64::from(path_target.0.y - goal_target.y); + let dz = f64::from(path_target.0.z - goal_target.z); let distance_sq = dx * dx + dy * dy + dz * dz; - // Adaptive threshold based on remaining distance let remaining = p.get_remaining_distance().clamp(4.0, 16.0); let threshold = remaining * 0.5; distance_sq > f64::from(threshold * threshold) }) } + pub fn can_move_directly( + world: &World, + start_pos: Vector3, + stop_pos: Vector3, + mob_height: f32, + blocked_by_fluids: bool, + ) -> bool { + let to = Vector3::new( + stop_pos.x, + stop_pos.y + f64::from(mob_height) * 0.5, + stop_pos.z, + ); + let diff = to - start_pos; + let dist = diff.length(); + if dist < 1e-5 { + return true; + } + let steps = (dist * 2.0).ceil() as usize; + let step_vec = diff / (steps as f64); + let mut current = start_pos; + for _ in 0..steps { + current += step_vec; + let bp = BlockPos::new( + current.x.floor() as i32, + current.y.floor() as i32, + current.z.floor() as i32, + ); + let block_state = world.get_block_state(&bp); + if !block_state.is_air() { + if blocked_by_fluids && block_state.is_liquid() { + return false; + } + if block_state.is_solid() { + return false; + } + } + } + true + } + + pub fn find_surface_position(world: &World, mut pos: BlockPos) -> BlockPos { + let block_state = world.get_block_state(&pos); + if block_state.is_air() { + let mut column_pos = pos; + while column_pos.0.y >= -64 && world.get_block_state(&column_pos).is_air() { + column_pos.0.y -= 1; + } + if column_pos.0.y >= -64 { + return BlockPos::new(column_pos.0.x, column_pos.0.y + 1, column_pos.0.z); + } + column_pos.0.y = pos.0.y + 1; + while column_pos.0.y <= 320 && world.get_block_state(&column_pos).is_air() { + column_pos.0.y += 1; + } + pos = column_pos; + } + if !world.get_block_state(&pos).is_solid() { + return pos; + } + let mut column_pos = pos; + while column_pos.0.y <= 320 && world.get_block_state(&column_pos).is_solid() { + column_pos.0.y += 1; + } + column_pos + } + + pub fn get_surface_y(&self, entity: &LivingEntity) -> f64 { + let pos = entity.entity.pos.load(); + if entity.entity.touching_water.load(Ordering::Relaxed) && self.can_float { + let mut surface = pos.y.floor() as i32; + let world = entity.entity.world.load(); + let mut steps = 0; + loop { + let bp = BlockPos::new(pos.x.floor() as i32, surface, pos.z.floor() as i32); + let state = world.get_block_state(&bp); + if state.is_liquid() { + surface += 1; + steps += 1; + if steps > 16 { + return pos.y; + } + } else { + break; + } + } + f64::from(surface) + } else { + (pos.y + 0.5).floor() + } + } + + pub const fn reset_stuck_timeout(&mut self) { + self.timeout_cached_node = Vector3::new(0, 0, 0); + self.timeout_timer = 0; + self.timeout_limit = 0.0; + self.is_stuck = false; + } + + pub fn do_stuck_detection(&mut self, mob_pos: Vector3, entity: &LivingEntity) { + let world_age = entity.entity.world.load().get_world_age() as u64; + if self.tick_count.saturating_sub(self.last_stuck_check) > 100 { + let speed = entity.get_attribute_value(&Attributes::MOVEMENT_SPEED) as f32; + let effective_speed = if speed >= 1.0 { speed } else { speed * speed }; + let threshold_distance = effective_speed * 100.0 * 0.25; + let dx = mob_pos.x - self.last_stuck_check_pos.x; + let dy = mob_pos.y - self.last_stuck_check_pos.y; + let dz = mob_pos.z - self.last_stuck_check_pos.z; + let dist_sq = dx * dx + dy * dy + dz * dz; + + if dist_sq < f64::from(threshold_distance * threshold_distance) { + self.is_stuck = true; + self.stop(); + } else { + self.is_stuck = false; + } + self.last_stuck_check = self.tick_count; + self.last_stuck_check_pos = mob_pos; + } + + if let Some(path) = &self.path + && !path.is_done() + && let Some(pos) = path.get_next_node_pos() + { + if pos.0 == self.timeout_cached_node { + self.timeout_timer = self + .timeout_timer + .saturating_add(world_age.saturating_sub(self.last_timeout_check)); + } else { + self.timeout_cached_node = pos.0; + let node_center = Vector3::new( + f64::from(pos.0.x) + 0.5, + f64::from(pos.0.y), + f64::from(pos.0.z) + 0.5, + ); + let dist_to_node = (mob_pos - node_center).length(); + let speed = entity.get_attribute_value(&Attributes::MOVEMENT_SPEED); + self.timeout_limit = if speed > 0.0 { + dist_to_node / speed * 20.0 + } else { + 0.0 + }; + } + + if self.timeout_limit > 0.0 && self.timeout_timer as f64 > self.timeout_limit * 3.0 { + self.reset_stuck_timeout(); + self.stop(); + } + self.last_timeout_check = world_age; + } + } + + #[must_use] + pub fn should_target_next_node_in_direction(mob_pos: Vector3, path: &Path) -> bool { + if path.get_next_node_index() + 1 >= path.get_node_count() { + return false; + } + let Some(curr_pos) = path.get_next_node_pos() else { + return false; + }; + let current_node = Vector3::new( + f64::from(curr_pos.0.x) + 0.5, + f64::from(curr_pos.0.y), + f64::from(curr_pos.0.z) + 0.5, + ); + let dx = mob_pos.x - current_node.x; + let dy = mob_pos.y - current_node.y; + let dz = mob_pos.z - current_node.z; + if dx * dx + dy * dy + dz * dz > 4.0 { + return false; + } + + let next_idx = path.get_next_node_index() + 1; + let Some(next_pos) = path.get_node_pos(next_idx) else { + return false; + }; + let next_node = Vector3::new( + f64::from(next_pos.0.x) + 0.5, + f64::from(next_pos.0.y), + f64::from(next_pos.0.z) + 0.5, + ); + + let mob_to_current = current_node - mob_pos; + let mob_to_next = next_node - mob_pos; + let mob_to_curr_sqr = mob_to_current.length_squared(); + let mob_to_next_sqr = mob_to_next.length_squared(); + + let closer_to_next = mob_to_next_sqr < mob_to_curr_sqr; + let within_curr = mob_to_curr_sqr < 0.5; + if !closer_to_next && !within_curr { + return false; + } + + let curr_len = mob_to_current.length(); + let next_len = mob_to_next.length(); + if curr_len < 1e-5 || next_len < 1e-5 { + return false; + } + let mob_dir = mob_to_current / curr_len; + let path_dir = mob_to_next / next_len; + path_dir.dot(&mob_dir) < 0.0 + } + + pub fn trim_path(&mut self, entity: &LivingEntity) { + if self.avoid_sun { + let pos = entity.entity.pos.load(); + let bp = BlockPos::new( + pos.x.floor() as i32, + (pos.y + 0.5).floor() as i32, + pos.z.floor() as i32, + ); + let world = entity.entity.world.load(); + if world.get_sky_light_level(&bp) < 15 + && let Some(path) = &mut self.path + { + let mut cut_index = None; + for i in 0..path.get_node_count() { + if let Some(node) = path.get_node(i) { + let node_bp = BlockPos::new(node.pos.0.x, node.pos.0.y, node.pos.0.z); + if world.get_sky_light_level(&node_bp) >= 15 { + cut_index = Some(i); + break; + } + } + } + if let Some(idx) = cut_index { + path.truncate_nodes(idx); + } + } + } + } + #[allow(clippy::too_many_lines)] - pub fn tick(&mut self, entity: &LivingEntity) { + pub fn tick_ground(&mut self, entity: &LivingEntity) { + self.tick_count += 1; + let world_age = entity.entity.world.load().get_world_age() as u64; + + if self.has_delayed_recomputation + && world_age.saturating_sub(self.time_last_recompute) > 20 + && let Some(target_pos) = self.target_pos + { + let target_v = Vector3::new( + f64::from(target_pos.0.x) + 0.5, + f64::from(target_pos.0.y), + f64::from(target_pos.0.z) + 0.5, + ); + self.path = self.compute_path(entity, target_v, self.reach_range); + self.time_last_recompute = world_age; + self.has_delayed_recomputation = false; + } + let Some(goal) = self.current_goal.take() else { - // Idle: stop the mob self.is_idle.store(true, Ordering::Relaxed); entity.movement_input.store(Vector3::new(0.0, 0.0, 0.0)); return; @@ -340,20 +1006,40 @@ impl Navigator { } if self.needs_new_path(&goal) { - self.current_path = self.compute_path(entity, goal.destination); + let mut dest_pos = goal.destination.to_block_pos(); + if !self.can_path_to_targets_below_surface { + let world = entity.entity.world.load(); + dest_pos = Self::find_surface_position(&world, dest_pos); + } + let dest_v = Vector3::new( + f64::from(dest_pos.0.x) + 0.5, + f64::from(dest_pos.0.y), + f64::from(dest_pos.0.z) + 0.5, + ); + self.path = self.compute_path(entity, dest_v, self.reach_range); self.ticks_on_current_node = 0; self.last_node_index = 0; self.path_start_pos = Some(entity.entity.pos.load()); - self.repath_cooldown = 15; // ~0.75 seconds cooldown before recomputing + self.repath_cooldown = 15; + self.time_last_recompute = world_age; } - if self.current_path.is_none() { + if self.path.is_none() { self.finish_navigation(entity); return; } - if let Some(path) = &mut self.current_path { - if path.is_done() || !path.is_valid() { + let surface_y = self.get_surface_y(entity); + let mob_pos = Vector3::new( + entity.entity.pos.load().x, + surface_y, + entity.entity.pos.load().z, + ); + + self.do_stuck_detection(mob_pos, entity); + + if let Some(path) = &mut self.path { + if path.is_done() { self.finish_navigation(entity); return; } @@ -378,7 +1064,7 @@ impl Navigator { let dy = current_pos.y - start_pos.y; let dz = current_pos.z - start_pos.z; let dist_sq = dx * dx + dy * dy + dz * dz; - if dist_sq < 2.0 * 2.0 { + if dist_sq < 4.0 { self.finish_navigation(entity); return; } @@ -390,9 +1076,9 @@ impl Navigator { if let Some(next_block) = path.get_next_node_pos() { let target_pos = Vector3::new( - f64::from(next_block.x) + 0.5, - f64::from(next_block.y), - f64::from(next_block.z) + 0.5, + f64::from(next_block.0.x) + 0.5, + f64::from(next_block.0.y), + f64::from(next_block.0.z) + 0.5, ); let current_pos = entity.entity.pos.load(); @@ -403,14 +1089,31 @@ impl Navigator { let horizontal_dist_sq = dx * dx + dz * dz; let horizontal_dist = horizontal_dist_sq.sqrt(); - // Skip node if we're above it on the same XZ column and airborne (falling toward it) - if !on_ground && horizontal_dist < NODE_REACH_XZ && dy < -0.5 { + self.max_distance_to_waypoint = if self.mob_width > 0.75 { + self.mob_width * 0.5 + } else { + 0.75 - self.mob_width * 0.5 + }; + + if !on_ground + && horizontal_dist < f64::from(self.max_distance_to_waypoint) + && dy < -0.5 + { path.advance(); self.current_goal = Some(goal); return; } - if horizontal_dist < NODE_REACH_XZ && dy.abs() < NODE_REACH_Y { + let close_enough = horizontal_dist < f64::from(self.max_distance_to_waypoint) + && dy.abs() < NODE_REACH_Y; + + let corner_cut = path.get_next_node().is_some_and(|n| { + n.path_type != PathType::DangerFire + && n.path_type != PathType::DamageFire + && n.path_type != PathType::WalkableDoor + }) && Self::should_target_next_node_in_direction(mob_pos, path); + + if close_enough || corner_cut { path.advance(); self.current_goal = Some(goal); return; @@ -425,7 +1128,6 @@ impl Navigator { entity.entity.head_yaw.store(target_yaw); entity.entity.body_yaw.store(target_yaw); - // Get movement speed from goal and mob attributes let mob_speed = goal.speed * entity.get_attribute_value(&Attributes::MOVEMENT_SPEED); @@ -433,21 +1135,15 @@ impl Navigator { .movement_input .store(Vector3::new(0.0, 0.0, mob_speed)); - let bbox = entity.entity.bounding_box.load(); - let width = bbox.max.x - bbox.min.x; - let jump_distance = 1.0f64.max(width); + let step_height = entity.get_attribute_value(&Attributes::STEP_HEIGHT); + let jump_distance = 1.0f64.max(f64::from(self.mob_width)); - // Jump when the next node is above step height and we're close enough horizontally - if dy > entity.get_attribute_value(&Attributes::STEP_HEIGHT) - && horizontal_dist_sq < jump_distance + if (dy > step_height || f64::from(next_block.0.y) > current_pos.y) + && horizontal_dist_sq < jump_distance * jump_distance { - entity - .jumping - .store(true, std::sync::atomic::Ordering::SeqCst); + entity.jumping.store(true, Ordering::SeqCst); } else { - entity - .jumping - .store(false, std::sync::atomic::Ordering::SeqCst); + entity.jumping.store(false, Ordering::SeqCst); } } else { self.finish_navigation(entity); @@ -457,19 +1153,1422 @@ impl Navigator { self.current_goal = Some(goal); } +} - #[must_use] - pub fn is_idle(&self) -> bool { - self.is_idle.load(Ordering::Relaxed) - } +pub struct GroundPathNavigation { + pub inner: PathNavigation, +} - #[must_use] - pub const fn get_path(&self) -> Option<&Path> { - self.current_path.as_ref() - } - - #[must_use] - pub const fn get_path_mut(&mut self) -> Option<&mut Path> { - self.current_path.as_mut() +impl Default for GroundPathNavigation { + fn default() -> Self { + Self::new() + } +} + +impl GroundPathNavigation { + #[must_use] + pub fn new() -> Self { + Self { + inner: PathNavigation::new(EvaluatorKind::Walk(WalkNodeEvaluator::default())), + } + } +} + +impl PathNavigationTrait for GroundPathNavigation { + fn set_progress(&mut self, goal: NavigatorGoal) { + self.inner.set_progress(goal); + } + + fn set_speed(&mut self, speed: f64) { + self.inner.set_speed(speed); + } + + fn stop(&mut self) { + self.inner.stop(); + } + + fn is_idle(&self) -> bool { + self.inner.is_idle.load(Ordering::Relaxed) + } + + fn is_done(&self) -> bool { + self.inner.path.as_ref().is_none_or(Path::is_done) + } + + fn is_in_progress(&self) -> bool { + !self.is_done() + } + + fn is_stuck(&self) -> bool { + self.inner.is_stuck + } + + fn get_path(&self) -> Option<&Path> { + self.inner.path.as_ref() + } + + fn get_path_mut(&mut self) -> Option<&mut Path> { + self.inner.path.as_mut() + } + + fn set_pathfinding_malus(&mut self, path_type: PathType, malus: f32) { + self.inner.set_pathfinding_malus(path_type, malus); + } + + fn get_pathfinding_malus(&self, path_type: PathType) -> f32 { + self.inner.get_pathfinding_malus(path_type) + } + + fn set_mob_dimensions(&mut self, width: f32, height: f32) { + self.inner.set_mob_dimensions(width, height); + } + + fn can_reach_within( + &mut self, + entity: &LivingEntity, + destination: Vector3, + distance: f32, + ) -> bool { + self.inner.can_reach_within(entity, destination, distance) + } + + fn tick(&mut self, entity: &LivingEntity) { + self.inner.tick_ground(entity); + } + + fn move_to_coords( + &mut self, + x: f64, + y: f64, + z: f64, + speed: f64, + entity: &LivingEntity, + ) -> bool { + let pos = entity.entity.pos.load(); + self.set_progress(NavigatorGoal::new(pos, Vector3::new(x, y, z), speed)); + true + } + + fn move_to_pos(&mut self, pos: BlockPos, speed: f64, entity: &LivingEntity) -> bool { + let p = entity.entity.pos.load(); + let target = Vector3::new( + f64::from(pos.0.x) + 0.5, + f64::from(pos.0.y) + 0.5, + f64::from(pos.0.z) + 0.5, + ); + self.set_progress(NavigatorGoal::new(p, target, speed)); + true + } + + fn move_to_entity(&mut self, target: &LivingEntity, speed: f64, entity: &LivingEntity) -> bool { + let p = entity.entity.pos.load(); + let target_pos = target.entity.pos.load(); + self.set_progress(NavigatorGoal::new(p, target_pos, speed)); + true + } + + fn move_to_path(&mut self, path: Option, speed: f64, entity: &LivingEntity) -> bool { + if let Some(new_path) = path { + self.inner.path = Some(new_path); + if self.is_done() { + return false; + } + self.inner.trim_path(entity); + if self + .inner + .path + .as_ref() + .map_or(0, path::Path::get_node_count) + == 0 + { + return false; + } + self.inner.speed_modifier = speed; + let mob_pos = Vector3::new( + entity.entity.pos.load().x, + self.inner.get_surface_y(entity), + entity.entity.pos.load().z, + ); + self.inner.last_stuck_check = self.inner.tick_count; + self.inner.last_stuck_check_pos = mob_pos; + self.inner.is_idle.store(false, Ordering::Relaxed); + true + } else { + self.inner.path = None; + self.inner.is_idle.store(true, Ordering::Relaxed); + false + } + } + + fn create_path( + &mut self, + entity: &LivingEntity, + destination: Vector3, + reach_range: i32, + ) -> Option { + let mut dest_pos = destination.to_block_pos(); + if !self.inner.can_path_to_targets_below_surface { + let world = entity.entity.world.load(); + dest_pos = PathNavigation::find_surface_position(&world, dest_pos); + } + let dest_v = Vector3::new( + f64::from(dest_pos.0.x) + 0.5, + f64::from(dest_pos.0.y), + f64::from(dest_pos.0.z) + 0.5, + ); + self.inner.compute_path(entity, dest_v, reach_range) + } + + fn recompute_path(&mut self, entity: &LivingEntity) { + let world_age = entity.entity.world.load().get_world_age() as u64; + if world_age.saturating_sub(self.inner.time_last_recompute) <= 20 { + self.inner.has_delayed_recomputation = true; + } else if let Some(target_pos) = self.inner.target_pos { + let target_v = Vector3::new( + f64::from(target_pos.0.x) + 0.5, + f64::from(target_pos.0.y), + f64::from(target_pos.0.z) + 0.5, + ); + self.inner.path = self + .inner + .compute_path(entity, target_v, self.inner.reach_range); + self.inner.time_last_recompute = world_age; + self.inner.has_delayed_recomputation = false; + } + } + + fn set_avoid_sun(&mut self, avoid_sun: bool) { + self.inner.avoid_sun = avoid_sun; + } + + fn set_can_walk_over_fences(&mut self, can_walk: bool) { + self.inner.can_walk_over_fences = can_walk; + } + + fn set_can_open_doors(&mut self, can_open: bool) { + self.inner.can_open_doors = can_open; + } + + fn set_can_pass_doors(&mut self, can_pass: bool) { + self.inner.can_pass_doors = can_pass; + } + + fn set_can_float(&mut self, can_float: bool) { + self.inner.can_float = can_float; + } + + fn can_float(&self) -> bool { + self.inner.can_float + } + + fn can_navigate_ground(&self) -> bool { + true + } + + fn set_required_path_length(&mut self, length: f32) { + self.inner.required_path_length = length; + } + + fn set_max_visited_nodes_multiplier(&mut self, multiplier: f32) { + self.inner.max_visited_nodes_multiplier = multiplier; + } + + fn reset_max_visited_nodes_multiplier(&mut self) { + self.inner.max_visited_nodes_multiplier = 1.0; + } + + fn get_target_pos(&self) -> Option { + self.inner.target_pos + } + + fn can_path_to_targets_below_surface(&self) -> bool { + self.inner.can_path_to_targets_below_surface + } + + fn set_can_path_to_targets_below_surface(&mut self, can_path: bool) { + self.inner.can_path_to_targets_below_surface = can_path; + } +} + +pub struct FlyingPathNavigation { + pub inner: PathNavigation, +} + +impl Default for FlyingPathNavigation { + fn default() -> Self { + Self::new() + } +} + +impl FlyingPathNavigation { + #[must_use] + pub fn new() -> Self { + Self { + inner: PathNavigation::new(EvaluatorKind::Fly(FlyNodeEvaluator::default())), + } + } +} + +impl PathNavigationTrait for FlyingPathNavigation { + fn set_progress(&mut self, goal: NavigatorGoal) { + self.inner.set_progress(goal); + } + + fn set_speed(&mut self, speed: f64) { + self.inner.set_speed(speed); + } + + fn stop(&mut self) { + self.inner.stop(); + } + + fn is_idle(&self) -> bool { + self.inner.is_idle.load(Ordering::Relaxed) + } + + fn is_done(&self) -> bool { + self.inner.path.as_ref().is_none_or(Path::is_done) + } + + fn is_in_progress(&self) -> bool { + !self.is_done() + } + + fn is_stuck(&self) -> bool { + self.inner.is_stuck + } + + fn get_path(&self) -> Option<&Path> { + self.inner.path.as_ref() + } + + fn get_path_mut(&mut self) -> Option<&mut Path> { + self.inner.path.as_mut() + } + + fn set_pathfinding_malus(&mut self, path_type: PathType, malus: f32) { + self.inner.set_pathfinding_malus(path_type, malus); + } + + fn get_pathfinding_malus(&self, path_type: PathType) -> f32 { + self.inner.get_pathfinding_malus(path_type) + } + + fn set_mob_dimensions(&mut self, width: f32, height: f32) { + self.inner.set_mob_dimensions(width, height); + } + + fn can_reach_within( + &mut self, + entity: &LivingEntity, + destination: Vector3, + distance: f32, + ) -> bool { + self.inner.can_reach_within(entity, destination, distance) + } + + #[allow(clippy::too_many_lines)] + fn tick(&mut self, entity: &LivingEntity) { + self.inner.tick_count += 1; + let world_age = entity.entity.world.load().get_world_age() as u64; + + if self.inner.has_delayed_recomputation { + self.recompute_path(entity); + } + + if let Some(goal) = self.inner.current_goal.take() { + if self.inner.needs_new_path(&goal) { + self.inner.path = self.inner.compute_path(entity, goal.destination, 1); + self.inner.ticks_on_current_node = 0; + self.inner.last_node_index = 0; + self.inner.path_start_pos = Some(entity.entity.pos.load()); + self.inner.repath_cooldown = 15; + self.inner.time_last_recompute = world_age; + } + self.inner.current_goal = Some(goal); + } + + let mob_pos = entity.entity.pos.load(); + self.inner.do_stuck_detection(mob_pos, entity); + + if self.is_done() { + self.inner.finish_navigation(entity); + } else { + if let Some(path) = &mut self.inner.path + && let Some(pos) = path.get_next_node_pos() + { + let target_pos = Vector3::new( + f64::from(pos.0.x) + 0.5, + f64::from(pos.0.y) + 0.5, + f64::from(pos.0.z) + 0.5, + ); + let current_pos = entity.entity.pos.load(); + let dx = target_pos.x - current_pos.x; + let dy = target_pos.y - current_pos.y; + let dz = target_pos.z - current_pos.z; + let dist_sq = dx * dx + dy * dy + dz * dz; + + if dist_sq < 0.5 * 0.5 { + path.advance(); + } + } + + if !self.is_done() + && let Some(path) = &self.inner.path + && let Some(next_block) = path.get_next_node_pos() + { + let target_pos = Vector3::new( + f64::from(next_block.0.x) + 0.5, + f64::from(next_block.0.y) + 0.5, + f64::from(next_block.0.z) + 0.5, + ); + let current_pos = entity.entity.pos.load(); + let dx = target_pos.x - current_pos.x; + let dy = target_pos.y - current_pos.y; + let dz = target_pos.z - current_pos.z; + let sd = dx.hypot(dz); + + let desired_yaw = wrap_degrees((dz.atan2(dx) as f32).to_degrees() - 90.0); + let desired_pitch = wrap_degrees(-(dy.atan2(sd) as f32).to_degrees()); + + entity.entity.yaw.store(desired_yaw); + entity.entity.head_yaw.store(desired_yaw); + entity.entity.body_yaw.store(desired_yaw); + entity.entity.pitch.store(desired_pitch); + + let flying_speed = entity.get_attribute_value(&Attributes::FLYING_SPEED); + let base_speed = if flying_speed > 0.0 { + flying_speed + } else { + entity.get_attribute_value(&Attributes::MOVEMENT_SPEED) + }; + let speed = self.inner.speed_modifier * base_speed; + let y_input = if dy.abs() > 0.1 { + if dy > 0.0 { speed } else { -speed } + } else { + 0.0 + }; + entity + .movement_input + .store(Vector3::new(0.0, y_input, speed)); + entity.entity.set_has_no_gravity(true); + } + } + } + + fn move_to_coords( + &mut self, + x: f64, + y: f64, + z: f64, + speed: f64, + entity: &LivingEntity, + ) -> bool { + let pos = entity.entity.pos.load(); + self.set_progress(NavigatorGoal::new(pos, Vector3::new(x, y, z), speed)); + true + } + + fn move_to_pos(&mut self, pos: BlockPos, speed: f64, entity: &LivingEntity) -> bool { + let p = entity.entity.pos.load(); + let target = Vector3::new( + f64::from(pos.0.x) + 0.5, + f64::from(pos.0.y) + 0.5, + f64::from(pos.0.z) + 0.5, + ); + self.set_progress(NavigatorGoal::new(p, target, speed)); + true + } + + fn move_to_entity(&mut self, target: &LivingEntity, speed: f64, entity: &LivingEntity) -> bool { + let p = entity.entity.pos.load(); + let target_pos = target.entity.pos.load(); + self.set_progress(NavigatorGoal::new(p, target_pos, speed)); + true + } + + fn move_to_path(&mut self, path: Option, speed: f64, entity: &LivingEntity) -> bool { + if let Some(new_path) = path { + self.inner.path = Some(new_path); + if self.is_done() { + return false; + } + self.inner.speed_modifier = speed; + let mob_pos = entity.entity.pos.load(); + self.inner.last_stuck_check = self.inner.tick_count; + self.inner.last_stuck_check_pos = mob_pos; + self.inner.is_idle.store(false, Ordering::Relaxed); + true + } else { + self.inner.path = None; + self.inner.is_idle.store(true, Ordering::Relaxed); + false + } + } + + fn create_path( + &mut self, + entity: &LivingEntity, + destination: Vector3, + reach_range: i32, + ) -> Option { + self.inner.compute_path(entity, destination, reach_range) + } + + fn recompute_path(&mut self, entity: &LivingEntity) { + let world_age = entity.entity.world.load().get_world_age() as u64; + if world_age.saturating_sub(self.inner.time_last_recompute) <= 20 { + self.inner.has_delayed_recomputation = true; + } else if let Some(target_pos) = self.inner.target_pos { + let target_v = Vector3::new( + f64::from(target_pos.0.x) + 0.5, + f64::from(target_pos.0.y) + 0.5, + f64::from(target_pos.0.z) + 0.5, + ); + self.inner.path = self + .inner + .compute_path(entity, target_v, self.inner.reach_range); + self.inner.time_last_recompute = world_age; + self.inner.has_delayed_recomputation = false; + } + } + + fn set_avoid_sun(&mut self, avoid_sun: bool) { + self.inner.avoid_sun = avoid_sun; + } + + fn set_can_walk_over_fences(&mut self, can_walk: bool) { + self.inner.can_walk_over_fences = can_walk; + } + + fn set_can_open_doors(&mut self, can_open: bool) { + self.inner.can_open_doors = can_open; + } + + fn set_can_pass_doors(&mut self, can_pass: bool) { + self.inner.can_pass_doors = can_pass; + } + + fn set_can_float(&mut self, can_float: bool) { + self.inner.can_float = can_float; + } + + fn can_float(&self) -> bool { + self.inner.can_float + } + + fn can_navigate_ground(&self) -> bool { + false + } + + fn set_required_path_length(&mut self, length: f32) { + self.inner.required_path_length = length; + } + + fn set_max_visited_nodes_multiplier(&mut self, multiplier: f32) { + self.inner.max_visited_nodes_multiplier = multiplier; + } + + fn reset_max_visited_nodes_multiplier(&mut self) { + self.inner.max_visited_nodes_multiplier = 1.0; + } + + fn get_target_pos(&self) -> Option { + self.inner.target_pos + } + + fn can_path_to_targets_below_surface(&self) -> bool { + self.inner.can_path_to_targets_below_surface + } + + fn set_can_path_to_targets_below_surface(&mut self, can_path: bool) { + self.inner.can_path_to_targets_below_surface = can_path; + } +} + +pub struct WaterBoundPathNavigation { + pub inner: PathNavigation, + pub allow_breaching: bool, +} + +impl Default for WaterBoundPathNavigation { + fn default() -> Self { + Self::new(false) + } +} + +impl WaterBoundPathNavigation { + #[must_use] + pub fn new(allow_breaching: bool) -> Self { + Self { + inner: PathNavigation::new(EvaluatorKind::Swim(SwimNodeEvaluator::new( + allow_breaching, + ))), + allow_breaching, + } + } +} + +impl PathNavigationTrait for WaterBoundPathNavigation { + fn set_progress(&mut self, goal: NavigatorGoal) { + self.inner.set_progress(goal); + } + + fn set_speed(&mut self, speed: f64) { + self.inner.set_speed(speed); + } + + fn stop(&mut self) { + self.inner.stop(); + } + + fn is_idle(&self) -> bool { + self.inner.is_idle.load(Ordering::Relaxed) + } + + fn is_done(&self) -> bool { + self.inner.path.as_ref().is_none_or(Path::is_done) + } + + fn is_in_progress(&self) -> bool { + !self.is_done() + } + + fn is_stuck(&self) -> bool { + self.inner.is_stuck + } + + fn get_path(&self) -> Option<&Path> { + self.inner.path.as_ref() + } + + fn get_path_mut(&mut self) -> Option<&mut Path> { + self.inner.path.as_mut() + } + + fn set_pathfinding_malus(&mut self, path_type: PathType, malus: f32) { + self.inner.set_pathfinding_malus(path_type, malus); + } + + fn get_pathfinding_malus(&self, path_type: PathType) -> f32 { + self.inner.get_pathfinding_malus(path_type) + } + + fn set_mob_dimensions(&mut self, width: f32, height: f32) { + self.inner.set_mob_dimensions(width, height); + } + + fn can_reach_within( + &mut self, + entity: &LivingEntity, + destination: Vector3, + distance: f32, + ) -> bool { + self.inner.can_reach_within(entity, destination, distance) + } + + #[allow(clippy::too_many_lines)] + fn tick(&mut self, entity: &LivingEntity) { + self.inner.tick_count += 1; + let world_age = entity.entity.world.load().get_world_age() as u64; + + if self.inner.has_delayed_recomputation { + self.recompute_path(entity); + } + + if let Some(goal) = self.inner.current_goal.take() { + if self.inner.needs_new_path(&goal) { + self.inner.path = self.inner.compute_path(entity, goal.destination, 1); + self.inner.ticks_on_current_node = 0; + self.inner.last_node_index = 0; + self.inner.path_start_pos = Some(entity.entity.pos.load()); + self.inner.repath_cooldown = 15; + self.inner.time_last_recompute = world_age; + } + self.inner.current_goal = Some(goal); + } + + let mob_pos = Vector3::new( + entity.entity.pos.load().x, + entity.entity.pos.load().y + f64::from(self.inner.mob_height) * 0.5, + entity.entity.pos.load().z, + ); + self.inner.do_stuck_detection(mob_pos, entity); + + if self.is_done() { + self.inner.finish_navigation(entity); + } else { + if let Some(path) = &mut self.inner.path + && let Some(pos) = path.get_next_node_pos() + { + let target_pos = Vector3::new( + f64::from(pos.0.x) + 0.5, + f64::from(pos.0.y) + 0.5, + f64::from(pos.0.z) + 0.5, + ); + let current_pos = entity.entity.pos.load(); + let dx = target_pos.x - current_pos.x; + let dy = target_pos.y - current_pos.y; + let dz = target_pos.z - current_pos.z; + let dist_sq = dx * dx + dy * dy + dz * dz; + + if dist_sq < 0.5 * 0.5 { + path.advance(); + } + } + + if !self.is_done() + && let Some(path) = &self.inner.path + && let Some(next_block) = path.get_next_node_pos() + { + let target_pos = Vector3::new( + f64::from(next_block.0.x) + 0.5, + f64::from(next_block.0.y) + 0.5, + f64::from(next_block.0.z) + 0.5, + ); + let current_pos = entity.entity.pos.load(); + let dx = target_pos.x - current_pos.x; + let dy = target_pos.y - current_pos.y; + let dz = target_pos.z - current_pos.z; + let sd = dx.hypot(dz); + + let desired_yaw = wrap_degrees((dz.atan2(dx) as f32).to_degrees() - 90.0); + let desired_pitch = wrap_degrees(-(dy.atan2(sd) as f32).to_degrees()); + + entity.entity.yaw.store(desired_yaw); + entity.entity.head_yaw.store(desired_yaw); + entity.entity.body_yaw.store(desired_yaw); + entity.entity.pitch.store(desired_pitch); + + let speed = self.inner.speed_modifier + * entity.get_attribute_value(&Attributes::MOVEMENT_SPEED); + let y_input = if dy.abs() > 0.1 { + if dy > 0.0 { speed } else { -speed } + } else { + 0.0 + }; + entity + .movement_input + .store(Vector3::new(0.0, y_input, speed)); + } + } + } + + fn move_to_coords( + &mut self, + x: f64, + y: f64, + z: f64, + speed: f64, + entity: &LivingEntity, + ) -> bool { + let pos = entity.entity.pos.load(); + self.set_progress(NavigatorGoal::new(pos, Vector3::new(x, y, z), speed)); + true + } + + fn move_to_pos(&mut self, pos: BlockPos, speed: f64, entity: &LivingEntity) -> bool { + let p = entity.entity.pos.load(); + let target = Vector3::new( + f64::from(pos.0.x) + 0.5, + f64::from(pos.0.y) + 0.5, + f64::from(pos.0.z) + 0.5, + ); + self.set_progress(NavigatorGoal::new(p, target, speed)); + true + } + + fn move_to_entity(&mut self, target: &LivingEntity, speed: f64, entity: &LivingEntity) -> bool { + let p = entity.entity.pos.load(); + let target_pos = target.entity.pos.load(); + self.set_progress(NavigatorGoal::new(p, target_pos, speed)); + true + } + + fn move_to_path(&mut self, path: Option, speed: f64, entity: &LivingEntity) -> bool { + if let Some(new_path) = path { + self.inner.path = Some(new_path); + if self.is_done() { + return false; + } + self.inner.speed_modifier = speed; + let mob_pos = Vector3::new( + entity.entity.pos.load().x, + entity.entity.pos.load().y + f64::from(self.inner.mob_height) * 0.5, + entity.entity.pos.load().z, + ); + self.inner.last_stuck_check = self.inner.tick_count; + self.inner.last_stuck_check_pos = mob_pos; + self.inner.is_idle.store(false, Ordering::Relaxed); + true + } else { + self.inner.path = None; + self.inner.is_idle.store(true, Ordering::Relaxed); + false + } + } + + fn create_path( + &mut self, + entity: &LivingEntity, + destination: Vector3, + reach_range: i32, + ) -> Option { + self.inner.compute_path(entity, destination, reach_range) + } + + fn recompute_path(&mut self, entity: &LivingEntity) { + let world_age = entity.entity.world.load().get_world_age() as u64; + if world_age.saturating_sub(self.inner.time_last_recompute) <= 20 { + self.inner.has_delayed_recomputation = true; + } else if let Some(target_pos) = self.inner.target_pos { + let target_v = Vector3::new( + f64::from(target_pos.0.x) + 0.5, + f64::from(target_pos.0.y) + 0.5, + f64::from(target_pos.0.z) + 0.5, + ); + self.inner.path = self + .inner + .compute_path(entity, target_v, self.inner.reach_range); + self.inner.time_last_recompute = world_age; + self.inner.has_delayed_recomputation = false; + } + } + + fn set_avoid_sun(&mut self, avoid_sun: bool) { + self.inner.avoid_sun = avoid_sun; + } + + fn set_can_walk_over_fences(&mut self, can_walk: bool) { + self.inner.can_walk_over_fences = can_walk; + } + + fn set_can_open_doors(&mut self, can_open: bool) { + self.inner.can_open_doors = can_open; + } + + fn set_can_pass_doors(&mut self, can_pass: bool) { + self.inner.can_pass_doors = can_pass; + } + + fn set_can_float(&mut self, _can_float: bool) {} + + fn can_float(&self) -> bool { + true + } + + fn can_navigate_ground(&self) -> bool { + false + } + + fn set_required_path_length(&mut self, length: f32) { + self.inner.required_path_length = length; + } + + fn set_max_visited_nodes_multiplier(&mut self, multiplier: f32) { + self.inner.max_visited_nodes_multiplier = multiplier; + } + + fn reset_max_visited_nodes_multiplier(&mut self) { + self.inner.max_visited_nodes_multiplier = 1.0; + } + + fn get_target_pos(&self) -> Option { + self.inner.target_pos + } + + fn can_path_to_targets_below_surface(&self) -> bool { + self.inner.can_path_to_targets_below_surface + } + + fn set_can_path_to_targets_below_surface(&mut self, can_path: bool) { + self.inner.can_path_to_targets_below_surface = can_path; + } +} + +pub struct WallClimberNavigation { + pub inner: GroundPathNavigation, + pub path_to_position: Option, +} + +impl Default for WallClimberNavigation { + fn default() -> Self { + Self::new() + } +} + +impl WallClimberNavigation { + #[must_use] + pub fn new() -> Self { + Self { + inner: GroundPathNavigation::new(), + path_to_position: None, + } + } +} + +impl PathNavigationTrait for WallClimberNavigation { + fn set_progress(&mut self, goal: NavigatorGoal) { + self.path_to_position = Some(goal.destination.to_block_pos()); + self.inner.set_progress(goal); + } + + fn set_speed(&mut self, speed: f64) { + self.inner.set_speed(speed); + } + + fn stop(&mut self) { + self.path_to_position = None; + self.inner.stop(); + } + + fn is_idle(&self) -> bool { + self.path_to_position.is_none() && self.inner.is_idle() + } + + fn is_done(&self) -> bool { + self.path_to_position.is_none() && self.inner.is_done() + } + + fn is_in_progress(&self) -> bool { + !self.is_done() + } + + fn is_stuck(&self) -> bool { + self.inner.is_stuck() + } + + fn get_path(&self) -> Option<&Path> { + self.inner.get_path() + } + + fn get_path_mut(&mut self) -> Option<&mut Path> { + self.inner.get_path_mut() + } + + fn set_pathfinding_malus(&mut self, path_type: PathType, malus: f32) { + self.inner.set_pathfinding_malus(path_type, malus); + } + + fn get_pathfinding_malus(&self, path_type: PathType) -> f32 { + self.inner.get_pathfinding_malus(path_type) + } + + fn set_mob_dimensions(&mut self, width: f32, height: f32) { + self.inner.set_mob_dimensions(width, height); + } + + fn can_reach_within( + &mut self, + entity: &LivingEntity, + destination: Vector3, + distance: f32, + ) -> bool { + self.inner.can_reach_within(entity, destination, distance) + } + + #[allow(clippy::too_many_lines)] + fn tick(&mut self, entity: &LivingEntity) { + if !self.inner.is_done() { + self.inner.tick(entity); + } else if let Some(target_pos) = self.path_to_position { + let current_pos = entity.entity.pos.load(); + let bb = entity.entity.bounding_box.load(); + let width = bb.max.x - bb.min.x; + let target_center = Vector3::new( + f64::from(target_pos.0.x) + 0.5, + f64::from(target_pos.0.y), + f64::from(target_pos.0.z) + 0.5, + ); + let dx = target_center.x - current_pos.x; + let dy = target_center.y - current_pos.y; + let dz = target_center.z - current_pos.z; + let dist_sq = dx * dx + dz * dz; + + let close_enough = dist_sq < width * width + && (current_pos.y > f64::from(target_pos.0.y) + || (current_pos.y - f64::from(target_pos.0.y)).abs() < 1.0); + + if close_enough { + self.path_to_position = None; + self.inner.inner.finish_navigation(entity); + } else { + let desired_yaw = wrap_degrees((dz.atan2(dx) as f32).to_degrees() - 90.0); + entity.entity.yaw.store(desired_yaw); + entity.entity.head_yaw.store(desired_yaw); + entity.entity.body_yaw.store(desired_yaw); + + let speed = self.inner.inner.speed_modifier + * entity.get_attribute_value(&Attributes::MOVEMENT_SPEED); + entity.movement_input.store(Vector3::new(0.0, 0.0, speed)); + if dy > 0.0 { + entity.jumping.store(true, Ordering::SeqCst); + } else { + entity.jumping.store(false, Ordering::SeqCst); + } + } + } + } + + fn move_to_coords( + &mut self, + x: f64, + y: f64, + z: f64, + speed: f64, + entity: &LivingEntity, + ) -> bool { + self.path_to_position = Some(BlockPos::new( + x.floor() as i32, + y.floor() as i32, + z.floor() as i32, + )); + self.inner.move_to_coords(x, y, z, speed, entity) + } + + fn move_to_pos(&mut self, pos: BlockPos, speed: f64, entity: &LivingEntity) -> bool { + self.path_to_position = Some(pos); + self.inner.move_to_pos(pos, speed, entity) + } + + fn move_to_entity(&mut self, target: &LivingEntity, speed: f64, entity: &LivingEntity) -> bool { + self.path_to_position = Some(target.entity.block_pos.load()); + let path = self.create_path(entity, target.entity.pos.load(), 0); + if path.is_some() { + self.move_to_path(path, speed, entity) + } else { + self.inner.inner.speed_modifier = speed; + true + } + } + + fn move_to_path(&mut self, path: Option, speed: f64, entity: &LivingEntity) -> bool { + self.inner.move_to_path(path, speed, entity) + } + + fn create_path( + &mut self, + entity: &LivingEntity, + destination: Vector3, + reach_range: i32, + ) -> Option { + self.path_to_position = Some(destination.to_block_pos()); + self.inner.create_path(entity, destination, reach_range) + } + + fn recompute_path(&mut self, entity: &LivingEntity) { + self.inner.recompute_path(entity); + } + + fn set_avoid_sun(&mut self, avoid_sun: bool) { + self.inner.set_avoid_sun(avoid_sun); + } + + fn set_can_walk_over_fences(&mut self, can_walk: bool) { + self.inner.set_can_walk_over_fences(can_walk); + } + + fn set_can_open_doors(&mut self, can_open: bool) { + self.inner.set_can_open_doors(can_open); + } + + fn set_can_pass_doors(&mut self, can_pass: bool) { + self.inner.set_can_pass_doors(can_pass); + } + + fn set_can_float(&mut self, can_float: bool) { + self.inner.set_can_float(can_float); + } + + fn can_float(&self) -> bool { + self.inner.can_float() + } + + fn can_navigate_ground(&self) -> bool { + true + } + + fn set_required_path_length(&mut self, length: f32) { + self.inner.set_required_path_length(length); + } + + fn set_max_visited_nodes_multiplier(&mut self, multiplier: f32) { + self.inner.set_max_visited_nodes_multiplier(multiplier); + } + + fn reset_max_visited_nodes_multiplier(&mut self) { + self.inner.reset_max_visited_nodes_multiplier(); + } + + fn get_target_pos(&self) -> Option { + self.inner.get_target_pos() + } + + fn can_path_to_targets_below_surface(&self) -> bool { + self.inner.can_path_to_targets_below_surface() + } + + fn set_can_path_to_targets_below_surface(&mut self, can_path: bool) { + self.inner.set_can_path_to_targets_below_surface(can_path); + } +} + +pub struct AmphibiousPathNavigation { + pub inner: PathNavigation, +} + +impl Default for AmphibiousPathNavigation { + fn default() -> Self { + Self::new(false) + } +} + +impl AmphibiousPathNavigation { + #[must_use] + pub fn new(prefers_shallow_swimming: bool) -> Self { + Self { + inner: PathNavigation::new(EvaluatorKind::Amphibious(AmphibiousNodeEvaluator::new( + prefers_shallow_swimming, + ))), + } + } +} + +impl PathNavigationTrait for AmphibiousPathNavigation { + fn set_progress(&mut self, goal: NavigatorGoal) { + self.inner.set_progress(goal); + } + + fn set_speed(&mut self, speed: f64) { + self.inner.set_speed(speed); + } + + fn stop(&mut self) { + self.inner.stop(); + } + + fn is_idle(&self) -> bool { + self.inner.is_idle.load(Ordering::Relaxed) + } + + fn is_done(&self) -> bool { + self.inner.path.as_ref().is_none_or(Path::is_done) + } + + fn is_in_progress(&self) -> bool { + !self.is_done() + } + + fn is_stuck(&self) -> bool { + self.inner.is_stuck + } + + fn get_path(&self) -> Option<&Path> { + self.inner.path.as_ref() + } + + fn get_path_mut(&mut self) -> Option<&mut Path> { + self.inner.path.as_mut() + } + + fn set_pathfinding_malus(&mut self, path_type: PathType, malus: f32) { + self.inner.set_pathfinding_malus(path_type, malus); + } + + fn get_pathfinding_malus(&self, path_type: PathType) -> f32 { + self.inner.get_pathfinding_malus(path_type) + } + + fn set_mob_dimensions(&mut self, width: f32, height: f32) { + self.inner.set_mob_dimensions(width, height); + } + + fn can_reach_within( + &mut self, + entity: &LivingEntity, + destination: Vector3, + distance: f32, + ) -> bool { + self.inner.can_reach_within(entity, destination, distance) + } + + #[allow(clippy::too_many_lines)] + fn tick(&mut self, entity: &LivingEntity) { + if entity.entity.touching_water.load(Ordering::Relaxed) { + self.inner.tick_count += 1; + let world_age = entity.entity.world.load().get_world_age() as u64; + + if self.inner.has_delayed_recomputation { + self.recompute_path(entity); + } + + if let Some(goal) = self.inner.current_goal.take() { + if self.inner.needs_new_path(&goal) { + self.inner.path = self.inner.compute_path(entity, goal.destination, 1); + self.inner.ticks_on_current_node = 0; + self.inner.last_node_index = 0; + self.inner.path_start_pos = Some(entity.entity.pos.load()); + self.inner.repath_cooldown = 15; + self.inner.time_last_recompute = world_age; + } + self.inner.current_goal = Some(goal); + } + + let mob_pos = Vector3::new( + entity.entity.pos.load().x, + entity.entity.pos.load().y + f64::from(self.inner.mob_height) * 0.5, + entity.entity.pos.load().z, + ); + self.inner.do_stuck_detection(mob_pos, entity); + + if self.is_done() { + self.inner.finish_navigation(entity); + } else { + if let Some(path) = &mut self.inner.path + && let Some(pos) = path.get_next_node_pos() + { + let target_pos = Vector3::new( + f64::from(pos.0.x) + 0.5, + f64::from(pos.0.y) + 0.5, + f64::from(pos.0.z) + 0.5, + ); + let current_pos = entity.entity.pos.load(); + let dx = target_pos.x - current_pos.x; + let dy = target_pos.y - current_pos.y; + let dz = target_pos.z - current_pos.z; + let dist_sq = dx * dx + dy * dy + dz * dz; + + if dist_sq < 0.5 * 0.5 { + path.advance(); + } + } + + if !self.is_done() + && let Some(path) = &self.inner.path + && let Some(next_block) = path.get_next_node_pos() + { + let target_pos = Vector3::new( + f64::from(next_block.0.x) + 0.5, + f64::from(next_block.0.y) + 0.5, + f64::from(next_block.0.z) + 0.5, + ); + let current_pos = entity.entity.pos.load(); + let dx = target_pos.x - current_pos.x; + let dy = target_pos.y - current_pos.y; + let dz = target_pos.z - current_pos.z; + let sd = dx.hypot(dz); + + let desired_yaw = wrap_degrees((dz.atan2(dx) as f32).to_degrees() - 90.0); + let desired_pitch = wrap_degrees(-(dy.atan2(sd) as f32).to_degrees()); + + entity.entity.yaw.store(desired_yaw); + entity.entity.head_yaw.store(desired_yaw); + entity.entity.body_yaw.store(desired_yaw); + entity.entity.pitch.store(desired_pitch); + + let speed = self.inner.speed_modifier + * entity.get_attribute_value(&Attributes::MOVEMENT_SPEED); + let y_input = if dy.abs() > 0.1 { + if dy > 0.0 { speed } else { -speed } + } else { + 0.0 + }; + entity + .movement_input + .store(Vector3::new(0.0, y_input, speed)); + } + } + } else { + self.inner.tick_ground(entity); + } + } + + fn move_to_coords( + &mut self, + x: f64, + y: f64, + z: f64, + speed: f64, + entity: &LivingEntity, + ) -> bool { + let pos = entity.entity.pos.load(); + self.set_progress(NavigatorGoal::new(pos, Vector3::new(x, y, z), speed)); + true + } + + fn move_to_pos(&mut self, pos: BlockPos, speed: f64, entity: &LivingEntity) -> bool { + let p = entity.entity.pos.load(); + let target = Vector3::new( + f64::from(pos.0.x) + 0.5, + f64::from(pos.0.y) + 0.5, + f64::from(pos.0.z) + 0.5, + ); + self.set_progress(NavigatorGoal::new(p, target, speed)); + true + } + + fn move_to_entity(&mut self, target: &LivingEntity, speed: f64, entity: &LivingEntity) -> bool { + let p = entity.entity.pos.load(); + let target_pos = target.entity.pos.load(); + self.set_progress(NavigatorGoal::new(p, target_pos, speed)); + true + } + + fn move_to_path(&mut self, path: Option, speed: f64, entity: &LivingEntity) -> bool { + if let Some(new_path) = path { + self.inner.path = Some(new_path); + if self.is_done() { + return false; + } + self.inner.speed_modifier = speed; + let mob_pos = Vector3::new( + entity.entity.pos.load().x, + entity.entity.pos.load().y + f64::from(self.inner.mob_height) * 0.5, + entity.entity.pos.load().z, + ); + self.inner.last_stuck_check = self.inner.tick_count; + self.inner.last_stuck_check_pos = mob_pos; + self.inner.is_idle.store(false, Ordering::Relaxed); + true + } else { + self.inner.path = None; + self.inner.is_idle.store(true, Ordering::Relaxed); + false + } + } + + fn create_path( + &mut self, + entity: &LivingEntity, + destination: Vector3, + reach_range: i32, + ) -> Option { + self.inner.compute_path(entity, destination, reach_range) + } + + fn recompute_path(&mut self, entity: &LivingEntity) { + let world_age = entity.entity.world.load().get_world_age() as u64; + if world_age.saturating_sub(self.inner.time_last_recompute) <= 20 { + self.inner.has_delayed_recomputation = true; + } else if let Some(target_pos) = self.inner.target_pos { + let target_v = Vector3::new( + f64::from(target_pos.0.x) + 0.5, + f64::from(target_pos.0.y) + 0.5, + f64::from(target_pos.0.z) + 0.5, + ); + self.inner.path = self + .inner + .compute_path(entity, target_v, self.inner.reach_range); + self.inner.time_last_recompute = world_age; + self.inner.has_delayed_recomputation = false; + } + } + + fn set_avoid_sun(&mut self, avoid_sun: bool) { + self.inner.avoid_sun = avoid_sun; + } + + fn set_can_walk_over_fences(&mut self, can_walk: bool) { + self.inner.can_walk_over_fences = can_walk; + } + + fn set_can_open_doors(&mut self, can_open: bool) { + self.inner.can_open_doors = can_open; + } + + fn set_can_pass_doors(&mut self, can_pass: bool) { + self.inner.can_pass_doors = can_pass; + } + + fn set_can_float(&mut self, _can_float: bool) {} + + fn can_float(&self) -> bool { + true + } + + fn can_navigate_ground(&self) -> bool { + true + } + + fn set_required_path_length(&mut self, length: f32) { + self.inner.required_path_length = length; + } + + fn set_max_visited_nodes_multiplier(&mut self, multiplier: f32) { + self.inner.max_visited_nodes_multiplier = multiplier; + } + + fn reset_max_visited_nodes_multiplier(&mut self) { + self.inner.max_visited_nodes_multiplier = 1.0; + } + + fn get_target_pos(&self) -> Option { + self.inner.target_pos + } + + fn can_path_to_targets_below_surface(&self) -> bool { + self.inner.can_path_to_targets_below_surface + } + + fn set_can_path_to_targets_below_surface(&mut self, can_path: bool) { + self.inner.can_path_to_targets_below_surface = can_path; + } +} + +pub struct Navigator { + inner: Box, +} + +impl Default for Navigator { + fn default() -> Self { + Self::ground() + } +} + +impl Navigator { + #[must_use] + pub fn new(nav: N) -> Self { + Self { + inner: Box::new(nav), + } + } + + #[must_use] + pub fn ground() -> Self { + Self::new(GroundPathNavigation::new()) + } + + #[must_use] + pub fn flying() -> Self { + Self::new(FlyingPathNavigation::new()) + } + + #[must_use] + pub fn water_bound(allow_breaching: bool) -> Self { + Self::new(WaterBoundPathNavigation::new(allow_breaching)) + } + + #[must_use] + pub fn wall_climber() -> Self { + Self::new(WallClimberNavigation::new()) + } + + #[must_use] + pub fn amphibious(prefers_shallow_swimming: bool) -> Self { + Self::new(AmphibiousPathNavigation::new(prefers_shallow_swimming)) + } +} + +impl Deref for Navigator { + type Target = dyn PathNavigationTrait; + + fn deref(&self) -> &Self::Target { + &*self.inner + } +} + +impl DerefMut for Navigator { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut *self.inner } } diff --git a/crates/pumpkin/src/entity/ai/pathfinder/node.rs b/crates/pumpkin/src/entity/ai/pathfinder/node.rs index bce7f38cc..cac9cf60d 100644 --- a/crates/pumpkin/src/entity/ai/pathfinder/node.rs +++ b/crates/pumpkin/src/entity/ai/pathfinder/node.rs @@ -1,5 +1,7 @@ use pumpkin_util::math::{position::BlockPos, vector3::Vector3}; +pub use crate::block::PathComputationType; + pub trait Coordinate { fn distance(&self, other: &dyn Coordinate) -> f32; fn distance_xz(&self, other: &dyn Coordinate) -> f32; @@ -14,14 +16,11 @@ pub trait Coordinate { #[derive(Clone, Copy, Debug, PartialEq)] pub struct Node { pub pos: BlockPos, + pub hash: i32, pub heap_idx: i32, - // Cost from start to this node pub g: f32, - // Heuristic cost from this node to target pub h: f32, - // g + h pub f: f32, - /// Position of the predecessor node for path reconstruction. pub came_from: Option>, pub closed: bool, pub walked_dist: f32, @@ -33,6 +32,7 @@ impl Default for Node { fn default() -> Self { Self { pos: BlockPos::new(0, 0, 0), + hash: Self::create_hash(0, 0, 0), heap_idx: -1, g: 0.0, h: 0.0, @@ -51,6 +51,7 @@ impl Node { pub fn new(pos: BlockPos) -> Self { Self { pos, + hash: Self::create_hash(pos.0.x, pos.0.y, pos.0.z), ..Default::default() } } @@ -59,6 +60,7 @@ impl Node { pub const fn clone_and_move(&self, pos: BlockPos) -> Self { Self { pos, + hash: Self::create_hash(pos.0.x, pos.0.y, pos.0.z), heap_idx: self.heap_idx, g: self.g, h: self.h, @@ -70,22 +72,36 @@ impl Node { path_type: self.path_type, } } -} -impl Coordinate for Node { - fn distance(&self, other: &dyn Coordinate) -> f32 { + #[must_use] + pub const fn create_hash(x: i32, y: i32, z: i32) -> i32 { + let x_neg = if x < 0 { i32::MIN } else { 0 }; + let z_neg = if z < 0 { 32768 } else { 0 }; + (y & 0xFF) | ((x & 32767) << 8) | ((z & 32767) << 24) | x_neg | z_neg + } + + #[must_use] + pub const fn in_open_set(&self) -> bool { + self.heap_idx >= 0 + } + + #[must_use] + pub fn distance(&self, other: &dyn Coordinate) -> f32 { (self.pos.0.squared_distance_to_vec(&other.as_vector3()) as f32).sqrt() } - fn distance_xz(&self, other: &dyn Coordinate) -> f32 { + #[must_use] + pub fn distance_xz(&self, other: &dyn Coordinate) -> f32 { (self.pos.0.squared_distance_to_vec_xz(other.as_vector3()) as f32).sqrt() } - fn distance_sqr(&self, other: &dyn Coordinate) -> f32 { + #[must_use] + pub fn distance_sqr(&self, other: &dyn Coordinate) -> f32 { self.pos.0.squared_distance_to_vec(&other.as_vector3()) as f32 } - fn distance_manhattan(&self, other: &dyn Coordinate) -> f32 { + #[must_use] + pub fn distance_manhattan(&self, other: &dyn Coordinate) -> f32 { let v = other.as_vector3(); let x = (self.pos.0.x - v.x).abs(); let y = (self.pos.0.y - v.y).abs(); @@ -93,6 +109,88 @@ impl Coordinate for Node { (x + y + z) as f32 } + #[must_use] + pub fn distance_to_node(&self, other: &Self) -> f32 { + let xd = (other.pos.0.x - self.pos.0.x) as f32; + let yd = (other.pos.0.y - self.pos.0.y) as f32; + let zd = (other.pos.0.z - self.pos.0.z) as f32; + (xd * xd + yd * yd + zd * zd).sqrt() + } + + #[must_use] + pub fn distance_to_xz_node(&self, other: &Self) -> f32 { + let xd = (other.pos.0.x - self.pos.0.x) as f32; + let zd = (other.pos.0.z - self.pos.0.z) as f32; + xd.hypot(zd) + } + + #[must_use] + pub fn distance_to_block_pos(&self, pos: BlockPos) -> f32 { + let xd = (pos.0.x - self.pos.0.x) as f32; + let yd = (pos.0.y - self.pos.0.y) as f32; + let zd = (pos.0.z - self.pos.0.z) as f32; + (xd * xd + yd * yd + zd * zd).sqrt() + } + + #[must_use] + pub fn distance_to_sqr_node(&self, other: &Self) -> f32 { + let xd = (other.pos.0.x - self.pos.0.x) as f32; + let yd = (other.pos.0.y - self.pos.0.y) as f32; + let zd = (other.pos.0.z - self.pos.0.z) as f32; + xd * xd + yd * yd + zd * zd + } + + #[must_use] + pub fn distance_to_sqr_block_pos(&self, pos: BlockPos) -> f32 { + let xd = (pos.0.x - self.pos.0.x) as f32; + let yd = (pos.0.y - self.pos.0.y) as f32; + let zd = (pos.0.z - self.pos.0.z) as f32; + xd * xd + yd * yd + zd * zd + } + + #[must_use] + pub const fn distance_manhattan_node(&self, other: &Self) -> f32 { + let xd = (other.pos.0.x - self.pos.0.x).abs(); + let yd = (other.pos.0.y - self.pos.0.y).abs(); + let zd = (other.pos.0.z - self.pos.0.z).abs(); + (xd + yd + zd) as f32 + } + + #[must_use] + pub const fn distance_manhattan_block_pos(&self, pos: BlockPos) -> f32 { + let xd = (pos.0.x - self.pos.0.x).abs(); + let yd = (pos.0.y - self.pos.0.y).abs(); + let zd = (pos.0.z - self.pos.0.z).abs(); + (xd + yd + zd) as f32 + } + + #[must_use] + pub const fn as_vec3(&self) -> Vector3 { + Vector3::new( + self.pos.0.x as f64, + self.pos.0.y as f64, + self.pos.0.z as f64, + ) + } +} + +impl Coordinate for Node { + fn distance(&self, other: &dyn Coordinate) -> f32 { + self.distance(other) + } + + fn distance_xz(&self, other: &dyn Coordinate) -> f32 { + self.distance_xz(other) + } + + fn distance_sqr(&self, other: &dyn Coordinate) -> f32 { + self.distance_sqr(other) + } + + fn distance_manhattan(&self, other: &dyn Coordinate) -> f32 { + self.distance_manhattan(other) + } + fn as_blockpos(&self) -> BlockPos { self.pos } @@ -106,7 +204,7 @@ impl Coordinate for Node { } } -#[derive(Default, Debug)] +#[derive(Default, Debug, Clone, Copy, PartialEq)] pub struct Target { pub node: Node, pub best_heuristic: f32, @@ -116,46 +214,77 @@ pub struct Target { impl Target { #[must_use] - pub fn new(node: Node) -> Self { + pub const fn new(node: Node) -> Self { Self { node, best_heuristic: f32::MAX, - ..Default::default() + best_node: None, + reached: false, } } + #[must_use] + pub fn from_pos(pos: BlockPos) -> Self { + Self::new(Node::new(pos)) + } + pub fn update_best(&mut self, heuristic: f32, node: &Node) { if heuristic < self.best_heuristic { self.best_heuristic = heuristic; self.best_node = Some(*node); } } + + #[must_use] + pub const fn get_best_node(&self) -> Option { + self.best_node + } + + pub const fn set_reached(&mut self) { + self.reached = true; + } + + #[must_use] + pub const fn is_reached(&self) -> bool { + self.reached + } + + #[must_use] + pub fn distance(&self, other: &dyn Coordinate) -> f32 { + self.node.distance(other) + } + + #[must_use] + pub fn distance_xz(&self, other: &dyn Coordinate) -> f32 { + self.node.distance_xz(other) + } + + #[must_use] + pub fn distance_sqr(&self, other: &dyn Coordinate) -> f32 { + self.node.distance_sqr(other) + } + + #[must_use] + pub fn distance_manhattan(&self, other: &dyn Coordinate) -> f32 { + self.node.distance_manhattan(other) + } } impl Coordinate for Target { fn distance(&self, other: &dyn Coordinate) -> f32 { - (self.node.pos.0.squared_distance_to_vec(&other.as_vector3()) as f32).sqrt() + self.node.distance(other) } fn distance_xz(&self, other: &dyn Coordinate) -> f32 { - (self - .node - .pos - .0 - .squared_distance_to_vec_xz(other.as_vector3()) as f32) - .sqrt() + self.node.distance_xz(other) } fn distance_sqr(&self, other: &dyn Coordinate) -> f32 { - self.node.pos.0.squared_distance_to_vec(&other.as_vector3()) as f32 + self.node.distance_sqr(other) } fn distance_manhattan(&self, other: &dyn Coordinate) -> f32 { - let v = other.as_vector3(); - let x = (self.node.pos.0.x - v.x).abs(); - let y = (self.node.pos.0.y - v.y).abs(); - let z = (self.node.pos.0.z - v.z).abs(); - (x + y + z) as f32 + self.node.distance_manhattan(other) } fn as_blockpos(&self) -> BlockPos { @@ -268,9 +397,10 @@ pub enum PathType { Cocoa = 23, DamageCautious = 24, DangerTrapdoor = 25, + BigMobsCloseToDanger = 26, } -pub const PATH_TYPE_COUNT: usize = 26; +pub const PATH_TYPE_COUNT: usize = 27; impl PathType { #[must_use] @@ -295,7 +425,7 @@ impl PathType { | Self::Cocoa | Self::DamageCautious | Self::DangerTrapdoor => 0.0, - Self::Breach => 4.0, + Self::Breach | Self::BigMobsCloseToDanger => 4.0, Self::Water | Self::WaterBorder | Self::DangerFire diff --git a/crates/pumpkin/src/entity/ai/pathfinder/node_evaluator.rs b/crates/pumpkin/src/entity/ai/pathfinder/node_evaluator.rs index 3fc563238..04b8cdf80 100644 --- a/crates/pumpkin/src/entity/ai/pathfinder/node_evaluator.rs +++ b/crates/pumpkin/src/entity/ai/pathfinder/node_evaluator.rs @@ -42,6 +42,9 @@ pub struct MobData { pub avoids_fire: bool, pub avoids_water: bool, pub on_ground: bool, + pub is_in_water: bool, + pub sea_level: i32, + pub min_y: i32, pub path_type_malus: [Option; PATH_TYPE_COUNT], } @@ -59,6 +62,9 @@ impl MobData { avoids_fire: true, avoids_water: false, on_ground, + is_in_water: false, + sea_level: 63, + min_y: -64, path_type_malus: [None; PATH_TYPE_COUNT], }; @@ -89,6 +95,9 @@ impl MobData { avoids_fire: true, avoids_water: false, on_ground: true, + is_in_water: false, + sea_level: 63, + min_y: -64, path_type_malus: [None; PATH_TYPE_COUNT], } } @@ -128,7 +137,7 @@ pub struct BaseNodeEvaluator { pub nodes: FxHashMap, Node>, pub entity_width: i32, pub entity_height: i32, - pub entity_depth: i32, // Same as width? + pub entity_depth: i32, pub can_pass_doors: bool, pub can_open_doors: bool, pub can_float: bool, diff --git a/crates/pumpkin/src/entity/ai/pathfinder/path.rs b/crates/pumpkin/src/entity/ai/pathfinder/path.rs index 276f65a90..820a32485 100644 --- a/crates/pumpkin/src/entity/ai/pathfinder/path.rs +++ b/crates/pumpkin/src/entity/ai/pathfinder/path.rs @@ -1,24 +1,24 @@ -use pumpkin_util::math::vector3::Vector3; +use pumpkin_util::math::{position::BlockPos, vector3::Vector3}; -use crate::entity::ai::pathfinder::node::{Coordinate, Node}; +use crate::entity::ai::pathfinder::node::Node; #[derive(Debug, Clone)] pub struct Path { nodes: Vec, pub next_node_index: usize, - target: Vector3, + target: BlockPos, dist_to_target: f32, reached: bool, } impl Path { #[must_use] - pub fn new(nodes: Vec, target: Vector3, reached: bool) -> Self { + pub fn new(nodes: Vec, target: BlockPos, reached: bool) -> Self { let dist_to_target = if nodes.is_empty() { f32::MAX } else { let last_node = &nodes[nodes.len() - 1]; - last_node.distance_manhattan(&target) + last_node.distance_manhattan_block_pos(target) }; Self { @@ -31,12 +31,12 @@ impl Path { } #[must_use] - pub fn empty(target: Vector3) -> Self { + pub fn empty(target: BlockPos) -> Self { Self::new(Vec::new(), target, false) } - pub fn advance(&mut self) { - self.next_node_index = (self.next_node_index + 1).min(self.nodes.len()); + pub const fn advance(&mut self) { + self.next_node_index += 1; } #[must_use] @@ -68,9 +68,9 @@ impl Path { } } - pub fn replace_node(&mut self, index: usize, new_node: Node) { + pub fn replace_node(&mut self, index: usize, replace_with: Node) { if index < self.nodes.len() { - self.nodes[index] = new_node; + self.nodes[index] = replace_with; } } @@ -84,38 +84,34 @@ impl Path { self.next_node_index } - pub fn set_next_node_index(&mut self, index: usize) { - self.next_node_index = index.min(self.nodes.len()); + pub const fn set_next_node_index(&mut self, index: usize) { + self.next_node_index = index; } #[must_use] - pub fn get_entity_pos_at_node( - &self, - index: usize, - entity_width: f32, - ) -> Option<(f32, f32, f32)> { + pub fn get_entity_pos_at_node(&self, entity_width: f32, index: usize) -> Option> { self.nodes.get(index).map(|node| { - let offset = ((entity_width + 1.0) as i32 as f32) * 0.5; - ( - node.pos.0.x as f32 + offset, - node.pos.0.y as f32, - node.pos.0.z as f32 + offset, + let offset = f64::from((entity_width + 1.0) as i32) * 0.5; + Vector3::new( + f64::from(node.pos.0.x) + offset, + f64::from(node.pos.0.y), + f64::from(node.pos.0.z) + offset, ) }) } #[must_use] - pub fn get_node_pos(&self, index: usize) -> Option> { - self.nodes.get(index).map(Coordinate::as_vector3) + pub fn get_node_pos(&self, index: usize) -> Option { + self.nodes.get(index).map(|node| node.pos) } #[must_use] - pub fn get_next_entity_pos(&self, entity_width: f32) -> Option<(f32, f32, f32)> { - self.get_entity_pos_at_node(self.next_node_index, entity_width) + pub fn get_next_entity_pos(&self, entity_width: f32) -> Option> { + self.get_entity_pos_at_node(entity_width, self.next_node_index) } #[must_use] - pub fn get_next_node_pos(&self) -> Option> { + pub fn get_next_node_pos(&self) -> Option { self.get_node_pos(self.next_node_index) } @@ -134,7 +130,12 @@ impl Path { } #[must_use] - pub fn same_as(&self, other: &Self) -> bool { + pub fn same_as(&self, other: Option<&Self>) -> bool { + other.is_some_and(|o| self.same_as_path(o)) + } + + #[must_use] + pub fn same_as_path(&self, other: &Self) -> bool { self.nodes.len() == other.nodes.len() && self.nodes.iter().zip(&other.nodes).all(|(a, b)| a == b) } @@ -145,7 +146,7 @@ impl Path { } #[must_use] - pub const fn get_target(&self) -> Vector3 { + pub const fn get_target(&self) -> BlockPos { self.target } @@ -170,9 +171,18 @@ impl Path { &self.nodes } + #[must_use] + pub fn get_nodes_mut(&mut self) -> &mut [Node] { + &mut self.nodes + } + #[must_use] pub fn get_remaining_nodes(&self) -> &[Node] { - &self.nodes[self.next_node_index..] + if self.next_node_index >= self.nodes.len() { + &[] + } else { + &self.nodes[self.next_node_index..] + } } #[must_use] @@ -183,7 +193,7 @@ impl Path { let mut total_length = 0.0; for i in 1..self.nodes.len() { - total_length += self.nodes[i - 1].distance(&self.nodes[i]); + total_length += self.nodes[i - 1].distance_to_node(&self.nodes[i]); } total_length } @@ -196,7 +206,7 @@ impl Path { let mut remaining = 0.0; for i in (self.next_node_index + 1)..self.nodes.len() { - remaining += self.nodes[i - 1].distance(&self.nodes[i]); + remaining += self.nodes[i - 1].distance_to_node(&self.nodes[i]); } remaining } @@ -218,6 +228,6 @@ impl PartialEq for Path { impl Default for Path { fn default() -> Self { - Self::empty(Vector3::new(0, 0, 0)) + Self::empty(BlockPos::new(0, 0, 0)) } } diff --git a/crates/pumpkin/src/entity/ai/pathfinder/pathfinding_context.rs b/crates/pumpkin/src/entity/ai/pathfinder/pathfinding_context.rs index 3d38a6447..60d6e405d 100644 --- a/crates/pumpkin/src/entity/ai/pathfinder/pathfinding_context.rs +++ b/crates/pumpkin/src/entity/ai/pathfinder/pathfinding_context.rs @@ -3,11 +3,11 @@ use pumpkin_data::{ fluid::Fluid, tag::{self, Taggable}, }; -use pumpkin_util::math::vector3::Vector3; +use pumpkin_util::math::{boundingbox::BoundingBox, position::BlockPos, vector3::Vector3}; use crate::{ entity::ai::pathfinder::{ - node::{Coordinate, PathType}, + node::{Coordinate, PathComputationType, PathType}, path_type_cache::PathTypeCache, }, world::World, @@ -20,10 +20,11 @@ pub struct PathfindingContext { path_type_cache: Option, mob_position: Vector3, world: Arc, - collision_cache: FxHashMap, bool>, + collision_cache: FxHashMap<[u64; 6], bool>, } impl PathfindingContext { + #[must_use] pub fn new(mob_position: Vector3, world: Arc) -> Self { Self { path_type_cache: Some(PathTypeCache::new()), @@ -33,6 +34,7 @@ impl PathfindingContext { } } + #[must_use] pub fn with_cache(mob_position: Vector3, world: Arc, cache: PathTypeCache) -> Self { Self { path_type_cache: Some(cache), @@ -47,6 +49,21 @@ impl PathfindingContext { self.mob_position } + #[must_use] + pub const fn world(&self) -> &Arc { + &self.world + } + + #[must_use] + pub const fn min_y(&self) -> i32 { + -64 + } + + #[must_use] + pub const fn sea_level(&self) -> i32 { + 63 + } + pub fn get_path_type_from_state(&mut self, pos: Vector3) -> PathType { if let Some(ref cache) = self.path_type_cache && let Some(pt) = cache.get(pos) @@ -63,20 +80,14 @@ impl PathfindingContext { pt } - /// Classifies a block position into a `PathType` for pathfinding. #[must_use] pub fn compute_path_type_from_state(&self, pos: Vector3) -> PathType { let block_pos = pos.as_blockpos(); - - // Single async chunk lookup, then derive block & state from static arrays let state_id = self.world.get_block_state_id(&block_pos); let block = Block::from_state_id(state_id); let state = BlockState::from_id(state_id); - if block.id == Block::AIR.id - || block.id == Block::VOID_AIR.id - || block.id == Block::CAVE_AIR.id - { + if state.is_air() { return PathType::Open; } @@ -148,11 +159,19 @@ impl PathfindingContext { return PathType::Fence; } - if block.has_tag(&tag::Block::MINECRAFT_FENCE_GATES) && !state.collision_shapes.is_empty() { - return PathType::Fence; + if block.has_tag(&tag::Block::MINECRAFT_FENCE_GATES) { + return if state.collision_shapes.is_empty() { + if fluid.is_some_and(|f| f.has_tag(&tag::Fluid::MINECRAFT_WATER)) { + PathType::Water + } else { + PathType::Open + } + } else { + PathType::Fence + }; } - if state.is_full_cube() { + if !self.is_pathfindable(&block_pos, PathComputationType::Land) { return PathType::Blocked; } @@ -163,11 +182,10 @@ impl PathfindingContext { PathType::Open } - /// Wraps the raw block type with below-check and neighbor danger scanning for OPEN nodes. pub fn get_land_node_type(&mut self, pos: Vector3) -> PathType { let raw_type = self.get_path_type_from_state(pos); - if raw_type == PathType::Open { + if raw_type == PathType::Open && pos.y > self.min_y() { let below_type = self.get_path_type_from_state(Vector3::new(pos.x, pos.y - 1, pos.z)); return match below_type { PathType::Open | PathType::Water | PathType::Lava | PathType::Walkable => { @@ -186,7 +204,6 @@ impl PathfindingContext { raw_type } - /// Scans a 3x3x3 neighborhood for danger blocks and returns the appropriate danger type. pub fn get_node_type_from_neighbors( &mut self, pos: Vector3, @@ -224,18 +241,79 @@ impl PathfindingContext { fallback } - pub fn has_collisions(&mut self, pos: Vector3) -> bool { - if let Some(&cached) = self.collision_cache.get(&pos) { + #[must_use] + pub fn get_floor_level(&self, pos: &BlockPos) -> f64 { + let target = pos.down(); + let state = self.world.get_block_state(&target); + let max_y = state + .get_block_collision_shapes_at(&target) + .map(|s| s.max.y) + .fold(0.0f64, f64::max); + f64::from(target.0.y) + max_y + } + + pub fn has_collision(&mut self, bb: &BoundingBox) -> bool { + let key = [ + bb.min.x.to_bits(), + bb.min.y.to_bits(), + bb.min.z.to_bits(), + bb.max.x.to_bits(), + bb.max.y.to_bits(), + bb.max.z.to_bits(), + ]; + + if let Some(&cached) = self.collision_cache.get(&key) { return cached; } - let block_pos = pos.as_blockpos(); - let state_id = self.world.get_block_state_id(&block_pos); - let state = BlockState::from_id(state_id); - let has_collision = state.is_full_cube(); + let min = bb.min_block_pos(); + let max = bb.max_block_pos(); + let mut collided = false; - self.collision_cache.insert(pos, has_collision); - has_collision + for pos in BlockPos::iterate(min, max) { + let state = self.world.get_block_state(&pos); + if state.is_air() || !state.is_solid() { + continue; + } + for shape in state.get_block_collision_shapes_at(&pos) { + let world_shape = shape.at_pos(pos); + if world_shape.intersects(bb) { + collided = true; + break; + } + } + if collided { + break; + } + } + + self.collision_cache.insert(key, collided); + collided + } + + #[must_use] + pub fn is_water(&self, pos: &BlockPos) -> bool { + let state_id = self.world.get_block_state_id(pos); + let fluid = Fluid::from_state_id(state_id); + fluid.is_some_and(|f| f.has_tag(&tag::Fluid::MINECRAFT_WATER)) + } + + #[must_use] + pub fn is_air(&self, pos: &BlockPos) -> bool { + self.world.get_block_state(pos).is_air() + } + + #[must_use] + pub fn get_block_state(&self, pos: &BlockPos) -> &'static BlockState { + self.world.get_block_state(pos) + } + + #[must_use] + pub fn is_pathfindable(&self, pos: &BlockPos, computation_type: PathComputationType) -> bool { + let (block, state) = self.world.get_block_and_state(pos); + self.world + .block_registry + .is_pathfindable(block, state, computation_type) } pub fn clear_caches(&mut self) { diff --git a/crates/pumpkin/src/entity/ai/pathfinder/swim_node_evaluator.rs b/crates/pumpkin/src/entity/ai/pathfinder/swim_node_evaluator.rs new file mode 100644 index 000000000..b64423bb6 --- /dev/null +++ b/crates/pumpkin/src/entity/ai/pathfinder/swim_node_evaluator.rs @@ -0,0 +1,246 @@ +use pumpkin_util::math::{position::BlockPos, vector3::Vector3}; +use rustc_hash::FxHashMap; + +use crate::entity::ai::pathfinder::{ + node::{Node, PathComputationType, PathType, Target}, + node_evaluator::{BaseNodeEvaluator, MobData, NodeEvaluator}, + pathfinding_context::PathfindingContext, +}; + +pub struct SwimNodeEvaluator { + pub base: BaseNodeEvaluator, + allow_breaching: bool, + path_types_cache: FxHashMap, PathType>, +} + +impl SwimNodeEvaluator { + #[must_use] + pub fn new(allow_breaching: bool) -> Self { + Self { + base: BaseNodeEvaluator::new(), + allow_breaching, + path_types_cache: FxHashMap::default(), + } + } + + pub fn get_cached_block_type(&mut self, pos: Vector3) -> PathType { + if let Some(&cached) = self.path_types_cache.get(&pos) { + return cached; + } + + let path_type = if let Some(mut ctx) = self.base.context.take() { + let res = self.get_path_type(&mut ctx, pos); + self.base.context = Some(ctx); + res + } else { + PathType::Blocked + }; + + self.path_types_cache.insert(pos, path_type); + path_type + } + + fn find_accepted_node(&mut self, x: i32, y: i32, z: i32) -> Option { + let path_type = self.get_cached_block_type(Vector3::new(x, y, z)); + if (self.allow_breaching && path_type == PathType::Breach) || path_type == PathType::Water { + let path_cost = self.get_mob_penalty(path_type); + (path_cost >= 0.0).then(|| { + let mut best = self.base.get_node(BlockPos::new(x, y, z)); + best.path_type = path_type; + best.cost_malus = best.cost_malus.max(path_cost); + if let Some(ref ctx) = self.base.context + && !ctx.is_water(&BlockPos::new(x, y, z)) + { + best.cost_malus += 8.0; + } + best + }) + } else { + None + } + } + + const fn is_node_valid(node: Option<&Node>) -> bool { + if let Some(n) = node { !n.closed } else { false } + } + + const fn has_malus(node: Option<&Node>) -> bool { + if let Some(n) = node { + n.cost_malus >= 0.0 + } else { + false + } + } + + fn get_mob_penalty(&self, path_type: PathType) -> f32 { + self.base + .mob_data + .as_ref() + .map_or(path_type.get_malus(), |d| { + d.get_pathfinding_malus(path_type) + }) + } +} + +impl NodeEvaluator for SwimNodeEvaluator { + fn prepare(&mut self, context: PathfindingContext, mob_data: MobData) { + self.base.entity_width = mob_data.get_bb_width(); + self.base.entity_height = mob_data.get_bb_height(); + self.base.entity_depth = mob_data.get_bb_width(); + + self.base.context = Some(context); + self.base.mob_data = Some(mob_data); + self.path_types_cache.clear(); + } + + fn done(&mut self) { + self.base.context = None; + self.base.mob_data = None; + self.path_types_cache.clear(); + } + + fn get_start(&mut self) -> Option { + let mob_data = self.base.mob_data?; + let min_x = (mob_data.position.x - f64::from(mob_data.width) / 2.0).floor() as i32; + let min_y = (mob_data.position.y + 0.5).floor() as i32; + let min_z = (mob_data.position.z - f64::from(mob_data.width) / 2.0).floor() as i32; + + let mut node = self.base.get_node(BlockPos::new(min_x, min_y, min_z)); + node.path_type = self.get_cached_block_type(Vector3::new(min_x, min_y, min_z)); + node.cost_malus = self.get_mob_penalty(node.path_type); + Some(node) + } + + fn get_target(&mut self, pos: BlockPos) -> Target { + Target::new(self.base.get_node(pos)) + } + + fn get_neighbors(&mut self, current: &Node, out: &mut Vec) { + let x = current.pos.0.x; + let y = current.pos.0.y; + let z = current.pos.0.z; + + let mut nodes = [None; 6]; + let directions = [ + (0usize, 0, -1, 0), + (1usize, 0, 1, 0), + (2usize, 0, 0, -1), + (3usize, 0, 0, 1), + (4usize, -1, 0, 0), + (5usize, 1, 0, 0), + ]; + + for &(idx, dx, dy, dz) in &directions { + let node = self.find_accepted_node(x + dx, y + dy, z + dz); + nodes[idx] = node; + if Self::is_node_valid(node.as_ref()) + && let Some(n) = node + { + out.push(n); + } + } + + let horizontal = [ + (2usize, 5usize, 1, -1), + (3usize, 4usize, -1, 1), + (4usize, 2usize, -1, -1), + (5usize, 3usize, 1, 1), + ]; + + for &(dir_idx, cw_idx, dx, dz) in &horizontal { + if Self::has_malus(nodes[dir_idx].as_ref()) && Self::has_malus(nodes[cw_idx].as_ref()) { + let diagonal_node = self.find_accepted_node(x + dx, y, z + dz); + if Self::is_node_valid(diagonal_node.as_ref()) + && let Some(d) = diagonal_node + { + out.push(d); + } + } + } + } + + fn get_path_type_of_mob( + &mut self, + context: &mut PathfindingContext, + pos: Vector3, + _mob_data: &MobData, + ) -> PathType { + for xx in pos.x..(pos.x + self.base.entity_width) { + for yy in pos.y..(pos.y + self.base.entity_height) { + for zz in pos.z..(pos.z + self.base.entity_depth) { + let check_pos = BlockPos::new(xx, yy, zz); + let is_water = context.is_water(&check_pos); + let below_pos = check_pos.down(); + let below_pathfindable = + context.is_pathfindable(&below_pos, PathComputationType::Water); + let is_air = context.is_air(&check_pos); + if !is_water && below_pathfindable && is_air { + return PathType::Breach; + } + if !is_water { + return PathType::Blocked; + } + } + } + } + + let p = pos.to_block_pos(); + if context.is_pathfindable(&p, PathComputationType::Water) { + PathType::Water + } else { + PathType::Blocked + } + } + + fn get_path_type(&mut self, context: &mut PathfindingContext, pos: Vector3) -> PathType { + let mob_data = self.base.mob_data.unwrap_or(MobData { + position: Vector3::new(0.0, 0.0, 0.0), + width: 0.6, + height: 1.95, + max_step_height: 1.0, + max_fall_distance: 3.0, + can_swim: true, + can_walk_on_water: false, + avoids_fire: true, + avoids_water: false, + on_ground: false, + is_in_water: true, + sea_level: 63, + min_y: -64, + path_type_malus: [None; crate::entity::ai::pathfinder::node::PATH_TYPE_COUNT], + }); + self.get_path_type_of_mob(context, pos, &mob_data) + } + + fn set_can_pass_doors(&mut self, can_pass: bool) { + self.base.can_pass_doors = can_pass; + } + + fn set_can_open_doors(&mut self, can_open: bool) { + self.base.can_open_doors = can_open; + } + + fn set_can_float(&mut self, can_float: bool) { + self.base.can_float = can_float; + } + + fn set_can_walk_over_fences(&mut self, can_walk: bool) { + self.base.can_walk_over_fences = can_walk; + } + + fn can_pass_doors(&self) -> bool { + self.base.can_pass_doors + } + + fn can_open_doors(&self) -> bool { + self.base.can_open_doors + } + + fn can_float(&self) -> bool { + self.base.can_float + } + + fn can_walk_over_fences(&self) -> bool { + self.base.can_walk_over_fences + } +} diff --git a/crates/pumpkin/src/entity/ai/pathfinder/walk_node_evaluator.rs b/crates/pumpkin/src/entity/ai/pathfinder/walk_node_evaluator.rs index 96ab4e157..2321e9bdc 100644 --- a/crates/pumpkin/src/entity/ai/pathfinder/walk_node_evaluator.rs +++ b/crates/pumpkin/src/entity/ai/pathfinder/walk_node_evaluator.rs @@ -1,4 +1,5 @@ -use pumpkin_util::math::{position::BlockPos, vector3::Vector3}; +use pumpkin_data::BlockDirection; +use pumpkin_util::math::{boundingbox::BoundingBox, position::BlockPos, vector3::Vector3}; use rustc_hash::FxHashMap; use crate::entity::ai::pathfinder::{ @@ -7,15 +8,11 @@ use crate::entity::ai::pathfinder::{ pathfinding_context::PathfindingContext, }; -const DIRECTIONS: [(i32, i32); 4] = [(1, 0), (-1, 0), (0, 1), (0, -1)]; -const DIAGONAL_DIRECTIONS: [(i32, i32); 4] = [(1, 1), (1, -1), (-1, 1), (-1, -1)]; - -const DEFAULT_MOB_JUMP_HEIGHT: f64 = 1.125; - pub struct WalkNodeEvaluator { - base: BaseNodeEvaluator, + pub base: BaseNodeEvaluator, path_types_cache: FxHashMap, PathType>, reusable_neighbors: [Option; 4], + pub is_amphibious: bool, } impl WalkNodeEvaluator { @@ -25,239 +22,304 @@ impl WalkNodeEvaluator { base: BaseNodeEvaluator::new(), path_types_cache: FxHashMap::default(), reusable_neighbors: [None, None, None, None], + is_amphibious: false, } } - const fn is_amphibious(&self) -> bool { - self.base.can_float + #[must_use] + pub fn get_floor_level(&self, pos: &BlockPos) -> f64 { + self.base.context.as_ref().map_or_else( + || f64::from(pos.0.y), + |ctx| { + if (self.base.can_float || self.is_amphibious) && ctx.is_water(pos) { + f64::from(pos.0.y) + 0.5 + } else { + ctx.get_floor_level(pos) + } + }, + ) } - // TODO: Check collision shapes for partial blocks (slabs/stairs) - #[allow(clippy::unused_self)] - fn get_floor_level(&self, pos: Vector3) -> f64 { - f64::from(pos.y) - } - - fn get_mob_jump_height(&self) -> f64 { + #[must_use] + pub fn get_mob_jump_height(&self) -> f64 { self.base .mob_data .as_ref() - .map_or(DEFAULT_MOB_JUMP_HEIGHT, |d| { - f64::from(d.max_step_height).max(DEFAULT_MOB_JUMP_HEIGHT) - }) + .map_or(1.125, |d| f64::from(d.max_step_height).max(1.125)) } - fn is_neighbor_valid(neighbor: Option<&Node>, current: &Node) -> bool { - if let Some(neighbor) = neighbor { - if neighbor.closed { - return false; + #[must_use] + pub fn is_neighbor_valid(neighbor: Option<&Node>, current: &Node) -> bool { + neighbor.is_some_and(|n| !n.closed && (n.cost_malus >= 0.0 || current.cost_malus < 0.0)) + } + + #[must_use] + pub fn is_diagonal_valid(&self, current: &Node, ew: Option<&Node>, ns: Option<&Node>) -> bool { + let (Some(ew), Some(ns)) = (ew, ns) else { + return false; + }; + if ns.pos.0.y > current.pos.0.y || ew.pos.0.y > current.pos.0.y { + return false; + } + if ew.path_type != PathType::WalkableDoor && ns.path_type != PathType::WalkableDoor { + let mob_width = self.base.mob_data.as_ref().map_or(0.6, |d| d.width); + if mob_width <= 1.0 || (ew.cost_malus <= 0.0 && ns.cost_malus <= 0.0) { + let can_pass_between_posts = ns.path_type == PathType::Fence + && ew.path_type == PathType::Fence + && mob_width < 0.5; + (ns.pos.0.y < current.pos.0.y || ns.cost_malus >= 0.0 || can_pass_between_posts) + && (ew.pos.0.y < current.pos.0.y + || ew.cost_malus >= 0.0 + || can_pass_between_posts) + } else { + false } - neighbor.cost_malus >= 0.0 || current.cost_malus < 0.0 } else { false } } - fn is_diagonal_valid( - &self, - current: &Node, - adj_x: Option<&Node>, - adj_z: Option<&Node>, - ) -> bool { - let (Some(adj_x), Some(adj_z)) = (adj_x, adj_z) else { - return false; - }; - if adj_x.pos.0.y > current.pos.0.y || adj_z.pos.0.y > current.pos.0.y { - return false; - } - if adj_z.path_type == PathType::WalkableDoor || adj_x.path_type == PathType::WalkableDoor { - return false; - } - let mob_width = self.base.mob_data.as_ref().map_or(0.6, |d| d.width); - let both_fence = adj_x.path_type == PathType::Fence && adj_z.path_type == PathType::Fence; - let fence_exception = both_fence && mob_width < 0.5; - - (adj_x.pos.0.y < current.pos.0.y || adj_x.cost_malus >= 0.0 || fence_exception) - && (adj_z.pos.0.y < current.pos.0.y || adj_z.cost_malus >= 0.0 || fence_exception) - } - - fn is_diagonal_node_valid(diagonal: Option<&Node>) -> bool { + #[must_use] + pub fn is_diagonal_node_valid(diagonal: Option<&Node>) -> bool { diagonal.is_some_and(|n| { !n.closed && n.path_type != PathType::WalkableDoor && n.cost_malus >= 0.0 }) } - /// Returns the best path node for the given position, handling step-ups, falls, and blocked nodes. - /// Returns the best path node for the given position, handling step-ups, falls, and blocked nodes. - fn find_accepted_node( + #[must_use] + pub const fn does_block_have_partial_collision(path_type: PathType) -> bool { + matches!( + path_type, + PathType::Fence | PathType::DoorWoodClosed | PathType::DoorIronClosed + ) + } + + pub fn can_reach_without_collision(&mut self, pos_to: Vector3) -> bool { + let Some(mob_data) = self.base.mob_data else { + return true; + }; + let mut bb = BoundingBox::new_from_pos( + mob_data.position.x, + mob_data.position.y, + mob_data.position.z, + &pumpkin_util::math::boundingbox::EntityDimensions::new( + mob_data.width, + mob_data.height, + 0.0, + ), + ); + let x_size = bb.max.x - bb.min.x; + let y_size = bb.max.y - bb.min.y; + let z_size = bb.max.z - bb.min.z; + let avg_size = bb.get_average_side_length(); + let mut delta = Vector3::new( + f64::from(pos_to.x) - mob_data.position.x + x_size / 2.0, + f64::from(pos_to.y) - mob_data.position.y + y_size / 2.0, + f64::from(pos_to.z) - mob_data.position.z + z_size / 2.0, + ); + let steps = (delta.length() / avg_size).ceil() as i32; + if steps > 0 { + delta = delta * (1.0 / f64::from(steps)); + for _ in 1..=steps { + bb = bb.shift(delta); + if self.has_collision(&bb) { + return false; + } + } + } + true + } + + #[expect(clippy::too_many_arguments)] + pub fn find_accepted_node( &mut self, - pos: Vector3, - max_y_step: i32, - last_feet_y: f64, + x: i32, + y: i32, + z: i32, + jump_size: i32, + node_height: f64, + travel_direction: BlockDirection, + block_path_type_current: PathType, ) -> Option { - let feet_y = self.get_floor_level(pos); - if feet_y - last_feet_y > self.get_mob_jump_height() { + let max_y_target = self.get_floor_level(&BlockPos::new(x, y, z)); + if max_y_target - node_height > self.get_mob_jump_height() { return None; } - let path_type = self.get_cached_path_type(pos); - let penalty = self.get_mob_penalty(path_type); + let path_type = self.get_cached_path_type(Vector3::new(x, y, z)); + let path_cost = self.get_mob_penalty(path_type); + let mut best = (path_cost >= 0.0) + .then(|| self.get_node_and_update_cost_to_max(x, y, z, path_type, path_cost)); - let mut node = (penalty >= 0.0).then(|| { - let mut n = self.base.get_node(pos.as_blockpos()); - n.path_type = path_type; - n.cost_malus = penalty.max(n.cost_malus); - n - }); - - // TODO: Add ray-march collision check for blocked types (fence/door) - - if path_type != PathType::Walkable - && !(self.is_amphibious() && path_type == PathType::Water) + if Self::does_block_have_partial_collision(block_path_type_current) + && best.as_ref().is_some_and(|b| b.cost_malus >= 0.0) + && !self.can_reach_without_collision(Vector3::new(x, y, z)) { - if (node.is_none() || node.as_ref().is_some_and(|n| n.cost_malus < 0.0)) - && max_y_step > 0 + best = None; + } + + if path_type != PathType::Walkable && (!self.is_amphibious || path_type != PathType::Water) + { + if (best.is_none() || best.as_ref().is_some_and(|b| b.cost_malus < 0.0)) + && jump_size > 0 && (path_type != PathType::Fence || self.base.can_walk_over_fences) && path_type != PathType::UnpassableRail && path_type != PathType::Trapdoor && path_type != PathType::PowderSnow { - let jump_node = self.get_jump_on_top_node(pos, max_y_step, last_feet_y); - if jump_node.is_some() { - node = jump_node; - } - } else if !self.is_amphibious() && path_type == PathType::Water && !self.base.can_float - { - node = self.get_non_water_node_below(pos, node); + best = self.try_jump_on( + x, + y, + z, + jump_size, + node_height, + travel_direction, + block_path_type_current, + ); + } else if !self.is_amphibious && path_type == PathType::Water && !self.base.can_float { + best = self.try_find_first_non_water_below(x, y, z, best); } else if path_type == PathType::Open { - node = Some(self.get_open_node(pos)); - } else if Self::is_blocked_type(path_type) && node.is_none() { - let mut n = self.base.get_node(pos.as_blockpos()); - n.closed = true; - n.path_type = path_type; - n.cost_malus = path_type.get_malus(); - node = Some(n); + best = Some(self.try_find_first_ground_node_below(x, y, z)); + } else if Self::does_block_have_partial_collision(path_type) && best.is_none() { + best = Some(self.get_closed_node(x, y, z, path_type)); } } - node + best } - /// Tries stepping up one block at a time (up to `max_y_step`). - fn get_jump_on_top_node( + #[expect(clippy::too_many_arguments)] + fn try_jump_on( &mut self, - pos: Vector3, - max_y_step: i32, - last_feet_y: f64, + x: i32, + y: i32, + z: i32, + jump_size: i32, + node_height: f64, + travel_direction: BlockDirection, + block_path_type_current: PathType, ) -> Option { - for dy in 1..=max_y_step { - let step_pos = Vector3::new(pos.x, pos.y + dy, pos.z); - let remaining_steps = max_y_step - dy; - - let feet_y = self.get_floor_level(step_pos); - if feet_y - last_feet_y > self.get_mob_jump_height() { - return None; - } - - let path_type = self.get_cached_path_type(step_pos); - let penalty = self.get_mob_penalty(path_type); - - if penalty >= 0.0 - && (path_type == PathType::Walkable - || (self.is_amphibious() && path_type == PathType::Water)) - { - let mut n = self.base.get_node(step_pos.as_blockpos()); - n.path_type = path_type; - n.cost_malus = penalty.max(n.cost_malus); - return Some(n); - } - - if remaining_steps > 0 - && (path_type != PathType::Fence || self.base.can_walk_over_fences) - && path_type != PathType::UnpassableRail - && path_type != PathType::Trapdoor - && path_type != PathType::PowderSnow - { - continue; - } - - if path_type == PathType::Open { - return Some(self.get_open_node(step_pos)); - } - - return None; + let node_above = self.find_accepted_node( + x, + y + 1, + z, + jump_size - 1, + node_height, + travel_direction, + block_path_type_current, + )?; + let mob_data = self.base.mob_data?; + if mob_data.width >= 1.0 { + return Some(node_above); + } + if node_above.path_type != PathType::Open && node_above.path_type != PathType::Walkable { + return Some(node_above); } - None - } - - /// Searches downward for the first non-`OPEN` block, respecting safe fall distance. - fn get_open_node(&mut self, pos: Vector3) -> Node { - let safe_fall_distance = self - .base - .mob_data - .as_ref() - .map_or(3, |d| d.max_fall_distance as i32); - - let mut check_y = pos.y - 1; - let bottom_y = pos.y - safe_fall_distance - 2; - - while check_y >= bottom_y { - let fall_dist = pos.y - check_y; - if fall_dist > safe_fall_distance { - let mut n = self.base.get_node(BlockPos::new(pos.x, check_y, pos.z)); - n.path_type = PathType::Blocked; - n.cost_malus = -1.0; - return n; - } - - let path_type = self.get_cached_path_type(Vector3::new(pos.x, check_y, pos.z)); - let penalty = self.get_mob_penalty(path_type); - - if path_type != PathType::Open { - if penalty >= 0.0 { - let mut n = self.base.get_node(BlockPos::new(pos.x, check_y, pos.z)); - n.path_type = path_type; - n.cost_malus = penalty.max(n.cost_malus); - return n; - } - let mut n = self.base.get_node(BlockPos::new(pos.x, check_y, pos.z)); - n.path_type = PathType::Blocked; - n.cost_malus = -1.0; - return n; - } - - check_y -= 1; + let center_x = f64::from(x) - f64::from(travel_direction.to_offset().x) + 0.5; + let center_z = f64::from(z) - f64::from(travel_direction.to_offset().z) + 0.5; + let half_width = f64::from(mob_data.width) / 2.0; + let floor_below = self.get_floor_level(&BlockPos::new( + center_x.floor() as i32, + y + 1, + center_z.floor() as i32, + )); + let floor_above = self.get_floor_level(&node_above.pos); + let grow = BoundingBox::new( + Vector3::new( + center_x - half_width, + floor_below + 0.001, + center_z - half_width, + ), + Vector3::new( + center_x + half_width, + f64::from(mob_data.height) + floor_above - 0.002, + center_z + half_width, + ), + ); + if self.has_collision(&grow) { + None + } else { + Some(node_above) } - - let mut n = self.base.get_node(pos.as_blockpos()); - n.path_type = PathType::Blocked; - n.cost_malus = -1.0; - n } - fn get_non_water_node_below( + fn try_find_first_non_water_below( &mut self, - pos: Vector3, - mut node: Option, + x: i32, + mut y: i32, + z: i32, + mut best: Option, ) -> Option { - let mut y = pos.y - 1; - while y > pos.y - 16 { - let path_type = self.get_cached_path_type(Vector3::new(pos.x, y, pos.z)); - if path_type != PathType::Water { - return node; + let min_y = self.base.mob_data.map_or(-64, |d| d.min_y); + y -= 1; + while y > min_y { + let path_type_local = self.get_cached_path_type(Vector3::new(x, y, z)); + if path_type_local != PathType::Water { + return best; } - let penalty = self.get_mob_penalty(path_type); - let mut n = self.base.get_node(BlockPos::new(pos.x, y, pos.z)); - n.path_type = path_type; - n.cost_malus = penalty.max(n.cost_malus); - node = Some(n); + let penalty = self.get_mob_penalty(path_type_local); + best = Some(self.get_node_and_update_cost_to_max(x, y, z, path_type_local, penalty)); y -= 1; } + best + } + + fn try_find_first_ground_node_below(&mut self, x: i32, y: i32, z: i32) -> Node { + let max_fall_distance = self + .base + .mob_data + .map_or(3, |d| d.max_fall_distance.floor() as i32); + let min_y = self.base.mob_data.map_or(-64, |d| d.min_y); + + for current_y in (min_y..y).rev() { + if y - current_y > max_fall_distance { + return self.get_blocked_node(x, current_y, z); + } + let path_type = self.get_cached_path_type(Vector3::new(x, current_y, z)); + let path_cost = self.get_mob_penalty(path_type); + if path_type != PathType::Open { + if path_cost >= 0.0 { + return self + .get_node_and_update_cost_to_max(x, current_y, z, path_type, path_cost); + } + return self.get_blocked_node(x, current_y, z); + } + } + self.get_blocked_node(x, y, z) + } + + fn get_node_and_update_cost_to_max( + &mut self, + x: i32, + y: i32, + z: i32, + path_type: PathType, + cost: f32, + ) -> Node { + let mut node = self.base.get_node(BlockPos::new(x, y, z)); + node.path_type = path_type; + node.cost_malus = node.cost_malus.max(cost); node } - fn get_mob_penalty(&self, path_type: PathType) -> f32 { + fn get_blocked_node(&mut self, x: i32, y: i32, z: i32) -> Node { + let mut node = self.base.get_node(BlockPos::new(x, y, z)); + node.path_type = PathType::Blocked; + node.cost_malus = -1.0; + node + } + + fn get_closed_node(&mut self, x: i32, y: i32, z: i32, path_type: PathType) -> Node { + let mut node = self.base.get_node(BlockPos::new(x, y, z)); + node.closed = true; + node.path_type = path_type; + node.cost_malus = path_type.get_malus(); + node + } + + #[must_use] + pub fn get_mob_penalty(&self, path_type: PathType) -> f32 { self.base .mob_data .as_ref() @@ -266,20 +328,11 @@ impl WalkNodeEvaluator { }) } - const fn is_blocked_type(path_type: PathType) -> bool { - matches!( - path_type, - PathType::Fence | PathType::DoorWoodClosed | PathType::DoorIronClosed - ) - } - - fn get_cached_path_type(&mut self, pos: Vector3) -> PathType { + pub fn get_cached_path_type(&mut self, pos: Vector3) -> PathType { if let Some(&cached) = self.path_types_cache.get(&pos) { return cached; } - // Temporarily take the context out to avoid overlapping borrows when calling - // the helper which requires `&mut self` let path_type = if let Some(mut ctx) = self.base.context.take() && let Some(mob_data) = self.base.mob_data { @@ -294,29 +347,74 @@ impl WalkNodeEvaluator { path_type } - fn has_collisions(&mut self, center: Vector3) -> bool { + pub fn has_collision(&mut self, bb: &BoundingBox) -> bool { self.base .context .as_mut() - .is_some_and(|ctx| ctx.has_collisions(center)) + .is_some_and(|ctx| ctx.has_collision(bb)) } - fn can_start_at(&mut self, pos: Vector3) -> bool { + pub fn can_start_at(&mut self, pos: Vector3) -> bool { let path_type = self.get_cached_path_type(pos); - path_type.is_passable() && !self.has_collisions(pos) + path_type != PathType::Open && self.get_mob_penalty(path_type) >= 0.0 } - fn get_start_node(&mut self, pos: Vector3) -> Option { - if !self.can_start_at(pos) { - return None; - } - + pub fn get_start_node(&mut self, pos: Vector3) -> Node { + let path_type = self.get_cached_path_type(pos); let mut node = self.base.get_node(pos.as_blockpos()); - let path_type = self.get_cached_path_type(pos); node.path_type = path_type; node.cost_malus = self.get_mob_penalty(path_type); + node + } - Some(node) + pub fn get_path_type_within_mob_bb( + &mut self, + context: &mut PathfindingContext, + x: i32, + y: i32, + z: i32, + mob_data: &MobData, + ) -> Vec { + let mut block_types = Vec::new(); + let mob_block_pos = mob_data.block_position(); + + for dx in 0..self.base.entity_width { + for dy in 0..self.base.entity_height { + for dz in 0..self.base.entity_depth { + let check_pos = Vector3::new(x + dx, y + dy, z + dz); + let mut block_type = self.get_path_type(context, check_pos); + + if block_type == PathType::DoorWoodClosed + && self.base.can_open_doors + && self.base.can_pass_doors + { + block_type = PathType::WalkableDoor; + } + + if block_type == PathType::DoorOpen && !self.base.can_pass_doors { + block_type = PathType::Blocked; + } + + if block_type == PathType::Rail { + let mob_pos = + Vector3::new(mob_block_pos.0, mob_block_pos.1, mob_block_pos.2); + let mob_below = + Vector3::new(mob_block_pos.0, mob_block_pos.1 - 1, mob_block_pos.2); + let mob_type = self.get_path_type(context, mob_pos); + let mob_below_type = self.get_path_type(context, mob_below); + if mob_type != PathType::Rail && mob_below_type != PathType::Rail { + block_type = PathType::UnpassableRail; + } + } + + block_types.push(block_type); + } + } + } + + block_types.sort(); + block_types.dedup(); + block_types } } @@ -329,119 +427,154 @@ impl NodeEvaluator for WalkNodeEvaluator { self.base.context = Some(context); self.base.mob_data = Some(mob_data); self.path_types_cache.clear(); + self.reusable_neighbors = [None, None, None, None]; } fn done(&mut self) { self.base.context = None; self.base.mob_data = None; self.path_types_cache.clear(); + self.reusable_neighbors = [None, None, None, None]; } fn get_start(&mut self) -> Option { - let mob_data = self.base.mob_data.as_ref()?; + let mob_data = self.base.mob_data?; let mob_x = mob_data.position.x; - let mob_y_f64 = mob_data.position.y; + let mob_y = mob_data.position.y; let mob_z = mob_data.position.z; - let on_ground = mob_data.on_ground; + let block_x = mob_x.floor() as i32; + let block_y = mob_y.floor() as i32; + let block_z = mob_z.floor() as i32; - // TODO: add swimming support - let y = if on_ground { - (mob_y_f64 + 0.5).floor() as i32 - } else { - let start_y = (mob_y_f64 + 1.0).floor() as i32; - let bottom_y = start_y - 64; - let mut found_y = start_y; - for check_y in (bottom_y..start_y).rev() { - let path_type = self.get_cached_path_type(Vector3::new( - mob_x.floor() as i32, - check_y, - mob_z.floor() as i32, - )); - if path_type != PathType::Open && path_type != PathType::Water { - found_y = check_y + 1; - break; + let mut start_y = block_y; + + if !mob_data.can_walk_on_water { + if self.base.can_float && mob_data.is_in_water { + let mut check_y = block_y; + if let Some(ref ctx) = self.base.context { + while ctx.is_water(&BlockPos::new(block_x, check_y + 1, block_z)) { + check_y += 1; + } + } + start_y = check_y; + } else if mob_data.on_ground { + start_y = (mob_y + 0.5).floor() as i32; + } else { + let start_check_y = (mob_y + 1.0).floor() as i32; + start_y = start_check_y; + let min_y = mob_data.min_y; + let mut check_y = start_check_y; + while check_y > min_y { + start_y = check_y; + check_y -= 1; + let below = BlockPos::new(block_x, check_y, block_z); + if let Some(ref ctx) = self.base.context + && !ctx.is_air(&below) + && !ctx.is_pathfindable( + &below, + crate::entity::ai::pathfinder::node::PathComputationType::Land, + ) + { + break; + } } } - found_y - }; - - let block_x = mob_x.floor() as i32; - let block_z = mob_z.floor() as i32; - let start_pos = Vector3::new(block_x, y, block_z); - - if let Some(node) = self.get_start_node(start_pos) { - return Some(node); } - for &(dx, dz) in &DIRECTIONS { - let try_pos = Vector3::new(block_x + dx, y, block_z + dz); - if let Some(node) = self.get_start_node(try_pos) { - return Some(node); + let start_pos = Vector3::new(block_x, start_y, block_z); + if !self.can_start_at(start_pos) { + let half_width = f64::from(mob_data.width) / 2.0; + let min_x = (mob_x - half_width).floor() as i32; + let max_x = (mob_x + half_width).floor() as i32; + let min_z = (mob_z - half_width).floor() as i32; + let max_z = (mob_z + half_width).floor() as i32; + + for candidate in [ + Vector3::new(min_x, start_y, min_z), + Vector3::new(min_x, start_y, max_z), + Vector3::new(max_x, start_y, min_z), + Vector3::new(max_x, start_y, max_z), + ] { + if self.can_start_at(candidate) { + return Some(self.get_start_node(candidate)); + } } } - let above_pos = Vector3::new(block_x, y + 1, block_z); - self.get_start_node(above_pos) + Some(self.get_start_node(start_pos)) } fn get_target(&mut self, pos: BlockPos) -> Target { - let node = self.base.get_node(pos); - Target::new(node) + Target::new(self.base.get_node(pos)) } - fn get_neighbors(&mut self, current: &Node, out_neighbors: &mut Vec) { - let headroom_type = self.get_cached_path_type(current.pos.0.add_raw(0, 1, 0)); - let current_type = self.get_cached_path_type(current.pos.0); + fn get_neighbors(&mut self, current: &Node, out: &mut Vec) { + let block_path_type_above = self.get_cached_path_type(current.pos.0.add_raw(0, 1, 0)); + let block_path_type_current = self.get_cached_path_type(current.pos.0); - let headroom_penalty = self.get_mob_penalty(headroom_type); - let max_y_step = if headroom_penalty >= 0.0 && current_type != PathType::StickyHoney { + let jump_size = if self.get_mob_penalty(block_path_type_above) >= 0.0 + && block_path_type_current != PathType::StickyHoney + { self.get_mob_jump_height().floor() as i32 } else { 0 }; - let floor_level = self.get_floor_level(current.pos.0); + let pos_height = self.get_floor_level(¤t.pos); - for i in 0..4 { - self.reusable_neighbors[i] = None; - } + self.reusable_neighbors.fill(None); - for (i, &(dx, dz)) in DIRECTIONS.iter().enumerate() { - let neighbor_pos = current.pos.0.add_raw(dx, 0, dz); + let horizontal_directions = [ + (2usize, 0, -1, BlockDirection::North), + (0usize, 0, 1, BlockDirection::South), + (1usize, -1, 0, BlockDirection::West), + (3usize, 1, 0, BlockDirection::East), + ]; - let neighbor_opt = self.find_accepted_node(neighbor_pos, max_y_step, floor_level); - - if let Some(neighbor) = neighbor_opt { - self.reusable_neighbors[i] = Some(neighbor); - if Self::is_neighbor_valid(Some(&neighbor), current) { - out_neighbors.push(neighbor); - } + for &(dir_idx, dx, dz, block_dir) in &horizontal_directions { + let node = self.find_accepted_node( + current.pos.0.x + dx, + current.pos.0.y, + current.pos.0.z + dz, + jump_size, + pos_height, + block_dir, + block_path_type_current, + ); + self.reusable_neighbors[dir_idx] = node; + if Self::is_neighbor_valid(node.as_ref(), current) + && let Some(n) = node + { + out.push(n); } } - for &(dx, dz) in &DIAGONAL_DIRECTIONS { - let dir1_idx = DIRECTIONS - .iter() - .position(|&(x, z)| x == dx && z == 0) - .unwrap_or(0); - let dir2_idx = DIRECTIONS - .iter() - .position(|&(x, z)| x == 0 && z == dz) - .unwrap_or(1); + let diagonal_directions = [ + (2usize, 3usize, 1, -1, BlockDirection::North), + (0usize, 1usize, -1, 1, BlockDirection::South), + (1usize, 2usize, -1, -1, BlockDirection::West), + (3usize, 0usize, 1, 1, BlockDirection::East), + ]; + for &(dir_idx, cw_idx, dx, dz, block_dir) in &diagonal_directions { if self.is_diagonal_valid( current, - self.reusable_neighbors[dir1_idx].as_ref(), - self.reusable_neighbors[dir2_idx].as_ref(), + self.reusable_neighbors[dir_idx].as_ref(), + self.reusable_neighbors[cw_idx].as_ref(), ) { - let diagonal_pos = current.pos.0.add_raw(dx, 0, dz); - - let diagonal_opt = self.find_accepted_node(diagonal_pos, max_y_step, floor_level); - - if let Some(diagonal) = diagonal_opt - && Self::is_diagonal_node_valid(Some(&diagonal)) + let diagonal_node = self.find_accepted_node( + current.pos.0.x + dx, + current.pos.0.y, + current.pos.0.z + dz, + jump_size, + pos_height, + block_dir, + block_path_type_current, + ); + if Self::is_diagonal_node_valid(diagonal_node.as_ref()) + && let Some(d) = diagonal_node { - out_neighbors.push(diagonal); + out.push(d); } } } @@ -453,82 +586,60 @@ impl NodeEvaluator for WalkNodeEvaluator { pos: Vector3, mob_data: &MobData, ) -> PathType { - let mut path_types = Vec::new(); - let mob_block_pos = mob_data.block_position(); - - for dy in 0..mob_data.get_bb_height() { - for dx in 0..mob_data.get_bb_width() { - for dz in 0..mob_data.get_bb_width() { - let check_pos = pos.add_raw(dx, dy, dz); - let mut cell_type = context.get_land_node_type(check_pos); - - if cell_type == PathType::DoorWoodClosed - && self.base.can_open_doors - && self.base.can_pass_doors - { - cell_type = PathType::WalkableDoor; - } - - if cell_type == PathType::DoorOpen && !self.base.can_pass_doors { - cell_type = PathType::Blocked; - } - - if cell_type == PathType::Rail { - let mob_pos = - Vector3::new(mob_block_pos.0, mob_block_pos.1, mob_block_pos.2); - let mob_below = - Vector3::new(mob_block_pos.0, mob_block_pos.1 - 1, mob_block_pos.2); - let mob_type = context.get_land_node_type(mob_pos); - let mob_below_type = context.get_land_node_type(mob_below); - if mob_type != PathType::Rail && mob_below_type != PathType::Rail { - cell_type = PathType::UnpassableRail; - } - } - - path_types.push(cell_type); - } - } + let block_types = self.get_path_type_within_mob_bb(context, pos.x, pos.y, pos.z, mob_data); + if block_types.len() == 1 { + return block_types[0]; } - // Sort+dedup to match vanilla's EnumSet ordinal iteration order - path_types.sort(); - path_types.dedup(); - - if path_types.contains(&PathType::Fence) { + if block_types.contains(&PathType::Fence) { return PathType::Fence; } - if path_types.contains(&PathType::UnpassableRail) { + + if block_types.contains(&PathType::UnpassableRail) { return PathType::UnpassableRail; } - let mut result = PathType::Blocked; - for &path_type in &path_types { - let penalty = mob_data.get_pathfinding_malus(path_type); - if penalty < 0.0 { + let mut highest_malus_path_type_within_bb = PathType::Blocked; + let mut highest_malus_within_bb = + mob_data.get_pathfinding_malus(highest_malus_path_type_within_bb); + + for &path_type in &block_types { + let malus = mob_data.get_pathfinding_malus(path_type); + if malus < 0.0 { return path_type; } - let result_penalty = mob_data.get_pathfinding_malus(result); - if penalty >= result_penalty { - result = path_type; + if malus >= highest_malus_within_bb { + highest_malus_within_bb = malus; + highest_malus_path_type_within_bb = path_type; } } - if self.base.entity_width <= 1 - && result != PathType::Open - && mob_data.get_pathfinding_malus(result) == 0.0 + let current_node_path_type = self.get_path_type(context, pos); + let is_large_mob = self.base.entity_width > 1; + if is_large_mob { + let is_current_node_cheaper = + mob_data.get_pathfinding_malus(current_node_path_type) < highest_malus_within_bb; + let cap_malus_due_to_cheap_node = is_current_node_cheaper + && mob_data.get_pathfinding_malus(PathType::BigMobsCloseToDanger) + < highest_malus_within_bb; + if cap_malus_due_to_cheap_node { + PathType::BigMobsCloseToDanger + } else { + highest_malus_path_type_within_bb + } + } else if current_node_path_type == PathType::Open + && highest_malus_path_type_within_bb != PathType::Open + && highest_malus_within_bb == 0.0 { - let raw_center = context.get_land_node_type(pos); - if raw_center == PathType::Open { - return PathType::Open; - } + PathType::Open + } else { + highest_malus_path_type_within_bb } - - result } fn get_path_type(&mut self, context: &mut PathfindingContext, pos: Vector3) -> PathType { - context.get_path_type_from_state(pos) + context.get_land_node_type(pos) } fn set_can_pass_doors(&mut self, can_pass: bool) { diff --git a/crates/pumpkin/src/entity/mob/blaze.rs b/crates/pumpkin/src/entity/mob/blaze.rs index 3e6589be9..41a64537a 100644 --- a/crates/pumpkin/src/entity/mob/blaze.rs +++ b/crates/pumpkin/src/entity/mob/blaze.rs @@ -1,9 +1,13 @@ +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Weak}; +use pumpkin_data::damage::DamageType; use pumpkin_data::entity::EntityType; +use pumpkin_protocol::java::client::play::Metadata; +use pumpkin_util::math::vector3::Vector3; use crate::entity::{ - Entity, + Entity, EntityBase, ai::goal::{ active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal, wander_around::WanderAroundGoal, @@ -13,13 +17,17 @@ use crate::entity::{ pub struct BlazeEntity { pub entity: Arc, + pub is_charged: AtomicBool, } impl BlazeEntity { pub fn new(entity: Entity) -> Arc { let entity = Arc::new(MobEntity::new(entity)); - let zombie = Self { entity }; - let mob_arc = Arc::new(zombie); + let blaze = Self { + entity, + is_charged: AtomicBool::new(false), + }; + let mob_arc = Arc::new(blaze); let mob_weak: Weak = { let mob_arc: Arc = mob_arc.clone(); Arc::downgrade(&mob_arc) @@ -63,24 +71,20 @@ impl BlazeEntity { mob_arc } - pub const fn set_charged(&self, _charged: bool) { - // TODO: - // let flags = &self.entity.living_entity.entity.flags; + pub fn is_charged(&self) -> bool { + self.is_charged.load(Ordering::Relaxed) + } - // let new_je_flags = if charged { - // flags.fetch_or(1, Ordering::Relaxed) | 1 - // } else { - // flags.fetch_and(!1, Ordering::Relaxed) & !1 - // }; - // self.entity - // .living_entity - // .entity - // .send_meta_data(&[Metadata::new( - // pumpkin_data::tracked_data::blaze::FLAGS_ID, - // MetaDataType::BYTE, - // new_je_flags, - // )]) - // .await; + pub fn set_charged(&self, charged: bool) { + self.is_charged.store(charged, Ordering::Relaxed); + let flags = i8::from(charged); + self.entity.living_entity.entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::blaze::DATA_FLAGS_ID, + flags, + )], + None, + ); } } @@ -88,4 +92,23 @@ impl Mob for BlazeEntity { fn get_mob_entity(&self) -> &MobEntity { &self.entity } + + fn mob_tick(&self, caller: &dyn EntityBase) { + let base_entity = &self.entity.living_entity.entity; + if !base_entity.is_alive() { + return; + } + + let on_ground = base_entity.on_ground.load(Ordering::Relaxed); + let vel = base_entity.velocity.load(); + if !on_ground && vel.y < 0.0 { + base_entity + .velocity + .store(Vector3::new(vel.x, vel.y * 0.6, vel.z)); + } + + if base_entity.touching_water.load(Ordering::Relaxed) { + caller.damage(caller, 1.0, DamageType::DROWN); + } + } } diff --git a/crates/pumpkin/src/entity/mob/cave_spider.rs b/crates/pumpkin/src/entity/mob/cave_spider.rs index 386bea4fc..9aba3492f 100644 --- a/crates/pumpkin/src/entity/mob/cave_spider.rs +++ b/crates/pumpkin/src/entity/mob/cave_spider.rs @@ -1,24 +1,93 @@ -use std::sync::Arc; +use std::sync::{Arc, Weak}; + +use pumpkin_data::effect::StatusEffect; +use pumpkin_data::entity::EntityType; +use pumpkin_data::potion::Effect; use crate::entity::{ - Entity, - mob::{Mob, MobEntity, spider::SpiderEntity}, + Entity, EntityBase, + ai::goal::{ + active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal, + look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, revenge::RevengeGoal, + swim::SwimGoal, wander_around::WanderAroundGoal, + }, + mob::{Mob, MobEntity}, }; pub struct CaveSpiderEntity { - pub spider: Arc, + pub mob_entity: MobEntity, } impl CaveSpiderEntity { pub fn new(entity: Entity) -> Arc { - let spider = SpiderEntity::new(entity); - Arc::new(Self { spider }) + let mob_entity = MobEntity::new(entity); + let cave_spider = Self { mob_entity }; + let mob_arc = Arc::new(cave_spider); + 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); + let mut target_selector = mob_arc + .mob_entity + .target_selector + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + + goal_selector.add_goal(1, Box::new(SwimGoal::default())); + goal_selector.add_goal(3, Box::new(MeleeAttackGoal::new(1.0, false))); + goal_selector.add_goal(5, Box::new(WanderAroundGoal::new(0.8))); + goal_selector.add_goal( + 6, + LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 8.0), + ); + goal_selector.add_goal(6, Box::new(RandomLookAroundGoal::default())); + + target_selector.add_goal(1, Box::new(RevengeGoal::new(true))); + target_selector.add_goal( + 2, + ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::PLAYER, true), + ); + target_selector.add_goal( + 3, + ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::IRON_GOLEM, true), + ); + }; + + mob_arc } } impl Mob for CaveSpiderEntity { fn get_mob_entity(&self) -> &MobEntity { - self.spider.get_mob_entity() + &self.mob_entity + } + + fn on_attack(&self, target: &dyn EntityBase) { + if let Some(living) = target.get_living_entity() { + let world = self.mob_entity.living_entity.entity.world.load(); + let duration = match world.level_info.load().difficulty { + pumpkin_util::Difficulty::Normal => 7 * 20, + pumpkin_util::Difficulty::Hard => 15 * 20, + _ => 0, + }; + if duration > 0 { + living.add_effect(Effect { + effect_type: &StatusEffect::POISON, + duration, + amplifier: 0, + ambient: false, + show_particles: true, + show_icon: true, + blend: false, + }); + } + } } - // TODO: Poison effect on attack } diff --git a/crates/pumpkin/src/entity/mob/elder_guardian.rs b/crates/pumpkin/src/entity/mob/elder_guardian.rs index 94f70ad1b..0f2b56933 100644 --- a/crates/pumpkin/src/entity/mob/elder_guardian.rs +++ b/crates/pumpkin/src/entity/mob/elder_guardian.rs @@ -1,9 +1,14 @@ +use std::sync::atomic::{AtomicI32, Ordering}; use std::sync::{Arc, Weak}; +use pumpkin_data::damage::DamageType; +use pumpkin_data::effect::StatusEffect; use pumpkin_data::entity::EntityType; +use pumpkin_data::potion::Effect; +use pumpkin_protocol::java::client::play::{CGameEvent, GameEvent}; use crate::entity::{ - Entity, + Entity, EntityBase, ai::goal::{ active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal, wander_around::WanderAroundGoal, @@ -13,12 +18,16 @@ use crate::entity::{ pub struct ElderGuardianEntity { pub mob_entity: MobEntity, + tick_count: AtomicI32, } impl ElderGuardianEntity { pub fn new(entity: Entity) -> Arc { let mob_entity = MobEntity::new(entity); - let guardian = Self { mob_entity }; + let guardian = Self { + mob_entity, + tick_count: AtomicI32::new(0), + }; let mob_arc = Arc::new(guardian); let mob_weak: Weak = { let mob_arc: Arc = mob_arc.clone(); @@ -59,4 +68,40 @@ impl Mob for ElderGuardianEntity { fn get_mob_entity(&self) -> &MobEntity { &self.mob_entity } + + fn mob_tick(&self, _caller: &dyn EntityBase) { + let entity = &self.mob_entity.living_entity.entity; + if !entity.is_alive() { + return; + } + + let ticks = self.tick_count.fetch_add(1, Ordering::Relaxed) + 1; + if ticks % 1200 == 0 { + let pos = entity.pos.load(); + let world = entity.world.load(); + let players = world.get_nearby_players(pos, 50.0); + let packet = CGameEvent::new(GameEvent::PlayElderGuardianMobAppearance, 1.0); + + for player in players { + player.try_send_client_packet(&packet); + player.living_entity.add_effect(Effect { + effect_type: &StatusEffect::MINING_FATIGUE, + duration: 6000, + amplifier: 2, + ambient: false, + show_particles: true, + show_icon: true, + blend: false, + }); + } + } + } + + fn on_damage(&self, _damage_type: DamageType, source: Option<&dyn EntityBase>) { + if let Some(src) = source + && let Some(living) = src.get_living_entity() + { + let _ = living.damage(src, 2.0, DamageType::THORNS); + } + } } diff --git a/crates/pumpkin/src/entity/mob/endermite.rs b/crates/pumpkin/src/entity/mob/endermite.rs index d0cf5bc39..5636e5c5a 100644 --- a/crates/pumpkin/src/entity/mob/endermite.rs +++ b/crates/pumpkin/src/entity/mob/endermite.rs @@ -1,9 +1,11 @@ +use std::sync::atomic::{AtomicI32, Ordering}; use std::sync::{Arc, Weak}; use pumpkin_data::entity::EntityType; +use pumpkin_nbt::compound::NbtCompound; use crate::entity::{ - Entity, + Entity, EntityBase, ai::goal::{ active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, swim::SwimGoal, @@ -14,12 +16,16 @@ use crate::entity::{ pub struct EndermiteEntity { pub mob_entity: MobEntity, + pub lifetime: AtomicI32, } impl EndermiteEntity { pub fn new(entity: Entity) -> Arc { let mob_entity = MobEntity::new(entity); - let endermite = Self { mob_entity }; + let endermite = Self { + mob_entity, + lifetime: AtomicI32::new(0), + }; let mob_arc = Arc::new(endermite); let mob_weak: Weak = { let mob_arc: Arc = mob_arc.clone(); @@ -61,4 +67,26 @@ impl Mob for EndermiteEntity { fn get_mob_entity(&self) -> &MobEntity { &self.mob_entity } + + fn mob_write_nbt(&self, nbt: &mut NbtCompound) { + nbt.put_int("Lifetime", self.lifetime.load(Ordering::Relaxed)); + } + + fn mob_read_nbt(&self, nbt: &NbtCompound) { + if let Some(life) = nbt.get_int("Lifetime") { + self.lifetime.store(life, Ordering::Relaxed); + } + } + + fn mob_tick(&self, _caller: &dyn EntityBase) { + let entity = &self.mob_entity.living_entity.entity; + if !entity.is_alive() { + return; + } + + let life = self.lifetime.fetch_add(1, Ordering::Relaxed) + 1; + if life >= 2400 { + entity.remove(); + } + } } diff --git a/crates/pumpkin/src/entity/mob/giant.rs b/crates/pumpkin/src/entity/mob/giant.rs index 08843ca30..699468bbd 100644 --- a/crates/pumpkin/src/entity/mob/giant.rs +++ b/crates/pumpkin/src/entity/mob/giant.rs @@ -18,6 +18,39 @@ pub struct GiantEntity { impl GiantEntity { pub fn new(entity: Entity) -> Arc { let mob_entity = MobEntity::new(entity); + { + let mut attributes = mob_entity + .living_entity + .attributes + .write() + .unwrap_or_else(std::sync::PoisonError::into_inner); + if let Some(health) = + attributes.get_mut(&pumpkin_data::attributes::Attributes::MAX_HEALTH.id) + { + health.base_value = 100.0; + health + .dirty + .store(true, std::sync::atomic::Ordering::Relaxed); + } + if let Some(speed) = + attributes.get_mut(&pumpkin_data::attributes::Attributes::MOVEMENT_SPEED.id) + { + speed.base_value = 0.5; + speed + .dirty + .store(true, std::sync::atomic::Ordering::Relaxed); + } + if let Some(damage) = + attributes.get_mut(&pumpkin_data::attributes::Attributes::ATTACK_DAMAGE.id) + { + damage.base_value = 50.0; + damage + .dirty + .store(true, std::sync::atomic::Ordering::Relaxed); + } + } + mob_entity.living_entity.health.store(100.0); + let giant = Self { mob_entity }; let mob_arc = Arc::new(giant); diff --git a/crates/pumpkin/src/entity/mob/guardian.rs b/crates/pumpkin/src/entity/mob/guardian.rs index 591f74455..94883f312 100644 --- a/crates/pumpkin/src/entity/mob/guardian.rs +++ b/crates/pumpkin/src/entity/mob/guardian.rs @@ -1,9 +1,10 @@ use std::sync::{Arc, Weak}; +use pumpkin_data::damage::DamageType; use pumpkin_data::entity::EntityType; use crate::entity::{ - Entity, + Entity, EntityBase, ai::goal::{ active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal, wander_around::WanderAroundGoal, @@ -71,4 +72,12 @@ impl Mob for GuardianEntity { fn get_mob_entity(&self) -> &MobEntity { &self.mob_entity } + + fn on_damage(&self, _damage_type: DamageType, source: Option<&dyn EntityBase>) { + if let Some(src) = source + && let Some(living) = src.get_living_entity() + { + let _ = living.damage(src, 2.0, DamageType::THORNS); + } + } } diff --git a/crates/pumpkin/src/entity/mob/hoglin.rs b/crates/pumpkin/src/entity/mob/hoglin.rs index 7a68c0774..fa87de6b6 100644 --- a/crates/pumpkin/src/entity/mob/hoglin.rs +++ b/crates/pumpkin/src/entity/mob/hoglin.rs @@ -1,25 +1,75 @@ +use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; use std::sync::{Arc, Weak}; +use pumpkin_data::attributes::Attributes; +use pumpkin_data::dimension::Dimension; use pumpkin_data::entity::EntityType; +use pumpkin_data::sound::{Sound, SoundCategory}; +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_protocol::java::client::play::Metadata; +use pumpkin_util::math::vector3::Vector3; use crate::entity::{ - Entity, + Entity, EntityBase, ai::goal::{ active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal, - look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, swim::SwimGoal, - wander_around::WanderAroundGoal, + look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, revenge::RevengeGoal, + swim::SwimGoal, wander_around::WanderAroundGoal, }, mob::{Mob, MobEntity}, }; +use crate::world::World; pub struct HoglinEntity { pub mob_entity: MobEntity, + pub immune_to_zombification: AtomicBool, + pub time_in_overworld: AtomicI32, + pub cannot_be_hunted: AtomicBool, + pub is_baby: AtomicBool, } impl HoglinEntity { + pub const CONVERSION_TIME: i32 = 300; + pub const XP_REWARD: u32 = 5; + pub fn new(entity: Entity) -> Arc { let mob_entity = MobEntity::new(entity); - let hoglin = Self { mob_entity }; + { + let mut attributes = 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 = 40.0; + health.dirty.store(true, Ordering::Relaxed); + } + if let Some(speed) = attributes.get_mut(&Attributes::MOVEMENT_SPEED.id) { + speed.base_value = 0.3; + speed.dirty.store(true, Ordering::Relaxed); + } + if let Some(knockback_res) = attributes.get_mut(&Attributes::KNOCKBACK_RESISTANCE.id) { + knockback_res.base_value = 0.6; + knockback_res.dirty.store(true, Ordering::Relaxed); + } + if let Some(attack_kb) = attributes.get_mut(&Attributes::ATTACK_KNOCKBACK.id) { + attack_kb.base_value = 1.0; + attack_kb.dirty.store(true, Ordering::Relaxed); + } + if let Some(damage) = attributes.get_mut(&Attributes::ATTACK_DAMAGE.id) { + damage.base_value = 6.0; + damage.dirty.store(true, Ordering::Relaxed); + } + } + mob_entity.living_entity.health.store(40.0); + + let hoglin = Self { + mob_entity, + immune_to_zombification: AtomicBool::new(false), + time_in_overworld: AtomicI32::new(0), + cannot_be_hunted: AtomicBool::new(false), + is_baby: AtomicBool::new(false), + }; let mob_arc = Arc::new(hoglin); let mob_weak: Weak = { let mob_arc: Arc = mob_arc.clone(); @@ -47,18 +97,134 @@ impl HoglinEntity { .target_selector .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); + target_selector.add_goal(1, Box::new(RevengeGoal::new(true))); target_selector.add_goal( - 1, + 2, ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::PLAYER, true), ); }; mob_arc } + + pub fn is_immune_to_zombification(&self) -> bool { + self.immune_to_zombification.load(Ordering::Relaxed) + } + + pub fn set_immune_to_zombification(&self, immune: bool) { + self.immune_to_zombification + .store(immune, Ordering::Relaxed); + self.mob_entity.living_entity.entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::hoglin::DATA_IMMUNE_TO_ZOMBIFICATION, + immune, + )], + None, + ); + } + + pub fn is_converting(&self, world: &World) -> bool { + !self.is_immune_to_zombification() + && !self.mob_entity.is_no_ai() + && world.dimension.minecraft_name != Dimension::THE_NETHER.minecraft_name + } + + fn convert_to_zoglin(&self) { + let entity = &self.mob_entity.living_entity.entity; + let world = entity.world.load(); + let pos = entity.pos.load(); + + if world.level_info.load().difficulty != pumpkin_util::Difficulty::Peaceful { + world.play_sound( + Sound::EntityHoglinConvertedToZombified, + SoundCategory::Hostile, + &pos, + ); + } + + let zoglin = crate::entity::r#type::from_type( + &EntityType::ZOGLIN, + pos, + &world, + uuid::Uuid::new_v4(), + ); + + let zoglin_base = zoglin.get_entity(); + zoglin_base.set_rotation(entity.yaw.load(), entity.pitch.load()); + zoglin_base.head_yaw.store(entity.head_yaw.load()); + zoglin_base.velocity.store(entity.velocity.load()); + + if let Some(living) = zoglin.get_living_entity() { + living.set_health(self.mob_entity.living_entity.health.load()); + } + + if let Some(custom_name) = &**entity.custom_name.load() { + zoglin_base.set_custom_name(custom_name.clone()); + } + + world.spawn_entity(zoglin); + entity.remove(); + } } impl Mob for HoglinEntity { fn get_mob_entity(&self) -> &MobEntity { &self.mob_entity } + + fn mob_write_nbt(&self, nbt: &mut NbtCompound) { + if self.is_immune_to_zombification() { + nbt.put_bool("IsImmuneToZombification", true); + } + let time = self.time_in_overworld.load(Ordering::Relaxed); + if time > 0 { + nbt.put_int("TimeInOverworld", time); + } + if self.cannot_be_hunted.load(Ordering::Relaxed) { + nbt.put_bool("CannotBeHunted", true); + } + } + + fn mob_read_nbt(&self, nbt: &NbtCompound) { + if let Some(immune) = nbt.get_bool("IsImmuneToZombification") { + self.set_immune_to_zombification(immune); + } + if let Some(time) = nbt.get_int("TimeInOverworld") { + self.time_in_overworld.store(time, Ordering::Relaxed); + } + if let Some(cannot_hunt) = nbt.get_bool("CannotBeHunted") { + self.cannot_be_hunted.store(cannot_hunt, Ordering::Relaxed); + } + } + + fn mob_tick(&self, _caller: &dyn EntityBase) { + let entity = &self.mob_entity.living_entity.entity; + if !entity.is_alive() { + return; + } + + let world = entity.world.load(); + if self.is_converting(&world) { + let time = self.time_in_overworld.fetch_add(1, Ordering::Relaxed) + 1; + if time > Self::CONVERSION_TIME { + self.convert_to_zoglin(); + } + } else { + self.time_in_overworld.store(0, Ordering::Relaxed); + } + } + + fn on_attack(&self, target: &dyn EntityBase) { + let my_pos = self.mob_entity.living_entity.entity.pos.load(); + let target_pos = target.get_entity().pos.load(); + let dx = target_pos.x - my_pos.x; + let dz = target_pos.z - my_pos.z; + let dist = dx.hypot(dz).max(0.001); + let vel = target.get_entity().velocity.load(); + target.get_entity().velocity.store(Vector3::new( + vel.x + (dx / dist) * 0.5, + 0.5, + vel.z + (dz / dist) * 0.5, + )); + } } diff --git a/crates/pumpkin/src/entity/mob/magma_cube.rs b/crates/pumpkin/src/entity/mob/magma_cube.rs index 2c900f97c..38dda2989 100644 --- a/crates/pumpkin/src/entity/mob/magma_cube.rs +++ b/crates/pumpkin/src/entity/mob/magma_cube.rs @@ -1,4 +1,7 @@ use std::sync::Arc; +use std::sync::atomic::Ordering; + +use pumpkin_data::attributes::Attributes; use crate::entity::{ Entity, EntityBase, @@ -12,6 +15,27 @@ pub struct MagmaCubeEntity { impl MagmaCubeEntity { pub fn new(entity: Entity) -> Arc { let slime = SlimeEntity::new(entity); + let size = slime.get_size(); + { + let mut attributes = slime + .get_mob_entity() + .living_entity + .attributes + .write() + .unwrap_or_else(std::sync::PoisonError::into_inner); + if let Some(speed) = attributes.get_mut(&Attributes::MOVEMENT_SPEED.id) { + speed.base_value = 0.2; + speed.dirty.store(true, Ordering::Relaxed); + } + if let Some(damage) = attributes.get_mut(&Attributes::ATTACK_DAMAGE.id) { + damage.base_value = (size + 2) as f64; + damage.dirty.store(true, Ordering::Relaxed); + } + if let Some(armor) = attributes.get_mut(&Attributes::ARMOR.id) { + armor.base_value = (size * 3) as f64; + armor.dirty.store(true, Ordering::Relaxed); + } + } Arc::new(Self { slime }) } } @@ -30,6 +54,8 @@ impl Mob for MagmaCubeEntity { } fn mob_player_collision(&self, player: &Arc) { - self.slime.mob_player_collision(player); + self.slime + .get_mob_entity() + .try_attack(&*self.slime, &**player); } } diff --git a/crates/pumpkin/src/entity/mob/mod.rs b/crates/pumpkin/src/entity/mob/mod.rs index b5e05f69e..ccb8c3855 100644 --- a/crates/pumpkin/src/entity/mob/mod.rs +++ b/crates/pumpkin/src/entity/mob/mod.rs @@ -14,6 +14,7 @@ use pumpkin_data::item::Item; use pumpkin_data::item_stack::ItemStack; use pumpkin_data::tag::{self, Taggable}; use pumpkin_data::tracked_data; +use pumpkin_data::{Block, BlockDirection}; use pumpkin_nbt::compound::NbtCompound; use pumpkin_protocol::java::client::play::{CHeadRot, CUpdateEntityRot, Metadata}; use pumpkin_util::Difficulty; @@ -395,6 +396,12 @@ impl MobEntity { current_brightness <= dimension.monster_spawn_light_level.get(&mut random) as u8 } + pub fn check_mob_spawn_rules(world: &World, pos: &BlockPos) -> bool { + let below = pos.down(); + let state = world.get_block_state(&below); + state.is_side_solid(BlockDirection::Up) + } + pub fn check_monster_spawn_rules(world: &World, pos: &BlockPos, is_thundering: bool) -> bool { if world.level_info.load().difficulty == Difficulty::Peaceful { return false; @@ -404,8 +411,53 @@ impl MobEntity { return false; } - //TODO:check_mob_spawn_rules(entity_type, world, spawn_reason, pos).await - true + Self::check_mob_spawn_rules(world, pos) + } + + pub fn check_any_light_monster_spawn_rules(world: &World, pos: &BlockPos) -> bool { + if world.level_info.load().difficulty == Difficulty::Peaceful { + return false; + } + + Self::check_mob_spawn_rules(world, pos) + } + + pub fn check_surface_monsters_spawn_rules( + world: &World, + pos: &BlockPos, + is_thundering: bool, + ) -> bool { + Self::check_monster_spawn_rules(world, pos, is_thundering) && world.can_see_sky(pos) + } + + pub fn check_animal_spawn_rules(world: &World, pos: &BlockPos) -> bool { + let below = pos.down(); + world + .get_block(&below) + .has_tag(&tag::Block::MINECRAFT_ANIMALS_SPAWNABLE_ON) + && Self::is_bright_enough_to_spawn(world, pos) + } + + pub fn is_bright_enough_to_spawn(world: &World, pos: &BlockPos) -> bool { + world.get_max_local_raw_brightness(pos) > 8 + } + + pub fn check_surface_water_animal_spawn_rules(world: &World, pos: &BlockPos) -> bool { + let sea_level = world.sea_level; + let min_spawn_level = sea_level - 13; + pos.0.y >= min_spawn_level + && pos.0.y <= sea_level + && world + .get_fluid(&pos.down()) + .has_tag(&tag::Fluid::MINECRAFT_WATER) + && (world.get_block(&pos.up()) == &Block::WATER + || world + .get_fluid(&pos.up()) + .has_tag(&tag::Fluid::MINECRAFT_WATER)) + } + + pub fn check_surface_ageable_water_creature_spawn_rules(world: &World, pos: &BlockPos) -> bool { + Self::check_surface_water_animal_spawn_rules(world, pos) } pub fn try_attack(&self, caller: &dyn EntityBase, target: &dyn EntityBase) { @@ -696,6 +748,8 @@ pub trait Mob: EntityBase + Send + Sync { fn on_damage(&self, _damage_type: DamageType, _source: Option<&dyn EntityBase>) {} + fn on_attack(&self, _target: &dyn EntityBase) {} + fn on_eating_grass(&self) {} fn modify_incoming_damage(&self, amount: f32, _damage_type: DamageType) -> f32 { diff --git a/crates/pumpkin/src/entity/mob/piglin.rs b/crates/pumpkin/src/entity/mob/piglin.rs index 4e9b1d62e..b366ce227 100644 --- a/crates/pumpkin/src/entity/mob/piglin.rs +++ b/crates/pumpkin/src/entity/mob/piglin.rs @@ -115,10 +115,8 @@ impl PiglinEntity { .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); - // Retaliate when attacked target_selector.add_goal(1, Box::new(RevengeGoal::new(true))); - // Hostile to players unless wearing safe gold armor target_selector.add_goal( 2, Box::new(ActiveTargetGoal::new( @@ -133,7 +131,6 @@ impl PiglinEntity { )), ); - // Hostile to wither skeletons and withers target_selector.add_goal( 3, ActiveTargetGoal::with_default( @@ -147,7 +144,6 @@ impl PiglinEntity { ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::WITHER, true), ); - // Adults that can hunt are hostile to hoglins let piglin_clone = mob_arc.clone(); target_selector.add_goal( 4, @@ -506,7 +502,6 @@ impl Mob for PiglinEntity { .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); - // Spawn weapon: 50% crossbow, 5% golden spear (10% of remaining 50%), 45% golden sword let weapon = if rand::random::() < 0.5 { &Item::CROSSBOW } else if rand::random_range(0..10) == 0 { @@ -516,7 +511,6 @@ impl Mob for PiglinEntity { }; equipment.put(&EquipmentSlot::MAIN_HAND, ItemStack::new(1, weapon)); - // Armor: 10% chance per piece for golden armor if rand::random::() < 0.1 { equipment.put( &EquipmentSlot::HEAD, diff --git a/crates/pumpkin/src/entity/mob/piglin_ai.rs b/crates/pumpkin/src/entity/mob/piglin_ai.rs index d58f9aed9..5f57f4084 100644 --- a/crates/pumpkin/src/entity/mob/piglin_ai.rs +++ b/crates/pumpkin/src/entity/mob/piglin_ai.rs @@ -11,8 +11,6 @@ use crate::entity::Entity; use crate::entity::living::LivingEntity; use crate::entity::mob::piglin::PiglinEntity; -/// Vanilla Piglin AI logic, bartering, reactions, and utilities. -/// Mirrors `net.minecraft.world.entity.monster.piglin.PiglinAi`. pub struct PiglinAi; impl PiglinAi { @@ -59,31 +57,65 @@ impl PiglinAi { rand::random::() < Self::PROBABILITY_OF_CELEBRATION_DANCE } - pub fn is_wearing_safe_armor(target: &LivingEntity) -> bool { - let Ok(equipment) = target.entity_equipment.try_lock() else { + #[must_use] + pub fn is_wearing_safe_armor(entity: &LivingEntity) -> bool { + let Ok(guard) = entity.entity_equipment.try_lock() else { return false; }; - [ + + for slot in [ EquipmentSlot::HEAD, EquipmentSlot::CHEST, EquipmentSlot::LEGS, EquipmentSlot::FEET, - ] - .iter() - .any(|slot| { - let stack = equipment.get(slot); - !stack.is_empty() + ] { + if let Some(stack) = guard.equipment.get(&slot) + && !stack.is_empty() && (stack.item.has_tag(&tag::Item::MINECRAFT_PIGLIN_SAFE_ARMOR) || stack.item.has_tag(&tag::Item::MINECRAFT_PIGLIN_LOVED)) - }) + { + return true; + } + } + false + } + + #[must_use] + pub fn is_holding_loved_item(entity: &LivingEntity) -> bool { + let Ok(guard) = entity.entity_equipment.try_lock() else { + return false; + }; + for slot in [EquipmentSlot::MAIN_HAND, EquipmentSlot::OFF_HAND] { + if let Some(stack) = guard.equipment.get(&slot) + && !stack.is_empty() + && Self::is_loved_item(stack) + { + return true; + } + } + false + } + + #[must_use] + pub fn is_admiring_disabled(piglin: &PiglinEntity) -> bool { + piglin.is_admiring_disabled() } #[must_use] pub fn can_admire(piglin: &PiglinEntity, item_stack: &ItemStack) -> bool { - !piglin.is_admiring_disabled() - && !piglin.is_admiring() - && piglin.is_adult() - && Self::is_barter_currency(item_stack) + if item_stack.is_empty() || Self::is_admiring_disabled(piglin) { + return false; + } + if Self::is_barter_currency(item_stack) { + return piglin.is_adult() && !piglin.is_admiring(); + } + if piglin.is_admiring() { + return false; + } + if Self::is_food(item_stack) { + return !piglin.has_eaten_recently(); + } + Self::is_loved_item(item_stack) } #[must_use] @@ -113,7 +145,6 @@ impl PiglinAi { Self::is_loved_item(item_stack) } - /// Generates randomized barter drop items matching vanilla `BuiltInLootTables.PIGLIN_BARTERING`. #[must_use] pub fn get_barter_response_items() -> Vec { let roll = rand::random_range(0..459); @@ -121,7 +152,7 @@ impl PiglinAi { 0..5 => vec![ItemStack::new(1, &Item::ENCHANTED_BOOK)], 5..13 => vec![ItemStack::new(1, &Item::IRON_BOOTS)], 13..21 => vec![ItemStack::new(1, &Item::SPLASH_POTION)], - 21..39 => vec![ItemStack::new(1, &Item::POTION)], // Potion / Water bottle + 21..39 => vec![ItemStack::new(1, &Item::POTION)], 39..49 => vec![ItemStack::new( rand::random_range(10..=36), &Item::IRON_NUGGET, diff --git a/crates/pumpkin/src/entity/mob/piglin_brute.rs b/crates/pumpkin/src/entity/mob/piglin_brute.rs index d989cbd2b..4ff19fc34 100644 --- a/crates/pumpkin/src/entity/mob/piglin_brute.rs +++ b/crates/pumpkin/src/entity/mob/piglin_brute.rs @@ -69,7 +69,6 @@ impl PiglinBruteEntity { .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); target_selector.add_goal(1, Box::new(RevengeGoal::new(true))); - // Piglin brutes are always hostile to players (even with gold armor) target_selector.add_goal( 2, ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::PLAYER, true), diff --git a/crates/pumpkin/src/entity/mob/shulker.rs b/crates/pumpkin/src/entity/mob/shulker.rs index 0df7f513d..5dd7678e1 100644 --- a/crates/pumpkin/src/entity/mob/shulker.rs +++ b/crates/pumpkin/src/entity/mob/shulker.rs @@ -28,7 +28,6 @@ const NO_COLOR: u8 = 16; const PEEK_PER_TICK: f32 = 0.05; const MAX_TELEPORT_DISTANCE: i32 = 8; -/// Axis type for bullet creation (the bullet cannot start moving along this axis) #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Axis { X = 0, @@ -36,7 +35,6 @@ pub enum Axis { Z = 2, } -/// Local extension trait so we can add helpers to the external `BlockDirection` type. trait BlockDirectionExt { fn axis_of(self) -> Axis; } @@ -56,7 +54,6 @@ pub struct ShulkerEntity { attach_face: AtomicU8, peek_amount: AtomicU8, color: AtomicU8, - /// Visual interpolation current_peek_amount: AtomicCell, prev_peek_amount: AtomicCell, } @@ -153,7 +150,6 @@ impl ShulkerEntity { let pos = entity.pos.load(); if amount == 0 { - // Closed from open state world.play_sound_fine( Sound::EntityShulkerClose, SoundCategory::Hostile, @@ -162,7 +158,6 @@ impl ShulkerEntity { 1.0, ); } else if self.peek_amount.load(Ordering::Relaxed) == 0 { - // Opened from closed state world.play_sound_fine( Sound::EntityShulkerOpen, SoundCategory::Hostile, @@ -187,8 +182,6 @@ impl ShulkerEntity { self.peek_amount.load(Ordering::Relaxed) == 0 } - /// Advances `current_peek_amount` toward target by `PEEK_PER_TICK`. - /// Returns true if the value changed. fn update_peek_amount(&self) -> bool { let current = self.current_peek_amount.load(); self.prev_peek_amount.store(current); @@ -208,8 +201,6 @@ impl ShulkerEntity { true } - /// Returns `Some(direction)` if the neighbour block in that direction from - /// `pos` is solid (the shulker can attach there). fn find_attachable_face(&self, pos: &BlockPos) -> Option { let entity = &self.mob_entity.living_entity.entity; let world = entity.world.load(); @@ -223,25 +214,20 @@ impl ShulkerEntity { None } - /// Check whether the shulker can stay where it is. - /// The block at `pos` must be free (air) and the block in the `face` direction from `pos` must be solid. fn can_stay_at(&self, pos: &BlockPos, face: BlockDirection) -> bool { let entity = &self.mob_entity.living_entity.entity; let world = entity.world.load(); - // Shulker's own block must not be occupied let own_state = world.get_block_state(pos); if !own_state.is_air() { return false; } - // The block the shulker is attached to must still be solid let neighbour = pos.offset_direction(face); let state = world.get_block_state(&neighbour); state.is_solid() } - /// Try to find a new attachment point, cascading to a random teleport. fn find_new_attachment(&self) { let pos = self.mob_entity.living_entity.entity.block_pos.load(); if let Some(dir) = self.find_attachable_face(&pos) { @@ -251,15 +237,11 @@ impl ShulkerEntity { } } - /// Attempt to teleport to a random nearby location where the shulker can attach. - /// Returns `true` on success. pub fn teleport_somewhere(&self) -> bool { let entity = &self.mob_entity.living_entity.entity; let base_pos = entity.block_pos.load(); let world = entity.world.load(); - // Collect all random offsets up-front so ThreadRng (which is !Send) is - // dropped before any .await boundary. let candidates: Vec<(i32, i32, i32)> = { let mut rng = rand::rng(); (0..20) @@ -276,7 +258,6 @@ impl ShulkerEntity { for (dx, dy, dz) in candidates { let candidate = BlockPos::new(base_pos.0.x + dx, base_pos.0.y + dy, base_pos.0.z + dz); - // Target block must be air and there must be an attachable adjacent solid face. let candidate_state = world.get_block_state(&candidate); if !candidate_state.is_air() { continue; @@ -315,7 +296,6 @@ impl ShulkerEntity { 1.0, ); - // Close the shulker and drop the current target after teleport. self.set_raw_peek(0); self.mob_entity.set_target(None); @@ -330,12 +310,9 @@ impl ShulkerEntity { let health = living.health.load(); let max = living.get_max_health(); - // Teleport at half-health (random 1-in-4 chance) if health < max * 0.5 && rand::rng().random_range(0..4) == 0 { self.teleport_somewhere(); } - - // pre_damage for arrow blocking below. } } @@ -375,10 +352,8 @@ impl Mob for ShulkerEntity { entity.velocity.store(Vector3::new(0.0, 0.0, 0.0)); - // Advance peek interpolation self.update_peek_amount(); - // Ensure the current attachment face still has a solid block behind it. let pos = entity.block_pos.load(); let face = self.get_attach_face(); if !self.can_stay_at(&pos, face) { @@ -390,7 +365,6 @@ impl Mob for ShulkerEntity { self.on_shulker_damage(); } - /// When closed, block arrows entirely. fn pre_damage(&self, damage_type: DamageType, _source: Option<&dyn EntityBase>) -> bool { if self.is_closed() && damage_type == DamageType::ARROW { return false; @@ -398,7 +372,6 @@ impl Mob for ShulkerEntity { true } - /// Apply armor modifier (20 armor) reduction when closed. fn modify_incoming_damage(&self, amount: f32, damage_type: DamageType) -> f32 { if self.is_closed() && !crate::entity::living::bypasses_armor_durability(&damage_type) { const ARMOR: f32 = 20.0; @@ -473,7 +446,6 @@ impl Goal for ShulkerAttackGoal { let target_pos = target.get_entity().pos.load(); let dist_sq = shulker_pos.squared_distance_to_vec(&target_pos); - // De-target if too far (>20 blocks) if dist_sq > 400.0 { mob_entity.set_target(None); return; @@ -481,11 +453,9 @@ impl Goal for ShulkerAttackGoal { let cooldown = self.attack_cooldown.fetch_sub(1, Ordering::Relaxed) - 1; if cooldown <= 0 { - // Reset cooldown let new_cd = 20 + mob.get_random().random_range(0..5) * 10; self.attack_cooldown.store(new_cd, Ordering::Relaxed); - // Spawn bullet let world = entity.world.load(); let target_pos = target.get_entity().pos.load(); let bullet = ShulkerBulletEntity::new( @@ -496,7 +466,6 @@ impl Goal for ShulkerAttackGoal { ); world.spawn_entity_non_save(Arc::new(bullet)); - // Shoot sound (random pitch) let pitch = 1.0 + (mob.get_random().random::() - mob.get_random().random::()) * 0.2; world.play_sound_fine( diff --git a/crates/pumpkin/src/entity/mob/silverfish.rs b/crates/pumpkin/src/entity/mob/silverfish.rs index 38dc84b86..22f287323 100644 --- a/crates/pumpkin/src/entity/mob/silverfish.rs +++ b/crates/pumpkin/src/entity/mob/silverfish.rs @@ -1,25 +1,31 @@ +use std::sync::atomic::{AtomicI32, Ordering}; use std::sync::{Arc, Weak}; +use pumpkin_data::damage::DamageType; use pumpkin_data::entity::EntityType; use crate::entity::{ - Entity, + Entity, EntityBase, ai::goal::{ active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal, - look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, swim::SwimGoal, - wander_around::WanderAroundGoal, + look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, revenge::RevengeGoal, + swim::SwimGoal, wander_around::WanderAroundGoal, }, mob::{Mob, MobEntity}, }; pub struct SilverfishEntity { - entity: Arc, + pub entity: Arc, + wake_up_friends_ticks: AtomicI32, } impl SilverfishEntity { pub fn new(entity: Entity) -> Arc { let entity = Arc::new(MobEntity::new(entity)); - let silverfish = Self { entity }; + let silverfish = Self { + entity, + wake_up_friends_ticks: AtomicI32::new(0), + }; let mob_arc = Arc::new(silverfish); let mob_weak: Weak = { let mob_arc: Arc = mob_arc.clone(); @@ -47,6 +53,7 @@ impl SilverfishEntity { ); goal_selector.add_goal(8, Box::new(RandomLookAroundGoal::default())); + target_selector.add_goal(1, Box::new(RevengeGoal::new(true))); target_selector.add_goal( 2, ActiveTargetGoal::with_default(&mob_arc.entity, &EntityType::PLAYER, true), @@ -61,4 +68,27 @@ impl Mob for SilverfishEntity { fn get_mob_entity(&self) -> &MobEntity { &self.entity } + + fn mob_tick(&self, _caller: &dyn EntityBase) { + let entity = &self.entity.living_entity.entity; + if !entity.is_alive() { + return; + } + + let yaw = entity.yaw.load(); + entity.body_yaw.store(yaw); + entity.head_yaw.store(yaw); + + let ticks = self.wake_up_friends_ticks.load(Ordering::Relaxed); + if ticks > 0 { + self.wake_up_friends_ticks + .store(ticks - 1, Ordering::Relaxed); + } + } + + fn on_damage(&self, _damage_type: DamageType, _source: Option<&dyn EntityBase>) { + if self.wake_up_friends_ticks.load(Ordering::Relaxed) == 0 { + self.wake_up_friends_ticks.store(20, Ordering::Relaxed); + } + } } diff --git a/crates/pumpkin/src/entity/mob/slime.rs b/crates/pumpkin/src/entity/mob/slime.rs index 54d166aad..71b05eb9e 100644 --- a/crates/pumpkin/src/entity/mob/slime.rs +++ b/crates/pumpkin/src/entity/mob/slime.rs @@ -617,18 +617,3 @@ impl Goal for SlimeKeepOnJumpingGoal { crate::entity::ai::goal::Controls::JUMP | crate::entity::ai::goal::Controls::MOVE } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn uses_small_hurt_sound_only_for_smallest_slimes() { - assert_eq!( - SlimeEntity::hurt_sound_for_size(1), - Sound::EntitySlimeHurtSmall - ); - assert_eq!(SlimeEntity::hurt_sound_for_size(0), Sound::EntitySlimeHurt); - assert_eq!(SlimeEntity::hurt_sound_for_size(2), Sound::EntitySlimeHurt); - } -} diff --git a/crates/pumpkin/src/entity/mob/spider.rs b/crates/pumpkin/src/entity/mob/spider.rs index a5bbfb6dd..06a2f5adf 100644 --- a/crates/pumpkin/src/entity/mob/spider.rs +++ b/crates/pumpkin/src/entity/mob/spider.rs @@ -1,9 +1,11 @@ +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Weak}; use pumpkin_data::entity::EntityType; +use pumpkin_protocol::java::client::play::Metadata; use crate::entity::{ - Entity, + Entity, EntityBase, ai::goal::{ active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, revenge::RevengeGoal, @@ -14,12 +16,16 @@ use crate::entity::{ pub struct SpiderEntity { pub mob_entity: MobEntity, + pub is_climbing: AtomicBool, } impl SpiderEntity { pub fn new(entity: Entity) -> Arc { let mob_entity = MobEntity::new(entity); - let spider = Self { mob_entity }; + let spider = Self { + mob_entity, + is_climbing: AtomicBool::new(false), + }; let mob_arc = Arc::new(spider); let mob_weak: Weak = { let mob_arc: Arc = mob_arc.clone(); @@ -39,7 +45,6 @@ impl SpiderEntity { .unwrap_or_else(std::sync::PoisonError::into_inner); goal_selector.add_goal(1, Box::new(SwimGoal::default())); - // TODO: SpiderAttackGoal for jumping goal_selector.add_goal(3, Box::new(MeleeAttackGoal::new(1.0, false))); goal_selector.add_goal(5, Box::new(WanderAroundGoal::new(0.8))); goal_selector.add_goal( @@ -61,10 +66,38 @@ impl SpiderEntity { mob_arc } + + pub fn is_climbing(&self) -> bool { + self.is_climbing.load(Ordering::Relaxed) + } + + pub fn set_climbing(&self, climbing: bool) { + if self.is_climbing.swap(climbing, Ordering::Relaxed) != climbing { + let flags = i8::from(climbing); + self.mob_entity.living_entity.entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::spider::DATA_FLAGS_ID, + flags, + )], + None, + ); + } + } } impl Mob for SpiderEntity { fn get_mob_entity(&self) -> &MobEntity { &self.mob_entity } + + fn mob_tick(&self, _caller: &dyn EntityBase) { + let entity = &self.mob_entity.living_entity.entity; + if !entity.is_alive() { + return; + } + + let vel = entity.velocity.load(); + let is_colliding_horizontally = vel.x.abs() < 1e-4 && vel.z.abs() < 1e-4; + self.set_climbing(is_colliding_horizontally); + } } diff --git a/crates/pumpkin/src/entity/mob/zoglin.rs b/crates/pumpkin/src/entity/mob/zoglin.rs index 17d9537b1..7802598d3 100644 --- a/crates/pumpkin/src/entity/mob/zoglin.rs +++ b/crates/pumpkin/src/entity/mob/zoglin.rs @@ -1,25 +1,66 @@ +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Weak}; +use pumpkin_data::attributes::Attributes; use pumpkin_data::entity::EntityType; +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_util::math::vector3::Vector3; use crate::entity::{ - Entity, + Entity, EntityBase, ai::goal::{ active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal, - look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, swim::SwimGoal, - wander_around::WanderAroundGoal, + look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, revenge::RevengeGoal, + swim::SwimGoal, wander_around::WanderAroundGoal, }, + living::LivingEntity, mob::{Mob, MobEntity}, }; +use crate::world::World; pub struct ZoglinEntity { pub mob_entity: MobEntity, + pub is_baby: AtomicBool, } impl ZoglinEntity { + pub const XP_REWARD: u32 = 5; + pub fn new(entity: Entity) -> Arc { let mob_entity = MobEntity::new(entity); - let zoglin = Self { mob_entity }; + { + let mut attributes = 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 = 40.0; + health.dirty.store(true, Ordering::Relaxed); + } + if let Some(speed) = attributes.get_mut(&Attributes::MOVEMENT_SPEED.id) { + speed.base_value = 0.3; + speed.dirty.store(true, Ordering::Relaxed); + } + if let Some(knockback_res) = attributes.get_mut(&Attributes::KNOCKBACK_RESISTANCE.id) { + knockback_res.base_value = 0.6; + knockback_res.dirty.store(true, Ordering::Relaxed); + } + if let Some(attack_kb) = attributes.get_mut(&Attributes::ATTACK_KNOCKBACK.id) { + attack_kb.base_value = 1.0; + attack_kb.dirty.store(true, Ordering::Relaxed); + } + if let Some(damage) = attributes.get_mut(&Attributes::ATTACK_DAMAGE.id) { + damage.base_value = 6.0; + damage.dirty.store(true, Ordering::Relaxed); + } + } + mob_entity.living_entity.health.store(40.0); + + let zoglin = Self { + mob_entity, + is_baby: AtomicBool::new(false), + }; let mob_arc = Arc::new(zoglin); let mob_weak: Weak = { let mob_arc: Arc = mob_arc.clone(); @@ -47,9 +88,20 @@ impl ZoglinEntity { .target_selector .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); + target_selector.add_goal(1, Box::new(RevengeGoal::new(true))); target_selector.add_goal( - 1, - ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::PLAYER, true), + 2, + Box::new(ActiveTargetGoal::new( + &mob_arc.mob_entity, + &EntityType::PLAYER, + 10, + true, + false, + Some(|target: &LivingEntity, _world: &World| { + target.entity.entity_type != &EntityType::ZOGLIN + && target.entity.entity_type != &EntityType::CREEPER + }), + )), ); }; @@ -61,4 +113,30 @@ impl Mob for ZoglinEntity { fn get_mob_entity(&self) -> &MobEntity { &self.mob_entity } + + fn mob_write_nbt(&self, nbt: &mut NbtCompound) { + if self.is_baby.load(Ordering::Relaxed) { + nbt.put_bool("IsBaby", true); + } + } + + fn mob_read_nbt(&self, nbt: &NbtCompound) { + if let Some(baby) = nbt.get_bool("IsBaby") { + self.is_baby.store(baby, Ordering::Relaxed); + } + } + + fn on_attack(&self, target: &dyn EntityBase) { + let my_pos = self.mob_entity.living_entity.entity.pos.load(); + let target_pos = target.get_entity().pos.load(); + let dx = target_pos.x - my_pos.x; + let dz = target_pos.z - my_pos.z; + let dist = dx.hypot(dz).max(0.001); + let vel = target.get_entity().velocity.load(); + target.get_entity().velocity.store(Vector3::new( + vel.x + (dx / dist) * 0.5, + 0.5, + vel.z + (dz / dist) * 0.5, + )); + } } diff --git a/crates/pumpkin/src/entity/mob/zombified_piglin.rs b/crates/pumpkin/src/entity/mob/zombified_piglin.rs index a06041224..855589ce1 100644 --- a/crates/pumpkin/src/entity/mob/zombified_piglin.rs +++ b/crates/pumpkin/src/entity/mob/zombified_piglin.rs @@ -1,24 +1,39 @@ +use std::sync::atomic::{AtomicI32, Ordering}; use std::sync::{Arc, Weak}; +use pumpkin_data::data_component_impl::EquipmentSlot; use pumpkin_data::entity::EntityType; +use pumpkin_data::item::Item; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_nbt::compound::NbtCompound; use crate::entity::{ - Entity, + Entity, EntityBase, ai::goal::{ look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, - melee_attack::MeleeAttackGoal, swim::SwimGoal, wander_around::WanderAroundGoal, + melee_attack::MeleeAttackGoal, revenge::RevengeGoal, swim::SwimGoal, + wander_around::WanderAroundGoal, }, - mob::{Mob, MobEntity}, + mob::{Mob, MobEntity, equipment::RegionalDifficulty}, }; +use crate::world::World; pub struct ZombifiedPiglinEntity { pub mob_entity: MobEntity, + anger_time: AtomicI32, + ticks_until_next_alert: AtomicI32, } impl ZombifiedPiglinEntity { + pub const XP_REWARD: u32 = 5; + pub fn new(entity: Entity) -> Arc { let mob_entity = MobEntity::new(entity); - let piglin = Self { mob_entity }; + let piglin = Self { + mob_entity, + anger_time: AtomicI32::new(0), + ticks_until_next_alert: AtomicI32::new(0), + }; let mob_arc = Arc::new(piglin); let mob_weak: Weak = { let mob_arc: Arc = mob_arc.clone(); @@ -41,23 +56,113 @@ impl ZombifiedPiglinEntity { ); goal_selector.add_goal(7, Box::new(RandomLookAroundGoal::default())); - let _target_selector = mob_arc + let mut target_selector = mob_arc .mob_entity .target_selector .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); - // Zombified piglins are neutral by default (only attack if hit), - // but for now we give it ActiveTargetGoal for players to make them hostile - // (or we can leave it empty for neutral behavior). - // Let's make them neutral for now (no ActiveTargetGoal). + target_selector.add_goal(1, Box::new(RevengeGoal::new(true))); }; mob_arc } + + pub fn is_angry(&self) -> bool { + self.anger_time.load(Ordering::Relaxed) > 0 + } + + pub fn set_anger_time(&self, ticks: i32) { + self.anger_time.store(ticks, Ordering::Relaxed); + } } impl Mob for ZombifiedPiglinEntity { fn get_mob_entity(&self) -> &MobEntity { &self.mob_entity } + + fn populate_default_equipment_slots( + &self, + _world: &Arc, + _difficulty: &RegionalDifficulty, + ) { + let living = &self.mob_entity.living_entity; + let mut equipment = living + .entity_equipment + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + + let weapon = if rand::random_range(0..20) == 0 { + &Item::GOLDEN_SPEAR + } else { + &Item::GOLDEN_SWORD + }; + equipment.put(&EquipmentSlot::MAIN_HAND, ItemStack::new(1, weapon)); + } + + fn mob_write_nbt(&self, nbt: &mut NbtCompound) { + let anger = self.anger_time.load(Ordering::Relaxed); + if anger > 0 { + nbt.put_int("AngerTime", anger); + } + } + + fn mob_read_nbt(&self, nbt: &NbtCompound) { + if let Some(anger) = nbt.get_int("AngerTime") { + self.anger_time.store(anger, Ordering::Relaxed); + } + } + + fn mob_tick(&self, _caller: &dyn EntityBase) { + let entity = &self.mob_entity.living_entity.entity; + if !entity.is_alive() { + return; + } + + let anger = self.anger_time.load(Ordering::Relaxed); + if anger > 0 { + self.anger_time.store(anger - 1, Ordering::Relaxed); + } + + if let Some(target) = self.mob_entity.get_target() { + let next_alert = self.ticks_until_next_alert.load(Ordering::Relaxed); + if next_alert > 0 { + self.ticks_until_next_alert + .store(next_alert - 1, Ordering::Relaxed); + } else { + self.ticks_until_next_alert + .store(rand::random_range(80..120), Ordering::Relaxed); + let world = entity.world.load(); + let my_pos = entity.pos.load(); + let nearby_zombies = world.get_nearby_entities(my_pos, 35.0); + for (_uuid, other) in nearby_zombies { + if other.get_entity().entity_id != entity.entity_id + && other.get_entity().entity_type == &EntityType::ZOMBIFIED_PIGLIN + && let Some(mob) = other.get_mob() + { + mob.get_mob_entity().set_target(Some(target.clone())); + } + } + } + } + } + + fn on_damage( + &self, + _damage_type: pumpkin_data::damage::DamageType, + source: Option<&dyn EntityBase>, + ) { + let anger_ticks = rand::random_range(400..780); + self.set_anger_time(anger_ticks); + + if let Some(attacker) = source + && let Some(player) = attacker.get_player() + { + let world = self.mob_entity.living_entity.entity.world.load(); + if let Some(player_arc) = world.get_player_by_id(player.living_entity.entity.entity_id) + { + self.mob_entity.set_target(Some(player_arc)); + } + } + } } diff --git a/crates/pumpkin/src/entity/passive/allay.rs b/crates/pumpkin/src/entity/passive/allay.rs index e42f17844..0f8400595 100644 --- a/crates/pumpkin/src/entity/passive/allay.rs +++ b/crates/pumpkin/src/entity/passive/allay.rs @@ -1,27 +1,47 @@ -use std::sync::{Arc, Weak}; - -use pumpkin_data::entity::EntityType; - -use crate::entity::{ - Entity, - ai::goal::{ - look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal, - wander_around::WanderAroundGoal, - }, - mob::{Mob, MobEntity}, +use std::sync::{ + Arc, Weak, + atomic::{AtomicBool, AtomicI32, Ordering}, +}; + +use crossbeam::atomic::AtomicCell; +use pumpkin_data::entity::EntityType; +use pumpkin_data::item::Item; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::particle::Particle; +use pumpkin_data::sound::{Sound, SoundCategory}; +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_protocol::java::client::play::Metadata; +use pumpkin_util::math::vector3::Vector3; +use uuid::Uuid; + +use crate::entity::{ + Entity, EntityBase, + ai::goal::{ + escape_danger::EscapeDangerGoal, look_around::RandomLookAroundGoal, + look_at_entity::LookAtEntityGoal, swim::SwimGoal, wander_around::WanderAroundGoal, + }, + mob::{Mob, MobEntity}, + player::Player, }; -/// Represents an Allay, a passive, flying entity that can collect items for the player. -/// -/// Wiki: pub struct AllayEntity { pub mob_entity: MobEntity, + pub dancing: AtomicBool, + pub can_duplicate: AtomicBool, + pub duplication_cooldown: AtomicI32, + pub owner: AtomicCell>, } impl AllayEntity { pub fn new(entity: Entity) -> Arc { let mob_entity = MobEntity::new(entity); - let allay = Self { mob_entity }; + let allay = Self { + mob_entity, + dancing: AtomicBool::new(false), + can_duplicate: AtomicBool::new(true), + duplication_cooldown: AtomicI32::new(0), + owner: AtomicCell::new(None), + }; let mob_arc = Arc::new(allay); let mob_weak: Weak = { let mob_arc: Arc = mob_arc.clone(); @@ -36,20 +56,143 @@ impl AllayEntity { .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(1.25)); + goal_selector.add_goal(6, Box::new(WanderAroundGoal::new(1.0))); goal_selector.add_goal( - 2, + 7, LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 6.0), ); - goal_selector.add_goal(3, Box::new(RandomLookAroundGoal::default())); + goal_selector.add_goal(8, Box::new(RandomLookAroundGoal::default())); }; mob_arc } + + #[must_use] + pub fn is_dancing(&self) -> bool { + self.dancing.load(Ordering::Relaxed) + } + + pub fn set_dancing(&self, dancing: bool) { + self.dancing.store(dancing, Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::allay::DATA_DANCING, + dancing, + )], + None, + ); + } + + #[must_use] + pub fn can_duplicate(&self) -> bool { + self.can_duplicate.load(Ordering::Relaxed) + } + + pub fn set_can_duplicate(&self, can_duplicate: bool) { + self.can_duplicate.store(can_duplicate, Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::allay::DATA_CAN_DUPLICATE, + can_duplicate, + )], + None, + ); + } } impl Mob for AllayEntity { + fn mob_write_nbt(&self, nbt: &mut NbtCompound) { + nbt.put_bool("CanDuplicate", self.can_duplicate()); + nbt.put_int( + "DuplicationCooldown", + self.duplication_cooldown.load(Ordering::Relaxed), + ); + if let Some(owner) = self.owner.load() { + nbt.put_uuid("Owner", owner); + } + } + + fn mob_read_nbt(&self, nbt: &NbtCompound) { + if let Some(can) = nbt.get_bool("CanDuplicate") { + self.set_can_duplicate(can); + } + if let Some(cd) = nbt.get_int("DuplicationCooldown") { + self.duplication_cooldown.store(cd, Ordering::Relaxed); + } + if let Some(owner) = nbt.get_uuid("Owner") { + self.owner.store(Some(owner)); + } + } + fn get_mob_entity(&self) -> &MobEntity { &self.mob_entity } + + fn mob_tick(&self, _caller: &dyn EntityBase) { + let cd = self.duplication_cooldown.load(Ordering::Relaxed); + if cd > 0 { + let next = cd - 1; + self.duplication_cooldown.store(next, Ordering::Relaxed); + if next == 0 { + self.set_can_duplicate(true); + } + } + } + + fn mob_init_data_tracker(&self) { + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::allay::DATA_DANCING, + self.is_dancing(), + )], + None, + ); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::allay::DATA_CAN_DUPLICATE, + self.can_duplicate(), + )], + None, + ); + } + + fn mob_interact(&self, player: &Arc, item_stack: &mut ItemStack) -> bool { + let item = item_stack.get_item(); + + if self.is_dancing() && self.can_duplicate() && item == &Item::AMETHYST_SHARD { + item_stack.decrement_unless_creative(player.gamemode.load(), 1); + self.set_can_duplicate(false); + self.duplication_cooldown.store(6000, Ordering::Relaxed); + + let entity = self.get_entity(); + let world = entity.world.load(); + let pos = entity.pos.load(); + world.spawn_particle( + pos + Vector3::new(0.0, f64::from(entity.height()), 0.0), + Vector3::new(0.5, 0.5, 0.5), + 1.0, + 7, + Particle::Heart, + ); + world.play_sound( + Sound::EntityAllayAmbientWithoutItem, + SoundCategory::Neutral, + &pos, + ); + + let new_allay = Self::new(Entity::new(world.clone(), pos, &EntityType::ALLAY)); + world.spawn_entity(new_allay); + return true; + } + + if self.owner.load().is_none() { + self.owner.store(Some(player.gameprofile.id)); + } + + false + } } diff --git a/crates/pumpkin/src/entity/passive/animal.rs b/crates/pumpkin/src/entity/passive/animal.rs index dbd4227b1..3d723a46e 100644 --- a/crates/pumpkin/src/entity/passive/animal.rs +++ b/crates/pumpkin/src/entity/passive/animal.rs @@ -103,3 +103,71 @@ pub trait Animal: Mob { mob_entity.mob_interact(player, item_stack) } } + +#[must_use] +pub fn get_dye_color_from_item(item: &pumpkin_data::item::Item) -> Option { + match item.registry_key { + "white_dye" => Some(0), + "orange_dye" => Some(1), + "magenta_dye" => Some(2), + "light_blue_dye" => Some(3), + "yellow_dye" => Some(4), + "lime_dye" => Some(5), + "pink_dye" => Some(6), + "gray_dye" => Some(7), + "light_gray_dye" => Some(8), + "cyan_dye" => Some(9), + "purple_dye" => Some(10), + "blue_dye" => Some(11), + "brown_dye" => Some(12), + "green_dye" => Some(13), + "red_dye" => Some(14), + "black_dye" => Some(15), + _ => None, + } +} + +#[must_use] +pub const fn get_wool_item_for_color(color: u8) -> &'static pumpkin_data::item::Item { + match color { + 0 => &pumpkin_data::item::Item::WHITE_WOOL, + 1 => &pumpkin_data::item::Item::ORANGE_WOOL, + 2 => &pumpkin_data::item::Item::MAGENTA_WOOL, + 3 => &pumpkin_data::item::Item::LIGHT_BLUE_WOOL, + 4 => &pumpkin_data::item::Item::YELLOW_WOOL, + 5 => &pumpkin_data::item::Item::LIME_WOOL, + 6 => &pumpkin_data::item::Item::PINK_WOOL, + 7 => &pumpkin_data::item::Item::GRAY_WOOL, + 8 => &pumpkin_data::item::Item::LIGHT_GRAY_WOOL, + 9 => &pumpkin_data::item::Item::CYAN_WOOL, + 10 => &pumpkin_data::item::Item::PURPLE_WOOL, + 11 => &pumpkin_data::item::Item::BLUE_WOOL, + 12 => &pumpkin_data::item::Item::BROWN_WOOL, + 13 => &pumpkin_data::item::Item::GREEN_WOOL, + 14 => &pumpkin_data::item::Item::RED_WOOL, + _ => &pumpkin_data::item::Item::BLACK_WOOL, + } +} + +#[must_use] +pub fn get_carpet_color_from_item(item: &pumpkin_data::item::Item) -> Option { + match item.registry_key { + "white_carpet" => Some(0), + "orange_carpet" => Some(1), + "magenta_carpet" => Some(2), + "light_blue_carpet" => Some(3), + "yellow_carpet" => Some(4), + "lime_carpet" => Some(5), + "pink_carpet" => Some(6), + "gray_carpet" => Some(7), + "light_gray_carpet" => Some(8), + "cyan_carpet" => Some(9), + "purple_carpet" => Some(10), + "blue_carpet" => Some(11), + "brown_carpet" => Some(12), + "green_carpet" => Some(13), + "red_carpet" => Some(14), + "black_carpet" => Some(15), + _ => None, + } +} diff --git a/crates/pumpkin/src/entity/passive/axolotl.rs b/crates/pumpkin/src/entity/passive/axolotl.rs index 945b71105..fb53ee1be 100644 --- a/crates/pumpkin/src/entity/passive/axolotl.rs +++ b/crates/pumpkin/src/entity/passive/axolotl.rs @@ -1,27 +1,100 @@ -use std::sync::{Arc, Weak}; - -use pumpkin_data::entity::EntityType; - -use crate::entity::{ - Entity, - ai::goal::{ - look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal, - try_find_water::TryFindWaterGoal, wander_around::WanderAroundGoal, - }, - mob::{Mob, MobEntity}, +use std::sync::{ + Arc, Weak, + atomic::{AtomicBool, AtomicI32, Ordering}, }; -/// Represents an Axolotl, a passive aquatic mob that can play dead to regenerate health. -/// -/// Wiki: +use pumpkin_data::entity::EntityType; +use pumpkin_data::item::Item; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::sound::{Sound, SoundCategory}; +use pumpkin_data::tag::{self, Taggable}; +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_protocol::codec::var_int::VarInt; +use pumpkin_protocol::java::client::play::Metadata; +use rand::RngExt; + +use crate::entity::{ + Entity, EntityBase, + ageable::{AgeableData, AgeableMob}, + ai::goal::{ + active_target::ActiveTargetGoal, breed::BreedGoal, escape_danger::EscapeDangerGoal, + follow_parent::FollowParentGoal, look_around::RandomLookAroundGoal, + look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, revenge::RevengeGoal, + swim::SwimGoal, tempt::TemptGoal, try_find_water::TryFindWaterGoal, + wander_around::WanderAroundGoal, + }, + mob::{Mob, MobEntity}, + passive::animal::Animal, + player::Player, +}; + +const TEMPT_ITEMS: &[&Item] = &[&Item::TROPICAL_FISH_BUCKET]; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[repr(i32)] +pub enum AxolotlVariant { + #[default] + Lucy = 0, + Wild = 1, + Gold = 2, + Cyan = 3, + Blue = 4, +} + +impl AxolotlVariant { + #[must_use] + pub const fn from_id(id: i32) -> Self { + match id { + 1 => Self::Wild, + 2 => Self::Gold, + 3 => Self::Cyan, + 4 => Self::Blue, + _ => Self::Lucy, + } + } + + #[must_use] + pub const fn id(self) -> i32 { + self as i32 + } + + #[must_use] + pub fn random_variant() -> Self { + let mut rng = rand::rng(); + if rng.random_range(0..1200) == 0 { + Self::Blue + } else { + match rng.random_range(0..4) { + 1 => Self::Wild, + 2 => Self::Gold, + 3 => Self::Cyan, + _ => Self::Lucy, + } + } + } +} + pub struct AxolotlEntity { pub mob_entity: MobEntity, + pub ageable_data: AgeableData, + pub variant: AtomicI32, + pub playing_dead: AtomicBool, + pub from_bucket: AtomicBool, + pub play_dead_ticks: AtomicI32, } impl AxolotlEntity { pub fn new(entity: Entity) -> Arc { let mob_entity = MobEntity::new(entity); - let axolotl = Self { mob_entity }; + let variant = AxolotlVariant::random_variant(); + let axolotl = Self { + mob_entity, + ageable_data: AgeableData::default(), + variant: AtomicI32::new(variant.id()), + playing_dead: AtomicBool::new(false), + from_bucket: AtomicBool::new(false), + play_dead_ticks: AtomicI32::new(0), + }; let mob_arc = Arc::new(axolotl); let mob_weak: Weak = { let mob_arc: Arc = mob_arc.clone(); @@ -37,20 +110,226 @@ impl AxolotlEntity { goal_selector.add_goal(0, Box::new(TryFindWaterGoal)); 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(1.5)); + goal_selector.add_goal(2, BreedGoal::new(1.0)); + goal_selector.add_goal(3, Box::new(TemptGoal::new(1.25, TEMPT_ITEMS))); + goal_selector.add_goal(4, Box::new(FollowParentGoal::new(1.25))); + goal_selector.add_goal(5, Box::new(MeleeAttackGoal::new(1.2, false))); + goal_selector.add_goal(6, Box::new(WanderAroundGoal::new(1.0))); goal_selector.add_goal( - 2, + 7, LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 6.0), ); - goal_selector.add_goal(3, Box::new(RandomLookAroundGoal::default())); + goal_selector.add_goal(8, Box::new(RandomLookAroundGoal::default())); + }; + + { + let mut target_selector = mob_arc + .mob_entity + .target_selector + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + + target_selector.add_goal(1, Box::new(RevengeGoal::new(true))); + target_selector.add_goal( + 2, + ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::DROWNED, false), + ); + target_selector.add_goal( + 2, + ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::GUARDIAN, false), + ); + target_selector.add_goal( + 2, + ActiveTargetGoal::with_default( + &mob_arc.mob_entity, + &EntityType::ELDER_GUARDIAN, + false, + ), + ); + target_selector.add_goal( + 3, + ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::SQUID, false), + ); + target_selector.add_goal( + 3, + ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::GLOW_SQUID, false), + ); + target_selector.add_goal( + 3, + ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::COD, false), + ); + target_selector.add_goal( + 3, + ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::SALMON, false), + ); + target_selector.add_goal( + 3, + ActiveTargetGoal::with_default( + &mob_arc.mob_entity, + &EntityType::TROPICAL_FISH, + false, + ), + ); }; mob_arc } + + #[must_use] + pub fn get_variant(&self) -> AxolotlVariant { + AxolotlVariant::from_id(self.variant.load(Ordering::Relaxed)) + } + + pub fn set_variant(&self, variant: AxolotlVariant) { + self.variant.store(variant.id(), Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::axolotl::DATA_VARIANT, + VarInt(variant.id()), + )], + None, + ); + } + + #[must_use] + pub fn is_playing_dead(&self) -> bool { + self.playing_dead.load(Ordering::Relaxed) + } + + pub fn set_playing_dead(&self, playing_dead: bool) { + self.playing_dead.store(playing_dead, Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::axolotl::DATA_PLAYING_DEAD, + playing_dead, + )], + None, + ); + } + + #[must_use] + pub fn is_from_bucket(&self) -> bool { + self.from_bucket.load(Ordering::Relaxed) + } + + pub fn set_from_bucket(&self, from_bucket: bool) { + self.from_bucket.store(from_bucket, Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::axolotl::FROM_BUCKET, + from_bucket, + )], + None, + ); + } +} + +impl AgeableMob for AxolotlEntity { + fn get_ageable_data(&self) -> &AgeableData { + &self.ageable_data + } +} + +impl Animal for AxolotlEntity { + fn is_food(&self, item_stack: &ItemStack) -> bool { + item_stack.item.has_tag(&tag::Item::MINECRAFT_AXOLOTL_FOOD) + || item_stack.item == &Item::TROPICAL_FISH_BUCKET + || item_stack.item == &Item::TROPICAL_FISH + } } impl Mob for AxolotlEntity { + fn as_ageable(&self) -> Option<&dyn AgeableMob> { + Some(self) + } + + fn as_animal(&self) -> Option<&dyn Animal> { + Some(self) + } + + fn mob_write_nbt(&self, nbt: &mut NbtCompound) { + nbt.put_int("Variant", self.get_variant().id()); + nbt.put_bool("FromBucket", self.is_from_bucket()); + } + + fn mob_read_nbt(&self, nbt: &NbtCompound) { + if let Some(variant) = nbt.get_int("Variant") { + self.set_variant(AxolotlVariant::from_id(variant)); + } + if let Some(from_bucket) = nbt.get_bool("FromBucket") { + self.set_from_bucket(from_bucket); + } + } + fn get_mob_entity(&self) -> &MobEntity { &self.mob_entity } + + fn mob_tick(&self, _caller: &dyn EntityBase) { + self.ageable_ai_step(); + + let play_dead_ticks = self.play_dead_ticks.load(Ordering::Relaxed); + if play_dead_ticks > 0 { + let remaining = play_dead_ticks - 1; + self.play_dead_ticks.store(remaining, Ordering::Relaxed); + if remaining == 0 { + self.set_playing_dead(false); + } + } + } + + fn mob_init_data_tracker(&self) { + let entity = self.get_entity(); + let is_baby = entity.age.load(Ordering::Relaxed) < 0; + if is_baby { + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::axolotl::DATA_BABY_ID, + true, + )], + None, + ); + } + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::axolotl::DATA_VARIANT, + VarInt(self.get_variant().id()), + )], + None, + ); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::axolotl::DATA_PLAYING_DEAD, + self.is_playing_dead(), + )], + None, + ); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::axolotl::FROM_BUCKET, + self.is_from_bucket(), + )], + None, + ); + } + + fn mob_interact(&self, player: &Arc, item_stack: &mut ItemStack) -> bool { + let item = item_stack.get_item(); + + if item == &Item::WATER_BUCKET { + item_stack.decrement_unless_creative(player.gamemode.load(), 1); + let entity = self.get_entity(); + let world = entity.world.load(); + let pos = entity.pos.load(); + world.play_sound(Sound::ItemBucketFillAxolotl, SoundCategory::Neutral, &pos); + entity.remove(); + return true; + } + + self.animal_interact(player, item_stack, Sound::EntityAxolotlIdleAir) + } } diff --git a/crates/pumpkin/src/entity/passive/bee.rs b/crates/pumpkin/src/entity/passive/bee.rs index 86ec85cd7..5db640683 100644 --- a/crates/pumpkin/src/entity/passive/bee.rs +++ b/crates/pumpkin/src/entity/passive/bee.rs @@ -1,27 +1,55 @@ -use std::sync::{Arc, Weak}; +use std::sync::{ + Arc, Weak, + atomic::{AtomicI32, AtomicU8, Ordering}, +}; use pumpkin_data::entity::EntityType; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::sound::Sound; +use pumpkin_data::tag::{self, Taggable}; +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_protocol::java::client::play::Metadata; use crate::entity::{ - Entity, + Entity, EntityBase, + ageable::{AgeableData, AgeableMob}, ai::goal::{ - look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal, + active_target::ActiveTargetGoal, breed::BreedGoal, follow_parent::FollowParentGoal, + look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, + melee_attack::MeleeAttackGoal, revenge::RevengeGoal, swim::SwimGoal, tempt::TemptGoal, wander_around::WanderAroundGoal, }, mob::{Mob, MobEntity}, + passive::animal::Animal, + player::Player, }; -/// Represents a Bee, a neutral flying mob that can pollinate crops and sting attackers. -/// -/// Wiki: +pub const FLAG_ROLL: u8 = 2; +pub const FLAG_HAS_STUNG: u8 = 4; +pub const FLAG_HAS_NECTAR: u8 = 8; + pub struct BeeEntity { pub mob_entity: MobEntity, + pub ageable_data: AgeableData, + pub flags: AtomicU8, + pub ticks_without_nectar: AtomicI32, + pub cannot_enter_hive_ticks: AtomicI32, + pub crops_grown_since_pollination: AtomicI32, + pub time_since_sting: AtomicI32, } impl BeeEntity { pub fn new(entity: Entity) -> Arc { let mob_entity = MobEntity::new(entity); - let bee = Self { mob_entity }; + let bee = Self { + mob_entity, + ageable_data: AgeableData::default(), + flags: AtomicU8::new(0), + ticks_without_nectar: AtomicI32::new(0), + cannot_enter_hive_ticks: AtomicI32::new(0), + crops_grown_since_pollination: AtomicI32::new(0), + time_since_sting: AtomicI32::new(0), + }; let mob_arc = Arc::new(bee); let mob_weak: Weak = { let mob_arc: Arc = mob_arc.clone(); @@ -35,21 +63,181 @@ impl BeeEntity { .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(0, Box::new(MeleeAttackGoal::new(1.4, true))); + goal_selector.add_goal(2, BreedGoal::new(1.0)); + goal_selector.add_goal(3, Box::new(TemptGoal::new(1.25, &[]))); + goal_selector.add_goal(5, Box::new(FollowParentGoal::new(1.25))); + goal_selector.add_goal(8, Box::new(WanderAroundGoal::new(1.0))); + goal_selector.add_goal(9, Box::new(SwimGoal::default())); goal_selector.add_goal( - 2, + 10, LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 6.0), ); - goal_selector.add_goal(3, Box::new(RandomLookAroundGoal::default())); + 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_selector.add_goal(1, Box::new(RevengeGoal::new(true))); + target_selector.add_goal( + 2, + ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::PLAYER, true), + ); }; mob_arc } + + #[must_use] + pub fn has_flag(&self, flag: u8) -> bool { + (self.flags.load(Ordering::Relaxed) & flag) != 0 + } + + pub fn set_flag(&self, flag: u8, val: bool) { + let current = self.flags.load(Ordering::Relaxed); + let new_flags = if val { current | flag } else { current & !flag }; + self.flags.store(new_flags, Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::bee::DATA_FLAGS_ID, + new_flags as i8, + )], + None, + ); + } + + #[must_use] + pub fn has_nectar(&self) -> bool { + self.has_flag(FLAG_HAS_NECTAR) + } + + pub fn set_has_nectar(&self, val: bool) { + self.set_flag(FLAG_HAS_NECTAR, val); + } + + #[must_use] + pub fn has_stung(&self) -> bool { + self.has_flag(FLAG_HAS_STUNG) + } + + pub fn set_has_stung(&self, val: bool) { + self.set_flag(FLAG_HAS_STUNG, val); + } + + #[must_use] + pub fn is_rolling(&self) -> bool { + self.has_flag(FLAG_ROLL) + } + + pub fn set_rolling(&self, val: bool) { + self.set_flag(FLAG_ROLL, val); + } +} + +impl AgeableMob for BeeEntity { + fn get_ageable_data(&self) -> &AgeableData { + &self.ageable_data + } +} + +impl Animal for BeeEntity { + fn is_food(&self, item_stack: &ItemStack) -> bool { + item_stack.item.has_tag(&tag::Item::MINECRAFT_FLOWERS) + || item_stack.item.has_tag(&tag::Item::MINECRAFT_BEE_FOOD) + } } impl Mob for BeeEntity { + fn as_ageable(&self) -> Option<&dyn AgeableMob> { + Some(self) + } + + fn as_animal(&self) -> Option<&dyn Animal> { + Some(self) + } + + fn mob_write_nbt(&self, nbt: &mut NbtCompound) { + self.write_ageable_nbt(nbt); + nbt.put_bool("HasNectar", self.has_nectar()); + nbt.put_bool("HasStung", self.has_stung()); + nbt.put_int( + "TicksSincePollination", + self.ticks_without_nectar.load(Ordering::Relaxed), + ); + nbt.put_int( + "CannotEnterHiveTicks", + self.cannot_enter_hive_ticks.load(Ordering::Relaxed), + ); + nbt.put_int( + "CropsGrownSincePollination", + self.crops_grown_since_pollination.load(Ordering::Relaxed), + ); + } + + fn mob_read_nbt(&self, nbt: &NbtCompound) { + self.read_ageable_nbt(nbt); + if let Some(nectar) = nbt.get_bool("HasNectar") { + self.set_has_nectar(nectar); + } + if let Some(stung) = nbt.get_bool("HasStung") { + self.set_has_stung(stung); + } + if let Some(ticks) = nbt.get_int("TicksSincePollination") { + self.ticks_without_nectar.store(ticks, Ordering::Relaxed); + } + if let Some(cannot) = nbt.get_int("CannotEnterHiveTicks") { + self.cannot_enter_hive_ticks + .store(cannot, Ordering::Relaxed); + } + if let Some(crops) = nbt.get_int("CropsGrownSincePollination") { + self.crops_grown_since_pollination + .store(crops, Ordering::Relaxed); + } + } + fn get_mob_entity(&self) -> &MobEntity { &self.mob_entity } + + fn mob_tick(&self, _caller: &dyn EntityBase) { + self.ageable_ai_step(); + + if self.has_stung() { + let time = self.time_since_sting.fetch_add(1, Ordering::Relaxed) + 1; + if time >= 1200 { + self.mob_entity.living_entity.set_health(0.0); + } + } + } + + fn mob_init_data_tracker(&self) { + let entity = self.get_entity(); + let is_baby = entity.age.load(Ordering::Relaxed) < 0; + if is_baby { + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::bee::DATA_BABY_ID, + true, + )], + None, + ); + } + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::bee::DATA_FLAGS_ID, + self.flags.load(Ordering::Relaxed) as i8, + )], + None, + ); + } + + fn mob_interact(&self, player: &Arc, item_stack: &mut ItemStack) -> bool { + self.animal_interact(player, item_stack, Sound::EntityBeePollinate) + } } diff --git a/crates/pumpkin/src/entity/passive/camel.rs b/crates/pumpkin/src/entity/passive/camel.rs index 784bf8b37..4f4653128 100644 --- a/crates/pumpkin/src/entity/passive/camel.rs +++ b/crates/pumpkin/src/entity/passive/camel.rs @@ -1,27 +1,53 @@ -use std::sync::{Arc, Weak}; - -use pumpkin_data::entity::EntityType; - -use crate::entity::{ - Entity, - ai::goal::{ - look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal, - wander_around::WanderAroundGoal, - }, - mob::{Mob, MobEntity}, +use std::sync::{ + Arc, Weak, + atomic::{AtomicBool, AtomicU8, Ordering}, }; -/// Represents a Camel, a passive mount that can carry two players and dash. -/// -/// Wiki: +use pumpkin_data::entity::EntityType; +use pumpkin_data::item::Item; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::sound::{Sound, SoundCategory}; +use pumpkin_data::tag::{self, Taggable}; +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_protocol::java::client::play::Metadata; + +use crate::entity::{ + Entity, EntityBase, + 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::CACTUS]; + +pub const FLAG_TAME: u8 = 2; +pub const FLAG_SADDLE: u8 = 4; +pub const FLAG_BRED: u8 = 8; +pub const FLAG_EATING: u8 = 16; +pub const FLAG_STANDING: u8 = 32; + pub struct CamelEntity { pub mob_entity: MobEntity, + pub ageable_data: AgeableData, + pub flags: AtomicU8, + pub dashing: AtomicBool, } impl CamelEntity { pub fn new(entity: Entity) -> Arc { let mob_entity = MobEntity::new(entity); - let camel = Self { mob_entity }; + let camel = Self { + mob_entity, + ageable_data: AgeableData::default(), + flags: AtomicU8::new(0), + dashing: AtomicBool::new(false), + }; let mob_arc = Arc::new(camel); let mob_weak: Weak = { let mob_arc: Arc = mob_arc.clone(); @@ -36,20 +62,164 @@ impl CamelEntity { .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, TEMPT_ITEMS))); + goal_selector.add_goal(4, Box::new(FollowParentGoal::new(1.0))); + goal_selector.add_goal(6, Box::new(WanderAroundGoal::new(0.7))); goal_selector.add_goal( - 2, + 7, LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 6.0), ); - goal_selector.add_goal(3, Box::new(RandomLookAroundGoal::default())); + goal_selector.add_goal(8, Box::new(RandomLookAroundGoal::default())); }; mob_arc } + + #[must_use] + pub fn has_flag(&self, flag: u8) -> bool { + (self.flags.load(Ordering::Relaxed) & flag) != 0 + } + + pub fn set_flag(&self, flag: u8, val: bool) { + let current = self.flags.load(Ordering::Relaxed); + let new_flags = if val { current | flag } else { current & !flag }; + self.flags.store(new_flags, Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::camel::DATA_ID_FLAGS, + new_flags as i8, + )], + None, + ); + } + + #[must_use] + pub fn is_saddled(&self) -> bool { + self.has_flag(FLAG_SADDLE) + } + + pub fn set_saddled(&self, val: bool) { + self.set_flag(FLAG_SADDLE, val); + } + + #[must_use] + pub fn is_dashing(&self) -> bool { + self.dashing.load(Ordering::Relaxed) + } + + pub fn set_dashing(&self, dashing: bool) { + self.dashing.store(dashing, Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::camel::DASH, + dashing, + )], + None, + ); + } +} + +impl AgeableMob for CamelEntity { + fn get_ageable_data(&self) -> &AgeableData { + &self.ageable_data + } +} + +impl Animal for CamelEntity { + fn is_food(&self, item_stack: &ItemStack) -> bool { + item_stack.item.has_tag(&tag::Item::MINECRAFT_CAMEL_FOOD) + || item_stack.item == &Item::CACTUS + } } impl Mob for CamelEntity { + fn as_ageable(&self) -> Option<&dyn AgeableMob> { + Some(self) + } + + fn as_animal(&self) -> Option<&dyn Animal> { + Some(self) + } + + fn mob_write_nbt(&self, nbt: &mut NbtCompound) { + self.write_ageable_nbt(nbt); + nbt.put_bool("Saddle", self.is_saddled()); + } + + fn mob_read_nbt(&self, nbt: &NbtCompound) { + self.read_ageable_nbt(nbt); + if let Some(saddle) = nbt.get_bool("Saddle") { + self.set_saddled(saddle); + } + } + fn get_mob_entity(&self) -> &MobEntity { &self.mob_entity } + + fn mob_tick(&self, _caller: &dyn EntityBase) { + self.ageable_ai_step(); + } + + fn mob_init_data_tracker(&self) { + let entity = self.get_entity(); + let is_baby = entity.age.load(Ordering::Relaxed) < 0; + if is_baby { + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::camel::DATA_BABY_ID, + true, + )], + None, + ); + } + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::camel::DATA_ID_FLAGS, + self.flags.load(Ordering::Relaxed) as i8, + )], + None, + ); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::camel::DASH, + self.is_dashing(), + )], + None, + ); + } + + fn mob_interact(&self, player: &Arc, item_stack: &mut ItemStack) -> bool { + let item = item_stack.get_item(); + + if item == &Item::SADDLE && !self.is_saddled() && !self.is_baby() { + self.set_saddled(true); + item_stack.decrement_unless_creative(player.gamemode.load(), 1); + let entity = self.get_entity(); + let world = entity.world.load(); + world.play_sound( + Sound::EntityCamelSaddle, + SoundCategory::Neutral, + &entity.pos.load(), + ); + return true; + } + + if self.is_saddled() && !self.is_baby() && !self.is_food(item_stack) { + let world = player.world(); + let ent = &self.mob_entity.living_entity.entity; + if let Some(vehicle) = world.get_entity_by_id(ent.entity_id) + && let Some(passenger) = world.get_player_by_id(player.entity_id()) + { + ent.add_passenger(vehicle, passenger as Arc); + return true; + } + } + + self.animal_interact(player, item_stack, Sound::EntityCamelAmbient) + } } diff --git a/crates/pumpkin/src/entity/passive/cow.rs b/crates/pumpkin/src/entity/passive/cow.rs index 97b7fa2f5..9501bed13 100644 --- a/crates/pumpkin/src/entity/passive/cow.rs +++ b/crates/pumpkin/src/entity/passive/cow.rs @@ -94,6 +94,17 @@ impl Mob for CowEntity { } fn mob_interact(&self, player: &Arc, item_stack: &mut ItemStack) -> bool { + if item_stack.get_item() == &Item::BUCKET && !self.is_baby() { + item_stack.decrement_unless_creative(player.gamemode.load(), 1); + let entity = &self.mob_entity.living_entity.entity; + let world = entity.world.load(); + world.play_sound( + Sound::EntityCowMilk, + pumpkin_data::sound::SoundCategory::Neutral, + &entity.pos.load(), + ); + return true; + } self.animal_interact(player, item_stack, Sound::EntityCowAmbient) } } diff --git a/crates/pumpkin/src/entity/passive/dolphin.rs b/crates/pumpkin/src/entity/passive/dolphin.rs index 50867bfe7..5849248bd 100644 --- a/crates/pumpkin/src/entity/passive/dolphin.rs +++ b/crates/pumpkin/src/entity/passive/dolphin.rs @@ -1,27 +1,44 @@ -use std::sync::{Arc, Weak}; - -use pumpkin_data::entity::EntityType; - -use crate::entity::{ - Entity, - ai::goal::{ - look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal, - try_find_water::TryFindWaterGoal, wander_around::WanderAroundGoal, - }, - mob::{Mob, MobEntity}, +use std::sync::{ + Arc, Weak, + atomic::{AtomicBool, AtomicI32, Ordering}, }; -/// Represents a Dolphin, a neutral aquatic mob that can give players the Dolphin's Grace effect. -/// -/// Wiki: +use pumpkin_data::entity::EntityType; +use pumpkin_data::item::Item; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::particle::Particle; +use pumpkin_data::sound::{Sound, SoundCategory}; +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_util::math::vector3::Vector3; + +use crate::entity::{ + Entity, EntityBase, + ai::goal::{ + escape_danger::EscapeDangerGoal, look_around::RandomLookAroundGoal, + look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, revenge::RevengeGoal, + swim::SwimGoal, tempt::TemptGoal, try_find_water::TryFindWaterGoal, + wander_around::WanderAroundGoal, + }, + mob::{Mob, MobEntity}, + player::Player, +}; + +const TEMPT_ITEMS: &[&Item] = &[&Item::COD, &Item::SALMON, &Item::TROPICAL_FISH]; + pub struct DolphinEntity { pub mob_entity: MobEntity, + pub got_fish: AtomicBool, + pub moistness_level: AtomicI32, } impl DolphinEntity { pub fn new(entity: Entity) -> Arc { let mob_entity = MobEntity::new(entity); - let dolphin = Self { mob_entity }; + let dolphin = Self { + mob_entity, + got_fish: AtomicBool::new(false), + moistness_level: AtomicI32::new(2400), + }; let mob_arc = Arc::new(dolphin); let mob_weak: Weak = { let mob_arc: Arc = mob_arc.clone(); @@ -37,20 +54,80 @@ impl DolphinEntity { goal_selector.add_goal(0, Box::new(TryFindWaterGoal)); 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(1.6)); + goal_selector.add_goal(2, Box::new(MeleeAttackGoal::new(1.2, true))); + goal_selector.add_goal(3, Box::new(TemptGoal::new(1.2, TEMPT_ITEMS))); + 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())); + }; + + { + let mut target_selector = mob_arc + .mob_entity + .target_selector + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + + target_selector.add_goal(1, Box::new(RevengeGoal::new(true))); }; mob_arc } + + #[must_use] + pub fn got_fish(&self) -> bool { + self.got_fish.load(Ordering::Relaxed) + } + + pub fn set_got_fish(&self, val: bool) { + self.got_fish.store(val, Ordering::Relaxed); + } } impl Mob for DolphinEntity { + fn mob_write_nbt(&self, nbt: &mut NbtCompound) { + nbt.put_bool("GotFish", self.got_fish()); + nbt.put_int("Moistness", self.moistness_level.load(Ordering::Relaxed)); + } + + fn mob_read_nbt(&self, nbt: &NbtCompound) { + if let Some(got_fish) = nbt.get_bool("GotFish") { + self.set_got_fish(got_fish); + } + if let Some(moistness) = nbt.get_int("Moistness") { + self.moistness_level.store(moistness, Ordering::Relaxed); + } + } + fn get_mob_entity(&self) -> &MobEntity { &self.mob_entity } + + fn mob_interact(&self, player: &Arc, item_stack: &mut ItemStack) -> bool { + let item = item_stack.get_item(); + if TEMPT_ITEMS.iter().any(|i| i.id == item.id) { + item_stack.decrement_unless_creative(player.gamemode.load(), 1); + self.set_got_fish(true); + self.mob_entity.living_entity.heal(2.0); + + let entity = self.get_entity(); + let world = entity.world.load(); + let pos = entity.pos.load(); + world.spawn_particle( + pos + Vector3::new(0.0, f64::from(entity.height()), 0.0), + Vector3::new(0.5, 0.5, 0.5), + 1.0, + 7, + Particle::HappyVillager, + ); + world.play_sound(Sound::EntityDolphinEat, SoundCategory::Neutral, &pos); + return true; + } + + false + } } diff --git a/crates/pumpkin/src/entity/passive/donkey.rs b/crates/pumpkin/src/entity/passive/donkey.rs index a0dddeae2..c95405162 100644 --- a/crates/pumpkin/src/entity/passive/donkey.rs +++ b/crates/pumpkin/src/entity/passive/donkey.rs @@ -1,27 +1,64 @@ -use std::sync::{Arc, Weak}; - -use pumpkin_data::entity::EntityType; - -use crate::entity::{ - Entity, - ai::goal::{ - look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal, - wander_around::WanderAroundGoal, - }, - mob::{Mob, MobEntity}, +use std::sync::{ + Arc, Weak, + atomic::{AtomicBool, AtomicI32, AtomicU8, Ordering}, }; -/// Represents a Donkey, a passive mob that can be tamed and equipped with chests. -/// -/// Wiki: +use crossbeam::atomic::AtomicCell; +use pumpkin_data::entity::EntityType; +use pumpkin_data::item::Item; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::sound::{Sound, SoundCategory}; +use pumpkin_data::tag::{self, Taggable}; +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_protocol::java::client::play::Metadata; +use uuid::Uuid; + +use crate::entity::{ + Entity, EntityBase, + 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::GOLDEN_APPLE, + &Item::ENCHANTED_GOLDEN_APPLE, + &Item::GOLDEN_CARROT, +]; + +pub const FLAG_TAME: u8 = 2; +pub const FLAG_SADDLE: u8 = 4; +pub const FLAG_BRED: u8 = 8; +pub const FLAG_EATING: u8 = 16; +pub const FLAG_STANDING: u8 = 32; +pub const FLAG_OPEN_MOUTH: u8 = 64; + pub struct DonkeyEntity { pub mob_entity: MobEntity, + pub ageable_data: AgeableData, + pub flags: AtomicU8, + pub has_chest: AtomicBool, + pub temper: AtomicI32, + pub owner: AtomicCell>, } impl DonkeyEntity { pub fn new(entity: Entity) -> Arc { let mob_entity = MobEntity::new(entity); - let donkey = Self { mob_entity }; + let donkey = Self { + mob_entity, + ageable_data: AgeableData::default(), + flags: AtomicU8::new(0), + has_chest: AtomicBool::new(false), + temper: AtomicI32::new(0), + owner: AtomicCell::new(None), + }; let mob_arc = Arc::new(donkey); let mob_weak: Weak = { let mob_arc: Arc = mob_arc.clone(); @@ -36,20 +73,206 @@ impl DonkeyEntity { .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(0.7))); + goal_selector.add_goal(1, EscapeDangerGoal::new(1.2)); + goal_selector.add_goal(2, BreedGoal::new(1.0)); + goal_selector.add_goal(3, Box::new(TemptGoal::new(1.25, TEMPT_ITEMS))); + goal_selector.add_goal(4, Box::new(FollowParentGoal::new(1.0))); + goal_selector.add_goal(6, Box::new(WanderAroundGoal::new(0.7))); goal_selector.add_goal( - 2, + 7, LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 6.0), ); - goal_selector.add_goal(3, Box::new(RandomLookAroundGoal::default())); + goal_selector.add_goal(8, Box::new(RandomLookAroundGoal::default())); }; mob_arc } + + #[must_use] + pub fn has_flag(&self, flag: u8) -> bool { + (self.flags.load(Ordering::Relaxed) & flag) != 0 + } + + pub fn set_flag(&self, flag: u8, val: bool) { + let current = self.flags.load(Ordering::Relaxed); + let new_flags = if val { current | flag } else { current & !flag }; + self.flags.store(new_flags, Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::donkey::DATA_ID_FLAGS, + new_flags as i8, + )], + None, + ); + } + + #[must_use] + pub fn is_tame(&self) -> bool { + self.has_flag(FLAG_TAME) + } + + pub fn set_tame(&self, val: bool) { + self.set_flag(FLAG_TAME, val); + } + + #[must_use] + pub fn is_saddled(&self) -> bool { + self.has_flag(FLAG_SADDLE) + } + + pub fn set_saddled(&self, val: bool) { + self.set_flag(FLAG_SADDLE, val); + } + + #[must_use] + pub fn has_chest(&self) -> bool { + self.has_chest.load(Ordering::Relaxed) + } + + pub fn set_has_chest(&self, val: bool) { + self.has_chest.store(val, Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::donkey::DATA_ID_CHEST, + val, + )], + None, + ); + } +} + +impl AgeableMob for DonkeyEntity { + fn get_ageable_data(&self) -> &AgeableData { + &self.ageable_data + } +} + +impl Animal for DonkeyEntity { + fn is_food(&self, item_stack: &ItemStack) -> bool { + item_stack.item.has_tag(&tag::Item::MINECRAFT_HORSE_FOOD) + || item_stack.item == &Item::WHEAT + || item_stack.item == &Item::SUGAR + || item_stack.item == &Item::HAY_BLOCK + || item_stack.item == &Item::APPLE + || item_stack.item == &Item::GOLDEN_CARROT + || item_stack.item == &Item::GOLDEN_APPLE + || item_stack.item == &Item::ENCHANTED_GOLDEN_APPLE + } } impl Mob for DonkeyEntity { + fn as_ageable(&self) -> Option<&dyn AgeableMob> { + Some(self) + } + + fn as_animal(&self) -> Option<&dyn Animal> { + Some(self) + } + + fn mob_write_nbt(&self, nbt: &mut NbtCompound) { + self.write_ageable_nbt(nbt); + nbt.put_bool("ChestedHorse", self.has_chest()); + nbt.put_bool("Tame", self.is_tame()); + nbt.put_int("Temper", self.temper.load(Ordering::Relaxed)); + if let Some(owner) = self.owner.load() { + nbt.put_uuid("Owner", owner); + } + } + + fn mob_read_nbt(&self, nbt: &NbtCompound) { + self.read_ageable_nbt(nbt); + if let Some(chested) = nbt.get_bool("ChestedHorse") { + self.set_has_chest(chested); + } + if let Some(tame) = nbt.get_bool("Tame") { + self.set_tame(tame); + } + if let Some(temper) = nbt.get_int("Temper") { + self.temper.store(temper, Ordering::Relaxed); + } + if let Some(owner) = nbt.get_uuid("Owner") { + self.owner.store(Some(owner)); + } + } + fn get_mob_entity(&self) -> &MobEntity { &self.mob_entity } + + fn mob_tick(&self, _caller: &dyn EntityBase) { + self.ageable_ai_step(); + } + + fn mob_init_data_tracker(&self) { + let entity = self.get_entity(); + let is_baby = entity.age.load(Ordering::Relaxed) < 0; + if is_baby { + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::donkey::DATA_BABY_ID, + true, + )], + None, + ); + } + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::donkey::DATA_ID_CHEST, + self.has_chest(), + )], + None, + ); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::donkey::DATA_ID_FLAGS, + self.flags.load(Ordering::Relaxed) as i8, + )], + None, + ); + } + + fn mob_interact(&self, player: &Arc, item_stack: &mut ItemStack) -> bool { + let item = item_stack.get_item(); + + if self.is_tame() && item == &Item::CHEST && !self.has_chest() && !self.is_baby() { + self.set_has_chest(true); + item_stack.decrement_unless_creative(player.gamemode.load(), 1); + let entity = self.get_entity(); + let world = entity.world.load(); + world.play_sound( + Sound::EntityDonkeyChest, + SoundCategory::Neutral, + &entity.pos.load(), + ); + return true; + } + + if self.is_tame() && item == &Item::SADDLE && !self.is_saddled() && !self.is_baby() { + self.set_saddled(true); + item_stack.decrement_unless_creative(player.gamemode.load(), 1); + let entity = self.get_entity(); + let world = entity.world.load(); + world.play_sound( + Sound::EntityHorseSaddle, + SoundCategory::Neutral, + &entity.pos.load(), + ); + return true; + } + + if !self.is_baby() && !self.is_food(item_stack) { + let world = player.world(); + let ent = &self.mob_entity.living_entity.entity; + if let Some(vehicle) = world.get_entity_by_id(ent.entity_id) + && let Some(passenger) = world.get_player_by_id(player.entity_id()) + { + ent.add_passenger(vehicle, passenger as Arc); + return true; + } + } + + self.animal_interact(player, item_stack, Sound::EntityDonkeyAmbient) + } } diff --git a/crates/pumpkin/src/entity/passive/goat.rs b/crates/pumpkin/src/entity/passive/goat.rs index bb99d68e2..c2e947dc1 100644 --- a/crates/pumpkin/src/entity/passive/goat.rs +++ b/crates/pumpkin/src/entity/passive/goat.rs @@ -1,27 +1,49 @@ -use std::sync::{Arc, Weak}; - -use pumpkin_data::entity::EntityType; - -use crate::entity::{ - Entity, - ai::goal::{ - look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal, - wander_around::WanderAroundGoal, - }, - mob::{Mob, MobEntity}, +use std::sync::{ + Arc, Weak, + atomic::{AtomicBool, Ordering}, }; -/// Represents a Goat, a neutral mob that can jump high and ram players or other mobs. -/// -/// Wiki: +use pumpkin_data::entity::EntityType; +use pumpkin_data::item::Item; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::sound::{Sound, SoundCategory}; +use pumpkin_data::tag::{self, Taggable}; +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_protocol::java::client::play::Metadata; + +use crate::entity::{ + Entity, EntityBase, + 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::WHEAT]; + pub struct GoatEntity { pub mob_entity: MobEntity, + pub ageable_data: AgeableData, + pub is_screaming: AtomicBool, + pub has_left_horn: AtomicBool, + pub has_right_horn: AtomicBool, } impl GoatEntity { pub fn new(entity: Entity) -> Arc { let mob_entity = MobEntity::new(entity); - let goat = Self { mob_entity }; + let goat = Self { + mob_entity, + ageable_data: AgeableData::default(), + is_screaming: AtomicBool::new(false), + has_left_horn: AtomicBool::new(true), + has_right_horn: AtomicBool::new(true), + }; let mob_arc = Arc::new(goat); let mob_weak: Weak = { let mob_arc: Arc = mob_arc.clone(); @@ -36,20 +58,176 @@ impl GoatEntity { .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, TEMPT_ITEMS))); + goal_selector.add_goal(4, Box::new(FollowParentGoal::new(1.25))); + goal_selector.add_goal(6, Box::new(WanderAroundGoal::new(1.0))); goal_selector.add_goal( - 2, + 7, LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 6.0), ); - goal_selector.add_goal(3, Box::new(RandomLookAroundGoal::default())); + goal_selector.add_goal(8, Box::new(RandomLookAroundGoal::default())); }; mob_arc } + + #[must_use] + pub fn is_screaming(&self) -> bool { + self.is_screaming.load(Ordering::Relaxed) + } + + pub fn set_screaming(&self, screaming: bool) { + self.is_screaming.store(screaming, Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::goat::DATA_IS_SCREAMING_GOAT, + screaming, + )], + None, + ); + } + + #[must_use] + pub fn has_left_horn(&self) -> bool { + self.has_left_horn.load(Ordering::Relaxed) + } + + pub fn set_has_left_horn(&self, has_horn: bool) { + self.has_left_horn.store(has_horn, Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::goat::DATA_HAS_LEFT_HORN, + has_horn, + )], + None, + ); + } + + #[must_use] + pub fn has_right_horn(&self) -> bool { + self.has_right_horn.load(Ordering::Relaxed) + } + + pub fn set_has_right_horn(&self, has_horn: bool) { + self.has_right_horn.store(has_horn, Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::goat::DATA_HAS_RIGHT_HORN, + has_horn, + )], + None, + ); + } +} + +impl AgeableMob for GoatEntity { + fn get_ageable_data(&self) -> &AgeableData { + &self.ageable_data + } +} + +impl Animal for GoatEntity { + fn is_food(&self, item_stack: &ItemStack) -> bool { + item_stack.item.has_tag(&tag::Item::MINECRAFT_GOAT_FOOD) + || TEMPT_ITEMS.iter().any(|i| i.id == item_stack.item.id) + } } impl Mob for GoatEntity { + fn as_ageable(&self) -> Option<&dyn AgeableMob> { + Some(self) + } + + fn as_animal(&self) -> Option<&dyn Animal> { + Some(self) + } + + fn mob_write_nbt(&self, nbt: &mut NbtCompound) { + nbt.put_bool("IsScreamingGoat", self.is_screaming()); + nbt.put_bool("HasLeftHorn", self.has_left_horn()); + nbt.put_bool("HasRightHorn", self.has_right_horn()); + } + + fn mob_read_nbt(&self, nbt: &NbtCompound) { + if let Some(screaming) = nbt.get_bool("IsScreamingGoat") { + self.set_screaming(screaming); + } + if let Some(left) = nbt.get_bool("HasLeftHorn") { + self.set_has_left_horn(left); + } + if let Some(right) = nbt.get_bool("HasRightHorn") { + self.set_has_right_horn(right); + } + } + fn get_mob_entity(&self) -> &MobEntity { &self.mob_entity } + + fn mob_tick(&self, _caller: &dyn EntityBase) { + self.ageable_ai_step(); + } + + fn mob_init_data_tracker(&self) { + let entity = self.get_entity(); + let is_baby = entity.age.load(Ordering::Relaxed) < 0; + if is_baby { + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::goat::DATA_BABY_ID, + true, + )], + None, + ); + } + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::goat::DATA_IS_SCREAMING_GOAT, + self.is_screaming(), + )], + None, + ); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::goat::DATA_HAS_LEFT_HORN, + self.has_left_horn(), + )], + None, + ); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::goat::DATA_HAS_RIGHT_HORN, + self.has_right_horn(), + )], + None, + ); + } + + fn mob_interact(&self, player: &Arc, item_stack: &mut ItemStack) -> bool { + let item = item_stack.get_item(); + if item == &Item::BUCKET && !self.is_baby() { + item_stack.decrement_unless_creative(player.gamemode.load(), 1); + let entity = self.get_entity(); + let world = entity.world.load(); + let sound = if self.is_screaming() { + Sound::EntityGoatScreamingMilk + } else { + Sound::EntityGoatMilk + }; + world.play_sound(sound, SoundCategory::Neutral, &entity.pos.load()); + return true; + } + + let ambient_sound = if self.is_screaming() { + Sound::EntityGoatScreamingAmbient + } else { + Sound::EntityGoatAmbient + }; + self.animal_interact(player, item_stack, ambient_sound) + } } diff --git a/crates/pumpkin/src/entity/passive/horse.rs b/crates/pumpkin/src/entity/passive/horse.rs index a3fce5acb..c7ac024a9 100644 --- a/crates/pumpkin/src/entity/passive/horse.rs +++ b/crates/pumpkin/src/entity/passive/horse.rs @@ -1,27 +1,71 @@ -use std::sync::{Arc, Weak}; - -use pumpkin_data::entity::EntityType; - -use crate::entity::{ - Entity, - ai::goal::{ - look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal, - wander_around::WanderAroundGoal, - }, - mob::{Mob, MobEntity}, +use std::sync::{ + Arc, Weak, + atomic::{AtomicI32, AtomicU8, Ordering}, }; -/// Represents a Horse, a passive mob that can be tamed and ridden. -/// -/// Wiki: +use crossbeam::atomic::AtomicCell; +use pumpkin_data::entity::EntityType; +use pumpkin_data::item::Item; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::sound::{Sound, SoundCategory}; +use pumpkin_data::tag::{self, Taggable}; +use pumpkin_nbt::compound::NbtCompound; +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, + 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::GOLDEN_APPLE, + &Item::ENCHANTED_GOLDEN_APPLE, + &Item::GOLDEN_CARROT, +]; + +pub const FLAG_TAME: u8 = 2; +pub const FLAG_SADDLE: u8 = 4; +pub const FLAG_BRED: u8 = 8; +pub const FLAG_EATING: u8 = 16; +pub const FLAG_STANDING: u8 = 32; +pub const FLAG_OPEN_MOUTH: u8 = 64; + pub struct HorseEntity { pub mob_entity: MobEntity, + pub ageable_data: AgeableData, + pub variant: AtomicI32, + pub flags: AtomicU8, + pub temper: AtomicI32, + pub owner: AtomicCell>, } impl HorseEntity { pub fn new(entity: Entity) -> Arc { let mob_entity = MobEntity::new(entity); - let horse = Self { mob_entity }; + let mut rng = rand::rng(); + let color = rng.random_range(0..7); + let style = rng.random_range(0..5); + let variant = color | (style << 8); + + let horse = Self { + mob_entity, + ageable_data: AgeableData::default(), + variant: AtomicI32::new(variant), + flags: AtomicU8::new(0), + temper: AtomicI32::new(0), + owner: AtomicCell::new(None), + }; let mob_arc = Arc::new(horse); let mob_weak: Weak = { let mob_arc: Arc = mob_arc.clone(); @@ -36,20 +80,193 @@ impl HorseEntity { .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(0.7))); + goal_selector.add_goal(1, EscapeDangerGoal::new(1.2)); + goal_selector.add_goal(2, BreedGoal::new(1.0)); + goal_selector.add_goal(3, Box::new(TemptGoal::new(1.25, TEMPT_ITEMS))); + goal_selector.add_goal(4, Box::new(FollowParentGoal::new(1.0))); + goal_selector.add_goal(6, Box::new(WanderAroundGoal::new(0.7))); goal_selector.add_goal( - 2, + 7, LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 6.0), ); - goal_selector.add_goal(3, Box::new(RandomLookAroundGoal::default())); + goal_selector.add_goal(8, Box::new(RandomLookAroundGoal::default())); }; mob_arc } + + #[must_use] + pub fn get_variant(&self) -> i32 { + self.variant.load(Ordering::Relaxed) + } + + pub fn set_variant(&self, variant: i32) { + self.variant.store(variant, Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::horse::DATA_ID_TYPE_VARIANT, + VarInt(variant), + )], + None, + ); + } + + #[must_use] + pub fn has_flag(&self, flag: u8) -> bool { + (self.flags.load(Ordering::Relaxed) & flag) != 0 + } + + pub fn set_flag(&self, flag: u8, val: bool) { + let current = self.flags.load(Ordering::Relaxed); + let new_flags = if val { current | flag } else { current & !flag }; + self.flags.store(new_flags, Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::horse::DATA_ID_FLAGS, + new_flags as i8, + )], + None, + ); + } + + #[must_use] + pub fn is_tame(&self) -> bool { + self.has_flag(FLAG_TAME) + } + + pub fn set_tame(&self, val: bool) { + self.set_flag(FLAG_TAME, val); + } + + #[must_use] + pub fn is_saddled(&self) -> bool { + self.has_flag(FLAG_SADDLE) + } + + pub fn set_saddled(&self, val: bool) { + self.set_flag(FLAG_SADDLE, val); + } +} + +impl AgeableMob for HorseEntity { + fn get_ageable_data(&self) -> &AgeableData { + &self.ageable_data + } +} + +impl Animal for HorseEntity { + fn is_food(&self, item_stack: &ItemStack) -> bool { + item_stack.item.has_tag(&tag::Item::MINECRAFT_HORSE_FOOD) + || item_stack.item == &Item::WHEAT + || item_stack.item == &Item::SUGAR + || item_stack.item == &Item::HAY_BLOCK + || item_stack.item == &Item::APPLE + || item_stack.item == &Item::GOLDEN_CARROT + || item_stack.item == &Item::GOLDEN_APPLE + || item_stack.item == &Item::ENCHANTED_GOLDEN_APPLE + } } impl Mob for HorseEntity { + fn as_ageable(&self) -> Option<&dyn AgeableMob> { + Some(self) + } + + fn as_animal(&self) -> Option<&dyn Animal> { + Some(self) + } + + fn mob_write_nbt(&self, nbt: &mut NbtCompound) { + self.write_ageable_nbt(nbt); + nbt.put_int("Variant", self.get_variant()); + nbt.put_bool("Tame", self.is_tame()); + nbt.put_int("Temper", self.temper.load(Ordering::Relaxed)); + if let Some(owner) = self.owner.load() { + nbt.put_uuid("Owner", owner); + } + } + + fn mob_read_nbt(&self, nbt: &NbtCompound) { + self.read_ageable_nbt(nbt); + if let Some(variant) = nbt.get_int("Variant") { + self.set_variant(variant); + } + if let Some(tame) = nbt.get_bool("Tame") { + self.set_tame(tame); + } + if let Some(temper) = nbt.get_int("Temper") { + self.temper.store(temper, Ordering::Relaxed); + } + if let Some(owner) = nbt.get_uuid("Owner") { + self.owner.store(Some(owner)); + } + } + fn get_mob_entity(&self) -> &MobEntity { &self.mob_entity } + + fn mob_tick(&self, _caller: &dyn EntityBase) { + self.ageable_ai_step(); + } + + fn mob_init_data_tracker(&self) { + let entity = self.get_entity(); + let is_baby = entity.age.load(Ordering::Relaxed) < 0; + if is_baby { + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::horse::DATA_BABY_ID, + true, + )], + None, + ); + } + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::horse::DATA_ID_TYPE_VARIANT, + VarInt(self.get_variant()), + )], + None, + ); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::horse::DATA_ID_FLAGS, + self.flags.load(Ordering::Relaxed) as i8, + )], + None, + ); + } + + fn mob_interact(&self, player: &Arc, item_stack: &mut ItemStack) -> bool { + let item = item_stack.get_item(); + + if self.is_tame() && item == &Item::SADDLE && !self.is_saddled() && !self.is_baby() { + self.set_saddled(true); + item_stack.decrement_unless_creative(player.gamemode.load(), 1); + let entity = self.get_entity(); + let world = entity.world.load(); + world.play_sound( + Sound::EntityHorseSaddle, + SoundCategory::Neutral, + &entity.pos.load(), + ); + return true; + } + + if !self.is_baby() && !self.is_food(item_stack) { + let world = player.world(); + let ent = &self.mob_entity.living_entity.entity; + if let Some(vehicle) = world.get_entity_by_id(ent.entity_id) + && let Some(passenger) = world.get_player_by_id(player.entity_id()) + { + ent.add_passenger(vehicle, passenger as Arc); + return true; + } + } + + self.animal_interact(player, item_stack, Sound::EntityHorseAmbient) + } } diff --git a/crates/pumpkin/src/entity/passive/llama.rs b/crates/pumpkin/src/entity/passive/llama.rs index bf4ec2764..e5b5ef6ae 100644 --- a/crates/pumpkin/src/entity/passive/llama.rs +++ b/crates/pumpkin/src/entity/passive/llama.rs @@ -1,31 +1,111 @@ -use std::sync::atomic::Ordering; -use std::sync::{Arc, Weak}; +use std::sync::{ + Arc, Weak, + atomic::{AtomicBool, AtomicI32, 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::sound::{Sound, SoundCategory}; +use pumpkin_data::tag::{self, Taggable}; +use pumpkin_nbt::compound::NbtCompound; +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, + ageable::{AgeableData, AgeableMob}, ai::goal::{ - active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal, + active_target::ActiveTargetGoal, breed::BreedGoal, escape_danger::EscapeDangerGoal, + follow_parent::FollowParentGoal, look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, ranged_attack::RangedAttackGoal, revenge::RevengeGoal, - swim::SwimGoal, wander_around::WanderAroundGoal, + swim::SwimGoal, tempt::TemptGoal, wander_around::WanderAroundGoal, }, mob::{Mob, MobEntity, RangedAttackMob}, + passive::animal::{Animal, get_carpet_color_from_item}, + player::Player, projectile::llama_spit::LlamaSpitEntity, }; -/// Represents a Llama, a neutral mob that can be used for carrying items and spits at enemies. -/// -/// Wiki: +const TEMPT_ITEMS: &[&Item] = &[&Item::HAY_BLOCK]; + +pub const FLAG_TAME: u8 = 2; +pub const FLAG_BRED: u8 = 8; +pub const FLAG_EATING: u8 = 16; +pub const FLAG_STANDING: u8 = 32; +pub const FLAG_OPEN_MOUTH: u8 = 64; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[repr(i32)] +pub enum LlamaVariant { + #[default] + Creamy = 0, + White = 1, + Brown = 2, + Gray = 3, +} + +impl LlamaVariant { + #[must_use] + pub const fn from_id(id: i32) -> Self { + match id { + 1 => Self::White, + 2 => Self::Brown, + 3 => Self::Gray, + _ => Self::Creamy, + } + } + + #[must_use] + pub const fn id(self) -> i32 { + self as i32 + } + + #[must_use] + pub fn random_variant() -> Self { + let mut rng = rand::rng(); + match rng.random_range(0..4) { + 1 => Self::White, + 2 => Self::Brown, + 3 => Self::Gray, + _ => Self::Creamy, + } + } +} + pub struct LlamaEntity { pub mob_entity: MobEntity, + pub ageable_data: AgeableData, + pub variant: AtomicI32, + pub strength: AtomicI32, + pub carpet_color: AtomicCell>, + pub flags: AtomicU8, + pub has_chest: AtomicBool, + pub temper: AtomicI32, + pub owner: AtomicCell>, } impl LlamaEntity { pub fn new(entity: Entity) -> Arc { let mob_entity = MobEntity::new(entity); - let llama = Self { mob_entity }; + let mut rng = rand::rng(); + let variant = LlamaVariant::random_variant(); + let strength = rng.random_range(1..=5); + + let llama = Self { + mob_entity, + ageable_data: AgeableData::default(), + variant: AtomicI32::new(variant.id()), + strength: AtomicI32::new(strength), + carpet_color: AtomicCell::new(None), + flags: AtomicU8::new(0), + has_chest: AtomicBool::new(false), + temper: AtomicI32::new(0), + owner: AtomicCell::new(None), + }; let mob_arc = Arc::new(llama); let mob_weak: Weak = { let mob_arc: Arc = mob_arc.clone(); @@ -49,10 +129,14 @@ impl LlamaEntity { .unwrap_or_else(std::sync::PoisonError::into_inner); goal_selector.add_goal(0, Box::new(SwimGoal::default())); + goal_selector.add_goal(1, EscapeDangerGoal::new(1.2)); + goal_selector.add_goal(2, BreedGoal::new(1.0)); goal_selector.add_goal( 3, Box::new(RangedAttackGoal::new(ranged_weak, 1.25, 40, 20.0)), ); + goal_selector.add_goal(4, Box::new(TemptGoal::new(1.25, TEMPT_ITEMS))); + goal_selector.add_goal(5, Box::new(FollowParentGoal::new(1.0))); goal_selector.add_goal(7, Box::new(WanderAroundGoal::new(0.7))); goal_selector.add_goal( 8, @@ -70,6 +154,85 @@ impl LlamaEntity { mob_arc } + #[must_use] + pub fn get_variant(&self) -> LlamaVariant { + LlamaVariant::from_id(self.variant.load(Ordering::Relaxed)) + } + + pub fn set_variant(&self, variant: LlamaVariant) { + self.variant.store(variant.id(), Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::llama::DATA_VARIANT_ID, + VarInt(variant.id()), + )], + None, + ); + } + + #[must_use] + pub fn get_strength(&self) -> i32 { + self.strength.load(Ordering::Relaxed) + } + + pub fn set_strength(&self, strength: i32) { + self.strength.store(strength, Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::llama::DATA_STRENGTH_ID, + VarInt(strength), + )], + None, + ); + } + + #[must_use] + pub fn has_flag(&self, flag: u8) -> bool { + (self.flags.load(Ordering::Relaxed) & flag) != 0 + } + + pub fn set_flag(&self, flag: u8, val: bool) { + let current = self.flags.load(Ordering::Relaxed); + let new_flags = if val { current | flag } else { current & !flag }; + self.flags.store(new_flags, Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::llama::DATA_ID_FLAGS, + new_flags as i8, + )], + None, + ); + } + + #[must_use] + pub fn is_tame(&self) -> bool { + self.has_flag(FLAG_TAME) + } + + pub fn set_tame(&self, val: bool) { + self.set_flag(FLAG_TAME, val); + } + + #[must_use] + pub fn has_chest(&self) -> bool { + self.has_chest.load(Ordering::Relaxed) + } + + pub fn set_has_chest(&self, val: bool) { + self.has_chest.store(val, Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::llama::DATA_ID_CHEST, + val, + )], + None, + ); + } + pub fn spit(&self, target: &Arc) { let entity = self.get_entity(); let world = entity.world.load_full(); @@ -99,10 +262,161 @@ impl LlamaEntity { } } +impl AgeableMob for LlamaEntity { + fn get_ageable_data(&self) -> &AgeableData { + &self.ageable_data + } +} + +impl Animal for LlamaEntity { + fn is_food(&self, item_stack: &ItemStack) -> bool { + item_stack.item.has_tag(&tag::Item::MINECRAFT_LLAMA_FOOD) + || item_stack + .item + .has_tag(&tag::Item::MINECRAFT_LLAMA_TEMPT_ITEMS) + || item_stack.item == &Item::WHEAT + || item_stack.item == &Item::HAY_BLOCK + } +} + impl Mob for LlamaEntity { + fn as_ageable(&self) -> Option<&dyn AgeableMob> { + Some(self) + } + + fn as_animal(&self) -> Option<&dyn Animal> { + Some(self) + } + + fn mob_write_nbt(&self, nbt: &mut NbtCompound) { + self.write_ageable_nbt(nbt); + nbt.put_int("Variant", self.get_variant().id()); + nbt.put_int("Strength", self.get_strength()); + nbt.put_bool("ChestedHorse", self.has_chest()); + nbt.put_bool("Tame", self.is_tame()); + nbt.put_int("Temper", self.temper.load(Ordering::Relaxed)); + if let Some(owner) = self.owner.load() { + nbt.put_uuid("Owner", owner); + } + } + + fn mob_read_nbt(&self, nbt: &NbtCompound) { + self.read_ageable_nbt(nbt); + if let Some(variant) = nbt.get_int("Variant") { + self.set_variant(LlamaVariant::from_id(variant)); + } + if let Some(strength) = nbt.get_int("Strength") { + self.set_strength(strength); + } + if let Some(chested) = nbt.get_bool("ChestedHorse") { + self.set_has_chest(chested); + } + if let Some(tame) = nbt.get_bool("Tame") { + self.set_tame(tame); + } + if let Some(temper) = nbt.get_int("Temper") { + self.temper.store(temper, Ordering::Relaxed); + } + if let Some(owner) = nbt.get_uuid("Owner") { + self.owner.store(Some(owner)); + } + } + fn get_mob_entity(&self) -> &MobEntity { &self.mob_entity } + + fn mob_tick(&self, _caller: &dyn EntityBase) { + self.ageable_ai_step(); + } + + fn mob_init_data_tracker(&self) { + let entity = self.get_entity(); + let is_baby = entity.age.load(Ordering::Relaxed) < 0; + if is_baby { + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::llama::DATA_BABY_ID, + true, + )], + None, + ); + } + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::llama::DATA_VARIANT_ID, + VarInt(self.get_variant().id()), + )], + None, + ); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::llama::DATA_STRENGTH_ID, + VarInt(self.get_strength()), + )], + None, + ); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::llama::DATA_ID_CHEST, + self.has_chest(), + )], + None, + ); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::llama::DATA_ID_FLAGS, + self.flags.load(Ordering::Relaxed) as i8, + )], + None, + ); + } + + fn mob_interact(&self, player: &Arc, item_stack: &mut ItemStack) -> bool { + let item = item_stack.get_item(); + + if self.is_tame() && item == &Item::CHEST && !self.has_chest() && !self.is_baby() { + self.set_has_chest(true); + item_stack.decrement_unless_creative(player.gamemode.load(), 1); + let entity = self.get_entity(); + let world = entity.world.load(); + world.play_sound( + Sound::EntityDonkeyChest, + SoundCategory::Neutral, + &entity.pos.load(), + ); + return true; + } + + if self.is_tame() + && !self.is_baby() + && let Some(color) = get_carpet_color_from_item(item) + { + self.carpet_color.store(Some(color)); + item_stack.decrement_unless_creative(player.gamemode.load(), 1); + let entity = self.get_entity(); + let world = entity.world.load(); + world.play_sound( + Sound::EntityLlamaSwag, + SoundCategory::Neutral, + &entity.pos.load(), + ); + return true; + } + + if !self.is_baby() && !self.is_food(item_stack) { + let world = player.world(); + let ent = &self.mob_entity.living_entity.entity; + if let Some(vehicle) = world.get_entity_by_id(ent.entity_id) + && let Some(passenger) = world.get_player_by_id(player.entity_id()) + { + ent.add_passenger(vehicle, passenger as Arc); + return true; + } + } + + self.animal_interact(player, item_stack, Sound::EntityLlamaAmbient) + } } impl RangedAttackMob for LlamaEntity { diff --git a/crates/pumpkin/src/entity/passive/mooshroom.rs b/crates/pumpkin/src/entity/passive/mooshroom.rs index f7b3ce813..834dc6984 100644 --- a/crates/pumpkin/src/entity/passive/mooshroom.rs +++ b/crates/pumpkin/src/entity/passive/mooshroom.rs @@ -1,27 +1,94 @@ -use std::sync::{Arc, Weak}; - -use pumpkin_data::entity::EntityType; - -use crate::entity::{ - Entity, - 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}, }; -/// Represents a Mooshroom, a fungal variant of cows that can be milked for mushroom stew. -/// -/// Wiki: +use crossbeam::atomic::AtomicCell; +use pumpkin_data::entity::EntityType; +use pumpkin_data::item::Item; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::particle::Particle; +use pumpkin_data::sound::{Sound, SoundCategory}; +use pumpkin_data::tag::{self, Taggable}; +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_protocol::codec::var_int::VarInt; +use pumpkin_protocol::java::client::play::Metadata; +use pumpkin_util::math::vector3::Vector3; +use uuid::Uuid; + +use crate::entity::{ + Entity, EntityBase, + 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, + }, + item::ItemEntity, + mob::{Mob, MobEntity}, + passive::animal::Animal, + player::Player, +}; + +const TEMPT_ITEMS: &[&Item] = &[&Item::WHEAT]; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[repr(i32)] +pub enum MooshroomVariant { + #[default] + Red = 0, + Brown = 1, +} + +impl MooshroomVariant { + #[must_use] + pub const fn from_id(id: i32) -> Self { + match id { + 1 => Self::Brown, + _ => 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::Brown => "brown", + } + } + + #[must_use] + pub fn from_name(name: &str) -> Self { + match name { + "brown" => Self::Brown, + _ => Self::Red, + } + } +} + pub struct MooshroomEntity { pub mob_entity: MobEntity, + pub ageable_data: AgeableData, + pub variant: AtomicI32, + pub stew_effect: AtomicCell>, + pub last_lightning_bolt_uuid: AtomicCell>, } impl MooshroomEntity { pub fn new(entity: Entity) -> Arc { let mob_entity = MobEntity::new(entity); - let mooshroom = Self { mob_entity }; + let mooshroom = Self { + mob_entity, + ageable_data: AgeableData::default(), + variant: AtomicI32::new(MooshroomVariant::Red.id()), + stew_effect: AtomicCell::new(None), + last_lightning_bolt_uuid: AtomicCell::new(None), + }; let mob_arc = Arc::new(mooshroom); let mob_weak: Weak = { let mob_arc: Arc = mob_arc.clone(); @@ -36,20 +103,182 @@ impl MooshroomEntity { .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, TEMPT_ITEMS))); + goal_selector.add_goal(4, Box::new(FollowParentGoal::new(1.25))); + 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_variant(&self) -> MooshroomVariant { + MooshroomVariant::from_id(self.variant.load(Ordering::Relaxed)) + } + + pub fn set_variant(&self, variant: MooshroomVariant) { + self.variant.store(variant.id(), Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::mooshroom::DATA_TYPE, + VarInt(variant.id()), + )], + None, + ); + } +} + +impl AgeableMob for MooshroomEntity { + fn get_ageable_data(&self) -> &AgeableData { + &self.ageable_data + } +} + +impl Animal for MooshroomEntity { + fn is_food(&self, item_stack: &ItemStack) -> bool { + item_stack.item.has_tag(&tag::Item::MINECRAFT_COW_FOOD) + || TEMPT_ITEMS.iter().any(|i| i.id == item_stack.item.id) + } } impl Mob for MooshroomEntity { + fn as_ageable(&self) -> Option<&dyn AgeableMob> { + Some(self) + } + + fn as_animal(&self) -> Option<&dyn Animal> { + Some(self) + } + + fn mob_write_nbt(&self, nbt: &mut NbtCompound) { + nbt.put_string("Type", self.get_variant().as_str().to_string()); + } + + fn mob_read_nbt(&self, nbt: &NbtCompound) { + if let Some(variant_str) = nbt.get_string("Type") { + self.set_variant(MooshroomVariant::from_name(variant_str)); + } + } + fn get_mob_entity(&self) -> &MobEntity { &self.mob_entity } + + fn mob_tick(&self, _caller: &dyn EntityBase) { + self.ageable_ai_step(); + } + + fn mob_init_data_tracker(&self) { + let entity = self.get_entity(); + let is_baby = entity.age.load(Ordering::Relaxed) < 0; + if is_baby { + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::mooshroom::DATA_BABY_ID, + true, + )], + None, + ); + } + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::mooshroom::DATA_TYPE, + VarInt(self.get_variant().id()), + )], + None, + ); + } + + fn mob_interact(&self, player: &Arc, item_stack: &mut ItemStack) -> bool { + let item = item_stack.get_item(); + + if item == &Item::BOWL && !self.is_baby() { + item_stack.decrement_unless_creative(player.gamemode.load(), 1); + let entity = self.get_entity(); + let world = entity.world.load(); + let pos = entity.pos.load(); + let is_suspicious = self.stew_effect.swap(None).is_some(); + let sound = if is_suspicious { + Sound::EntityMooshroomSuspiciousMilk + } else { + Sound::EntityMooshroomMilk + }; + world.play_sound(sound, SoundCategory::Neutral, &pos); + return true; + } + + if item == &Item::BUCKET && !self.is_baby() { + item_stack.decrement_unless_creative(player.gamemode.load(), 1); + let entity = self.get_entity(); + let world = entity.world.load(); + world.play_sound( + Sound::EntityCowMilk, + SoundCategory::Neutral, + &entity.pos.load(), + ); + return true; + } + + if item == &Item::SHEARS && !self.is_baby() { + let entity = self.get_entity(); + let world = entity.world.load(); + let pos = entity.pos.load(); + world.play_sound(Sound::EntityMooshroomShear, SoundCategory::Players, &pos); + + let mushroom_item = if self.get_variant() == MooshroomVariant::Brown { + &Item::BROWN_MUSHROOM + } else { + &Item::RED_MUSHROOM + }; + + for _ in 0..5 { + let item_entity = Arc::new(ItemEntity::new( + Entity::new(world.clone(), pos, &EntityType::ITEM), + ItemStack::new(1, mushroom_item), + )); + world.spawn_entity(item_entity); + } + + world.spawn_particle( + pos + Vector3::new(0.0, 0.5, 0.0), + Vector3::new(0.5, 0.5, 0.5), + 0.0, + 1, + Particle::Explosion, + ); + + player.damage_held_item(1); + return true; + } + + if self.get_variant() == MooshroomVariant::Brown + && !self.is_baby() + && item.has_tag(&tag::Item::MINECRAFT_SMALL_FLOWERS) + { + item_stack.decrement_unless_creative(player.gamemode.load(), 1); + let entity = self.get_entity(); + let world = entity.world.load(); + let pos = entity.pos.load(); + self.stew_effect.store(Some(1)); + world.play_sound(Sound::EntityMooshroomEat, SoundCategory::Neutral, &pos); + world.spawn_particle( + pos + Vector3::new(0.0, 0.5, 0.0), + Vector3::new(0.5, 0.5, 0.5), + 0.0, + 4, + Particle::Effect, + ); + return true; + } + + self.animal_interact(player, item_stack, Sound::EntityCowAmbient) + } } diff --git a/crates/pumpkin/src/entity/passive/mule.rs b/crates/pumpkin/src/entity/passive/mule.rs index 1b4192623..2c4f73056 100644 --- a/crates/pumpkin/src/entity/passive/mule.rs +++ b/crates/pumpkin/src/entity/passive/mule.rs @@ -1,27 +1,64 @@ -use std::sync::{Arc, Weak}; - -use pumpkin_data::entity::EntityType; - -use crate::entity::{ - Entity, - ai::goal::{ - look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal, - wander_around::WanderAroundGoal, - }, - mob::{Mob, MobEntity}, +use std::sync::{ + Arc, Weak, + atomic::{AtomicBool, AtomicI32, AtomicU8, Ordering}, }; -/// Represents a Mule, a passive mob created by breeding a horse and a donkey. -/// -/// Wiki: +use crossbeam::atomic::AtomicCell; +use pumpkin_data::entity::EntityType; +use pumpkin_data::item::Item; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::sound::{Sound, SoundCategory}; +use pumpkin_data::tag::{self, Taggable}; +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_protocol::java::client::play::Metadata; +use uuid::Uuid; + +use crate::entity::{ + Entity, EntityBase, + ageable::{AgeableData, AgeableMob}, + ai::goal::{ + 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::GOLDEN_APPLE, + &Item::ENCHANTED_GOLDEN_APPLE, + &Item::GOLDEN_CARROT, +]; + +pub const FLAG_TAME: u8 = 2; +pub const FLAG_SADDLE: u8 = 4; +pub const FLAG_BRED: u8 = 8; +pub const FLAG_EATING: u8 = 16; +pub const FLAG_STANDING: u8 = 32; +pub const FLAG_OPEN_MOUTH: u8 = 64; + pub struct MuleEntity { pub mob_entity: MobEntity, + pub ageable_data: AgeableData, + pub flags: AtomicU8, + pub has_chest: AtomicBool, + pub temper: AtomicI32, + pub owner: AtomicCell>, } impl MuleEntity { pub fn new(entity: Entity) -> Arc { let mob_entity = MobEntity::new(entity); - let mule = Self { mob_entity }; + let mule = Self { + mob_entity, + ageable_data: AgeableData::default(), + flags: AtomicU8::new(0), + has_chest: AtomicBool::new(false), + temper: AtomicI32::new(0), + owner: AtomicCell::new(None), + }; let mob_arc = Arc::new(mule); let mob_weak: Weak = { let mob_arc: Arc = mob_arc.clone(); @@ -36,20 +73,205 @@ impl MuleEntity { .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(0.7))); + goal_selector.add_goal(1, EscapeDangerGoal::new(1.2)); + goal_selector.add_goal(3, Box::new(TemptGoal::new(1.25, TEMPT_ITEMS))); + goal_selector.add_goal(4, Box::new(FollowParentGoal::new(1.0))); + goal_selector.add_goal(6, Box::new(WanderAroundGoal::new(0.7))); goal_selector.add_goal( - 2, + 7, LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 6.0), ); - goal_selector.add_goal(3, Box::new(RandomLookAroundGoal::default())); + goal_selector.add_goal(8, Box::new(RandomLookAroundGoal::default())); }; mob_arc } + + #[must_use] + pub fn has_flag(&self, flag: u8) -> bool { + (self.flags.load(Ordering::Relaxed) & flag) != 0 + } + + pub fn set_flag(&self, flag: u8, val: bool) { + let current = self.flags.load(Ordering::Relaxed); + let new_flags = if val { current | flag } else { current & !flag }; + self.flags.store(new_flags, Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::mule::DATA_ID_FLAGS, + new_flags as i8, + )], + None, + ); + } + + #[must_use] + pub fn is_tame(&self) -> bool { + self.has_flag(FLAG_TAME) + } + + pub fn set_tame(&self, val: bool) { + self.set_flag(FLAG_TAME, val); + } + + #[must_use] + pub fn is_saddled(&self) -> bool { + self.has_flag(FLAG_SADDLE) + } + + pub fn set_saddled(&self, val: bool) { + self.set_flag(FLAG_SADDLE, val); + } + + #[must_use] + pub fn has_chest(&self) -> bool { + self.has_chest.load(Ordering::Relaxed) + } + + pub fn set_has_chest(&self, val: bool) { + self.has_chest.store(val, Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::mule::DATA_ID_CHEST, + val, + )], + None, + ); + } +} + +impl AgeableMob for MuleEntity { + fn get_ageable_data(&self) -> &AgeableData { + &self.ageable_data + } +} + +impl Animal for MuleEntity { + fn is_food(&self, item_stack: &ItemStack) -> bool { + item_stack.item.has_tag(&tag::Item::MINECRAFT_HORSE_FOOD) + || item_stack.item == &Item::WHEAT + || item_stack.item == &Item::SUGAR + || item_stack.item == &Item::HAY_BLOCK + || item_stack.item == &Item::APPLE + || item_stack.item == &Item::GOLDEN_CARROT + || item_stack.item == &Item::GOLDEN_APPLE + || item_stack.item == &Item::ENCHANTED_GOLDEN_APPLE + } } impl Mob for MuleEntity { + fn as_ageable(&self) -> Option<&dyn AgeableMob> { + Some(self) + } + + fn as_animal(&self) -> Option<&dyn Animal> { + Some(self) + } + + fn mob_write_nbt(&self, nbt: &mut NbtCompound) { + self.write_ageable_nbt(nbt); + nbt.put_bool("ChestedHorse", self.has_chest()); + nbt.put_bool("Tame", self.is_tame()); + nbt.put_int("Temper", self.temper.load(Ordering::Relaxed)); + if let Some(owner) = self.owner.load() { + nbt.put_uuid("Owner", owner); + } + } + + fn mob_read_nbt(&self, nbt: &NbtCompound) { + self.read_ageable_nbt(nbt); + if let Some(chested) = nbt.get_bool("ChestedHorse") { + self.set_has_chest(chested); + } + if let Some(tame) = nbt.get_bool("Tame") { + self.set_tame(tame); + } + if let Some(temper) = nbt.get_int("Temper") { + self.temper.store(temper, Ordering::Relaxed); + } + if let Some(owner) = nbt.get_uuid("Owner") { + self.owner.store(Some(owner)); + } + } + fn get_mob_entity(&self) -> &MobEntity { &self.mob_entity } + + fn mob_tick(&self, _caller: &dyn EntityBase) { + self.ageable_ai_step(); + } + + fn mob_init_data_tracker(&self) { + let entity = self.get_entity(); + let is_baby = entity.age.load(Ordering::Relaxed) < 0; + if is_baby { + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::mule::DATA_BABY_ID, + true, + )], + None, + ); + } + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::mule::DATA_ID_CHEST, + self.has_chest(), + )], + None, + ); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::mule::DATA_ID_FLAGS, + self.flags.load(Ordering::Relaxed) as i8, + )], + None, + ); + } + + fn mob_interact(&self, player: &Arc, item_stack: &mut ItemStack) -> bool { + let item = item_stack.get_item(); + + if self.is_tame() && item == &Item::CHEST && !self.has_chest() && !self.is_baby() { + self.set_has_chest(true); + item_stack.decrement_unless_creative(player.gamemode.load(), 1); + let entity = self.get_entity(); + let world = entity.world.load(); + world.play_sound( + Sound::EntityDonkeyChest, + SoundCategory::Neutral, + &entity.pos.load(), + ); + return true; + } + + if self.is_tame() && item == &Item::SADDLE && !self.is_saddled() && !self.is_baby() { + self.set_saddled(true); + item_stack.decrement_unless_creative(player.gamemode.load(), 1); + let entity = self.get_entity(); + let world = entity.world.load(); + world.play_sound( + Sound::EntityHorseSaddle, + SoundCategory::Neutral, + &entity.pos.load(), + ); + return true; + } + + if !self.is_baby() && !self.is_food(item_stack) { + let world = player.world(); + let ent = &self.mob_entity.living_entity.entity; + if let Some(vehicle) = world.get_entity_by_id(ent.entity_id) + && let Some(passenger) = world.get_player_by_id(player.entity_id()) + { + ent.add_passenger(vehicle, passenger as Arc); + return true; + } + } + + self.animal_interact(player, item_stack, Sound::EntityMuleAmbient) + } } diff --git a/crates/pumpkin/src/entity/passive/panda.rs b/crates/pumpkin/src/entity/passive/panda.rs index 259cd5cfd..6c0b4369d 100644 --- a/crates/pumpkin/src/entity/passive/panda.rs +++ b/crates/pumpkin/src/entity/passive/panda.rs @@ -1,27 +1,135 @@ -use std::sync::{Arc, Weak}; - -use pumpkin_data::entity::EntityType; - -use crate::entity::{ - Entity, - ai::goal::{ - look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal, - wander_around::WanderAroundGoal, - }, - mob::{Mob, MobEntity}, +use std::sync::{ + Arc, Weak, + atomic::{AtomicI32, AtomicU8, Ordering}, }; -/// Represents a Panda, a rare passive mob with various personalities. -/// -/// Wiki: +use pumpkin_data::entity::EntityType; +use pumpkin_data::item::Item; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::sound::Sound; +use pumpkin_data::tag::{self, Taggable}; +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_protocol::codec::var_int::VarInt; +use pumpkin_protocol::java::client::play::Metadata; +use rand::RngExt; + +use crate::entity::{ + Entity, EntityBase, + 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::BAMBOO]; + +pub const FLAG_SNEEZE: u8 = 2; +pub const FLAG_ROLL: u8 = 4; +pub const FLAG_SIT: u8 = 8; +pub const FLAG_ON_BACK: u8 = 16; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[repr(u8)] +pub enum PandaGene { + #[default] + Normal = 0, + Lazy = 1, + Worried = 2, + Playful = 3, + Brown = 4, + Weak = 5, + Aggressive = 6, +} + +impl PandaGene { + #[must_use] + pub const fn from_id(id: u8) -> Self { + match id { + 1 => Self::Lazy, + 2 => Self::Worried, + 3 => Self::Playful, + 4 => Self::Brown, + 5 => Self::Weak, + 6 => Self::Aggressive, + _ => Self::Normal, + } + } + + #[must_use] + pub const fn id(self) -> u8 { + self as u8 + } + + #[must_use] + pub const fn as_str(self) -> &'static str { + match self { + Self::Normal => "normal", + Self::Lazy => "lazy", + Self::Worried => "worried", + Self::Playful => "playful", + Self::Brown => "brown", + Self::Weak => "weak", + Self::Aggressive => "aggressive", + } + } + + #[must_use] + pub fn from_name(name: &str) -> Self { + match name { + "lazy" => Self::Lazy, + "worried" => Self::Worried, + "playful" => Self::Playful, + "brown" => Self::Brown, + "weak" => Self::Weak, + "aggressive" => Self::Aggressive, + _ => Self::Normal, + } + } + + #[must_use] + pub fn random_gene() -> Self { + let mut rng = rand::rng(); + if rng.random_range(0..16) == 0 { + Self::from_id(rng.random_range(4..=6)) + } else if rng.random_range(0..8) == 0 { + Self::from_id(rng.random_range(1..=3)) + } else { + Self::Normal + } + } +} + pub struct PandaEntity { pub mob_entity: MobEntity, + pub ageable_data: AgeableData, + pub main_gene: AtomicU8, + pub hidden_gene: AtomicU8, + pub flags: AtomicU8, + pub eat_counter: AtomicI32, + pub sneeze_counter: AtomicI32, + pub unhappy_counter: AtomicI32, } impl PandaEntity { pub fn new(entity: Entity) -> Arc { let mob_entity = MobEntity::new(entity); - let panda = Self { mob_entity }; + let gene = PandaGene::random_gene(); + let hidden = PandaGene::random_gene(); + let panda = Self { + mob_entity, + ageable_data: AgeableData::default(), + main_gene: AtomicU8::new(gene.id()), + hidden_gene: AtomicU8::new(hidden.id()), + flags: AtomicU8::new(0), + eat_counter: AtomicI32::new(0), + sneeze_counter: AtomicI32::new(0), + unhappy_counter: AtomicI32::new(0), + }; let mob_arc = Arc::new(panda); let mob_weak: Weak = { let mob_arc: Arc = mob_arc.clone(); @@ -36,20 +144,215 @@ impl PandaEntity { .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.2, TEMPT_ITEMS))); + goal_selector.add_goal(4, Box::new(FollowParentGoal::new(1.25))); + goal_selector.add_goal(6, Box::new(WanderAroundGoal::new(1.0))); goal_selector.add_goal( - 2, + 7, LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 6.0), ); - goal_selector.add_goal(3, Box::new(RandomLookAroundGoal::default())); + goal_selector.add_goal(8, Box::new(RandomLookAroundGoal::default())); }; mob_arc } + + #[must_use] + pub fn get_main_gene(&self) -> PandaGene { + PandaGene::from_id(self.main_gene.load(Ordering::Relaxed)) + } + + pub fn set_main_gene(&self, gene: PandaGene) { + self.main_gene.store(gene.id(), Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::panda::MAIN_GENE_ID, + gene.id() as i8, + )], + None, + ); + } + + #[must_use] + pub fn get_hidden_gene(&self) -> PandaGene { + PandaGene::from_id(self.hidden_gene.load(Ordering::Relaxed)) + } + + pub fn set_hidden_gene(&self, gene: PandaGene) { + self.hidden_gene.store(gene.id(), Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::panda::HIDDEN_GENE_ID, + gene.id() as i8, + )], + None, + ); + } + + #[must_use] + pub fn has_flag(&self, flag: u8) -> bool { + (self.flags.load(Ordering::Relaxed) & flag) != 0 + } + + pub fn set_flag(&self, flag: u8, val: bool) { + let current = self.flags.load(Ordering::Relaxed); + let new_flags = if val { current | flag } else { current & !flag }; + self.flags.store(new_flags, Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::panda::DATA_ID_FLAGS, + new_flags as i8, + )], + None, + ); + } + + #[must_use] + pub fn is_sneezing(&self) -> bool { + self.has_flag(FLAG_SNEEZE) + } + + pub fn set_sneezing(&self, val: bool) { + self.set_flag(FLAG_SNEEZE, val); + } + + #[must_use] + pub fn is_sitting(&self) -> bool { + self.has_flag(FLAG_SIT) + } + + pub fn set_sitting(&self, val: bool) { + self.set_flag(FLAG_SIT, val); + } + + #[must_use] + pub fn is_on_back(&self) -> bool { + self.has_flag(FLAG_ON_BACK) + } + + pub fn set_on_back(&self, val: bool) { + self.set_flag(FLAG_ON_BACK, val); + } + + #[must_use] + pub fn is_rolling(&self) -> bool { + self.has_flag(FLAG_ROLL) + } + + pub fn set_rolling(&self, val: bool) { + self.set_flag(FLAG_ROLL, val); + } +} + +impl AgeableMob for PandaEntity { + fn get_ageable_data(&self) -> &AgeableData { + &self.ageable_data + } +} + +impl Animal for PandaEntity { + fn is_food(&self, item_stack: &ItemStack) -> bool { + item_stack.item.has_tag(&tag::Item::MINECRAFT_PANDA_FOOD) + || item_stack.item == &Item::BAMBOO + || item_stack.item == &Item::CAKE + } } impl Mob for PandaEntity { + fn as_ageable(&self) -> Option<&dyn AgeableMob> { + Some(self) + } + + fn as_animal(&self) -> Option<&dyn Animal> { + Some(self) + } + + fn mob_write_nbt(&self, nbt: &mut NbtCompound) { + self.write_ageable_nbt(nbt); + nbt.put_string("MainGene", self.get_main_gene().as_str().to_string()); + nbt.put_string("HiddenGene", self.get_hidden_gene().as_str().to_string()); + } + + fn mob_read_nbt(&self, nbt: &NbtCompound) { + self.read_ageable_nbt(nbt); + if let Some(main) = nbt.get_string("MainGene") { + self.set_main_gene(PandaGene::from_name(main)); + } + if let Some(hidden) = nbt.get_string("HiddenGene") { + self.set_hidden_gene(PandaGene::from_name(hidden)); + } + } + fn get_mob_entity(&self) -> &MobEntity { &self.mob_entity } + + fn mob_tick(&self, _caller: &dyn EntityBase) { + self.ageable_ai_step(); + } + + fn mob_init_data_tracker(&self) { + let entity = self.get_entity(); + let is_baby = entity.age.load(Ordering::Relaxed) < 0; + if is_baby { + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::panda::DATA_BABY_ID, + true, + )], + None, + ); + } + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::panda::MAIN_GENE_ID, + self.main_gene.load(Ordering::Relaxed) as i8, + )], + None, + ); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::panda::HIDDEN_GENE_ID, + self.hidden_gene.load(Ordering::Relaxed) as i8, + )], + None, + ); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::panda::DATA_ID_FLAGS, + self.flags.load(Ordering::Relaxed) as i8, + )], + None, + ); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::panda::UNHAPPY_COUNTER, + VarInt(self.unhappy_counter.load(Ordering::Relaxed)), + )], + None, + ); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::panda::SNEEZE_COUNTER, + VarInt(self.sneeze_counter.load(Ordering::Relaxed)), + )], + None, + ); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::panda::EAT_COUNTER, + VarInt(self.eat_counter.load(Ordering::Relaxed)), + )], + None, + ); + } + + fn mob_interact(&self, player: &Arc, item_stack: &mut ItemStack) -> bool { + self.animal_interact(player, item_stack, Sound::EntityPandaAmbient) + } } diff --git a/crates/pumpkin/src/entity/passive/pig.rs b/crates/pumpkin/src/entity/passive/pig.rs index 4e9449fd7..9d43760e4 100644 --- a/crates/pumpkin/src/entity/passive/pig.rs +++ b/crates/pumpkin/src/entity/passive/pig.rs @@ -136,6 +136,23 @@ impl Mob for PigEntity { fn mob_interact(&self, player: &Arc, item_stack: &mut ItemStack) -> bool { use super::animal::Animal; + if item_stack.get_item() == &pumpkin_data::item::Item::SADDLE + && self.can_be_saddled() + && !self.is_saddled() + { + self.set_saddled(true); + item_stack.decrement_unless_creative(player.gamemode.load(), 1); + let entity = self.get_entity(); + let world = entity.world.load(); + let pos = entity.pos.load(); + world.play_sound( + Sound::EntityPigSaddle, + pumpkin_data::sound::SoundCategory::Neutral, + &pos, + ); + return true; + } + if self.is_saddled() && !self.is_food(item_stack) { let world = player.world(); if let Some(vehicle) = world.get_entity_by_id(self.get_entity().entity_id) diff --git a/crates/pumpkin/src/entity/passive/polar_bear.rs b/crates/pumpkin/src/entity/passive/polar_bear.rs index da46259c4..c87641c70 100644 --- a/crates/pumpkin/src/entity/passive/polar_bear.rs +++ b/crates/pumpkin/src/entity/passive/polar_bear.rs @@ -1,27 +1,42 @@ -use std::sync::{Arc, Weak}; - -use pumpkin_data::entity::EntityType; - -use crate::entity::{ - Entity, - ai::goal::{ - active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal, - look_at_entity::LookAtEntityGoal, swim::SwimGoal, wander_around::WanderAroundGoal, - }, - mob::{Mob, MobEntity}, +use std::sync::{ + Arc, Weak, + atomic::{AtomicBool, Ordering}, +}; + +use pumpkin_data::entity::EntityType; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::sound::Sound; +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_protocol::java::client::play::Metadata; + +use crate::entity::{ + Entity, EntityBase, + ageable::{AgeableData, AgeableMob}, + ai::goal::{ + active_target::ActiveTargetGoal, escape_danger::EscapeDangerGoal, + follow_parent::FollowParentGoal, look_around::RandomLookAroundGoal, + look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, revenge::RevengeGoal, + swim::SwimGoal, wander_around::WanderAroundGoal, + }, + mob::{Mob, MobEntity}, + passive::animal::Animal, + player::Player, }; -/// Represents a Polar Bear, a neutral mob found in cold biomes. -/// -/// Wiki: pub struct PolarBearEntity { pub mob_entity: MobEntity, + pub ageable_data: AgeableData, + pub standing: AtomicBool, } impl PolarBearEntity { pub fn new(entity: Entity) -> Arc { let mob_entity = MobEntity::new(entity); - let polar_bear = Self { mob_entity }; + let polar_bear = Self { + mob_entity, + ageable_data: AgeableData::default(), + standing: AtomicBool::new(false), + }; let mob_arc = Arc::new(polar_bear); let mob_weak: Weak = { let mob_arc: Arc = mob_arc.clone(); @@ -36,31 +51,115 @@ impl PolarBearEntity { .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(MeleeAttackGoal::new(1.25, true))); + goal_selector.add_goal(1, EscapeDangerGoal::new(2.0)); + goal_selector.add_goal(4, Box::new(FollowParentGoal::new(1.25))); + 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())); + }; + { let mut target_selector = mob_arc .mob_entity .target_selector .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); - // Polar bears are neutral but aggressive towards foxes + + target_selector.add_goal(1, Box::new(RevengeGoal::new(true))); target_selector.add_goal( - 1, + 2, + ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::PLAYER, false), + ); + target_selector.add_goal( + 3, ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::FOX, true), ); }; mob_arc } + + #[must_use] + pub fn is_standing(&self) -> bool { + self.standing.load(Ordering::Relaxed) + } + + pub fn set_standing(&self, standing: bool) { + self.standing.store(standing, Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::polar_bear::DATA_STANDING_ID, + standing, + )], + None, + ); + } +} + +impl AgeableMob for PolarBearEntity { + fn get_ageable_data(&self) -> &AgeableData { + &self.ageable_data + } +} + +impl Animal for PolarBearEntity { + fn is_food(&self, _item_stack: &ItemStack) -> bool { + false + } } impl Mob for PolarBearEntity { + fn as_ageable(&self) -> Option<&dyn AgeableMob> { + Some(self) + } + + fn as_animal(&self) -> Option<&dyn Animal> { + Some(self) + } + + fn mob_write_nbt(&self, nbt: &mut NbtCompound) { + self.write_ageable_nbt(nbt); + } + + fn mob_read_nbt(&self, nbt: &NbtCompound) { + self.read_ageable_nbt(nbt); + } + fn get_mob_entity(&self) -> &MobEntity { &self.mob_entity } + + fn mob_tick(&self, _caller: &dyn EntityBase) { + self.ageable_ai_step(); + } + + fn mob_init_data_tracker(&self) { + let entity = self.get_entity(); + let is_baby = entity.age.load(Ordering::Relaxed) < 0; + if is_baby { + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::polar_bear::DATA_BABY_ID, + true, + )], + None, + ); + } + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::polar_bear::DATA_STANDING_ID, + self.is_standing(), + )], + None, + ); + } + + fn mob_interact(&self, player: &Arc, item_stack: &mut ItemStack) -> bool { + self.animal_interact(player, item_stack, Sound::EntityPolarBearAmbient) + } } diff --git a/crates/pumpkin/src/entity/passive/rabbit.rs b/crates/pumpkin/src/entity/passive/rabbit.rs index df1ebc9f2..5a3256711 100644 --- a/crates/pumpkin/src/entity/passive/rabbit.rs +++ b/crates/pumpkin/src/entity/passive/rabbit.rs @@ -1,27 +1,98 @@ -use std::sync::{Arc, Weak}; +use std::sync::{ + Arc, Weak, + atomic::{AtomicI32, 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::sound::Sound; +use pumpkin_data::tag::{self, Taggable}; +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_protocol::codec::var_int::VarInt; +use pumpkin_protocol::java::client::play::Metadata; +use rand::RngExt; use crate::entity::{ - Entity, + Entity, EntityBase, + 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, + active_target::ActiveTargetGoal, avoid_entity::AvoidEntityGoal, breed::BreedGoal, + escape_danger::EscapeDangerGoal, follow_parent::FollowParentGoal, + look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, + melee_attack::MeleeAttackGoal, swim::SwimGoal, tempt::TemptGoal, + wander_around::WanderAroundGoal, }, mob::{Mob, MobEntity}, + passive::animal::Animal, + player::Player, }; const TEMPT_ITEMS: &[&Item] = &[&Item::CARROT, &Item::GOLDEN_CARROT, &Item::DANDELION]; +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[repr(i32)] +pub enum RabbitVariant { + #[default] + Brown = 0, + White = 1, + Black = 2, + WhiteSplotched = 3, + Gold = 4, + Salt = 5, + Evil = 99, +} + +impl RabbitVariant { + #[must_use] + pub const fn from_id(id: i32) -> Self { + match id { + 1 => Self::White, + 2 => Self::Black, + 3 => Self::WhiteSplotched, + 4 => Self::Gold, + 5 => Self::Salt, + 99 => Self::Evil, + _ => Self::Brown, + } + } + + #[must_use] + pub const fn id(self) -> i32 { + self as i32 + } + + #[must_use] + pub fn random_variant() -> Self { + let mut rng = rand::rng(); + match rng.random_range(0..6) { + 1 => Self::White, + 2 => Self::Black, + 3 => Self::WhiteSplotched, + 4 => Self::Gold, + 5 => Self::Salt, + _ => Self::Brown, + } + } +} + pub struct RabbitEntity { pub mob_entity: MobEntity, + pub ageable_data: AgeableData, + pub variant: AtomicI32, + pub more_carrot_ticks: AtomicI32, } impl RabbitEntity { pub fn new(entity: Entity) -> Arc { let mob_entity = MobEntity::new(entity); - let rabbit = Self { mob_entity }; + let variant = RabbitVariant::random_variant(); + let rabbit = Self { + mob_entity, + ageable_data: AgeableData::default(), + variant: AtomicI32::new(variant.id()), + more_carrot_ticks: AtomicI32::new(0), + }; let mob_arc = Arc::new(rabbit); let mob_weak: Weak = { let mob_arc: Arc = mob_arc.clone(); @@ -35,12 +106,24 @@ impl RabbitEntity { .lock() .unwrap_or_else(std::sync::PoisonError::into_inner); - goal_selector.add_goal(1, Box::new(SwimGoal::default())); + goal_selector.add_goal(0, Box::new(SwimGoal::default())); goal_selector.add_goal(1, EscapeDangerGoal::new(2.2)); goal_selector.add_goal(2, BreedGoal::new(0.8)); goal_selector.add_goal(3, Box::new(TemptGoal::new(1.0, TEMPT_ITEMS))); - goal_selector.add_goal(4, Box::new(FollowParentGoal::new(0.8))); - goal_selector.add_goal(5, Box::new(WanderAroundGoal::new(0.6))); + goal_selector.add_goal( + 4, + Box::new(AvoidEntityGoal::new(&EntityType::PLAYER, 8.0, 2.2, 2.2)), + ); + goal_selector.add_goal( + 4, + Box::new(AvoidEntityGoal::new(&EntityType::WOLF, 10.0, 2.2, 2.2)), + ); + goal_selector.add_goal( + 4, + Box::new(AvoidEntityGoal::new(&EntityType::FOX, 10.0, 2.2, 2.2)), + ); + goal_selector.add_goal(5, Box::new(FollowParentGoal::new(0.8))); + goal_selector.add_goal(6, Box::new(WanderAroundGoal::new(0.6))); goal_selector.add_goal( 11, LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 10.0), @@ -50,10 +133,123 @@ impl RabbitEntity { mob_arc } + + #[must_use] + pub fn get_variant(&self) -> RabbitVariant { + RabbitVariant::from_id(self.variant.load(Ordering::Relaxed)) + } + + pub fn set_variant(&self, variant: RabbitVariant) { + self.variant.store(variant.id(), Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::rabbit::DATA_TYPE_ID, + VarInt(variant.id()), + )], + None, + ); + + if variant == RabbitVariant::Evil { + let mut goal_selector = self + .mob_entity + .goals_selector + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + goal_selector.add_goal(4, Box::new(MeleeAttackGoal::new(1.4, true))); + + let mut target_selector = self + .mob_entity + .target_selector + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + target_selector.add_goal( + 1, + ActiveTargetGoal::with_default(&self.mob_entity, &EntityType::PLAYER, true), + ); + target_selector.add_goal( + 2, + ActiveTargetGoal::with_default(&self.mob_entity, &EntityType::WOLF, true), + ); + target_selector.add_goal( + 2, + ActiveTargetGoal::with_default(&self.mob_entity, &EntityType::FOX, true), + ); + } + } +} + +impl AgeableMob for RabbitEntity { + fn get_ageable_data(&self) -> &AgeableData { + &self.ageable_data + } +} + +impl Animal for RabbitEntity { + fn is_food(&self, item_stack: &ItemStack) -> bool { + item_stack.item.has_tag(&tag::Item::MINECRAFT_RABBIT_FOOD) + || TEMPT_ITEMS.iter().any(|i| i.id == item_stack.item.id) + } } impl Mob for RabbitEntity { + fn as_ageable(&self) -> Option<&dyn AgeableMob> { + Some(self) + } + + fn as_animal(&self) -> Option<&dyn Animal> { + Some(self) + } + + fn mob_write_nbt(&self, nbt: &mut NbtCompound) { + self.write_ageable_nbt(nbt); + nbt.put_int("RabbitType", self.get_variant().id()); + nbt.put_int( + "MoreCarrotTicks", + self.more_carrot_ticks.load(Ordering::Relaxed), + ); + } + + fn mob_read_nbt(&self, nbt: &NbtCompound) { + self.read_ageable_nbt(nbt); + if let Some(rabbit_type) = nbt.get_int("RabbitType") { + self.set_variant(RabbitVariant::from_id(rabbit_type)); + } + if let Some(ticks) = nbt.get_int("MoreCarrotTicks") { + self.more_carrot_ticks.store(ticks, Ordering::Relaxed); + } + } + fn get_mob_entity(&self) -> &MobEntity { &self.mob_entity } + + fn mob_tick(&self, _caller: &dyn EntityBase) { + self.ageable_ai_step(); + } + + fn mob_init_data_tracker(&self) { + let entity = self.get_entity(); + let is_baby = entity.age.load(Ordering::Relaxed) < 0; + if is_baby { + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::rabbit::DATA_BABY_ID, + true, + )], + None, + ); + } + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::rabbit::DATA_TYPE_ID, + VarInt(self.get_variant().id()), + )], + None, + ); + } + + fn mob_interact(&self, player: &Arc, item_stack: &mut ItemStack) -> bool { + self.animal_interact(player, item_stack, Sound::EntityRabbitAmbient) + } } diff --git a/crates/pumpkin/src/entity/passive/sheep.rs b/crates/pumpkin/src/entity/passive/sheep.rs index d3a6b7469..8fead6178 100644 --- a/crates/pumpkin/src/entity/passive/sheep.rs +++ b/crates/pumpkin/src/entity/passive/sheep.rs @@ -6,9 +6,10 @@ use std::sync::{ use pumpkin_data::{entity::EntityType, item::Item}; use pumpkin_nbt::compound::NbtCompound; use pumpkin_protocol::java::client::play::Metadata; +use rand::RngExt; use crate::entity::{ - Entity, + Entity, EntityBase, ageable::AgeableMob, ai::goal::{ breed::BreedGoal, eat_grass::EatGrassGoal, escape_danger::EscapeDangerGoal, @@ -157,7 +158,41 @@ impl Mob for SheepEntity { } fn mob_interact(&self, player: &Arc, item_stack: &mut ItemStack) -> bool { - use super::animal::Animal; + use super::animal::{Animal, get_dye_color_from_item, get_wool_item_for_color}; + let item = item_stack.get_item(); + + if item == &Item::SHEARS && !self.is_sheared() && !self.is_baby() { + self.set_sheared(true); + let entity = self.get_entity(); + let world = entity.world.load(); + let pos = entity.pos.load(); + world.play_sound( + Sound::EntitySheepShear, + pumpkin_data::sound::SoundCategory::Players, + &pos, + ); + + let wool_item = get_wool_item_for_color(self.get_color()); + let mut rng = rand::rng(); + let count = rng.random_range(1..=3); + let item_entity = Arc::new(crate::entity::item::ItemEntity::new( + Entity::new(world.clone(), pos, &EntityType::ITEM), + ItemStack::new(count, wool_item), + )); + world.spawn_entity(item_entity); + player.damage_held_item(1); + return true; + } + + if let Some(color) = get_dye_color_from_item(item) + && !self.is_sheared() + && color != self.get_color() + { + self.set_color(color); + item_stack.decrement_unless_creative(player.gamemode.load(), 1); + return true; + } + self.animal_interact(player, item_stack, Sound::EntitySheepAmbient) } } diff --git a/crates/pumpkin/src/entity/passive/skeleton_horse.rs b/crates/pumpkin/src/entity/passive/skeleton_horse.rs index 04abbcb73..a74833c04 100644 --- a/crates/pumpkin/src/entity/passive/skeleton_horse.rs +++ b/crates/pumpkin/src/entity/passive/skeleton_horse.rs @@ -1,24 +1,53 @@ -use std::sync::{Arc, Weak}; +use std::sync::{ + Arc, Weak, + atomic::{AtomicI32, 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::sound::{Sound, SoundCategory}; +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_protocol::java::client::play::Metadata; +use uuid::Uuid; use crate::entity::{ - Entity, + Entity, EntityBase, + ageable::{AgeableData, AgeableMob}, ai::goal::{ - look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal, - wander_around::WanderAroundGoal, + escape_danger::EscapeDangerGoal, look_around::RandomLookAroundGoal, + look_at_entity::LookAtEntityGoal, swim::SwimGoal, wander_around::WanderAroundGoal, }, mob::{Mob, MobEntity}, + passive::animal::Animal, + player::Player, }; +pub const FLAG_TAME: u8 = 2; +pub const FLAG_SADDLE: u8 = 4; +pub const FLAG_EATING: u8 = 16; +pub const FLAG_STANDING: u8 = 32; +pub const FLAG_OPEN_MOUTH: u8 = 64; + pub struct SkeletonHorseEntity { pub mob_entity: MobEntity, + pub ageable_data: AgeableData, + pub flags: AtomicU8, + pub temper: AtomicI32, + pub owner: AtomicCell>, } impl SkeletonHorseEntity { pub fn new(entity: Entity) -> Arc { let mob_entity = MobEntity::new(entity); - let horse = Self { mob_entity }; + let horse = Self { + mob_entity, + ageable_data: AgeableData::default(), + flags: AtomicU8::new(0), + temper: AtomicI32::new(0), + owner: AtomicCell::new(None), + }; let mob_arc = Arc::new(horse); let mob_weak: Weak = { let mob_arc: Arc = mob_arc.clone(); @@ -33,20 +62,155 @@ impl SkeletonHorseEntity { .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(0.7))); + goal_selector.add_goal(1, EscapeDangerGoal::new(1.2)); + goal_selector.add_goal(6, Box::new(WanderAroundGoal::new(0.7))); goal_selector.add_goal( - 2, + 7, LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 6.0), ); - goal_selector.add_goal(3, Box::new(RandomLookAroundGoal::default())); + goal_selector.add_goal(8, Box::new(RandomLookAroundGoal::default())); }; mob_arc } + + #[must_use] + pub fn has_flag(&self, flag: u8) -> bool { + (self.flags.load(Ordering::Relaxed) & flag) != 0 + } + + pub fn set_flag(&self, flag: u8, val: bool) { + let current = self.flags.load(Ordering::Relaxed); + let new_flags = if val { current | flag } else { current & !flag }; + self.flags.store(new_flags, Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::skeleton_horse::DATA_ID_FLAGS, + new_flags as i8, + )], + None, + ); + } + + #[must_use] + pub fn is_tame(&self) -> bool { + self.has_flag(FLAG_TAME) + } + + pub fn set_tame(&self, val: bool) { + self.set_flag(FLAG_TAME, val); + } + + #[must_use] + pub fn is_saddled(&self) -> bool { + self.has_flag(FLAG_SADDLE) + } + + pub fn set_saddled(&self, val: bool) { + self.set_flag(FLAG_SADDLE, val); + } +} + +impl AgeableMob for SkeletonHorseEntity { + fn get_ageable_data(&self) -> &AgeableData { + &self.ageable_data + } +} + +impl Animal for SkeletonHorseEntity { + fn is_food(&self, _item_stack: &ItemStack) -> bool { + false + } } impl Mob for SkeletonHorseEntity { + fn as_ageable(&self) -> Option<&dyn AgeableMob> { + Some(self) + } + + fn as_animal(&self) -> Option<&dyn Animal> { + Some(self) + } + + fn mob_write_nbt(&self, nbt: &mut NbtCompound) { + self.write_ageable_nbt(nbt); + nbt.put_bool("Tame", self.is_tame()); + nbt.put_int("Temper", self.temper.load(Ordering::Relaxed)); + if let Some(owner) = self.owner.load() { + nbt.put_uuid("Owner", owner); + } + } + + fn mob_read_nbt(&self, nbt: &NbtCompound) { + self.read_ageable_nbt(nbt); + if let Some(tame) = nbt.get_bool("Tame") { + self.set_tame(tame); + } + if let Some(temper) = nbt.get_int("Temper") { + self.temper.store(temper, Ordering::Relaxed); + } + if let Some(owner) = nbt.get_uuid("Owner") { + self.owner.store(Some(owner)); + } + } + fn get_mob_entity(&self) -> &MobEntity { &self.mob_entity } + + fn mob_tick(&self, _caller: &dyn EntityBase) { + self.ageable_ai_step(); + } + + fn mob_init_data_tracker(&self) { + let entity = self.get_entity(); + let is_baby = entity.age.load(Ordering::Relaxed) < 0; + if is_baby { + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::skeleton_horse::DATA_BABY_ID, + true, + )], + None, + ); + } + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::skeleton_horse::DATA_ID_FLAGS, + self.flags.load(Ordering::Relaxed) as i8, + )], + None, + ); + } + + fn mob_interact(&self, player: &Arc, item_stack: &mut ItemStack) -> bool { + let item = item_stack.get_item(); + + if self.is_tame() && item == &Item::SADDLE && !self.is_saddled() && !self.is_baby() { + self.set_saddled(true); + item_stack.decrement_unless_creative(player.gamemode.load(), 1); + let entity = self.get_entity(); + let world = entity.world.load(); + world.play_sound( + Sound::EntityHorseSaddle, + SoundCategory::Neutral, + &entity.pos.load(), + ); + return true; + } + + if !self.is_baby() { + let world = player.world(); + let ent = &self.mob_entity.living_entity.entity; + if let Some(vehicle) = world.get_entity_by_id(ent.entity_id) + && let Some(passenger) = world.get_player_by_id(player.entity_id()) + { + ent.add_passenger(vehicle, passenger as Arc); + return true; + } + } + + self.animal_interact(player, item_stack, Sound::EntitySkeletonHorseAmbient) + } } diff --git a/crates/pumpkin/src/entity/passive/snow_golem.rs b/crates/pumpkin/src/entity/passive/snow_golem.rs index 9bfbe2cce..904e17077 100644 --- a/crates/pumpkin/src/entity/passive/snow_golem.rs +++ b/crates/pumpkin/src/entity/passive/snow_golem.rs @@ -1,8 +1,12 @@ -use std::sync::atomic::Ordering; +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Weak}; use pumpkin_data::entity::EntityType; +use pumpkin_data::item::Item; +use pumpkin_data::item_stack::ItemStack; use pumpkin_data::sound::{Sound, SoundCategory}; +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_protocol::java::client::play::Metadata; use crate::entity::{ Entity, EntityBase, @@ -11,18 +15,24 @@ use crate::entity::{ look_at_entity::LookAtEntityGoal, ranged_attack::RangedAttackGoal, wander_around::WanderAroundGoal, }, + item::ItemEntity, mob::{Mob, MobEntity, RangedAttackMob}, + player::Player, projectile::snowball::SnowballEntity, }; pub struct SnowGolemEntity { pub mob_entity: MobEntity, + pub has_pumpkin: AtomicBool, } impl SnowGolemEntity { pub fn new(entity: Entity) -> Arc { let mob_entity = MobEntity::new(entity); - let snow_golem = Self { mob_entity }; + let snow_golem = Self { + mob_entity, + has_pumpkin: AtomicBool::new(true), + }; let mob_arc = Arc::new(snow_golem); let mob_weak: Weak = { let mob_arc: Arc = mob_arc.clone(); @@ -65,6 +75,24 @@ impl SnowGolemEntity { mob_arc } + #[must_use] + pub fn has_pumpkin(&self) -> bool { + self.has_pumpkin.load(Ordering::Relaxed) + } + + pub fn set_has_pumpkin(&self, has_pumpkin: bool) { + self.has_pumpkin.store(has_pumpkin, Ordering::Relaxed); + let entity = self.get_entity(); + let flags = if has_pumpkin { 16u8 } else { 0u8 }; + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::snow_golem::DATA_PUMPKIN_ID, + flags as i8, + )], + None, + ); + } + pub fn throw_snowball(&self, target: &Arc) { let entity = self.get_entity(); let world = entity.world.load_full(); @@ -95,9 +123,49 @@ impl SnowGolemEntity { } impl Mob for SnowGolemEntity { + fn mob_write_nbt(&self, nbt: &mut NbtCompound) { + nbt.put_bool("Pumpkin", self.has_pumpkin()); + } + + fn mob_read_nbt(&self, nbt: &NbtCompound) { + if let Some(pumpkin) = nbt.get_bool("Pumpkin") { + self.set_has_pumpkin(pumpkin); + } + } + fn get_mob_entity(&self) -> &MobEntity { &self.mob_entity } + + fn mob_init_data_tracker(&self) { + let entity = self.get_entity(); + let flags = if self.has_pumpkin() { 16u8 } else { 0u8 }; + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::snow_golem::DATA_PUMPKIN_ID, + flags as i8, + )], + None, + ); + } + + fn mob_interact(&self, player: &Arc, item_stack: &mut ItemStack) -> bool { + if item_stack.get_item() == &Item::SHEARS && self.has_pumpkin() { + self.set_has_pumpkin(false); + let entity = self.get_entity(); + let world = entity.world.load(); + let pos = entity.pos.load(); + world.play_sound(Sound::EntitySnowGolemShear, SoundCategory::Players, &pos); + let item_entity = Arc::new(ItemEntity::new( + Entity::new(world.clone(), pos, &EntityType::ITEM), + ItemStack::new(1, &Item::CARVED_PUMPKIN), + )); + world.spawn_entity(item_entity); + player.damage_held_item(1); + return true; + } + false + } } impl RangedAttackMob for SnowGolemEntity { diff --git a/crates/pumpkin/src/entity/passive/strider.rs b/crates/pumpkin/src/entity/passive/strider.rs index 46af7efad..947109700 100644 --- a/crates/pumpkin/src/entity/passive/strider.rs +++ b/crates/pumpkin/src/entity/passive/strider.rs @@ -1,22 +1,36 @@ +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Weak}; use pumpkin_data::entity::EntityType; +use pumpkin_data::item::Item; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::sound::{Sound, SoundCategory}; +use pumpkin_data::tag::{self, Taggable}; +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_protocol::java::client::play::Metadata; use crate::entity::{ Entity, EntityBase, + 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_steerable::{ItemBasedSteering, ItemSteerable}, mob::{Mob, MobEntity}, + passive::animal::Animal, + player::Player, }; -use crate::entity::item_steerable::{ItemBasedSteering, ItemSteerable}; +const TEMPT_ITEMS: &[&Item] = &[&Item::WARPED_FUNGUS, &Item::WARPED_FUNGUS_ON_A_STICK]; pub struct StriderEntity { pub mob_entity: MobEntity, + pub ageable_data: AgeableData, pub steering: ItemBasedSteering, - pub saddled: std::sync::atomic::AtomicBool, + pub saddled: AtomicBool, + pub suffocating: AtomicBool, } impl StriderEntity { @@ -24,8 +38,10 @@ impl StriderEntity { let mob_entity = MobEntity::new(entity); let strider = Self { mob_entity, + ageable_data: AgeableData::default(), steering: ItemBasedSteering::default(), - saddled: std::sync::atomic::AtomicBool::new(false), + saddled: AtomicBool::new(false), + suffocating: AtomicBool::new(false), }; let mob_arc = Arc::new(strider); let mob_weak: Weak = { @@ -41,26 +57,72 @@ impl StriderEntity { .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(1.65)); + goal_selector.add_goal(2, BreedGoal::new(1.0)); + goal_selector.add_goal(3, Box::new(TemptGoal::new(1.4, TEMPT_ITEMS))); + 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 is_suffocating(&self) -> bool { + self.suffocating.load(Ordering::Relaxed) + } + + pub fn set_suffocating(&self, suffocating: bool) { + self.suffocating.store(suffocating, Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::strider::DATA_SUFFOCATING, + suffocating, + )], + None, + ); + } +} + +impl AgeableMob for StriderEntity { + fn get_ageable_data(&self) -> &AgeableData { + &self.ageable_data + } +} + +impl Animal for StriderEntity { + fn is_food(&self, item_stack: &ItemStack) -> bool { + item_stack.item.has_tag(&tag::Item::MINECRAFT_STRIDER_FOOD) + || item_stack.item == &Item::WARPED_FUNGUS + } } impl Mob for StriderEntity { - fn mob_write_nbt(&self, nbt: &mut pumpkin_nbt::compound::NbtCompound) { + fn as_ageable(&self) -> Option<&dyn AgeableMob> { + Some(self) + } + + fn as_animal(&self) -> Option<&dyn Animal> { + Some(self) + } + + fn mob_write_nbt(&self, nbt: &mut NbtCompound) { + self.write_ageable_nbt(nbt); nbt.put_bool("Saddle", self.is_saddled()); } - fn mob_read_nbt(&self, nbt: &pumpkin_nbt::compound::NbtCompound) { - if let Some(saddle) = nbt.get_byte("Saddle") { - self.set_saddled(saddle == 1); + fn mob_read_nbt(&self, nbt: &NbtCompound) { + self.read_ageable_nbt(nbt); + if let Some(saddle) = nbt.get_bool("Saddle") { + self.set_saddled(saddle); + } else if let Some(saddle_byte) = nbt.get_byte("Saddle") { + self.set_saddled(saddle_byte == 1); } } @@ -73,24 +135,56 @@ impl Mob for StriderEntity { } fn is_saddled(&self) -> bool { - self.saddled.load(std::sync::atomic::Ordering::Relaxed) + self.saddled.load(Ordering::Relaxed) } fn can_be_saddled(&self) -> bool { - self.mob_entity.living_entity.entity.is_alive() + self.mob_entity.living_entity.entity.is_alive() && !self.is_baby() } fn set_saddled(&self, saddled: bool) { - self.saddled - .store(saddled, std::sync::atomic::Ordering::Relaxed); + self.saddled.store(saddled, Ordering::Relaxed); } - fn mob_interact( - &self, - player: &Arc, - _item_stack: &mut pumpkin_data::item_stack::ItemStack, - ) -> bool { - if self.is_saddled() { + fn mob_tick(&self, _caller: &dyn EntityBase) { + self.ageable_ai_step(); + } + + fn mob_init_data_tracker(&self) { + let entity = self.get_entity(); + let is_baby = entity.age.load(Ordering::Relaxed) < 0; + if is_baby { + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::strider::DATA_BABY_ID, + true, + )], + None, + ); + } + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::strider::DATA_SUFFOCATING, + self.is_suffocating(), + )], + None, + ); + } + + fn mob_interact(&self, player: &Arc, item_stack: &mut ItemStack) -> bool { + let item = item_stack.get_item(); + + if item == &Item::SADDLE && self.can_be_saddled() && !self.is_saddled() { + self.set_saddled(true); + item_stack.decrement_unless_creative(player.gamemode.load(), 1); + let entity = self.get_entity(); + let world = entity.world.load(); + let pos = entity.pos.load(); + world.play_sound(Sound::EntityStriderSaddle, SoundCategory::Neutral, &pos); + return true; + } + + if self.is_saddled() && !self.is_food(item_stack) { let world = player.world(); let ent = &self.mob_entity.living_entity.entity; if let Some(vehicle) = world.get_entity_by_id(ent.entity_id) @@ -100,7 +194,8 @@ impl Mob for StriderEntity { return true; } } - false + + self.animal_interact(player, item_stack, Sound::EntityStriderAmbient) } } diff --git a/crates/pumpkin/src/entity/passive/tadpole.rs b/crates/pumpkin/src/entity/passive/tadpole.rs index 5329291dd..a42980470 100644 --- a/crates/pumpkin/src/entity/passive/tadpole.rs +++ b/crates/pumpkin/src/entity/passive/tadpole.rs @@ -1,24 +1,40 @@ use std::sync::{Arc, Weak}; use pumpkin_data::entity::EntityType; +use pumpkin_data::item::Item; +use pumpkin_data::item_stack::ItemStack; +use pumpkin_data::particle::Particle; +use pumpkin_data::sound::{Sound, SoundCategory}; +use pumpkin_data::tag::{self, Taggable}; +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_util::math::vector3::Vector3; use crate::entity::{ - Entity, + Entity, EntityBase, + ageable::{AgeableData, AgeableMob}, ai::goal::{ - look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal, + escape_danger::EscapeDangerGoal, look_around::RandomLookAroundGoal, + look_at_entity::LookAtEntityGoal, swim::SwimGoal, tempt::TemptGoal, try_find_water::TryFindWaterGoal, wander_around::WanderAroundGoal, }, mob::{Mob, MobEntity}, + player::Player, }; +const TEMPT_ITEMS: &[&Item] = &[&Item::SLIME_BALL]; + pub struct TadpoleEntity { pub mob_entity: MobEntity, + pub ageable_data: AgeableData, } impl TadpoleEntity { pub fn new(entity: Entity) -> Arc { let mob_entity = MobEntity::new(entity); - let tadpole = Self { mob_entity }; + let tadpole = Self { + mob_entity, + ageable_data: AgeableData::default(), + }; let mob_arc = Arc::new(tadpole); let mob_weak: Weak = { let mob_arc: Arc = mob_arc.clone(); @@ -34,20 +50,92 @@ impl TadpoleEntity { goal_selector.add_goal(0, Box::new(TryFindWaterGoal)); 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(1.5)); + goal_selector.add_goal(2, Box::new(TemptGoal::new(1.25, TEMPT_ITEMS))); + goal_selector.add_goal(3, Box::new(WanderAroundGoal::new(1.0))); goal_selector.add_goal( - 2, + 4, LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 6.0), ); - goal_selector.add_goal(3, Box::new(RandomLookAroundGoal::default())); + goal_selector.add_goal(5, Box::new(RandomLookAroundGoal::default())); }; mob_arc } + + fn is_food(item_stack: &ItemStack) -> bool { + item_stack.item.has_tag(&tag::Item::MINECRAFT_FROG_FOOD) + || item_stack.item == &Item::SLIME_BALL + } +} + +impl AgeableMob for TadpoleEntity { + fn get_ageable_data(&self) -> &AgeableData { + &self.ageable_data + } } impl Mob for TadpoleEntity { + fn as_ageable(&self) -> Option<&dyn AgeableMob> { + Some(self) + } + + fn mob_write_nbt(&self, nbt: &mut NbtCompound) { + self.write_ageable_nbt(nbt); + } + + fn mob_read_nbt(&self, nbt: &NbtCompound) { + self.read_ageable_nbt(nbt); + } + fn get_mob_entity(&self) -> &MobEntity { &self.mob_entity } + + fn mob_tick(&self, _caller: &dyn EntityBase) { + self.ageable_ai_step(); + } + + fn mob_interact(&self, player: &Arc, item_stack: &mut ItemStack) -> bool { + if Self::is_food(item_stack) { + item_stack.decrement_unless_creative(player.gamemode.load(), 1); + let age = self + .get_mob_entity() + .living_entity + .entity + .age + .load(std::sync::atomic::Ordering::Relaxed); + let speedup = (-age / 10).max(1); + self.get_mob_entity() + .living_entity + .entity + .age + .fetch_add(speedup, std::sync::atomic::Ordering::Relaxed); + + let entity = self.get_entity(); + let world = entity.world.load(); + let pos = entity.pos.load(); + world.spawn_particle( + pos + Vector3::new(0.0, f64::from(entity.height()), 0.0), + Vector3::new(0.5, 0.5, 0.5), + 1.0, + 7, + Particle::HappyVillager, + ); + world.play_sound(Sound::EntityTadpoleGrowUp, SoundCategory::Neutral, &pos); + return true; + } + + if item_stack.get_item() == &Item::WATER_BUCKET { + item_stack.decrement_unless_creative(player.gamemode.load(), 1); + let entity = self.get_entity(); + let world = entity.world.load(); + let pos = entity.pos.load(); + world.play_sound(Sound::ItemBucketFillTadpole, SoundCategory::Neutral, &pos); + entity.remove(); + return true; + } + + false + } } diff --git a/crates/pumpkin/src/entity/passive/trader_llama.rs b/crates/pumpkin/src/entity/passive/trader_llama.rs index fcbfd449f..302066da4 100644 --- a/crates/pumpkin/src/entity/passive/trader_llama.rs +++ b/crates/pumpkin/src/entity/passive/trader_llama.rs @@ -1,28 +1,111 @@ -use std::sync::atomic::Ordering; -use std::sync::{Arc, Weak}; +use std::sync::{ + Arc, Weak, + atomic::{AtomicBool, AtomicI32, 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::sound::{Sound, SoundCategory}; +use pumpkin_data::tag::{self, Taggable}; +use pumpkin_nbt::compound::NbtCompound; +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, + ageable::{AgeableData, AgeableMob}, ai::goal::{ - active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal, + active_target::ActiveTargetGoal, breed::BreedGoal, escape_danger::EscapeDangerGoal, + follow_parent::FollowParentGoal, look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, ranged_attack::RangedAttackGoal, revenge::RevengeGoal, - swim::SwimGoal, wander_around::WanderAroundGoal, + swim::SwimGoal, tempt::TemptGoal, wander_around::WanderAroundGoal, }, mob::{Mob, MobEntity, RangedAttackMob}, + passive::animal::{Animal, get_carpet_color_from_item}, + player::Player, projectile::llama_spit::LlamaSpitEntity, }; +const TEMPT_ITEMS: &[&Item] = &[&Item::HAY_BLOCK]; + +pub const FLAG_TAME: u8 = 2; +pub const FLAG_BRED: u8 = 8; +pub const FLAG_EATING: u8 = 16; +pub const FLAG_STANDING: u8 = 32; +pub const FLAG_OPEN_MOUTH: u8 = 64; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[repr(i32)] +pub enum TraderLlamaVariant { + #[default] + Creamy = 0, + White = 1, + Brown = 2, + Gray = 3, +} + +impl TraderLlamaVariant { + #[must_use] + pub const fn from_id(id: i32) -> Self { + match id { + 1 => Self::White, + 2 => Self::Brown, + 3 => Self::Gray, + _ => Self::Creamy, + } + } + + #[must_use] + pub const fn id(self) -> i32 { + self as i32 + } + + #[must_use] + pub fn random_variant() -> Self { + let mut rng = rand::rng(); + match rng.random_range(0..4) { + 1 => Self::White, + 2 => Self::Brown, + 3 => Self::Gray, + _ => Self::Creamy, + } + } +} + pub struct TraderLlamaEntity { pub mob_entity: MobEntity, + pub ageable_data: AgeableData, + pub variant: AtomicI32, + pub strength: AtomicI32, + pub carpet_color: AtomicCell>, + pub flags: AtomicU8, + pub has_chest: AtomicBool, + pub temper: AtomicI32, + pub owner: AtomicCell>, } impl TraderLlamaEntity { pub fn new(entity: Entity) -> Arc { let mob_entity = MobEntity::new(entity); - let llama = Self { mob_entity }; + let mut rng = rand::rng(); + let variant = TraderLlamaVariant::random_variant(); + let strength = rng.random_range(1..=5); + + let llama = Self { + mob_entity, + ageable_data: AgeableData::default(), + variant: AtomicI32::new(variant.id()), + strength: AtomicI32::new(strength), + carpet_color: AtomicCell::new(None), + flags: AtomicU8::new(0), + has_chest: AtomicBool::new(false), + temper: AtomicI32::new(0), + owner: AtomicCell::new(None), + }; let mob_arc = Arc::new(llama); let mob_weak: Weak = { let mob_arc: Arc = mob_arc.clone(); @@ -46,10 +129,14 @@ impl TraderLlamaEntity { .unwrap_or_else(std::sync::PoisonError::into_inner); goal_selector.add_goal(0, Box::new(SwimGoal::default())); + goal_selector.add_goal(1, EscapeDangerGoal::new(1.2)); + goal_selector.add_goal(2, BreedGoal::new(1.0)); goal_selector.add_goal( 3, Box::new(RangedAttackGoal::new(ranged_weak, 1.25, 40, 20.0)), ); + goal_selector.add_goal(4, Box::new(TemptGoal::new(1.25, TEMPT_ITEMS))); + goal_selector.add_goal(5, Box::new(FollowParentGoal::new(1.0))); goal_selector.add_goal(7, Box::new(WanderAroundGoal::new(0.7))); goal_selector.add_goal( 8, @@ -67,6 +154,85 @@ impl TraderLlamaEntity { mob_arc } + #[must_use] + pub fn get_variant(&self) -> TraderLlamaVariant { + TraderLlamaVariant::from_id(self.variant.load(Ordering::Relaxed)) + } + + pub fn set_variant(&self, variant: TraderLlamaVariant) { + self.variant.store(variant.id(), Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::trader_llama::DATA_VARIANT_ID, + VarInt(variant.id()), + )], + None, + ); + } + + #[must_use] + pub fn get_strength(&self) -> i32 { + self.strength.load(Ordering::Relaxed) + } + + pub fn set_strength(&self, strength: i32) { + self.strength.store(strength, Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::trader_llama::DATA_STRENGTH_ID, + VarInt(strength), + )], + None, + ); + } + + #[must_use] + pub fn has_flag(&self, flag: u8) -> bool { + (self.flags.load(Ordering::Relaxed) & flag) != 0 + } + + pub fn set_flag(&self, flag: u8, val: bool) { + let current = self.flags.load(Ordering::Relaxed); + let new_flags = if val { current | flag } else { current & !flag }; + self.flags.store(new_flags, Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::trader_llama::DATA_ID_FLAGS, + new_flags as i8, + )], + None, + ); + } + + #[must_use] + pub fn is_tame(&self) -> bool { + self.has_flag(FLAG_TAME) + } + + pub fn set_tame(&self, val: bool) { + self.set_flag(FLAG_TAME, val); + } + + #[must_use] + pub fn has_chest(&self) -> bool { + self.has_chest.load(Ordering::Relaxed) + } + + pub fn set_has_chest(&self, val: bool) { + self.has_chest.store(val, Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::trader_llama::DATA_ID_CHEST, + val, + )], + None, + ); + } + pub fn spit(&self, target: &Arc) { let entity = self.get_entity(); let world = entity.world.load_full(); @@ -96,10 +262,161 @@ impl TraderLlamaEntity { } } +impl AgeableMob for TraderLlamaEntity { + fn get_ageable_data(&self) -> &AgeableData { + &self.ageable_data + } +} + +impl Animal for TraderLlamaEntity { + fn is_food(&self, item_stack: &ItemStack) -> bool { + item_stack.item.has_tag(&tag::Item::MINECRAFT_LLAMA_FOOD) + || item_stack + .item + .has_tag(&tag::Item::MINECRAFT_LLAMA_TEMPT_ITEMS) + || item_stack.item == &Item::WHEAT + || item_stack.item == &Item::HAY_BLOCK + } +} + impl Mob for TraderLlamaEntity { + fn as_ageable(&self) -> Option<&dyn AgeableMob> { + Some(self) + } + + fn as_animal(&self) -> Option<&dyn Animal> { + Some(self) + } + + fn mob_write_nbt(&self, nbt: &mut NbtCompound) { + self.write_ageable_nbt(nbt); + nbt.put_int("Variant", self.get_variant().id()); + nbt.put_int("Strength", self.get_strength()); + nbt.put_bool("ChestedHorse", self.has_chest()); + nbt.put_bool("Tame", self.is_tame()); + nbt.put_int("Temper", self.temper.load(Ordering::Relaxed)); + if let Some(owner) = self.owner.load() { + nbt.put_uuid("Owner", owner); + } + } + + fn mob_read_nbt(&self, nbt: &NbtCompound) { + self.read_ageable_nbt(nbt); + if let Some(variant) = nbt.get_int("Variant") { + self.set_variant(TraderLlamaVariant::from_id(variant)); + } + if let Some(strength) = nbt.get_int("Strength") { + self.set_strength(strength); + } + if let Some(chested) = nbt.get_bool("ChestedHorse") { + self.set_has_chest(chested); + } + if let Some(tame) = nbt.get_bool("Tame") { + self.set_tame(tame); + } + if let Some(temper) = nbt.get_int("Temper") { + self.temper.store(temper, Ordering::Relaxed); + } + if let Some(owner) = nbt.get_uuid("Owner") { + self.owner.store(Some(owner)); + } + } + fn get_mob_entity(&self) -> &MobEntity { &self.mob_entity } + + fn mob_tick(&self, _caller: &dyn EntityBase) { + self.ageable_ai_step(); + } + + fn mob_init_data_tracker(&self) { + let entity = self.get_entity(); + let is_baby = entity.age.load(Ordering::Relaxed) < 0; + if is_baby { + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::trader_llama::DATA_BABY_ID, + true, + )], + None, + ); + } + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::trader_llama::DATA_VARIANT_ID, + VarInt(self.get_variant().id()), + )], + None, + ); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::trader_llama::DATA_STRENGTH_ID, + VarInt(self.get_strength()), + )], + None, + ); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::trader_llama::DATA_ID_CHEST, + self.has_chest(), + )], + None, + ); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::trader_llama::DATA_ID_FLAGS, + self.flags.load(Ordering::Relaxed) as i8, + )], + None, + ); + } + + fn mob_interact(&self, player: &Arc, item_stack: &mut ItemStack) -> bool { + let item = item_stack.get_item(); + + if self.is_tame() && item == &Item::CHEST && !self.has_chest() && !self.is_baby() { + self.set_has_chest(true); + item_stack.decrement_unless_creative(player.gamemode.load(), 1); + let entity = self.get_entity(); + let world = entity.world.load(); + world.play_sound( + Sound::EntityDonkeyChest, + SoundCategory::Neutral, + &entity.pos.load(), + ); + return true; + } + + if self.is_tame() + && !self.is_baby() + && let Some(color) = get_carpet_color_from_item(item) + { + self.carpet_color.store(Some(color)); + item_stack.decrement_unless_creative(player.gamemode.load(), 1); + let entity = self.get_entity(); + let world = entity.world.load(); + world.play_sound( + Sound::EntityLlamaSwag, + SoundCategory::Neutral, + &entity.pos.load(), + ); + return true; + } + + if !self.is_baby() && !self.is_food(item_stack) { + let world = player.world(); + let ent = &self.mob_entity.living_entity.entity; + if let Some(vehicle) = world.get_entity_by_id(ent.entity_id) + && let Some(passenger) = world.get_player_by_id(player.entity_id()) + { + ent.add_passenger(vehicle, passenger as Arc); + return true; + } + } + + self.animal_interact(player, item_stack, Sound::EntityLlamaAmbient) + } } impl RangedAttackMob for TraderLlamaEntity { diff --git a/crates/pumpkin/src/entity/passive/turtle.rs b/crates/pumpkin/src/entity/passive/turtle.rs index aec7dedec..03e3cc773 100644 --- a/crates/pumpkin/src/entity/passive/turtle.rs +++ b/crates/pumpkin/src/entity/passive/turtle.rs @@ -1,27 +1,47 @@ -use std::sync::{Arc, Weak}; +use std::sync::{ + Arc, Weak, + atomic::{AtomicBool, 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::sound::Sound; +use pumpkin_data::tag::{self, Taggable}; +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_protocol::java::client::play::Metadata; use crate::entity::{ - Entity, + Entity, EntityBase, + ageable::{AgeableData, AgeableMob}, ai::goal::{ - breed::BreedGoal, look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, - swim::SwimGoal, tempt::TemptGoal, try_find_water::TryFindWaterGoal, - wander_around::WanderAroundGoal, + breed::BreedGoal, escape_danger::EscapeDangerGoal, look_around::RandomLookAroundGoal, + look_at_entity::LookAtEntityGoal, swim::SwimGoal, tempt::TemptGoal, + try_find_water::TryFindWaterGoal, wander_around::WanderAroundGoal, }, mob::{Mob, MobEntity}, + passive::animal::Animal, + player::Player, }; const TEMPT_ITEMS: &[&Item] = &[&Item::SEAGRASS]; pub struct TurtleEntity { pub mob_entity: MobEntity, + pub ageable_data: AgeableData, + pub has_egg: AtomicBool, + pub laying_egg: AtomicBool, } impl TurtleEntity { pub fn new(entity: Entity) -> Arc { let mob_entity = MobEntity::new(entity); - let turtle = Self { mob_entity }; + let turtle = Self { + mob_entity, + ageable_data: AgeableData::default(), + has_egg: AtomicBool::new(false), + laying_egg: AtomicBool::new(false), + }; let mob_arc = Arc::new(turtle); let mob_weak: Weak = { let mob_arc: Arc = mob_arc.clone(); @@ -37,8 +57,9 @@ impl TurtleEntity { goal_selector.add_goal(0, Box::new(TryFindWaterGoal)); goal_selector.add_goal(1, Box::new(SwimGoal::default())); - goal_selector.add_goal(2, BreedGoal::new(1.0)); - goal_selector.add_goal(3, Box::new(TemptGoal::new(1.1, TEMPT_ITEMS))); + goal_selector.add_goal(2, EscapeDangerGoal::new(1.2)); + goal_selector.add_goal(3, BreedGoal::new(1.0)); + goal_selector.add_goal(4, Box::new(TemptGoal::new(1.1, TEMPT_ITEMS))); goal_selector.add_goal(5, Box::new(WanderAroundGoal::new(1.0))); goal_selector.add_goal( 6, @@ -49,10 +70,113 @@ impl TurtleEntity { mob_arc } + + #[must_use] + pub fn has_egg(&self) -> bool { + self.has_egg.load(Ordering::Relaxed) + } + + pub fn set_has_egg(&self, has_egg: bool) { + self.has_egg.store(has_egg, Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::turtle::HAS_EGG, + has_egg, + )], + None, + ); + } + + #[must_use] + pub fn is_laying_egg(&self) -> bool { + self.laying_egg.load(Ordering::Relaxed) + } + + pub fn set_laying_egg(&self, laying_egg: bool) { + self.laying_egg.store(laying_egg, Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::turtle::LAYING_EGG, + laying_egg, + )], + None, + ); + } +} + +impl AgeableMob for TurtleEntity { + fn get_ageable_data(&self) -> &AgeableData { + &self.ageable_data + } +} + +impl Animal for TurtleEntity { + fn is_food(&self, item_stack: &ItemStack) -> bool { + item_stack.item.has_tag(&tag::Item::MINECRAFT_TURTLE_FOOD) + || item_stack.item == &Item::SEAGRASS + } } impl Mob for TurtleEntity { + fn as_ageable(&self) -> Option<&dyn AgeableMob> { + Some(self) + } + + fn as_animal(&self) -> Option<&dyn Animal> { + Some(self) + } + + fn mob_write_nbt(&self, nbt: &mut NbtCompound) { + self.write_ageable_nbt(nbt); + nbt.put_bool("HasEgg", self.has_egg()); + } + + fn mob_read_nbt(&self, nbt: &NbtCompound) { + self.read_ageable_nbt(nbt); + if let Some(has_egg) = nbt.get_bool("HasEgg") { + self.set_has_egg(has_egg); + } + } + fn get_mob_entity(&self) -> &MobEntity { &self.mob_entity } + + fn mob_tick(&self, _caller: &dyn EntityBase) { + self.ageable_ai_step(); + } + + fn mob_init_data_tracker(&self) { + let entity = self.get_entity(); + let is_baby = entity.age.load(Ordering::Relaxed) < 0; + if is_baby { + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::turtle::DATA_BABY_ID, + true, + )], + None, + ); + } + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::turtle::HAS_EGG, + self.has_egg(), + )], + None, + ); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::turtle::LAYING_EGG, + self.is_laying_egg(), + )], + None, + ); + } + + fn mob_interact(&self, player: &Arc, item_stack: &mut ItemStack) -> bool { + self.animal_interact(player, item_stack, Sound::EntityTurtleAmbientLand) + } } diff --git a/crates/pumpkin/src/entity/passive/wolf.rs b/crates/pumpkin/src/entity/passive/wolf.rs index e5adbf286..f0670e7bd 100644 --- a/crates/pumpkin/src/entity/passive/wolf.rs +++ b/crates/pumpkin/src/entity/passive/wolf.rs @@ -10,6 +10,7 @@ use pumpkin_data::tag::{self, Taggable}; use pumpkin_nbt::compound::NbtCompound; use pumpkin_protocol::codec::var_int::VarInt; use pumpkin_protocol::java::client::play::Metadata; +use rand::RngExt; use crate::entity::{ Entity, EntityBase, @@ -27,6 +28,7 @@ use crate::entity::{ animal::Animal, tamable::{TamableAnimal, TamableData}, }, + player::Player, }; pub struct WolfEntity { @@ -327,6 +329,59 @@ impl Mob for WolfEntity { None, ); } + + fn mob_interact(&self, player: &Arc, item_stack: &mut ItemStack) -> bool { + let item = item_stack.get_item(); + if self.is_tame() { + if self.is_food(item_stack) + && 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(pumpkin_data::sound::Sound::EntityWolfAmbient); + return true; + } + + if self.is_owned_by(&player.gameprofile.id) { + if let Some(color) = super::animal::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; + } + + let parent_interaction = self.animal_interact( + player, + item_stack, + pumpkin_data::sound::Sound::EntityWolfAmbient, + ); + if !parent_interaction { + self.set_ordered_to_sit(!self.is_ordered_to_sit()); + return true; + } + return parent_interaction; + } + } else if item == &Item::BONE && !self.mob_entity.is_attacking() { + item_stack.decrement_unless_creative(player.gamemode.load(), 1); + let mut rng = rand::rng(); + if rng.random_range(0..3) == 0 { + TamableAnimal::tame(self, player.gameprofile.id); + self.set_ordered_to_sit(true); + self.spawn_taming_particles(true); + } else { + self.spawn_taming_particles(false); + } + return true; + } + + self.animal_interact( + player, + item_stack, + pumpkin_data::sound::Sound::EntityWolfAmbient, + ) + } } impl WolfEntity { diff --git a/crates/pumpkin/src/entity/passive/zombie_horse.rs b/crates/pumpkin/src/entity/passive/zombie_horse.rs index 8189ed034..f7a806606 100644 --- a/crates/pumpkin/src/entity/passive/zombie_horse.rs +++ b/crates/pumpkin/src/entity/passive/zombie_horse.rs @@ -1,24 +1,53 @@ -use std::sync::{Arc, Weak}; +use std::sync::{ + Arc, Weak, + atomic::{AtomicI32, 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::sound::{Sound, SoundCategory}; +use pumpkin_nbt::compound::NbtCompound; +use pumpkin_protocol::java::client::play::Metadata; +use uuid::Uuid; use crate::entity::{ - Entity, + Entity, EntityBase, + ageable::{AgeableData, AgeableMob}, ai::goal::{ - look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal, - wander_around::WanderAroundGoal, + escape_danger::EscapeDangerGoal, look_around::RandomLookAroundGoal, + look_at_entity::LookAtEntityGoal, swim::SwimGoal, wander_around::WanderAroundGoal, }, mob::{Mob, MobEntity}, + passive::animal::Animal, + player::Player, }; +pub const FLAG_TAME: u8 = 2; +pub const FLAG_SADDLE: u8 = 4; +pub const FLAG_EATING: u8 = 16; +pub const FLAG_STANDING: u8 = 32; +pub const FLAG_OPEN_MOUTH: u8 = 64; + pub struct ZombieHorseEntity { pub mob_entity: MobEntity, + pub ageable_data: AgeableData, + pub flags: AtomicU8, + pub temper: AtomicI32, + pub owner: AtomicCell>, } impl ZombieHorseEntity { pub fn new(entity: Entity) -> Arc { let mob_entity = MobEntity::new(entity); - let horse = Self { mob_entity }; + let horse = Self { + mob_entity, + ageable_data: AgeableData::default(), + flags: AtomicU8::new(0), + temper: AtomicI32::new(0), + owner: AtomicCell::new(None), + }; let mob_arc = Arc::new(horse); let mob_weak: Weak = { let mob_arc: Arc = mob_arc.clone(); @@ -33,20 +62,155 @@ impl ZombieHorseEntity { .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(0.7))); + goal_selector.add_goal(1, EscapeDangerGoal::new(1.2)); + goal_selector.add_goal(6, Box::new(WanderAroundGoal::new(0.7))); goal_selector.add_goal( - 2, + 7, LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 6.0), ); - goal_selector.add_goal(3, Box::new(RandomLookAroundGoal::default())); + goal_selector.add_goal(8, Box::new(RandomLookAroundGoal::default())); }; mob_arc } + + #[must_use] + pub fn has_flag(&self, flag: u8) -> bool { + (self.flags.load(Ordering::Relaxed) & flag) != 0 + } + + pub fn set_flag(&self, flag: u8, val: bool) { + let current = self.flags.load(Ordering::Relaxed); + let new_flags = if val { current | flag } else { current & !flag }; + self.flags.store(new_flags, Ordering::Relaxed); + let entity = self.get_entity(); + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::zombie_horse::DATA_ID_FLAGS, + new_flags as i8, + )], + None, + ); + } + + #[must_use] + pub fn is_tame(&self) -> bool { + self.has_flag(FLAG_TAME) + } + + pub fn set_tame(&self, val: bool) { + self.set_flag(FLAG_TAME, val); + } + + #[must_use] + pub fn is_saddled(&self) -> bool { + self.has_flag(FLAG_SADDLE) + } + + pub fn set_saddled(&self, val: bool) { + self.set_flag(FLAG_SADDLE, val); + } +} + +impl AgeableMob for ZombieHorseEntity { + fn get_ageable_data(&self) -> &AgeableData { + &self.ageable_data + } +} + +impl Animal for ZombieHorseEntity { + fn is_food(&self, _item_stack: &ItemStack) -> bool { + false + } } impl Mob for ZombieHorseEntity { + fn as_ageable(&self) -> Option<&dyn AgeableMob> { + Some(self) + } + + fn as_animal(&self) -> Option<&dyn Animal> { + Some(self) + } + + fn mob_write_nbt(&self, nbt: &mut NbtCompound) { + self.write_ageable_nbt(nbt); + nbt.put_bool("Tame", self.is_tame()); + nbt.put_int("Temper", self.temper.load(Ordering::Relaxed)); + if let Some(owner) = self.owner.load() { + nbt.put_uuid("Owner", owner); + } + } + + fn mob_read_nbt(&self, nbt: &NbtCompound) { + self.read_ageable_nbt(nbt); + if let Some(tame) = nbt.get_bool("Tame") { + self.set_tame(tame); + } + if let Some(temper) = nbt.get_int("Temper") { + self.temper.store(temper, Ordering::Relaxed); + } + if let Some(owner) = nbt.get_uuid("Owner") { + self.owner.store(Some(owner)); + } + } + fn get_mob_entity(&self) -> &MobEntity { &self.mob_entity } + + fn mob_tick(&self, _caller: &dyn EntityBase) { + self.ageable_ai_step(); + } + + fn mob_init_data_tracker(&self) { + let entity = self.get_entity(); + let is_baby = entity.age.load(Ordering::Relaxed) < 0; + if is_baby { + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::zombie_horse::DATA_BABY_ID, + true, + )], + None, + ); + } + entity.send_meta_data( + &[Metadata::new( + pumpkin_data::tracked_data::zombie_horse::DATA_ID_FLAGS, + self.flags.load(Ordering::Relaxed) as i8, + )], + None, + ); + } + + fn mob_interact(&self, player: &Arc, item_stack: &mut ItemStack) -> bool { + let item = item_stack.get_item(); + + if self.is_tame() && item == &Item::SADDLE && !self.is_saddled() && !self.is_baby() { + self.set_saddled(true); + item_stack.decrement_unless_creative(player.gamemode.load(), 1); + let entity = self.get_entity(); + let world = entity.world.load(); + world.play_sound( + Sound::EntityHorseSaddle, + SoundCategory::Neutral, + &entity.pos.load(), + ); + return true; + } + + if !self.is_baby() { + let world = player.world(); + let ent = &self.mob_entity.living_entity.entity; + if let Some(vehicle) = world.get_entity_by_id(ent.entity_id) + && let Some(passenger) = world.get_player_by_id(player.entity_id()) + { + ent.add_passenger(vehicle, passenger as Arc); + return true; + } + } + + self.animal_interact(player, item_stack, Sound::EntityZombieHorseAmbient) + } } diff --git a/crates/pumpkin/src/entity/projectile/evoker_fangs.rs b/crates/pumpkin/src/entity/projectile/evoker_fangs.rs index 8c4cc095b..1a2bfcbde 100644 --- a/crates/pumpkin/src/entity/projectile/evoker_fangs.rs +++ b/crates/pumpkin/src/entity/projectile/evoker_fangs.rs @@ -1,6 +1,7 @@ -use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicI32, Ordering}; use pumpkin_data::damage::DamageType; +use pumpkin_data::entity::EntityStatus; use pumpkin_data::sound::Sound; use crate::{ @@ -10,10 +11,10 @@ use crate::{ pub struct EvokerFangsEntity { pub entity: Entity, - pub warmup_ticks: AtomicU32, - pub life_ticks: AtomicU32, + pub warmup_delay_ticks: AtomicI32, + pub life_ticks: AtomicI32, pub owner_id: Option, - pub has_bitten: AtomicBool, + pub sent_spike_event: AtomicBool, } impl EvokerFangsEntity { @@ -22,22 +23,22 @@ impl EvokerFangsEntity { entity.set_rotation(yaw, 0.0); Self { entity, - warmup_ticks: AtomicU32::new(warmup_ticks), - life_ticks: AtomicU32::new(0), + warmup_delay_ticks: AtomicI32::new(warmup_ticks as i32), + life_ticks: AtomicI32::new(22), owner_id, - has_bitten: AtomicBool::new(false), + sent_spike_event: AtomicBool::new(false), } } } impl EntityBase for EvokerFangsEntity { fn write_custom_nbt(&self, nbt: &mut pumpkin_nbt::compound::NbtCompound) { - nbt.put_int("Warmup", self.warmup_ticks.load(Ordering::Relaxed) as i32); + nbt.put_int("Warmup", self.warmup_delay_ticks.load(Ordering::Relaxed)); } fn read_custom_nbt(&self, nbt: &pumpkin_nbt::compound::NbtCompound) { if let Some(warmup) = nbt.get_int("Warmup") { - self.warmup_ticks.store(warmup as u32, Ordering::Relaxed); + self.warmup_delay_ticks.store(warmup, Ordering::Relaxed); } } @@ -45,29 +46,48 @@ impl EntityBase for EvokerFangsEntity { 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 warmup = self.warmup_delay_ticks.fetch_sub(1, Ordering::Relaxed) - 1; + if warmup < 0 { + if warmup == -8 { + let bb = entity.bounding_box.load().expand(0.2, 0.0, 0.2); let candidates = world.get_entities_at_box(&bb); + let owner = self.owner_id.and_then(|id| world.get_entity_by_id(id)); + 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.damage(cand.as_ref(), 6.0, DamageType::MAGIC); + if cand_ent.entity_id != entity.entity_id && cand.get_living_entity().is_some() + { + let damage_type = if owner.is_some() { + DamageType::INDIRECT_MAGIC + } else { + DamageType::MAGIC + }; + let _ = cand.damage_with_context( + cand.as_ref(), + 6.0, + damage_type, + Some(entity.pos.load()), + Some(entity), + owner.as_deref(), + ); } } } - if life > warmup + 20 { + if !self.sent_spike_event.swap(true, Ordering::SeqCst) { + world.send_entity_status(entity, EntityStatus::StartAttacking, None); + if !entity.silent.load(Ordering::Relaxed) { + entity.play_sound(Sound::EntityEvokerFangsAttack); + } + } + + let life = self.life_ticks.fetch_sub(1, Ordering::Relaxed) - 1; + if life < 0 { entity.remove(); } } diff --git a/crates/pumpkin/src/entity/projectile/firework_rocket.rs b/crates/pumpkin/src/entity/projectile/firework_rocket.rs index 4a3dfc9fb..da2176f37 100644 --- a/crates/pumpkin/src/entity/projectile/firework_rocket.rs +++ b/crates/pumpkin/src/entity/projectile/firework_rocket.rs @@ -38,7 +38,6 @@ impl FireworkRocketEntity { gravity: GRAVITY, }, life: 0.into(), - // TODO life_time: (10 + random.next_bounded_i32(6) as u32 + random.next_bounded_i32(7) as u32) .into(), } @@ -47,8 +46,6 @@ impl FireworkRocketEntity { pub fn new_shot(entity: Entity, shooter: &Entity) -> Self { let mut random = RandomGenerator::Xoroshiro(Xoroshiro::from_seed(get_seed())); - // Set random initial velocity - // Set on the inner entity after constructing ThrownItemEntity let thrown = ThrownItemEntity::new(entity, shooter, GRAVITY); thrown.entity.set_velocity(Vector3::new( random.next_triangular(0.0, 0.002_297), @@ -56,7 +53,6 @@ impl FireworkRocketEntity { random.next_triangular(0.0, 0.002_297), )); - // Set random life let rocket = Self { entity: thrown, life: 0.into(), @@ -64,7 +60,6 @@ impl FireworkRocketEntity { .into(), }; - // Set shooter metadata rocket.entity.entity.send_meta_data( &[Metadata::new( pumpkin_data::tracked_data::firework_rocket::ATTACHED_TO_TARGET, @@ -84,8 +79,6 @@ impl FireworkRocketEntity { Some(ActorEventID::FireworksExplode), ); - // TODO: Explode/colors - entity.remove(); } } @@ -99,11 +92,9 @@ impl EntityBase for FireworkRocketEntity { let mut velocity = entity.velocity.load(); if let Some(shooter_id) = self.entity.owner_id { - // Check if the player who fired this rocket still exists in the world if let Some(shooter) = world.get_entity_by_id(shooter_id) { let shooter = shooter.get_entity(); - // Logic for boosting Elytra flight if shooter.is_fall_flying() { let rotation = shooter.rotation().to_f64(); let shooter_vel = shooter.velocity.load(); @@ -118,14 +109,12 @@ impl EntityBase for FireworkRocketEntity { } } } else { - // Standard firework rocket flight logic velocity.x *= 1.15; velocity.z *= 1.15; velocity.y += 0.04; entity.set_velocity(velocity); } - // Increment life and check for explosion let current_life = self.life.fetch_add(1, Ordering::Relaxed); if current_life > self.life_time.load(Ordering::Relaxed) { self.explode_and_remove(&world); @@ -139,7 +128,13 @@ impl EntityBase for FireworkRocketEntity { fn get_living_entity(&self) -> Option<&crate::entity::living::LivingEntity> { None } + fn cast_any(&self) -> &dyn std::any::Any { self } + + fn on_hit(&self, _hit: crate::entity::projectile::ProjectileHit) { + let world = self.get_entity().world.load(); + self.explode_and_remove(&world); + } } diff --git a/crates/pumpkin/src/entity/projectile/llama_spit.rs b/crates/pumpkin/src/entity/projectile/llama_spit.rs index 802c9dd3f..062a35127 100644 --- a/crates/pumpkin/src/entity/projectile/llama_spit.rs +++ b/crates/pumpkin/src/entity/projectile/llama_spit.rs @@ -92,7 +92,7 @@ impl EntityBase for LlamaSpitEntity { 1.0, DamageType::SPIT, Some(hit_pos), - None, + Some(self.get_entity()), owner.as_deref(), ); } diff --git a/crates/pumpkin/src/entity/projectile/wind_charge.rs b/crates/pumpkin/src/entity/projectile/wind_charge.rs index c963e99e9..89aae951d 100644 --- a/crates/pumpkin/src/entity/projectile/wind_charge.rs +++ b/crates/pumpkin/src/entity/projectile/wind_charge.rs @@ -1,4 +1,7 @@ +use pumpkin_data::damage::DamageType; +use pumpkin_data::tag; use pumpkin_util::math::vector3::Vector3; +use std::sync::LazyLock; use std::{ f64, sync::{ @@ -9,22 +12,20 @@ use std::{ use crate::{ entity::{ - Entity, EntityBase, living::LivingEntity, projectile::ThrownItemEntity, + Entity, EntityBase, + living::LivingEntity, + projectile::{ProjectileHit, ThrownItemEntity}, projectile_deflection::ProjectileDeflectionType, }, server::Server, + world::SimpleExplosionDamageCalculator, }; -const EXPLOSION_POWER: f32 = 1.2; const DEFAULT_DEFLECT_COOLDOWN: u8 = 5; pub const WIND_CHARGE_GRAVITY: f64 = 0.0; -/// A kind to differentiate both types of wind charges from each other. enum WindChargeKind { - /// Represents a wind charge spawned by a player or dispenser. - /// This wind charge also has a deflect cooldown counter. Normal { deflect_cooldown: AtomicU8 }, - /// Represents a wind charge spawned by a breeze. Breeze, } @@ -33,10 +34,6 @@ pub struct WindChargeEntity { thrown_item_entity: ThrownItemEntity, } -use crate::world::SimpleExplosionDamageCalculator; -use pumpkin_data::tag; -use std::sync::LazyLock; - pub static WIND_CHARGE_EXPLOSION_DAMAGE_CALCULATOR: LazyLock> = LazyLock::new(|| { Arc::new(SimpleExplosionDamageCalculator::new( @@ -89,13 +86,13 @@ impl WindChargeEntity { } pub fn create_explosion(&self, position: Vector3) { - let calculator = match self.kind { - WindChargeKind::Normal { .. } => WIND_CHARGE_EXPLOSION_DAMAGE_CALCULATOR.clone(), - WindChargeKind::Breeze => BREEZE_WIND_CHARGE_EXPLOSION_DAMAGE_CALCULATOR.clone(), + let (power, calculator) = match self.kind { + WindChargeKind::Normal { .. } => (1.2, WIND_CHARGE_EXPLOSION_DAMAGE_CALCULATOR.clone()), + WindChargeKind::Breeze => (3.0, BREEZE_WIND_CHARGE_EXPLOSION_DAMAGE_CALCULATOR.clone()), }; self.get_entity().world.load().explode_with_calculator( position, - EXPLOSION_POWER, + power, crate::world::ExplosionInteraction::Trigger, Some(calculator), ); @@ -113,13 +110,6 @@ impl WindChargeEntity { } deflection.deflect(self, deflector); - - /* TODO: Does this need to be implemented? - if self.get_entity().world().is_client() { - self.set_owner(); - self.on_Deflected(from_attack); - } - */ true } } @@ -143,7 +133,27 @@ impl EntityBase for WindChargeEntity { fn get_living_entity(&self) -> Option<&LivingEntity> { None } + fn cast_any(&self) -> &dyn std::any::Any { self } + + fn on_hit(&self, hit: ProjectileHit) { + let hit_pos = hit.hit_pos(); + if let ProjectileHit::Entity { ref entity, .. } = hit { + let world = self.get_entity().world.load(); + let owner_id = self.thrown_item_entity.owner_id; + let owner = owner_id.and_then(|id| world.get_entity_by_id(id)); + + let _ = entity.damage_with_context( + entity.as_ref(), + 1.0, + DamageType::WIND_CHARGE, + Some(hit_pos), + Some(self.get_entity()), + owner.as_deref(), + ); + } + self.create_explosion(hit_pos); + } } diff --git a/crates/pumpkin/src/entity/type.rs b/crates/pumpkin/src/entity/type.rs index 1aa7fc983..f5af2f9de 100644 --- a/crates/pumpkin/src/entity/type.rs +++ b/crates/pumpkin/src/entity/type.rs @@ -1,6 +1,7 @@ use std::sync::Arc; use pumpkin_data::entity::EntityType; +use pumpkin_data::tag::Taggable; use pumpkin_util::math::position::BlockPos; use pumpkin_util::math::vector3::Vector3; use uuid::Uuid; @@ -342,6 +343,7 @@ pub fn from_type( mob } +#[expect(clippy::too_many_lines)] pub fn check_spawn_rules( entity_type: &'static EntityType, world: &World, @@ -350,6 +352,7 @@ pub fn check_spawn_rules( ) -> bool { let id = entity_type.id; + // Monsters (Standard Darkness + Peaceful check) if id == EntityType::BOGGED.id || id == EntityType::CAVE_SPIDER.id || id == EntityType::CREEPER.id @@ -373,13 +376,321 @@ pub fn check_spawn_rules( { return mob::MobEntity::check_monster_spawn_rules(world, pos, is_thundering); } - if id == EntityType::BAT.id { - return bat::BatEntity::check_bat_spawn_rules(world, pos); + + // Any-light monsters (Blaze, Breeze, Zoglin) + if id == EntityType::BLAZE.id || id == EntityType::BREEZE.id || id == EntityType::ZOGLIN.id { + return mob::MobEntity::check_any_light_monster_spawn_rules(world, pos); } + + // Surface monsters (Husk, Parched, Camel Husk) + if id == EntityType::HUSK.id || id == EntityType::PARCHED.id || id == EntityType::CAMEL_HUSK.id + { + return mob::MobEntity::check_surface_monsters_spawn_rules(world, pos, is_thundering); + } + + // Stray + if id == EntityType::STRAY.id { + if !mob::MobEntity::check_monster_spawn_rules(world, pos, is_thundering) { + return false; + } + let mut check_sky_pos = *pos; + while world.get_block(&check_sky_pos) == &pumpkin_data::Block::POWDER_SNOW { + check_sky_pos = check_sky_pos.up(); + } + return world.can_see_sky(&check_sky_pos.down()); + } + + // Pillager (Patrolling Monster) + if id == EntityType::PILLAGER.id { + if world.get_block_light_level(pos).unwrap_or(0) > 8 { + return false; + } + return mob::MobEntity::check_any_light_monster_spawn_rules(world, pos); + } + + // Endermite & Silverfish + if id == EntityType::ENDERMITE.id || id == EntityType::SILVERFISH.id { + if !mob::MobEntity::check_any_light_monster_spawn_rules(world, pos) { + return false; + } + return world + .get_closest_player(pos.to_centered_f64(), 5.0) + .is_none(); + } + + // Ghast + if id == EntityType::GHAST.id { + if world.level_info.load().difficulty == pumpkin_util::Difficulty::Peaceful { + return false; + } + if rand::random_range(0..20) != 0 { + return false; + } + return mob::MobEntity::check_mob_spawn_rules(world, pos); + } + + // Magma Cube + if id == EntityType::MAGMA_CUBE.id { + if world.level_info.load().difficulty == pumpkin_util::Difficulty::Peaceful { + return false; + } + return mob::MobEntity::check_mob_spawn_rules(world, pos); + } + + // Sulfur Cube + if id == EntityType::SULFUR_CUBE.id { + return true; + } + + // Slime if id == EntityType::SLIME.id { return SlimeEntity::check_slime_spawn_rules(world, pos); } - // TODO + // Nether Mobs: Hoglin, Piglin, Zombified Piglin + if id == EntityType::HOGLIN.id || id == EntityType::PIGLIN.id { + let below = pos.down(); + if world.get_block(&below) == &pumpkin_data::Block::NETHER_WART_BLOCK { + return false; + } + return mob::MobEntity::check_mob_spawn_rules(world, pos); + } + if id == EntityType::ZOMBIFIED_PIGLIN.id { + if world.level_info.load().difficulty == pumpkin_util::Difficulty::Peaceful { + return false; + } + let below = pos.down(); + if world.get_block(&below) == &pumpkin_data::Block::NETHER_WART_BLOCK { + return false; + } + return mob::MobEntity::check_mob_spawn_rules(world, pos); + } + + // Strider + if id == EntityType::STRIDER.id { + let mut check_pos = *pos; + while world + .get_fluid(&check_pos) + .has_tag(&pumpkin_data::tag::Fluid::MINECRAFT_LAVA) + { + check_pos = check_pos.up(); + } + return world.get_block(&check_pos).is_air(); + } + + // Bat + if id == EntityType::BAT.id { + return bat::BatEntity::check_bat_spawn_rules(world, pos); + } + + // Animals (General) + if id == EntityType::CHICKEN.id + || id == EntityType::COW.id + || id == EntityType::DONKEY.id + || id == EntityType::HAPPY_GHAST.id + || id == EntityType::HORSE.id + || id == EntityType::LLAMA.id + || id == EntityType::MULE.id + || id == EntityType::PIG.id + || id == EntityType::SHEEP.id + || id == EntityType::CAT.id + || id == EntityType::PANDA.id + || id == EntityType::TRADER_LLAMA.id + { + return mob::MobEntity::check_animal_spawn_rules(world, pos); + } + + // Tagged Animals: Armadillo, Camel, Frog, Goat, Mooshroom, Parrot, Rabbit, Wolf, Fox + if id == EntityType::ARMADILLO.id { + let below = pos.down(); + return world + .get_block(&below) + .has_tag(&pumpkin_data::tag::Block::MINECRAFT_ARMADILLO_SPAWNABLE_ON) + && mob::MobEntity::is_bright_enough_to_spawn(world, pos); + } + if id == EntityType::CAMEL.id { + let below = pos.down(); + return world + .get_block(&below) + .has_tag(&pumpkin_data::tag::Block::MINECRAFT_CAMELS_SPAWNABLE_ON) + && mob::MobEntity::is_bright_enough_to_spawn(world, pos); + } + if id == EntityType::FROG.id { + let below = pos.down(); + return world + .get_block(&below) + .has_tag(&pumpkin_data::tag::Block::MINECRAFT_FROGS_SPAWNABLE_ON) + && mob::MobEntity::is_bright_enough_to_spawn(world, pos); + } + if id == EntityType::GOAT.id { + let below = pos.down(); + return world + .get_block(&below) + .has_tag(&pumpkin_data::tag::Block::MINECRAFT_GOATS_SPAWNABLE_ON) + && mob::MobEntity::is_bright_enough_to_spawn(world, pos); + } + if id == EntityType::MOOSHROOM.id { + let below = pos.down(); + return world + .get_block(&below) + .has_tag(&pumpkin_data::tag::Block::MINECRAFT_MOOSHROOMS_SPAWNABLE_ON) + && mob::MobEntity::is_bright_enough_to_spawn(world, pos); + } + if id == EntityType::PARROT.id { + let below = pos.down(); + return world + .get_block(&below) + .has_tag(&pumpkin_data::tag::Block::MINECRAFT_PARROTS_SPAWNABLE_ON) + && mob::MobEntity::is_bright_enough_to_spawn(world, pos); + } + if id == EntityType::RABBIT.id { + let below = pos.down(); + return world + .get_block(&below) + .has_tag(&pumpkin_data::tag::Block::MINECRAFT_RABBITS_SPAWNABLE_ON) + && mob::MobEntity::is_bright_enough_to_spawn(world, pos); + } + if id == EntityType::WOLF.id { + let below = pos.down(); + return world + .get_block(&below) + .has_tag(&pumpkin_data::tag::Block::MINECRAFT_WOLVES_SPAWNABLE_ON) + && mob::MobEntity::is_bright_enough_to_spawn(world, pos); + } + if id == EntityType::FOX.id { + let below = pos.down(); + return world + .get_block(&below) + .has_tag(&pumpkin_data::tag::Block::MINECRAFT_FOXES_SPAWNABLE_ON) + && mob::MobEntity::is_bright_enough_to_spawn(world, pos); + } + if id == EntityType::OCELOT.id { + if rand::random_range(0..3) == 0 { + return false; + } + return mob::MobEntity::check_mob_spawn_rules(world, pos); + } + if id == EntityType::POLAR_BEAR.id { + let below = pos.down(); + let biome = world.get_biome(pos); + if biome.has_tag( + &pumpkin_data::tag::WorldgenBiome::MINECRAFT_POLAR_BEARS_SPAWN_ON_ALTERNATE_BLOCKS, + ) { + return mob::MobEntity::is_bright_enough_to_spawn(world, pos) + && world.get_block(&below).has_tag( + &pumpkin_data::tag::Block::MINECRAFT_POLAR_BEARS_SPAWNABLE_ON_ALTERNATE, + ); + } + return mob::MobEntity::check_animal_spawn_rules(world, pos); + } + if id == EntityType::TURTLE.id { + let sea_level = world.sea_level; + let below = pos.down(); + return pos.0.y < sea_level + 4 + && world + .get_block(&below) + .has_tag(&pumpkin_data::tag::Block::MINECRAFT_SAND) + && mob::MobEntity::is_bright_enough_to_spawn(world, pos); + } + if id == EntityType::SKELETON_HORSE.id { + return mob::MobEntity::check_animal_spawn_rules(world, pos); + } + + // Water Animals / Creatures + if id == EntityType::COD.id || id == EntityType::PUFFERFISH.id || id == EntityType::SALMON.id { + return mob::MobEntity::check_surface_water_animal_spawn_rules(world, pos); + } + if id == EntityType::DOLPHIN.id || id == EntityType::SQUID.id { + return mob::MobEntity::check_surface_ageable_water_creature_spawn_rules(world, pos); + } + if id == EntityType::TROPICAL_FISH.id { + let biome = world.get_biome(pos); + let valid_water = world + .get_fluid(&pos.down()) + .has_tag(&pumpkin_data::tag::Fluid::MINECRAFT_WATER) + && (world.get_block(&pos.up()) == &pumpkin_data::Block::WATER + || world + .get_fluid(&pos.up()) + .has_tag(&pumpkin_data::tag::Fluid::MINECRAFT_WATER)); + if !valid_water { + return false; + } + return biome.has_tag( + &pumpkin_data::tag::WorldgenBiome::MINECRAFT_ALLOWS_TROPICAL_FISH_SPAWNS_AT_ANY_HEIGHT, + ) || mob::MobEntity::check_surface_water_animal_spawn_rules(world, pos); + } + if id == EntityType::AXOLOTL.id { + let below = pos.down(); + return world + .get_block(&below) + .has_tag(&pumpkin_data::tag::Block::MINECRAFT_AXOLOTLS_SPAWNABLE_ON); + } + if id == EntityType::GLOW_SQUID.id { + let sea_level = world.sea_level; + return pos.0.y <= sea_level - 33 + && world.get_max_local_raw_brightness(pos) == 0 + && (world.get_block(pos) == &pumpkin_data::Block::WATER + || world + .get_fluid(pos) + .has_tag(&pumpkin_data::tag::Fluid::MINECRAFT_WATER)); + } + if id == EntityType::NAUTILUS.id { + let sea_level = world.sea_level; + let min_spawn_level = sea_level - 25; + return pos.0.y >= min_spawn_level + && pos.0.y <= sea_level - 5 + && world + .get_fluid(&pos.down()) + .has_tag(&pumpkin_data::tag::Fluid::MINECRAFT_WATER) + && (world.get_block(&pos.up()) == &pumpkin_data::Block::WATER + || world + .get_fluid(&pos.up()) + .has_tag(&pumpkin_data::tag::Fluid::MINECRAFT_WATER)); + } + if id == EntityType::DROWNED.id { + if world.level_info.load().difficulty == pumpkin_util::Difficulty::Peaceful { + return false; + } + if !world + .get_fluid(&pos.down()) + .has_tag(&pumpkin_data::tag::Fluid::MINECRAFT_WATER) + || !world + .get_fluid(pos) + .has_tag(&pumpkin_data::tag::Fluid::MINECRAFT_WATER) + { + return false; + } + if !mob::MobEntity::is_dark_enough_to_spawn(world, pos, is_thundering) { + return false; + } + let biome = world.get_biome(pos); + if biome.has_tag(&pumpkin_data::tag::WorldgenBiome::MINECRAFT_MORE_FREQUENT_DROWNED_SPAWNS) + { + return rand::random_range(0..15) == 0; + } + let sea_level = world.sea_level; + return pos.0.y < sea_level - 5 && rand::random_range(0..40) == 0; + } + if id == EntityType::GUARDIAN.id || id == EntityType::ELDER_GUARDIAN.id { + if world.level_info.load().difficulty == pumpkin_util::Difficulty::Peaceful { + return false; + } + if !world + .get_fluid(&pos.down()) + .has_tag(&pumpkin_data::tag::Fluid::MINECRAFT_WATER) + || !world + .get_fluid(pos) + .has_tag(&pumpkin_data::tag::Fluid::MINECRAFT_WATER) + { + return false; + } + return rand::random_range(0..20) == 0 || !world.can_see_sky(pos); + } + + // Generic mob spawn rules (Iron Golem, Snow Golem, Villager, Phantom, Shulker, Wandering Trader, Ender Dragon, etc.) + if entity_type.mob { + return mob::MobEntity::check_mob_spawn_rules(world, pos); + } + true } diff --git a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/mob.rs b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/mob.rs index f25dc7cb2..51ce0c566 100644 --- a/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/mob.rs +++ b/crates/pumpkin/src/plugin/loader/wasm/wasm_host/wit/v0_1/mob.rs @@ -600,8 +600,7 @@ impl HostMob for PluginHostState { .navigator .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) - .is_idle - .load(std::sync::atomic::Ordering::Relaxed); + .is_idle(); !is_idle })) } @@ -613,8 +612,7 @@ impl HostMob for PluginHostState { .navigator .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) - .is_idle - .load(std::sync::atomic::Ordering::Relaxed) + .is_idle() })) } diff --git a/crates/pumpkin/src/world/natural_spawner.rs b/crates/pumpkin/src/world/natural_spawner.rs index 3cb4cd9c5..4c1f87284 100644 --- a/crates/pumpkin/src/world/natural_spawner.rs +++ b/crates/pumpkin/src/world/natural_spawner.rs @@ -1,3 +1,4 @@ +use crate::block::PathComputationType; use crate::entity::EntityBase; use crate::entity::r#type::{check_spawn_rules, from_type}; use crate::world::World; @@ -938,9 +939,11 @@ pub fn adjust_spawn_position( SpawnLocation::OnGround ) { let below = pos.down(); - let state = world.get_block_state(&below); - // Approximation of isPathfindable(LAND) - if !state.is_full_cube() && !state.is_liquid() { + let (block, state) = world.get_block_and_state(&below); + if world + .block_registry + .is_pathfindable(block, state, PathComputationType::Land) + { return below; } }