feat: add some more entity logic

This commit is contained in:
Alexander Medvedev 2026-08-13 17:28:56 +02:00
parent 24d9dbdc6b
commit 5a5bc19bbb
39 changed files with 6811 additions and 245 deletions

View File

@ -39,6 +39,8 @@ pub enum ActorEventType {
StartOfferFlower = 19,
StopOfferFlower = 20,
LoveHearts = 21,
TrustingFailed = 40,
TrustingSucceeded = 41,
VillagerAngry = 22,
VillagerHappy = 23,
WitchHatMagic = 24,

View File

@ -283,6 +283,24 @@ impl MetadataSerializer for crate::codec::optional_int::OptionalInt {
}
}
impl MetadataSerializer for uuid::Uuid {
fn write_metadata(&self, writer: &mut impl std::io::Write) -> Result<(), WritingError> {
writer.write_uuid(self)
}
}
impl MetadataSerializer for Option<uuid::Uuid> {
fn write_metadata(&self, writer: &mut impl std::io::Write) -> Result<(), WritingError> {
if let Some(uuid) = self {
writer.write_bool(true)?;
writer.write_uuid(uuid)?;
} else {
writer.write_bool(false)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::io::{Cursor, Read};

View File

@ -1,12 +1,19 @@
use std::pin::Pin;
use std::sync::Arc;
use crossbeam::atomic::AtomicCell;
use pumpkin_data::sound::{Sound, SoundCategory};
use pumpkin_nbt::compound::NbtCompound;
use pumpkin_util::math::position::BlockPos;
use uuid::Uuid;
use crate::world::World;
use super::BlockEntity;
pub struct CreakingHeartBlockEntity {
pub position: BlockPos,
pub creaking_uuid: AtomicCell<Option<Uuid>>,
}
impl BlockEntity for CreakingHeartBlockEntity {
@ -18,18 +25,28 @@ impl BlockEntity for CreakingHeartBlockEntity {
self.position
}
fn from_nbt(_nbt: &pumpkin_nbt::compound::NbtCompound, position: BlockPos) -> Self
fn from_nbt(nbt: &pumpkin_nbt::compound::NbtCompound, position: BlockPos) -> Self
where
Self: Sized,
{
Self { position }
let creaking_uuid = nbt
.get_string("creaking_uuid")
.and_then(|uuid_str| Uuid::parse_str(uuid_str).ok());
Self {
position,
creaking_uuid: AtomicCell::new(creaking_uuid),
}
}
fn write_nbt<'a>(
&'a self,
_nbt: &'a mut NbtCompound,
nbt: &'a mut NbtCompound,
) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
Box::pin(async {})
Box::pin(async move {
if let Some(uuid) = self.creaking_uuid.load() {
nbt.put_string("creaking_uuid", uuid.to_string());
}
})
}
fn as_any(&self) -> &dyn std::any::Any {
@ -39,8 +56,30 @@ impl BlockEntity for CreakingHeartBlockEntity {
impl CreakingHeartBlockEntity {
pub const ID: &'static str = "minecraft:creaking_heart";
#[must_use]
pub const fn new(position: BlockPos) -> Self {
Self { position }
Self {
position,
creaking_uuid: AtomicCell::new(None),
}
}
pub fn is_protector(&self, creaking_uuid: Uuid) -> bool {
self.creaking_uuid
.load()
.is_none_or(|uuid| uuid == creaking_uuid)
}
pub fn set_creaking_uuid(&self, uuid: Option<Uuid>) {
self.creaking_uuid.store(uuid);
}
pub fn creaking_hurt(&self, world: &Arc<World>) {
world.play_sound(
Sound::BlockCreakingHeartHurt,
SoundCategory::Blocks,
&self.position.to_f64(),
);
}
}

View File

@ -4,41 +4,40 @@ use crate::entity::mob::Mob;
use crate::entity::player::Player;
use pumpkin_data::item::Item;
use pumpkin_data::meta_data_type::MetaDataType;
use pumpkin_data::tag::{self, Taggable};
use pumpkin_data::tracked_data::TrackedData;
use pumpkin_protocol::java::client::play::Metadata;
use rand::RngExt;
use std::sync::Arc;
pub struct BegGoal {
beg_distance_sq: f64,
attractive_items: &'static [&'static Item],
timer: i32,
target: Option<Arc<Player>>,
look_distance_sq: f64,
look_time: i32,
player: Option<Arc<Player>>,
}
impl BegGoal {
#[must_use]
pub fn new(beg_distance: f32, attractive_items: &'static [&'static Item]) -> Box<Self> {
pub fn new(look_distance: f32) -> Box<Self> {
Box::new(Self {
beg_distance_sq: (beg_distance * beg_distance) as f64,
attractive_items,
timer: 0,
target: None,
look_distance_sq: f64::from(look_distance) * f64::from(look_distance),
look_time: 0,
player: None,
})
}
fn is_attractive(&self, item: &Item) -> bool {
self.attractive_items.iter().any(|i| i.id == item.id)
fn is_interesting_item(item: &Item) -> bool {
item.id == Item::BONE.id || item.has_tag(&tag::Item::MINECRAFT_WOLF_FOOD)
}
async fn is_player_holding_attractive(&self, player: &Player) -> bool {
async fn player_holding_interesting(&self, player: &Player) -> bool {
let main_stack = player.inventory().held_item().await;
if main_stack.item_count > 0 && self.is_attractive(main_stack.item) {
if main_stack.item_count > 0 && Self::is_interesting_item(main_stack.item) {
return true;
}
let off_stack = player.inventory().off_hand_item().await;
off_stack.item_count > 0 && self.is_attractive(off_stack.item)
off_stack.item_count > 0 && Self::is_interesting_item(off_stack.item)
}
fn distance_sq(mob: &dyn Mob, player: &Player) -> f64 {
@ -47,12 +46,12 @@ impl BegGoal {
mob_pos.squared_distance_to_vec(&player_pos)
}
fn set_begging(mob: &dyn Mob, begging: bool) {
fn set_is_interested(mob: &dyn Mob, value: bool) {
mob.get_mob_entity().living_entity.entity.send_meta_data(
&[Metadata::new(
TrackedData::INTERESTED_ID,
MetaDataType::BOOLEAN,
begging,
value,
)],
None,
);
@ -61,28 +60,28 @@ impl BegGoal {
impl Goal for BegGoal {
fn can_start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> {
Box::pin(async {
Box::pin(async move {
let entity = &mob.get_mob_entity().living_entity.entity;
let world = entity.world.load_full();
let pos = entity.pos.load();
let radius = self.beg_distance_sq.sqrt();
let radius = self.look_distance_sq.sqrt();
let Some(player) = world.get_closest_player(pos, radius) else {
return false;
};
if !self.is_player_holding_attractive(&player).await {
if !self.player_holding_interesting(&player).await {
return false;
}
self.target = Some(player);
self.player = Some(player);
true
})
}
fn should_continue<'a>(&'a self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> {
Box::pin(async {
let Some(player) = &self.target else {
Box::pin(async move {
let Some(player) = &self.player else {
return false;
};
@ -90,32 +89,32 @@ impl Goal for BegGoal {
return false;
}
if Self::distance_sq(mob, player) > self.beg_distance_sq {
if Self::distance_sq(mob, player) > self.look_distance_sq {
return false;
}
self.timer > 0 && self.is_player_holding_attractive(player).await
self.look_time > 0 && self.player_holding_interesting(player).await
})
}
fn start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
Box::pin(async {
Self::set_begging(mob, true);
Box::pin(async move {
Self::set_is_interested(mob, true);
let ticks = 40 + mob.get_random().random_range(0..40);
self.timer = self.get_tick_count(ticks);
self.look_time = self.get_tick_count(ticks);
})
}
fn stop<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
Box::pin(async {
Self::set_begging(mob, false);
self.target = None;
Box::pin(async move {
Self::set_is_interested(mob, false);
self.player = None;
})
}
fn tick<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
Box::pin(async {
if let Some(player) = &self.target {
Box::pin(async move {
if let Some(player) = &self.player {
let player_pos = player.get_entity().get_eye_pos();
let mut look_control = mob
.get_mob_entity()
@ -130,7 +129,7 @@ impl Goal for BegGoal {
mob.get_max_look_pitch_change(),
);
}
self.timer -= 1;
self.look_time -= 1;
})
}

View File

@ -1,6 +1,7 @@
use super::{Controls, Goal, GoalFuture, to_goal_ticks};
use crate::entity::EntityBase;
use crate::entity::ai::pathfinder::NavigatorGoal;
use crate::entity::ai::pathfinder::node::PathType;
use crate::entity::mob::Mob;
use crate::entity::player::Player;
use pumpkin_util::math::position::BlockPos;
@ -11,27 +12,42 @@ use std::sync::Arc;
const TELEPORT_DISTANCE_SQ: f64 = 144.0;
pub struct FollowOwnerGoal {
speed: f64,
min_distance_sq: f64,
max_distance_sq: f64,
update_countdown: i32,
speed_modifier: f64,
start_distance_sq: f64,
stop_distance_sq: f64,
time_to_recalc_path: i32,
owner: Option<Arc<Player>>,
old_water_cost: f32,
}
impl FollowOwnerGoal {
#[must_use]
pub fn new(speed: f64, min_distance: f32, max_distance: f32) -> Box<Self> {
pub fn new(speed_modifier: f64, start_distance: f32, stop_distance: f32) -> Box<Self> {
Box::new(Self {
speed,
min_distance_sq: (min_distance * min_distance) as f64,
max_distance_sq: (max_distance * max_distance) as f64,
update_countdown: 0,
speed_modifier,
start_distance_sq: f64::from(start_distance) * f64::from(start_distance),
stop_distance_sq: f64::from(stop_distance) * f64::from(stop_distance),
time_to_recalc_path: 0,
owner: None,
old_water_cost: 0.0,
})
}
fn can_follow(mob: &dyn Mob) -> bool {
!mob.is_sitting()
async fn unable_to_move_to_owner(mob: &dyn Mob, owner: Option<&Player>) -> bool {
if mob.is_sitting() {
return true;
}
let mob_entity = &mob.get_mob_entity().living_entity.entity;
if mob_entity.has_vehicle().await || mob_entity.is_leashed().await {
return true;
}
let Some(owner) = owner else {
return true;
};
if owner.is_spectator() || !owner.living_entity.entity.is_alive() {
return true;
}
false
}
fn find_owner(mob: &dyn Mob) -> Option<Arc<Player>> {
@ -50,6 +66,11 @@ impl FollowOwnerGoal {
mob_pos.squared_distance_to_vec(&owner_pos)
}
fn should_try_teleport_to_owner(mob: &dyn Mob, owner: &Player) -> bool {
let dist_sq = Self::distance_to_owner_sq(mob, owner);
dist_sq >= TELEPORT_DISTANCE_SQ
}
fn try_teleport_to_owner(mob: &dyn Mob, owner: &Player) {
let owner_pos = owner.living_entity.entity.pos.load();
let mob_entity = &mob.get_mob_entity().living_entity.entity;
@ -92,7 +113,7 @@ impl FollowOwnerGoal {
mob_entity.teleport(
Vector3::new(
target_x as f64 + 0.5,
target_y as f64,
f64::from(target_y),
target_z as f64 + 0.5,
),
None,
@ -113,17 +134,17 @@ impl FollowOwnerGoal {
impl Goal for FollowOwnerGoal {
fn can_start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> {
Box::pin(async {
if !Self::can_follow(mob) {
return false;
}
Box::pin(async move {
let Some(owner) = Self::find_owner(mob) else {
return false;
};
if Self::unable_to_move_to_owner(mob, Some(&owner)).await {
return false;
}
let dist_sq = Self::distance_to_owner_sq(mob, &owner);
if dist_sq < self.min_distance_sq {
if dist_sq < self.start_distance_sq {
return false;
}
@ -133,49 +154,67 @@ impl Goal for FollowOwnerGoal {
}
fn should_continue<'a>(&'a self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> {
Box::pin(async {
if !Self::can_follow(mob) {
return false;
}
let Some(owner) = &self.owner else {
return false;
};
if owner.is_spectator() || !owner.living_entity.entity.is_alive() {
return false;
}
let dist_sq = Self::distance_to_owner_sq(mob, owner);
if dist_sq <= self.max_distance_sq {
return false;
}
Box::pin(async move {
let is_idle = {
let navigator = mob
.get_mob_entity()
.navigator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
!navigator.is_idle()
navigator.is_idle()
};
if is_idle {
return false;
}
if Self::unable_to_move_to_owner(mob, self.owner.as_deref()).await {
return false;
}
let Some(owner) = &self.owner else {
return false;
};
let dist_sq = Self::distance_to_owner_sq(mob, owner);
dist_sq > self.stop_distance_sq
})
}
fn start<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
Box::pin(async {
self.update_countdown = 0;
fn start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
Box::pin(async move {
self.time_to_recalc_path = 0;
let mut navigator = mob
.get_mob_entity()
.navigator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
self.old_water_cost = navigator.get_pathfinding_malus(PathType::Water);
navigator.set_pathfinding_malus(PathType::Water, 0.0);
})
}
fn stop<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
Box::pin(async move {
self.owner = None;
let mut navigator = mob
.get_mob_entity()
.navigator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
navigator.stop();
navigator.set_pathfinding_malus(PathType::Water, self.old_water_cost);
})
}
fn tick<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
Box::pin(async {
Box::pin(async move {
let Some(owner) = &self.owner else {
return;
};
let dist_sq = Self::distance_to_owner_sq(mob, owner);
let should_teleport = dist_sq >= TELEPORT_DISTANCE_SQ;
let is_owner_far_away = Self::should_try_teleport_to_owner(mob, owner);
if !should_teleport {
if !is_owner_far_away {
let mob_entity = mob.get_mob_entity();
let owner_eye_pos = owner.living_entity.entity.get_eye_pos();
let mut look_control = mob_entity
@ -189,14 +228,12 @@ impl Goal for FollowOwnerGoal {
10.0,
mob.get_max_look_pitch_change(),
);
drop(look_control);
}
self.update_countdown -= 1;
if self.update_countdown <= 0 {
self.update_countdown = to_goal_ticks(10);
if should_teleport {
self.time_to_recalc_path -= 1;
if self.time_to_recalc_path <= 0 {
self.time_to_recalc_path = to_goal_ticks(10);
if is_owner_far_away {
Self::try_teleport_to_owner(mob, owner);
} else {
let mob_pos = mob.get_mob_entity().living_entity.entity.pos.load();
@ -206,24 +243,16 @@ impl Goal for FollowOwnerGoal {
.navigator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
navigator.set_progress(NavigatorGoal::new(mob_pos, owner_pos, self.speed));
navigator.set_progress(NavigatorGoal::new(
mob_pos,
owner_pos,
self.speed_modifier,
));
}
}
})
}
fn stop<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
Box::pin(async {
self.owner = None;
let mut navigator = mob
.get_mob_entity()
.navigator
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
navigator.stop();
})
}
fn should_run_every_tick(&self) -> bool {
true
}

View File

@ -123,6 +123,14 @@ impl Navigator {
self.path_type_overrides.insert(path_type, malus);
}
#[must_use]
pub fn get_pathfinding_malus(&self, path_type: PathType) -> f32 {
self.path_type_overrides
.get(&path_type)
.copied()
.unwrap_or_else(|| path_type.get_malus())
}
pub const fn set_mob_dimensions(&mut self, width: f32, height: f32) {
self.mob_width = width;
self.mob_height = height;

View File

@ -0,0 +1,796 @@
use std::sync::{
Arc,
atomic::{AtomicI8, AtomicI32, AtomicU8, Ordering},
};
use tokio::sync::Mutex;
use pumpkin_data::{
damage::DamageType, item_stack::ItemStack, meta_data_type::MetaDataType,
tracked_data::TrackedData,
};
use pumpkin_nbt::{compound::NbtCompound, tag::NbtTag};
use pumpkin_protocol::{
codec::{item_stack_seralizer::ItemStackSerializer, var_int::VarInt},
java::client::play::{Metadata, MetadataSerializer},
ser::{NetworkWriteExt, WritingError},
};
use pumpkin_util::{math::vector3::Vector3, text::TextComponent};
use crate::{
entity::{Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture, living::LivingEntity},
server::Server,
};
#[derive(Clone, Copy, Debug)]
pub struct Vector3fSerializer(pub f32, pub f32, pub f32);
impl MetadataSerializer for Vector3fSerializer {
fn write_metadata(&self, writer: &mut impl std::io::Write) -> Result<(), WritingError> {
writer.write_f32(self.0)?;
writer.write_f32(self.1)?;
writer.write_f32(self.2)
}
}
#[derive(Clone, Copy, Debug)]
pub struct QuaternionfSerializer(pub f32, pub f32, pub f32, pub f32);
impl MetadataSerializer for QuaternionfSerializer {
fn write_metadata(&self, writer: &mut impl std::io::Write) -> Result<(), WritingError> {
writer.write_f32(self.0)?;
writer.write_f32(self.1)?;
writer.write_f32(self.2)?;
writer.write_f32(self.3)
}
}
pub struct DisplayEntity {
pub entity: Entity,
pub interpolation_start_delta_ticks: AtomicI32,
pub interpolation_duration: AtomicI32,
pub view_range: Mutex<f32>,
pub shadow_radius: Mutex<f32>,
pub shadow_strength: Mutex<f32>,
pub width: Mutex<f32>,
pub height: Mutex<f32>,
pub glow_color_override: AtomicI32,
pub billboard: AtomicU8,
pub brightness: AtomicI32,
pub translation: Mutex<Vector3<f32>>,
pub scale: Mutex<Vector3<f32>>,
pub left_rotation: Mutex<[f32; 4]>,
pub right_rotation: Mutex<[f32; 4]>,
}
impl DisplayEntity {
pub fn new(entity: Entity) -> Self {
entity.no_clip.store(true, Ordering::Relaxed);
Self {
entity,
interpolation_start_delta_ticks: AtomicI32::new(0),
interpolation_duration: AtomicI32::new(0),
view_range: Mutex::new(1.0),
shadow_radius: Mutex::new(0.0),
shadow_strength: Mutex::new(1.0),
width: Mutex::new(0.0),
height: Mutex::new(0.0),
glow_color_override: AtomicI32::new(-1),
billboard: AtomicU8::new(0),
brightness: AtomicI32::new(-1),
translation: Mutex::new(Vector3::new(0.0, 0.0, 0.0)),
scale: Mutex::new(Vector3::new(1.0, 1.0, 1.0)),
left_rotation: Mutex::new([0.0, 0.0, 0.0, 1.0]),
right_rotation: Mutex::new([0.0, 0.0, 0.0, 1.0]),
}
}
#[allow(clippy::too_many_lines)]
pub async fn init_display_data_tracker(&self) {
let view_range = *self.view_range.lock().await;
let shadow_radius = *self.shadow_radius.lock().await;
let shadow_strength = *self.shadow_strength.lock().await;
let width = *self.width.lock().await;
let height = *self.height.lock().await;
let translation = *self.translation.lock().await;
let scale = *self.scale.lock().await;
let left_rotation = *self.left_rotation.lock().await;
let right_rotation = *self.right_rotation.lock().await;
self.entity.send_meta_data(
&[Metadata::new(
TrackedData::START_INTERPOLATION,
MetaDataType::INT,
VarInt(self.interpolation_start_delta_ticks.load(Ordering::Relaxed)),
)],
None,
);
self.entity.send_meta_data(
&[Metadata::new(
TrackedData::INTERPOLATION_DURATION,
MetaDataType::INT,
VarInt(self.interpolation_duration.load(Ordering::Relaxed)),
)],
None,
);
self.entity.send_meta_data(
&[Metadata::new(
TrackedData::TRANSLATION,
MetaDataType::VECTOR_3F,
Vector3fSerializer(translation.x, translation.y, translation.z),
)],
None,
);
self.entity.send_meta_data(
&[Metadata::new(
TrackedData::SCALE,
MetaDataType::VECTOR_3F,
Vector3fSerializer(scale.x, scale.y, scale.z),
)],
None,
);
self.entity.send_meta_data(
&[Metadata::new(
TrackedData::LEFT_ROTATION,
MetaDataType::QUATERNION_F,
QuaternionfSerializer(
left_rotation[0],
left_rotation[1],
left_rotation[2],
left_rotation[3],
),
)],
None,
);
self.entity.send_meta_data(
&[Metadata::new(
TrackedData::RIGHT_ROTATION,
MetaDataType::QUATERNION_F,
QuaternionfSerializer(
right_rotation[0],
right_rotation[1],
right_rotation[2],
right_rotation[3],
),
)],
None,
);
self.entity.send_meta_data(
&[Metadata::new(
TrackedData::BILLBOARD,
MetaDataType::BYTE,
self.billboard.load(Ordering::Relaxed),
)],
None,
);
self.entity.send_meta_data(
&[Metadata::new(
TrackedData::BRIGHTNESS,
MetaDataType::INT,
VarInt(self.brightness.load(Ordering::Relaxed)),
)],
None,
);
self.entity.send_meta_data(
&[Metadata::new(
TrackedData::VIEW_RANGE,
MetaDataType::FLOAT,
view_range,
)],
None,
);
self.entity.send_meta_data(
&[Metadata::new(
TrackedData::SHADOW_RADIUS,
MetaDataType::FLOAT,
shadow_radius,
)],
None,
);
self.entity.send_meta_data(
&[Metadata::new(
TrackedData::SHADOW_STRENGTH,
MetaDataType::FLOAT,
shadow_strength,
)],
None,
);
self.entity.send_meta_data(
&[Metadata::new(
TrackedData::WIDTH,
MetaDataType::FLOAT,
width,
)],
None,
);
self.entity.send_meta_data(
&[Metadata::new(
TrackedData::HEIGHT,
MetaDataType::FLOAT,
height,
)],
None,
);
}
pub async fn write_display_nbt(&self, nbt: &mut NbtCompound) {
self.entity.write_nbt(nbt).await;
nbt.put_int(
"interpolation_duration",
self.interpolation_duration.load(Ordering::Relaxed),
);
nbt.put_int(
"start_interpolation",
self.interpolation_start_delta_ticks.load(Ordering::Relaxed),
);
nbt.put_float("view_range", *self.view_range.lock().await);
nbt.put_float("shadow_radius", *self.shadow_radius.lock().await);
nbt.put_float("shadow_strength", *self.shadow_strength.lock().await);
nbt.put_float("width", *self.width.lock().await);
nbt.put_float("height", *self.height.lock().await);
nbt.put_int(
"glow_color_override",
self.glow_color_override.load(Ordering::Relaxed),
);
let billboard_str = match self.billboard.load(Ordering::Relaxed) {
1 => "vertical",
2 => "horizontal",
3 => "center",
_ => "fixed",
};
nbt.put_string("billboard", billboard_str.to_string());
let mut transform = NbtCompound::new();
let translation = *self.translation.lock().await;
transform.put(
"translation",
NbtTag::List(vec![
translation.x.into(),
translation.y.into(),
translation.z.into(),
]),
);
let scale = *self.scale.lock().await;
transform.put(
"scale",
NbtTag::List(vec![scale.x.into(), scale.y.into(), scale.z.into()]),
);
let left_rot = *self.left_rotation.lock().await;
transform.put(
"left_rotation",
NbtTag::List(vec![
left_rot[0].into(),
left_rot[1].into(),
left_rot[2].into(),
left_rot[3].into(),
]),
);
let right_rot = *self.right_rotation.lock().await;
transform.put(
"right_rotation",
NbtTag::List(vec![
right_rot[0].into(),
right_rot[1].into(),
right_rot[2].into(),
right_rot[3].into(),
]),
);
nbt.put("transformation", NbtTag::Compound(transform));
}
pub async fn read_display_nbt(&self, nbt: &NbtCompound) {
self.entity.read_nbt_non_mut(nbt).await;
if let Some(dur) = nbt.get_int("interpolation_duration") {
self.interpolation_duration.store(dur, Ordering::Relaxed);
}
if let Some(start) = nbt.get_int("start_interpolation") {
self.interpolation_start_delta_ticks
.store(start, Ordering::Relaxed);
}
if let Some(vr) = nbt.get_float("view_range") {
*self.view_range.lock().await = vr;
}
if let Some(sr) = nbt.get_float("shadow_radius") {
*self.shadow_radius.lock().await = sr;
}
if let Some(ss) = nbt.get_float("shadow_strength") {
*self.shadow_strength.lock().await = ss;
}
if let Some(w) = nbt.get_float("width") {
*self.width.lock().await = w;
}
if let Some(h) = nbt.get_float("height") {
*self.height.lock().await = h;
}
if let Some(glow) = nbt.get_int("glow_color_override") {
self.glow_color_override.store(glow, Ordering::Relaxed);
}
if let Some(bb) = nbt.get_string("billboard") {
let mode = match bb {
"vertical" => 1,
"horizontal" => 2,
"center" => 3,
_ => 0,
};
self.billboard.store(mode, Ordering::Relaxed);
}
if let Some(transform) = nbt.get_compound("transformation") {
if let Some(t_list) = transform.get_list("translation")
&& t_list.len() >= 3
{
let x = t_list[0].extract_float().unwrap_or(0.0);
let y = t_list[1].extract_float().unwrap_or(0.0);
let z = t_list[2].extract_float().unwrap_or(0.0);
*self.translation.lock().await = Vector3::new(x, y, z);
}
if let Some(s_list) = transform.get_list("scale")
&& s_list.len() >= 3
{
let x = s_list[0].extract_float().unwrap_or(1.0);
let y = s_list[1].extract_float().unwrap_or(1.0);
let z = s_list[2].extract_float().unwrap_or(1.0);
*self.scale.lock().await = Vector3::new(x, y, z);
}
if let Some(lr_list) = transform.get_list("left_rotation")
&& lr_list.len() >= 4
{
let x = lr_list[0].extract_float().unwrap_or(0.0);
let y = lr_list[1].extract_float().unwrap_or(0.0);
let z = lr_list[2].extract_float().unwrap_or(0.0);
let w = lr_list[3].extract_float().unwrap_or(1.0);
*self.left_rotation.lock().await = [x, y, z, w];
}
if let Some(rr_list) = transform.get_list("right_rotation")
&& rr_list.len() >= 4
{
let x = rr_list[0].extract_float().unwrap_or(0.0);
let y = rr_list[1].extract_float().unwrap_or(0.0);
let z = rr_list[2].extract_float().unwrap_or(0.0);
let w = rr_list[3].extract_float().unwrap_or(1.0);
*self.right_rotation.lock().await = [x, y, z, w];
}
}
}
}
pub struct BlockDisplayEntity {
pub display: DisplayEntity,
pub block_state: AtomicI32,
}
impl BlockDisplayEntity {
pub fn new(entity: Entity) -> Arc<Self> {
Arc::new(Self {
display: DisplayEntity::new(entity),
block_state: AtomicI32::new(0),
})
}
}
impl NBTStorage for BlockDisplayEntity {
fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
self.display.write_display_nbt(nbt).await;
nbt.put_int("block_state", self.block_state.load(Ordering::Relaxed));
})
}
fn read_nbt<'a>(&'a mut self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
self.read_nbt_non_mut(nbt).await;
})
}
fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
self.display.read_display_nbt(nbt).await;
if let Some(state) = nbt.get_int("block_state") {
self.block_state.store(state, Ordering::Relaxed);
}
})
}
}
impl EntityBase for BlockDisplayEntity {
fn tick<'a>(
&'a self,
_caller: &'a Arc<dyn EntityBase>,
_server: &'a Server,
) -> EntityBaseFuture<'a, ()> {
Box::pin(async move {})
}
fn init_data_tracker(&self) -> EntityBaseFuture<'_, ()> {
Box::pin(async move {
self.display.init_display_data_tracker().await;
self.display.entity.send_meta_data(
&[Metadata::new(
TrackedData::BLOCK_STATE,
MetaDataType::BLOCK_STATE,
VarInt(self.block_state.load(Ordering::Relaxed)),
)],
None,
);
})
}
fn get_entity(&self) -> &Entity {
&self.display.entity
}
fn get_living_entity(&self) -> Option<&LivingEntity> {
None
}
fn as_nbt_storage(&self) -> &dyn NBTStorage {
self
}
fn cast_any(&self) -> &dyn std::any::Any {
self
}
fn is_pushable(&self) -> bool {
false
}
fn is_pushed_by_fluids(&self) -> bool {
false
}
fn can_hit(&self) -> bool {
false
}
fn is_immune_to_explosion(&self) -> bool {
true
}
fn damage_with_context<'a>(
&'a self,
_caller: &'a dyn EntityBase,
_amount: f32,
_damage_type: DamageType,
_position: Option<Vector3<f64>>,
_source: Option<&'a dyn EntityBase>,
_cause: Option<&'a dyn EntityBase>,
) -> EntityBaseFuture<'a, bool> {
Box::pin(async move { false })
}
}
pub struct ItemDisplayEntity {
pub display: DisplayEntity,
pub item_stack: Mutex<ItemStack>,
pub item_display: AtomicU8,
}
impl ItemDisplayEntity {
pub fn new(entity: Entity) -> Arc<Self> {
Arc::new(Self {
display: DisplayEntity::new(entity),
item_stack: Mutex::new(ItemStack::new(0, &pumpkin_data::item::Item::AIR)),
item_display: AtomicU8::new(0),
})
}
}
impl NBTStorage for ItemDisplayEntity {
fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
self.display.write_display_nbt(nbt).await;
let display_mode_str = match self.item_display.load(Ordering::Relaxed) {
1 => "thirdperson_lefthand",
2 => "thirdperson_righthand",
3 => "firstperson_lefthand",
4 => "firstperson_righthand",
5 => "head",
6 => "gui",
7 => "ground",
8 => "fixed",
_ => "none",
};
nbt.put_string("item_display", display_mode_str.to_string());
})
}
fn read_nbt<'a>(&'a mut self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
self.read_nbt_non_mut(nbt).await;
})
}
fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
self.display.read_display_nbt(nbt).await;
if let Some(mode_str) = nbt.get_string("item_display") {
let mode = match mode_str {
"thirdperson_lefthand" => 1,
"thirdperson_righthand" => 2,
"firstperson_lefthand" => 3,
"firstperson_righthand" => 4,
"head" => 5,
"gui" => 6,
"ground" => 7,
"fixed" => 8,
_ => 0,
};
self.item_display.store(mode, Ordering::Relaxed);
}
})
}
}
impl EntityBase for ItemDisplayEntity {
fn tick<'a>(
&'a self,
_caller: &'a Arc<dyn EntityBase>,
_server: &'a Server,
) -> EntityBaseFuture<'a, ()> {
Box::pin(async move {})
}
fn init_data_tracker(&self) -> EntityBaseFuture<'_, ()> {
Box::pin(async move {
self.display.init_display_data_tracker().await;
self.display.entity.send_meta_data(
&[Metadata::new(
TrackedData::ITEM,
MetaDataType::ITEM_STACK,
ItemStackSerializer::from(self.item_stack.lock().await.clone()),
)],
None,
);
self.display.entity.send_meta_data(
&[Metadata::new(
TrackedData::ITEM_DISPLAY,
MetaDataType::BYTE,
self.item_display.load(Ordering::Relaxed),
)],
None,
);
})
}
fn get_entity(&self) -> &Entity {
&self.display.entity
}
fn get_living_entity(&self) -> Option<&LivingEntity> {
None
}
fn as_nbt_storage(&self) -> &dyn NBTStorage {
self
}
fn cast_any(&self) -> &dyn std::any::Any {
self
}
fn is_pushable(&self) -> bool {
false
}
fn is_pushed_by_fluids(&self) -> bool {
false
}
fn can_hit(&self) -> bool {
false
}
fn is_immune_to_explosion(&self) -> bool {
true
}
fn damage_with_context<'a>(
&'a self,
_caller: &'a dyn EntityBase,
_amount: f32,
_damage_type: DamageType,
_position: Option<Vector3<f64>>,
_source: Option<&'a dyn EntityBase>,
_cause: Option<&'a dyn EntityBase>,
) -> EntityBaseFuture<'a, bool> {
Box::pin(async move { false })
}
}
pub struct TextDisplayEntity {
pub display: DisplayEntity,
pub text: Mutex<TextComponent>,
pub line_width: AtomicI32,
pub background: AtomicI32,
pub text_opacity: AtomicI8,
pub flags: AtomicU8,
}
impl TextDisplayEntity {
pub fn new(entity: Entity) -> Arc<Self> {
Arc::new(Self {
display: DisplayEntity::new(entity),
text: Mutex::new(TextComponent::text("")),
line_width: AtomicI32::new(200),
background: AtomicI32::new(1_073_741_824),
text_opacity: AtomicI8::new(-1),
flags: AtomicU8::new(0),
})
}
}
impl NBTStorage for TextDisplayEntity {
fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
self.display.write_display_nbt(nbt).await;
let text_json_res = pumpkin_util::serde_json::to_string(&*self.text.lock().await);
if let Ok(text_json) = text_json_res {
nbt.put_string("text", text_json);
}
nbt.put_int("line_width", self.line_width.load(Ordering::Relaxed));
nbt.put_int("background", self.background.load(Ordering::Relaxed));
nbt.put_byte("text_opacity", self.text_opacity.load(Ordering::Relaxed));
let flags = self.flags.load(Ordering::Relaxed);
nbt.put_bool("shadow", flags & 1 != 0);
nbt.put_bool("see_through", flags & 2 != 0);
nbt.put_bool("default_background", flags & 4 != 0);
let align_str = if flags & 8 != 0 {
"left"
} else if flags & 16 != 0 {
"right"
} else {
"center"
};
nbt.put_string("alignment", align_str.to_string());
})
}
fn read_nbt<'a>(&'a mut self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
self.read_nbt_non_mut(nbt).await;
})
}
fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
self.display.read_display_nbt(nbt).await;
if let Some(text_json) = nbt.get_string("text")
&& let Ok(component) = pumpkin_util::serde_json::from_str(text_json)
{
*self.text.lock().await = component;
}
if let Some(lw) = nbt.get_int("line_width") {
self.line_width.store(lw, Ordering::Relaxed);
}
if let Some(bg) = nbt.get_int("background") {
self.background.store(bg, Ordering::Relaxed);
}
if let Some(opacity) = nbt.get_byte("text_opacity") {
self.text_opacity.store(opacity, Ordering::Relaxed);
}
let mut flags = 0u8;
if nbt.get_bool("shadow").unwrap_or(false) {
flags |= 1;
}
if nbt.get_bool("see_through").unwrap_or(false) {
flags |= 2;
}
if nbt.get_bool("default_background").unwrap_or(false) {
flags |= 4;
}
if let Some(align) = nbt.get_string("alignment") {
match align {
"left" => flags |= 8,
"right" => flags |= 16,
_ => {}
}
}
self.flags.store(flags, Ordering::Relaxed);
})
}
}
impl EntityBase for TextDisplayEntity {
fn tick<'a>(
&'a self,
_caller: &'a Arc<dyn EntityBase>,
_server: &'a Server,
) -> EntityBaseFuture<'a, ()> {
Box::pin(async move {})
}
fn init_data_tracker(&self) -> EntityBaseFuture<'_, ()> {
Box::pin(async move {
self.display.init_display_data_tracker().await;
let text = self.text.lock().await.clone();
self.display.entity.send_meta_data(
&[Metadata::new(
TrackedData::TEXT,
MetaDataType::COMPONENT,
text,
)],
None,
);
self.display.entity.send_meta_data(
&[Metadata::new(
TrackedData::LINE_WIDTH,
MetaDataType::INT,
VarInt(self.line_width.load(Ordering::Relaxed)),
)],
None,
);
self.display.entity.send_meta_data(
&[Metadata::new(
TrackedData::BACKGROUND,
MetaDataType::INT,
VarInt(self.background.load(Ordering::Relaxed)),
)],
None,
);
self.display.entity.send_meta_data(
&[Metadata::new(
TrackedData::TEXT_OPACITY,
MetaDataType::BYTE,
self.text_opacity.load(Ordering::Relaxed) as u8,
)],
None,
);
self.display.entity.send_meta_data(
&[Metadata::new(
TrackedData::TEXT_DISPLAY_FLAGS,
MetaDataType::BYTE,
self.flags.load(Ordering::Relaxed),
)],
None,
);
})
}
fn get_entity(&self) -> &Entity {
&self.display.entity
}
fn get_living_entity(&self) -> Option<&LivingEntity> {
None
}
fn as_nbt_storage(&self) -> &dyn NBTStorage {
self
}
fn cast_any(&self) -> &dyn std::any::Any {
self
}
fn is_pushable(&self) -> bool {
false
}
fn is_pushed_by_fluids(&self) -> bool {
false
}
fn can_hit(&self) -> bool {
false
}
fn is_immune_to_explosion(&self) -> bool {
true
}
fn damage_with_context<'a>(
&'a self,
_caller: &'a dyn EntityBase,
_amount: f32,
_damage_type: DamageType,
_position: Option<Vector3<f64>>,
_source: Option<&'a dyn EntityBase>,
_cause: Option<&'a dyn EntityBase>,
) -> EntityBaseFuture<'a, bool> {
Box::pin(async move { false })
}
}

View File

@ -1,13 +1,22 @@
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
use crate::entity::player::Player;
use crate::entity::{
Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture, living::LivingEntity,
};
use crossbeam::atomic::AtomicCell;
use pumpkin_data::BlockDirection;
use pumpkin_data::damage::DamageType;
use pumpkin_data::entity::EntityType;
use pumpkin_data::item::Item;
use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::meta_data_type::MetaDataType;
use pumpkin_data::sound::Sound;
use pumpkin_data::tracked_data::TrackedData;
use pumpkin_nbt::compound::NbtCompound;
use pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer;
use pumpkin_protocol::java::client::play::{CSetEntityMetadata, Metadata};
use pumpkin_util::math::vector3::Vector3;
use tokio::sync::Mutex;
@ -50,11 +59,167 @@ impl ItemFrameEntity {
}
}
pub const fn is_glow(&self) -> bool {
self.entity.entity_type.id == EntityType::GLOW_ITEM_FRAME.id
}
pub const fn get_add_item_sound(&self) -> Sound {
if self.is_glow() {
Sound::EntityGlowItemFrameAddItem
} else {
Sound::EntityItemFrameAddItem
}
}
pub const fn get_remove_item_sound(&self) -> Sound {
if self.is_glow() {
Sound::EntityGlowItemFrameRemoveItem
} else {
Sound::EntityItemFrameRemoveItem
}
}
pub const fn get_rotate_item_sound(&self) -> Sound {
if self.is_glow() {
Sound::EntityGlowItemFrameRotateItem
} else {
Sound::EntityItemFrameRotateItem
}
}
pub const fn get_break_sound(&self) -> Sound {
if self.is_glow() {
Sound::EntityGlowItemFrameBreak
} else {
Sound::EntityItemFrameBreak
}
}
pub const fn get_place_sound(&self) -> Sound {
if self.is_glow() {
Sound::EntityGlowItemFramePlace
} else {
Sound::EntityItemFramePlace
}
}
pub fn get_facing(&self) -> BlockDirection {
BlockDirection::from_index(self.facing.load(Ordering::Relaxed))
.unwrap_or(Self::DEFAULT_FACING)
}
pub fn set_facing(&self, facing: BlockDirection) {
let index = facing.to_index();
self.facing.store(index, Ordering::Relaxed);
self.entity.data.store(i32::from(index), Ordering::Relaxed);
}
pub async fn get_item(&self) -> ItemStack {
self.item_stack.lock().await.clone()
}
pub async fn set_item(&self, mut item_stack: ItemStack, update_neighbours: bool) {
if !item_stack.is_empty() {
item_stack.item_count = 1;
}
let play_sound = !item_stack.is_empty();
let item_serializer = ItemStackSerializer::from(item_stack.clone());
*self.item_stack.lock().await = item_stack;
self.entity.send_meta_data(
&[Metadata::new(
TrackedData::ITEM,
MetaDataType::ITEM_STACK,
&item_serializer,
)],
None,
);
if play_sound {
self.entity.play_sound(self.get_add_item_sound());
}
if update_neighbours {
let world = self.entity.world.load();
let pos = self.entity.block_pos.load();
world.update_neighbors(&pos, None).await;
}
}
pub fn get_rotation(&self) -> u8 {
self.rotation.load(Ordering::Relaxed) % 8
}
pub async fn set_rotation(&self, rotation: u8, update_neighbours: bool) {
let rot = rotation % 8;
self.rotation.store(rot, Ordering::Relaxed);
self.entity.send_meta_data(
&[Metadata::new(
TrackedData::ROTATION,
MetaDataType::INT,
rot as i32,
)],
None,
);
if update_neighbours {
let world = self.entity.world.load();
let pos = self.entity.block_pos.load();
world.update_neighbors(&pos, None).await;
}
}
pub fn get_drop_chance(&self) -> f32 {
self.item_drop_chance.load()
}
pub fn set_drop_chance(&self, chance: f32) {
self.item_drop_chance.store(chance);
}
pub fn is_fixed(&self) -> bool {
self.fixed.load(Ordering::Relaxed)
}
pub fn set_fixed(&self, fixed: bool) {
self.fixed.store(fixed, Ordering::Relaxed);
}
pub fn is_invisible(&self) -> bool {
self.invisible.load(Ordering::Relaxed)
}
pub fn set_invisible(&self, invisible: bool) {
self.invisible.store(invisible, Ordering::Relaxed);
}
pub fn get_frame_item_stack(&self) -> ItemStack {
if self.is_glow() {
ItemStack::new(1, &Item::GLOW_ITEM_FRAME)
} else {
ItemStack::new(1, &Item::ITEM_FRAME)
}
}
pub fn get_frame_item_stack_with_data(&self) -> ItemStack {
let mut stack = self.get_frame_item_stack();
if let Some(custom_name) = self.entity.custom_name.load().as_ref().clone() {
stack.set_custom_name(custom_name.to_pretty_console());
}
stack
}
pub async fn get_pick_result(&self) -> ItemStack {
let framed_stack = self.get_item().await;
if framed_stack.is_empty() {
self.get_frame_item_stack_with_data()
} else {
framed_stack
}
}
/// The comparator signal this frame produces.
///
/// Vanilla: `getItem().isEmpty() ? 0 : getRotation() % 8 + 1`.
@ -65,6 +230,41 @@ impl ItemFrameEntity {
self.rotation.load(Ordering::Relaxed) % 8 + 1
}
}
pub async fn drop_item(&self, caused_by: Option<&dyn EntityBase>, with_frame: bool) {
if self.is_fixed() {
return;
}
let item_stack = self.get_item().await;
self.set_item(ItemStack::EMPTY.clone(), true).await;
let is_creative_player = caused_by.is_some_and(|s| {
s.cast_any()
.downcast_ref::<Player>()
.is_some_and(Player::is_creative)
});
if is_creative_player {
return;
}
let world = self.entity.world.load();
let pos = self.entity.block_pos.load();
if with_frame {
world
.drop_stack(&pos, self.get_frame_item_stack_with_data())
.await;
}
if !item_stack.is_empty() {
let drop_chance = self.item_drop_chance.load();
if rand::random::<f32>() < drop_chance {
world.drop_stack(&pos, item_stack).await;
}
}
}
}
impl NBTStorage for ItemFrameEntity {
@ -77,8 +277,8 @@ impl NBTStorage for ItemFrameEntity {
let mut item_compound = NbtCompound::new();
item.write_item_stack(&mut item_compound);
nbt.put_compound("Item", item_compound);
nbt.put_float("ItemDropChance", self.item_drop_chance.load());
}
nbt.put_float("ItemDropChance", self.item_drop_chance.load());
nbt.put_byte("ItemRotation", self.rotation.load(Ordering::Relaxed) as i8);
nbt.put_byte("Facing", self.facing.load(Ordering::Relaxed) as i8);
nbt.put_bool("Invisible", self.invisible.load(Ordering::Relaxed));
@ -124,19 +324,133 @@ impl EntityBase for ItemFrameEntity {
None
}
fn init_data_tracker(&self) -> EntityBaseFuture<'_, ()> {
Box::pin(async {
let item_serializer = ItemStackSerializer::from(self.item_stack.lock().await.clone());
let rotation = self.get_rotation() as i32;
self.entity.send_meta_data(
&[Metadata::new(
TrackedData::ITEM,
MetaDataType::ITEM_STACK,
&item_serializer,
)],
None,
);
self.entity.send_meta_data(
&[Metadata::new(
TrackedData::ROTATION,
MetaDataType::INT,
rotation,
)],
None,
);
})
}
fn send_java_spawn_packet<'a>(
&'a self,
client: &'a crate::net::java::JavaClient,
) -> EntityBaseFuture<'a, ()> {
Box::pin(async move {
client
.enqueue_packet(&self.entity.create_spawn_packet())
.await;
let item_serializer = ItemStackSerializer::from(self.item_stack.lock().await.clone());
let rotation = self.get_rotation() as i32;
let mut data = Vec::new();
let meta_item =
Metadata::new(TrackedData::ITEM, MetaDataType::ITEM_STACK, item_serializer);
let meta_rot = Metadata::new(TrackedData::ROTATION, MetaDataType::INT, rotation);
let ver = client.version.load();
if meta_item.write(&mut data, &ver).is_ok() && meta_rot.write(&mut data, &ver).is_ok() {
data.push(255);
client
.enqueue_packet(&CSetEntityMetadata::new(
self.entity.entity_id.into(),
data.into(),
))
.await;
}
})
}
fn interact<'a>(
&'a self,
player: &'a Arc<Player>,
item_stack: &'a mut ItemStack,
) -> EntityBaseFuture<'a, bool> {
Box::pin(async move {
if self.is_fixed() {
return false;
}
let frame_has_item = !self.get_item().await.is_empty();
let has_held_item = !item_stack.is_empty();
if frame_has_item {
let new_rot = self.get_rotation() + 1;
self.set_rotation(new_rot, true).await;
self.entity.play_sound(self.get_rotate_item_sound());
true
} else if has_held_item && !self.entity.removed.load(Ordering::Relaxed) {
let mut new_stack = item_stack.clone();
new_stack.item_count = 1;
self.set_item(new_stack, true).await;
if !player.is_creative() {
item_stack.decrement(1);
}
true
} else {
false
}
})
}
fn damage_with_context<'a>(
&'a self,
_caller: &'a dyn EntityBase,
_amount: f32,
_damage_type: DamageType,
damage_type: DamageType,
_position: Option<Vector3<f64>>,
_source: Option<&'a dyn EntityBase>,
source: Option<&'a dyn EntityBase>,
_cause: Option<&'a dyn EntityBase>,
) -> EntityBaseFuture<'a, bool> {
Box::pin(async {
// TODO: vanilla pops the displayed item first and only removes the
// frame itself when hit while empty; both should drop their items.
Box::pin(async move {
let fixed = self.is_fixed();
let is_creative_player = source.is_some_and(|s| {
s.cast_any()
.downcast_ref::<Player>()
.is_some_and(Player::is_creative)
});
let bypasses_invuln =
damage_type == DamageType::OUT_OF_WORLD || damage_type == DamageType::GENERIC_KILL;
if fixed {
if !bypasses_invuln && !is_creative_player {
return false;
}
self.drop_item(source, true).await;
self.entity.remove().await;
return true;
}
let has_item = !self.get_item().await.is_empty();
let is_explosion =
damage_type == DamageType::EXPLOSION || damage_type == DamageType::PLAYER_EXPLOSION;
if !is_explosion && has_item {
self.drop_item(source, false).await;
self.entity.play_sound(self.get_remove_item_sound());
} else {
self.drop_item(source, true).await;
self.entity.play_sound(self.get_break_sound());
self.entity.remove().await;
}
true
})
}

View File

@ -1,4 +1,5 @@
pub mod armor_stand;
pub mod display;
pub mod end_crystal;
pub mod item_frame;
pub mod leash_knot;

View File

@ -0,0 +1,313 @@
use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
};
use tokio::sync::Mutex;
use uuid::Uuid;
use pumpkin_data::{
damage::DamageType, item_stack::ItemStack, meta_data_type::MetaDataType,
tracked_data::TrackedData,
};
use pumpkin_nbt::{compound::NbtCompound, tag::NbtTag};
use pumpkin_protocol::java::client::play::Metadata;
use pumpkin_util::math::{
boundingbox::{BoundingBox, EntityDimensions},
vector3::Vector3,
};
use crate::{
entity::{
Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture, living::LivingEntity,
player::Player,
},
server::Server,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PlayerAction {
pub player: Uuid,
pub timestamp: i64,
}
impl PlayerAction {
#[must_use]
pub fn to_nbt(&self) -> NbtCompound {
let mut compound = NbtCompound::new();
compound.put_uuid("player", self.player);
compound.put_long("timestamp", self.timestamp);
compound
}
#[must_use]
pub fn from_nbt(compound: &NbtCompound) -> Option<Self> {
let player = compound.get_uuid("player")?;
let timestamp = compound.get_long("timestamp")?;
Some(Self { player, timestamp })
}
}
pub struct InteractionEntity {
pub entity: Entity,
pub width: Mutex<f32>,
pub height: Mutex<f32>,
pub response: AtomicBool,
pub attack: Mutex<Option<PlayerAction>>,
pub interaction: Mutex<Option<PlayerAction>>,
}
impl InteractionEntity {
pub fn new(entity: Entity) -> Arc<Self> {
entity.no_clip.store(true, Ordering::Relaxed);
let width = 1.0;
let height = 1.0;
let dimensions = EntityDimensions::new(width, height, height * 0.85);
let pos = entity.pos.load();
let aabb = BoundingBox::new_from_pos(pos.x, pos.y, pos.z, &dimensions);
entity.bounding_box.store(aabb);
Arc::new(Self {
entity,
width: Mutex::new(width),
height: Mutex::new(height),
response: AtomicBool::new(false),
attack: Mutex::new(None),
interaction: Mutex::new(None),
})
}
pub async fn get_width(&self) -> f32 {
*self.width.lock().await
}
pub async fn set_width(&self, width: f32) {
*self.width.lock().await = width;
self.update_dimensions().await;
self.entity.send_meta_data(
&[Metadata::new(
TrackedData::WIDTH,
MetaDataType::FLOAT,
width,
)],
None,
);
}
pub async fn get_height(&self) -> f32 {
*self.height.lock().await
}
pub async fn set_height(&self, height: f32) {
*self.height.lock().await = height;
self.update_dimensions().await;
self.entity.send_meta_data(
&[Metadata::new(
TrackedData::HEIGHT,
MetaDataType::FLOAT,
height,
)],
None,
);
}
pub fn get_response(&self) -> bool {
self.response.load(Ordering::Relaxed)
}
pub fn set_response(&self, response: bool) {
self.response.store(response, Ordering::Relaxed);
self.entity.send_meta_data(
&[Metadata::new(
TrackedData::RESPONSE,
MetaDataType::BOOLEAN,
response,
)],
None,
);
}
pub async fn update_dimensions(&self) {
let width = *self.width.lock().await;
let height = *self.height.lock().await;
let dimensions = EntityDimensions::new(width, height, height * 0.85);
let pos = self.entity.pos.load();
let aabb = BoundingBox::new_from_pos(pos.x, pos.y, pos.z, &dimensions);
self.entity.bounding_box.store(aabb);
}
pub async fn get_last_attacker(&self) -> Option<PlayerAction> {
*self.attack.lock().await
}
pub async fn get_target(&self) -> Option<PlayerAction> {
*self.interaction.lock().await
}
}
impl NBTStorage for InteractionEntity {
fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
self.entity.write_nbt(nbt).await;
nbt.put_float("width", *self.width.lock().await);
nbt.put_float("height", *self.height.lock().await);
nbt.put_bool("response", self.response.load(Ordering::Relaxed));
let attack = *self.attack.lock().await;
if let Some(attack) = attack {
nbt.put("attack", NbtTag::Compound(attack.to_nbt()));
}
let interaction = *self.interaction.lock().await;
if let Some(interaction) = interaction {
nbt.put("interaction", NbtTag::Compound(interaction.to_nbt()));
}
})
}
fn read_nbt<'a>(&'a mut self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
self.read_nbt_non_mut(nbt).await;
})
}
fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
self.entity.read_nbt_non_mut(nbt).await;
let width = nbt.get_float("width").unwrap_or(1.0);
let height = nbt.get_float("height").unwrap_or(1.0);
let response = nbt.get_bool("response").unwrap_or(false);
*self.width.lock().await = width;
*self.height.lock().await = height;
self.response.store(response, Ordering::Relaxed);
self.update_dimensions().await;
if let Some(attack_compound) = nbt.get_compound("attack") {
*self.attack.lock().await = PlayerAction::from_nbt(attack_compound);
} else {
*self.attack.lock().await = None;
}
if let Some(interaction_compound) = nbt.get_compound("interaction") {
*self.interaction.lock().await = PlayerAction::from_nbt(interaction_compound);
} else {
*self.interaction.lock().await = None;
}
})
}
}
impl EntityBase for InteractionEntity {
fn tick<'a>(
&'a self,
_caller: &'a Arc<dyn EntityBase>,
_server: &'a Server,
) -> EntityBaseFuture<'a, ()> {
Box::pin(async move {})
}
fn init_data_tracker(&self) -> EntityBaseFuture<'_, ()> {
Box::pin(async move {
let width = *self.width.lock().await;
let height = *self.height.lock().await;
let response = self.response.load(Ordering::Relaxed);
self.entity.send_meta_data(
&[Metadata::new(
TrackedData::WIDTH,
MetaDataType::FLOAT,
width,
)],
None,
);
self.entity.send_meta_data(
&[Metadata::new(
TrackedData::HEIGHT,
MetaDataType::FLOAT,
height,
)],
None,
);
self.entity.send_meta_data(
&[Metadata::new(
TrackedData::RESPONSE,
MetaDataType::BOOLEAN,
response,
)],
None,
);
})
}
fn get_entity(&self) -> &Entity {
&self.entity
}
fn get_living_entity(&self) -> Option<&LivingEntity> {
None
}
fn as_nbt_storage(&self) -> &dyn NBTStorage {
self
}
fn cast_any(&self) -> &dyn std::any::Any {
self
}
fn is_pushable(&self) -> bool {
false
}
fn is_pushed_by_fluids(&self) -> bool {
false
}
fn can_hit(&self) -> bool {
true
}
fn is_immune_to_explosion(&self) -> bool {
true
}
fn damage_with_context<'a>(
&'a self,
_caller: &'a dyn EntityBase,
_amount: f32,
_damage_type: DamageType,
_position: Option<Vector3<f64>>,
source: Option<&'a dyn EntityBase>,
cause: Option<&'a dyn EntityBase>,
) -> EntityBaseFuture<'a, bool> {
Box::pin(async move {
let player = source
.or(cause)
.and_then(|e| e.cast_any().downcast_ref::<Player>());
if let Some(player) = player {
let timestamp = self.entity.world.load().level_time.lock().await.world_age as i64;
*self.attack.lock().await = Some(PlayerAction {
player: player.gameprofile.id,
timestamp,
});
}
false
})
}
fn interact<'a>(
&'a self,
player: &'a Arc<Player>,
_item_stack: &'a mut ItemStack,
) -> EntityBaseFuture<'a, bool> {
Box::pin(async move {
let timestamp = self.entity.world.load().level_time.lock().await.world_age as i64;
*self.interaction.lock().await = Some(PlayerAction {
player: player.gameprofile.id,
timestamp,
});
true
})
}
}

