feat: add some more entity logic
This commit is contained in:
parent
24d9dbdc6b
commit
5a5bc19bbb
@ -39,6 +39,8 @@ pub enum ActorEventType {
|
|||||||
StartOfferFlower = 19,
|
StartOfferFlower = 19,
|
||||||
StopOfferFlower = 20,
|
StopOfferFlower = 20,
|
||||||
LoveHearts = 21,
|
LoveHearts = 21,
|
||||||
|
TrustingFailed = 40,
|
||||||
|
TrustingSucceeded = 41,
|
||||||
VillagerAngry = 22,
|
VillagerAngry = 22,
|
||||||
VillagerHappy = 23,
|
VillagerHappy = 23,
|
||||||
WitchHatMagic = 24,
|
WitchHatMagic = 24,
|
||||||
|
|||||||
@ -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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::io::{Cursor, Read};
|
use std::io::{Cursor, Read};
|
||||||
|
|||||||
@ -1,12 +1,19 @@
|
|||||||
use std::pin::Pin;
|
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_nbt::compound::NbtCompound;
|
||||||
use pumpkin_util::math::position::BlockPos;
|
use pumpkin_util::math::position::BlockPos;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::world::World;
|
||||||
|
|
||||||
use super::BlockEntity;
|
use super::BlockEntity;
|
||||||
|
|
||||||
pub struct CreakingHeartBlockEntity {
|
pub struct CreakingHeartBlockEntity {
|
||||||
pub position: BlockPos,
|
pub position: BlockPos,
|
||||||
|
pub creaking_uuid: AtomicCell<Option<Uuid>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BlockEntity for CreakingHeartBlockEntity {
|
impl BlockEntity for CreakingHeartBlockEntity {
|
||||||
@ -18,18 +25,28 @@ impl BlockEntity for CreakingHeartBlockEntity {
|
|||||||
self.position
|
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
|
where
|
||||||
Self: Sized,
|
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>(
|
fn write_nbt<'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
_nbt: &'a mut NbtCompound,
|
nbt: &'a mut NbtCompound,
|
||||||
) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
|
) -> 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 {
|
fn as_any(&self) -> &dyn std::any::Any {
|
||||||
@ -39,8 +56,30 @@ impl BlockEntity for CreakingHeartBlockEntity {
|
|||||||
|
|
||||||
impl CreakingHeartBlockEntity {
|
impl CreakingHeartBlockEntity {
|
||||||
pub const ID: &'static str = "minecraft:creaking_heart";
|
pub const ID: &'static str = "minecraft:creaking_heart";
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn new(position: BlockPos) -> Self {
|
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(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,41 +4,40 @@ use crate::entity::mob::Mob;
|
|||||||
use crate::entity::player::Player;
|
use crate::entity::player::Player;
|
||||||
use pumpkin_data::item::Item;
|
use pumpkin_data::item::Item;
|
||||||
use pumpkin_data::meta_data_type::MetaDataType;
|
use pumpkin_data::meta_data_type::MetaDataType;
|
||||||
|
use pumpkin_data::tag::{self, Taggable};
|
||||||
use pumpkin_data::tracked_data::TrackedData;
|
use pumpkin_data::tracked_data::TrackedData;
|
||||||
use pumpkin_protocol::java::client::play::Metadata;
|
use pumpkin_protocol::java::client::play::Metadata;
|
||||||
use rand::RngExt;
|
use rand::RngExt;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub struct BegGoal {
|
pub struct BegGoal {
|
||||||
beg_distance_sq: f64,
|
look_distance_sq: f64,
|
||||||
attractive_items: &'static [&'static Item],
|
look_time: i32,
|
||||||
timer: i32,
|
player: Option<Arc<Player>>,
|
||||||
target: Option<Arc<Player>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BegGoal {
|
impl BegGoal {
|
||||||
#[must_use]
|
#[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 {
|
Box::new(Self {
|
||||||
beg_distance_sq: (beg_distance * beg_distance) as f64,
|
look_distance_sq: f64::from(look_distance) * f64::from(look_distance),
|
||||||
attractive_items,
|
look_time: 0,
|
||||||
timer: 0,
|
player: None,
|
||||||
target: None,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_attractive(&self, item: &Item) -> bool {
|
fn is_interesting_item(item: &Item) -> bool {
|
||||||
self.attractive_items.iter().any(|i| i.id == item.id)
|
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;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let off_stack = player.inventory().off_hand_item().await;
|
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 {
|
fn distance_sq(mob: &dyn Mob, player: &Player) -> f64 {
|
||||||
@ -47,12 +46,12 @@ impl BegGoal {
|
|||||||
mob_pos.squared_distance_to_vec(&player_pos)
|
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(
|
mob.get_mob_entity().living_entity.entity.send_meta_data(
|
||||||
&[Metadata::new(
|
&[Metadata::new(
|
||||||
TrackedData::INTERESTED_ID,
|
TrackedData::INTERESTED_ID,
|
||||||
MetaDataType::BOOLEAN,
|
MetaDataType::BOOLEAN,
|
||||||
begging,
|
value,
|
||||||
)],
|
)],
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
@ -61,28 +60,28 @@ impl BegGoal {
|
|||||||
|
|
||||||
impl Goal for BegGoal {
|
impl Goal for BegGoal {
|
||||||
fn can_start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> {
|
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 entity = &mob.get_mob_entity().living_entity.entity;
|
||||||
let world = entity.world.load_full();
|
let world = entity.world.load_full();
|
||||||
let pos = entity.pos.load();
|
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 {
|
let Some(player) = world.get_closest_player(pos, radius) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
if !self.is_player_holding_attractive(&player).await {
|
if !self.player_holding_interesting(&player).await {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.target = Some(player);
|
self.player = Some(player);
|
||||||
true
|
true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn should_continue<'a>(&'a self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> {
|
fn should_continue<'a>(&'a self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> {
|
||||||
Box::pin(async {
|
Box::pin(async move {
|
||||||
let Some(player) = &self.target else {
|
let Some(player) = &self.player else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -90,32 +89,32 @@ impl Goal for BegGoal {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if Self::distance_sq(mob, player) > self.beg_distance_sq {
|
if Self::distance_sq(mob, player) > self.look_distance_sq {
|
||||||
return false;
|
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, ()> {
|
fn start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
|
||||||
Box::pin(async {
|
Box::pin(async move {
|
||||||
Self::set_begging(mob, true);
|
Self::set_is_interested(mob, true);
|
||||||
let ticks = 40 + mob.get_random().random_range(0..40);
|
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, ()> {
|
fn stop<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
|
||||||
Box::pin(async {
|
Box::pin(async move {
|
||||||
Self::set_begging(mob, false);
|
Self::set_is_interested(mob, false);
|
||||||
self.target = None;
|
self.player = None;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tick<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
|
fn tick<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
|
||||||
Box::pin(async {
|
Box::pin(async move {
|
||||||
if let Some(player) = &self.target {
|
if let Some(player) = &self.player {
|
||||||
let player_pos = player.get_entity().get_eye_pos();
|
let player_pos = player.get_entity().get_eye_pos();
|
||||||
let mut look_control = mob
|
let mut look_control = mob
|
||||||
.get_mob_entity()
|
.get_mob_entity()
|
||||||
@ -130,7 +129,7 @@ impl Goal for BegGoal {
|
|||||||
mob.get_max_look_pitch_change(),
|
mob.get_max_look_pitch_change(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
self.timer -= 1;
|
self.look_time -= 1;
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
use super::{Controls, Goal, GoalFuture, to_goal_ticks};
|
use super::{Controls, Goal, GoalFuture, to_goal_ticks};
|
||||||
use crate::entity::EntityBase;
|
use crate::entity::EntityBase;
|
||||||
use crate::entity::ai::pathfinder::NavigatorGoal;
|
use crate::entity::ai::pathfinder::NavigatorGoal;
|
||||||
|
use crate::entity::ai::pathfinder::node::PathType;
|
||||||
use crate::entity::mob::Mob;
|
use crate::entity::mob::Mob;
|
||||||
use crate::entity::player::Player;
|
use crate::entity::player::Player;
|
||||||
use pumpkin_util::math::position::BlockPos;
|
use pumpkin_util::math::position::BlockPos;
|
||||||
@ -11,27 +12,42 @@ use std::sync::Arc;
|
|||||||
const TELEPORT_DISTANCE_SQ: f64 = 144.0;
|
const TELEPORT_DISTANCE_SQ: f64 = 144.0;
|
||||||
|
|
||||||
pub struct FollowOwnerGoal {
|
pub struct FollowOwnerGoal {
|
||||||
speed: f64,
|
speed_modifier: f64,
|
||||||
min_distance_sq: f64,
|
start_distance_sq: f64,
|
||||||
max_distance_sq: f64,
|
stop_distance_sq: f64,
|
||||||
update_countdown: i32,
|
time_to_recalc_path: i32,
|
||||||
owner: Option<Arc<Player>>,
|
owner: Option<Arc<Player>>,
|
||||||
|
old_water_cost: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FollowOwnerGoal {
|
impl FollowOwnerGoal {
|
||||||
#[must_use]
|
#[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 {
|
Box::new(Self {
|
||||||
speed,
|
speed_modifier,
|
||||||
min_distance_sq: (min_distance * min_distance) as f64,
|
start_distance_sq: f64::from(start_distance) * f64::from(start_distance),
|
||||||
max_distance_sq: (max_distance * max_distance) as f64,
|
stop_distance_sq: f64::from(stop_distance) * f64::from(stop_distance),
|
||||||
update_countdown: 0,
|
time_to_recalc_path: 0,
|
||||||
owner: None,
|
owner: None,
|
||||||
|
old_water_cost: 0.0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn can_follow(mob: &dyn Mob) -> bool {
|
async fn unable_to_move_to_owner(mob: &dyn Mob, owner: Option<&Player>) -> bool {
|
||||||
!mob.is_sitting()
|
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>> {
|
fn find_owner(mob: &dyn Mob) -> Option<Arc<Player>> {
|
||||||
@ -50,6 +66,11 @@ impl FollowOwnerGoal {
|
|||||||
mob_pos.squared_distance_to_vec(&owner_pos)
|
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) {
|
fn try_teleport_to_owner(mob: &dyn Mob, owner: &Player) {
|
||||||
let owner_pos = owner.living_entity.entity.pos.load();
|
let owner_pos = owner.living_entity.entity.pos.load();
|
||||||
let mob_entity = &mob.get_mob_entity().living_entity.entity;
|
let mob_entity = &mob.get_mob_entity().living_entity.entity;
|
||||||
@ -92,7 +113,7 @@ impl FollowOwnerGoal {
|
|||||||
mob_entity.teleport(
|
mob_entity.teleport(
|
||||||
Vector3::new(
|
Vector3::new(
|
||||||
target_x as f64 + 0.5,
|
target_x as f64 + 0.5,
|
||||||
target_y as f64,
|
f64::from(target_y),
|
||||||
target_z as f64 + 0.5,
|
target_z as f64 + 0.5,
|
||||||
),
|
),
|
||||||
None,
|
None,
|
||||||
@ -113,17 +134,17 @@ impl FollowOwnerGoal {
|
|||||||
|
|
||||||
impl Goal for FollowOwnerGoal {
|
impl Goal for FollowOwnerGoal {
|
||||||
fn can_start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> {
|
fn can_start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> {
|
||||||
Box::pin(async {
|
Box::pin(async move {
|
||||||
if !Self::can_follow(mob) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(owner) = Self::find_owner(mob) else {
|
let Some(owner) = Self::find_owner(mob) else {
|
||||||
return false;
|
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);
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,8 +154,20 @@ impl Goal for FollowOwnerGoal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn should_continue<'a>(&'a self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> {
|
fn should_continue<'a>(&'a self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> {
|
||||||
Box::pin(async {
|
Box::pin(async move {
|
||||||
if !Self::can_follow(mob) {
|
let is_idle = {
|
||||||
|
let navigator = mob
|
||||||
|
.get_mob_entity()
|
||||||
|
.navigator
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||||
|
navigator.is_idle()
|
||||||
|
};
|
||||||
|
if is_idle {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if Self::unable_to_move_to_owner(mob, self.owner.as_deref()).await {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,40 +175,46 @@ impl Goal for FollowOwnerGoal {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
if owner.is_spectator() || !owner.living_entity.entity.is_alive() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let dist_sq = Self::distance_to_owner_sq(mob, owner);
|
let dist_sq = Self::distance_to_owner_sq(mob, owner);
|
||||||
if dist_sq <= self.max_distance_sq {
|
dist_sq > self.stop_distance_sq
|
||||||
return false;
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
let navigator = mob
|
fn start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
self.time_to_recalc_path = 0;
|
||||||
|
let mut navigator = mob
|
||||||
.get_mob_entity()
|
.get_mob_entity()
|
||||||
.navigator
|
.navigator
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||||
!navigator.is_idle()
|
self.old_water_cost = navigator.get_pathfinding_malus(PathType::Water);
|
||||||
|
navigator.set_pathfinding_malus(PathType::Water, 0.0);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
|
fn stop<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
|
||||||
Box::pin(async {
|
Box::pin(async move {
|
||||||
self.update_countdown = 0;
|
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, ()> {
|
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 {
|
let Some(owner) = &self.owner else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let dist_sq = Self::distance_to_owner_sq(mob, owner);
|
let is_owner_far_away = Self::should_try_teleport_to_owner(mob, owner);
|
||||||
let should_teleport = dist_sq >= TELEPORT_DISTANCE_SQ;
|
|
||||||
|
|
||||||
if !should_teleport {
|
if !is_owner_far_away {
|
||||||
let mob_entity = mob.get_mob_entity();
|
let mob_entity = mob.get_mob_entity();
|
||||||
let owner_eye_pos = owner.living_entity.entity.get_eye_pos();
|
let owner_eye_pos = owner.living_entity.entity.get_eye_pos();
|
||||||
let mut look_control = mob_entity
|
let mut look_control = mob_entity
|
||||||
@ -189,14 +228,12 @@ impl Goal for FollowOwnerGoal {
|
|||||||
10.0,
|
10.0,
|
||||||
mob.get_max_look_pitch_change(),
|
mob.get_max_look_pitch_change(),
|
||||||
);
|
);
|
||||||
drop(look_control);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.update_countdown -= 1;
|
self.time_to_recalc_path -= 1;
|
||||||
if self.update_countdown <= 0 {
|
if self.time_to_recalc_path <= 0 {
|
||||||
self.update_countdown = to_goal_ticks(10);
|
self.time_to_recalc_path = to_goal_ticks(10);
|
||||||
|
if is_owner_far_away {
|
||||||
if should_teleport {
|
|
||||||
Self::try_teleport_to_owner(mob, owner);
|
Self::try_teleport_to_owner(mob, owner);
|
||||||
} else {
|
} else {
|
||||||
let mob_pos = mob.get_mob_entity().living_entity.entity.pos.load();
|
let mob_pos = mob.get_mob_entity().living_entity.entity.pos.load();
|
||||||
@ -206,24 +243,16 @@ impl Goal for FollowOwnerGoal {
|
|||||||
.navigator
|
.navigator
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
.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 {
|
fn should_run_every_tick(&self) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|||||||
@ -123,6 +123,14 @@ impl Navigator {
|
|||||||
self.path_type_overrides.insert(path_type, malus);
|
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) {
|
pub const fn set_mob_dimensions(&mut self, width: f32, height: f32) {
|
||||||
self.mob_width = width;
|
self.mob_width = width;
|
||||||
self.mob_height = height;
|
self.mob_height = height;
|
||||||
|
|||||||
796
crates/pumpkin/src/entity/decoration/display.rs
Normal file
796
crates/pumpkin/src/entity/decoration/display.rs
Normal 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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,13 +1,22 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
|
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
|
||||||
|
|
||||||
|
use crate::entity::player::Player;
|
||||||
use crate::entity::{
|
use crate::entity::{
|
||||||
Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture, living::LivingEntity,
|
Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture, living::LivingEntity,
|
||||||
};
|
};
|
||||||
use crossbeam::atomic::AtomicCell;
|
use crossbeam::atomic::AtomicCell;
|
||||||
use pumpkin_data::BlockDirection;
|
use pumpkin_data::BlockDirection;
|
||||||
use pumpkin_data::damage::DamageType;
|
use pumpkin_data::damage::DamageType;
|
||||||
|
use pumpkin_data::entity::EntityType;
|
||||||
|
use pumpkin_data::item::Item;
|
||||||
use pumpkin_data::item_stack::ItemStack;
|
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_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 pumpkin_util::math::vector3::Vector3;
|
||||||
use tokio::sync::Mutex;
|
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 {
|
pub fn get_facing(&self) -> BlockDirection {
|
||||||
BlockDirection::from_index(self.facing.load(Ordering::Relaxed))
|
BlockDirection::from_index(self.facing.load(Ordering::Relaxed))
|
||||||
.unwrap_or(Self::DEFAULT_FACING)
|
.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.
|
/// The comparator signal this frame produces.
|
||||||
///
|
///
|
||||||
/// Vanilla: `getItem().isEmpty() ? 0 : getRotation() % 8 + 1`.
|
/// Vanilla: `getItem().isEmpty() ? 0 : getRotation() % 8 + 1`.
|
||||||
@ -65,6 +230,41 @@ impl ItemFrameEntity {
|
|||||||
self.rotation.load(Ordering::Relaxed) % 8 + 1
|
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 {
|
impl NBTStorage for ItemFrameEntity {
|
||||||
@ -77,8 +277,8 @@ impl NBTStorage for ItemFrameEntity {
|
|||||||
let mut item_compound = NbtCompound::new();
|
let mut item_compound = NbtCompound::new();
|
||||||
item.write_item_stack(&mut item_compound);
|
item.write_item_stack(&mut item_compound);
|
||||||
nbt.put_compound("Item", 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("ItemRotation", self.rotation.load(Ordering::Relaxed) as i8);
|
||||||
nbt.put_byte("Facing", self.facing.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));
|
nbt.put_bool("Invisible", self.invisible.load(Ordering::Relaxed));
|
||||||
@ -124,19 +324,133 @@ impl EntityBase for ItemFrameEntity {
|
|||||||
None
|
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>(
|
fn damage_with_context<'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
_caller: &'a dyn EntityBase,
|
_caller: &'a dyn EntityBase,
|
||||||
_amount: f32,
|
_amount: f32,
|
||||||
_damage_type: DamageType,
|
damage_type: DamageType,
|
||||||
_position: Option<Vector3<f64>>,
|
_position: Option<Vector3<f64>>,
|
||||||
_source: Option<&'a dyn EntityBase>,
|
source: Option<&'a dyn EntityBase>,
|
||||||
_cause: Option<&'a dyn EntityBase>,
|
_cause: Option<&'a dyn EntityBase>,
|
||||||
) -> EntityBaseFuture<'a, bool> {
|
) -> EntityBaseFuture<'a, bool> {
|
||||||
Box::pin(async {
|
Box::pin(async move {
|
||||||
// TODO: vanilla pops the displayed item first and only removes the
|
let fixed = self.is_fixed();
|
||||||
// frame itself when hit while empty; both should drop their items.
|
let is_creative_player = source.is_some_and(|s| {
|
||||||
self.entity.remove().await;
|
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
|
true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
pub mod armor_stand;
|
pub mod armor_stand;
|
||||||
|
pub mod display;
|
||||||
pub mod end_crystal;
|
pub mod end_crystal;
|
||||||
pub mod item_frame;
|
pub mod item_frame;
|
||||||
pub mod leash_knot;
|
pub mod leash_knot;
|
||||||
|
|||||||
313
crates/pumpkin/src/entity/interaction.rs
Normal file
313
crates/pumpkin/src/entity/interaction.rs
Normal 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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
500
crates/pumpkin/src/entity/lightning.rs
Normal file
500
crates/pumpkin/src/entity/lightning.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2106,7 +2106,7 @@ impl NBTStorage for LivingEntity {
|
|||||||
};
|
};
|
||||||
// Persist current absorption amount
|
// Persist current absorption amount
|
||||||
nbt.put("AbsorptionAmount", NbtTag::Float(self.absorption.load()));
|
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;
|
let effects = self.active_effects.lock().await;
|
||||||
if !effects.is_empty() {
|
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
|
// 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.
|
// 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) {
|
if self.dead.load(Relaxed) {
|
||||||
self.fall_distance.store(0.0);
|
self.fall_distance.store(0.0);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
125
crates/pumpkin/src/entity/marker.rs
Normal file
125
crates/pumpkin/src/entity/marker.rs
Normal 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 {})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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::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::{
|
use crate::entity::{
|
||||||
Entity, NBTStorage,
|
Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture,
|
||||||
ai::goal::{
|
ai::goal::{
|
||||||
active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal,
|
active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal,
|
||||||
look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, swim::SwimGoal,
|
look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, swim::SwimGoal,
|
||||||
wander_around::WanderAroundGoal,
|
wander_around::WanderAroundGoal,
|
||||||
},
|
},
|
||||||
mob::{Mob, MobEntity},
|
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 struct CreakingEntity {
|
||||||
pub mob_entity: MobEntity,
|
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 {
|
impl CreakingEntity {
|
||||||
pub fn new(entity: Entity) -> Arc<Self> {
|
pub fn new(entity: Entity) -> Arc<Self> {
|
||||||
let mob_entity = MobEntity::new(entity);
|
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_arc = Arc::new(creaking);
|
||||||
let mob_weak: Weak<dyn Mob> = {
|
let mob_weak: Weak<dyn Mob> = {
|
||||||
let mob_arc: Arc<dyn Mob> = mob_arc.clone();
|
let mob_arc: Arc<dyn Mob> = mob_arc.clone();
|
||||||
@ -55,12 +142,411 @@ impl CreakingEntity {
|
|||||||
|
|
||||||
mob_arc
|
mob_arc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_transient(&self, pos: BlockPos) {
|
||||||
|
self.set_home_pos(Some(pos));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_heart_bound(&self) -> bool {
|
||||||
|
self.get_home_pos().is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn can_move(&self) -> bool {
|
||||||
|
self.can_move.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_can_move(&self, can_move: bool) {
|
||||||
|
self.can_move.store(can_move, Ordering::Relaxed);
|
||||||
|
self.mob_entity.living_entity.entity.send_meta_data(
|
||||||
|
&[Metadata::new(
|
||||||
|
TrackedData::CAN_MOVE,
|
||||||
|
MetaDataType::BOOLEAN,
|
||||||
|
can_move,
|
||||||
|
)],
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_active(&self) -> bool {
|
||||||
|
self.is_active.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_is_active(&self, active: bool) {
|
||||||
|
self.is_active.store(active, Ordering::Relaxed);
|
||||||
|
self.mob_entity.living_entity.entity.send_meta_data(
|
||||||
|
&[Metadata::new(
|
||||||
|
TrackedData::IS_ACTIVE,
|
||||||
|
MetaDataType::BOOLEAN,
|
||||||
|
active,
|
||||||
|
)],
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_tearing_down(&self) -> bool {
|
||||||
|
self.is_tearing_down.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_tearing_down(&self) {
|
||||||
|
self.is_tearing_down.store(true, Ordering::Relaxed);
|
||||||
|
self.mob_entity.living_entity.entity.send_meta_data(
|
||||||
|
&[Metadata::new(
|
||||||
|
TrackedData::IS_TEARING_DOWN,
|
||||||
|
MetaDataType::BOOLEAN,
|
||||||
|
true,
|
||||||
|
)],
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_home_pos(&self) -> Option<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 {}
|
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 {
|
impl Mob for CreakingEntity {
|
||||||
fn get_mob_entity(&self) -> &MobEntity {
|
fn get_mob_entity(&self) -> &MobEntity {
|
||||||
&self.mob_entity
|
&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
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -163,6 +163,28 @@ impl Mob for CreeperEntity {
|
|||||||
&self.mob_entity
|
&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, ()> {
|
fn mob_tick<'a>(&'a self, _caller: &'a Arc<dyn EntityBase>) -> EntityBaseFuture<'a, ()> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let entity = &self.mob_entity.living_entity.entity;
|
let entity = &self.mob_entity.living_entity.entity;
|
||||||
|
|||||||
@ -1,28 +1,74 @@
|
|||||||
|
use std::sync::atomic::{AtomicI32, AtomicU8, Ordering};
|
||||||
use std::sync::{Arc, Weak};
|
use std::sync::{Arc, Weak};
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use pumpkin_data::entity::EntityType;
|
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::{
|
use crate::entity::{
|
||||||
Entity, NBTStorage,
|
Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture,
|
||||||
ai::goal::{
|
ai::goal::{
|
||||||
active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal,
|
Controls, Goal, GoalFuture, active_target::ActiveTargetGoal,
|
||||||
look_at_entity::LookAtEntityGoal, swim::SwimGoal, wander_around::WanderAroundGoal,
|
look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal,
|
||||||
|
wander_around::WanderAroundGoal,
|
||||||
},
|
},
|
||||||
mob::{Mob, MobEntity},
|
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 struct EvokerEntity {
|
||||||
pub mob_entity: MobEntity,
|
pub mob_entity: MobEntity,
|
||||||
|
spell_casting_tick_count: AtomicI32,
|
||||||
|
current_spell: AtomicU8,
|
||||||
|
wololo_target_id: Arc<Mutex<Option<i32>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EvokerEntity {
|
impl EvokerEntity {
|
||||||
pub fn new(entity: Entity) -> Arc<Self> {
|
pub fn new(entity: Entity) -> Arc<Self> {
|
||||||
let mob_entity = MobEntity::new(entity);
|
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_arc = Arc::new(evoker);
|
||||||
let mob_weak: Weak<dyn Mob> = {
|
let mob_weak: Weak<Self> = Arc::downgrade(&mob_arc);
|
||||||
let mob_arc: Arc<dyn Mob> = mob_arc.clone();
|
let mob_interface_weak: Weak<dyn Mob> = {
|
||||||
Arc::downgrade(&mob_arc)
|
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);
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||||
|
|
||||||
goal_selector.add_goal(0, Box::new(SwimGoal::default()));
|
goal_selector.add_goal(0, Box::new(SwimGoal::default()));
|
||||||
// Evokers have complex spell AI, but for now basic movement
|
goal_selector.add_goal(1, Box::new(EvokerCastingSpellGoal::new(mob_weak.clone())));
|
||||||
goal_selector.add_goal(5, Box::new(WanderAroundGoal::new(1.0)));
|
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(
|
goal_selector.add_goal(
|
||||||
6,
|
9,
|
||||||
LookAtEntityGoal::with_default(mob_weak.clone(), &EntityType::PLAYER, 8.0),
|
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
|
let mut target_selector = mob_arc
|
||||||
.mob_entity
|
.mob_entity
|
||||||
@ -62,12 +111,511 @@ impl EvokerEntity {
|
|||||||
|
|
||||||
mob_arc
|
mob_arc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_casting_spell(&self) -> bool {
|
||||||
|
self.spell_casting_tick_count.load(Ordering::Relaxed) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_is_casting_spell(&self, spell: IllagerSpell) {
|
||||||
|
self.current_spell.store(spell as u8, Ordering::Relaxed);
|
||||||
|
let entity = &self.mob_entity.living_entity.entity;
|
||||||
|
entity.send_meta_data(
|
||||||
|
&[Metadata::new(
|
||||||
|
TrackedData::SPELL_CASTING_ID,
|
||||||
|
MetaDataType::BYTE,
|
||||||
|
spell as u8 as i8,
|
||||||
|
)],
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_current_spell(&self) -> IllagerSpell {
|
||||||
|
IllagerSpell::from_u8(self.current_spell.load(Ordering::Relaxed))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_spell_casting_time(&self) -> i32 {
|
||||||
|
self.spell_casting_tick_count.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_spell_casting_time(&self, ticks: i32) {
|
||||||
|
self.spell_casting_tick_count
|
||||||
|
.store(ticks, Ordering::Relaxed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NBTStorage for EvokerEntity {}
|
impl NBTStorage for EvokerEntity {
|
||||||
|
fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
self.mob_entity.living_entity.entity.write_nbt(nbt).await;
|
||||||
|
nbt.put_int("SpellTicks", self.get_spell_casting_time());
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
self.mob_entity
|
||||||
|
.living_entity
|
||||||
|
.entity
|
||||||
|
.read_nbt_non_mut(nbt)
|
||||||
|
.await;
|
||||||
|
if let Some(ticks) = nbt.get_int("SpellTicks") {
|
||||||
|
self.set_spell_casting_time(ticks);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Mob for EvokerEntity {
|
impl Mob for EvokerEntity {
|
||||||
fn get_mob_entity(&self) -> &MobEntity {
|
fn get_mob_entity(&self) -> &MobEntity {
|
||||||
&self.mob_entity
|
&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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -623,12 +623,35 @@ pub trait Mob: EntityBase + Send + Sync {
|
|||||||
fn get_sheep(&self) -> Option<&crate::entity::passive::sheep::SheepEntity> {
|
fn get_sheep(&self) -> Option<&crate::entity::passive::sheep::SheepEntity> {
|
||||||
None
|
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 {
|
impl<T: Mob + Send + 'static> EntityBase for T {
|
||||||
fn get_mob(&self) -> Option<&dyn Mob> {
|
fn get_mob(&self) -> Option<&dyn Mob> {
|
||||||
Some(self)
|
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> {
|
fn get_item_steerable(&self) -> Option<&dyn crate::entity::item_steerable::ItemSteerable> {
|
||||||
Mob::get_item_steerable(self)
|
Mob::get_item_steerable(self)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -91,9 +91,12 @@ pub mod effect;
|
|||||||
pub mod experience_orb;
|
pub mod experience_orb;
|
||||||
pub mod falling;
|
pub mod falling;
|
||||||
pub mod hunger;
|
pub mod hunger;
|
||||||
|
pub mod interaction;
|
||||||
pub mod item;
|
pub mod item;
|
||||||
pub mod item_steerable;
|
pub mod item_steerable;
|
||||||
|
pub mod lightning;
|
||||||
pub mod living;
|
pub mod living;
|
||||||
|
pub mod marker;
|
||||||
pub mod mob;
|
pub mod mob;
|
||||||
pub mod passive;
|
pub mod passive;
|
||||||
pub mod player;
|
pub mod player;
|
||||||
@ -103,6 +106,8 @@ pub mod tnt;
|
|||||||
pub mod r#type;
|
pub mod r#type;
|
||||||
pub mod vehicle;
|
pub mod vehicle;
|
||||||
|
|
||||||
|
pub use lightning::LightningBoltEntity;
|
||||||
|
|
||||||
mod combat;
|
mod combat;
|
||||||
pub mod predicate;
|
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 {
|
fn is_spectator(&self) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@ -3210,6 +3237,11 @@ impl Entity {
|
|||||||
vehicle.is_some()
|
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(
|
pub async fn add_passenger(
|
||||||
&self,
|
&self,
|
||||||
vehicle: Arc<dyn EntityBase>,
|
vehicle: Arc<dyn EntityBase>,
|
||||||
|
|||||||
@ -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::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::{
|
use crate::entity::{
|
||||||
Entity, NBTStorage,
|
Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture,
|
||||||
|
ageable::{AgeableData, AgeableMob},
|
||||||
ai::goal::{
|
ai::goal::{
|
||||||
|
breed::BreedGoal, escape_danger::EscapeDangerGoal, follow_parent::FollowParentGoal,
|
||||||
look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal,
|
look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal,
|
||||||
wander_around::WanderAroundGoal,
|
tempt::TemptGoal, wander_around::WanderAroundGoal,
|
||||||
},
|
},
|
||||||
|
item::ItemEntity,
|
||||||
mob::{Mob, MobEntity},
|
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.
|
/// Represents an Armadillo, a passive entity that can roll into a ball when threatened.
|
||||||
///
|
///
|
||||||
/// Wiki: <https://minecraft.wiki/w/Armadillo>
|
/// Wiki: <https://minecraft.wiki/w/Armadillo>
|
||||||
pub struct ArmadilloEntity {
|
pub struct ArmadilloEntity {
|
||||||
pub mob_entity: MobEntity,
|
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 {
|
impl ArmadilloEntity {
|
||||||
pub fn new(entity: Entity) -> Arc<Self> {
|
pub fn new(entity: Entity) -> Arc<Self> {
|
||||||
let mob_entity = MobEntity::new(entity);
|
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_arc = Arc::new(armadillo);
|
||||||
let mob_weak: Weak<dyn Mob> = {
|
let mob_weak: Weak<dyn Mob> = {
|
||||||
let mob_arc: Arc<dyn Mob> = mob_arc.clone();
|
let mob_arc: Arc<dyn Mob> = mob_arc.clone();
|
||||||
@ -36,22 +149,320 @@ impl ArmadilloEntity {
|
|||||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||||
|
|
||||||
goal_selector.add_goal(0, Box::new(SwimGoal::default()));
|
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(
|
goal_selector.add_goal(
|
||||||
2,
|
6,
|
||||||
LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 6.0),
|
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
|
mob_arc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn get_state(&self) -> ArmadilloState {
|
||||||
|
ArmadilloState::from_id(self.state.load(Ordering::Relaxed))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn switch_to_state(&self, state: ArmadilloState) {
|
||||||
|
self.state.store(state.id(), Ordering::Relaxed);
|
||||||
|
self.in_state_ticks.store(0, Ordering::Relaxed);
|
||||||
|
let entity = self.get_entity();
|
||||||
|
entity.send_meta_data(
|
||||||
|
&[Metadata::new(
|
||||||
|
TrackedData::ARMADILLO_STATE,
|
||||||
|
MetaDataType::ARMADILLO_STATE,
|
||||||
|
VarInt(state.id()),
|
||||||
|
)],
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_scared(&self) -> bool {
|
||||||
|
self.get_state() != ArmadilloState::Idle
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn should_hide_in_shell(&self) -> bool {
|
||||||
|
self.get_state()
|
||||||
|
.should_hide_in_shell(self.in_state_ticks.load(Ordering::Relaxed))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn should_switch_to_scared_state(&self) -> bool {
|
||||||
|
self.get_state() == ArmadilloState::Rolling
|
||||||
|
&& self.in_state_ticks.load(Ordering::Relaxed)
|
||||||
|
> ArmadilloState::Rolling.animation_duration()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn can_stay_rolled_up(&self) -> bool {
|
||||||
|
!self.is_panicking()
|
||||||
|
&& !self.mob_entity.living_entity.is_in_water()
|
||||||
|
&& !self.get_entity().has_vehicle().await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn roll_up(&self) {
|
||||||
|
if !self.is_scared() {
|
||||||
|
self.mob_entity.reset_love_ticks();
|
||||||
|
let entity = self.get_entity();
|
||||||
|
let world = entity.world.load();
|
||||||
|
world.play_sound(
|
||||||
|
Sound::EntityArmadilloRoll,
|
||||||
|
SoundCategory::Neutral,
|
||||||
|
&entity.pos.load(),
|
||||||
|
);
|
||||||
|
self.switch_to_state(ArmadilloState::Rolling);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn roll_out(&self) {
|
||||||
|
if self.is_scared() {
|
||||||
|
let entity = self.get_entity();
|
||||||
|
let world = entity.world.load();
|
||||||
|
world.play_sound(
|
||||||
|
Sound::EntityArmadilloUnrollFinish,
|
||||||
|
SoundCategory::Neutral,
|
||||||
|
&entity.pos.load(),
|
||||||
|
);
|
||||||
|
self.switch_to_state(ArmadilloState::Idle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn brush_off_scute<'a>(&'a self, player: &'a Arc<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 NBTStorage for ArmadilloEntity {}
|
impl AgeableMob for ArmadilloEntity {
|
||||||
|
fn get_ageable_data(&self) -> &AgeableData {
|
||||||
|
&self.ageable_data
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_baby_start_age(&self) -> i32 {
|
||||||
|
ARMADILLO_BABY_START_AGE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NBTStorage for ArmadilloEntity {
|
||||||
|
fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
self.mob_entity.living_entity.write_nbt(nbt).await;
|
||||||
|
self.write_ageable_nbt(nbt);
|
||||||
|
self.write_animal_nbt(nbt);
|
||||||
|
nbt.put_string("state", self.get_state().name().to_string());
|
||||||
|
nbt.put_int("scute_time", self.scute_time.load(Ordering::Relaxed));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
self.mob_entity.living_entity.read_nbt_non_mut(nbt).await;
|
||||||
|
self.read_ageable_nbt(nbt);
|
||||||
|
self.read_animal_nbt(nbt);
|
||||||
|
if let Some(state_name) = nbt.get_string("state") {
|
||||||
|
self.switch_to_state(ArmadilloState::from_name(state_name));
|
||||||
|
} else if let Some(state_id) = nbt.get_int("state") {
|
||||||
|
self.switch_to_state(ArmadilloState::from_id(state_id));
|
||||||
|
}
|
||||||
|
if let Some(scute_time) = nbt.get_int("scute_time") {
|
||||||
|
self.scute_time.store(scute_time, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Animal for ArmadilloEntity {
|
||||||
|
fn is_food(&self, item_stack: &ItemStack) -> bool {
|
||||||
|
item_stack
|
||||||
|
.item
|
||||||
|
.has_tag(&tag::Item::MINECRAFT_ARMADILLO_FOOD)
|
||||||
|
|| ARMADILLO_FOOD.iter().any(|i| i.id == item_stack.item.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Mob for ArmadilloEntity {
|
impl Mob for ArmadilloEntity {
|
||||||
fn get_mob_entity(&self) -> &MobEntity {
|
fn get_mob_entity(&self) -> &MobEntity {
|
||||||
&self.mob_entity
|
&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
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,31 +1,87 @@
|
|||||||
use std::sync::{
|
use std::sync::{
|
||||||
Arc, Weak,
|
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::Item;
|
||||||
|
use pumpkin_data::item_stack::ItemStack;
|
||||||
use pumpkin_data::meta_data_type::MetaDataType;
|
use pumpkin_data::meta_data_type::MetaDataType;
|
||||||
|
use pumpkin_data::tag::{self, Taggable};
|
||||||
use pumpkin_data::tracked_data::TrackedData;
|
use pumpkin_data::tracked_data::TrackedData;
|
||||||
use pumpkin_nbt::compound::NbtCompound;
|
use pumpkin_nbt::compound::NbtCompound;
|
||||||
|
use pumpkin_protocol::bedrock::server::actor_event::ActorEventType;
|
||||||
use pumpkin_protocol::codec::var_int::VarInt;
|
use pumpkin_protocol::codec::var_int::VarInt;
|
||||||
use pumpkin_protocol::java::client::play::Metadata;
|
use pumpkin_protocol::java::client::play::Metadata;
|
||||||
|
use rand::RngExt;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::entity::{
|
use crate::entity::{
|
||||||
Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture,
|
Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture,
|
||||||
ai::goal::{
|
ai::goal::{
|
||||||
breed::BreedGoal, escape_danger::EscapeDangerGoal, follow_parent::FollowParentGoal,
|
active_target::ActiveTargetGoal, avoid_entity::AvoidEntityGoal, breed::BreedGoal,
|
||||||
look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal,
|
escape_danger::EscapeDangerGoal, follow_owner::FollowOwnerGoal,
|
||||||
tempt::TemptGoal, wander_around::WanderAroundGoal,
|
follow_parent::FollowParentGoal, look_around::RandomLookAroundGoal,
|
||||||
|
look_at_entity::LookAtEntityGoal, swim::SwimGoal, tempt::TemptGoal,
|
||||||
|
wander_around::WanderAroundGoal,
|
||||||
},
|
},
|
||||||
mob::{Mob, MobEntity},
|
mob::{Mob, MobEntity},
|
||||||
|
passive::animal::Animal,
|
||||||
|
player::Player,
|
||||||
};
|
};
|
||||||
|
|
||||||
const TEMPT_ITEMS: &[&Item] = &[&Item::COD, &Item::SALMON];
|
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 struct CatEntity {
|
||||||
pub mob_entity: MobEntity,
|
pub mob_entity: MobEntity,
|
||||||
pub variant: AtomicU8,
|
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 {
|
impl CatEntity {
|
||||||
@ -33,7 +89,14 @@ impl CatEntity {
|
|||||||
let mob_entity = MobEntity::new(entity);
|
let mob_entity = MobEntity::new(entity);
|
||||||
let cat = Self {
|
let cat = Self {
|
||||||
mob_entity,
|
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_arc = Arc::new(cat);
|
||||||
let mob_weak: Weak<dyn Mob> = {
|
let mob_weak: Weak<dyn Mob> = {
|
||||||
@ -48,29 +111,177 @@ impl CatEntity {
|
|||||||
.lock()
|
.lock()
|
||||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||||
|
|
||||||
|
// Goal 1: SwimGoal (FloatGoal)
|
||||||
goal_selector.add_goal(1, Box::new(SwimGoal::default()));
|
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(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_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(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_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_selector.add_goal(11, Box::new(WanderAroundGoal::new(0.8)));
|
||||||
|
// Goal 12: LookAtPlayerGoal
|
||||||
goal_selector.add_goal(
|
goal_selector.add_goal(
|
||||||
12,
|
12,
|
||||||
LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 10.0),
|
LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 10.0),
|
||||||
);
|
);
|
||||||
|
// Goal 12: RandomLookAroundGoal
|
||||||
goal_selector.add_goal(12, Box::new(RandomLookAroundGoal::default()));
|
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
|
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 {
|
impl NBTStorage for CatEntity {
|
||||||
fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
|
fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
|
||||||
Box::pin(async {
|
Box::pin(async {
|
||||||
self.mob_entity.living_entity.write_nbt(nbt).await;
|
self.mob_entity.living_entity.write_nbt(nbt).await;
|
||||||
|
self.write_animal_nbt(nbt);
|
||||||
let variant_str = match self.variant.load(Ordering::Relaxed) {
|
let variant_str = match self.variant.load(Ordering::Relaxed) {
|
||||||
0 => "minecraft:all_black",
|
0 => "minecraft:all_black",
|
||||||
1 => "minecraft:black",
|
1 => "minecraft:black",
|
||||||
@ -85,12 +296,23 @@ impl NBTStorage for CatEntity {
|
|||||||
_ => "minecraft:tabby",
|
_ => "minecraft:tabby",
|
||||||
};
|
};
|
||||||
nbt.put_string("variant", variant_str.to_string());
|
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, ()> {
|
fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
|
||||||
Box::pin(async {
|
Box::pin(async {
|
||||||
self.mob_entity.living_entity.read_nbt_non_mut(nbt).await;
|
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") {
|
if let Some(variant_str) = nbt.get_string("variant") {
|
||||||
let variant = match variant_str
|
let variant = match variant_str
|
||||||
.strip_prefix("minecraft:")
|
.strip_prefix("minecraft:")
|
||||||
@ -110,15 +332,44 @@ impl NBTStorage for CatEntity {
|
|||||||
};
|
};
|
||||||
self.variant.store(variant, Ordering::Relaxed);
|
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 {
|
impl Mob for CatEntity {
|
||||||
fn get_mob_entity(&self) -> &MobEntity {
|
fn get_mob_entity(&self) -> &MobEntity {
|
||||||
&self.mob_entity
|
&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) {
|
fn mob_set_variant_name(&self, name: &str) {
|
||||||
let variant = match name.strip_prefix("minecraft:").unwrap_or(name) {
|
let variant = match name.strip_prefix("minecraft:").unwrap_or(name) {
|
||||||
"all_black" => 0,
|
"all_black" => 0,
|
||||||
@ -150,6 +401,22 @@ impl Mob for CatEntity {
|
|||||||
None,
|
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(
|
entity.send_meta_data(
|
||||||
&[Metadata::new(
|
&[Metadata::new(
|
||||||
TrackedData::CAT_VARIANT,
|
TrackedData::CAT_VARIANT,
|
||||||
@ -158,6 +425,104 @@ impl Mob for CatEntity {
|
|||||||
)],
|
)],
|
||||||
None,
|
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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -130,7 +130,11 @@ impl NBTStorage for ChickenEntity {
|
|||||||
|
|
||||||
impl super::animal::Animal for ChickenEntity {
|
impl super::animal::Animal for ChickenEntity {
|
||||||
fn is_food(&self, item_stack: &ItemStack) -> bool {
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
316
crates/pumpkin/src/entity/passive/copper_golem.rs
Normal file
316
crates/pumpkin/src/entity/passive/copper_golem.rs
Normal 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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -91,7 +91,11 @@ impl NBTStorage for CowEntity {
|
|||||||
|
|
||||||
impl super::animal::Animal for CowEntity {
|
impl super::animal::Animal for CowEntity {
|
||||||
fn is_food(&self, item_stack: &ItemStack) -> bool {
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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::{
|
use crate::entity::{
|
||||||
Entity, NBTStorage,
|
Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture,
|
||||||
|
ageable::{AgeableData, AgeableMob},
|
||||||
ai::goal::{
|
ai::goal::{
|
||||||
breed::BreedGoal, escape_danger::EscapeDangerGoal, follow_parent::FollowParentGoal,
|
breed::BreedGoal, escape_danger::EscapeDangerGoal, follow_parent::FollowParentGoal,
|
||||||
look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal,
|
look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal,
|
||||||
tempt::TemptGoal, wander_around::WanderAroundGoal,
|
tempt::TemptGoal, wander_around::WanderAroundGoal,
|
||||||
},
|
},
|
||||||
mob::{Mob, MobEntity},
|
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>
|
/// Wiki: <https://minecraft.wiki/w/Fox>
|
||||||
pub struct FoxEntity {
|
pub struct FoxEntity {
|
||||||
pub mob_entity: MobEntity,
|
pub mob_entity: MobEntity,
|
||||||
|
pub ageable_data: AgeableData,
|
||||||
|
pub variant: AtomicI32,
|
||||||
|
pub flags: AtomicU8,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FoxEntity {
|
impl FoxEntity {
|
||||||
pub fn new(entity: Entity) -> Arc<Self> {
|
pub fn new(entity: Entity) -> Arc<Self> {
|
||||||
let mob_entity = MobEntity::new(entity);
|
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_arc = Arc::new(fox);
|
||||||
let mob_weak: Weak<dyn Mob> = {
|
let mob_weak: Weak<dyn Mob> = {
|
||||||
let mob_arc: Arc<dyn Mob> = mob_arc.clone();
|
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(0, Box::new(SwimGoal::default()));
|
||||||
goal_selector.add_goal(1, EscapeDangerGoal::new(1.5));
|
goal_selector.add_goal(1, EscapeDangerGoal::new(1.5));
|
||||||
goal_selector.add_goal(2, BreedGoal::new(1.0));
|
goal_selector.add_goal(2, BreedGoal::new(1.0));
|
||||||
goal_selector.add_goal(3, Box::new(TemptGoal::new(1.2, TEMPT_ITEMS)));
|
goal_selector.add_goal(3, Box::new(TemptGoal::new(1.2, FOX_FOOD)));
|
||||||
goal_selector.add_goal(4, Box::new(FollowParentGoal::new(1.1)));
|
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(6, Box::new(WanderAroundGoal::new(1.0)));
|
||||||
goal_selector.add_goal(
|
goal_selector.add_goal(
|
||||||
@ -53,12 +115,224 @@ impl FoxEntity {
|
|||||||
|
|
||||||
mob_arc
|
mob_arc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn get_variant(&self) -> FoxVariant {
|
||||||
|
FoxVariant::from_id(self.variant.load(Ordering::Relaxed))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_variant(&self, variant: FoxVariant) {
|
||||||
|
self.variant.store(variant.id(), Ordering::Relaxed);
|
||||||
|
let entity = self.get_entity();
|
||||||
|
entity.send_meta_data(
|
||||||
|
&[Metadata::new(
|
||||||
|
TrackedData::TYPE_ID,
|
||||||
|
MetaDataType::INT,
|
||||||
|
VarInt(variant.id()),
|
||||||
|
)],
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_flag(&self, flag: u8) -> bool {
|
||||||
|
(self.flags.load(Ordering::Relaxed) & flag) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_flag(&self, flag: u8, value: bool) {
|
||||||
|
let current = self.flags.load(Ordering::Relaxed);
|
||||||
|
let new_flags = if value {
|
||||||
|
current | flag
|
||||||
|
} else {
|
||||||
|
current & !flag
|
||||||
|
};
|
||||||
|
self.flags.store(new_flags, Ordering::Relaxed);
|
||||||
|
let entity = self.get_entity();
|
||||||
|
entity.send_meta_data(
|
||||||
|
&[Metadata::new(
|
||||||
|
TrackedData::FLAGS_ID,
|
||||||
|
MetaDataType::BYTE,
|
||||||
|
new_flags,
|
||||||
|
)],
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_sitting(&self) -> bool {
|
||||||
|
self.get_flag(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_sitting(&self, val: bool) {
|
||||||
|
self.set_flag(1, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_crouching(&self) -> bool {
|
||||||
|
self.get_flag(4)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_crouching(&self, val: bool) {
|
||||||
|
self.set_flag(4, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_interested(&self) -> bool {
|
||||||
|
self.get_flag(8)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_interested(&self, val: bool) {
|
||||||
|
self.set_flag(8, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_pouncing(&self) -> bool {
|
||||||
|
self.get_flag(16)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_pouncing(&self, val: bool) {
|
||||||
|
self.set_flag(16, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_sleeping(&self) -> bool {
|
||||||
|
self.get_flag(32)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_sleeping(&self, val: bool) {
|
||||||
|
self.set_flag(32, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_faceplanted(&self) -> bool {
|
||||||
|
self.get_flag(64)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_faceplanted(&self, val: bool) {
|
||||||
|
self.set_flag(64, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_defending(&self) -> bool {
|
||||||
|
self.get_flag(128)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_defending(&self, val: bool) {
|
||||||
|
self.set_flag(128, val);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear_states(&self) {
|
||||||
|
self.set_interested(false);
|
||||||
|
self.set_crouching(false);
|
||||||
|
self.set_sitting(false);
|
||||||
|
self.set_sleeping(false);
|
||||||
|
self.set_defending(false);
|
||||||
|
self.set_faceplanted(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NBTStorage for FoxEntity {}
|
impl AgeableMob for FoxEntity {
|
||||||
|
fn get_ageable_data(&self) -> &AgeableData {
|
||||||
|
&self.ageable_data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Animal for FoxEntity {
|
||||||
|
fn is_food(&self, item_stack: &ItemStack) -> bool {
|
||||||
|
item_stack.item.has_tag(&tag::Item::MINECRAFT_FOX_FOOD)
|
||||||
|
|| FOX_FOOD.iter().any(|i| i.id == item_stack.item.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NBTStorage for FoxEntity {
|
||||||
|
fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
self.mob_entity.living_entity.write_nbt(nbt).await;
|
||||||
|
self.write_ageable_nbt(nbt);
|
||||||
|
self.write_animal_nbt(nbt);
|
||||||
|
nbt.put_string("Type", self.get_variant().as_str().to_string());
|
||||||
|
nbt.put_bool("Sleeping", self.is_sleeping());
|
||||||
|
nbt.put_bool("Sitting", self.is_sitting());
|
||||||
|
nbt.put_bool("Crouching", self.is_crouching());
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
self.mob_entity.living_entity.read_nbt_non_mut(nbt).await;
|
||||||
|
self.read_ageable_nbt(nbt);
|
||||||
|
self.read_animal_nbt(nbt);
|
||||||
|
if let Some(variant_str) = nbt.get_string("Type") {
|
||||||
|
self.set_variant(FoxVariant::from_name(variant_str));
|
||||||
|
}
|
||||||
|
if let Some(sleeping) = nbt.get_bool("Sleeping") {
|
||||||
|
self.set_sleeping(sleeping);
|
||||||
|
}
|
||||||
|
if let Some(sitting) = nbt.get_bool("Sitting") {
|
||||||
|
self.set_sitting(sitting);
|
||||||
|
}
|
||||||
|
if let Some(crouching) = nbt.get_bool("Crouching") {
|
||||||
|
self.set_crouching(crouching);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Mob for FoxEntity {
|
impl Mob for FoxEntity {
|
||||||
fn get_mob_entity(&self) -> &MobEntity {
|
fn get_mob_entity(&self) -> &MobEntity {
|
||||||
&self.mob_entity
|
&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
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,14 @@
|
|||||||
use std::sync::{
|
use std::sync::{
|
||||||
Arc, Weak,
|
Arc, Weak,
|
||||||
atomic::{AtomicU8, Ordering},
|
atomic::{AtomicI32, Ordering},
|
||||||
};
|
};
|
||||||
|
|
||||||
use pumpkin_data::entity::EntityType;
|
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::meta_data_type::MetaDataType;
|
||||||
|
use pumpkin_data::sound::Sound;
|
||||||
|
use pumpkin_data::tag::{self, Taggable};
|
||||||
use pumpkin_data::tracked_data::TrackedData;
|
use pumpkin_data::tracked_data::TrackedData;
|
||||||
use pumpkin_nbt::compound::NbtCompound;
|
use pumpkin_nbt::compound::NbtCompound;
|
||||||
use pumpkin_protocol::codec::var_int::VarInt;
|
use pumpkin_protocol::codec::var_int::VarInt;
|
||||||
@ -12,19 +16,69 @@ use pumpkin_protocol::java::client::play::Metadata;
|
|||||||
|
|
||||||
use crate::entity::{
|
use crate::entity::{
|
||||||
Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture,
|
Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture,
|
||||||
|
ageable::{AgeableData, AgeableMob},
|
||||||
ai::goal::{
|
ai::goal::{
|
||||||
look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal,
|
look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal,
|
||||||
wander_around::WanderAroundGoal,
|
tempt::TemptGoal, wander_around::WanderAroundGoal,
|
||||||
},
|
},
|
||||||
mob::{Mob, MobEntity},
|
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>
|
/// Wiki: <https://minecraft.wiki/w/Frog>
|
||||||
pub struct FrogEntity {
|
pub struct FrogEntity {
|
||||||
pub mob_entity: MobEntity,
|
pub mob_entity: MobEntity,
|
||||||
pub variant: AtomicU8,
|
pub ageable_data: AgeableData,
|
||||||
|
pub variant: AtomicI32,
|
||||||
|
pub tongue_target_id: AtomicI32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FrogEntity {
|
impl FrogEntity {
|
||||||
@ -32,7 +86,9 @@ impl FrogEntity {
|
|||||||
let mob_entity = MobEntity::new(entity);
|
let mob_entity = MobEntity::new(entity);
|
||||||
let frog = Self {
|
let frog = Self {
|
||||||
mob_entity,
|
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_arc = Arc::new(frog);
|
||||||
let mob_weak: Weak<dyn Mob> = {
|
let mob_weak: Weak<dyn Mob> = {
|
||||||
@ -48,41 +104,67 @@ impl FrogEntity {
|
|||||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||||
|
|
||||||
goal_selector.add_goal(0, Box::new(SwimGoal::default()));
|
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(
|
goal_selector.add_goal(
|
||||||
2,
|
3,
|
||||||
LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 6.0),
|
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
|
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 {
|
impl NBTStorage for FrogEntity {
|
||||||
fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
|
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;
|
self.mob_entity.living_entity.write_nbt(nbt).await;
|
||||||
let variant_str = match self.variant.load(Ordering::Relaxed) {
|
self.write_ageable_nbt(nbt);
|
||||||
0 => "minecraft:cold",
|
self.write_animal_nbt(nbt);
|
||||||
2 => "minecraft:warm",
|
nbt.put_string("variant", self.get_variant().as_str().to_string());
|
||||||
_ => "minecraft:temperate",
|
|
||||||
};
|
|
||||||
nbt.put_string("variant", variant_str.to_string());
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
|
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.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") {
|
if let Some(variant_str) = nbt.get_string("variant") {
|
||||||
let variant = match variant_str {
|
self.set_variant(FrogVariant::from_name(variant_str));
|
||||||
"minecraft:cold" | "cold" => 0,
|
|
||||||
"minecraft:warm" | "warm" => 2,
|
|
||||||
_ => 1,
|
|
||||||
};
|
|
||||||
self.variant.store(variant, Ordering::Relaxed);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -94,12 +176,13 @@ impl Mob for FrogEntity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn mob_set_variant_name(&self, name: &str) {
|
fn mob_set_variant_name(&self, name: &str) {
|
||||||
let variant = match name {
|
self.set_variant(FrogVariant::from_name(name));
|
||||||
"minecraft:cold" | "cold" => 0,
|
}
|
||||||
"minecraft:warm" | "warm" => 2,
|
|
||||||
_ => 1,
|
fn mob_tick<'a>(&'a self, _caller: &'a Arc<dyn EntityBase>) -> EntityBaseFuture<'a, ()> {
|
||||||
};
|
Box::pin(async move {
|
||||||
self.variant.store(variant, Ordering::Relaxed);
|
self.ageable_ai_step();
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mob_init_data_tracker(&self) -> EntityBaseFuture<'_, ()> {
|
fn mob_init_data_tracker(&self) -> EntityBaseFuture<'_, ()> {
|
||||||
@ -120,10 +203,21 @@ impl Mob for FrogEntity {
|
|||||||
&[Metadata::new(
|
&[Metadata::new(
|
||||||
TrackedData::VARIANT,
|
TrackedData::VARIANT,
|
||||||
MetaDataType::FROG_VARIANT,
|
MetaDataType::FROG_VARIANT,
|
||||||
VarInt(self.variant.load(Ordering::Relaxed) as i32),
|
VarInt(self.get_variant().id()),
|
||||||
)],
|
)],
|
||||||
None,
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
249
crates/pumpkin/src/entity/passive/happy_ghast.rs
Normal file
249
crates/pumpkin/src/entity/passive/happy_ghast.rs
Normal 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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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::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::{
|
use crate::entity::{
|
||||||
Entity, NBTStorage, NbtFuture,
|
Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture,
|
||||||
ai::goal::{
|
ai::goal::{
|
||||||
active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal,
|
active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal,
|
||||||
look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, revenge::RevengeGoal,
|
look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, revenge::RevengeGoal,
|
||||||
wander_around::WanderAroundGoal,
|
wander_around::WanderAroundGoal,
|
||||||
},
|
},
|
||||||
mob::{Mob, MobEntity},
|
mob::{Mob, MobEntity},
|
||||||
|
player::Player,
|
||||||
};
|
};
|
||||||
use pumpkin_nbt::compound::NbtCompound;
|
|
||||||
|
|
||||||
/// Represents an Iron Golem, a powerful neutral mob that protects villagers and players.
|
/// Represents an Iron Golem, a powerful neutral mob that protects villagers and players.
|
||||||
///
|
///
|
||||||
/// Wiki: <https://minecraft.wiki/w/Iron_Golem>
|
/// Wiki: <https://minecraft.wiki/w/Iron_Golem>
|
||||||
pub struct IronGolemEntity {
|
pub struct IronGolemEntity {
|
||||||
pub mob_entity: MobEntity,
|
pub mob_entity: MobEntity,
|
||||||
|
pub player_created: AtomicBool,
|
||||||
|
pub attack_animation_tick: AtomicI32,
|
||||||
|
pub offer_flower_tick: AtomicI32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IronGolemEntity {
|
impl IronGolemEntity {
|
||||||
pub fn new(entity: Entity) -> Arc<Self> {
|
pub fn new(entity: Entity) -> Arc<Self> {
|
||||||
let mob_entity = MobEntity::new(entity);
|
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_arc = Arc::new(iron_golem);
|
||||||
let mob_weak: Weak<dyn Mob> = {
|
let mob_weak: Weak<dyn Mob> = {
|
||||||
let mob_arc: Arc<dyn Mob> = mob_arc.clone();
|
let mob_arc: Arc<dyn Mob> = mob_arc.clone();
|
||||||
@ -63,15 +82,42 @@ impl IronGolemEntity {
|
|||||||
|
|
||||||
mob_arc
|
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 {
|
impl NBTStorage for IronGolemEntity {
|
||||||
fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
|
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, ()> {
|
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 {
|
fn get_mob_entity(&self) -> &MobEntity {
|
||||||
&self.mob_entity
|
&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
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ pub mod camel;
|
|||||||
pub mod cat;
|
pub mod cat;
|
||||||
pub mod chicken;
|
pub mod chicken;
|
||||||
pub mod cod;
|
pub mod cod;
|
||||||
|
pub mod copper_golem;
|
||||||
pub mod cow;
|
pub mod cow;
|
||||||
pub mod dolphin;
|
pub mod dolphin;
|
||||||
pub mod donkey;
|
pub mod donkey;
|
||||||
@ -14,11 +15,13 @@ pub mod fox;
|
|||||||
pub mod frog;
|
pub mod frog;
|
||||||
pub mod glow_squid;
|
pub mod glow_squid;
|
||||||
pub mod goat;
|
pub mod goat;
|
||||||
|
pub mod happy_ghast;
|
||||||
pub mod horse;
|
pub mod horse;
|
||||||
pub mod iron_golem;
|
pub mod iron_golem;
|
||||||
pub mod llama;
|
pub mod llama;
|
||||||
pub mod mooshroom;
|
pub mod mooshroom;
|
||||||
pub mod mule;
|
pub mod mule;
|
||||||
|
pub mod nautilus;
|
||||||
pub mod ocelot;
|
pub mod ocelot;
|
||||||
pub mod panda;
|
pub mod panda;
|
||||||
pub mod parrot;
|
pub mod parrot;
|
||||||
|
|||||||
416
crates/pumpkin/src/entity/passive/nautilus.rs
Normal file
416
crates/pumpkin/src/entity/passive/nautilus.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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::{
|
use crate::entity::{
|
||||||
Entity, NBTStorage,
|
Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture,
|
||||||
ai::goal::{
|
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,
|
look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal,
|
||||||
wander_around::WanderAroundGoal,
|
tempt::TemptGoal, wander_around::WanderAroundGoal,
|
||||||
},
|
},
|
||||||
mob::{Mob, MobEntity},
|
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.
|
/// Represents an Ocelot, a shy passive mob found in jungles.
|
||||||
///
|
///
|
||||||
/// Wiki: <https://minecraft.wiki/w/Ocelot>
|
/// Wiki: <https://minecraft.wiki/w/Ocelot>
|
||||||
pub struct OcelotEntity {
|
pub struct OcelotEntity {
|
||||||
pub mob_entity: MobEntity,
|
pub mob_entity: MobEntity,
|
||||||
|
pub is_trusting: AtomicBool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OcelotEntity {
|
impl OcelotEntity {
|
||||||
pub fn new(entity: Entity) -> Arc<Self> {
|
pub fn new(entity: Entity) -> Arc<Self> {
|
||||||
let mob_entity = MobEntity::new(entity);
|
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_arc = Arc::new(ocelot);
|
||||||
let mob_weak: Weak<dyn Mob> = {
|
let mob_weak: Weak<dyn Mob> = {
|
||||||
let mob_arc: Arc<dyn Mob> = mob_arc.clone();
|
let mob_arc: Arc<dyn Mob> = mob_arc.clone();
|
||||||
@ -35,23 +57,171 @@ impl OcelotEntity {
|
|||||||
.lock()
|
.lock()
|
||||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||||
|
|
||||||
goal_selector.add_goal(0, Box::new(SwimGoal::default()));
|
// Goal 1: FloatGoal (SwimGoal)
|
||||||
goal_selector.add_goal(1, Box::new(WanderAroundGoal::new(1.0)));
|
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(
|
goal_selector.add_goal(
|
||||||
2,
|
4,
|
||||||
LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 6.0),
|
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
|
mob_arc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_trusting(&self) -> bool {
|
||||||
|
self.is_trusting.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_trusting(&self, trusting: bool) {
|
||||||
|
self.is_trusting.store(trusting, Ordering::Relaxed);
|
||||||
|
let entity = self.get_entity();
|
||||||
|
entity.send_meta_data(
|
||||||
|
&[Metadata::new(
|
||||||
|
TrackedData::TRUSTING,
|
||||||
|
MetaDataType::BOOLEAN,
|
||||||
|
trusting,
|
||||||
|
)],
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NBTStorage for OcelotEntity {}
|
impl NBTStorage for OcelotEntity {
|
||||||
|
fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
|
||||||
|
Box::pin(async {
|
||||||
|
self.mob_entity.living_entity.write_nbt(nbt).await;
|
||||||
|
self.write_animal_nbt(nbt);
|
||||||
|
nbt.put_bool("Trusting", self.is_trusting.load(Ordering::Relaxed));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
|
||||||
|
Box::pin(async {
|
||||||
|
self.mob_entity.living_entity.read_nbt_non_mut(nbt).await;
|
||||||
|
self.read_animal_nbt(nbt);
|
||||||
|
if let Some(trusting) = nbt.get_bool("Trusting") {
|
||||||
|
self.is_trusting.store(trusting, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Animal for OcelotEntity {
|
||||||
|
fn is_food(&self, item_stack: &ItemStack) -> bool {
|
||||||
|
let item = item_stack.get_item();
|
||||||
|
item.has_tag(&tag::Item::MINECRAFT_OCELOT_FOOD)
|
||||||
|
|| item == &Item::COD
|
||||||
|
|| item == &Item::SALMON
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Mob for OcelotEntity {
|
impl Mob for OcelotEntity {
|
||||||
fn get_mob_entity(&self) -> &MobEntity {
|
fn get_mob_entity(&self) -> &MobEntity {
|
||||||
&self.mob_entity
|
&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
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -107,7 +107,11 @@ impl NBTStorage for PigEntity {
|
|||||||
|
|
||||||
impl super::animal::Animal for PigEntity {
|
impl super::animal::Animal for PigEntity {
|
||||||
fn is_food(&self, item_stack: &ItemStack) -> bool {
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,7 @@ use pumpkin_protocol::java::client::play::Metadata;
|
|||||||
|
|
||||||
use crate::entity::{
|
use crate::entity::{
|
||||||
Entity, EntityBaseFuture, NBTStorage, NbtFuture,
|
Entity, EntityBaseFuture, NBTStorage, NbtFuture,
|
||||||
|
ageable::AgeableMob,
|
||||||
ai::goal::{
|
ai::goal::{
|
||||||
breed::BreedGoal, eat_grass::EatGrassGoal, escape_danger::EscapeDangerGoal,
|
breed::BreedGoal, eat_grass::EatGrassGoal, escape_danger::EscapeDangerGoal,
|
||||||
follow_parent::FollowParentGoal, look_around::RandomLookAroundGoal,
|
follow_parent::FollowParentGoal, look_around::RandomLookAroundGoal,
|
||||||
@ -18,6 +19,7 @@ use crate::entity::{
|
|||||||
wander_around::WanderAroundGoal,
|
wander_around::WanderAroundGoal,
|
||||||
},
|
},
|
||||||
mob::{Mob, MobEntity},
|
mob::{Mob, MobEntity},
|
||||||
|
passive::animal::Animal,
|
||||||
player::Player,
|
player::Player,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -29,6 +31,7 @@ const TEMPT_ITEMS: &[&Item] = &[&Item::WHEAT];
|
|||||||
pub struct SheepEntity {
|
pub struct SheepEntity {
|
||||||
pub mob_entity: MobEntity,
|
pub mob_entity: MobEntity,
|
||||||
color_and_sheared: AtomicU8,
|
color_and_sheared: AtomicU8,
|
||||||
|
pub ageable_data: crate::entity::ageable::AgeableData,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SheepEntity {
|
impl SheepEntity {
|
||||||
@ -37,6 +40,7 @@ impl SheepEntity {
|
|||||||
let sheep = Self {
|
let sheep = Self {
|
||||||
mob_entity,
|
mob_entity,
|
||||||
color_and_sheared: AtomicU8::new(0),
|
color_and_sheared: AtomicU8::new(0),
|
||||||
|
ageable_data: crate::entity::ageable::AgeableData::default(),
|
||||||
};
|
};
|
||||||
let mob_arc = Arc::new(sheep);
|
let mob_arc = Arc::new(sheep);
|
||||||
let mob_weak: Weak<dyn Mob> = {
|
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 {
|
impl NBTStorage for SheepEntity {
|
||||||
fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
|
fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
|
||||||
Box::pin(async {
|
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_bool("Sheared", self.is_sheared());
|
||||||
nbt.put_byte("Color", self.get_color() as i8);
|
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, ()> {
|
fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
|
||||||
Box::pin(async {
|
Box::pin(async {
|
||||||
self.mob_entity
|
self.mob_entity.living_entity.read_nbt_non_mut(nbt).await;
|
||||||
.living_entity
|
self.read_ageable_nbt(nbt);
|
||||||
.entity
|
self.read_animal_nbt(nbt);
|
||||||
.read_nbt_non_mut(nbt)
|
let sheared = nbt
|
||||||
.await;
|
.get_bool("Sheared")
|
||||||
let sheared = nbt.get_bool("Sheared").unwrap_or(false);
|
.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 color = nbt.get_byte("Color").unwrap_or(0) as u8;
|
||||||
let byte = (color & 0x0F) | if sheared { 0x10 } else { 0 };
|
let byte = (color & 0x0F) | if sheared { 0x10 } else { 0 };
|
||||||
self.color_and_sheared.store(byte, Ordering::Relaxed);
|
self.color_and_sheared.store(byte, Ordering::Relaxed);
|
||||||
@ -133,7 +146,11 @@ impl NBTStorage for SheepEntity {
|
|||||||
|
|
||||||
impl super::animal::Animal for SheepEntity {
|
impl super::animal::Animal for SheepEntity {
|
||||||
fn is_food(&self, item_stack: &ItemStack) -> bool {
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,24 +1,93 @@
|
|||||||
use std::sync::{Arc, Weak};
|
use std::sync::{
|
||||||
|
Arc, Weak,
|
||||||
use pumpkin_data::entity::EntityType;
|
atomic::{AtomicI32, Ordering},
|
||||||
|
|
||||||
use crate::entity::{
|
|
||||||
Entity, NBTStorage,
|
|
||||||
ai::goal::{
|
|
||||||
look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal, swim::SwimGoal,
|
|
||||||
wander_around::WanderAroundGoal,
|
|
||||||
},
|
|
||||||
mob::{Mob, MobEntity},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 struct SnifferEntity {
|
||||||
pub mob_entity: MobEntity,
|
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 {
|
impl SnifferEntity {
|
||||||
pub fn new(entity: Entity) -> Arc<Self> {
|
pub fn new(entity: Entity) -> Arc<Self> {
|
||||||
let mob_entity = MobEntity::new(entity);
|
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_arc = Arc::new(sniffer);
|
||||||
let mob_weak: Weak<dyn Mob> = {
|
let mob_weak: Weak<dyn Mob> = {
|
||||||
let mob_arc: Arc<dyn Mob> = mob_arc.clone();
|
let mob_arc: Arc<dyn Mob> = mob_arc.clone();
|
||||||
@ -33,22 +102,334 @@ impl SnifferEntity {
|
|||||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||||
|
|
||||||
goal_selector.add_goal(0, Box::new(SwimGoal::default()));
|
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(
|
goal_selector.add_goal(
|
||||||
2,
|
5,
|
||||||
LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 6.0),
|
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
|
mob_arc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn get_state(&self) -> SnifferState {
|
||||||
|
SnifferState::from_id(self.state.load(Ordering::Relaxed))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_state(&self, state: SnifferState) {
|
||||||
|
self.state.store(state.id(), Ordering::Relaxed);
|
||||||
|
let entity = self.get_entity();
|
||||||
|
entity.send_meta_data(
|
||||||
|
&[Metadata::new(
|
||||||
|
TrackedData::STATE,
|
||||||
|
MetaDataType::SNIFFER_STATE,
|
||||||
|
VarInt(state.id()),
|
||||||
|
)],
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn transition_to(&self, state: SnifferState) {
|
||||||
|
let entity = self.get_entity();
|
||||||
|
let world = entity.world.load();
|
||||||
|
let pos = entity.pos.load();
|
||||||
|
|
||||||
|
match state {
|
||||||
|
SnifferState::Idling => {
|
||||||
|
self.set_state(SnifferState::Idling);
|
||||||
|
}
|
||||||
|
SnifferState::FeelingHappy => {
|
||||||
|
world.play_sound(Sound::EntitySnifferHappy, SoundCategory::Neutral, &pos);
|
||||||
|
self.set_state(SnifferState::FeelingHappy);
|
||||||
|
}
|
||||||
|
SnifferState::Scenting => {
|
||||||
|
self.set_state(SnifferState::Scenting);
|
||||||
|
self.on_scenting_start();
|
||||||
|
}
|
||||||
|
SnifferState::Sniffing => {
|
||||||
|
world.play_sound(Sound::EntitySnifferSniffing, SoundCategory::Neutral, &pos);
|
||||||
|
self.set_state(SnifferState::Sniffing);
|
||||||
|
}
|
||||||
|
SnifferState::Searching => {
|
||||||
|
self.set_state(SnifferState::Searching);
|
||||||
|
}
|
||||||
|
SnifferState::Digging => {
|
||||||
|
self.set_state(SnifferState::Digging);
|
||||||
|
self.on_digging_start();
|
||||||
|
}
|
||||||
|
SnifferState::Rising => {
|
||||||
|
world.play_sound(
|
||||||
|
Sound::EntitySnifferDiggingStop,
|
||||||
|
SoundCategory::Neutral,
|
||||||
|
&pos,
|
||||||
|
);
|
||||||
|
self.set_state(SnifferState::Rising);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_scenting_start(&self) {
|
||||||
|
let entity = self.get_entity();
|
||||||
|
let world = entity.world.load();
|
||||||
|
let pos = entity.pos.load();
|
||||||
|
let pitch = if self.is_baby() { 1.3 } else { 1.0 };
|
||||||
|
world.play_sound_fine(
|
||||||
|
Sound::EntitySnifferScenting,
|
||||||
|
SoundCategory::Neutral,
|
||||||
|
&pos,
|
||||||
|
1.0,
|
||||||
|
pitch,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_digging_start(&self) {
|
||||||
|
let entity = self.get_entity();
|
||||||
|
let world = entity.world.load();
|
||||||
|
let current_ticks = world.level_time.try_lock().map_or(0, |t| t.world_age);
|
||||||
|
let drop_tick = current_ticks as i32 + DIGGING_DROP_SEED_OFFSET_TICKS;
|
||||||
|
self.drop_seed_at_tick.store(drop_tick, Ordering::Relaxed);
|
||||||
|
entity.send_meta_data(
|
||||||
|
&[Metadata::new(
|
||||||
|
TrackedData::DROP_SEED_AT_TICK,
|
||||||
|
MetaDataType::INT,
|
||||||
|
VarInt(drop_tick),
|
||||||
|
)],
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
world.send_entity_status(entity, EntityStatus::SnifferDiggingSound, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_digging_complete(&self, success: bool) {
|
||||||
|
if success {
|
||||||
|
let head_block = self.get_head_block().down();
|
||||||
|
self.store_explored_position(head_block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn get_head_position(&self) -> Vector3<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 NBTStorage for SnifferEntity {}
|
impl AgeableMob for SnifferEntity {
|
||||||
|
fn get_ageable_data(&self) -> &AgeableData {
|
||||||
|
&self.ageable_data
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_baby_start_age(&self) -> i32 {
|
||||||
|
SNIFFER_BABY_START_AGE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NBTStorage for SnifferEntity {
|
||||||
|
fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
self.mob_entity.living_entity.write_nbt(nbt).await;
|
||||||
|
self.write_ageable_nbt(nbt);
|
||||||
|
self.write_animal_nbt(nbt);
|
||||||
|
nbt.put_int("State", self.get_state().id());
|
||||||
|
nbt.put_int(
|
||||||
|
"DropSeedAtTick",
|
||||||
|
self.drop_seed_at_tick.load(Ordering::Relaxed),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
self.mob_entity.living_entity.read_nbt_non_mut(nbt).await;
|
||||||
|
self.read_ageable_nbt(nbt);
|
||||||
|
self.read_animal_nbt(nbt);
|
||||||
|
if let Some(state_id) = nbt.get_int("State") {
|
||||||
|
self.state.store(state_id, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
if let Some(drop_tick) = nbt.get_int("DropSeedAtTick") {
|
||||||
|
self.drop_seed_at_tick.store(drop_tick, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Animal for SnifferEntity {
|
||||||
|
fn is_food(&self, item_stack: &ItemStack) -> bool {
|
||||||
|
item_stack.item.has_tag(&tag::Item::MINECRAFT_SNIFFER_FOOD)
|
||||||
|
|| SNIFFER_FOOD.iter().any(|i| i.id == item_stack.item.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Mob for SnifferEntity {
|
impl Mob for SnifferEntity {
|
||||||
fn get_mob_entity(&self) -> &MobEntity {
|
fn get_mob_entity(&self) -> &MobEntity {
|
||||||
&self.mob_entity
|
&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
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,29 +1,43 @@
|
|||||||
use std::sync::{
|
use std::sync::{
|
||||||
Arc, Weak,
|
Arc, Weak,
|
||||||
atomic::{AtomicU8, Ordering},
|
atomic::{AtomicBool, AtomicU8, Ordering},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crossbeam::atomic::AtomicCell;
|
||||||
use pumpkin_data::entity::EntityType;
|
use pumpkin_data::entity::EntityType;
|
||||||
use pumpkin_data::item::Item;
|
use pumpkin_data::item::Item;
|
||||||
|
use pumpkin_data::item_stack::ItemStack;
|
||||||
use pumpkin_data::meta_data_type::MetaDataType;
|
use pumpkin_data::meta_data_type::MetaDataType;
|
||||||
|
use pumpkin_data::tag::{self, Taggable};
|
||||||
use pumpkin_data::tracked_data::TrackedData;
|
use pumpkin_data::tracked_data::TrackedData;
|
||||||
use pumpkin_nbt::compound::NbtCompound;
|
use pumpkin_nbt::compound::NbtCompound;
|
||||||
use pumpkin_protocol::codec::var_int::VarInt;
|
use pumpkin_protocol::codec::var_int::VarInt;
|
||||||
use pumpkin_protocol::java::client::play::Metadata;
|
use pumpkin_protocol::java::client::play::Metadata;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::entity::{
|
use crate::entity::{
|
||||||
Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture,
|
Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture,
|
||||||
|
ageable::AgeableMob,
|
||||||
ai::goal::{
|
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,
|
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},
|
mob::{Mob, MobEntity},
|
||||||
|
passive::animal::Animal,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct WolfEntity {
|
pub struct WolfEntity {
|
||||||
pub mob_entity: MobEntity,
|
pub mob_entity: MobEntity,
|
||||||
pub variant: AtomicU8,
|
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 {
|
impl WolfEntity {
|
||||||
@ -31,7 +45,12 @@ impl WolfEntity {
|
|||||||
let mob_entity = MobEntity::new(entity);
|
let mob_entity = MobEntity::new(entity);
|
||||||
let wolf = Self {
|
let wolf = Self {
|
||||||
mob_entity,
|
mob_entity,
|
||||||
variant: AtomicU8::new(3), // Default to pale
|
variant: AtomicU8::new(3), // Default to pale
|
||||||
|
collar_color: AtomicU8::new(14), // Default to red
|
||||||
|
is_tame: AtomicBool::new(false),
|
||||||
|
is_sitting: AtomicBool::new(false),
|
||||||
|
owner: AtomicCell::new(None),
|
||||||
|
ageable_data: crate::entity::ageable::AgeableData::default(),
|
||||||
};
|
};
|
||||||
let mob_arc = Arc::new(wolf);
|
let mob_arc = Arc::new(wolf);
|
||||||
let mob_weak: Weak<dyn Mob> = {
|
let mob_weak: Weak<dyn Mob> = {
|
||||||
@ -46,29 +65,108 @@ impl WolfEntity {
|
|||||||
.lock()
|
.lock()
|
||||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
.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(1, Box::new(SwimGoal::default()));
|
||||||
// goal_selector.add_goal(2, SitGoal::new(mob_arc.clone()));
|
// 1: EscapeDangerGoal (TamableAnimalPanicGoal)
|
||||||
goal_selector.add_goal(4, EscapeDangerGoal::new(1.5));
|
goal_selector.add_goal(1, EscapeDangerGoal::new(1.5));
|
||||||
goal_selector.add_goal(5, BreedGoal::new(1.0));
|
// 3: Avoid Llama
|
||||||
// goal_selector.add_goal(6, FollowOwnerGoal::new(1.0, 10.0, 2.0, false));
|
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(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(
|
goal_selector.add_goal(
|
||||||
10,
|
10,
|
||||||
LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 8.0),
|
LookAtEntityGoal::with_default(mob_weak, &EntityType::PLAYER, 8.0),
|
||||||
);
|
);
|
||||||
goal_selector.add_goal(10, Box::new(RandomLookAroundGoal::default()));
|
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
|
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 {
|
impl NBTStorage for WolfEntity {
|
||||||
fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
|
fn write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
|
||||||
Box::pin(async {
|
Box::pin(async {
|
||||||
self.mob_entity.living_entity.write_nbt(nbt).await;
|
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) {
|
let variant_str = match self.variant.load(Ordering::Relaxed) {
|
||||||
0 => "minecraft:ashen",
|
0 => "minecraft:ashen",
|
||||||
1 => "minecraft:black",
|
1 => "minecraft:black",
|
||||||
@ -81,12 +179,23 @@ impl NBTStorage for WolfEntity {
|
|||||||
_ => "minecraft:pale",
|
_ => "minecraft:pale",
|
||||||
};
|
};
|
||||||
nbt.put_string("variant", variant_str.to_string());
|
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, ()> {
|
fn read_nbt_non_mut<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
|
||||||
Box::pin(async {
|
Box::pin(async {
|
||||||
self.mob_entity.living_entity.read_nbt_non_mut(nbt).await;
|
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") {
|
if let Some(variant_str) = nbt.get_string("variant") {
|
||||||
let variant = match variant_str
|
let variant = match variant_str
|
||||||
.strip_prefix("minecraft:")
|
.strip_prefix("minecraft:")
|
||||||
@ -104,6 +213,20 @@ impl NBTStorage for WolfEntity {
|
|||||||
};
|
};
|
||||||
self.variant.store(variant, Ordering::Relaxed);
|
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
|
&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) {
|
fn mob_set_variant_name(&self, name: &str) {
|
||||||
let variant = match name.strip_prefix("minecraft:").unwrap_or(name) {
|
let variant = match name.strip_prefix("minecraft:").unwrap_or(name) {
|
||||||
"ashen" => 0,
|
"ashen" => 0,
|
||||||
@ -142,6 +273,22 @@ impl Mob for WolfEntity {
|
|||||||
None,
|
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(
|
entity.send_meta_data(
|
||||||
&[Metadata::new(
|
&[Metadata::new(
|
||||||
TrackedData::WOLF_VARIANT_ID,
|
TrackedData::WOLF_VARIANT_ID,
|
||||||
@ -150,6 +297,14 @@ impl Mob for WolfEntity {
|
|||||||
)],
|
)],
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
|
entity.send_meta_data(
|
||||||
|
&[Metadata::new(
|
||||||
|
TrackedData::OWNER_UUID,
|
||||||
|
MetaDataType::OPTIONAL_UUID,
|
||||||
|
self.owner.load(),
|
||||||
|
)],
|
||||||
|
None,
|
||||||
|
);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
115
crates/pumpkin/src/entity/projectile/evoker_fangs.rs
Normal file
115
crates/pumpkin/src/entity/projectile/evoker_fangs.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,58 +1,243 @@
|
|||||||
use std::sync::Arc;
|
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::{
|
use crate::{
|
||||||
entity::{
|
entity::{
|
||||||
Entity, EntityBase, EntityBaseFuture, NBTStorage,
|
Entity, EntityBase, EntityBaseFuture, NBTStorage, NbtFuture,
|
||||||
projectile::{ProjectileHit, ThrownItemEntity},
|
projectile::{ProjectileHit, ThrownItemEntity},
|
||||||
|
projectile_deflection::ProjectileDeflectionType,
|
||||||
},
|
},
|
||||||
server::Server,
|
server::Server,
|
||||||
};
|
};
|
||||||
|
|
||||||
const EXPLOSION_POWER: f32 = 1.0;
|
pub const MIN_CAMERA_DISTANCE_SQUARED: f32 = 12.25;
|
||||||
const GRAVITY: f64 = 0.0;
|
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 struct FireballEntity {
|
||||||
pub thrown: ThrownItemEntity,
|
pub thrown: ThrownItemEntity,
|
||||||
pub explosion_power: f32,
|
pub item_stack: RwLock<ItemStack>,
|
||||||
|
pub acceleration_power: AtomicU64,
|
||||||
|
pub explosion_power: AtomicF32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FireballEntity {
|
impl FireballEntity {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn new(entity: Entity) -> Self {
|
pub fn new(entity: Entity) -> Self {
|
||||||
let thrown = ThrownItemEntity {
|
let thrown = ThrownItemEntity {
|
||||||
entity,
|
entity,
|
||||||
owner_id: None,
|
owner_id: None,
|
||||||
collides_with_projectiles: false,
|
collides_with_projectiles: false,
|
||||||
has_hit: AtomicBool::new(false),
|
has_hit: AtomicBool::new(false),
|
||||||
gravity: GRAVITY,
|
gravity: 0.0,
|
||||||
};
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
thrown,
|
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]
|
#[must_use]
|
||||||
pub fn new_shot(entity: Entity, shooter: &Entity) -> Self {
|
pub fn new_shot(entity: Entity, shooter: &Entity, direction: Vector3<f64>) -> Self {
|
||||||
let thrown = ThrownItemEntity::new(entity, shooter, GRAVITY);
|
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 {
|
Self {
|
||||||
thrown,
|
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 {
|
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>(
|
fn tick<'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
caller: &'a Arc<dyn EntityBase>,
|
caller: &'a Arc<dyn EntityBase>,
|
||||||
server: &'a Server,
|
server: &'a Server,
|
||||||
) -> EntityBaseFuture<'a, ()> {
|
) -> 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 {
|
fn get_entity(&self) -> &Entity {
|
||||||
@ -75,12 +260,10 @@ impl EntityBase for FireballEntity {
|
|||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let world = self.get_entity().world.load();
|
let world = self.get_entity().world.load();
|
||||||
|
|
||||||
// Handle entity/block hit
|
|
||||||
if let ProjectileHit::Entity { ref entity, .. } = hit {
|
if let ProjectileHit::Entity { ref entity, .. } = hit {
|
||||||
let entity_clone = entity.clone();
|
let entity_clone = entity.clone();
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
// Fireball does 6.0 damage in vanilla
|
|
||||||
entity_clone.get_entity().set_on_fire_for(5.0);
|
entity_clone.get_entity().set_on_fire_for(5.0);
|
||||||
let _ = entity_clone
|
let _ = entity_clone
|
||||||
.damage(
|
.damage(
|
||||||
@ -93,8 +276,7 @@ impl EntityBase for FireballEntity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let hit_pos = hit.hit_pos();
|
let hit_pos = hit.hit_pos();
|
||||||
// Explosion sets fire if mob griefing is enabled (assuming true for now)
|
world.explode(hit_pos, self.get_explosion_power()).await;
|
||||||
world.explode(hit_pos, self.explosion_power).await;
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ use std::{
|
|||||||
pub mod arrow;
|
pub mod arrow;
|
||||||
pub mod egg;
|
pub mod egg;
|
||||||
pub mod ender_pearl;
|
pub mod ender_pearl;
|
||||||
|
pub mod evoker_fangs;
|
||||||
pub mod eye_of_ender;
|
pub mod eye_of_ender;
|
||||||
pub mod fireball;
|
pub mod fireball;
|
||||||
pub mod firework_rocket;
|
pub mod firework_rocket;
|
||||||
|
|||||||
@ -8,13 +8,19 @@ use uuid::Uuid;
|
|||||||
use crate::entity::boss::ender_dragon::EnderDragonEntity;
|
use crate::entity::boss::ender_dragon::EnderDragonEntity;
|
||||||
use crate::entity::boss::wither::WitherEntity;
|
use crate::entity::boss::wither::WitherEntity;
|
||||||
use crate::entity::decoration::{
|
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,
|
painting::PaintingEntity,
|
||||||
};
|
};
|
||||||
use crate::entity::experience_orb::ExperienceOrbEntity;
|
use crate::entity::experience_orb::ExperienceOrbEntity;
|
||||||
use crate::entity::falling::FallingEntity;
|
use crate::entity::falling::FallingEntity;
|
||||||
|
use crate::entity::interaction::InteractionEntity;
|
||||||
use crate::entity::item::ItemEntity;
|
use crate::entity::item::ItemEntity;
|
||||||
|
use crate::entity::lightning::LightningBoltEntity;
|
||||||
use crate::entity::living::LivingEntity;
|
use crate::entity::living::LivingEntity;
|
||||||
|
use crate::entity::marker::MarkerEntity;
|
||||||
use crate::entity::mob::bat::{self, BatEntity};
|
use crate::entity::mob::bat::{self, BatEntity};
|
||||||
use crate::entity::mob::blaze::BlazeEntity;
|
use crate::entity::mob::blaze::BlazeEntity;
|
||||||
use crate::entity::mob::breeze::BreezeEntity;
|
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::cat::CatEntity;
|
||||||
use crate::entity::passive::chicken::ChickenEntity;
|
use crate::entity::passive::chicken::ChickenEntity;
|
||||||
use crate::entity::passive::cod::CodEntity;
|
use crate::entity::passive::cod::CodEntity;
|
||||||
|
use crate::entity::passive::copper_golem::CopperGolemEntity;
|
||||||
use crate::entity::passive::cow::CowEntity;
|
use crate::entity::passive::cow::CowEntity;
|
||||||
use crate::entity::passive::dolphin::DolphinEntity;
|
use crate::entity::passive::dolphin::DolphinEntity;
|
||||||
use crate::entity::passive::donkey::DonkeyEntity;
|
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::frog::FrogEntity;
|
||||||
use crate::entity::passive::glow_squid::GlowSquidEntity;
|
use crate::entity::passive::glow_squid::GlowSquidEntity;
|
||||||
use crate::entity::passive::goat::GoatEntity;
|
use crate::entity::passive::goat::GoatEntity;
|
||||||
|
use crate::entity::passive::happy_ghast::HappyGhastEntity;
|
||||||
use crate::entity::passive::horse::HorseEntity;
|
use crate::entity::passive::horse::HorseEntity;
|
||||||
use crate::entity::passive::iron_golem::IronGolemEntity;
|
use crate::entity::passive::iron_golem::IronGolemEntity;
|
||||||
use crate::entity::passive::llama::LlamaEntity;
|
use crate::entity::passive::llama::LlamaEntity;
|
||||||
use crate::entity::passive::mooshroom::MooshroomEntity;
|
use crate::entity::passive::mooshroom::MooshroomEntity;
|
||||||
use crate::entity::passive::mule::MuleEntity;
|
use crate::entity::passive::mule::MuleEntity;
|
||||||
|
use crate::entity::passive::nautilus::NautilusEntity;
|
||||||
use crate::entity::passive::ocelot::OcelotEntity;
|
use crate::entity::passive::ocelot::OcelotEntity;
|
||||||
use crate::entity::passive::panda::PandaEntity;
|
use crate::entity::passive::panda::PandaEntity;
|
||||||
use crate::entity::passive::parrot::ParrotEntity;
|
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::AXOLOTL.id => AxolotlEntity::new(entity),
|
||||||
id if id == EntityType::BEE.id => BeeEntity::new(entity),
|
id if id == EntityType::BEE.id => BeeEntity::new(entity),
|
||||||
id if id == EntityType::CAMEL.id => CamelEntity::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::FROG.id => FrogEntity::new(entity),
|
||||||
id if id == EntityType::GOAT.id => GoatEntity::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::MOOSHROOM.id => MooshroomEntity::new(entity),
|
||||||
id if id == EntityType::OCELOT.id => OcelotEntity::new(entity),
|
id if id == EntityType::OCELOT.id => OcelotEntity::new(entity),
|
||||||
id if id == EntityType::PANDA.id => PandaEntity::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::TROPICAL_FISH.id => TropicalFishEntity::new(entity),
|
||||||
id if id == EntityType::TADPOLE.id => TadpoleEntity::new(entity),
|
id if id == EntityType::TADPOLE.id => TadpoleEntity::new(entity),
|
||||||
id if id == EntityType::DOLPHIN.id => DolphinEntity::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::SNOW_GOLEM.id => SnowGolemEntity::new(entity),
|
||||||
id if id == EntityType::IRON_GOLEM.id => IronGolemEntity::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)
|
crate::entity::area_effect_cloud::AreaEffectCloudEntity::new(entity)
|
||||||
}
|
}
|
||||||
id if id == EntityType::ARMOR_STAND.id => Arc::new(ArmorStandEntity::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::PAINTING.id => Arc::new(PaintingEntity::new(entity)),
|
||||||
id if id == EntityType::ITEM_FRAME.id || id == EntityType::GLOW_ITEM_FRAME.id => {
|
id if id == EntityType::ITEM_FRAME.id || id == EntityType::GLOW_ITEM_FRAME.id => {
|
||||||
Arc::new(ItemFrameEntity::new(entity))
|
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::END_CRYSTAL.id => Arc::new(EndCrystalEntity::new(entity)),
|
||||||
id if id == EntityType::ENDER_PEARL.id => Arc::new(EnderPearlEntity::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)),
|
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))
|
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::FIREWORK_ROCKET.id => Arc::new(FireworkRocketEntity::new(entity)),
|
||||||
id if id == EntityType::SPLASH_POTION.id => Arc::new(SplashPotionEntity::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)),
|
id if id == EntityType::LINGERING_POTION.id => Arc::new(LingeringPotionEntity::new(entity)),
|
||||||
|
|||||||
@ -5011,15 +5011,40 @@ impl World {
|
|||||||
self.spawn_entity(item_entity).await;
|
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 pumpkin_data::entity::EntityType;
|
||||||
use uuid::Uuid;
|
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(
|
let lightning = crate::entity::r#type::from_type(
|
||||||
&EntityType::LIGHTNING_BOLT,
|
&EntityType::LIGHTNING_BOLT,
|
||||||
pos,
|
pos,
|
||||||
self,
|
self,
|
||||||
Uuid::new_v4(),
|
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;
|
self.spawn_entity(lightning).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user