View File

@ -0,0 +1,500 @@
use std::collections::HashSet;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicI32, AtomicI64, Ordering};
use tokio::sync::Mutex;
use pumpkin_data::sound::{Sound, SoundCategory};
use pumpkin_data::world::WorldEvent;
use pumpkin_data::{Block, BlockId, BlockStateId};
use pumpkin_util::Difficulty;
use pumpkin_util::math::boundingbox::BoundingBox;
use pumpkin_util::math::position::BlockPos;
use pumpkin_util::math::vector3::Vector3;
use pumpkin_world::world::BlockFlags;
use rand::RngExt;
use crate::block::blocks::fire::FireBlockBase;
use crate::entity::player::Player;
use crate::entity::{Entity, EntityBase, EntityBaseFuture, NBTStorage};
use crate::server::Server;
use crate::world::World;
pub struct LightningBoltEntity {
entity: Entity,
life: AtomicI32,
seed: AtomicI64,
flashes: AtomicI32,
visual_only: AtomicBool,
cause: Mutex<Option<Arc<Player>>>,
hit_entities: Mutex<HashSet<i32>>,
blocks_set_on_fire: AtomicI32,
}
impl LightningBoltEntity {
pub fn new(entity: Entity) -> Self {
let seed = rand::rng().random::<i64>();
let flashes = rand::rng().random_range(1..=3);
Self {
entity,
life: AtomicI32::new(2),
seed: AtomicI64::new(seed),
flashes: AtomicI32::new(flashes),
visual_only: AtomicBool::new(false),
cause: Mutex::new(None),
hit_entities: Mutex::new(HashSet::new()),
blocks_set_on_fire: AtomicI32::new(0),
}
}
pub fn set_visual_only(&self, visual_only: bool) {
self.visual_only.store(visual_only, Ordering::Relaxed);
}
pub fn is_visual_only(&self) -> bool {
self.visual_only.load(Ordering::Relaxed)
}
pub async fn set_cause(&self, cause: Option<Arc<Player>>) {
*self.cause.lock().await = cause;
}
pub async fn get_cause(&self) -> Option<Arc<Player>> {
self.cause.lock().await.clone()
}
pub fn get_blocks_set_on_fire(&self) -> i32 {
self.blocks_set_on_fire.load(Ordering::Relaxed)
}
pub fn get_seed(&self) -> i64 {
self.seed.load(Ordering::Relaxed)
}
pub fn set_seed(&self, seed: i64) {
self.seed.store(seed, Ordering::Relaxed);
}
fn get_strike_position(&self) -> BlockPos {
let pos = self.entity.pos.load();
Vector3::new(pos.x, pos.y - 1.0e-6, pos.z).to_block_pos()
}
async fn power_lightning_rod(&self, world: &Arc<World>) {
let strike_pos = self.get_strike_position();
let block = world.get_block(&strike_pos);
if block == &Block::LIGHTNING_ROD {
crate::block::blocks::redstone::lightning_rod::LightningRodBlock::trigger(
world,
&strike_pos,
)
.await;
}
}
async fn spawn_fire(&self, world: &Arc<World>, additional_sources: i32) {
if self.visual_only.load(Ordering::Relaxed) {
return;
}
let pos = self.entity.block_pos.load();
let try_place = |p: BlockPos| async move {
if world.get_block_state(&p).is_air() && FireBlockBase::can_place_at(world, &p) {
let fire_block = FireBlockBase::get_fire_type(world, &p);
world
.set_block_state(&p, fire_block.default_state.id, BlockFlags::NOTIFY_ALL)
.await;
self.blocks_set_on_fire.fetch_add(1, Ordering::Relaxed);
}
};
try_place(pos).await;
for _ in 0..additional_sources {
let dx = rand::rng().random_range(-1..=1);
let dy = rand::rng().random_range(-1..=1);
let dz = rand::rng().random_range(-1..=1);
let nearby_pos = pos.offset(Vector3::new(dx, dy, dz));
try_place(nearby_pos).await;
}
}
async fn clear_copper_on_lightning_strike(&self, world: &Arc<World>) {
let strike_pos = self.get_strike_position();
let struck_state = world.get_block_state(&strike_pos);
let struck_block = struck_state.id.to_block();
let is_waxed = is_waxed_copper(struck_block.id);
let is_weathering = is_weathering_copper(struck_block.id);
if is_weathering || is_waxed {
if is_weathering && let Some(first_block_id) = get_first_stage_copper(struck_block.id) {
let first_block = first_block_id.to_block();
let offset = struck_state
.id
.as_u16()
.saturating_sub(struck_block.default_state.id.as_u16());
let new_state_id =
BlockStateId::new(first_block.default_state.id.as_u16() + offset)
.unwrap_or(first_block.default_state.id);
world
.set_block_state(&strike_pos, new_state_id, BlockFlags::NOTIFY_ALL)
.await;
}
let strikes_count = rand::rng().random_range(3..=5);
for _ in 0..strikes_count {
let step_count = rand::rng().random_range(1..=8);
self.random_walk_cleaning_copper(world, &strike_pos, step_count)
.await;
}
}
}
async fn random_walk_cleaning_copper(
&self,
world: &Arc<World>,
original_strike_pos: &BlockPos,
step_count: i32,
) {
let mut work_pos = *original_strike_pos;
for _ in 0..step_count {
if let Some(next_pos) = self.random_step_cleaning_copper(world, &work_pos).await {
work_pos = next_pos;
} else {
break;
}
}
}
async fn random_step_cleaning_copper(
&self,
world: &Arc<World>,
pos: &BlockPos,
) -> Option<BlockPos> {
let candidates = random_in_cube(10, pos, 1);
for candidate in candidates {
let state = world.get_block_state(&candidate);
let block = state.id.to_block();
if is_weathering_copper(block.id)
&& let Some(prev_block_id) = get_previous_stage_copper(block.id)
{
let prev_block = prev_block_id.to_block();
let offset = state
.id
.as_u16()
.saturating_sub(block.default_state.id.as_u16());
let new_state_id = BlockStateId::new(prev_block.default_state.id.as_u16() + offset)
.unwrap_or(prev_block.default_state.id);
world
.set_block_state(&candidate, new_state_id, BlockFlags::NOTIFY_ALL)
.await;
world.sync_world_event(WorldEvent::ParticlesElectricSpark, candidate, -1);
return Some(candidate);
}
}
None
}
}
impl NBTStorage for LightningBoltEntity {}
impl EntityBase for LightningBoltEntity {
fn tick<'a>(
&'a self,
_caller: &'a Arc<dyn EntityBase>,
_server: &'a Server,
) -> EntityBaseFuture<'a, ()> {
Box::pin(async move {
let entity = &self.entity;
let life = self.life.load(Ordering::Relaxed);
if life == 2 {
let world = entity.world.load();
let pos = entity.pos.load();
let pitch_thunder = 0.8 + rand::rng().random::<f32>() * 0.2;
world.play_sound_fine(
Sound::EntityLightningBoltThunder,
SoundCategory::Weather,
&pos,
10000.0,
pitch_thunder,
);
let pitch_impact = 0.5 + rand::rng().random::<f32>() * 0.2;
world.play_sound_fine(
Sound::EntityLightningBoltImpact,
SoundCategory::Weather,
&pos,
2.0,
pitch_impact,
);
let difficulty = world.level_info.load().difficulty;
if difficulty == Difficulty::Normal || difficulty == Difficulty::Hard {
self.spawn_fire(&world, 4).await;
}
self.power_lightning_rod(&world).await;
self.clear_copper_on_lightning_strike(&world).await;
}
let new_life = life - 1;
self.life.store(new_life, Ordering::Relaxed);
if new_life < 0 {
let flashes = self.flashes.load(Ordering::Relaxed);
if flashes == 0 {
entity.remove().await;
return;
} else if new_life < -rand::rng().random_range(0..10) {
self.flashes.store(flashes - 1, Ordering::Relaxed);
self.life.store(1, Ordering::Relaxed);
self.seed.store(rand::random::<i64>(), Ordering::Relaxed);
let world = entity.world.load();
self.spawn_fire(&world, 0).await;
}
}
let current_life = self.life.load(Ordering::Relaxed);
if current_life >= 0 && !self.visual_only.load(Ordering::Relaxed) {
let world = entity.world.load();
let pos = entity.pos.load();
let damage_box = BoundingBox::new(
Vector3::new(pos.x - 3.0, pos.y - 3.0, pos.z - 3.0),
Vector3::new(pos.x + 3.0, pos.y + 9.0, pos.z + 3.0),
);
let entities = world.get_all_at_box(&damage_box);
let mut hit_guard = self.hit_entities.lock().await;
for hit_entity in entities {
if hit_entity.get_entity().entity_id == entity.entity_id {
continue;
}
let hit_id = hit_entity.get_entity().entity_id;
hit_entity
.on_lightning_strike(hit_entity.as_ref(), self)
.await;
hit_guard.insert(hit_id);
}
}
})
}
fn init_data_tracker(&self) -> EntityBaseFuture<'_, ()> {
Box::pin(async {})
}
fn get_entity(&self) -> &Entity {
&self.entity
}
fn get_living_entity(&self) -> Option<&crate::entity::living::LivingEntity> {
None
}
fn as_nbt_storage(&self) -> &dyn NBTStorage {
self
}
fn cast_any(&self) -> &dyn std::any::Any {
self
}
}
fn random_in_cube(count: usize, center: &BlockPos, radius: i32) -> Vec<BlockPos> {
let mut list = Vec::with_capacity(count);
for _ in 0..count {
let dx = rand::rng().random_range(-radius..=radius);
let dy = rand::rng().random_range(-radius..=radius);
let dz = rand::rng().random_range(-radius..=radius);
list.push(center.offset(Vector3::new(dx, dy, dz)));
}
list
}
#[must_use]
pub const fn is_waxed_copper(id: BlockId) -> bool {
matches!(
id,
BlockId::WAXED_COPPER_BLOCK
| BlockId::WAXED_EXPOSED_COPPER
| BlockId::WAXED_WEATHERED_COPPER
| BlockId::WAXED_OXIDIZED_COPPER
| BlockId::WAXED_CHISELED_COPPER
| BlockId::WAXED_EXPOSED_CHISELED_COPPER
| BlockId::WAXED_WEATHERED_CHISELED_COPPER
| BlockId::WAXED_OXIDIZED_CHISELED_COPPER
| BlockId::WAXED_COPPER_GRATE
| BlockId::WAXED_EXPOSED_COPPER_GRATE
| BlockId::WAXED_WEATHERED_COPPER_GRATE
| BlockId::WAXED_OXIDIZED_COPPER_GRATE
| BlockId::WAXED_CUT_COPPER
| BlockId::WAXED_EXPOSED_CUT_COPPER
| BlockId::WAXED_WEATHERED_CUT_COPPER
| BlockId::WAXED_OXIDIZED_CUT_COPPER
| BlockId::WAXED_CUT_COPPER_STAIRS
| BlockId::WAXED_EXPOSED_CUT_COPPER_STAIRS
| BlockId::WAXED_WEATHERED_CUT_COPPER_STAIRS
| BlockId::WAXED_OXIDIZED_CUT_COPPER_STAIRS
| BlockId::WAXED_CUT_COPPER_SLAB
| BlockId::WAXED_EXPOSED_CUT_COPPER_SLAB
| BlockId::WAXED_WEATHERED_CUT_COPPER_SLAB
| BlockId::WAXED_OXIDIZED_CUT_COPPER_SLAB
| BlockId::WAXED_COPPER_BULB
| BlockId::WAXED_EXPOSED_COPPER_BULB
| BlockId::WAXED_WEATHERED_COPPER_BULB
| BlockId::WAXED_OXIDIZED_COPPER_BULB
| BlockId::WAXED_COPPER_DOOR
| BlockId::WAXED_EXPOSED_COPPER_DOOR
| BlockId::WAXED_WEATHERED_COPPER_DOOR
| BlockId::WAXED_OXIDIZED_COPPER_DOOR
| BlockId::WAXED_COPPER_TRAPDOOR
| BlockId::WAXED_EXPOSED_COPPER_TRAPDOOR
| BlockId::WAXED_WEATHERED_COPPER_TRAPDOOR
| BlockId::WAXED_OXIDIZED_COPPER_TRAPDOOR
)
}
#[must_use]
pub const fn is_weathering_copper(id: BlockId) -> bool {
matches!(
id,
BlockId::COPPER_BLOCK
| BlockId::EXPOSED_COPPER
| BlockId::WEATHERED_COPPER
| BlockId::OXIDIZED_COPPER
| BlockId::CHISELED_COPPER
| BlockId::EXPOSED_CHISELED_COPPER
| BlockId::WEATHERED_CHISELED_COPPER
| BlockId::OXIDIZED_CHISELED_COPPER
| BlockId::COPPER_GRATE
| BlockId::EXPOSED_COPPER_GRATE
| BlockId::WEATHERED_COPPER_GRATE
| BlockId::OXIDIZED_COPPER_GRATE
| BlockId::CUT_COPPER
| BlockId::EXPOSED_CUT_COPPER
| BlockId::WEATHERED_CUT_COPPER
| BlockId::OXIDIZED_CUT_COPPER
| BlockId::CUT_COPPER_STAIRS
| BlockId::EXPOSED_CUT_COPPER_STAIRS
| BlockId::WEATHERED_CUT_COPPER_STAIRS
| BlockId::OXIDIZED_CUT_COPPER_STAIRS
| BlockId::CUT_COPPER_SLAB
| BlockId::EXPOSED_CUT_COPPER_SLAB
| BlockId::WEATHERED_CUT_COPPER_SLAB
| BlockId::OXIDIZED_CUT_COPPER_SLAB
| BlockId::COPPER_BULB
| BlockId::EXPOSED_COPPER_BULB
| BlockId::WEATHERED_COPPER_BULB
| BlockId::OXIDIZED_COPPER_BULB
| BlockId::COPPER_DOOR
| BlockId::EXPOSED_COPPER_DOOR
| BlockId::WEATHERED_COPPER_DOOR
| BlockId::OXIDIZED_COPPER_DOOR
| BlockId::COPPER_TRAPDOOR
| BlockId::EXPOSED_COPPER_TRAPDOOR
| BlockId::WEATHERED_COPPER_TRAPDOOR
| BlockId::OXIDIZED_COPPER_TRAPDOOR
)
}
#[must_use]
pub const fn get_first_stage_copper(id: BlockId) -> Option<BlockId> {
match id {
BlockId::COPPER_BLOCK
| BlockId::EXPOSED_COPPER
| BlockId::WEATHERED_COPPER
| BlockId::OXIDIZED_COPPER => Some(BlockId::COPPER_BLOCK),
BlockId::CHISELED_COPPER
| BlockId::EXPOSED_CHISELED_COPPER
| BlockId::WEATHERED_CHISELED_COPPER
| BlockId::OXIDIZED_CHISELED_COPPER => Some(BlockId::CHISELED_COPPER),
BlockId::COPPER_GRATE
| BlockId::EXPOSED_COPPER_GRATE
| BlockId::WEATHERED_COPPER_GRATE
| BlockId::OXIDIZED_COPPER_GRATE => Some(BlockId::COPPER_GRATE),
BlockId::CUT_COPPER
| BlockId::EXPOSED_CUT_COPPER
| BlockId::WEATHERED_CUT_COPPER
| BlockId::OXIDIZED_CUT_COPPER => Some(BlockId::CUT_COPPER),
BlockId::CUT_COPPER_STAIRS
| BlockId::EXPOSED_CUT_COPPER_STAIRS
| BlockId::WEATHERED_CUT_COPPER_STAIRS
| BlockId::OXIDIZED_CUT_COPPER_STAIRS => Some(BlockId::CUT_COPPER_STAIRS),
BlockId::CUT_COPPER_SLAB
| BlockId::EXPOSED_CUT_COPPER_SLAB
| BlockId::WEATHERED_CUT_COPPER_SLAB
| BlockId::OXIDIZED_CUT_COPPER_SLAB => Some(BlockId::CUT_COPPER_SLAB),
BlockId::COPPER_BULB
| BlockId::EXPOSED_COPPER_BULB
| BlockId::WEATHERED_COPPER_BULB
| BlockId::OXIDIZED_COPPER_BULB => Some(BlockId::COPPER_BULB),
BlockId::COPPER_DOOR
| BlockId::EXPOSED_COPPER_DOOR
| BlockId::WEATHERED_COPPER_DOOR
| BlockId::OXIDIZED_COPPER_DOOR => Some(BlockId::COPPER_DOOR),
BlockId::COPPER_TRAPDOOR
| BlockId::EXPOSED_COPPER_TRAPDOOR
| BlockId::WEATHERED_COPPER_TRAPDOOR
| BlockId::OXIDIZED_COPPER_TRAPDOOR => Some(BlockId::COPPER_TRAPDOOR),
_ => None,
}
}
#[must_use]
pub const fn get_previous_stage_copper(id: BlockId) -> Option<BlockId> {
match id {
BlockId::OXIDIZED_COPPER => Some(BlockId::WEATHERED_COPPER),
BlockId::WEATHERED_COPPER => Some(BlockId::EXPOSED_COPPER),
BlockId::EXPOSED_COPPER => Some(BlockId::COPPER_BLOCK),
BlockId::OXIDIZED_CHISELED_COPPER => Some(BlockId::WEATHERED_CHISELED_COPPER),
BlockId::WEATHERED_CHISELED_COPPER => Some(BlockId::EXPOSED_CHISELED_COPPER),
BlockId::EXPOSED_CHISELED_COPPER => Some(BlockId::CHISELED_COPPER),
BlockId::OXIDIZED_COPPER_GRATE => Some(BlockId::WEATHERED_COPPER_GRATE),
BlockId::WEATHERED_COPPER_GRATE => Some(BlockId::EXPOSED_COPPER_GRATE),
BlockId::EXPOSED_COPPER_GRATE => Some(BlockId::COPPER_GRATE),
BlockId::OXIDIZED_CUT_COPPER => Some(BlockId::WEATHERED_CUT_COPPER),
BlockId::WEATHERED_CUT_COPPER => Some(BlockId::EXPOSED_CUT_COPPER),
BlockId::EXPOSED_CUT_COPPER => Some(BlockId::CUT_COPPER),
BlockId::OXIDIZED_CUT_COPPER_STAIRS => Some(BlockId::WEATHERED_CUT_COPPER_STAIRS),
BlockId::WEATHERED_CUT_COPPER_STAIRS => Some(BlockId::EXPOSED_CUT_COPPER_STAIRS),
BlockId::EXPOSED_CUT_COPPER_STAIRS => Some(BlockId::CUT_COPPER_STAIRS),
BlockId::OXIDIZED_CUT_COPPER_SLAB => Some(BlockId::WEATHERED_CUT_COPPER_SLAB),
BlockId::WEATHERED_CUT_COPPER_SLAB => Some(BlockId::EXPOSED_CUT_COPPER_SLAB),
BlockId::EXPOSED_CUT_COPPER_SLAB => Some(BlockId::CUT_COPPER_SLAB),
BlockId::OXIDIZED_COPPER_BULB => Some(BlockId::WEATHERED_COPPER_BULB),
BlockId::WEATHERED_COPPER_BULB => Some(BlockId::EXPOSED_COPPER_BULB),
BlockId::EXPOSED_COPPER_BULB => Some(BlockId::COPPER_BULB),
BlockId::OXIDIZED_COPPER_DOOR => Some(BlockId::WEATHERED_COPPER_DOOR),
BlockId::WEATHERED_COPPER_DOOR => Some(BlockId::EXPOSED_COPPER_DOOR),
BlockId::EXPOSED_COPPER_DOOR => Some(BlockId::COPPER_DOOR),
BlockId::OXIDIZED_COPPER_TRAPDOOR => Some(BlockId::WEATHERED_COPPER_TRAPDOOR),
BlockId::WEATHERED_COPPER_TRAPDOOR => Some(BlockId::EXPOSED_COPPER_TRAPDOOR),
BlockId::EXPOSED_COPPER_TRAPDOOR => Some(BlockId::COPPER_TRAPDOOR),
_ => None,
}
}

View File

@ -2106,7 +2106,7 @@ impl NBTStorage for LivingEntity {
};
// Persist current absorption amount
nbt.put("AbsorptionAmount", NbtTag::Float(self.absorption.load()));
nbt.put("fall_distance", NbtTag::Float(fall_distance));
nbt.put("FallDistance", NbtTag::Float(fall_distance));
{
let effects = self.active_effects.lock().await;
if !effects.is_empty() {
@ -2138,7 +2138,10 @@ impl NBTStorage for LivingEntity {
// Load fall distance, but if this entity is currently marked dead ensure we don't restore
// a lethal fall distance that would immediately re-kill on spawn.
let fd = nbt.get_float("fall_distance").unwrap_or(0.0);
let fd = nbt
.get_float("FallDistance")
.or_else(|| nbt.get_float("fall_distance"))
.unwrap_or(0.0);
if self.dead.load(Relaxed) {
self.fall_distance.store(0.0);
} else {

View File

@ -0,0 +1,125 @@
use std::sync::{Arc, atomic::Ordering};
use tokio::sync::Mutex;
use crate::{
entity::{Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture, living::LivingEntity},
net::{bedrock::BedrockClient, java::JavaClient},
server::Server,
};
use pumpkin_data::damage::DamageType;
use pumpkin_nbt::{compound::NbtCompound, tag::NbtTag};
use pumpkin_util::math::vector3::Vector3;
pub struct MarkerEntity {
pub entity: Entity,
pub data: Mutex<NbtCompound>,
}
impl MarkerEntity {
pub fn new(entity: Entity) -> Arc<Self> {
entity.no_clip.store(true, Ordering::Relaxed);
Arc::new(Self {
entity,
data: Mutex::new(NbtCompound::new()),
})
}
}
impl NBTStorage for MarkerEntity {
fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
self.entity.write_nbt(nbt).await;
let data = self.data.lock().await;
if !data.is_empty() {
nbt.put("data", NbtTag::Compound(data.clone()));
}
})
}
fn read_nbt<'a>(&'a mut self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
self.entity.read_nbt(nbt).await;
if let Some(data) = nbt.get_compound("data") {
*self.data.lock().await = data.clone();
}
})
}
fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
self.entity.read_nbt_non_mut(nbt).await;
if let Some(data) = nbt.get_compound("data") {
*self.data.lock().await = data.clone();
}
})
}
}
impl EntityBase for MarkerEntity {
fn tick<'a>(
&'a self,
_caller: &'a Arc<dyn EntityBase>,
_server: &'a Server,
) -> EntityBaseFuture<'a, ()> {
Box::pin(async move {})
}
fn init_data_tracker(&self) -> EntityBaseFuture<'_, ()> {
Box::pin(async move {})
}
fn get_entity(&self) -> &Entity {
&self.entity
}
fn get_living_entity(&self) -> Option<&LivingEntity> {
None
}
fn as_nbt_storage(&self) -> &dyn NBTStorage {
self
}
fn cast_any(&self) -> &dyn std::any::Any {
self
}
fn is_pushable(&self) -> bool {
false
}
fn is_pushed_by_fluids(&self) -> bool {
false
}
fn can_hit(&self) -> bool {
false
}
fn is_immune_to_explosion(&self) -> bool {
true
}
fn damage_with_context<'a>(
&'a self,
_caller: &'a dyn EntityBase,
_amount: f32,
_damage_type: DamageType,
_position: Option<Vector3<f64>>,
_source: Option<&'a dyn EntityBase>,
_cause: Option<&'a dyn EntityBase>,
) -> EntityBaseFuture<'a, bool> {
Box::pin(async move { false })
}
fn send_java_spawn_packet<'a>(&'a self, _client: &'a JavaClient) -> EntityBaseFuture<'a, ()> {
Box::pin(async move {})
}
fn send_bedrock_spawn_packet<'a>(
&'a self,
_client: &'a BedrockClient,
) -> EntityBaseFuture<'a, ()> {
Box::pin(async move {})
}
}

View File

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

View File

@ -163,6 +163,28 @@ impl Mob for CreeperEntity {
&self.mob_entity
}
fn mob_on_lightning_strike<'a>(
&'a self,
caller: &'a dyn EntityBase,
lightning: &'a crate::entity::lightning::LightningBoltEntity,
) -> EntityBaseFuture<'a, ()> {
Box::pin(async move {
self.charged.store(true, Ordering::Relaxed);
self.mob_entity.living_entity.entity.send_meta_data(
&[Metadata::new(
TrackedData::CHARGED,
MetaDataType::BOOLEAN,
true,
)],
None,
);
self.mob_entity
.living_entity
.on_lightning_strike(caller, lightning)
.await;
})
}
fn mob_tick<'a>(&'a self, _caller: &'a Arc<dyn EntityBase>) -> EntityBaseFuture<'a, ()> {
Box::pin(async move {
let entity = &self.mob_entity.living_entity.entity;

View File

@ -1,28 +1,74 @@
use std::sync::atomic::{AtomicI32, AtomicU8, Ordering};
use std::sync::{Arc, Weak};
use tokio::sync::Mutex;
use uuid::Uuid;
use pumpkin_data::entity::EntityType;
use pumpkin_data::meta_data_type::MetaDataType;
use pumpkin_data::sound::Sound;
use pumpkin_data::tracked_data::TrackedData;
use pumpkin_nbt::compound::NbtCompound;
use pumpkin_protocol::java::client::play::Metadata;
use pumpkin_util::math::vector3::Vector3;
use crate::entity::{
Entity, NBTStorage,
Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture,
ai::goal::{
active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal,
look_at_entity::LookAtEntityGoal, swim::SwimGoal, wander_around::WanderAroundGoal,
Controls, Goal, GoalFuture, active_target::ActiveTargetGoal,
look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal,
wander_around::WanderAroundGoal,
},
mob::{Mob, MobEntity},
projectile::evoker_fangs::EvokerFangsEntity,
r#type::from_type,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum IllagerSpell {
None = 0,
SummonVex = 1,
Fangs = 2,
Wololo = 3,
Disappear = 4,
Blindness = 5,
}
impl IllagerSpell {
#[must_use]
pub const fn from_u8(val: u8) -> Self {
match val {
1 => Self::SummonVex,
2 => Self::Fangs,
3 => Self::Wololo,
4 => Self::Disappear,
5 => Self::Blindness,
_ => Self::None,
}
}
}
pub struct EvokerEntity {
pub mob_entity: MobEntity,
spell_casting_tick_count: AtomicI32,
current_spell: AtomicU8,
wololo_target_id: Arc<Mutex<Option<i32>>>,
}
impl EvokerEntity {
pub fn new(entity: Entity) -> Arc<Self> {
let mob_entity = MobEntity::new(entity);
let evoker = Self { mob_entity };
let evoker = Self {
mob_entity,
spell_casting_tick_count: AtomicI32::new(0),
current_spell: AtomicU8::new(IllagerSpell::None as u8),
wololo_target_id: Arc::new(Mutex::new(None)),
};
let mob_arc = Arc::new(evoker);
let mob_weak: Weak<dyn Mob> = {
let mob_arc: Arc<dyn Mob> = mob_arc.clone();
Arc::downgrade(&mob_arc)
let mob_weak: Weak<Self> = Arc::downgrade(&mob_arc);
let mob_interface_weak: Weak<dyn Mob> = {
let mob_dyn: Arc<dyn Mob> = mob_arc.clone();
Arc::downgrade(&mob_dyn)
};
{
@ -33,13 +79,16 @@ impl EvokerEntity {
.unwrap_or_else(std::sync::PoisonError::into_inner);
goal_selector.add_goal(0, Box::new(SwimGoal::default()));
// Evokers have complex spell AI, but for now basic movement
goal_selector.add_goal(5, Box::new(WanderAroundGoal::new(1.0)));
goal_selector.add_goal(1, Box::new(EvokerCastingSpellGoal::new(mob_weak.clone())));
goal_selector.add_goal(4, Box::new(EvokerSummonSpellGoal::new(mob_weak.clone())));
goal_selector.add_goal(5, Box::new(EvokerAttackSpellGoal::new(mob_weak.clone())));
goal_selector.add_goal(6, Box::new(EvokerWololoSpellGoal::new(mob_weak)));
goal_selector.add_goal(8, Box::new(WanderAroundGoal::new(0.6)));
goal_selector.add_goal(
6,
LookAtEntityGoal::with_default(mob_weak.clone(), &EntityType::PLAYER, 8.0),
9,
LookAtEntityGoal::with_default(mob_interface_weak, &EntityType::PLAYER, 3.0),
);
goal_selector.add_goal(7, Box::new(RandomLookAroundGoal::default()));
goal_selector.add_goal(10, Box::new(RandomLookAroundGoal::default()));
let mut target_selector = mob_arc
.mob_entity
@ -62,12 +111,511 @@ impl EvokerEntity {
mob_arc
}
pub fn is_casting_spell(&self) -> bool {
self.spell_casting_tick_count.load(Ordering::Relaxed) > 0
}
impl NBTStorage for EvokerEntity {}
pub fn set_is_casting_spell(&self, spell: IllagerSpell) {
self.current_spell.store(spell as u8, Ordering::Relaxed);
let entity = &self.mob_entity.living_entity.entity;
entity.send_meta_data(
&[Metadata::new(
TrackedData::SPELL_CASTING_ID,
MetaDataType::BYTE,
spell as u8 as i8,
)],
None,
);
}
pub fn get_current_spell(&self) -> IllagerSpell {
IllagerSpell::from_u8(self.current_spell.load(Ordering::Relaxed))
}
pub fn get_spell_casting_time(&self) -> i32 {
self.spell_casting_tick_count.load(Ordering::Relaxed)
}
pub fn set_spell_casting_time(&self, ticks: i32) {
self.spell_casting_tick_count
.store(ticks, Ordering::Relaxed);
}
}
impl NBTStorage for EvokerEntity {
fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
self.mob_entity.living_entity.entity.write_nbt(nbt).await;
nbt.put_int("SpellTicks", self.get_spell_casting_time());
})
}
fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
self.mob_entity
.living_entity
.entity
.read_nbt_non_mut(nbt)
.await;
if let Some(ticks) = nbt.get_int("SpellTicks") {
self.set_spell_casting_time(ticks);
}
})
}
}
impl Mob for EvokerEntity {
fn get_mob_entity(&self) -> &MobEntity {
&self.mob_entity
}
fn mob_tick<'a>(&'a self, _caller: &'a Arc<dyn EntityBase>) -> EntityBaseFuture<'a, ()> {
Box::pin(async move {
let ticks = self.spell_casting_tick_count.load(Ordering::Relaxed);
if ticks > 0 {
self.spell_casting_tick_count
.store(ticks - 1, Ordering::Relaxed);
}
})
}
}
pub struct EvokerCastingSpellGoal {
evoker: Weak<EvokerEntity>,
}
impl EvokerCastingSpellGoal {
#[must_use]
pub const fn new(evoker: Weak<EvokerEntity>) -> Self {
Self { evoker }
}
}
impl Goal for EvokerCastingSpellGoal {
fn can_start<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, bool> {
Box::pin(async move {
let Some(evoker) = self.evoker.upgrade() else {
return false;
};
evoker.is_casting_spell()
})
}
fn should_continue<'a>(&'a self, _mob: &'a dyn Mob) -> GoalFuture<'a, bool> {
Box::pin(async move {
let Some(evoker) = self.evoker.upgrade() else {
return false;
};
evoker.is_casting_spell()
})
}
fn start<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
Box::pin(async move {
if let Some(evoker) = self.evoker.upgrade() {
evoker
.mob_entity
.living_entity
.entity
.velocity
.store(Vector3::new(0.0, 0.0, 0.0));
}
})
}
fn stop<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
Box::pin(async move {
if let Some(evoker) = self.evoker.upgrade() {
evoker.set_is_casting_spell(IllagerSpell::None);
}
})
}
fn controls(&self) -> Controls {
Controls::MOVE | Controls::LOOK
}
}
pub struct EvokerAttackSpellGoal {
evoker: Weak<EvokerEntity>,
warmup_delay: i32,
next_attack_tick: i32,
}
impl EvokerAttackSpellGoal {
#[must_use]
pub const fn new(evoker: Weak<EvokerEntity>) -> Self {
Self {
evoker,
warmup_delay: 0,
next_attack_tick: 0,
}
}
}
impl Goal for EvokerAttackSpellGoal {
fn can_start<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, bool> {
Box::pin(async move {
let Some(evoker) = self.evoker.upgrade() else {
return false;
};
if evoker.is_casting_spell() {
return false;
}
let entity = &evoker.mob_entity.living_entity.entity;
if entity.age.load(Ordering::Relaxed) < self.next_attack_tick {
return false;
}
let target = evoker.mob_entity.target.lock().await.clone();
target.is_some()
})
}
fn should_continue<'a>(&'a self, _mob: &'a dyn Mob) -> GoalFuture<'a, bool> {
Box::pin(async move { self.warmup_delay > 0 })
}
fn start<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
Box::pin(async move {
self.warmup_delay = 20;
if let Some(evoker) = self.evoker.upgrade() {
evoker.set_spell_casting_time(40);
let age = evoker
.mob_entity
.living_entity
.entity
.age
.load(Ordering::Relaxed);
self.next_attack_tick = age + 100;
evoker.set_is_casting_spell(IllagerSpell::Fangs);
evoker
.mob_entity
.living_entity
.entity
.play_sound(Sound::EntityEvokerPrepareAttack);
}
})
}
fn tick<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
Box::pin(async move {
self.warmup_delay -= 1;
if self.warmup_delay == 0 {
let Some(evoker) = self.evoker.upgrade() else {
return;
};
let target = evoker.mob_entity.target.lock().await.clone();
let Some(target) = target else {
return;
};
let evoker_ent = &evoker.mob_entity.living_entity.entity;
evoker_ent.play_sound(Sound::EntityEvokerCastSpell);
let evoker_pos = evoker_ent.pos.load();
let target_pos = target.get_entity().pos.load();
let dx = target_pos.x - evoker_pos.x;
let dz = target_pos.z - evoker_pos.z;
let angle_towards_target = (dz.atan2(dx)) as f32;
let min_y = evoker_pos.y.min(target_pos.y);
let dist_sq = evoker_pos.squared_distance_to_vec(&target_pos);
let world = evoker_ent.world.load();
let evoker_id = evoker_ent.entity_id;
if dist_sq < 81.0 {
// Close range: concentric rings around Evoker
for i in 0..5 {
let angle = angle_towards_target + (i as f32) * std::f32::consts::PI * 0.4;
let spawn_x = evoker_pos.x + (angle.cos() as f64) * 1.5;
let spawn_z = evoker_pos.z + (angle.sin() as f64) * 1.5;
let pos = Vector3::new(spawn_x, min_y, spawn_z);
let entity = Entity::from_uuid(
Uuid::new_v4(),
world.clone(),
pos,
&EntityType::EVOKER_FANGS,
);
let fangs =
Arc::new(EvokerFangsEntity::new(entity, 0, angle, Some(evoker_id)));
world.spawn_entity(fangs).await;
}
for i in 0..8 {
let angle = angle_towards_target
+ (i as f32) * std::f32::consts::PI * 2.0 / 8.0
+ 1.256_637_1;
let spawn_x = evoker_pos.x + (angle.cos() as f64) * 2.5;
let spawn_z = evoker_pos.z + (angle.sin() as f64) * 2.5;
let pos = Vector3::new(spawn_x, min_y, spawn_z);
let entity = Entity::from_uuid(
Uuid::new_v4(),
world.clone(),
pos,
&EntityType::EVOKER_FANGS,
);
let fangs =
Arc::new(EvokerFangsEntity::new(entity, 3, angle, Some(evoker_id)));
world.spawn_entity(fangs).await;
}
} else {
// Long range: line of fangs towards target
for i in 0..16 {
let reach = 1.25 * ((i + 1) as f64);
let spawn_x = evoker_pos.x + (angle_towards_target.cos() as f64) * reach;
let spawn_z = evoker_pos.z + (angle_towards_target.sin() as f64) * reach;
let pos = Vector3::new(spawn_x, min_y, spawn_z);
let entity = Entity::from_uuid(
Uuid::new_v4(),
world.clone(),
pos,
&EntityType::EVOKER_FANGS,
);
let fangs = Arc::new(EvokerFangsEntity::new(
entity,
i as u32,
angle_towards_target,
Some(evoker_id),
));
world.spawn_entity(fangs).await;
}
}
}
})
}
}
pub struct EvokerSummonSpellGoal {
evoker: Weak<EvokerEntity>,
warmup_delay: i32,
next_attack_tick: i32,
}
impl EvokerSummonSpellGoal {
#[must_use]
pub const fn new(evoker: Weak<EvokerEntity>) -> Self {
Self {
evoker,
warmup_delay: 0,
next_attack_tick: 0,
}
}
}
impl Goal for EvokerSummonSpellGoal {
fn can_start<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, bool> {
Box::pin(async move {
let Some(evoker) = self.evoker.upgrade() else {
return false;
};
if evoker.is_casting_spell() {
return false;
}
let entity = &evoker.mob_entity.living_entity.entity;
if entity.age.load(Ordering::Relaxed) < self.next_attack_tick {
return false;
}
let target = evoker.mob_entity.target.lock().await.clone();
if target.is_none() {
return false;
}
// Count nearby Vexes
let bb = entity.bounding_box.load().expand(16.0, 16.0, 16.0);
let world = entity.world.load();
let nearby = world.get_entities_at_box(&bb);
let vex_count = nearby
.iter()
.filter(|e| *e.get_entity().entity_type == EntityType::VEX)
.count();
let max_allowed = (rand::random::<u8>() % 8 + 1) as usize;
vex_count < max_allowed
})
}
fn should_continue<'a>(&'a self, _mob: &'a dyn Mob) -> GoalFuture<'a, bool> {
Box::pin(async move { self.warmup_delay > 0 })
}
fn start<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
Box::pin(async move {
self.warmup_delay = 20;
if let Some(evoker) = self.evoker.upgrade() {
evoker.set_spell_casting_time(100);
let age = evoker
.mob_entity
.living_entity
.entity
.age
.load(Ordering::Relaxed);
self.next_attack_tick = age + 340;
evoker.set_is_casting_spell(IllagerSpell::SummonVex);
evoker
.mob_entity
.living_entity
.entity
.play_sound(Sound::EntityEvokerPrepareSummon);
}
})
}
fn tick<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
Box::pin(async move {
self.warmup_delay -= 1;
if self.warmup_delay == 0 {
let Some(evoker) = self.evoker.upgrade() else {
return;
};
let evoker_ent = &evoker.mob_entity.living_entity.entity;
evoker_ent.play_sound(Sound::EntityEvokerCastSpell);
let world = evoker_ent.world.load();
let evoker_pos = evoker_ent.pos.load();
for _ in 0..3 {
let offset_x = (rand::random::<i32>() % 5 - 2) as f64;
let offset_z = (rand::random::<i32>() % 5 - 2) as f64;
let spawn_pos = Vector3::new(
evoker_pos.x + offset_x,
evoker_pos.y + 1.0,
evoker_pos.z + offset_z,
);
let vex = from_type(&EntityType::VEX, spawn_pos, &world, Uuid::new_v4());
world.spawn_entity(vex).await;
}
}
})
}
}
pub struct EvokerWololoSpellGoal {
evoker: Weak<EvokerEntity>,
warmup_delay: i32,
next_attack_tick: i32,
}
impl EvokerWololoSpellGoal {
#[must_use]
pub const fn new(evoker: Weak<EvokerEntity>) -> Self {
Self {
evoker,
warmup_delay: 0,
next_attack_tick: 0,
}
}
}
impl Goal for EvokerWololoSpellGoal {
fn can_start<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, bool> {
Box::pin(async move {
let Some(evoker) = self.evoker.upgrade() else {
return false;
};
if evoker.is_casting_spell() {
return false;
}
let entity = &evoker.mob_entity.living_entity.entity;
if entity.age.load(Ordering::Relaxed) < self.next_attack_tick {
return false;
}
let target = evoker.mob_entity.target.lock().await.clone();
if target.is_some() {
return false;
}
// Find blue sheep within 16 blocks
let bb = entity.bounding_box.load().expand(16.0, 4.0, 16.0);
let world = entity.world.load();
let candidates = world.get_entities_at_box(&bb);
for cand in candidates {
if *cand.get_entity().entity_type == EntityType::SHEEP
&& let Some(mob) = cand.get_mob()
&& let Some(sheep) = mob.get_sheep()
{
// Blue color is 11 in Minecraft
if sheep.get_color() == 11 {
*evoker.wololo_target_id.lock().await = Some(cand.get_entity().entity_id);
return true;
}
}
}
false
})
}
fn should_continue<'a>(&'a self, _mob: &'a dyn Mob) -> GoalFuture<'a, bool> {
Box::pin(async move { self.warmup_delay > 0 })
}
fn start<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
Box::pin(async move {
self.warmup_delay = 40;
if let Some(evoker) = self.evoker.upgrade() {
evoker.set_spell_casting_time(60);
let age = evoker
.mob_entity
.living_entity
.entity
.age
.load(Ordering::Relaxed);
self.next_attack_tick = age + 140;
evoker.set_is_casting_spell(IllagerSpell::Wololo);
evoker
.mob_entity
.living_entity
.entity
.play_sound(Sound::EntityEvokerPrepareWololo);
}
})
}
fn stop<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
Box::pin(async move {
if let Some(evoker) = self.evoker.upgrade() {
*evoker.wololo_target_id.lock().await = None;
}
})
}
fn tick<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
Box::pin(async move {
self.warmup_delay -= 1;
if self.warmup_delay == 0 {
let Some(evoker) = self.evoker.upgrade() else {
return;
};
let evoker_ent = &evoker.mob_entity.living_entity.entity;
evoker_ent.play_sound(Sound::EntityEvokerCastSpell);
let target_id = *evoker.wololo_target_id.lock().await;
let Some(target_id) = target_id else {
return;
};
let world = evoker_ent.world.load();
let bb = evoker_ent.bounding_box.load().expand(16.0, 4.0, 16.0);
let candidates = world.get_entities_at_box(&bb);
for cand in candidates {
if cand.get_entity().entity_id == target_id
&& let Some(mob) = cand.get_mob()
&& let Some(sheep) = mob.get_sheep()
{
// Convert color to Red (14)
sheep.set_color(14);
break;
}
}
}
})
}
}

View File

@ -623,12 +623,35 @@ pub trait Mob: EntityBase + Send + Sync {
fn get_sheep(&self) -> Option<&crate::entity::passive::sheep::SheepEntity> {
None
}
fn mob_on_lightning_strike<'a>(
&'a self,
caller: &'a dyn EntityBase,
lightning: &'a crate::entity::lightning::LightningBoltEntity,
) -> EntityBaseFuture<'a, ()> {
Box::pin(async move {
self.get_mob_entity()
.living_entity
.on_lightning_strike(caller, lightning)
.await;
})
}
}
impl<T: Mob + Send + 'static> EntityBase for T {
fn get_mob(&self) -> Option<&dyn Mob> {
Some(self)
}
fn on_lightning_strike<'a>(
&'a self,
caller: &'a dyn EntityBase,
lightning: &'a crate::entity::lightning::LightningBoltEntity,
) -> EntityBaseFuture<'a, ()> {
Box::pin(async move {
self.mob_on_lightning_strike(caller, lightning).await;
})
}
fn get_item_steerable(&self) -> Option<&dyn crate::entity::item_steerable::ItemSteerable> {
Mob::get_item_steerable(self)
}

View File

@ -91,9 +91,12 @@ pub mod effect;
pub mod experience_orb;
pub mod falling;
pub mod hunger;
pub mod interaction;
pub mod item;
pub mod item_steerable;
pub mod lightning;
pub mod living;
pub mod marker;
pub mod mob;
pub mod passive;
pub mod player;
@ -103,6 +106,8 @@ pub mod tnt;
pub mod r#type;
pub mod vehicle;
pub use lightning::LightningBoltEntity;
mod combat;
pub mod predicate;
@ -253,6 +258,28 @@ pub trait EntityBase: Send + Sync + NBTStorage + std::any::Any {
})
}
fn on_lightning_strike<'a>(
&'a self,
caller: &'a dyn EntityBase,
lightning: &'a lightning::LightningBoltEntity,
) -> EntityBaseFuture<'a, ()> {
Box::pin(async move {
if self.get_living_entity().is_some() {
self.set_on_fire_for(8.0);
let cause = lightning.get_cause().await;
self.damage_with_context(
caller,
5.0,
DamageType::LIGHTNING_BOLT,
None,
Some(lightning),
cause.as_deref().map(|p| p as &dyn EntityBase),
)
.await;
}
})
}
fn is_spectator(&self) -> bool {
false
}
@ -3210,6 +3237,11 @@ impl Entity {
vehicle.is_some()
}
pub async fn is_leashed(&self) -> bool {
let leashed_to = self.leashed_to.lock().await;
leashed_to.is_some()
}
pub async fn add_passenger(
&self,
vehicle: Arc<dyn EntityBase>,

View File

@ -1,27 +1,140 @@
use std::sync::{Arc, Weak};
use std::sync::{
Arc, Weak,
atomic::{AtomicI32, AtomicU64, Ordering},
};
use pumpkin_data::damage::DamageType;
use pumpkin_data::entity::EntityType;
use pumpkin_data::item::Item;
use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::meta_data_type::MetaDataType;
use pumpkin_data::sound::{Sound, SoundCategory};
use pumpkin_data::tag::{self, Taggable};
use pumpkin_data::tracked_data::TrackedData;
use pumpkin_nbt::compound::NbtCompound;
use pumpkin_protocol::codec::var_int::VarInt;
use pumpkin_protocol::java::client::play::Metadata;
use crate::entity::{
Entity, NBTStorage,
Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture,
ageable::{AgeableData, AgeableMob},
ai::goal::{
breed::BreedGoal, escape_danger::EscapeDangerGoal, follow_parent::FollowParentGoal,
look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal,
wander_around::WanderAroundGoal,
tempt::TemptGoal, wander_around::WanderAroundGoal,
},
item::ItemEntity,
mob::{Mob, MobEntity},
passive::animal::Animal,
player::Player,
};
pub const ARMADILLO_FOOD: &[&Item] = &[&Item::SPIDER_EYE];
pub const ARMADILLO_BABY_START_AGE: i32 = -48000;
pub const SCARE_CHECK_INTERVAL: i32 = 80;
pub const SCARE_DISTANCE_HORIZONTAL: f64 = 7.0;
pub const SCARE_DISTANCE_VERTICAL: f64 = 2.0;
fn pick_next_scute_drop_time() -> i32 {
let rand_ticks = (rand::random::<u32>() % 6000) as i32;
rand_ticks + 6000
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[repr(i32)]
pub enum ArmadilloState {
#[default]
Idle = 0,
Rolling = 1,
Scared = 2,
Unrolling = 3,
}
impl ArmadilloState {
#[must_use]
pub const fn from_id(id: i32) -> Self {
match id {
1 => Self::Rolling,
2 => Self::Scared,
3 => Self::Unrolling,
_ => Self::Idle,
}
}
#[must_use]
pub const fn id(self) -> i32 {
self as i32
}
#[must_use]
pub const fn is_threatened(self) -> bool {
!matches!(self, Self::Idle)
}
#[must_use]
pub const fn animation_duration(self) -> u64 {
match self {
Self::Idle => 0,
Self::Rolling => 10,
Self::Scared => 50,
Self::Unrolling => 30,
}
}
#[must_use]
pub const fn should_hide_in_shell(self, ticks_in_state: u64) -> bool {
match self {
Self::Idle => false,
Self::Rolling => ticks_in_state > 5,
Self::Scared => true,
Self::Unrolling => ticks_in_state < 26,
}
}
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Idle => "idle",
Self::Rolling => "rolling",
Self::Scared => "scared",
Self::Unrolling => "unrolling",
}
}
#[must_use]
pub fn from_name(name: &str) -> Self {
match name {
"rolling" => Self::Rolling,
"scared" => Self::Scared,
"unrolling" => Self::Unrolling,
_ => Self::Idle,
}
}
}
/// Represents an Armadillo, a passive entity that can roll into a ball when threatened.
///
/// Wiki: <https://minecraft.wiki/w/Armadillo>
pub struct ArmadilloEntity {
pub mob_entity: MobEntity,
pub ageable_data: AgeableData,
pub state: AtomicI32,
pub in_state_ticks: AtomicU64,
pub scute_time: AtomicI32,
pub danger_detected_recently_ticks: AtomicI32,
}
impl ArmadilloEntity {
pub fn new(entity: Entity) -> Arc<Self> {
let mob_entity = MobEntity::new(entity);
let armadillo = Self { mob_entity };
let armadillo = Self {
mob_entity,
ageable_data: AgeableData::default(),
state: AtomicI32::new(ArmadilloState::Idle.id()),
in_state_ticks: AtomicU64::new(0),
scute_time: AtomicI32::new(pick_next_scute_drop_time()),
danger_detected_recently_ticks: AtomicI32::new(0),
};
let mob_arc = Arc::new(armadillo);
let mob_weak: Weak<dyn Mob> = {
let mob_arc: Arc<dyn Mob> = mob_arc.clone();
@ -36,22 +149,320 @@ impl ArmadilloEntity {
.unwrap_or_else(std::sync::PoisonError::into_inner);
goal_selector.add_goal(0, Box::new(SwimGoal::default()));
goal_selector.add_goal(1, Box::new(WanderAroundGoal::new(1.0)));
goal_selector.add_goal(1, EscapeDangerGoal::new(2.0));
goal_selector.add_goal(2, BreedGoal::new(1.0));
goal_selector.add_goal(3, Box::new(TemptGoal::new(1.25, ARMADILLO_FOOD)));
goal_selector.add_goal(4, Box::new(FollowParentGoal::new(1.1)));
goal_selector.add_goal(5, Box::new(WanderAroundGoal::new(1.0)));
goal_selector.add_goal(
2,
6,
LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 6.0),
);
goal_selector.add_goal(3, Box::new(RandomLookAroundGoal::default()));
goal_selector.add_goal(7, Box::new(RandomLookAroundGoal::default()));
};
mob_arc
}
#[must_use]
pub fn get_state(&self) -> ArmadilloState {
ArmadilloState::from_id(self.state.load(Ordering::Relaxed))
}
impl NBTStorage for ArmadilloEntity {}
pub fn switch_to_state(&self, state: ArmadilloState) {
self.state.store(state.id(), Ordering::Relaxed);
self.in_state_ticks.store(0, Ordering::Relaxed);
let entity = self.get_entity();
entity.send_meta_data(
&[Metadata::new(
TrackedData::ARMADILLO_STATE,
MetaDataType::ARMADILLO_STATE,
VarInt(state.id()),
)],
None,
);
}
#[must_use]
pub fn is_scared(&self) -> bool {
self.get_state() != ArmadilloState::Idle
}
#[must_use]
pub fn should_hide_in_shell(&self) -> bool {
self.get_state()
.should_hide_in_shell(self.in_state_ticks.load(Ordering::Relaxed))
}
#[must_use]
pub fn should_switch_to_scared_state(&self) -> bool {
self.get_state() == ArmadilloState::Rolling
&& self.in_state_ticks.load(Ordering::Relaxed)
> ArmadilloState::Rolling.animation_duration()
}
pub async fn can_stay_rolled_up(&self) -> bool {
!self.is_panicking()
&& !self.mob_entity.living_entity.is_in_water()
&& !self.get_entity().has_vehicle().await
}
pub fn roll_up(&self) {
if !self.is_scared() {
self.mob_entity.reset_love_ticks();
let entity = self.get_entity();
let world = entity.world.load();
world.play_sound(
Sound::EntityArmadilloRoll,
SoundCategory::Neutral,
&entity.pos.load(),
);
self.switch_to_state(ArmadilloState::Rolling);
}
}
pub fn roll_out(&self) {
if self.is_scared() {
let entity = self.get_entity();
let world = entity.world.load();
world.play_sound(
Sound::EntityArmadilloUnrollFinish,
SoundCategory::Neutral,
&entity.pos.load(),
);
self.switch_to_state(ArmadilloState::Idle);
}
}
pub fn brush_off_scute<'a>(&'a self, player: &'a Arc<Player>) -> EntityBaseFuture<'a, bool> {
Box::pin(async move {
if self.is_baby() {
return false;
}
let entity = self.get_entity();
let world = entity.world.load();
let pos = entity.pos.load();
let item_entity = Arc::new(ItemEntity::new(
Entity::new(world.clone(), pos, &EntityType::ITEM),
ItemStack::new(1, &Item::ARMADILLO_SCUTE),
));
world.spawn_entity(item_entity).await;
world.play_sound(Sound::EntityArmadilloBrush, SoundCategory::Neutral, &pos);
player.damage_held_item(16).await;
true
})
}
pub async fn is_scared_by(&self, living_entity: &dyn EntityBase) -> bool {
let entity = self.get_entity();
let pos = entity.pos.load();
let target_pos = living_entity.get_entity().pos.load();
let dx = (pos.x - target_pos.x).abs();
let dy = (pos.y - target_pos.y).abs();
let dz = (pos.z - target_pos.z).abs();
if dx > SCARE_DISTANCE_HORIZONTAL
|| dz > SCARE_DISTANCE_HORIZONTAL
|| dy > SCARE_DISTANCE_VERTICAL
{
return false;
}
let target_type = living_entity.get_entity().entity_type;
if target_type.has_tag(&tag::EntityType::MINECRAFT_UNDEAD) {
return true;
}
if target_type == &EntityType::PLAYER {
let target_ent = living_entity.get_entity();
if target_ent.is_sprinting() || target_ent.has_vehicle().await {
return true;
}
}
false
}
}
impl AgeableMob for ArmadilloEntity {
fn get_ageable_data(&self) -> &AgeableData {
&self.ageable_data
}
fn get_baby_start_age(&self) -> i32 {
ARMADILLO_BABY_START_AGE
}
}
impl NBTStorage for ArmadilloEntity {
fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
self.mob_entity.living_entity.write_nbt(nbt).await;
self.write_ageable_nbt(nbt);
self.write_animal_nbt(nbt);
nbt.put_string("state", self.get_state().name().to_string());
nbt.put_int("scute_time", self.scute_time.load(Ordering::Relaxed));
})
}
fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
self.mob_entity.living_entity.read_nbt_non_mut(nbt).await;
self.read_ageable_nbt(nbt);
self.read_animal_nbt(nbt);
if let Some(state_name) = nbt.get_string("state") {
self.switch_to_state(ArmadilloState::from_name(state_name));
} else if let Some(state_id) = nbt.get_int("state") {
self.switch_to_state(ArmadilloState::from_id(state_id));
}
if let Some(scute_time) = nbt.get_int("scute_time") {
self.scute_time.store(scute_time, Ordering::Relaxed);
}
})
}
}
impl Animal for ArmadilloEntity {
fn is_food(&self, item_stack: &ItemStack) -> bool {
item_stack
.item
.has_tag(&tag::Item::MINECRAFT_ARMADILLO_FOOD)
|| ARMADILLO_FOOD.iter().any(|i| i.id == item_stack.item.id)
}
}
impl Mob for ArmadilloEntity {
fn get_mob_entity(&self) -> &MobEntity {
&self.mob_entity
}
fn modify_incoming_damage(&self, amount: f32, _damage_type: DamageType) -> f32 {
if self.is_scared() {
(amount - 1.0).max(0.0) / 2.0
} else {
amount
}
}
fn on_damage<'a>(
&'a self,
_damage_type: DamageType,
source: Option<&'a dyn EntityBase>,
) -> EntityBaseFuture<'a, ()> {
Box::pin(async move {
if self.get_entity().is_alive()
&& let Some(src) = source
&& src.get_entity().entity_type != &EntityType::ITEM
{
self.danger_detected_recently_ticks
.store(SCARE_CHECK_INTERVAL, Ordering::Relaxed);
if self.can_stay_rolled_up().await {
self.roll_up();
}
}
})
}
fn mob_tick<'a>(&'a self, _caller: &'a Arc<dyn EntityBase>) -> EntityBaseFuture<'a, ()> {
Box::pin(async move {
self.ageable_ai_step();
self.in_state_ticks.fetch_add(1, Ordering::Relaxed);
let danger_ticks = self.danger_detected_recently_ticks.load(Ordering::Relaxed);
if danger_ticks > 0 {
self.danger_detected_recently_ticks
.store(danger_ticks - 1, Ordering::Relaxed);
}
let entity = self.get_entity();
let world = entity.world.load();
if entity.is_alive() && !self.is_baby() {
let scute_time = self.scute_time.fetch_sub(1, Ordering::Relaxed) - 1;
if scute_time <= 0 {
let pos = entity.pos.load();
let item_entity = Arc::new(ItemEntity::new(
Entity::new(world.clone(), pos, &EntityType::ITEM),
ItemStack::new(1, &Item::ARMADILLO_SCUTE),
));
world.spawn_entity(item_entity).await;
world.play_sound(
Sound::EntityArmadilloScuteDrop,
SoundCategory::Neutral,
&pos,
);
self.scute_time
.store(pick_next_scute_drop_time(), Ordering::Relaxed);
}
}
let state = self.get_state();
let ticks_in_state = self.in_state_ticks.load(Ordering::Relaxed);
match state {
ArmadilloState::Rolling => {
if ticks_in_state > ArmadilloState::Rolling.animation_duration() {
self.switch_to_state(ArmadilloState::Scared);
}
}
ArmadilloState::Scared => {
if !self.can_stay_rolled_up().await {
self.roll_out();
} else if ticks_in_state > ArmadilloState::Scared.animation_duration()
&& self.danger_detected_recently_ticks.load(Ordering::Relaxed) == 0
{
self.switch_to_state(ArmadilloState::Unrolling);
}
}
ArmadilloState::Unrolling => {
if ticks_in_state > ArmadilloState::Unrolling.animation_duration() {
self.roll_out();
}
}
ArmadilloState::Idle => {}
}
})
}
fn mob_init_data_tracker(&self) -> EntityBaseFuture<'_, ()> {
Box::pin(async move {
let entity = self.get_entity();
let is_baby = entity.age.load(Ordering::Relaxed) < 0;
if is_baby {
entity.send_meta_data(
&[Metadata::new(
TrackedData::BABY_ID,
MetaDataType::BOOLEAN,
true,
)],
None,
);
}
entity.send_meta_data(
&[Metadata::new(
TrackedData::ARMADILLO_STATE,
MetaDataType::ARMADILLO_STATE,
VarInt(self.get_state().id()),
)],
None,
);
})
}
fn mob_interact<'a>(
&'a self,
player: &'a Arc<Player>,
item_stack: &'a mut ItemStack,
) -> EntityBaseFuture<'a, bool> {
Box::pin(async move {
if item_stack.item == &Item::BRUSH && self.brush_off_scute(player).await {
return true;
}
if self.is_scared() {
return false;
}
self.animal_interact(player, item_stack, Sound::EntityArmadilloAmbient)
.await
})
}
}

View File

@ -1,31 +1,87 @@
use std::sync::{
Arc, Weak,
atomic::{AtomicU8, Ordering},
atomic::{AtomicBool, AtomicU8, Ordering},
};
use pumpkin_data::entity::EntityType;
use crossbeam::atomic::AtomicCell;
use pumpkin_data::entity::{EntityStatus, EntityType};
use pumpkin_data::item::Item;
use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::meta_data_type::MetaDataType;
use pumpkin_data::tag::{self, Taggable};
use pumpkin_data::tracked_data::TrackedData;
use pumpkin_nbt::compound::NbtCompound;
use pumpkin_protocol::bedrock::server::actor_event::ActorEventType;
use pumpkin_protocol::codec::var_int::VarInt;
use pumpkin_protocol::java::client::play::Metadata;
use rand::RngExt;
use uuid::Uuid;
use crate::entity::{
Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture,
ai::goal::{
breed::BreedGoal, escape_danger::EscapeDangerGoal, follow_parent::FollowParentGoal,
look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal,
tempt::TemptGoal, wander_around::WanderAroundGoal,
active_target::ActiveTargetGoal, avoid_entity::AvoidEntityGoal, breed::BreedGoal,
escape_danger::EscapeDangerGoal, follow_owner::FollowOwnerGoal,
follow_parent::FollowParentGoal, look_around::RandomLookAroundGoal,
look_at_entity::LookAtEntityGoal, swim::SwimGoal, tempt::TemptGoal,
wander_around::WanderAroundGoal,
},
mob::{Mob, MobEntity},
passive::animal::Animal,
player::Player,
};
const TEMPT_ITEMS: &[&Item] = &[&Item::COD, &Item::SALMON];
fn get_dye_color_from_item(item: &Item) -> Option<u8> {
let key = item.registry_key;
if key.contains("white") {
Some(0)
} else if key.contains("orange") {
Some(1)
} else if key.contains("magenta") {
Some(2)
} else if key.contains("light_blue") {
Some(3)
} else if key.contains("yellow") {
Some(4)
} else if key.contains("lime") {
Some(5)
} else if key.contains("pink") {
Some(6)
} else if key.contains("light_gray") {
Some(8)
} else if key.contains("gray") {
Some(7)
} else if key.contains("cyan") {
Some(9)
} else if key.contains("purple") {
Some(10)
} else if key.contains("blue") {
Some(11)
} else if key.contains("brown") {
Some(12)
} else if key.contains("green") {
Some(13)
} else if key.contains("red") {
Some(14)
} else if key.contains("black") {
Some(15)
} else {
None
}
}
pub struct CatEntity {
pub mob_entity: MobEntity,
pub variant: AtomicU8,
pub sound_variant: AtomicU8,
pub collar_color: AtomicU8,
pub is_tame: AtomicBool,
pub is_sitting: AtomicBool,
pub is_lying: AtomicBool,
pub relax_state_one: AtomicBool,
pub owner: AtomicCell<Option<Uuid>>,
}
impl CatEntity {
@ -33,7 +89,14 @@ impl CatEntity {
let mob_entity = MobEntity::new(entity);
let cat = Self {
mob_entity,
variant: AtomicU8::new(9), // Default to tabby
variant: AtomicU8::new(1), // Default to black
sound_variant: AtomicU8::new(0), // Default to classic
collar_color: AtomicU8::new(14), // Default to red
is_tame: AtomicBool::new(false),
is_sitting: AtomicBool::new(false),
is_lying: AtomicBool::new(false),
relax_state_one: AtomicBool::new(false),
owner: AtomicCell::new(None),
};
let mob_arc = Arc::new(cat);
let mob_weak: Weak<dyn Mob> = {
@ -48,29 +111,177 @@ impl CatEntity {
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
// Goal 1: SwimGoal (FloatGoal)
goal_selector.add_goal(1, Box::new(SwimGoal::default()));
// Goal 1: TamableAnimalPanicGoal (EscapeDangerGoal)
goal_selector.add_goal(1, EscapeDangerGoal::new(1.5));
// goal_selector.add_goal(2, SitGoal::new(mob_arc.clone()));
// Goal 4: CatTemptGoal
goal_selector.add_goal(4, Box::new(TemptGoal::new(0.6, TEMPT_ITEMS)));
// Goal 4: CatAvoidEntityGoal (when untamed)
goal_selector.add_goal(
4,
Box::new(AvoidEntityGoal::new(&EntityType::PLAYER, 16.0, 0.8, 1.33)),
);
// Goal 5: BreedGoal
goal_selector.add_goal(5, BreedGoal::new(0.8));
// goal_selector.add_goal(7, FollowOwnerGoal::new(1.0, 10.0, 5.0, false));
// Goal 6: FollowOwnerGoal
goal_selector.add_goal(6, FollowOwnerGoal::new(1.0, 10.0, 5.0));
// Goal 9: FollowParentGoal
goal_selector.add_goal(9, Box::new(FollowParentGoal::new(0.8)));
// Goal 11: WanderAroundGoal
goal_selector.add_goal(11, Box::new(WanderAroundGoal::new(0.8)));
// Goal 12: LookAtPlayerGoal
goal_selector.add_goal(
12,
LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 10.0),
);
// Goal 12: RandomLookAroundGoal
goal_selector.add_goal(12, Box::new(RandomLookAroundGoal::default()));
};
{
let mut target_selector = mob_arc
.mob_entity
.target_selector
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
// Target Goal 1: NonTameRandomTargetGoal for Rabbit and Turtle
target_selector.add_goal(
1,
ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::RABBIT, false),
);
target_selector.add_goal(
1,
ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::TURTLE, false),
);
};
mob_arc
}
pub fn get_tame_flags(&self) -> u8 {
let mut flags = 0u8;
if self.is_sitting() {
flags |= 0x01;
}
if self.is_tame() {
flags |= 0x04;
}
flags
}
pub fn is_tame(&self) -> bool {
self.is_tame.load(Ordering::Relaxed)
}
pub fn is_sitting(&self) -> bool {
self.is_sitting.load(Ordering::Relaxed)
}
pub fn is_lying(&self) -> bool {
self.is_lying.load(Ordering::Relaxed)
}
pub fn is_relax_state_one(&self) -> bool {
self.relax_state_one.load(Ordering::Relaxed)
}
pub fn get_collar_color(&self) -> u8 {
self.collar_color.load(Ordering::Relaxed)
}
pub fn set_collar_color(&self, color: u8) {
self.collar_color.store(color, Ordering::Relaxed);
let entity = self.get_entity();
entity.send_meta_data(
&[Metadata::new(
TrackedData::COLLAR_COLOR,
MetaDataType::INT,
VarInt(color as i32),
)],
None,
);
}
pub fn set_sitting(&self, sitting: bool) {
self.is_sitting.store(sitting, Ordering::Relaxed);
let entity = self.get_entity();
entity.send_meta_data(
&[Metadata::new(
TrackedData::TAMEABLE_FLAGS,
MetaDataType::BYTE,
self.get_tame_flags(),
)],
None,
);
}
pub fn set_tame(&self, tame: bool, owner: Option<Uuid>) {
self.is_tame.store(tame, Ordering::Relaxed);
self.owner.store(owner);
let entity = self.get_entity();
entity.send_meta_data(
&[Metadata::new(
TrackedData::TAMEABLE_FLAGS,
MetaDataType::BYTE,
self.get_tame_flags(),
)],
None,
);
entity.send_meta_data(
&[Metadata::new(
TrackedData::OWNER_UUID,
MetaDataType::OPTIONAL_UUID,
owner,
)],
None,
);
}
pub fn set_lying(&self, lying: bool) {
self.is_lying.store(lying, Ordering::Relaxed);
let entity = self.get_entity();
entity.send_meta_data(
&[Metadata::new(
TrackedData::IS_LYING,
MetaDataType::BOOLEAN,
lying,
)],
None,
);
}
pub fn set_relax_state_one(&self, relax: bool) {
self.relax_state_one.store(relax, Ordering::Relaxed);
let entity = self.get_entity();
entity.send_meta_data(
&[Metadata::new(
TrackedData::RELAX_STATE_ONE,
MetaDataType::BOOLEAN,
relax,
)],
None,
);
}
pub fn play_eating_sound(&self) {
let mob_entity = self.get_mob_entity();
let entity = &mob_entity.living_entity.entity;
let world = entity.world.load();
world.play_sound(
pumpkin_data::sound::Sound::EntityCatEat,
pumpkin_data::sound::SoundCategory::Neutral,
&entity.pos.load(),
);
}
}
impl NBTStorage for CatEntity {
fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async {
self.mob_entity.living_entity.write_nbt(nbt).await;
self.write_animal_nbt(nbt);
let variant_str = match self.variant.load(Ordering::Relaxed) {
0 => "minecraft:all_black",
1 => "minecraft:black",
@ -85,12 +296,23 @@ impl NBTStorage for CatEntity {
_ => "minecraft:tabby",
};
nbt.put_string("variant", variant_str.to_string());
nbt.put_string("sound_variant", "minecraft:classic".to_string());
nbt.put_byte(
"CollarColor",
self.collar_color.load(Ordering::Relaxed) as i8,
);
nbt.put_bool("IsTame", self.is_tame.load(Ordering::Relaxed));
nbt.put_bool("Sitting", self.is_sitting.load(Ordering::Relaxed));
if let Some(owner) = self.owner.load() {
nbt.put_uuid("Owner", owner);
}
})
}
fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async {
self.mob_entity.living_entity.read_nbt_non_mut(nbt).await;
self.read_animal_nbt(nbt);
if let Some(variant_str) = nbt.get_string("variant") {
let variant = match variant_str
.strip_prefix("minecraft:")
@ -110,15 +332,44 @@ impl NBTStorage for CatEntity {
};
self.variant.store(variant, Ordering::Relaxed);
}
if let Some(collar) = nbt.get_byte("CollarColor") {
self.collar_color.store(collar as u8, Ordering::Relaxed);
} else if let Some(collar_int) = nbt.get_int("CollarColor") {
self.collar_color.store(collar_int as u8, Ordering::Relaxed);
}
if let Some(sitting) = nbt.get_bool("Sitting") {
self.is_sitting.store(sitting, Ordering::Relaxed);
}
if let Some(owner) = nbt.get_uuid("Owner") {
self.owner.store(Some(owner));
self.is_tame.store(true, Ordering::Relaxed);
} else if let Some(is_tame) = nbt.get_bool("IsTame") {
self.is_tame.store(is_tame, Ordering::Relaxed);
}
})
}
}
impl Animal for CatEntity {
fn is_food(&self, item_stack: &ItemStack) -> bool {
let item = item_stack.get_item();
item.has_tag(&tag::Item::MINECRAFT_CAT_FOOD) || item == &Item::COD || item == &Item::SALMON
}
}
impl Mob for CatEntity {
fn get_mob_entity(&self) -> &MobEntity {
&self.mob_entity
}
fn get_owner_uuid(&self) -> Option<Uuid> {
self.owner.load()
}
fn is_sitting(&self) -> bool {
self.is_sitting.load(Ordering::Relaxed)
}
fn mob_set_variant_name(&self, name: &str) {
let variant = match name.strip_prefix("minecraft:").unwrap_or(name) {
"all_black" => 0,
@ -150,6 +401,22 @@ impl Mob for CatEntity {
None,
);
}
entity.send_meta_data(
&[Metadata::new(
TrackedData::TAMEABLE_FLAGS,
MetaDataType::BYTE,
self.get_tame_flags(),
)],
None,
);
entity.send_meta_data(
&[Metadata::new(
TrackedData::OWNER_UUID,
MetaDataType::OPTIONAL_UUID,
self.owner.load(),
)],
None,
);
entity.send_meta_data(
&[Metadata::new(
TrackedData::CAT_VARIANT,
@ -158,6 +425,104 @@ impl Mob for CatEntity {
)],
None,
);
entity.send_meta_data(
&[Metadata::new(
TrackedData::IS_LYING,
MetaDataType::BOOLEAN,
self.is_lying.load(Ordering::Relaxed),
)],
None,
);
entity.send_meta_data(
&[Metadata::new(
TrackedData::RELAX_STATE_ONE,
MetaDataType::BOOLEAN,
self.relax_state_one.load(Ordering::Relaxed),
)],
None,
);
entity.send_meta_data(
&[Metadata::new(
TrackedData::COLLAR_COLOR,
MetaDataType::INT,
VarInt(self.collar_color.load(Ordering::Relaxed) as i32),
)],
None,
);
entity.send_meta_data(
&[Metadata::new(
TrackedData::SOUND_VARIANT,
MetaDataType::CAT_SOUND_VARIANT,
VarInt(self.sound_variant.load(Ordering::Relaxed) as i32),
)],
None,
);
})
}
fn mob_interact<'a>(
&'a self,
player: &'a Arc<Player>,
item_stack: &'a mut ItemStack,
) -> EntityBaseFuture<'a, bool> {
Box::pin(async move {
let item = item_stack.get_item();
let is_food = self.is_food(item_stack);
if self.is_tame() {
if self.get_owner_uuid() == Some(player.gameprofile.id) {
if item.has_tag(&tag::Item::MINECRAFT_CAT_COLLAR_DYES)
|| item.has_tag(&tag::Item::C_DYES)
{
if let Some(color) = get_dye_color_from_item(item)
&& color != self.get_collar_color()
{
self.set_collar_color(color);
item_stack.decrement_unless_creative(player.gamemode.load(), 1);
return true;
}
} else if is_food
&& self.mob_entity.living_entity.health.load()
< self.mob_entity.living_entity.get_max_health()
{
item_stack.decrement_unless_creative(player.gamemode.load(), 1);
self.mob_entity.living_entity.heal(2.0);
self.play_eating_sound();
return true;
}
let parent_interaction = self.mob_entity.mob_interact(player, item_stack).await;
if !parent_interaction {
self.set_sitting(!self.is_sitting());
return true;
}
return parent_interaction;
}
} else if is_food {
item_stack.decrement_unless_creative(player.gamemode.load(), 1);
self.play_eating_sound();
let mut rng = rand::rng();
if rng.random_range(0..3) == 0 {
self.set_tame(true, Some(player.gameprofile.id));
self.set_sitting(true);
self.get_entity().world.load().send_entity_status(
self.get_entity(),
EntityStatus::TamingSucceeded,
Some(ActorEventType::TamingSucceeded),
);
} else {
self.get_entity().world.load().send_entity_status(
self.get_entity(),
EntityStatus::TamingFailed,
Some(ActorEventType::TamingFailed),
);
}
return true;
}
self.mob_entity.mob_interact(player, item_stack).await
})
}
}

View File

@ -130,7 +130,11 @@ impl NBTStorage for ChickenEntity {
impl super::animal::Animal for ChickenEntity {
fn is_food(&self, item_stack: &ItemStack) -> bool {
TEMPT_ITEMS.iter().any(|i| i.id == item_stack.item.id)
use pumpkin_data::tag::Taggable;
item_stack
.item
.has_tag(&pumpkin_data::tag::Item::MINECRAFT_CHICKEN_FOOD)
|| TEMPT_ITEMS.iter().any(|i| i.id == item_stack.item.id)
}
}

View File

@ -0,0 +1,316 @@
use std::sync::{
Arc, Weak,
atomic::{AtomicI32, AtomicI64, Ordering},
};
use pumpkin_data::entity::EntityType;
use pumpkin_data::item::Item;
use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::meta_data_type::MetaDataType;
use pumpkin_data::sound::{Sound, SoundCategory};
use pumpkin_data::tag::{self, Taggable};
use pumpkin_data::tracked_data::TrackedData;
use pumpkin_nbt::compound::NbtCompound;
use pumpkin_protocol::codec::var_int::VarInt;
use pumpkin_protocol::java::client::play::Metadata;
use crate::entity::{
Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture,
ai::goal::{
look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal,
wander_around::WanderAroundGoal,
},
mob::{Mob, MobEntity},
player::Player,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[repr(i32)]
pub enum WeatherState {
#[default]
Unaffected = 0,
Exposed = 1,
Weathered = 2,
Oxidized = 3,
}
impl WeatherState {
#[must_use]
pub const fn from_id(id: i32) -> Self {
match id {
1 => Self::Exposed,
2 => Self::Weathered,
3 => Self::Oxidized,
_ => Self::Unaffected,
}
}
#[must_use]
pub const fn id(self) -> i32 {
self as i32
}
#[must_use]
pub const fn next(self) -> Self {
match self {
Self::Unaffected => Self::Exposed,
Self::Exposed => Self::Weathered,
Self::Weathered | Self::Oxidized => Self::Oxidized,
}
}
#[must_use]
pub const fn previous(self) -> Self {
match self {
Self::Oxidized => Self::Weathered,
Self::Weathered => Self::Exposed,
Self::Exposed | Self::Unaffected => Self::Unaffected,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[repr(i32)]
pub enum CopperGolemState {
#[default]
Idle = 0,
GettingItem = 1,
GettingNoItem = 2,
DroppingItem = 3,
DroppingNoItem = 4,
}
impl CopperGolemState {
#[must_use]
pub const fn from_id(id: i32) -> Self {
match id {
1 => Self::GettingItem,
2 => Self::GettingNoItem,
3 => Self::DroppingItem,
4 => Self::DroppingNoItem,
_ => Self::Idle,
}
}
#[must_use]
pub const fn id(self) -> i32 {
self as i32
}
}
/// Represents a Copper Golem, a passive creation mob made of copper blocks and lightning rod.
///
/// Wiki: <https://minecraft.wiki/w/Copper_Golem>
pub struct CopperGolemEntity {
pub mob_entity: MobEntity,
pub weather_state: AtomicI32,
pub state: AtomicI32,
pub next_weathering_tick: AtomicI64,
}
impl CopperGolemEntity {
pub fn new(entity: Entity) -> Arc<Self> {
let mob_entity = MobEntity::new(entity);
let golem = Self {
mob_entity,
weather_state: AtomicI32::new(WeatherState::Unaffected.id()),
state: AtomicI32::new(CopperGolemState::Idle.id()),
next_weathering_tick: AtomicI64::new(-1),
};
let mob_arc = Arc::new(golem);
let mob_weak: Weak<dyn Mob> = {
let mob_arc: Arc<dyn Mob> = mob_arc.clone();
Arc::downgrade(&mob_arc)
};
{
let mut goal_selector = mob_arc
.mob_entity
.goals_selector
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
goal_selector.add_goal(0, Box::new(SwimGoal::default()));
goal_selector.add_goal(1, Box::new(WanderAroundGoal::new(1.0)));
goal_selector.add_goal(
2,
LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 6.0),
);
goal_selector.add_goal(3, Box::new(RandomLookAroundGoal::default()));
};
mob_arc
}
#[must_use]
pub fn get_weather_state(&self) -> WeatherState {
WeatherState::from_id(self.weather_state.load(Ordering::Relaxed))
}
pub fn set_weather_state(&self, state: WeatherState) {
self.weather_state.store(state.id(), Ordering::Relaxed);
let entity = self.get_entity();
entity.send_meta_data(
&[Metadata::new(
TrackedData::WEATHER_STATE,
MetaDataType::WEATHERING_COPPER_STATE,
VarInt(state.id()),
)],
None,
);
}
#[must_use]
pub fn get_state(&self) -> CopperGolemState {
CopperGolemState::from_id(self.state.load(Ordering::Relaxed))
}
pub fn set_state(&self, state: CopperGolemState) {
self.state.store(state.id(), Ordering::Relaxed);
let entity = self.get_entity();
entity.send_meta_data(
&[Metadata::new(
TrackedData::COPPER_GOLEM_STATE,
MetaDataType::COPPER_GOLEM_STATE,
VarInt(state.id()),
)],
None,
);
}
#[must_use]
pub fn hurt_sound(&self) -> Sound {
match self.get_weather_state() {
WeatherState::Unaffected | WeatherState::Exposed => Sound::EntityCopperGolemHurt,
WeatherState::Weathered => Sound::EntityCopperGolemWeatheredHurt,
WeatherState::Oxidized => Sound::EntityCopperGolemOxidizedHurt,
}
}
#[must_use]
pub fn death_sound(&self) -> Sound {
match self.get_weather_state() {
WeatherState::Unaffected | WeatherState::Exposed => Sound::EntityCopperGolemDeath,
WeatherState::Weathered => Sound::EntityCopperGolemWeatheredDeath,
WeatherState::Oxidized => Sound::EntityCopperGolemOxidizedDeath,
}
}
#[must_use]
pub fn step_sound(&self) -> Sound {
match self.get_weather_state() {
WeatherState::Unaffected | WeatherState::Exposed => Sound::EntityCopperGolemStep,
WeatherState::Weathered => Sound::EntityCopperGolemWeatheredStep,
WeatherState::Oxidized => Sound::EntityCopperGolemOxidizedStep,
}
}
}
impl NBTStorage for CopperGolemEntity {
fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
self.mob_entity.living_entity.write_nbt(nbt).await;
nbt.put_long(
"next_weather_age",
self.next_weathering_tick.load(Ordering::Relaxed),
);
nbt.put_int("weather_state", self.get_weather_state().id());
})
}
fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
self.mob_entity.living_entity.read_nbt_non_mut(nbt).await;
if let Some(next) = nbt.get_long("next_weather_age") {
self.next_weathering_tick.store(next, Ordering::Relaxed);
}
if let Some(state) = nbt.get_int("weather_state") {
self.set_weather_state(WeatherState::from_id(state));
}
})
}
}
impl Mob for CopperGolemEntity {
fn get_mob_entity(&self) -> &MobEntity {
&self.mob_entity
}
fn mob_on_lightning_strike<'a>(
&'a self,
caller: &'a dyn EntityBase,
lightning: &'a crate::entity::lightning::LightningBoltEntity,
) -> EntityBaseFuture<'a, ()> {
Box::pin(async move {
self.set_weather_state(WeatherState::Unaffected);
self.mob_entity
.living_entity
.on_lightning_strike(caller, lightning)
.await;
})
}
fn mob_init_data_tracker(&self) -> EntityBaseFuture<'_, ()> {
Box::pin(async move {
let entity = self.get_entity();
entity.send_meta_data(
&[
Metadata::new(
TrackedData::WEATHER_STATE,
MetaDataType::WEATHERING_COPPER_STATE,
VarInt(self.get_weather_state().id()),
),
Metadata::new(
TrackedData::COPPER_GOLEM_STATE,
MetaDataType::COPPER_GOLEM_STATE,
VarInt(self.get_state().id()),
),
],
None,
);
})
}
fn mob_interact<'a>(
&'a self,
_player: &'a Arc<Player>,
item_stack: &'a mut ItemStack,
) -> EntityBaseFuture<'a, bool> {
Box::pin(async move {
let entity = self.get_entity();
let world = entity.world.load();
// Honeycomb waxing
if item_stack.item.id == Item::HONEYCOMB.id
&& self.next_weathering_tick.load(Ordering::Relaxed) != -2
{
self.next_weathering_tick.store(-2, Ordering::Relaxed);
let pos = entity.pos.load();
world.play_sound(Sound::ItemHoneycombWaxOn, SoundCategory::Blocks, &pos);
return true;
}
// Axe scraping
if item_stack.item.has_tag(&tag::Item::MINECRAFT_AXES) {
let current_next = self.next_weathering_tick.load(Ordering::Relaxed);
if current_next == -2 {
self.next_weathering_tick.store(-1, Ordering::Relaxed);
let pos = entity.pos.load();
world.play_sound(Sound::ItemAxeScrape, SoundCategory::Blocks, &pos);
return true;
}
let weather_state = self.get_weather_state();
if weather_state != WeatherState::Unaffected {
self.set_weather_state(weather_state.previous());
self.next_weathering_tick.store(-1, Ordering::Relaxed);
let pos = entity.pos.load();
world.play_sound(Sound::ItemAxeScrape, SoundCategory::Blocks, &pos);
return true;
}
}
false
})
}
}

View File

@ -91,7 +91,11 @@ impl NBTStorage for CowEntity {
impl super::animal::Animal for CowEntity {
fn is_food(&self, item_stack: &ItemStack) -> bool {
TEMPT_ITEMS.iter().any(|i| i.id == item_stack.item.id)
use pumpkin_data::tag::Taggable;
item_stack
.item
.has_tag(&pumpkin_data::tag::Item::MINECRAFT_COW_FOOD)
|| TEMPT_ITEMS.iter().any(|i| i.id == item_stack.item.id)
}
}

View File

@ -1,30 +1,92 @@
use std::sync::{Arc, Weak};
use std::sync::{
Arc, Weak,
atomic::{AtomicI32, AtomicU8, Ordering},
};
use pumpkin_data::{entity::EntityType, item::Item};
use pumpkin_data::entity::EntityType;
use pumpkin_data::item::Item;
use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::meta_data_type::MetaDataType;
use pumpkin_data::sound::Sound;
use pumpkin_data::tag::{self, Taggable};
use pumpkin_data::tracked_data::TrackedData;
use pumpkin_nbt::compound::NbtCompound;
use pumpkin_protocol::codec::var_int::VarInt;
use pumpkin_protocol::java::client::play::Metadata;
use crate::entity::{
Entity, NBTStorage,
Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture,
ageable::{AgeableData, AgeableMob},
ai::goal::{
breed::BreedGoal, escape_danger::EscapeDangerGoal, follow_parent::FollowParentGoal,
look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal,
tempt::TemptGoal, wander_around::WanderAroundGoal,
},
mob::{Mob, MobEntity},
passive::animal::Animal,
player::Player,
};
const TEMPT_ITEMS: &[&Item] = &[&Item::SWEET_BERRIES, &Item::GLOW_BERRIES];
pub const FOX_FOOD: &[&Item] = &[&Item::SWEET_BERRIES, &Item::GLOW_BERRIES];
/// Represents a Fox, a passive nocturnal mob.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[repr(i32)]
pub enum FoxVariant {
#[default]
Red = 0,
Snow = 1,
}
impl FoxVariant {
#[must_use]
pub const fn from_id(id: i32) -> Self {
match id {
1 => Self::Snow,
_ => Self::Red,
}
}
#[must_use]
pub const fn id(self) -> i32 {
self as i32
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Red => "red",
Self::Snow => "snow",
}
}
#[must_use]
pub fn from_name(s: &str) -> Self {
match s {
"snow" | "minecraft:snow" => Self::Snow,
_ => Self::Red,
}
}
}
/// Represents a Fox, a passive/neutral mob.
///
/// Wiki: <https://minecraft.wiki/w/Fox>
pub struct FoxEntity {
pub mob_entity: MobEntity,
pub ageable_data: AgeableData,
pub variant: AtomicI32,
pub flags: AtomicU8,
}
impl FoxEntity {
pub fn new(entity: Entity) -> Arc<Self> {
let mob_entity = MobEntity::new(entity);
let fox = Self { mob_entity };
let fox = Self {
mob_entity,
ageable_data: AgeableData::default(),
variant: AtomicI32::new(FoxVariant::Red.id()),
flags: AtomicU8::new(0),
};
let mob_arc = Arc::new(fox);
let mob_weak: Weak<dyn Mob> = {
let mob_arc: Arc<dyn Mob> = mob_arc.clone();
@ -41,7 +103,7 @@ impl FoxEntity {
goal_selector.add_goal(0, Box::new(SwimGoal::default()));
goal_selector.add_goal(1, EscapeDangerGoal::new(1.5));
goal_selector.add_goal(2, BreedGoal::new(1.0));
goal_selector.add_goal(3, Box::new(TemptGoal::new(1.2, TEMPT_ITEMS)));
goal_selector.add_goal(3, Box::new(TemptGoal::new(1.2, FOX_FOOD)));
goal_selector.add_goal(4, Box::new(FollowParentGoal::new(1.1)));
goal_selector.add_goal(6, Box::new(WanderAroundGoal::new(1.0)));
goal_selector.add_goal(
@ -53,12 +115,224 @@ impl FoxEntity {
mob_arc
}
#[must_use]
pub fn get_variant(&self) -> FoxVariant {
FoxVariant::from_id(self.variant.load(Ordering::Relaxed))
}
impl NBTStorage for FoxEntity {}
pub fn set_variant(&self, variant: FoxVariant) {
self.variant.store(variant.id(), Ordering::Relaxed);
let entity = self.get_entity();
entity.send_meta_data(
&[Metadata::new(
TrackedData::TYPE_ID,
MetaDataType::INT,
VarInt(variant.id()),
)],
None,
);
}
fn get_flag(&self, flag: u8) -> bool {
(self.flags.load(Ordering::Relaxed) & flag) != 0
}
fn set_flag(&self, flag: u8, value: bool) {
let current = self.flags.load(Ordering::Relaxed);
let new_flags = if value {
current | flag
} else {
current & !flag
};
self.flags.store(new_flags, Ordering::Relaxed);
let entity = self.get_entity();
entity.send_meta_data(
&[Metadata::new(
TrackedData::FLAGS_ID,
MetaDataType::BYTE,
new_flags,
)],
None,
);
}
#[must_use]
pub fn is_sitting(&self) -> bool {
self.get_flag(1)
}
pub fn set_sitting(&self, val: bool) {
self.set_flag(1, val);
}
#[must_use]
pub fn is_crouching(&self) -> bool {
self.get_flag(4)
}
pub fn set_crouching(&self, val: bool) {
self.set_flag(4, val);
}
#[must_use]
pub fn is_interested(&self) -> bool {
self.get_flag(8)
}
pub fn set_interested(&self, val: bool) {
self.set_flag(8, val);
}
#[must_use]
pub fn is_pouncing(&self) -> bool {
self.get_flag(16)
}
pub fn set_pouncing(&self, val: bool) {
self.set_flag(16, val);
}
#[must_use]
pub fn is_sleeping(&self) -> bool {
self.get_flag(32)
}
pub fn set_sleeping(&self, val: bool) {
self.set_flag(32, val);
}
#[must_use]
pub fn is_faceplanted(&self) -> bool {
self.get_flag(64)
}
pub fn set_faceplanted(&self, val: bool) {
self.set_flag(64, val);
}
#[must_use]
pub fn is_defending(&self) -> bool {
self.get_flag(128)
}
pub fn set_defending(&self, val: bool) {
self.set_flag(128, val);
}
pub fn clear_states(&self) {
self.set_interested(false);
self.set_crouching(false);
self.set_sitting(false);
self.set_sleeping(false);
self.set_defending(false);
self.set_faceplanted(false);
}
}
impl AgeableMob for FoxEntity {
fn get_ageable_data(&self) -> &AgeableData {
&self.ageable_data
}
}
impl Animal for FoxEntity {
fn is_food(&self, item_stack: &ItemStack) -> bool {
item_stack.item.has_tag(&tag::Item::MINECRAFT_FOX_FOOD)
|| FOX_FOOD.iter().any(|i| i.id == item_stack.item.id)
}
}
impl NBTStorage for FoxEntity {
fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
self.mob_entity.living_entity.write_nbt(nbt).await;
self.write_ageable_nbt(nbt);
self.write_animal_nbt(nbt);
nbt.put_string("Type", self.get_variant().as_str().to_string());
nbt.put_bool("Sleeping", self.is_sleeping());
nbt.put_bool("Sitting", self.is_sitting());
nbt.put_bool("Crouching", self.is_crouching());
})
}
fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
self.mob_entity.living_entity.read_nbt_non_mut(nbt).await;
self.read_ageable_nbt(nbt);
self.read_animal_nbt(nbt);
if let Some(variant_str) = nbt.get_string("Type") {
self.set_variant(FoxVariant::from_name(variant_str));
}
if let Some(sleeping) = nbt.get_bool("Sleeping") {
self.set_sleeping(sleeping);
}
if let Some(sitting) = nbt.get_bool("Sitting") {
self.set_sitting(sitting);
}
if let Some(crouching) = nbt.get_bool("Crouching") {
self.set_crouching(crouching);
}
})
}
}
impl Mob for FoxEntity {
fn get_mob_entity(&self) -> &MobEntity {
&self.mob_entity
}
fn mob_set_variant_name(&self, name: &str) {
self.set_variant(FoxVariant::from_name(name));
}
fn mob_tick<'a>(&'a self, _caller: &'a Arc<dyn EntityBase>) -> EntityBaseFuture<'a, ()> {
Box::pin(async move {
self.ageable_ai_step();
})
}
fn mob_init_data_tracker(&self) -> EntityBaseFuture<'_, ()> {
Box::pin(async move {
let entity = self.get_entity();
let is_baby = entity.age.load(Ordering::Relaxed) < 0;
if is_baby {
entity.send_meta_data(
&[Metadata::new(
TrackedData::BABY_ID,
MetaDataType::BOOLEAN,
true,
)],
None,
);
}
entity.send_meta_data(
&[Metadata::new(
TrackedData::TYPE_ID,
MetaDataType::INT,
VarInt(self.get_variant().id()),
)],
None,
);
entity.send_meta_data(
&[Metadata::new(
TrackedData::FLAGS_ID,
MetaDataType::BYTE,
self.flags.load(Ordering::Relaxed),
)],
None,
);
})
}
fn mob_interact<'a>(
&'a self,
player: &'a Arc<Player>,
item_stack: &'a mut ItemStack,
) -> EntityBaseFuture<'a, bool> {
Box::pin(async move {
self.animal_interact(player, item_stack, Sound::EntityFoxAmbient)
.await
})
}
}

View File

@ -1,10 +1,14 @@
use std::sync::{
Arc, Weak,
atomic::{AtomicU8, Ordering},
atomic::{AtomicI32, Ordering},
};
use pumpkin_data::entity::EntityType;
use pumpkin_data::item::Item;
use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::meta_data_type::MetaDataType;
use pumpkin_data::sound::Sound;
use pumpkin_data::tag::{self, Taggable};
use pumpkin_data::tracked_data::TrackedData;
use pumpkin_nbt::compound::NbtCompound;
use pumpkin_protocol::codec::var_int::VarInt;
@ -12,19 +16,69 @@ use pumpkin_protocol::java::client::play::Metadata;
use crate::entity::{
Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture,
ageable::{AgeableData, AgeableMob},
ai::goal::{
look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal,
wander_around::WanderAroundGoal,
tempt::TemptGoal, wander_around::WanderAroundGoal,
},
mob::{Mob, MobEntity},
passive::animal::Animal,
player::Player,
};
/// Represents a Frog, a passive mob that can eat small slimes and magma cubes.
pub const FROG_FOOD: &[&Item] = &[&Item::SLIME_BALL];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[repr(i32)]
pub enum FrogVariant {
Cold = 0,
#[default]
Temperate = 1,
Warm = 2,
}
impl FrogVariant {
#[must_use]
pub const fn from_id(id: i32) -> Self {
match id {
0 => Self::Cold,
2 => Self::Warm,
_ => Self::Temperate,
}
}
#[must_use]
pub const fn id(self) -> i32 {
self as i32
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Cold => "minecraft:cold",
Self::Temperate => "minecraft:temperate",
Self::Warm => "minecraft:warm",
}
}
#[must_use]
pub fn from_name(s: &str) -> Self {
match s {
"minecraft:cold" | "cold" => Self::Cold,
"minecraft:warm" | "warm" => Self::Warm,
_ => Self::Temperate,
}
}
}
/// Represents a Frog, an amphibious mob that can eat small slimes and magma cubes.
///
/// Wiki: <https://minecraft.wiki/w/Frog>
pub struct FrogEntity {
pub mob_entity: MobEntity,
pub variant: AtomicU8,
pub ageable_data: AgeableData,
pub variant: AtomicI32,
pub tongue_target_id: AtomicI32,
}
impl FrogEntity {
@ -32,7 +86,9 @@ impl FrogEntity {
let mob_entity = MobEntity::new(entity);
let frog = Self {
mob_entity,
variant: AtomicU8::new(1), // Default to temperate
ageable_data: AgeableData::default(),
variant: AtomicI32::new(FrogVariant::Temperate.id()),
tongue_target_id: AtomicI32::new(-1),
};
let mob_arc = Arc::new(frog);
let mob_weak: Weak<dyn Mob> = {
@ -48,41 +104,67 @@ impl FrogEntity {
.unwrap_or_else(std::sync::PoisonError::into_inner);
goal_selector.add_goal(0, Box::new(SwimGoal::default()));
goal_selector.add_goal(1, Box::new(WanderAroundGoal::new(1.0)));
goal_selector.add_goal(1, Box::new(TemptGoal::new(1.0, FROG_FOOD)));
goal_selector.add_goal(2, Box::new(WanderAroundGoal::new(1.0)));
goal_selector.add_goal(
2,
3,
LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 6.0),
);
goal_selector.add_goal(3, Box::new(RandomLookAroundGoal::default()));
goal_selector.add_goal(4, Box::new(RandomLookAroundGoal::default()));
};
mob_arc
}
#[must_use]
pub fn get_variant(&self) -> FrogVariant {
FrogVariant::from_id(self.variant.load(Ordering::Relaxed))
}
pub fn set_variant(&self, variant: FrogVariant) {
self.variant.store(variant.id(), Ordering::Relaxed);
let entity = self.get_entity();
entity.send_meta_data(
&[Metadata::new(
TrackedData::VARIANT,
MetaDataType::FROG_VARIANT,
VarInt(variant.id()),
)],
None,
);
}
}
impl AgeableMob for FrogEntity {
fn get_ageable_data(&self) -> &AgeableData {
&self.ageable_data
}
}
impl Animal for FrogEntity {
fn is_food(&self, item_stack: &ItemStack) -> bool {
item_stack.item.has_tag(&tag::Item::MINECRAFT_FROG_FOOD)
|| FROG_FOOD.iter().any(|i| i.id == item_stack.item.id)
}
}
impl NBTStorage for FrogEntity {
fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async {
Box::pin(async move {
self.mob_entity.living_entity.write_nbt(nbt).await;
let variant_str = match self.variant.load(Ordering::Relaxed) {
0 => "minecraft:cold",
2 => "minecraft:warm",
_ => "minecraft:temperate",
};
nbt.put_string("variant", variant_str.to_string());
self.write_ageable_nbt(nbt);
self.write_animal_nbt(nbt);
nbt.put_string("variant", self.get_variant().as_str().to_string());
})
}
fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async {
Box::pin(async move {
self.mob_entity.living_entity.read_nbt_non_mut(nbt).await;
self.read_ageable_nbt(nbt);
self.read_animal_nbt(nbt);
if let Some(variant_str) = nbt.get_string("variant") {
let variant = match variant_str {
"minecraft:cold" | "cold" => 0,
"minecraft:warm" | "warm" => 2,
_ => 1,
};
self.variant.store(variant, Ordering::Relaxed);
self.set_variant(FrogVariant::from_name(variant_str));
}
})
}
@ -94,12 +176,13 @@ impl Mob for FrogEntity {
}
fn mob_set_variant_name(&self, name: &str) {
let variant = match name {
"minecraft:cold" | "cold" => 0,
"minecraft:warm" | "warm" => 2,
_ => 1,
};
self.variant.store(variant, Ordering::Relaxed);
self.set_variant(FrogVariant::from_name(name));
}
fn mob_tick<'a>(&'a self, _caller: &'a Arc<dyn EntityBase>) -> EntityBaseFuture<'a, ()> {
Box::pin(async move {
self.ageable_ai_step();
})
}
fn mob_init_data_tracker(&self) -> EntityBaseFuture<'_, ()> {
@ -120,10 +203,21 @@ impl Mob for FrogEntity {
&[Metadata::new(
TrackedData::VARIANT,
MetaDataType::FROG_VARIANT,
VarInt(self.variant.load(Ordering::Relaxed) as i32),
VarInt(self.get_variant().id()),
)],
None,
);
})
}
fn mob_interact<'a>(
&'a self,
player: &'a Arc<Player>,
item_stack: &'a mut ItemStack,
) -> EntityBaseFuture<'a, bool> {
Box::pin(async move {
self.animal_interact(player, item_stack, Sound::EntityFrogAmbient)
.await
})
}
}

View File

@ -0,0 +1,249 @@
use std::sync::{
Arc, Weak,
atomic::{AtomicBool, AtomicI32, Ordering},
};
use pumpkin_data::entity::EntityType;
use pumpkin_data::item::Item;
use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::meta_data_type::MetaDataType;
use pumpkin_data::sound::Sound;
use pumpkin_data::tag::{self, Taggable};
use pumpkin_data::tracked_data::TrackedData;
use pumpkin_nbt::compound::NbtCompound;
use pumpkin_protocol::java::client::play::Metadata;
use crate::entity::{
Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture,
ageable::{AgeableData, AgeableMob},
ai::goal::{
look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal,
tempt::TemptGoal, wander_around::WanderAroundGoal,
},
mob::{Mob, MobEntity},
passive::animal::Animal,
player::Player,
};
pub const HAPPY_GHAST_FOOD: &[&Item] = &[&Item::SNOWBALL];
/// Represents a Happy Ghast, a passive flying mob.
///
/// Wiki: <https://minecraft.wiki/w/Happy_Ghast>
pub struct HappyGhastEntity {
pub mob_entity: MobEntity,
pub ageable_data: AgeableData,
pub server_still_timeout: AtomicI32,
pub leash_holder_time: AtomicI32,
pub is_leash_holder: AtomicBool,
pub stays_still: AtomicBool,
}
impl HappyGhastEntity {
pub fn new(entity: Entity) -> Arc<Self> {
let mob_entity = MobEntity::new(entity);
let happy_ghast = Self {
mob_entity,
ageable_data: AgeableData::default(),
server_still_timeout: AtomicI32::new(0),
leash_holder_time: AtomicI32::new(0),
is_leash_holder: AtomicBool::new(false),
stays_still: AtomicBool::new(false),
};
let mob_arc = Arc::new(happy_ghast);
let mob_weak: Weak<dyn Mob> = {
let mob_arc: Arc<dyn Mob> = mob_arc.clone();
Arc::downgrade(&mob_arc)
};
{
let mut goal_selector = mob_arc
.mob_entity
.goals_selector
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
goal_selector.add_goal(0, Box::new(SwimGoal::default()));
goal_selector.add_goal(1, Box::new(TemptGoal::new(1.0, HAPPY_GHAST_FOOD)));
goal_selector.add_goal(2, Box::new(WanderAroundGoal::new(1.0)));
goal_selector.add_goal(
3,
LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 6.0),
);
goal_selector.add_goal(4, Box::new(RandomLookAroundGoal::default()));
};
mob_arc
}
pub fn set_server_still_timeout(&self, timeout: i32) {
self.server_still_timeout.store(timeout, Ordering::Relaxed);
self.sync_stay_still_flag();
}
#[must_use]
pub fn is_on_still_timeout(&self) -> bool {
self.stays_still.load(Ordering::Relaxed)
|| self.server_still_timeout.load(Ordering::Relaxed) > 0
}
fn set_leash_holder(&self, is_leash_holder: bool) {
self.is_leash_holder
.store(is_leash_holder, Ordering::Relaxed);
let entity = self.get_entity();
entity.send_meta_data(
&[Metadata::new(
TrackedData::IS_LEASH_HOLDER,
MetaDataType::BOOLEAN,
is_leash_holder,
)],
None,
);
}
fn sync_stay_still_flag(&self) {
let stays_still = self.server_still_timeout.load(Ordering::Relaxed) > 0;
self.stays_still.store(stays_still, Ordering::Relaxed);
let entity = self.get_entity();
entity.send_meta_data(
&[Metadata::new(
TrackedData::STAYS_STILL,
MetaDataType::BOOLEAN,
stays_still,
)],
None,
);
}
}
impl AgeableMob for HappyGhastEntity {
fn get_ageable_data(&self) -> &AgeableData {
&self.ageable_data
}
}
impl NBTStorage for HappyGhastEntity {
fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
self.mob_entity.living_entity.write_nbt(nbt).await;
self.write_ageable_nbt(nbt);
self.write_animal_nbt(nbt);
nbt.put_int(
"still_timeout",
self.server_still_timeout.load(Ordering::Relaxed),
);
})
}
fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
self.mob_entity.living_entity.read_nbt_non_mut(nbt).await;
self.read_ageable_nbt(nbt);
self.read_animal_nbt(nbt);
if let Some(timeout) = nbt.get_int("still_timeout") {
self.set_server_still_timeout(timeout);
}
})
}
}
impl Animal for HappyGhastEntity {
fn is_food(&self, item_stack: &ItemStack) -> bool {
item_stack
.item
.has_tag(&tag::Item::MINECRAFT_HAPPY_GHAST_FOOD)
|| HAPPY_GHAST_FOOD.iter().any(|i| i.id == item_stack.item.id)
}
}
impl Mob for HappyGhastEntity {
fn get_mob_entity(&self) -> &MobEntity {
&self.mob_entity
}
fn mob_tick<'a>(&'a self, _caller: &'a Arc<dyn EntityBase>) -> EntityBaseFuture<'a, ()> {
Box::pin(async move {
self.ageable_ai_step();
let leash_time = self.leash_holder_time.load(Ordering::Relaxed);
if leash_time > 0 {
self.leash_holder_time.fetch_sub(1, Ordering::Relaxed);
}
self.set_leash_holder(leash_time > 0);
let still_timeout = self.server_still_timeout.load(Ordering::Relaxed);
if still_timeout > 0 {
let entity = self.get_entity();
if entity.age.load(Ordering::Relaxed) > 60 {
self.server_still_timeout.fetch_sub(1, Ordering::Relaxed);
}
self.sync_stay_still_flag();
}
// Continuous healing
let entity = self.get_entity();
if entity.is_alive() {
let living = &self.mob_entity.living_entity;
let current_health = living.health.load();
let max_health = living.get_max_health();
if current_health < max_health {
let world = entity.world.load();
let ticks = world.level_time.lock().await.world_age;
let heal_interval = 600;
if ticks % heal_interval == 0 {
living.set_health(current_health + 1.0);
}
}
}
})
}
fn mob_init_data_tracker(&self) -> EntityBaseFuture<'_, ()> {
Box::pin(async move {
let entity = self.get_entity();
let is_baby = entity.age.load(Ordering::Relaxed) < 0;
if is_baby {
entity.send_meta_data(
&[Metadata::new(
TrackedData::BABY_ID,
MetaDataType::BOOLEAN,
true,
)],
None,
);
}
entity.send_meta_data(
&[
Metadata::new(
TrackedData::IS_LEASH_HOLDER,
MetaDataType::BOOLEAN,
self.is_leash_holder.load(Ordering::Relaxed),
),
Metadata::new(
TrackedData::STAYS_STILL,
MetaDataType::BOOLEAN,
self.stays_still.load(Ordering::Relaxed),
),
],
None,
);
})
}
fn mob_interact<'a>(
&'a self,
player: &'a Arc<Player>,
item_stack: &'a mut ItemStack,
) -> EntityBaseFuture<'a, bool> {
Box::pin(async move {
if self.is_baby() {
return self
.animal_interact(player, item_stack, Sound::EntityGhastlingAmbient)
.await;
}
self.animal_interact(player, item_stack, Sound::EntityHappyGhastAmbient)
.await
})
}
}

View File

@ -1,29 +1,48 @@
use std::sync::{Arc, Weak};
use std::sync::{
Arc, Weak,
atomic::{AtomicBool, AtomicI32, Ordering},
};
use pumpkin_data::entity::EntityType;
use pumpkin_data::item::Item;
use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::meta_data_type::MetaDataType;
use pumpkin_data::sound::{Sound, SoundCategory};
use pumpkin_data::tracked_data::TrackedData;
use pumpkin_nbt::compound::NbtCompound;
use pumpkin_protocol::java::client::play::Metadata;
use pumpkin_util::GameMode;
use crate::entity::{
Entity, NBTStorage, NbtFuture,
Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture,
ai::goal::{
active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal,
look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, revenge::RevengeGoal,
wander_around::WanderAroundGoal,
},
mob::{Mob, MobEntity},
player::Player,
};
use pumpkin_nbt::compound::NbtCompound;
/// Represents an Iron Golem, a powerful neutral mob that protects villagers and players.
///
/// Wiki: <https://minecraft.wiki/w/Iron_Golem>
pub struct IronGolemEntity {
pub mob_entity: MobEntity,
pub player_created: AtomicBool,
pub attack_animation_tick: AtomicI32,
pub offer_flower_tick: AtomicI32,
}
impl IronGolemEntity {
pub fn new(entity: Entity) -> Arc<Self> {
let mob_entity = MobEntity::new(entity);
let iron_golem = Self { mob_entity };
let iron_golem = Self {
mob_entity,
player_created: AtomicBool::new(false),
attack_animation_tick: AtomicI32::new(0),
offer_flower_tick: AtomicI32::new(0),
};
let mob_arc = Arc::new(iron_golem);
let mob_weak: Weak<dyn Mob> = {
let mob_arc: Arc<dyn Mob> = mob_arc.clone();
@ -63,15 +82,42 @@ impl IronGolemEntity {
mob_arc
}
#[must_use]
pub fn is_player_created(&self) -> bool {
self.player_created.load(Ordering::Relaxed)
}
pub fn set_player_created(&self, value: bool) {
self.player_created.store(value, Ordering::Relaxed);
let entity = self.get_entity();
let flag: u8 = u8::from(value);
entity.send_meta_data(
&[Metadata::new(
TrackedData::FLAGS_ID,
MetaDataType::BYTE,
flag,
)],
None,
);
}
}
impl NBTStorage for IronGolemEntity {
fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
self.mob_entity.living_entity.write_nbt(nbt)
Box::pin(async move {
self.mob_entity.living_entity.write_nbt(nbt).await;
nbt.put_bool("PlayerCreated", self.is_player_created());
})
}
fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
self.mob_entity.living_entity.read_nbt_non_mut(nbt)
Box::pin(async move {
self.mob_entity.living_entity.read_nbt_non_mut(nbt).await;
if let Some(created) = nbt.get_bool("PlayerCreated") {
self.set_player_created(created);
}
})
}
}
@ -79,4 +125,59 @@ impl Mob for IronGolemEntity {
fn get_mob_entity(&self) -> &MobEntity {
&self.mob_entity
}
fn mob_tick<'a>(&'a self, _caller: &'a Arc<dyn EntityBase>) -> EntityBaseFuture<'a, ()> {
Box::pin(async move {
let attack_tick = self.attack_animation_tick.load(Ordering::Relaxed);
if attack_tick > 0 {
self.attack_animation_tick.fetch_sub(1, Ordering::Relaxed);
}
let flower_tick = self.offer_flower_tick.load(Ordering::Relaxed);
if flower_tick > 0 {
self.offer_flower_tick.fetch_sub(1, Ordering::Relaxed);
}
})
}
fn mob_init_data_tracker(&self) -> EntityBaseFuture<'_, ()> {
Box::pin(async move {
let entity = self.get_entity();
let flag: u8 = u8::from(self.is_player_created());
entity.send_meta_data(
&[Metadata::new(
TrackedData::FLAGS_ID,
MetaDataType::BYTE,
flag,
)],
None,
);
})
}
fn mob_interact<'a>(
&'a self,
player: &'a Arc<Player>,
item_stack: &'a mut ItemStack,
) -> EntityBaseFuture<'a, bool> {
Box::pin(async move {
if item_stack.item.id == Item::IRON_INGOT.id {
let living = &self.mob_entity.living_entity;
let current_health = living.health.load();
let max_health = living.get_max_health();
if current_health < max_health {
living.set_health((current_health + 25.0).min(max_health));
let entity = self.get_entity();
let world = entity.world.load();
let pos = entity.pos.load();
world.play_sound(Sound::EntityIronGolemRepair, SoundCategory::Neutral, &pos);
if player.gamemode.load() != GameMode::Creative {
item_stack.item_count = item_stack.item_count.saturating_sub(1);
}
return true;
}
}
false
})
}
}

View File

@ -7,6 +7,7 @@ pub mod camel;
pub mod cat;
pub mod chicken;
pub mod cod;
pub mod copper_golem;
pub mod cow;
pub mod dolphin;
pub mod donkey;
@ -14,11 +15,13 @@ pub mod fox;
pub mod frog;
pub mod glow_squid;
pub mod goat;
pub mod happy_ghast;
pub mod horse;
pub mod iron_golem;
pub mod llama;
pub mod mooshroom;
pub mod mule;
pub mod nautilus;
pub mod ocelot;
pub mod panda;
pub mod parrot;

View File

@ -0,0 +1,416 @@
use crossbeam::atomic::AtomicCell;
use std::sync::{
Arc,
atomic::{AtomicBool, AtomicI32, Ordering},
};
use tokio::sync::Mutex;
use uuid::Uuid;
use pumpkin_data::{
effect::StatusEffect,
entity::EntityStatus,
item_stack::ItemStack,
meta_data_type::MetaDataType,
particle::Particle,
potion::Effect,
sound::{Sound, SoundCategory},
tracked_data::TrackedData,
};
use pumpkin_nbt::compound::NbtCompound;
use pumpkin_protocol::java::client::play::Metadata;
use pumpkin_util::math::vector3::Vector3;
use crate::entity::{
Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture,
mob::{Mob, MobEntity},
passive::animal::Animal,
player::Player,
};
pub struct NautilusEntity {
pub mob_entity: MobEntity,
pub is_tame: AtomicBool,
pub owner: AtomicCell<Option<Uuid>>,
pub is_dashing: AtomicBool,
pub dash_cooldown: AtomicI32,
pub is_saddled: AtomicBool,
pub inventory: Mutex<Vec<ItemStack>>,
}
impl NautilusEntity {
pub fn new(entity: Entity) -> Arc<Self> {
let mob_entity = MobEntity::new(entity);
let nautilus = Self {
mob_entity,
is_tame: AtomicBool::new(false),
owner: AtomicCell::new(None),
is_dashing: AtomicBool::new(false),
dash_cooldown: AtomicI32::new(0),
is_saddled: AtomicBool::new(false),
inventory: Mutex::new(vec![ItemStack::new(0, &pumpkin_data::item::Item::AIR); 9]),
};
Arc::new(nautilus)
}
pub fn is_dashing(&self) -> bool {
self.is_dashing.load(Ordering::Relaxed)
}
pub fn set_dashing(&self, dashing: bool) {
self.is_dashing.store(dashing, Ordering::Relaxed);
self.mob_entity.living_entity.entity.send_meta_data(
&[Metadata::new(
TrackedData::DASH,
MetaDataType::BOOLEAN,
dashing,
)],
None,
);
}
pub fn is_tame(&self) -> bool {
self.is_tame.load(Ordering::Relaxed)
}
pub fn set_tame(&self, tame: bool, owner: Option<Uuid>) {
self.is_tame.store(tame, Ordering::Relaxed);
self.owner.store(owner);
}
pub fn get_ambient_sound(&self) -> Sound {
let is_baby = self
.mob_entity
.living_entity
.entity
.age
.load(Ordering::Relaxed)
< 0;
let is_water = self
.mob_entity
.living_entity
.entity
.touching_water
.load(Ordering::Relaxed);
if is_baby {
if is_water {
Sound::EntityBabyNautilusAmbient
} else {
Sound::EntityBabyNautilusAmbientLand
}
} else if is_water {
Sound::EntityNautilusAmbient
} else {
Sound::EntityNautilusAmbientLand
}
}
pub fn get_hurt_sound(&self) -> Sound {
let is_baby = self
.mob_entity
.living_entity
.entity
.age
.load(Ordering::Relaxed)
< 0;
let is_water = self
.mob_entity
.living_entity
.entity
.touching_water
.load(Ordering::Relaxed);
if is_baby {
if is_water {
Sound::EntityBabyNautilusHurt
} else {
Sound::EntityBabyNautilusHurtLand
}
} else if is_water {
Sound::EntityNautilusHurt
} else {
Sound::EntityNautilusHurtLand
}
}
pub fn get_death_sound(&self) -> Sound {
let is_baby = self
.mob_entity
.living_entity
.entity
.age
.load(Ordering::Relaxed)
< 0;
let is_water = self
.mob_entity
.living_entity
.entity
.touching_water
.load(Ordering::Relaxed);
if is_baby {
if is_water {
Sound::EntityBabyNautilusDeath
} else {
Sound::EntityBabyNautilusDeathLand
}
} else if is_water {
Sound::EntityNautilusDeath
} else {
Sound::EntityNautilusDeathLand
}
}
pub fn get_dash_sound(&self) -> Sound {
let is_water = self
.mob_entity
.living_entity
.entity
.touching_water
.load(Ordering::Relaxed);
if is_water {
Sound::EntityNautilusDash
} else {
Sound::EntityNautilusDashLand
}
}
pub fn get_dash_ready_sound(&self) -> Sound {
let is_water = self
.mob_entity
.living_entity
.entity
.touching_water
.load(Ordering::Relaxed);
if is_water {
Sound::EntityNautilusDashReady
} else {
Sound::EntityNautilusDashReadyLand
}
}
pub fn get_eat_sound(&self) -> Sound {
let is_baby = self
.mob_entity
.living_entity
.entity
.age
.load(Ordering::Relaxed)
< 0;
if is_baby {
Sound::EntityBabyNautilusEat
} else {
Sound::EntityNautilusEat
}
}
pub fn get_swim_sound(&self) -> Sound {
let is_baby = self
.mob_entity
.living_entity
.entity
.age
.load(Ordering::Relaxed)
< 0;
if is_baby {
Sound::EntityBabyNautilusSwim
} else {
Sound::EntityNautilusSwim
}
}
}
impl NBTStorage for NautilusEntity {
fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
self.mob_entity.living_entity.write_nbt(nbt).await;
self.write_animal_nbt(nbt);
nbt.put_bool("IsTame", self.is_tame.load(Ordering::Relaxed));
nbt.put_bool("Saddled", self.is_saddled.load(Ordering::Relaxed));
nbt.put_int("DashCooldown", self.dash_cooldown.load(Ordering::Relaxed));
if let Some(owner) = self.owner.load() {
nbt.put_uuid("Owner", owner);
}
})
}
fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
self.mob_entity.living_entity.read_nbt_non_mut(nbt).await;
self.read_animal_nbt(nbt);
if let Some(is_tame) = nbt.get_bool("IsTame") {
self.is_tame.store(is_tame, Ordering::Relaxed);
}
if let Some(saddled) = nbt.get_bool("Saddled") {
self.is_saddled.store(saddled, Ordering::Relaxed);
}
if let Some(dash) = nbt.get_int("DashCooldown") {
self.dash_cooldown.store(dash, Ordering::Relaxed);
}
if let Some(owner) = nbt.get_uuid("Owner") {
self.owner.store(Some(owner));
}
})
}
}
impl Animal for NautilusEntity {
fn is_food(&self, item_stack: &ItemStack) -> bool {
item_stack.item == &pumpkin_data::item::Item::NAUTILUS_SHELL
|| item_stack.item == &pumpkin_data::item::Item::PUFFERFISH
|| item_stack.item == &pumpkin_data::item::Item::COD
|| item_stack.item == &pumpkin_data::item::Item::SALMON
|| item_stack.item == &pumpkin_data::item::Item::TROPICAL_FISH
}
}
impl Mob for NautilusEntity {
fn get_mob_entity(&self) -> &MobEntity {
&self.mob_entity
}
fn mob_init_data_tracker(&self) -> EntityBaseFuture<'_, ()> {
Box::pin(async move {
self.mob_entity.living_entity.entity.send_meta_data(
&[Metadata::new(
TrackedData::DASH,
MetaDataType::BOOLEAN,
self.is_dashing(),
)],
None,
);
})
}
fn mob_tick<'a>(&'a self, _caller: &'a Arc<dyn EntityBase>) -> EntityBaseFuture<'a, ()> {
Box::pin(async move {
let entity = &self.mob_entity.living_entity.entity;
let passengers = entity.passengers.lock().await;
if let Some(passenger) = passengers.first()
&& let Some(player) = passenger.cast_any().downcast_ref::<Player>()
{
let world = entity.world.load();
let game_time = world.level_time.lock().await.world_age;
if game_time % 40 == 0 {
player
.living_entity
.add_effect(Effect {
effect_type: &StatusEffect::BREATH_OF_THE_NAUTILUS,
duration: 60,
amplifier: 0,
ambient: true,
show_particles: true,
show_icon: true,
blend: true,
})
.await;
}
}
if self.is_dashing() && self.dash_cooldown.load(Ordering::Relaxed) < 35 {
self.set_dashing(false);
}
let cooldown = self.dash_cooldown.load(Ordering::Relaxed);
if cooldown > 0 {
let next = cooldown - 1;
self.dash_cooldown.store(next, Ordering::Relaxed);
if next == 0 {
let world = entity.world.load();
world.play_sound(
self.get_dash_ready_sound(),
SoundCategory::Neutral,
&entity.pos.load(),
);
}
}
if entity.touching_water.load(Ordering::Relaxed) {
let velo = entity.velocity.load();
let speed = velo.length();
let prob = (speed * 2.0).clamp(0.15, 1.0);
if rand::random::<f64>() < prob {
let world = entity.world.load();
let pos = entity.pos.load();
world.spawn_particle(
pos + Vector3::new(0.0, 0.25, 0.0),
Vector3::new(0.4, 0.4, 0.4),
0.5,
2,
Particle::Bubble,
);
}
}
})
}
fn mob_interact<'a>(
&'a self,
player: &'a Arc<Player>,
item_stack: &'a mut ItemStack,
) -> EntityBaseFuture<'a, bool> {
Box::pin(async move {
let mob_entity = &self.mob_entity;
let entity = &mob_entity.living_entity.entity;
if !self.is_tame() && self.is_food(item_stack) {
item_stack.decrement_unless_creative(player.gamemode.load(), 1);
if rand::random::<u32>().is_multiple_of(3) {
self.set_tame(true, Some(player.gameprofile.id));
let world = entity.world.load();
world.send_entity_status(entity, EntityStatus::TamingSucceeded, None);
} else {
let world = entity.world.load();
world.send_entity_status(entity, EntityStatus::TamingFailed, None);
}
let world = entity.world.load();
world.play_sound(
self.get_eat_sound(),
SoundCategory::Neutral,
&entity.pos.load(),
);
return true;
}
if self.is_tame() && !player.get_entity().is_sneaking() {
if !self.is_saddled.load(Ordering::Relaxed)
&& item_stack.item == &pumpkin_data::item::Item::SADDLE
{
item_stack.decrement_unless_creative(player.gamemode.load(), 1);
self.is_saddled.store(true, Ordering::Relaxed);
let world = entity.world.load();
world.play_sound(
Sound::ItemNautilusSaddleEquip,
SoundCategory::Neutral,
&entity.pos.load(),
);
return true;
}
let world = player.world();
if let Some(vehicle) = world.get_entity_by_id(entity.entity_id)
&& let Some(passenger) = world.get_player_by_id(player.entity_id())
{
entity
.add_passenger(vehicle, passenger as Arc<dyn EntityBase>)
.await;
return true;
}
}
self.animal_interact(player, item_stack, self.get_ambient_sound())
.await
})
}
fn is_saddled(&self) -> bool {
self.is_saddled.load(Ordering::Relaxed)
}
fn can_be_saddled(&self) -> bool {
self.mob_entity.living_entity.entity.is_alive()
}
fn set_saddled(&self, saddled: bool) {
self.is_saddled.store(saddled, Ordering::Relaxed);
}
}

View File

@ -1,27 +1,49 @@
use std::sync::{Arc, Weak};
use std::sync::{
Arc, Weak,
atomic::{AtomicBool, Ordering},
};
use pumpkin_data::entity::EntityType;
use pumpkin_data::entity::{EntityStatus, EntityType};
use pumpkin_data::item::Item;
use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::meta_data_type::MetaDataType;
use pumpkin_data::tag::{self, Taggable};
use pumpkin_data::tracked_data::TrackedData;
use pumpkin_nbt::compound::NbtCompound;
use pumpkin_protocol::bedrock::server::actor_event::ActorEventType;
use pumpkin_protocol::java::client::play::Metadata;
use rand::RngExt;
use crate::entity::{
Entity, NBTStorage,
Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture,
ai::goal::{
active_target::ActiveTargetGoal, avoid_entity::AvoidEntityGoal, breed::BreedGoal,
escape_danger::EscapeDangerGoal, follow_parent::FollowParentGoal,
look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal,
wander_around::WanderAroundGoal,
tempt::TemptGoal, wander_around::WanderAroundGoal,
},
mob::{Mob, MobEntity},
passive::animal::Animal,
player::Player,
};
const TEMPT_ITEMS: &[&Item] = &[&Item::COD, &Item::SALMON];
/// Represents an Ocelot, a shy passive mob found in jungles.
///
/// Wiki: <https://minecraft.wiki/w/Ocelot>
pub struct OcelotEntity {
pub mob_entity: MobEntity,
pub is_trusting: AtomicBool,
}
impl OcelotEntity {
pub fn new(entity: Entity) -> Arc<Self> {
let mob_entity = MobEntity::new(entity);
let ocelot = Self { mob_entity };
let ocelot = Self {
mob_entity,
is_trusting: AtomicBool::new(false),
};
let mob_arc = Arc::new(ocelot);
let mob_weak: Weak<dyn Mob> = {
let mob_arc: Arc<dyn Mob> = mob_arc.clone();
@ -35,23 +57,171 @@ impl OcelotEntity {
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
goal_selector.add_goal(0, Box::new(SwimGoal::default()));
goal_selector.add_goal(1, Box::new(WanderAroundGoal::new(1.0)));
// Goal 1: FloatGoal (SwimGoal)
goal_selector.add_goal(1, Box::new(SwimGoal::default()));
// Goal 1: PanicGoal (EscapeDangerGoal)
goal_selector.add_goal(1, EscapeDangerGoal::new(1.5));
// Goal 3: OcelotTemptGoal
goal_selector.add_goal(3, Box::new(TemptGoal::new(0.6, TEMPT_ITEMS)));
// Goal 4: OcelotAvoidEntityGoal (when not trusting)
goal_selector.add_goal(
2,
LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 6.0),
4,
Box::new(AvoidEntityGoal::new(&EntityType::PLAYER, 16.0, 0.8, 1.33)),
);
// Goal 9: BreedGoal
goal_selector.add_goal(9, BreedGoal::new(0.8));
// Goal 9: FollowParentGoal
goal_selector.add_goal(9, Box::new(FollowParentGoal::new(0.8)));
// Goal 10: WaterAvoidingRandomStrollGoal (WanderAroundGoal)
goal_selector.add_goal(10, Box::new(WanderAroundGoal::new(0.8)));
// Goal 11: LookAtPlayerGoal
goal_selector.add_goal(
11,
LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 10.0),
);
// Goal 11: RandomLookAroundGoal
goal_selector.add_goal(11, Box::new(RandomLookAroundGoal::default()));
};
{
let mut target_selector = mob_arc
.mob_entity
.target_selector
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
// Target Goal 1: NearestAttackableTargetGoal for Chicken and Turtle
target_selector.add_goal(
1,
ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::CHICKEN, false),
);
target_selector.add_goal(
1,
ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::TURTLE, false),
);
goal_selector.add_goal(3, Box::new(RandomLookAroundGoal::default()));
};
mob_arc
}
pub fn is_trusting(&self) -> bool {
self.is_trusting.load(Ordering::Relaxed)
}
impl NBTStorage for OcelotEntity {}
pub fn set_trusting(&self, trusting: bool) {
self.is_trusting.store(trusting, Ordering::Relaxed);
let entity = self.get_entity();
entity.send_meta_data(
&[Metadata::new(
TrackedData::TRUSTING,
MetaDataType::BOOLEAN,
trusting,
)],
None,
);
}
}
impl NBTStorage for OcelotEntity {
fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async {
self.mob_entity.living_entity.write_nbt(nbt).await;
self.write_animal_nbt(nbt);
nbt.put_bool("Trusting", self.is_trusting.load(Ordering::Relaxed));
})
}
fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async {
self.mob_entity.living_entity.read_nbt_non_mut(nbt).await;
self.read_animal_nbt(nbt);
if let Some(trusting) = nbt.get_bool("Trusting") {
self.is_trusting.store(trusting, Ordering::Relaxed);
}
})
}
}
impl Animal for OcelotEntity {
fn is_food(&self, item_stack: &ItemStack) -> bool {
let item = item_stack.get_item();
item.has_tag(&tag::Item::MINECRAFT_OCELOT_FOOD)
|| item == &Item::COD
|| item == &Item::SALMON
}
}
impl Mob for OcelotEntity {
fn get_mob_entity(&self) -> &MobEntity {
&self.mob_entity
}
fn mob_init_data_tracker(&self) -> EntityBaseFuture<'_, ()> {
Box::pin(async move {
let entity = self.get_entity();
let is_baby = entity.age.load(Ordering::Relaxed) < 0;
if is_baby {
entity.send_meta_data(
&[Metadata::new(
TrackedData::BABY_ID,
MetaDataType::BOOLEAN,
true,
)],
None,
);
}
entity.send_meta_data(
&[Metadata::new(
TrackedData::TRUSTING,
MetaDataType::BOOLEAN,
self.is_trusting.load(Ordering::Relaxed),
)],
None,
);
})
}
fn mob_interact<'a>(
&'a self,
player: &'a Arc<Player>,
item_stack: &'a mut ItemStack,
) -> EntityBaseFuture<'a, bool> {
Box::pin(async move {
let is_food = self.is_food(item_stack);
let dist_sqr = self
.get_entity()
.pos
.load()
.squared_distance_to_vec(&player.get_entity().pos.load());
if !self.is_trusting() && is_food && dist_sqr < 9.0 {
item_stack.decrement_unless_creative(player.gamemode.load(), 1);
let mut rng = rand::rng();
if rng.random_range(0..3) == 0 {
self.set_trusting(true);
self.get_entity().world.load().send_entity_status(
self.get_entity(),
EntityStatus::TrustingSucceeded,
Some(ActorEventType::TrustingSucceeded),
);
} else {
self.get_entity().world.load().send_entity_status(
self.get_entity(),
EntityStatus::TrustingFailed,
Some(ActorEventType::TrustingFailed),
);
}
return true;
}
self.animal_interact(
player,
item_stack,
pumpkin_data::sound::Sound::EntityOcelotAmbient,
)
.await
})
}
}

View File

@ -107,7 +107,11 @@ impl NBTStorage for PigEntity {
impl super::animal::Animal for PigEntity {
fn is_food(&self, item_stack: &ItemStack) -> bool {
PIG_FOOD.iter().any(|i| i.id == item_stack.item.id)
use pumpkin_data::tag::Taggable;
item_stack
.item
.has_tag(&pumpkin_data::tag::Item::MINECRAFT_PIG_FOOD)
|| PIG_FOOD.iter().any(|i| i.id == item_stack.item.id)
}
}

View File

@ -11,6 +11,7 @@ use pumpkin_protocol::java::client::play::Metadata;
use crate::entity::{
Entity, EntityBaseFuture, NBTStorage, NbtFuture,
ageable::AgeableMob,
ai::goal::{
breed::BreedGoal, eat_grass::EatGrassGoal, escape_danger::EscapeDangerGoal,
follow_parent::FollowParentGoal, look_around::RandomLookAroundGoal,
@ -18,6 +19,7 @@ use crate::entity::{
wander_around::WanderAroundGoal,
},
mob::{Mob, MobEntity},
passive::animal::Animal,
player::Player,
};
@ -29,6 +31,7 @@ const TEMPT_ITEMS: &[&Item] = &[&Item::WHEAT];
pub struct SheepEntity {
pub mob_entity: MobEntity,
color_and_sheared: AtomicU8,
pub ageable_data: crate::entity::ageable::AgeableData,
}
impl SheepEntity {
@ -37,6 +40,7 @@ impl SheepEntity {
let sheep = Self {
mob_entity,
color_and_sheared: AtomicU8::new(0),
ageable_data: crate::entity::ageable::AgeableData::default(),
};
let mob_arc = Arc::new(sheep);
let mob_weak: Weak<dyn Mob> = {
@ -107,10 +111,18 @@ impl SheepEntity {
}
}
impl crate::entity::ageable::AgeableMob for SheepEntity {
fn get_ageable_data(&self) -> &crate::entity::ageable::AgeableData {
&self.ageable_data
}
}
impl NBTStorage for SheepEntity {
fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async {
self.mob_entity.living_entity.entity.write_nbt(nbt).await;
self.mob_entity.living_entity.write_nbt(nbt).await;
self.write_ageable_nbt(nbt);
self.write_animal_nbt(nbt);
nbt.put_bool("Sheared", self.is_sheared());
nbt.put_byte("Color", self.get_color() as i8);
})
@ -118,12 +130,13 @@ impl NBTStorage for SheepEntity {
fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async {
self.mob_entity
.living_entity
.entity
.read_nbt_non_mut(nbt)
.await;
let sheared = nbt.get_bool("Sheared").unwrap_or(false);
self.mob_entity.living_entity.read_nbt_non_mut(nbt).await;
self.read_ageable_nbt(nbt);
self.read_animal_nbt(nbt);
let sheared = nbt
.get_bool("Sheared")
.or_else(|| nbt.get_byte("Sheared").map(|b| b == 1))
.unwrap_or(false);
let color = nbt.get_byte("Color").unwrap_or(0) as u8;
let byte = (color & 0x0F) | if sheared { 0x10 } else { 0 };
self.color_and_sheared.store(byte, Ordering::Relaxed);
@ -133,7 +146,11 @@ impl NBTStorage for SheepEntity {
impl super::animal::Animal for SheepEntity {
fn is_food(&self, item_stack: &ItemStack) -> bool {
TEMPT_ITEMS.iter().any(|i| i.id == item_stack.item.id)
use pumpkin_data::tag::Taggable;
item_stack
.item
.has_tag(&pumpkin_data::tag::Item::MINECRAFT_SHEEP_FOOD)
|| TEMPT_ITEMS.iter().any(|i| i.id == item_stack.item.id)
}
}

View File

@ -1,24 +1,93 @@
use std::sync::{Arc, Weak};
use pumpkin_data::entity::EntityType;
use crate::entity::{
Entity, NBTStorage,
ai::goal::{
look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal,
wander_around::WanderAroundGoal,
},
mob::{Mob, MobEntity},
use std::sync::{
Arc, Weak,
atomic::{AtomicI32, Ordering},
};
use pumpkin_data::entity::{EntityStatus, EntityType};
use pumpkin_data::item::Item;
use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::meta_data_type::MetaDataType;
use pumpkin_data::sound::{Sound, SoundCategory};
use pumpkin_data::tag::{self, Taggable};
use pumpkin_data::tracked_data::TrackedData;
use pumpkin_nbt::compound::NbtCompound;
use pumpkin_protocol::codec::var_int::VarInt;
use pumpkin_protocol::java::client::play::Metadata;
use pumpkin_util::math::position::BlockPos;
use pumpkin_util::math::vector3::Vector3;
use crate::entity::{
Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture,
ageable::{AgeableData, AgeableMob},
ai::goal::{
breed::BreedGoal, follow_parent::FollowParentGoal, look_around::RandomLookAroundGoal,
look_at_entity::LookAtEntityGoal, swim::SwimGoal, tempt::TemptGoal,
wander_around::WanderAroundGoal,
},
item::ItemEntity,
mob::{Mob, MobEntity},
passive::animal::Animal,
player::Player,
};
pub const SNIFFER_FOOD: &[&Item] = &[&Item::TORCHFLOWER_SEEDS, &Item::PITCHER_POD];
pub const SNIFFER_BABY_START_AGE: i32 = -48000;
pub const DIGGING_DROP_SEED_OFFSET_TICKS: i32 = 120;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[repr(i32)]
pub enum SnifferState {
#[default]
Idling = 0,
FeelingHappy = 1,
Scenting = 2,
Sniffing = 3,
Searching = 4,
Digging = 5,
Rising = 6,
}
impl SnifferState {
#[must_use]
pub const fn from_id(id: i32) -> Self {
match id {
1 => Self::FeelingHappy,
2 => Self::Scenting,
3 => Self::Sniffing,
4 => Self::Searching,
5 => Self::Digging,
6 => Self::Rising,
_ => Self::Idling,
}
}
#[must_use]
pub const fn id(self) -> i32 {
self as i32
}
}
/// Represents a Sniffer, a large passive mob that digs up ancient seeds.
///
/// Wiki: <https://minecraft.wiki/w/Sniffer>
pub struct SnifferEntity {
pub mob_entity: MobEntity,
pub ageable_data: AgeableData,
pub state: AtomicI32,
pub drop_seed_at_tick: AtomicI32,
pub explored_positions: std::sync::Mutex<Vec<BlockPos>>,
}
impl SnifferEntity {
pub fn new(entity: Entity) -> Arc<Self> {
let mob_entity = MobEntity::new(entity);
let sniffer = Self { mob_entity };
let sniffer = Self {
mob_entity,
ageable_data: AgeableData::default(),
state: AtomicI32::new(SnifferState::Idling.id()),
drop_seed_at_tick: AtomicI32::new(0),
explored_positions: std::sync::Mutex::new(Vec::new()),
};
let mob_arc = Arc::new(sniffer);
let mob_weak: Weak<dyn Mob> = {
let mob_arc: Arc<dyn Mob> = mob_arc.clone();
@ -33,22 +102,334 @@ impl SnifferEntity {
.unwrap_or_else(std::sync::PoisonError::into_inner);
goal_selector.add_goal(0, Box::new(SwimGoal::default()));
goal_selector.add_goal(1, Box::new(WanderAroundGoal::new(1.0)));
goal_selector.add_goal(1, BreedGoal::new(1.0));
goal_selector.add_goal(2, Box::new(TemptGoal::new(1.2, SNIFFER_FOOD)));
goal_selector.add_goal(3, Box::new(FollowParentGoal::new(1.1)));
goal_selector.add_goal(4, Box::new(WanderAroundGoal::new(1.0)));
goal_selector.add_goal(
2,
5,
LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 6.0),
);
goal_selector.add_goal(3, Box::new(RandomLookAroundGoal::default()));
goal_selector.add_goal(6, Box::new(RandomLookAroundGoal::default()));
};
mob_arc
}
#[must_use]
pub fn get_state(&self) -> SnifferState {
SnifferState::from_id(self.state.load(Ordering::Relaxed))
}
impl NBTStorage for SnifferEntity {}
pub fn set_state(&self, state: SnifferState) {
self.state.store(state.id(), Ordering::Relaxed);
let entity = self.get_entity();
entity.send_meta_data(
&[Metadata::new(
TrackedData::STATE,
MetaDataType::SNIFFER_STATE,
VarInt(state.id()),
)],
None,
);
}
pub fn transition_to(&self, state: SnifferState) {
let entity = self.get_entity();
let world = entity.world.load();
let pos = entity.pos.load();
match state {
SnifferState::Idling => {
self.set_state(SnifferState::Idling);
}
SnifferState::FeelingHappy => {
world.play_sound(Sound::EntitySnifferHappy, SoundCategory::Neutral, &pos);
self.set_state(SnifferState::FeelingHappy);
}
SnifferState::Scenting => {
self.set_state(SnifferState::Scenting);
self.on_scenting_start();
}
SnifferState::Sniffing => {
world.play_sound(Sound::EntitySnifferSniffing, SoundCategory::Neutral, &pos);
self.set_state(SnifferState::Sniffing);
}
SnifferState::Searching => {
self.set_state(SnifferState::Searching);
}
SnifferState::Digging => {
self.set_state(SnifferState::Digging);
self.on_digging_start();
}
SnifferState::Rising => {
world.play_sound(
Sound::EntitySnifferDiggingStop,
SoundCategory::Neutral,
&pos,
);
self.set_state(SnifferState::Rising);
}
}
}
fn on_scenting_start(&self) {
let entity = self.get_entity();
let world = entity.world.load();
let pos = entity.pos.load();
let pitch = if self.is_baby() { 1.3 } else { 1.0 };
world.play_sound_fine(
Sound::EntitySnifferScenting,
SoundCategory::Neutral,
&pos,
1.0,
pitch,
);
}
fn on_digging_start(&self) {
let entity = self.get_entity();
let world = entity.world.load();
let current_ticks = world.level_time.try_lock().map_or(0, |t| t.world_age);
let drop_tick = current_ticks as i32 + DIGGING_DROP_SEED_OFFSET_TICKS;
self.drop_seed_at_tick.store(drop_tick, Ordering::Relaxed);
entity.send_meta_data(
&[Metadata::new(
TrackedData::DROP_SEED_AT_TICK,
MetaDataType::INT,
VarInt(drop_tick),
)],
None,
);
world.send_entity_status(entity, EntityStatus::SnifferDiggingSound, None);
}
pub fn on_digging_complete(&self, success: bool) {
if success {
let head_block = self.get_head_block().down();
self.store_explored_position(head_block);
}
}
#[must_use]
pub fn get_head_position(&self) -> Vector3<f64> {
let entity = self.get_entity();
let pos = entity.pos.load();
let yaw = f64::from(entity.yaw.load());
let yaw_rad = yaw.to_radians();
let forward = Vector3::new(-yaw_rad.sin(), 0.0, yaw_rad.cos());
pos + forward * 2.25
}
#[must_use]
pub fn get_head_block(&self) -> BlockPos {
let head_pos = self.get_head_position();
let entity = self.get_entity();
let pos = entity.pos.load();
BlockPos::floored(head_pos.x, pos.y + 0.2, head_pos.z)
}
#[must_use]
pub fn can_dig(&self) -> bool {
let entity = self.get_entity();
!self.is_panicking()
&& !self.is_baby()
&& !self.mob_entity.living_entity.is_in_water()
&& entity.on_ground.load(Ordering::Relaxed)
&& self.can_dig_at(self.get_head_block().down())
}
fn can_dig_at(&self, pos: BlockPos) -> bool {
let entity = self.get_entity();
let world = entity.world.load();
let block_state = world.get_block_state(&pos);
let block = pumpkin_data::Block::from_state_id(block_state.id);
if !block.has_tag(&tag::Block::MINECRAFT_SNIFFER_DIGGABLE_BLOCK) {
return false;
}
let explored = self
.explored_positions
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
!explored.contains(&pos)
}
fn store_explored_position(&self, pos: BlockPos) {
let mut explored = self
.explored_positions
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if explored.len() >= 20 {
explored.pop();
}
explored.insert(0, pos);
}
pub async fn drop_seed(&self) {
let entity = self.get_entity();
let world = entity.world.load();
let current_tick = world.level_time.lock().await.world_age as i32;
if self.drop_seed_at_tick.load(Ordering::Relaxed) == current_tick {
let head_pos = self.get_head_position();
let seed_item = if rand::random::<bool>() {
&Item::TORCHFLOWER_SEEDS
} else {
&Item::PITCHER_POD
};
let item_stack = ItemStack::new(1, seed_item);
let item_entity = Entity::new(world.clone(), head_pos, &EntityType::ITEM);
let item_arc = Arc::new(ItemEntity::new(item_entity, item_stack));
world.spawn_entity(item_arc).await;
world.play_sound(
Sound::EntitySnifferDropSeed,
SoundCategory::Neutral,
&head_pos,
);
}
}
pub async fn spawn_child_from_breeding(&self, partner: &dyn EntityBase) {
let entity = self.get_entity();
let world = entity.world.load();
let pos = entity.pos.load();
let item_stack = ItemStack::new(1, &Item::SNIFFER_EGG);
let egg_entity = Entity::new(world.clone(), pos, &EntityType::ITEM);
let egg_arc = Arc::new(ItemEntity::new(egg_entity, item_stack));
world.spawn_entity(egg_arc).await;
world.play_sound(Sound::BlockSnifferEggPlop, SoundCategory::Neutral, &pos);
self.mob_entity.reset_love_ticks();
self.mob_entity
.breeding_cooldown
.store(6000, Ordering::Relaxed);
partner.reset_love();
partner.set_breeding_cooldown(6000);
}
}
impl AgeableMob for SnifferEntity {
fn get_ageable_data(&self) -> &AgeableData {
&self.ageable_data
}
fn get_baby_start_age(&self) -> i32 {
SNIFFER_BABY_START_AGE
}
}
impl NBTStorage for SnifferEntity {
fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
self.mob_entity.living_entity.write_nbt(nbt).await;
self.write_ageable_nbt(nbt);
self.write_animal_nbt(nbt);
nbt.put_int("State", self.get_state().id());
nbt.put_int(
"DropSeedAtTick",
self.drop_seed_at_tick.load(Ordering::Relaxed),
);
})
}
fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
self.mob_entity.living_entity.read_nbt_non_mut(nbt).await;
self.read_ageable_nbt(nbt);
self.read_animal_nbt(nbt);
if let Some(state_id) = nbt.get_int("State") {
self.state.store(state_id, Ordering::Relaxed);
}
if let Some(drop_tick) = nbt.get_int("DropSeedAtTick") {
self.drop_seed_at_tick.store(drop_tick, Ordering::Relaxed);
}
})
}
}
impl Animal for SnifferEntity {
fn is_food(&self, item_stack: &ItemStack) -> bool {
item_stack.item.has_tag(&tag::Item::MINECRAFT_SNIFFER_FOOD)
|| SNIFFER_FOOD.iter().any(|i| i.id == item_stack.item.id)
}
}
impl Mob for SnifferEntity {
fn get_mob_entity(&self) -> &MobEntity {
&self.mob_entity
}
fn mob_tick<'a>(&'a self, _caller: &'a Arc<dyn EntityBase>) -> EntityBaseFuture<'a, ()> {
Box::pin(async move {
self.ageable_ai_step();
let state = self.get_state();
match state {
SnifferState::Searching => {
let entity = self.get_entity();
let world = entity.world.load();
let ticks = world.level_time.lock().await.world_age;
if ticks % 20 == 0 {
world.play_sound(
Sound::EntitySnifferSearching,
SoundCategory::Neutral,
&entity.pos.load(),
);
}
}
SnifferState::Digging => {
self.drop_seed().await;
}
_ => {}
}
})
}
fn mob_init_data_tracker(&self) -> EntityBaseFuture<'_, ()> {
Box::pin(async move {
let entity = self.get_entity();
let is_baby = entity.age.load(Ordering::Relaxed) < 0;
if is_baby {
entity.send_meta_data(
&[Metadata::new(
TrackedData::BABY_ID,
MetaDataType::BOOLEAN,
true,
)],
None,
);
}
entity.send_meta_data(
&[
Metadata::new(
TrackedData::STATE,
MetaDataType::SNIFFER_STATE,
VarInt(self.get_state().id()),
),
Metadata::new(
TrackedData::DROP_SEED_AT_TICK,
MetaDataType::INT,
VarInt(self.drop_seed_at_tick.load(Ordering::Relaxed)),
),
],
None,
);
})
}
fn mob_interact<'a>(
&'a self,
player: &'a Arc<Player>,
item_stack: &'a mut ItemStack,
) -> EntityBaseFuture<'a, bool> {
Box::pin(async move {
self.animal_interact(player, item_stack, Sound::EntitySnifferEat)
.await
})
}
}

View File

@ -1,29 +1,43 @@
use std::sync::{
Arc, Weak,
atomic::{AtomicU8, Ordering},
atomic::{AtomicBool, AtomicU8, Ordering},
};
use crossbeam::atomic::AtomicCell;
use pumpkin_data::entity::EntityType;
use pumpkin_data::item::Item;
use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::meta_data_type::MetaDataType;
use pumpkin_data::tag::{self, Taggable};
use pumpkin_data::tracked_data::TrackedData;
use pumpkin_nbt::compound::NbtCompound;
use pumpkin_protocol::codec::var_int::VarInt;
use pumpkin_protocol::java::client::play::Metadata;
use uuid::Uuid;
use crate::entity::{
Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture,
ageable::AgeableMob,
ai::goal::{
beg::BegGoal, breed::BreedGoal, escape_danger::EscapeDangerGoal,
active_target::ActiveTargetGoal, avoid_entity::AvoidEntityGoal, beg::BegGoal,
breed::BreedGoal, escape_danger::EscapeDangerGoal, follow_owner::FollowOwnerGoal,
follow_parent::FollowParentGoal, look_around::RandomLookAroundGoal,
look_at_entity::LookAtEntityGoal, swim::SwimGoal, wander_around::WanderAroundGoal,
look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal,
owner_hurt_by_target::OwnerHurtByTargetGoal, owner_hurt_target::OwnerHurtTargetGoal,
revenge::RevengeGoal, swim::SwimGoal, wander_around::WanderAroundGoal,
},
mob::{Mob, MobEntity},
passive::animal::Animal,
};
pub struct WolfEntity {
pub mob_entity: MobEntity,
pub variant: AtomicU8,
pub collar_color: AtomicU8,
pub is_tame: AtomicBool,
pub is_sitting: AtomicBool,
pub owner: AtomicCell<Option<Uuid>>,
pub ageable_data: crate::entity::ageable::AgeableData,
}
impl WolfEntity {
@ -32,6 +46,11 @@ impl WolfEntity {
let wolf = Self {
mob_entity,
variant: AtomicU8::new(3), // Default to pale
collar_color: AtomicU8::new(14), // Default to red
is_tame: AtomicBool::new(false),
is_sitting: AtomicBool::new(false),
owner: AtomicCell::new(None),
ageable_data: crate::entity::ageable::AgeableData::default(),
};
let mob_arc = Arc::new(wolf);
let mob_weak: Weak<dyn Mob> = {
@ -46,29 +65,108 @@ impl WolfEntity {
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
// Goal selector (matching Vanilla registerGoals):
// 1: SwimGoal (FloatGoal)
goal_selector.add_goal(1, Box::new(SwimGoal::default()));
// goal_selector.add_goal(2, SitGoal::new(mob_arc.clone()));
goal_selector.add_goal(4, EscapeDangerGoal::new(1.5));
goal_selector.add_goal(5, BreedGoal::new(1.0));
// goal_selector.add_goal(6, FollowOwnerGoal::new(1.0, 10.0, 2.0, false));
// 1: EscapeDangerGoal (TamableAnimalPanicGoal)
goal_selector.add_goal(1, EscapeDangerGoal::new(1.5));
// 3: Avoid Llama
goal_selector.add_goal(
3,
Box::new(AvoidEntityGoal::new(&EntityType::LLAMA, 24.0, 1.5, 1.5)),
);
// 5: MeleeAttackGoal
goal_selector.add_goal(5, Box::new(MeleeAttackGoal::new(1.0, true)));
// 6: FollowOwnerGoal
goal_selector.add_goal(6, FollowOwnerGoal::new(1.0, 10.0, 2.0));
// 7: BreedGoal
goal_selector.add_goal(7, BreedGoal::new(1.0));
// 8: FollowParentGoal & WanderAroundGoal
goal_selector.add_goal(8, Box::new(FollowParentGoal::new(1.1)));
goal_selector.add_goal(9, BegGoal::new(8.0, &[&Item::BONE]));
goal_selector.add_goal(8, Box::new(WanderAroundGoal::new(1.0)));
// 9: BegGoal
goal_selector.add_goal(9, BegGoal::new(8.0));
// 10: LookAtPlayer & RandomLookAround
goal_selector.add_goal(
10,
LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 8.0),
);
goal_selector.add_goal(10, Box::new(RandomLookAroundGoal::default()));
goal_selector.add_goal(12, Box::new(WanderAroundGoal::new(1.0)));
};
{
let mut target_selector = mob_arc
.mob_entity
.target_selector
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
// Target selector (matching Vanilla registerGoals):
// 1: OwnerHurtByTargetGoal
target_selector.add_goal(1, OwnerHurtByTargetGoal::new());
// 2: OwnerHurtTargetGoal
target_selector.add_goal(2, OwnerHurtTargetGoal::new());
// 3: HurtByTargetGoal (RevengeGoal)
target_selector.add_goal(3, Box::new(RevengeGoal::new(true)));
// 4: NearestAttackableTarget (Player)
target_selector.add_goal(
4,
ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::PLAYER, true),
);
// 5: NonTameRandomTarget (Sheep, Rabbit, Fox)
target_selector.add_goal(
5,
ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::SHEEP, false),
);
target_selector.add_goal(
5,
ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::RABBIT, false),
);
target_selector.add_goal(
5,
ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::FOX, false),
);
// 7: NearestAttackableTarget (Skeleton)
target_selector.add_goal(
7,
ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::SKELETON, false),
);
};
mob_arc
}
pub fn get_tame_flags(&self) -> u8 {
let mut flags = 0u8;
if self.is_sitting.load(Ordering::Relaxed) {
flags |= 0x01;
}
if self.is_tame.load(Ordering::Relaxed) {
flags |= 0x04;
}
flags
}
}
impl AgeableMob for WolfEntity {
fn get_ageable_data(&self) -> &crate::entity::ageable::AgeableData {
&self.ageable_data
}
}
impl Animal for WolfEntity {
fn is_food(&self, item_stack: &ItemStack) -> bool {
let item = item_stack.get_item();
item.has_tag(&tag::Item::MINECRAFT_WOLF_FOOD) || item == &Item::BONE
}
}
impl NBTStorage for WolfEntity {
fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async {
self.mob_entity.living_entity.write_nbt(nbt).await;
self.write_ageable_nbt(nbt);
self.write_animal_nbt(nbt);
let variant_str = match self.variant.load(Ordering::Relaxed) {
0 => "minecraft:ashen",
1 => "minecraft:black",
@ -81,12 +179,23 @@ impl NBTStorage for WolfEntity {
_ => "minecraft:pale",
};
nbt.put_string("variant", variant_str.to_string());
nbt.put_byte(
"CollarColor",
self.collar_color.load(Ordering::Relaxed) as i8,
);
nbt.put_bool("IsTame", self.is_tame.load(Ordering::Relaxed));
nbt.put_bool("Sitting", self.is_sitting.load(Ordering::Relaxed));
if let Some(owner) = self.owner.load() {
nbt.put_uuid("Owner", owner);
}
})
}
fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async {
self.mob_entity.living_entity.read_nbt_non_mut(nbt).await;
self.read_ageable_nbt(nbt);
self.read_animal_nbt(nbt);
if let Some(variant_str) = nbt.get_string("variant") {
let variant = match variant_str
.strip_prefix("minecraft:")
@ -104,6 +213,20 @@ impl NBTStorage for WolfEntity {
};
self.variant.store(variant, Ordering::Relaxed);
}
if let Some(collar) = nbt.get_byte("CollarColor") {
self.collar_color.store(collar as u8, Ordering::Relaxed);
} else if let Some(collar_int) = nbt.get_int("CollarColor") {
self.collar_color.store(collar_int as u8, Ordering::Relaxed);
}
if let Some(sitting) = nbt.get_bool("Sitting") {
self.is_sitting.store(sitting, Ordering::Relaxed);
}
if let Some(owner) = nbt.get_uuid("Owner") {
self.owner.store(Some(owner));
self.is_tame.store(true, Ordering::Relaxed);
} else if let Some(is_tame) = nbt.get_bool("IsTame") {
self.is_tame.store(is_tame, Ordering::Relaxed);
}
})
}
}
@ -113,6 +236,14 @@ impl Mob for WolfEntity {
&self.mob_entity
}
fn get_owner_uuid(&self) -> Option<Uuid> {
self.owner.load()
}
fn is_sitting(&self) -> bool {
self.is_sitting.load(Ordering::Relaxed)
}
fn mob_set_variant_name(&self, name: &str) {
let variant = match name.strip_prefix("minecraft:").unwrap_or(name) {
"ashen" => 0,
@ -142,6 +273,22 @@ impl Mob for WolfEntity {
None,
);
}
entity.send_meta_data(
&[Metadata::new(
TrackedData::TAMEABLE_FLAGS,
MetaDataType::BYTE,
self.get_tame_flags(),
)],
None,
);
entity.send_meta_data(
&[Metadata::new(
TrackedData::COLLAR_COLOR,
MetaDataType::INT,
VarInt(self.collar_color.load(Ordering::Relaxed) as i32),
)],
None,
);
entity.send_meta_data(
&[Metadata::new(
TrackedData::WOLF_VARIANT_ID,
@ -150,6 +297,14 @@ impl Mob for WolfEntity {
)],
None,
);
entity.send_meta_data(
&[Metadata::new(
TrackedData::OWNER_UUID,
MetaDataType::OPTIONAL_UUID,
self.owner.load(),
)],
None,
);
})
}
}

View File

@ -0,0 +1,115 @@
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use pumpkin_data::damage::DamageType;
use pumpkin_data::sound::Sound;
use crate::{
entity::{Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture},
server::Server,
};
pub struct EvokerFangsEntity {
pub entity: Entity,
pub warmup_ticks: AtomicU32,
pub life_ticks: AtomicU32,
pub owner_id: Option<i32>,
pub has_bitten: AtomicBool,
}
impl EvokerFangsEntity {
#[must_use]
pub fn new(entity: Entity, warmup_ticks: u32, yaw: f32, owner_id: Option<i32>) -> Self {
entity.set_rotation(yaw, 0.0);
Self {
entity,
warmup_ticks: AtomicU32::new(warmup_ticks),
life_ticks: AtomicU32::new(0),
owner_id,
has_bitten: AtomicBool::new(false),
}
}
}
impl NBTStorage for EvokerFangsEntity {
fn write_nbt<'a>(
&'a self,
nbt: &'a mut pumpkin_nbt::compound::NbtCompound,
) -> NbtFuture<'a, ()> {
Box::pin(async move {
nbt.put_int("Warmup", self.warmup_ticks.load(Ordering::Relaxed) as i32);
})
}
fn read_nbt_non_mut<'a>(
&'a self,
nbt: &'a pumpkin_nbt::compound::NbtCompound,
) -> NbtFuture<'a, ()> {
Box::pin(async move {
if let Some(warmup) = nbt.get_int("Warmup") {
self.warmup_ticks.store(warmup as u32, Ordering::Relaxed);
}
})
}
}
impl EntityBase for EvokerFangsEntity {
fn tick<'a>(
&'a self,
_caller: &'a Arc<dyn EntityBase>,
_server: &'a Server,
) -> EntityBaseFuture<'a, ()> {
Box::pin(async move {
let entity = &self.entity;
let world = entity.world.load();
let warmup = self.warmup_ticks.load(Ordering::Relaxed);
let life = self.life_ticks.fetch_add(1, Ordering::Relaxed) + 1;
if life >= warmup {
if !self.has_bitten.swap(true, Ordering::SeqCst) {
entity.play_sound(Sound::EntityEvokerFangsAttack);
let bb = entity.bounding_box.load().expand(0.2, 0.2, 0.2);
let candidates = world.get_entities_at_box(&bb);
for cand in candidates {
let cand_ent = cand.get_entity();
if Some(cand_ent.entity_id) == self.owner_id {
continue;
}
if cand_ent.entity_id != entity.entity_id {
let cand_clone = cand.clone();
tokio::spawn(async move {
let _ = cand_clone
.damage(cand_clone.as_ref(), 6.0, DamageType::MAGIC)
.await;
});
}
}
}
if life > warmup + 20 {
entity.remove().await;
}
}
})
}
fn get_entity(&self) -> &Entity {
&self.entity
}
fn get_living_entity(&self) -> Option<&crate::entity::living::LivingEntity> {
None
}
fn as_nbt_storage(&self) -> &dyn NBTStorage {
self
}
fn cast_any(&self) -> &dyn std::any::Any {
self
}
}

View File

@ -1,58 +1,243 @@
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use tokio::sync::RwLock;
use pumpkin_data::item::Item;
use pumpkin_data::item_stack::ItemStack;
use pumpkin_data::meta_data_type::MetaDataType;
use pumpkin_data::tracked_data::TrackedData;
use pumpkin_nbt::compound::NbtCompound;
use pumpkin_protocol::codec::item_stack_seralizer::ItemStackSerializer;
use pumpkin_protocol::java::client::play::Metadata;
use pumpkin_util::math::atomic_f32::AtomicF32;
use pumpkin_util::math::vector3::Vector3;
use crate::{
entity::{
Entity, EntityBase, EntityBaseFuture, NBTStorage,
Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture,
projectile::{ProjectileHit, ThrownItemEntity},
projectile_deflection::ProjectileDeflectionType,
},
server::Server,
};
const EXPLOSION_POWER: f32 = 1.0;
const GRAVITY: f64 = 0.0;
pub const MIN_CAMERA_DISTANCE_SQUARED: f32 = 12.25;
pub const INITIAL_ACCELERATION_POWER: f64 = 0.1;
pub const DEFLECTION_SCALE: f64 = 0.5;
pub const DEFAULT_EXPLOSION_POWER: f32 = 1.0;
pub const AIR_INERTIA: f64 = 0.95;
pub const WATER_INERTIA: f64 = 0.8;
pub struct FireballEntity {
pub thrown: ThrownItemEntity,
pub explosion_power: f32,
pub item_stack: RwLock<ItemStack>,
pub acceleration_power: AtomicU64,
pub explosion_power: AtomicF32,
}
impl FireballEntity {
#[must_use]
pub const fn new(entity: Entity) -> Self {
pub fn new(entity: Entity) -> Self {
let thrown = ThrownItemEntity {
entity,
owner_id: None,
collides_with_projectiles: false,
has_hit: AtomicBool::new(false),
gravity: GRAVITY,
gravity: 0.0,
};
Self {
thrown,
explosion_power: EXPLOSION_POWER,
item_stack: RwLock::new(Self::get_default_item()),
acceleration_power: AtomicU64::new(INITIAL_ACCELERATION_POWER.to_bits()),
explosion_power: AtomicF32::new(DEFAULT_EXPLOSION_POWER),
}
}
#[must_use]
pub fn new_shot(entity: Entity, shooter: &Entity) -> Self {
let thrown = ThrownItemEntity::new(entity, shooter, GRAVITY);
pub fn new_shot(entity: Entity, shooter: &Entity, direction: Vector3<f64>) -> Self {
let thrown = ThrownItemEntity::new(entity, shooter, 0.0);
let accel = INITIAL_ACCELERATION_POWER;
let vel = direction.normalize().multiply(accel, accel, accel);
thrown.entity.velocity.store(vel);
Self {
thrown,
explosion_power: EXPLOSION_POWER,
item_stack: RwLock::new(Self::get_default_item()),
acceleration_power: AtomicU64::new(accel.to_bits()),
explosion_power: AtomicF32::new(DEFAULT_EXPLOSION_POWER),
}
}
#[must_use]
pub fn new_directional(
entity: Entity,
direction: Vector3<f64>,
acceleration_power: f64,
) -> Self {
let thrown = ThrownItemEntity {
entity,
owner_id: None,
collides_with_projectiles: false,
has_hit: AtomicBool::new(false),
gravity: 0.0,
};
let vel = direction.normalize().multiply(
acceleration_power,
acceleration_power,
acceleration_power,
);
thrown.entity.velocity.store(vel);
Self {
thrown,
item_stack: RwLock::new(Self::get_default_item()),
acceleration_power: AtomicU64::new(acceleration_power.to_bits()),
explosion_power: AtomicF32::new(DEFAULT_EXPLOSION_POWER),
}
}
#[must_use]
pub fn get_default_item() -> ItemStack {
ItemStack::new(1, &Item::FIRE_CHARGE)
}
pub async fn get_item(&self) -> ItemStack {
self.item_stack.read().await.clone()
}
pub async fn set_item(&self, source: ItemStack) {
let new_item = if source.item_count == 0 {
Self::get_default_item()
} else {
let mut item = source;
item.item_count = 1;
item
};
*self.item_stack.write().await = new_item.clone();
self.get_entity().send_meta_data(
&[Metadata::new(
TrackedData::ITEM_STACK,
MetaDataType::ITEM_STACK,
&ItemStackSerializer::from(new_item),
)],
None,
);
}
pub fn get_acceleration_power(&self) -> f64 {
f64::from_bits(self.acceleration_power.load(Ordering::Relaxed))
}
pub fn set_acceleration_power(&self, power: f64) {
self.acceleration_power
.store(power.to_bits(), Ordering::Relaxed);
}
pub fn get_explosion_power(&self) -> f32 {
self.explosion_power.load(Ordering::Relaxed)
}
pub fn set_explosion_power(&self, power: f32) {
self.explosion_power.store(power, Ordering::Relaxed);
}
pub fn on_deflection(&self, _deflection: &ProjectileDeflectionType, by_attack: bool) {
if by_attack {
self.set_acceleration_power(INITIAL_ACCELERATION_POWER);
} else {
let current = self.get_acceleration_power();
self.set_acceleration_power(current * DEFLECTION_SCALE);
}
}
pub fn should_render_at_sqr_distance(&self, distance_sqr: f64) -> bool {
if self.get_entity().age.load(Ordering::Relaxed) < 2
&& distance_sqr < f64::from(MIN_CAMERA_DISTANCE_SQUARED)
{
false
} else {
let bb_size = self
.get_entity()
.bounding_box
.load()
.get_average_side_length()
* 4.0;
let size = if bb_size.is_nan() { 4.0 } else { bb_size } * 64.0;
distance_sqr < size * size
}
}
}
impl NBTStorage for FireballEntity {}
impl NBTStorage for FireballEntity {
fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
nbt.put_double("acceleration_power", self.get_acceleration_power());
nbt.put_float("ExplosionPower", self.get_explosion_power());
})
}
fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
Box::pin(async move {
if let Some(accel) = nbt
.get_double("acceleration_power")
.or_else(|| nbt.get_double("power"))
{
self.set_acceleration_power(accel);
}
if let Some(exp) = nbt.get_float("ExplosionPower") {
self.set_explosion_power(exp);
}
})
}
}
impl EntityBase for FireballEntity {
fn init_data_tracker(&self) -> EntityBaseFuture<'_, ()> {
Box::pin(async move {
let entity = self.get_entity();
let stack = self.item_stack.read().await;
entity.send_meta_data(
&[Metadata::new(
TrackedData::ITEM_STACK,
MetaDataType::ITEM_STACK,
&ItemStackSerializer::from(stack.clone()),
)],
None,
);
})
}
fn tick<'a>(
&'a self,
caller: &'a Arc<dyn EntityBase>,
server: &'a Server,
) -> EntityBaseFuture<'a, ()> {
Box::pin(async move { self.thrown.process_tick(caller, server).await })
Box::pin(async move {
let entity = self.get_entity();
let mut velocity = entity.velocity.load();
let inertia = if entity.touching_water.load(Ordering::Relaxed) {
WATER_INERTIA
} else {
AIR_INERTIA
};
let accel = self.get_acceleration_power();
let speed = velocity.length();
if speed > 1e-6 {
let norm = velocity.normalize();
velocity = norm
.multiply(accel, accel, accel)
.add(&velocity)
.multiply(inertia, inertia, inertia);
entity.velocity.store(velocity);
}
self.thrown.process_tick(caller, server).await;
})
}
fn get_entity(&self) -> &Entity {
@ -75,12 +260,10 @@ impl EntityBase for FireballEntity {
Box::pin(async move {
let world = self.get_entity().world.load();
// Handle entity/block hit
if let ProjectileHit::Entity { ref entity, .. } = hit {
let entity_clone = entity.clone();
tokio::spawn(async move {
// Fireball does 6.0 damage in vanilla
entity_clone.get_entity().set_on_fire_for(5.0);
let _ = entity_clone
.damage(
@ -93,8 +276,7 @@ impl EntityBase for FireballEntity {
}
let hit_pos = hit.hit_pos();
// Explosion sets fire if mob griefing is enabled (assuming true for now)
world.explode(hit_pos, self.explosion_power).await;
world.explode(hit_pos, self.get_explosion_power()).await;
})
}
}

View File

@ -12,6 +12,7 @@ use std::{
pub mod arrow;
pub mod egg;
pub mod ender_pearl;
pub mod evoker_fangs;
pub mod eye_of_ender;
pub mod fireball;
pub mod firework_rocket;

View File

@ -8,13 +8,19 @@ use uuid::Uuid;
use crate::entity::boss::ender_dragon::EnderDragonEntity;
use crate::entity::boss::wither::WitherEntity;
use crate::entity::decoration::{
armor_stand::ArmorStandEntity, end_crystal::EndCrystalEntity, item_frame::ItemFrameEntity,
armor_stand::ArmorStandEntity,
display::{BlockDisplayEntity, ItemDisplayEntity, TextDisplayEntity},
end_crystal::EndCrystalEntity,
item_frame::ItemFrameEntity,
painting::PaintingEntity,
};
use crate::entity::experience_orb::ExperienceOrbEntity;
use crate::entity::falling::FallingEntity;
use crate::entity::interaction::InteractionEntity;
use crate::entity::item::ItemEntity;
use crate::entity::lightning::LightningBoltEntity;
use crate::entity::living::LivingEntity;
use crate::entity::marker::MarkerEntity;
use crate::entity::mob::bat::{self, BatEntity};
use crate::entity::mob::blaze::BlazeEntity;
use crate::entity::mob::breeze::BreezeEntity;
@ -60,6 +66,7 @@ use crate::entity::passive::camel::CamelEntity;
use crate::entity::passive::cat::CatEntity;
use crate::entity::passive::chicken::ChickenEntity;
use crate::entity::passive::cod::CodEntity;
use crate::entity::passive::copper_golem::CopperGolemEntity;
use crate::entity::passive::cow::CowEntity;
use crate::entity::passive::dolphin::DolphinEntity;
use crate::entity::passive::donkey::DonkeyEntity;
@ -67,11 +74,13 @@ use crate::entity::passive::fox::FoxEntity;
use crate::entity::passive::frog::FrogEntity;
use crate::entity::passive::glow_squid::GlowSquidEntity;
use crate::entity::passive::goat::GoatEntity;
use crate::entity::passive::happy_ghast::HappyGhastEntity;
use crate::entity::passive::horse::HorseEntity;
use crate::entity::passive::iron_golem::IronGolemEntity;
use crate::entity::passive::llama::LlamaEntity;
use crate::entity::passive::mooshroom::MooshroomEntity;
use crate::entity::passive::mule::MuleEntity;
use crate::entity::passive::nautilus::NautilusEntity;
use crate::entity::passive::ocelot::OcelotEntity;
use crate::entity::passive::panda::PandaEntity;
use crate::entity::passive::parrot::ParrotEntity;
@ -193,8 +202,10 @@ pub fn from_type(
id if id == EntityType::AXOLOTL.id => AxolotlEntity::new(entity),
id if id == EntityType::BEE.id => BeeEntity::new(entity),
id if id == EntityType::CAMEL.id => CamelEntity::new(entity),
id if id == EntityType::COPPER_GOLEM.id => CopperGolemEntity::new(entity),
id if id == EntityType::FROG.id => FrogEntity::new(entity),
id if id == EntityType::GOAT.id => GoatEntity::new(entity),
id if id == EntityType::HAPPY_GHAST.id => HappyGhastEntity::new(entity),
id if id == EntityType::MOOSHROOM.id => MooshroomEntity::new(entity),
id if id == EntityType::OCELOT.id => OcelotEntity::new(entity),
id if id == EntityType::PANDA.id => PandaEntity::new(entity),
@ -209,6 +220,7 @@ pub fn from_type(
id if id == EntityType::TROPICAL_FISH.id => TropicalFishEntity::new(entity),
id if id == EntityType::TADPOLE.id => TadpoleEntity::new(entity),
id if id == EntityType::DOLPHIN.id => DolphinEntity::new(entity),
id if id == EntityType::NAUTILUS.id => NautilusEntity::new(entity),
id if id == EntityType::SNOW_GOLEM.id => SnowGolemEntity::new(entity),
id if id == EntityType::IRON_GOLEM.id => IronGolemEntity::new(entity),
@ -220,10 +232,16 @@ pub fn from_type(
crate::entity::area_effect_cloud::AreaEffectCloudEntity::new(entity)
}
id if id == EntityType::ARMOR_STAND.id => Arc::new(ArmorStandEntity::new(entity)),
id if id == EntityType::BLOCK_DISPLAY.id => BlockDisplayEntity::new(entity),
id if id == EntityType::ITEM_DISPLAY.id => ItemDisplayEntity::new(entity),
id if id == EntityType::TEXT_DISPLAY.id => TextDisplayEntity::new(entity),
id if id == EntityType::PAINTING.id => Arc::new(PaintingEntity::new(entity)),
id if id == EntityType::ITEM_FRAME.id || id == EntityType::GLOW_ITEM_FRAME.id => {
Arc::new(ItemFrameEntity::new(entity))
}
id if id == EntityType::EVOKER_FANGS.id => Arc::new(
crate::entity::projectile::evoker_fangs::EvokerFangsEntity::new(entity, 0, 0.0, None),
),
id if id == EntityType::END_CRYSTAL.id => Arc::new(EndCrystalEntity::new(entity)),
id if id == EntityType::ENDER_PEARL.id => Arc::new(EnderPearlEntity::new(entity)),
id if id == EntityType::SNOWBALL.id => Arc::new(SnowballEntity::new(entity)),
@ -277,6 +295,9 @@ pub fn from_type(
};
Arc::new(WindChargeEntity::new_breeze(thrown))
}
id if id == EntityType::LIGHTNING_BOLT.id => Arc::new(LightningBoltEntity::new(entity)),
id if id == EntityType::MARKER.id => MarkerEntity::new(entity),
id if id == EntityType::INTERACTION.id => InteractionEntity::new(entity),
id if id == EntityType::FIREWORK_ROCKET.id => Arc::new(FireworkRocketEntity::new(entity)),
id if id == EntityType::SPLASH_POTION.id => Arc::new(SplashPotionEntity::new(entity)),
id if id == EntityType::LINGERING_POTION.id => Arc::new(LingeringPotionEntity::new(entity)),

View File

@ -5011,15 +5011,40 @@ impl World {
self.spawn_entity(item_entity).await;
}
pub async fn strike_lightning(self: &Arc<Self>, pos: Vector3<f64>, _effect_only: bool) {
pub async fn strike_lightning(self: &Arc<Self>, pos: Vector3<f64>, effect_only: bool) {
use pumpkin_data::entity::EntityType;
use uuid::Uuid;
let server_ref = self.server.upgrade();
if let Some(server_ref) = server_ref {
let mut event =
crate::plugin::api::events::world::lightning_strike::LightningStrikeEvent::new(
pos,
effect_only,
);
server_ref
.plugin_manager
.fire(&server_ref, &mut event)
.await;
if event.cancelled {
return;
}
}
let lightning = crate::entity::r#type::from_type(
&EntityType::LIGHTNING_BOLT,
pos,
self,
Uuid::new_v4(),
);
if let Some(bolt) = lightning
.cast_any()
.downcast_ref::<crate::entity::lightning::LightningBoltEntity>()
{
bolt.set_visual_only(effect_only);
}
self.spawn_entity(lightning).await;
}