fix: respawn in nether
This commit is contained in:
parent
dafc2b2000
commit
d8c7c23c0b
@ -55,7 +55,7 @@ impl BlockBehaviour for RespawnAnchorBlock {
|
|||||||
fn normal_use<'a>(&'a self, args: NormalUseArgs<'a>) -> BlockFuture<'a, BlockActionResult> {
|
fn normal_use<'a>(&'a self, args: NormalUseArgs<'a>) -> BlockFuture<'a, BlockActionResult> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let state_id = args.world.get_block_state_id(args.position);
|
let state_id = args.world.get_block_state_id(args.position);
|
||||||
let mut props = RespawnAnchorLikeProperties::from_state_id(state_id, args.block);
|
let props = RespawnAnchorLikeProperties::from_state_id(state_id, args.block);
|
||||||
|
|
||||||
if args.world.dimension != Dimension::THE_NETHER {
|
if args.world.dimension != Dimension::THE_NETHER {
|
||||||
args.world
|
args.world
|
||||||
@ -92,15 +92,6 @@ impl BlockBehaviour for RespawnAnchorBlock {
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
props.charges -= 1;
|
|
||||||
args.world
|
|
||||||
.set_block_state(
|
|
||||||
args.position,
|
|
||||||
props.to_state_id(args.block),
|
|
||||||
BlockFlags::NOTIFY_ALL,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
args.world.play_sound(
|
args.world.play_sound(
|
||||||
Sound::BlockRespawnAnchorSetSpawn,
|
Sound::BlockRespawnAnchorSetSpawn,
|
||||||
SoundCategory::Blocks,
|
SoundCategory::Blocks,
|
||||||
|
|||||||
@ -1,18 +1,37 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::block::entities::mob_spawner::MobSpawnerBlockEntity;
|
use crate::block::entities::mob_spawner::MobSpawnerBlockEntity;
|
||||||
|
use crate::entity::experience_orb::ExperienceOrbEntity;
|
||||||
use pumpkin_macros::pumpkin_block;
|
use pumpkin_macros::pumpkin_block;
|
||||||
|
use pumpkin_util::GameMode;
|
||||||
|
|
||||||
use crate::block::{BlockBehaviour, BlockFuture, PlacedArgs};
|
use crate::block::{BlockBehaviour, BlockFuture, BrokenArgs, OnSyncedBlockEventArgs, PlacedArgs};
|
||||||
|
|
||||||
#[pumpkin_block("minecraft:spawner")]
|
#[pumpkin_block("minecraft:spawner")]
|
||||||
pub struct SpawnerBlock;
|
pub struct SpawnerBlock;
|
||||||
|
|
||||||
impl BlockBehaviour for SpawnerBlock {
|
impl BlockBehaviour for SpawnerBlock {
|
||||||
|
fn on_synced_block_event<'a>(
|
||||||
|
&'a self,
|
||||||
|
_args: OnSyncedBlockEventArgs<'a>,
|
||||||
|
) -> BlockFuture<'a, bool> {
|
||||||
|
Box::pin(async move { true })
|
||||||
|
}
|
||||||
|
|
||||||
fn placed<'a>(&'a self, args: PlacedArgs<'a>) -> BlockFuture<'a, ()> {
|
fn placed<'a>(&'a self, args: PlacedArgs<'a>) -> BlockFuture<'a, ()> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let hopper_block_entity = MobSpawnerBlockEntity::new(*args.position, None);
|
let spawner_block_entity = MobSpawnerBlockEntity::new(*args.position, None);
|
||||||
args.world.add_block_entity(Arc::new(hopper_block_entity));
|
args.world.add_block_entity(Arc::new(spawner_block_entity));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn broken<'a>(&'a self, args: BrokenArgs<'a>) -> BlockFuture<'a, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
if args.player.gamemode.load() != GameMode::Creative {
|
||||||
|
let xp_count = 15 + rand::random_range(0..15) + rand::random_range(0..15);
|
||||||
|
ExperienceOrbEntity::spawn(args.world, args.position.to_centered_f64(), xp_count)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,7 @@ use pumpkin_util::math::{
|
|||||||
vector3::Vector3,
|
vector3::Vector3,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{block::entities::BlockEntity, world::World};
|
use crate::{block::entities::BlockEntity, entity::EntityBase, world::World};
|
||||||
|
|
||||||
pub struct MobSpawnerBlockEntity {
|
pub struct MobSpawnerBlockEntity {
|
||||||
pub position: BlockPos,
|
pub position: BlockPos,
|
||||||
@ -24,6 +24,8 @@ pub struct MobSpawnerBlockEntity {
|
|||||||
pub min_delay: i32,
|
pub min_delay: i32,
|
||||||
pub spawn_count: i32,
|
pub spawn_count: i32,
|
||||||
pub spawn_range: i32,
|
pub spawn_range: i32,
|
||||||
|
pub max_nearby_entities: i32,
|
||||||
|
pub required_player_range: i32,
|
||||||
pub entity_type: AtomicCell<Option<&'static EntityType>>,
|
pub entity_type: AtomicCell<Option<&'static EntityType>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,6 +36,8 @@ impl MobSpawnerBlockEntity {
|
|||||||
pub const DEFAULT_MIN_SPAWN_DELAY: i32 = 200;
|
pub const DEFAULT_MIN_SPAWN_DELAY: i32 = 200;
|
||||||
pub const DEFAULT_SPAWN_COUNT: i32 = 4;
|
pub const DEFAULT_SPAWN_COUNT: i32 = 4;
|
||||||
pub const DEFAULT_SPAWN_RANGE: i32 = 4;
|
pub const DEFAULT_SPAWN_RANGE: i32 = 4;
|
||||||
|
pub const DEFAULT_MAX_NEARBY_ENTITIES: i32 = 6;
|
||||||
|
pub const DEFAULT_REQUIRED_PLAYER_RANGE: i32 = 16;
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn new(position: BlockPos, entity_type: Option<&'static EntityType>) -> Self {
|
pub const fn new(position: BlockPos, entity_type: Option<&'static EntityType>) -> Self {
|
||||||
@ -44,6 +48,8 @@ impl MobSpawnerBlockEntity {
|
|||||||
min_delay: Self::DEFAULT_MIN_SPAWN_DELAY,
|
min_delay: Self::DEFAULT_MIN_SPAWN_DELAY,
|
||||||
spawn_count: Self::DEFAULT_SPAWN_COUNT,
|
spawn_count: Self::DEFAULT_SPAWN_COUNT,
|
||||||
spawn_range: Self::DEFAULT_SPAWN_RANGE,
|
spawn_range: Self::DEFAULT_SPAWN_RANGE,
|
||||||
|
max_nearby_entities: Self::DEFAULT_MAX_NEARBY_ENTITIES,
|
||||||
|
required_player_range: Self::DEFAULT_REQUIRED_PLAYER_RANGE,
|
||||||
entity_type: AtomicCell::new(entity_type),
|
entity_type: AtomicCell::new(entity_type),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -55,6 +61,14 @@ impl MobSpawnerBlockEntity {
|
|||||||
nbt.put_int("x", position.0.x);
|
nbt.put_int("x", position.0.x);
|
||||||
nbt.put_int("y", position.0.y);
|
nbt.put_int("y", position.0.y);
|
||||||
nbt.put_int("z", position.0.z);
|
nbt.put_int("z", position.0.z);
|
||||||
|
nbt.put_short("Delay", self.delay.load(Ordering::Relaxed) as i16);
|
||||||
|
nbt.put_short("MinSpawnDelay", self.min_delay as i16);
|
||||||
|
nbt.put_short("MaxSpawnDelay", self.max_delay as i16);
|
||||||
|
nbt.put_short("SpawnCount", self.spawn_count as i16);
|
||||||
|
nbt.put_short("SpawnRange", self.spawn_range as i16);
|
||||||
|
nbt.put_short("MaxNearbyEntities", self.max_nearby_entities as i16);
|
||||||
|
nbt.put_short("RequiredPlayerRange", self.required_player_range as i16);
|
||||||
|
|
||||||
if let Some(entity_type) = self.entity_type.load() {
|
if let Some(entity_type) = self.entity_type.load() {
|
||||||
let mut spawn_entry = NbtCompound::new();
|
let mut spawn_entry = NbtCompound::new();
|
||||||
|
|
||||||
@ -101,24 +115,60 @@ impl BlockEntity for MobSpawnerBlockEntity {
|
|||||||
fn tick<'a>(&'a self, world: &'a Arc<World>) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
|
fn tick<'a>(&'a self, world: &'a Arc<World>) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
if let Some(entity_type) = &self.entity_type.load() {
|
if let Some(entity_type) = &self.entity_type.load() {
|
||||||
if self.delay.load(Ordering::Relaxed) == -1 {
|
let center = self.position.to_centered_f64();
|
||||||
|
let max_player_dist_sq = (self.required_player_range as f64).powi(2);
|
||||||
|
let player_nearby = world.players.load().iter().any(|p| {
|
||||||
|
p.get_entity().pos.load().squared_distance_to_vec(¢er) <= max_player_dist_sq
|
||||||
|
});
|
||||||
|
|
||||||
|
if !player_nearby {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.delay.load(Ordering::Relaxed) < 0 {
|
||||||
self.update_spawns(world).await;
|
self.update_spawns(world).await;
|
||||||
} else {
|
return;
|
||||||
|
}
|
||||||
|
if self.delay.load(Ordering::Relaxed) > 0 {
|
||||||
self.delay.fetch_sub(1, Ordering::Relaxed);
|
self.delay.fetch_sub(1, Ordering::Relaxed);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let search_radius_horiz = (self.spawn_range * 2) as f64;
|
||||||
|
let search_radius_vert = 4.0;
|
||||||
|
let nearby_count = world
|
||||||
|
.entities
|
||||||
|
.load()
|
||||||
|
.iter()
|
||||||
|
.filter(|e| {
|
||||||
|
let ent = e.get_entity();
|
||||||
|
if ent.entity_type.id != entity_type.id {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let pos = ent.pos.load();
|
||||||
|
(pos.x - center.x).abs() <= search_radius_horiz
|
||||||
|
&& (pos.z - center.z).abs() <= search_radius_horiz
|
||||||
|
&& (pos.y - center.y).abs() <= search_radius_vert
|
||||||
|
})
|
||||||
|
.count();
|
||||||
|
|
||||||
|
if nearby_count as i32 >= self.max_nearby_entities {
|
||||||
|
self.update_spawns(world).await;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let spawn_range = self.spawn_range;
|
let spawn_range = self.spawn_range;
|
||||||
let mut update_spawns = false;
|
let mut spawned_any = false;
|
||||||
for _ in 0..self.spawn_count {
|
for _ in 0..self.spawn_count {
|
||||||
let pos = self.position.0;
|
let pos = self.position.0;
|
||||||
|
|
||||||
let spawn_pos = Vector3::new(
|
let spawn_pos = Vector3::new(
|
||||||
pos.x as f64
|
pos.x as f64
|
||||||
+ (rand::random::<f64>() + rand::random::<f64>()) * spawn_range as f64
|
+ (rand::random::<f64>() - rand::random::<f64>()) * spawn_range as f64
|
||||||
+ 0.5,
|
+ 0.5,
|
||||||
(pos.y + rand::random_range(0..3) - 1) as f64,
|
(pos.y + rand::random_range(0..3) - 1) as f64,
|
||||||
pos.z as f64
|
pos.z as f64
|
||||||
+ (rand::random::<f64>() + rand::random::<f64>()) * spawn_range as f64
|
+ (rand::random::<f64>() - rand::random::<f64>()) * spawn_range as f64
|
||||||
+ 0.5,
|
+ 0.5,
|
||||||
);
|
);
|
||||||
// TODO: we should use getSpawnBox, but this is only modified for slimes and magma slimes
|
// TODO: we should use getSpawnBox, but this is only modified for slimes and magma slimes
|
||||||
@ -140,11 +190,13 @@ impl BlockEntity for MobSpawnerBlockEntity {
|
|||||||
world,
|
world,
|
||||||
uuid::Uuid::new_v4(),
|
uuid::Uuid::new_v4(),
|
||||||
);
|
);
|
||||||
|
let yaw = rand::random::<f32>() * 360.0;
|
||||||
|
entity.get_entity().set_rotation(yaw, 0.0);
|
||||||
world.spawn_entity(entity).await;
|
world.spawn_entity(entity).await;
|
||||||
world.sync_world_event(WorldEvent::ParticlesMobblockSpawn, self.position, 0);
|
world.sync_world_event(WorldEvent::ParticlesMobblockSpawn, self.position, 0);
|
||||||
update_spawns = true;
|
spawned_any = true;
|
||||||
}
|
}
|
||||||
if update_spawns {
|
if spawned_any {
|
||||||
self.update_spawns(world).await;
|
self.update_spawns(world).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -155,28 +207,51 @@ impl BlockEntity for MobSpawnerBlockEntity {
|
|||||||
where
|
where
|
||||||
Self: Sized,
|
Self: Sized,
|
||||||
{
|
{
|
||||||
let delay = nbt.get_short("Delay").unwrap_or(Self::DEFAULT_DELAY as i16) as i32;
|
let get_num = |name: &str| {
|
||||||
let min_delay = nbt
|
nbt.get_short(name)
|
||||||
.get_int("MinSpawnDelay")
|
.map(i32::from)
|
||||||
.unwrap_or(Self::DEFAULT_MIN_SPAWN_DELAY);
|
.or_else(|| nbt.get_int(name))
|
||||||
let max_delay = nbt
|
.or_else(|| nbt.get_byte(name).map(i32::from))
|
||||||
.get_int("MaxSpawnDelay")
|
};
|
||||||
.unwrap_or(Self::DEFAULT_MAX_SPAWN_DELAY);
|
|
||||||
let spawn_count = nbt
|
let delay = get_num("Delay").unwrap_or(Self::DEFAULT_DELAY);
|
||||||
.get_int("SpawnCount")
|
let min_delay = get_num("MinSpawnDelay").unwrap_or(Self::DEFAULT_MIN_SPAWN_DELAY);
|
||||||
.unwrap_or(Self::DEFAULT_SPAWN_COUNT);
|
let max_delay = get_num("MaxSpawnDelay").unwrap_or(Self::DEFAULT_MAX_SPAWN_DELAY);
|
||||||
let spawn_range = nbt
|
let spawn_count = get_num("SpawnCount").unwrap_or(Self::DEFAULT_SPAWN_COUNT);
|
||||||
.get_int("SpawnRange")
|
let spawn_range = get_num("SpawnRange").unwrap_or(Self::DEFAULT_SPAWN_RANGE);
|
||||||
.unwrap_or(Self::DEFAULT_SPAWN_RANGE);
|
let max_nearby_entities =
|
||||||
|
get_num("MaxNearbyEntities").unwrap_or(Self::DEFAULT_MAX_NEARBY_ENTITIES);
|
||||||
|
let required_player_range =
|
||||||
|
get_num("RequiredPlayerRange").unwrap_or(Self::DEFAULT_REQUIRED_PLAYER_RANGE);
|
||||||
|
|
||||||
let entity_type = nbt
|
let entity_type = nbt
|
||||||
.get_compound("SpawnData")
|
.get_compound("SpawnData")
|
||||||
.and_then(|data| data.get_compound("entity"))
|
.and_then(|data| {
|
||||||
|
data.get_compound("entity")
|
||||||
.and_then(|entity| entity.get_string("id"))
|
.and_then(|entity| entity.get_string("id"))
|
||||||
.and_then(|id| {
|
.or_else(|| data.get_string("id"))
|
||||||
let name = id.strip_prefix("minecraft:").unwrap_or(id);
|
})
|
||||||
EntityType::from_name(name)
|
.or_else(|| {
|
||||||
});
|
nbt.get_list("SpawnPotentials")
|
||||||
|
.and_then(|list| list.first())
|
||||||
|
.and_then(|tag| tag.extract_compound())
|
||||||
|
.and_then(|entry| {
|
||||||
|
entry
|
||||||
|
.get_compound("data")
|
||||||
|
.and_then(|data| {
|
||||||
|
data.get_compound("entity")
|
||||||
|
.and_then(|entity| entity.get_string("id"))
|
||||||
|
.or_else(|| data.get_string("id"))
|
||||||
|
})
|
||||||
|
.or_else(|| {
|
||||||
|
entry
|
||||||
|
.get_compound("entity")
|
||||||
|
.and_then(|entity| entity.get_string("id"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.or_else(|| nbt.get_string("EntityId"))
|
||||||
|
.and_then(EntityType::from_name);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
position,
|
position,
|
||||||
@ -185,6 +260,8 @@ impl BlockEntity for MobSpawnerBlockEntity {
|
|||||||
min_delay,
|
min_delay,
|
||||||
spawn_count,
|
spawn_count,
|
||||||
spawn_range,
|
spawn_range,
|
||||||
|
max_nearby_entities,
|
||||||
|
required_player_range,
|
||||||
entity_type: AtomicCell::new(entity_type),
|
entity_type: AtomicCell::new(entity_type),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -200,6 +277,14 @@ impl BlockEntity for MobSpawnerBlockEntity {
|
|||||||
|
|
||||||
fn chunk_data_nbt(&self) -> Option<NbtCompound> {
|
fn chunk_data_nbt(&self) -> Option<NbtCompound> {
|
||||||
let mut final_nbt = NbtCompound::new();
|
let mut final_nbt = NbtCompound::new();
|
||||||
|
final_nbt.put_short("Delay", self.delay.load(Ordering::Relaxed) as i16);
|
||||||
|
final_nbt.put_short("MinSpawnDelay", self.min_delay as i16);
|
||||||
|
final_nbt.put_short("MaxSpawnDelay", self.max_delay as i16);
|
||||||
|
final_nbt.put_short("SpawnCount", self.spawn_count as i16);
|
||||||
|
final_nbt.put_short("SpawnRange", self.spawn_range as i16);
|
||||||
|
final_nbt.put_short("MaxNearbyEntities", self.max_nearby_entities as i16);
|
||||||
|
final_nbt.put_short("RequiredPlayerRange", self.required_player_range as i16);
|
||||||
|
|
||||||
if let Some(entity_type) = self.entity_type.load() {
|
if let Some(entity_type) = self.entity_type.load() {
|
||||||
let mut spawn_entry = NbtCompound::new();
|
let mut spawn_entry = NbtCompound::new();
|
||||||
|
|
||||||
|
|||||||
@ -30,6 +30,7 @@ pub mod pathfind_to_raid;
|
|||||||
pub mod pick_up_block;
|
pub mod pick_up_block;
|
||||||
pub mod place_block;
|
pub mod place_block;
|
||||||
pub mod ranged_attack;
|
pub mod ranged_attack;
|
||||||
|
pub mod ranged_crossbow_attack;
|
||||||
pub mod revenge;
|
pub mod revenge;
|
||||||
pub mod step_and_destroy_block;
|
pub mod step_and_destroy_block;
|
||||||
pub mod swim;
|
pub mod swim;
|
||||||
|
|||||||
298
crates/pumpkin/src/entity/ai/goal/ranged_crossbow_attack.rs
Normal file
298
crates/pumpkin/src/entity/ai/goal/ranged_crossbow_attack.rs
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use pumpkin_data::data_component_impl::EquipmentSlot;
|
||||||
|
use pumpkin_data::entity::EntityType;
|
||||||
|
use pumpkin_data::item::Item;
|
||||||
|
use pumpkin_data::item_stack::ItemStack;
|
||||||
|
use pumpkin_data::sound::{Sound, SoundCategory};
|
||||||
|
use pumpkin_util::Hand;
|
||||||
|
|
||||||
|
use crate::entity::ai::goal::{Controls, Goal, GoalFuture};
|
||||||
|
use crate::entity::ai::pathfinder::NavigatorGoal;
|
||||||
|
use crate::entity::mob::Mob;
|
||||||
|
use crate::entity::projectile::arrow::{ArrowEntity, ArrowPickup};
|
||||||
|
use crate::entity::{Entity, EntityBase};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
enum CrossbowState {
|
||||||
|
Uncharged,
|
||||||
|
Charging,
|
||||||
|
Charged,
|
||||||
|
ReadyToAttack,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ranged crossbow attack used by Pillagers, Piglins, and other mobs.
|
||||||
|
/// Mirrors vanilla `RangedCrossbowAttackGoal`.
|
||||||
|
pub struct RangedCrossbowAttackGoal {
|
||||||
|
goal_control: Controls,
|
||||||
|
speed: f64,
|
||||||
|
squared_range: f64,
|
||||||
|
state: CrossbowState,
|
||||||
|
see_time: i32,
|
||||||
|
attack_delay: i32,
|
||||||
|
update_path_delay: i32,
|
||||||
|
charge_ticks: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RangedCrossbowAttackGoal {
|
||||||
|
/// Vanilla crossbow charge duration (ticks).
|
||||||
|
const CHARGE_DURATION: i32 = 25;
|
||||||
|
/// Vanilla arrow speed for crossbow shots.
|
||||||
|
const ARROW_SPEED: f64 = 1.6;
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(speed: f64, range: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
goal_control: Controls::MOVE | Controls::LOOK,
|
||||||
|
speed,
|
||||||
|
squared_range: f64::from(range * range),
|
||||||
|
state: CrossbowState::Uncharged,
|
||||||
|
see_time: 0,
|
||||||
|
attack_delay: 0,
|
||||||
|
update_path_delay: 0,
|
||||||
|
charge_ticks: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn is_holding_crossbow(mob: &dyn Mob) -> bool {
|
||||||
|
let equipment = mob
|
||||||
|
.get_mob_entity()
|
||||||
|
.living_entity
|
||||||
|
.entity_equipment
|
||||||
|
.lock()
|
||||||
|
.await;
|
||||||
|
equipment.get(&EquipmentSlot::MAIN_HAND).item.id == Item::CROSSBOW.id
|
||||||
|
|| equipment.get(&EquipmentSlot::OFF_HAND).item.id == Item::CROSSBOW.id
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn shoot(mob: &dyn Mob, target: &Arc<dyn EntityBase>) {
|
||||||
|
let entity = mob.get_entity();
|
||||||
|
let world = entity.world.load();
|
||||||
|
|
||||||
|
let mut event =
|
||||||
|
crate::plugin::api::events::entity::entity_shoot_bow::EntityShootBowEvent::new(
|
||||||
|
entity.entity_id,
|
||||||
|
"minecraft:crossbow".to_string(),
|
||||||
|
1.0,
|
||||||
|
);
|
||||||
|
if let Some(server) = world.server.upgrade() {
|
||||||
|
server.plugin_manager.fire(&server, &mut event).await;
|
||||||
|
}
|
||||||
|
if event.cancelled {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mob_pos = entity.pos.load();
|
||||||
|
let target_entity = target.get_entity();
|
||||||
|
let target_pos = target_entity.pos.load();
|
||||||
|
|
||||||
|
let arrow_entity = Entity::new(world.clone(), mob_pos, &EntityType::ARROW);
|
||||||
|
let projectile = ItemStack::new(1, &Item::ARROW);
|
||||||
|
let arrow = ArrowEntity::new_shot(arrow_entity, entity, &projectile, ArrowPickup::Allowed);
|
||||||
|
|
||||||
|
let dx = target_pos.x - mob_pos.x;
|
||||||
|
let dy = (target_pos.y + f64::from(target_entity.entity_dimension.load().height) / 3.0)
|
||||||
|
- arrow.entity.pos.load().y;
|
||||||
|
let dz = target_pos.z - mob_pos.z;
|
||||||
|
let horizontal_distance = dx.hypot(dz);
|
||||||
|
|
||||||
|
let difficulty = world.level_info.load().difficulty as i32;
|
||||||
|
let divergence = f64::from(14 - difficulty * 4);
|
||||||
|
|
||||||
|
arrow.set_velocity(
|
||||||
|
dx,
|
||||||
|
horizontal_distance.mul_add(0.2, dy),
|
||||||
|
dz,
|
||||||
|
Self::ARROW_SPEED,
|
||||||
|
divergence,
|
||||||
|
);
|
||||||
|
|
||||||
|
world.play_sound(Sound::ItemCrossbowShoot, SoundCategory::Hostile, &mob_pos);
|
||||||
|
|
||||||
|
let arrow: Arc<dyn EntityBase> = Arc::new(arrow);
|
||||||
|
world.spawn_entity(arrow).await;
|
||||||
|
|
||||||
|
if let Some(crossbow_mob) = mob.as_crossbow_attack_mob() {
|
||||||
|
crossbow_mob.on_crossbow_attack_performed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Goal for RangedCrossbowAttackGoal {
|
||||||
|
fn can_start<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> {
|
||||||
|
Box::pin(async move {
|
||||||
|
let target = mob.get_mob_entity().target.lock().await.clone();
|
||||||
|
let Some(target) = target else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if !target.get_entity().is_alive() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Self::is_holding_crossbow(mob).await
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_continue<'a>(&'a self, mob: &'a dyn Mob) -> GoalFuture<'a, bool> {
|
||||||
|
Box::pin(async move {
|
||||||
|
let target = mob.get_mob_entity().target.lock().await.clone();
|
||||||
|
let Some(target) = target else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
target.get_entity().is_alive() && Self::is_holding_crossbow(mob).await
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
self.state = CrossbowState::Uncharged;
|
||||||
|
self.see_time = 0;
|
||||||
|
self.attack_delay = 0;
|
||||||
|
self.update_path_delay = 0;
|
||||||
|
self.charge_ticks = 0;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stop<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
if let Some(crossbow_mob) = mob.as_crossbow_attack_mob() {
|
||||||
|
crossbow_mob.set_charging_crossbow(false);
|
||||||
|
}
|
||||||
|
mob.get_mob_entity().living_entity.clear_active_hand().await;
|
||||||
|
self.state = CrossbowState::Uncharged;
|
||||||
|
mob.get_mob_entity()
|
||||||
|
.navigator
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||||
|
.stop();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[expect(clippy::too_many_lines)]
|
||||||
|
fn tick<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
let target = mob.get_mob_entity().target.lock().await.clone();
|
||||||
|
let Some(target) = target else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mob_pos = mob.get_entity().pos.load();
|
||||||
|
let target_pos = target.get_entity().pos.load();
|
||||||
|
let distance_sq = mob_pos.squared_distance_to_vec(&target_pos);
|
||||||
|
|
||||||
|
let has_line_of_sight = true; // In future: raycast check
|
||||||
|
if has_line_of_sight {
|
||||||
|
self.see_time += 1;
|
||||||
|
} else {
|
||||||
|
self.see_time = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let needs_to_move =
|
||||||
|
(distance_sq > self.squared_range || self.see_time < 5) && self.attack_delay == 0;
|
||||||
|
|
||||||
|
if needs_to_move {
|
||||||
|
self.update_path_delay -= 1;
|
||||||
|
if self.update_path_delay <= 0 {
|
||||||
|
let move_speed = if self.state == CrossbowState::Uncharged {
|
||||||
|
self.speed
|
||||||
|
} else {
|
||||||
|
self.speed * 0.5
|
||||||
|
};
|
||||||
|
mob.get_mob_entity()
|
||||||
|
.navigator
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||||
|
.set_progress(NavigatorGoal {
|
||||||
|
current_progress: mob_pos,
|
||||||
|
destination: target_pos,
|
||||||
|
speed: move_speed,
|
||||||
|
});
|
||||||
|
self.update_path_delay = 20 + rand::random_range(0..20);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.update_path_delay = 0;
|
||||||
|
mob.get_mob_entity()
|
||||||
|
.navigator
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||||
|
.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
mob.get_mob_entity()
|
||||||
|
.look_control
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||||
|
.look_at_entity_with_range(&target, 30.0, 30.0);
|
||||||
|
|
||||||
|
match self.state {
|
||||||
|
CrossbowState::Uncharged => {
|
||||||
|
if !needs_to_move {
|
||||||
|
let stack = mob
|
||||||
|
.get_mob_entity()
|
||||||
|
.living_entity
|
||||||
|
.entity_equipment
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.get(&EquipmentSlot::MAIN_HAND);
|
||||||
|
mob.get_mob_entity()
|
||||||
|
.living_entity
|
||||||
|
.set_active_hand(Hand::Right, stack, i32::MAX)
|
||||||
|
.await;
|
||||||
|
self.state = CrossbowState::Charging;
|
||||||
|
self.charge_ticks = 0;
|
||||||
|
if let Some(crossbow_mob) = mob.as_crossbow_attack_mob() {
|
||||||
|
crossbow_mob.set_charging_crossbow(true);
|
||||||
|
}
|
||||||
|
mob.get_entity().world.load().play_sound(
|
||||||
|
Sound::ItemCrossbowLoadingStart,
|
||||||
|
SoundCategory::Hostile,
|
||||||
|
&mob_pos,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CrossbowState::Charging => {
|
||||||
|
self.charge_ticks += 1;
|
||||||
|
if self.charge_ticks == 10 {
|
||||||
|
mob.get_entity().world.load().play_sound(
|
||||||
|
Sound::ItemCrossbowLoadingMiddle,
|
||||||
|
SoundCategory::Hostile,
|
||||||
|
&mob_pos,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if self.charge_ticks >= Self::CHARGE_DURATION {
|
||||||
|
mob.get_mob_entity().living_entity.clear_active_hand().await;
|
||||||
|
self.state = CrossbowState::Charged;
|
||||||
|
self.attack_delay = 20 + rand::random_range(0..20);
|
||||||
|
if let Some(crossbow_mob) = mob.as_crossbow_attack_mob() {
|
||||||
|
crossbow_mob.set_charging_crossbow(false);
|
||||||
|
}
|
||||||
|
mob.get_entity().world.load().play_sound(
|
||||||
|
Sound::ItemCrossbowLoadingEnd,
|
||||||
|
SoundCategory::Hostile,
|
||||||
|
&mob_pos,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CrossbowState::Charged => {
|
||||||
|
self.attack_delay -= 1;
|
||||||
|
if self.attack_delay <= 0 {
|
||||||
|
self.state = CrossbowState::ReadyToAttack;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CrossbowState::ReadyToAttack => {
|
||||||
|
if has_line_of_sight {
|
||||||
|
Self::shoot(mob, &target).await;
|
||||||
|
self.state = CrossbowState::Uncharged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_run_every_tick(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn controls(&self) -> Controls {
|
||||||
|
self.goal_control
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,9 +2,11 @@ use std::sync::Arc;
|
|||||||
use std::sync::atomic::{AtomicBool, AtomicI32, Ordering::Relaxed};
|
use std::sync::atomic::{AtomicBool, AtomicI32, Ordering::Relaxed};
|
||||||
|
|
||||||
use pumpkin_data::damage::DamageType;
|
use pumpkin_data::damage::DamageType;
|
||||||
use pumpkin_data::sound::Sound;
|
use pumpkin_data::sound::{Sound, SoundCategory};
|
||||||
use pumpkin_data::tag::{self, Taggable};
|
use pumpkin_data::tag::{self, Taggable};
|
||||||
|
use pumpkin_data::tracked_data;
|
||||||
use pumpkin_nbt::compound::NbtCompound;
|
use pumpkin_nbt::compound::NbtCompound;
|
||||||
|
use pumpkin_protocol::java::client::play::Metadata;
|
||||||
use pumpkin_util::math::position::BlockPos;
|
use pumpkin_util::math::position::BlockPos;
|
||||||
use pumpkin_util::math::vector3::Vector3;
|
use pumpkin_util::math::vector3::Vector3;
|
||||||
use pumpkin_world::chunk::ChunkHeightmapType;
|
use pumpkin_world::chunk::ChunkHeightmapType;
|
||||||
@ -33,111 +35,104 @@ impl BatEntity {
|
|||||||
let bat = Self {
|
let bat = Self {
|
||||||
mob_entity,
|
mob_entity,
|
||||||
hanging_position: Mutex::new(None),
|
hanging_position: Mutex::new(None),
|
||||||
roosting: AtomicBool::new(false),
|
roosting: AtomicBool::new(true),
|
||||||
ambient_sound_chance: AtomicI32::new(MIN_AMBIENT_SOUND_DELAY),
|
ambient_sound_chance: AtomicI32::new(MIN_AMBIENT_SOUND_DELAY),
|
||||||
};
|
};
|
||||||
Arc::new(bat)
|
Arc::new(bat)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn check_bat_spawn_rules(world: &World, pos: &BlockPos) -> bool {
|
pub fn check_bat_spawn_rules(world: &World, pos: &BlockPos) -> bool {
|
||||||
if pos.0.y >= world.get_heightmap_height(ChunkHeightmapType::WorldSurface, pos.0.x, pos.0.z)
|
if pos.0.y >= world.get_heightmap_height(ChunkHeightmapType::WorldSurface, pos.0.x, pos.0.z)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if rand::random_bool(1.0) {
|
if rand::random_bool(0.5) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if world.get_max_local_raw_brightness(pos) > rand::random_range(0..4) {
|
if world.get_max_local_raw_brightness(pos) > rand::random_range(0..4) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if world
|
let below_pos = BlockPos::new(pos.0.x, pos.0.y - 1, pos.0.z);
|
||||||
.get_block(pos)
|
if !world
|
||||||
|
.get_block(&below_pos)
|
||||||
.has_tag(&tag::Block::MINECRAFT_BATS_SPAWNABLE_ON)
|
.has_tag(&tag::Block::MINECRAFT_BATS_SPAWNABLE_ON)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
//TODO:check_mob_spawn_rules(entity_type, world, spawn_reason, pos).await
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn is_roosting(&self) -> bool {
|
pub fn is_roosting(&self) -> bool {
|
||||||
self.roosting.load(Relaxed)
|
self.roosting.load(Relaxed)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_roosting(&self, roosting: bool) {
|
pub fn set_roosting(&self, roosting: bool) {
|
||||||
self.roosting.store(roosting, Relaxed);
|
self.roosting.store(roosting, Relaxed);
|
||||||
// TODO:
|
let flags: u8 = if roosting { ROOSTING_FLAG } else { 0 };
|
||||||
// let flags = if roosting { ROOSTING_FLAG } else { 0 };
|
self.mob_entity.living_entity.entity.send_meta_data(
|
||||||
// self.mob_entity
|
&[Metadata::new(tracked_data::bat::DATA_ID_FLAGS, flags)],
|
||||||
// .living_entity
|
None,
|
||||||
// .entity
|
);
|
||||||
// .send_meta_data(&[Metadata::new(
|
|
||||||
// pumpkin_data::tracked_data::bat::ID_FLAGS,
|
|
||||||
// MetaDataType::BYTE,
|
|
||||||
// flags,
|
|
||||||
// )])
|
|
||||||
// .await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Mob for BatEntity {
|
|
||||||
fn mob_write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
|
|
||||||
Box::pin(async move {
|
|
||||||
let flags: u8 = if self.is_roosting() { ROOSTING_FLAG } else { 0 };
|
|
||||||
nbt.put_byte("BatFlags", flags as i8);
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mob_read_nbt<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
|
fn tick_ambient_sound(&self, world: &World, pos: &Vector3<f64>) {
|
||||||
Box::pin(async move {
|
|
||||||
let flags = nbt.get_byte("BatFlags").unwrap_or(0) as u8;
|
|
||||||
let roosting = (flags & ROOSTING_FLAG) != 0;
|
|
||||||
self.set_roosting(roosting);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_mob_entity(&self) -> &MobEntity {
|
|
||||||
&self.mob_entity
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mob_tick<'a>(&'a self, _caller: &'a Arc<dyn EntityBase>) -> EntityBaseFuture<'a, ()> {
|
|
||||||
Box::pin(async move {
|
|
||||||
let entity = &self.mob_entity.living_entity.entity;
|
|
||||||
let block_pos = entity.block_pos.load();
|
|
||||||
let above_pos = BlockPos::new(block_pos.0.x, block_pos.0.y + 1, block_pos.0.z);
|
|
||||||
let world = entity.world.load();
|
|
||||||
|
|
||||||
// Ambient idle sound (vanilla: MobEntity.mobTick → playAmbientSound)
|
|
||||||
let chance = self.ambient_sound_chance.fetch_sub(1, Relaxed);
|
let chance = self.ambient_sound_chance.fetch_sub(1, Relaxed);
|
||||||
if chance <= 0 {
|
if chance <= 0 {
|
||||||
self.ambient_sound_chance
|
self.ambient_sound_chance
|
||||||
.store(MIN_AMBIENT_SOUND_DELAY, Relaxed);
|
.store(MIN_AMBIENT_SOUND_DELAY, Relaxed);
|
||||||
entity.play_sound(Sound::EntityBatAmbient);
|
if !self.is_roosting() || rand::random_range(0..4) == 0 {
|
||||||
|
world.play_sound_fine(
|
||||||
|
Sound::EntityBatAmbient,
|
||||||
|
SoundCategory::Ambient,
|
||||||
|
pos,
|
||||||
|
0.1,
|
||||||
|
0.95,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.is_roosting() {
|
fn tick_roosting(&self, world: &World, above_pos: &BlockPos, pos: &Vector3<f64>) {
|
||||||
let above_state = world.get_block_state(&above_pos);
|
let entity = &self.mob_entity.living_entity.entity;
|
||||||
|
let above_state = world.get_block_state(above_pos);
|
||||||
if above_state.is_solid_block() {
|
if above_state.is_solid_block() {
|
||||||
let rotate_head = {
|
let rotate_head = {
|
||||||
let mut rng = rand::rng();
|
let mut rng = rand::rng();
|
||||||
(rng.random_range(0u32..200) == 0)
|
(rng.random_range(0u32..200) == 0).then(|| rng.random_range(0i32..360) as f32)
|
||||||
.then(|| rng.random_range(0i32..360) as f32)
|
|
||||||
};
|
};
|
||||||
if let Some(head_yaw) = rotate_head {
|
if let Some(head_yaw) = rotate_head {
|
||||||
entity.head_yaw.store(head_yaw);
|
entity.head_yaw.store(head_yaw);
|
||||||
}
|
}
|
||||||
|
|
||||||
let pos = entity.pos.load();
|
|
||||||
if world
|
if world
|
||||||
.get_closest_player(pos, CLOSE_PLAYER_DISTANCE)
|
.get_closest_player(*pos, CLOSE_PLAYER_DISTANCE)
|
||||||
.is_some()
|
.is_some()
|
||||||
{
|
{
|
||||||
self.set_roosting(false);
|
self.set_roosting(false);
|
||||||
|
world.play_sound_fine(
|
||||||
|
Sound::EntityBatTakeoff,
|
||||||
|
SoundCategory::Ambient,
|
||||||
|
pos,
|
||||||
|
0.1,
|
||||||
|
0.95,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.set_roosting(false);
|
self.set_roosting(false);
|
||||||
|
world.play_sound_fine(
|
||||||
|
Sound::EntityBatTakeoff,
|
||||||
|
SoundCategory::Ambient,
|
||||||
|
pos,
|
||||||
|
0.1,
|
||||||
|
0.95,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
async fn tick_flying(&self, world: &World, above_pos: &BlockPos, pos: &Vector3<f64>) {
|
||||||
|
let entity = &self.mob_entity.living_entity.entity;
|
||||||
let mut hanging_pos = self.hanging_position.lock().await;
|
let mut hanging_pos = self.hanging_position.lock().await;
|
||||||
|
|
||||||
if let Some(hp) = *hanging_pos {
|
if let Some(hp) = *hanging_pos {
|
||||||
@ -152,14 +147,12 @@ impl Mob for BatEntity {
|
|||||||
let should_pick = hanging_pos.is_none()
|
let should_pick = hanging_pos.is_none()
|
||||||
|| rng.random_range(0u32..30) == 0
|
|| rng.random_range(0u32..30) == 0
|
||||||
|| hanging_pos.is_some_and(|hp| {
|
|| hanging_pos.is_some_and(|hp| {
|
||||||
let pos = entity.pos.load();
|
|
||||||
let dx = f64::from(hp.0.x) + 0.5 - pos.x;
|
let dx = f64::from(hp.0.x) + 0.5 - pos.x;
|
||||||
let dy = f64::from(hp.0.y) + 0.1 - pos.y;
|
let dy = f64::from(hp.0.y) + 0.1 - pos.y;
|
||||||
let dz = f64::from(hp.0.z) + 0.5 - pos.z;
|
let dz = f64::from(hp.0.z) + 0.5 - pos.z;
|
||||||
dx * dx + dy * dy + dz * dz < 4.0
|
dx * dx + dy * dy + dz * dz < 4.0
|
||||||
});
|
});
|
||||||
let new_target = should_pick.then(|| {
|
let new_target = should_pick.then(|| {
|
||||||
let pos = entity.pos.load();
|
|
||||||
BlockPos::new(
|
BlockPos::new(
|
||||||
pos.x as i32 + rng.random_range(0i32..7) - rng.random_range(0i32..7),
|
pos.x as i32 + rng.random_range(0i32..7) - rng.random_range(0i32..7),
|
||||||
(pos.y + f64::from(rng.random_range(0i32..6)) - 2.0) as i32,
|
(pos.y + f64::from(rng.random_range(0i32..6)) - 2.0) as i32,
|
||||||
@ -171,7 +164,6 @@ impl Mob for BatEntity {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if should_pick_new {
|
if should_pick_new {
|
||||||
// Pre-validate: only accept targets in air (avoids water, lava, hazards)
|
|
||||||
if let Some(target) = new_target {
|
if let Some(target) = new_target {
|
||||||
let target_state = world.get_block_state(&target);
|
let target_state = world.get_block_state(&target);
|
||||||
if target_state.is_air() && target.0.y > world.dimension.min_y {
|
if target_state.is_air() && target.0.y > world.dimension.min_y {
|
||||||
@ -185,7 +177,6 @@ impl Mob for BatEntity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(target) = *hanging_pos {
|
if let Some(target) = *hanging_pos {
|
||||||
let pos = entity.pos.load();
|
|
||||||
let d = f64::from(target.0.x) + 0.5 - pos.x;
|
let d = f64::from(target.0.x) + 0.5 - pos.x;
|
||||||
let e = f64::from(target.0.y) + 0.1 - pos.y;
|
let e = f64::from(target.0.y) + 0.1 - pos.y;
|
||||||
let f = f64::from(target.0.z) + 0.5 - pos.z;
|
let f = f64::from(target.0.z) + 0.5 - pos.z;
|
||||||
@ -205,12 +196,60 @@ impl Mob for BatEntity {
|
|||||||
drop(hanging_pos);
|
drop(hanging_pos);
|
||||||
|
|
||||||
if try_roost {
|
if try_roost {
|
||||||
let above_state = world.get_block_state(&above_pos);
|
let above_state = world.get_block_state(above_pos);
|
||||||
if above_state.is_solid_block() {
|
if above_state.is_solid_block() {
|
||||||
self.set_roosting(true);
|
self.set_roosting(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mob for BatEntity {
|
||||||
|
fn mob_init_data_tracker(&self) -> EntityBaseFuture<'_, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
let entity = self.get_entity();
|
||||||
|
let flags: u8 = if self.is_roosting() { ROOSTING_FLAG } else { 0 };
|
||||||
|
entity.send_meta_data(
|
||||||
|
&[Metadata::new(tracked_data::bat::DATA_ID_FLAGS, flags)],
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mob_write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
let flags: u8 = if self.is_roosting() { ROOSTING_FLAG } else { 0 };
|
||||||
|
nbt.put_byte("BatFlags", i8::try_from(flags).unwrap_or(0));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mob_read_nbt<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
let flags = u8::try_from(nbt.get_byte("BatFlags").unwrap_or(0)).unwrap_or(0);
|
||||||
|
let roosting = (flags & ROOSTING_FLAG) != 0;
|
||||||
|
self.set_roosting(roosting);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_mob_entity(&self) -> &MobEntity {
|
||||||
|
&self.mob_entity
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mob_tick<'a>(&'a self, _caller: &'a Arc<dyn EntityBase>) -> EntityBaseFuture<'a, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
let entity = &self.mob_entity.living_entity.entity;
|
||||||
|
let block_pos = entity.block_pos.load();
|
||||||
|
let above_pos = BlockPos::new(block_pos.0.x, block_pos.0.y + 1, block_pos.0.z);
|
||||||
|
let world = entity.world.load();
|
||||||
|
let pos = entity.pos.load();
|
||||||
|
|
||||||
|
self.tick_ambient_sound(&world, &pos);
|
||||||
|
|
||||||
|
if self.is_roosting() {
|
||||||
|
self.tick_roosting(&world, &above_pos, &pos);
|
||||||
|
} else {
|
||||||
|
self.tick_flying(&world, &above_pos, &pos).await;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -242,6 +281,15 @@ impl Mob for BatEntity {
|
|||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
if self.is_roosting() {
|
if self.is_roosting() {
|
||||||
self.set_roosting(false);
|
self.set_roosting(false);
|
||||||
|
let entity = &self.mob_entity.living_entity.entity;
|
||||||
|
let pos = entity.pos.load();
|
||||||
|
entity.world.load().play_sound_fine(
|
||||||
|
Sound::EntityBatTakeoff,
|
||||||
|
SoundCategory::Ambient,
|
||||||
|
&pos,
|
||||||
|
0.1,
|
||||||
|
0.95,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
5
crates/pumpkin/src/entity/mob/crossbow_attack_mob.rs
Normal file
5
crates/pumpkin/src/entity/mob/crossbow_attack_mob.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
pub trait CrossbowAttackMob: Send + Sync {
|
||||||
|
fn set_charging_crossbow(&self, is_charging: bool);
|
||||||
|
fn is_charging_crossbow(&self) -> bool;
|
||||||
|
fn on_crossbow_attack_performed(&self) {}
|
||||||
|
}
|
||||||
@ -753,7 +753,7 @@ fn conflicts_with(candidate: &Enchantment, applied: &[&Enchantment]) -> bool {
|
|||||||
/// enchantments by weight, resolves exclusive-set conflicts, and determines
|
/// enchantments by weight, resolves exclusive-set conflicts, and determines
|
||||||
/// the level from the remaining cost. Cost is halved each iteration so
|
/// the level from the remaining cost. Cost is halved each iteration so
|
||||||
/// later enchantments receive lower levels.
|
/// later enchantments receive lower levels.
|
||||||
fn apply_vanilla_enchantments(
|
pub fn apply_vanilla_enchantments(
|
||||||
stack: &mut ItemStack,
|
stack: &mut ItemStack,
|
||||||
slot: &EquipmentSlot,
|
slot: &EquipmentSlot,
|
||||||
special_multiplier: f32,
|
special_multiplier: f32,
|
||||||
|
|||||||
@ -1,32 +1,49 @@
|
|||||||
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
|
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
|
||||||
use std::sync::{Arc, Weak};
|
use std::sync::{Arc, Weak};
|
||||||
|
|
||||||
|
use pumpkin_data::damage::DamageType;
|
||||||
|
use pumpkin_data::entity::EntityType;
|
||||||
|
use pumpkin_data::sound::{Sound, SoundCategory};
|
||||||
|
use pumpkin_data::tracked_data;
|
||||||
|
use pumpkin_nbt::compound::NbtCompound;
|
||||||
|
use pumpkin_protocol::java::client::play::Metadata;
|
||||||
|
use pumpkin_util::math::position::BlockPos;
|
||||||
|
use pumpkin_util::math::vector3::Vector3;
|
||||||
|
use rand::RngExt;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::entity::ai::goal::active_target::ActiveTargetGoal;
|
||||||
|
use crate::entity::living::LivingEntity;
|
||||||
|
use crate::entity::projectile::fireball::FireballEntity;
|
||||||
use crate::entity::{
|
use crate::entity::{
|
||||||
Entity,
|
Entity, EntityBase, EntityBaseFuture, NbtFuture,
|
||||||
ai::goal::{Controls, Goal, GoalFuture},
|
ai::goal::{Controls, Goal, GoalFuture},
|
||||||
mob::{Mob, MobEntity},
|
mob::{Mob, MobEntity},
|
||||||
};
|
};
|
||||||
|
use crate::world::World;
|
||||||
|
|
||||||
pub struct GhastEntity {
|
pub struct GhastEntity {
|
||||||
pub mob_entity: MobEntity,
|
pub mob_entity: MobEntity,
|
||||||
pub is_charging: AtomicBool,
|
pub is_charging: AtomicBool,
|
||||||
pub explosion_power: AtomicU8,
|
pub explosion_power: AtomicU8,
|
||||||
|
pub wanted_fly_target: Mutex<Option<Vector3<f64>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GhastEntity {
|
impl GhastEntity {
|
||||||
|
pub const DEFAULT_EXPLOSION_POWER: u8 = 1;
|
||||||
|
pub const XP_REWARD: u32 = 5;
|
||||||
|
pub const FLYING_SPEED: f64 = 0.06;
|
||||||
|
|
||||||
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 ghast = Self {
|
let ghast = Self {
|
||||||
mob_entity,
|
mob_entity,
|
||||||
is_charging: AtomicBool::new(false),
|
is_charging: AtomicBool::new(false),
|
||||||
explosion_power: AtomicU8::new(1),
|
explosion_power: AtomicU8::new(Self::DEFAULT_EXPLOSION_POWER),
|
||||||
|
wanted_fly_target: Mutex::new(None),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mob_arc = Arc::new(ghast);
|
let mob_arc = Arc::new(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
|
let mut goal_selector = mob_arc
|
||||||
@ -35,20 +52,83 @@ impl GhastEntity {
|
|||||||
.lock()
|
.lock()
|
||||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||||
|
|
||||||
goal_selector.add_goal(7, Box::new(GhastLookGoal::new(mob_weak.clone())));
|
// Priority 5: Random floating around
|
||||||
|
goal_selector.add_goal(
|
||||||
|
5,
|
||||||
|
Box::new(RandomFloatAroundGoal::new(Arc::downgrade(&mob_arc))),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Priority 7: Face target / movement direction
|
||||||
|
goal_selector.add_goal(7, Box::new(GhastLookGoal::new()));
|
||||||
|
|
||||||
|
// Priority 7: Shoot large fireballs at target
|
||||||
|
goal_selector.add_goal(
|
||||||
|
7,
|
||||||
|
Box::new(GhastShootFireballGoal::new(Arc::downgrade(&mob_arc))),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut target_selector = mob_arc
|
||||||
|
.mob_entity
|
||||||
|
.target_selector
|
||||||
|
.lock()
|
||||||
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||||
|
|
||||||
|
// Priority 1: Target nearest player within vertical proximity
|
||||||
|
target_selector.add_goal(
|
||||||
|
1,
|
||||||
|
Box::new(ActiveTargetGoal::new(
|
||||||
|
&mob_arc.mob_entity,
|
||||||
|
&EntityType::PLAYER,
|
||||||
|
10,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
Some(|_target: Arc<LivingEntity>, _world: Arc<World>| {
|
||||||
|
Box::pin(async move { true })
|
||||||
|
}),
|
||||||
|
)),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
mob_arc
|
mob_arc
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_charging(&self, charging: bool) {
|
pub fn set_charging(&self, charging: bool) {
|
||||||
// You would also sync this to the client via EntityMetadata here
|
|
||||||
self.is_charging.store(charging, Ordering::Relaxed);
|
self.is_charging.store(charging, Ordering::Relaxed);
|
||||||
|
let entity = &self.mob_entity.living_entity.entity;
|
||||||
|
entity.send_meta_data(
|
||||||
|
&[Metadata::new(
|
||||||
|
tracked_data::ghast::DATA_IS_CHARGING,
|
||||||
|
charging,
|
||||||
|
)],
|
||||||
|
None,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
pub fn is_charging(&self) -> bool {
|
pub fn is_charging(&self) -> bool {
|
||||||
self.is_charging.load(Ordering::Relaxed)
|
self.is_charging.load(Ordering::Relaxed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn get_explosion_power(&self) -> u8 {
|
||||||
|
self.explosion_power.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_explosion_power(&self, power: u8) {
|
||||||
|
self.explosion_power.store(power, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn check_ghast_spawn_rules(world: &World, pos: &BlockPos) -> bool {
|
||||||
|
if world.level_info.load().difficulty == pumpkin_util::Difficulty::Peaceful {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if rand::random_range(0..20) != 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let state = world.get_block_state(pos);
|
||||||
|
state.is_air()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Mob for GhastEntity {
|
impl Mob for GhastEntity {
|
||||||
@ -59,21 +139,69 @@ impl Mob for GhastEntity {
|
|||||||
fn get_mob_gravity(&self) -> f64 {
|
fn get_mob_gravity(&self) -> f64 {
|
||||||
0.0 // Ghasts fly, no gravity applied in standard travel
|
0.0 // Ghasts fly, no gravity applied in standard travel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_mob_y_velocity_drag(&self) -> Option<f64> {
|
||||||
|
Some(0.95)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mob_init_data_tracker(&self) -> EntityBaseFuture<'_, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
let entity = self.get_entity();
|
||||||
|
if self.is_charging() {
|
||||||
|
entity.send_meta_data(
|
||||||
|
&[Metadata::new(tracked_data::ghast::DATA_IS_CHARGING, true)],
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mob_write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
let power = self.get_explosion_power();
|
||||||
|
nbt.put_byte("ExplosionPower", i8::try_from(power).unwrap_or(1));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mob_read_nbt<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
if let Some(power) = nbt.get_byte("ExplosionPower") {
|
||||||
|
self.set_explosion_power(
|
||||||
|
u8::try_from(power).unwrap_or(Self::DEFAULT_EXPLOSION_POWER),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn modify_incoming_damage(&self, amount: f32, damage_type: DamageType) -> f32 {
|
||||||
|
if damage_type.id == DamageType::FIREBALL.id {
|
||||||
|
1000.0
|
||||||
|
} else {
|
||||||
|
amount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_base_experience_reward(&self) -> u32 {
|
||||||
|
Self::XP_REWARD
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[expect(dead_code)]
|
|
||||||
pub struct GhastLookGoal {
|
pub struct GhastLookGoal {
|
||||||
goal_control: Controls,
|
goal_control: Controls,
|
||||||
mob_weak: Weak<dyn Mob>,
|
}
|
||||||
|
|
||||||
|
impl Default for GhastLookGoal {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
goal_control: Controls::LOOK,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GhastLookGoal {
|
impl GhastLookGoal {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new(mob_weak: Weak<dyn Mob>) -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self::default()
|
||||||
goal_control: Controls::LOOK,
|
|
||||||
mob_weak,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,7 +215,7 @@ impl Goal for GhastLookGoal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 mob_entity = mob.get_mob_entity();
|
let mob_entity = mob.get_mob_entity();
|
||||||
let target_opt = mob_entity.target.lock().await.clone();
|
let target_opt = mob_entity.target.lock().await.clone();
|
||||||
|
|
||||||
@ -96,19 +224,18 @@ impl Goal for GhastLookGoal {
|
|||||||
let target_pos = target.get_entity().pos.load();
|
let target_pos = target.get_entity().pos.load();
|
||||||
|
|
||||||
if mob_pos.squared_distance_to_vec(&target_pos) < 4096.0 {
|
if mob_pos.squared_distance_to_vec(&target_pos) < 4096.0 {
|
||||||
let mut look_control = mob_entity
|
let dx = target_pos.x - mob_pos.x;
|
||||||
.look_control
|
let dz = target_pos.z - mob_pos.z;
|
||||||
.lock()
|
let yaw = (-f64::atan2(dx, dz).to_degrees()) as f32;
|
||||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
mob_entity.living_entity.entity.yaw.store(yaw);
|
||||||
look_control.look_at(mob, target_pos.x, target_pos.y, target_pos.z);
|
mob_entity.living_entity.entity.head_yaw.store(yaw);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If no target, face the movement direction
|
|
||||||
let velocity = mob_entity.living_entity.entity.velocity.load();
|
let velocity = mob_entity.living_entity.entity.velocity.load();
|
||||||
if velocity.x != 0.0 || velocity.z != 0.0 {
|
if velocity.x != 0.0 || velocity.z != 0.0 {
|
||||||
let yaw = (-f64::atan2(velocity.x, velocity.z) * (180.0 / std::f64::consts::PI))
|
let yaw = (-f64::atan2(velocity.x, velocity.z).to_degrees()) as f32;
|
||||||
as f32;
|
|
||||||
mob_entity.living_entity.entity.yaw.store(yaw);
|
mob_entity.living_entity.entity.yaw.store(yaw);
|
||||||
|
mob_entity.living_entity.entity.head_yaw.store(yaw);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -118,3 +245,249 @@ impl Goal for GhastLookGoal {
|
|||||||
self.goal_control
|
self.goal_control
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct GhastShootFireballGoal {
|
||||||
|
ghast: Weak<GhastEntity>,
|
||||||
|
charge_time: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GhastShootFireballGoal {
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(ghast: Weak<GhastEntity>) -> Self {
|
||||||
|
Self {
|
||||||
|
ghast,
|
||||||
|
charge_time: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Goal for GhastShootFireballGoal {
|
||||||
|
fn can_start<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, bool> {
|
||||||
|
Box::pin(async move {
|
||||||
|
let Some(ghast) = self.ghast.upgrade() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let target = ghast.mob_entity.target.lock().await.clone();
|
||||||
|
target.is_some_and(|t| t.get_entity().is_alive())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_continue<'a>(&'a self, _mob: &'a dyn Mob) -> GoalFuture<'a, bool> {
|
||||||
|
Box::pin(async move {
|
||||||
|
let Some(ghast) = self.ghast.upgrade() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let target = ghast.mob_entity.target.lock().await.clone();
|
||||||
|
target.is_some_and(|t| t.get_entity().is_alive())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
self.charge_time = 0;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stop<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
if let Some(ghast) = self.ghast.upgrade() {
|
||||||
|
ghast.set_charging(false);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_run_every_tick(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tick<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
let Some(ghast) = self.ghast.upgrade() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let target_opt = ghast.mob_entity.target.lock().await.clone();
|
||||||
|
let Some(target) = target_opt else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let entity = &ghast.mob_entity.living_entity.entity;
|
||||||
|
let ghast_pos = entity.pos.load();
|
||||||
|
let target_pos = target.get_entity().pos.load();
|
||||||
|
let dist_sq = ghast_pos.squared_distance_to_vec(&target_pos);
|
||||||
|
|
||||||
|
if dist_sq < 4096.0 {
|
||||||
|
let world = entity.world.load();
|
||||||
|
self.charge_time += 1;
|
||||||
|
|
||||||
|
if self.charge_time == 10 {
|
||||||
|
world.play_sound_fine(
|
||||||
|
Sound::EntityGhastWarn,
|
||||||
|
SoundCategory::Hostile,
|
||||||
|
&ghast_pos,
|
||||||
|
5.0,
|
||||||
|
1.0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.charge_time == 20 {
|
||||||
|
world.play_sound_fine(
|
||||||
|
Sound::EntityGhastShoot,
|
||||||
|
SoundCategory::Hostile,
|
||||||
|
&ghast_pos,
|
||||||
|
5.0,
|
||||||
|
1.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
let yaw_rad = f64::from(entity.yaw.load()).to_radians();
|
||||||
|
let pitch_rad = f64::from(entity.pitch.load()).to_radians();
|
||||||
|
let view_x = -pitch_rad.cos() * yaw_rad.sin();
|
||||||
|
let view_z = pitch_rad.cos() * yaw_rad.cos();
|
||||||
|
|
||||||
|
let spawn_pos = Vector3::new(
|
||||||
|
ghast_pos.x + view_x * 4.0,
|
||||||
|
ghast_pos.y + 2.5,
|
||||||
|
ghast_pos.z + view_z * 4.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
let target_y = target_pos.y + target.get_entity().get_eye_height() * 0.5;
|
||||||
|
let dir_x = target_pos.x - spawn_pos.x;
|
||||||
|
let dir_y = target_y - spawn_pos.y;
|
||||||
|
let dir_z = target_pos.z - spawn_pos.z;
|
||||||
|
let direction = Vector3::new(dir_x, dir_y, dir_z);
|
||||||
|
|
||||||
|
let fireball_base = Entity::from_uuid(
|
||||||
|
uuid::Uuid::new_v4(),
|
||||||
|
world.clone(),
|
||||||
|
spawn_pos,
|
||||||
|
&EntityType::FIREBALL,
|
||||||
|
);
|
||||||
|
|
||||||
|
let fireball = FireballEntity::new_shot(fireball_base, entity, direction);
|
||||||
|
fireball
|
||||||
|
.explosion_power
|
||||||
|
.store(f32::from(ghast.get_explosion_power()), Ordering::Relaxed);
|
||||||
|
|
||||||
|
world.spawn_entity(Arc::new(fireball)).await;
|
||||||
|
self.charge_time = -40;
|
||||||
|
}
|
||||||
|
} else if self.charge_time > 0 {
|
||||||
|
self.charge_time -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ghast.set_charging(self.charge_time > 10);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn controls(&self) -> Controls {
|
||||||
|
Controls::empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RandomFloatAroundGoal {
|
||||||
|
ghast: Weak<GhastEntity>,
|
||||||
|
float_duration: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RandomFloatAroundGoal {
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(ghast: Weak<GhastEntity>) -> Self {
|
||||||
|
Self {
|
||||||
|
ghast,
|
||||||
|
float_duration: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Goal for RandomFloatAroundGoal {
|
||||||
|
fn can_start<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, bool> {
|
||||||
|
Box::pin(async move {
|
||||||
|
let Some(ghast) = self.ghast.upgrade() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let wanted = *ghast.wanted_fly_target.lock().await;
|
||||||
|
wanted.is_none_or(|target| {
|
||||||
|
let pos = ghast.mob_entity.living_entity.entity.pos.load();
|
||||||
|
let dist_sq = pos.squared_distance_to_vec(&target);
|
||||||
|
dist_sq < 1.0 || dist_sq > 3600.0
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
let Some(ghast) = self.ghast.upgrade() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let pos = ghast.mob_entity.living_entity.entity.pos.load();
|
||||||
|
let new_target = {
|
||||||
|
let mut rng = rand::rng();
|
||||||
|
let target_x = pos.x + (rng.random::<f64>() * 2.0 - 1.0) * 16.0;
|
||||||
|
let target_y = pos.y + (rng.random::<f64>() * 2.0 - 1.0) * 16.0;
|
||||||
|
let target_z = pos.z + (rng.random::<f64>() * 2.0 - 1.0) * 16.0;
|
||||||
|
Vector3::new(target_x, target_y, target_z)
|
||||||
|
};
|
||||||
|
*ghast.wanted_fly_target.lock().await = Some(new_target);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_continue<'a>(&'a self, _mob: &'a dyn Mob) -> GoalFuture<'a, bool> {
|
||||||
|
Box::pin(async move {
|
||||||
|
let Some(ghast) = self.ghast.upgrade() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let wanted = *ghast.wanted_fly_target.lock().await;
|
||||||
|
wanted.is_some_and(|target| {
|
||||||
|
let pos = ghast.mob_entity.living_entity.entity.pos.load();
|
||||||
|
let dist_sq = pos.squared_distance_to_vec(&target);
|
||||||
|
(1.0..=3600.0).contains(&dist_sq)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_run_every_tick(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tick<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
let Some(ghast) = self.ghast.upgrade() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let wanted = *ghast.wanted_fly_target.lock().await;
|
||||||
|
let Some(target) = wanted else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let entity = &ghast.mob_entity.living_entity.entity;
|
||||||
|
let pos = entity.pos.load();
|
||||||
|
self.float_duration -= 1;
|
||||||
|
|
||||||
|
if self.float_duration <= 0 {
|
||||||
|
self.float_duration = rand::random_range(2..=6);
|
||||||
|
let travel = Vector3::new(target.x - pos.x, target.y - pos.y, target.z - pos.z);
|
||||||
|
let dist = travel.length();
|
||||||
|
if dist > 0.001 {
|
||||||
|
let move_scale = GhastEntity::FLYING_SPEED * 5.0 / 3.0; // 0.1
|
||||||
|
let norm = travel.normalize();
|
||||||
|
let delta = Vector3::new(
|
||||||
|
norm.x * move_scale,
|
||||||
|
norm.y * move_scale,
|
||||||
|
norm.z * move_scale,
|
||||||
|
);
|
||||||
|
let current_vel = entity.velocity.load();
|
||||||
|
entity.velocity.store(Vector3::new(
|
||||||
|
current_vel.x + delta.x,
|
||||||
|
current_vel.y + delta.y,
|
||||||
|
current_vel.z + delta.z,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn controls(&self) -> Controls {
|
||||||
|
Controls::MOVE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -10,6 +10,8 @@ use crate::world::World;
|
|||||||
use crossbeam::atomic::AtomicCell;
|
use crossbeam::atomic::AtomicCell;
|
||||||
use pumpkin_data::attributes::Attributes;
|
use pumpkin_data::attributes::Attributes;
|
||||||
use pumpkin_data::damage::DamageType;
|
use pumpkin_data::damage::DamageType;
|
||||||
|
use pumpkin_data::data_component_impl::EquipmentSlot;
|
||||||
|
use pumpkin_data::item::Item;
|
||||||
use pumpkin_data::item_stack::ItemStack;
|
use pumpkin_data::item_stack::ItemStack;
|
||||||
use pumpkin_data::tag::{self, Taggable};
|
use pumpkin_data::tag::{self, Taggable};
|
||||||
use pumpkin_data::tracked_data;
|
use pumpkin_data::tracked_data;
|
||||||
@ -36,6 +38,7 @@ pub mod breeze;
|
|||||||
pub mod cave_spider;
|
pub mod cave_spider;
|
||||||
pub mod creaking;
|
pub mod creaking;
|
||||||
pub mod creeper;
|
pub mod creeper;
|
||||||
|
pub mod crossbow_attack_mob;
|
||||||
pub mod elder_guardian;
|
pub mod elder_guardian;
|
||||||
pub mod enderman;
|
pub mod enderman;
|
||||||
pub mod endermite;
|
pub mod endermite;
|
||||||
@ -50,6 +53,7 @@ pub mod magma_cube;
|
|||||||
pub mod patrol;
|
pub mod patrol;
|
||||||
pub mod phantom;
|
pub mod phantom;
|
||||||
pub mod piglin;
|
pub mod piglin;
|
||||||
|
pub mod piglin_ai;
|
||||||
pub mod piglin_brute;
|
pub mod piglin_brute;
|
||||||
pub mod pillager;
|
pub mod pillager;
|
||||||
pub mod raider;
|
pub mod raider;
|
||||||
@ -102,6 +106,65 @@ impl MobEntity {
|
|||||||
const ATTACKING_FLAG: u8 = 4;
|
const ATTACKING_FLAG: u8 = 4;
|
||||||
const CAN_PICK_UP_LOOT_FLAG: u8 = 8;
|
const CAN_PICK_UP_LOOT_FLAG: u8 = 8;
|
||||||
|
|
||||||
|
pub const MAX_WEARING_ARMOR_CHANCE: f32 = 0.15;
|
||||||
|
pub const WEARING_ARMOR_UPGRADE_MATERIAL_CHANCE: f32 = 0.1087;
|
||||||
|
pub const WEARING_ARMOR_UPGRADE_MATERIAL_ATTEMPTS: f32 = 3.0;
|
||||||
|
pub const MAX_PICKUP_LOOT_CHANCE: f32 = 0.55;
|
||||||
|
pub const MAX_ENCHANTED_ARMOR_CHANCE: f32 = 0.5;
|
||||||
|
pub const MAX_ENCHANTED_WEAPON_CHANCE: f32 = 0.25;
|
||||||
|
pub const EQUIPMENT_POPULATION_ORDER: [EquipmentSlot; 4] = [
|
||||||
|
EquipmentSlot::HEAD,
|
||||||
|
EquipmentSlot::CHEST,
|
||||||
|
EquipmentSlot::LEGS,
|
||||||
|
EquipmentSlot::FEET,
|
||||||
|
];
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn get_equipment_for_slot(
|
||||||
|
slot: &EquipmentSlot,
|
||||||
|
armor_type: i32,
|
||||||
|
) -> Option<&'static Item> {
|
||||||
|
match slot {
|
||||||
|
EquipmentSlot::Head(_) => match armor_type {
|
||||||
|
0 => Some(&Item::LEATHER_HELMET),
|
||||||
|
1 => Some(&Item::COPPER_HELMET),
|
||||||
|
2 => Some(&Item::GOLDEN_HELMET),
|
||||||
|
3 => Some(&Item::CHAINMAIL_HELMET),
|
||||||
|
4 => Some(&Item::IRON_HELMET),
|
||||||
|
5 => Some(&Item::DIAMOND_HELMET),
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
EquipmentSlot::Chest(_) => match armor_type {
|
||||||
|
0 => Some(&Item::LEATHER_CHESTPLATE),
|
||||||
|
1 => Some(&Item::COPPER_CHESTPLATE),
|
||||||
|
2 => Some(&Item::GOLDEN_CHESTPLATE),
|
||||||
|
3 => Some(&Item::CHAINMAIL_CHESTPLATE),
|
||||||
|
4 => Some(&Item::IRON_CHESTPLATE),
|
||||||
|
5 => Some(&Item::DIAMOND_CHESTPLATE),
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
EquipmentSlot::Legs(_) => match armor_type {
|
||||||
|
0 => Some(&Item::LEATHER_LEGGINGS),
|
||||||
|
1 => Some(&Item::COPPER_LEGGINGS),
|
||||||
|
2 => Some(&Item::GOLDEN_LEGGINGS),
|
||||||
|
3 => Some(&Item::CHAINMAIL_LEGGINGS),
|
||||||
|
4 => Some(&Item::IRON_LEGGINGS),
|
||||||
|
5 => Some(&Item::DIAMOND_LEGGINGS),
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
EquipmentSlot::Feet(_) => match armor_type {
|
||||||
|
0 => Some(&Item::LEATHER_BOOTS),
|
||||||
|
1 => Some(&Item::COPPER_BOOTS),
|
||||||
|
2 => Some(&Item::GOLDEN_BOOTS),
|
||||||
|
3 => Some(&Item::CHAINMAIL_BOOTS),
|
||||||
|
4 => Some(&Item::IRON_BOOTS),
|
||||||
|
5 => Some(&Item::DIAMOND_BOOTS),
|
||||||
|
_ => None,
|
||||||
|
},
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new(entity: Entity) -> Self {
|
pub fn new(entity: Entity) -> Self {
|
||||||
Self {
|
Self {
|
||||||
@ -654,6 +717,105 @@ pub trait Mob: EntityBase + Send + Sync {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn as_crossbow_attack_mob(&self) -> Option<&dyn crossbow_attack_mob::CrossbowAttackMob> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn populate_default_equipment_slots<'a>(
|
||||||
|
&'a self,
|
||||||
|
_world: &'a Arc<World>,
|
||||||
|
difficulty: &'a crate::entity::mob::equipment::RegionalDifficulty,
|
||||||
|
) -> EntityBaseFuture<'a, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
if rand::random::<f32>()
|
||||||
|
< MobEntity::MAX_WEARING_ARMOR_CHANCE * difficulty.special_multiplier
|
||||||
|
{
|
||||||
|
let mut armor_type = rand::random_range(0..3);
|
||||||
|
for _ in 1..=3 {
|
||||||
|
if rand::random::<f32>() < MobEntity::WEARING_ARMOR_UPGRADE_MATERIAL_CHANCE {
|
||||||
|
armor_type += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let partial_chance = if difficulty.base_difficulty == Difficulty::Hard {
|
||||||
|
0.1f32
|
||||||
|
} else {
|
||||||
|
0.25f32
|
||||||
|
};
|
||||||
|
|
||||||
|
let living = &self.get_mob_entity().living_entity;
|
||||||
|
let mut equipment = living.entity_equipment.lock().await;
|
||||||
|
let mut first = true;
|
||||||
|
|
||||||
|
for slot in &MobEntity::EQUIPMENT_POPULATION_ORDER {
|
||||||
|
let current = equipment.get(slot);
|
||||||
|
if !first && rand::random::<f32>() < partial_chance {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
if current.is_empty()
|
||||||
|
&& let Some(item) = MobEntity::get_equipment_for_slot(slot, armor_type)
|
||||||
|
{
|
||||||
|
equipment.put(slot, ItemStack::new(1, item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn populate_default_equipment_enchantments<'a>(
|
||||||
|
&'a self,
|
||||||
|
difficulty: &'a crate::entity::mob::equipment::RegionalDifficulty,
|
||||||
|
) -> EntityBaseFuture<'a, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
self.enchant_spawned_weapon(difficulty).await;
|
||||||
|
for slot in &MobEntity::EQUIPMENT_POPULATION_ORDER {
|
||||||
|
self.enchant_spawned_armor(slot, difficulty).await;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enchant_spawned_weapon<'a>(
|
||||||
|
&'a self,
|
||||||
|
difficulty: &'a crate::entity::mob::equipment::RegionalDifficulty,
|
||||||
|
) -> EntityBaseFuture<'a, ()> {
|
||||||
|
self.enchant_spawned_equipment(
|
||||||
|
&EquipmentSlot::MAIN_HAND,
|
||||||
|
MobEntity::MAX_ENCHANTED_WEAPON_CHANCE,
|
||||||
|
difficulty,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enchant_spawned_armor<'a>(
|
||||||
|
&'a self,
|
||||||
|
slot: &'a EquipmentSlot,
|
||||||
|
difficulty: &'a crate::entity::mob::equipment::RegionalDifficulty,
|
||||||
|
) -> EntityBaseFuture<'a, ()> {
|
||||||
|
self.enchant_spawned_equipment(slot, MobEntity::MAX_ENCHANTED_ARMOR_CHANCE, difficulty)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enchant_spawned_equipment<'a>(
|
||||||
|
&'a self,
|
||||||
|
slot: &'a EquipmentSlot,
|
||||||
|
chance: f32,
|
||||||
|
difficulty: &'a crate::entity::mob::equipment::RegionalDifficulty,
|
||||||
|
) -> EntityBaseFuture<'a, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
let living = &self.get_mob_entity().living_entity;
|
||||||
|
let mut equipment = living.entity_equipment.lock().await;
|
||||||
|
if let Some(stack) = equipment.equipment.get_mut(slot)
|
||||||
|
&& !stack.is_empty()
|
||||||
|
&& rand::random::<f32>() < chance * difficulty.special_multiplier
|
||||||
|
{
|
||||||
|
crate::entity::mob::equipment::apply_vanilla_enchantments(
|
||||||
|
stack,
|
||||||
|
slot,
|
||||||
|
difficulty.special_multiplier,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn mob_write_nbt<'a>(&'a self, _nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
|
fn mob_write_nbt<'a>(&'a self, _nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
|
||||||
Box::pin(async {})
|
Box::pin(async {})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,25 +1,91 @@
|
|||||||
use std::sync::{Arc, Weak};
|
use std::sync::{
|
||||||
|
Arc, Weak,
|
||||||
|
atomic::{AtomicBool, AtomicI32, Ordering},
|
||||||
|
};
|
||||||
|
|
||||||
|
use pumpkin_data::Block;
|
||||||
|
use pumpkin_data::data_component_impl::EquipmentSlot;
|
||||||
|
use pumpkin_data::dimension::Dimension;
|
||||||
use pumpkin_data::entity::EntityType;
|
use pumpkin_data::entity::EntityType;
|
||||||
|
use pumpkin_data::item::Item;
|
||||||
|
use pumpkin_data::item_stack::ItemStack;
|
||||||
|
use pumpkin_data::sound::{Sound, SoundCategory};
|
||||||
|
use pumpkin_data::tracked_data;
|
||||||
|
use pumpkin_nbt::compound::NbtCompound;
|
||||||
|
use pumpkin_nbt::tag::NbtTag;
|
||||||
|
use pumpkin_protocol::java::client::play::Metadata;
|
||||||
|
use pumpkin_util::math::boundingbox::EntityDimensions;
|
||||||
|
use pumpkin_util::math::position::BlockPos;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use crate::entity::living::LivingEntity;
|
||||||
|
use crate::entity::player::Player;
|
||||||
use crate::entity::{
|
use crate::entity::{
|
||||||
Entity,
|
Entity, EntityBase, EntityBaseFuture, NbtFuture,
|
||||||
ai::goal::{
|
ai::goal::{
|
||||||
active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal,
|
active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal,
|
||||||
look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, open_door::OpenDoorGoal,
|
look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, open_door::OpenDoorGoal,
|
||||||
swim::SwimGoal, wander_around::WanderAroundGoal,
|
ranged_crossbow_attack::RangedCrossbowAttackGoal, revenge::RevengeGoal, swim::SwimGoal,
|
||||||
|
wander_around::WanderAroundGoal,
|
||||||
|
},
|
||||||
|
mob::{
|
||||||
|
Mob, MobEntity, crossbow_attack_mob::CrossbowAttackMob, equipment::RegionalDifficulty,
|
||||||
|
piglin_ai::PiglinAi,
|
||||||
},
|
},
|
||||||
mob::{Mob, MobEntity},
|
|
||||||
};
|
};
|
||||||
|
use crate::world::World;
|
||||||
|
|
||||||
pub struct PiglinEntity {
|
pub struct PiglinEntity {
|
||||||
pub mob_entity: MobEntity,
|
pub mob_entity: MobEntity,
|
||||||
|
pub immune_to_zombification: AtomicBool,
|
||||||
|
pub time_in_overworld: AtomicI32,
|
||||||
|
pub is_baby: AtomicBool,
|
||||||
|
pub cannot_hunt: AtomicBool,
|
||||||
|
pub is_charging_crossbow: AtomicBool,
|
||||||
|
pub is_dancing: AtomicBool,
|
||||||
|
pub inventory: Mutex<Vec<ItemStack>>,
|
||||||
|
pub admire_timer: AtomicI32,
|
||||||
|
pub admiring_disabled_timer: AtomicI32,
|
||||||
|
pub eat_cooldown_timer: AtomicI32,
|
||||||
|
pub celebration_timer: AtomicI32,
|
||||||
|
pub hunt_cooldown_timer: AtomicI32,
|
||||||
|
pub admiring_item: Mutex<Option<ItemStack>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PiglinEntity {
|
impl PiglinEntity {
|
||||||
|
pub const CONVERSION_TIME: i32 = 300;
|
||||||
|
pub const INVENTORY_SIZE: usize = 8;
|
||||||
|
pub const XP_REWARD: u32 = 5;
|
||||||
|
|
||||||
|
pub const ADULT_DIMENSIONS: EntityDimensions = EntityDimensions {
|
||||||
|
width: 0.6,
|
||||||
|
height: 1.95,
|
||||||
|
eye_height: 1.79,
|
||||||
|
};
|
||||||
|
pub const BABY_DIMENSIONS: EntityDimensions = EntityDimensions {
|
||||||
|
width: 0.49,
|
||||||
|
height: 0.98,
|
||||||
|
eye_height: 0.78,
|
||||||
|
};
|
||||||
|
|
||||||
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 piglin = Self { mob_entity };
|
let piglin = Self {
|
||||||
|
mob_entity,
|
||||||
|
immune_to_zombification: AtomicBool::new(false),
|
||||||
|
time_in_overworld: AtomicI32::new(0),
|
||||||
|
is_baby: AtomicBool::new(false),
|
||||||
|
cannot_hunt: AtomicBool::new(false),
|
||||||
|
is_charging_crossbow: AtomicBool::new(false),
|
||||||
|
is_dancing: AtomicBool::new(false),
|
||||||
|
inventory: Mutex::new(Vec::new()),
|
||||||
|
admire_timer: AtomicI32::new(0),
|
||||||
|
admiring_disabled_timer: AtomicI32::new(0),
|
||||||
|
eat_cooldown_timer: AtomicI32::new(0),
|
||||||
|
celebration_timer: AtomicI32::new(0),
|
||||||
|
hunt_cooldown_timer: AtomicI32::new(0),
|
||||||
|
admiring_item: Mutex::new(None),
|
||||||
|
};
|
||||||
let mob_arc = Arc::new(piglin);
|
let mob_arc = Arc::new(piglin);
|
||||||
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,8 +101,8 @@ impl PiglinEntity {
|
|||||||
|
|
||||||
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(OpenDoorGoal::new(true)));
|
goal_selector.add_goal(1, Box::new(OpenDoorGoal::new(true)));
|
||||||
// Piglins use crossbows or swords, but for now we give them melee
|
|
||||||
goal_selector.add_goal(2, Box::new(MeleeAttackGoal::new(1.0, true)));
|
goal_selector.add_goal(2, Box::new(MeleeAttackGoal::new(1.0, true)));
|
||||||
|
goal_selector.add_goal(3, Box::new(RangedCrossbowAttackGoal::new(1.0, 8.0)));
|
||||||
goal_selector.add_goal(5, Box::new(WanderAroundGoal::new(1.0)));
|
goal_selector.add_goal(5, Box::new(WanderAroundGoal::new(1.0)));
|
||||||
goal_selector.add_goal(
|
goal_selector.add_goal(
|
||||||
6,
|
6,
|
||||||
@ -49,12 +115,28 @@ impl PiglinEntity {
|
|||||||
.target_selector
|
.target_selector
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||||
target_selector.add_goal(
|
|
||||||
1,
|
// Retaliate when attacked
|
||||||
ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::PLAYER, true),
|
target_selector.add_goal(1, Box::new(RevengeGoal::new(true)));
|
||||||
);
|
|
||||||
|
// Hostile to players unless wearing safe gold armor
|
||||||
target_selector.add_goal(
|
target_selector.add_goal(
|
||||||
2,
|
2,
|
||||||
|
Box::new(ActiveTargetGoal::new(
|
||||||
|
&mob_arc.mob_entity,
|
||||||
|
&EntityType::PLAYER,
|
||||||
|
10,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
Some(|target: Arc<LivingEntity>, _world: Arc<World>| {
|
||||||
|
Box::pin(async move { !PiglinAi::is_wearing_safe_armor(&target).await })
|
||||||
|
}),
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Hostile to wither skeletons and withers
|
||||||
|
target_selector.add_goal(
|
||||||
|
3,
|
||||||
ActiveTargetGoal::with_default(
|
ActiveTargetGoal::with_default(
|
||||||
&mob_arc.mob_entity,
|
&mob_arc.mob_entity,
|
||||||
&EntityType::WITHER_SKELETON,
|
&EntityType::WITHER_SKELETON,
|
||||||
@ -62,17 +144,561 @@ impl PiglinEntity {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
target_selector.add_goal(
|
target_selector.add_goal(
|
||||||
2,
|
3,
|
||||||
ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::WITHER, true),
|
ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::WITHER, true),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Adults that can hunt are hostile to hoglins
|
||||||
|
let piglin_clone = mob_arc.clone();
|
||||||
|
target_selector.add_goal(
|
||||||
|
4,
|
||||||
|
Box::new(ActiveTargetGoal::new(
|
||||||
|
&mob_arc.mob_entity,
|
||||||
|
&EntityType::HOGLIN,
|
||||||
|
10,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
Some(move |_target: Arc<LivingEntity>, _world: Arc<World>| {
|
||||||
|
let piglin = piglin_clone.clone();
|
||||||
|
Box::pin(async move { piglin.is_adult() && piglin.can_hunt() })
|
||||||
|
}),
|
||||||
|
)),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
mob_arc
|
mob_arc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_immune_to_zombification(&self) -> bool {
|
||||||
|
self.immune_to_zombification.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_immune_to_zombification(&self, immune: bool) {
|
||||||
|
self.immune_to_zombification
|
||||||
|
.store(immune, Ordering::Relaxed);
|
||||||
|
self.mob_entity.living_entity.entity.send_meta_data(
|
||||||
|
&[Metadata::new(
|
||||||
|
tracked_data::piglin::DATA_IMMUNE_TO_ZOMBIFICATION,
|
||||||
|
immune,
|
||||||
|
)],
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_converting(&self, world: &World) -> bool {
|
||||||
|
!self.is_immune_to_zombification()
|
||||||
|
&& !self.mob_entity.is_no_ai()
|
||||||
|
&& world.dimension.minecraft_name != Dimension::THE_NETHER.minecraft_name
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_baby(&self) -> bool {
|
||||||
|
self.is_baby.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_adult(&self) -> bool {
|
||||||
|
!self.is_baby()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_baby(&self, baby: bool) {
|
||||||
|
self.is_baby.store(baby, Ordering::Relaxed);
|
||||||
|
let entity = &self.mob_entity.living_entity.entity;
|
||||||
|
entity.send_meta_data(
|
||||||
|
&[Metadata::new(tracked_data::piglin::DATA_BABY_ID, baby)],
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
if baby {
|
||||||
|
entity.entity_dimension.store(Self::BABY_DIMENSIONS);
|
||||||
|
} else {
|
||||||
|
entity.entity_dimension.store(Self::ADULT_DIMENSIONS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_charging_crossbow(&self) -> bool {
|
||||||
|
self.is_charging_crossbow.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_charging_crossbow(&self, is_charging: bool) {
|
||||||
|
self.is_charging_crossbow
|
||||||
|
.store(is_charging, Ordering::Relaxed);
|
||||||
|
self.mob_entity.living_entity.entity.send_meta_data(
|
||||||
|
&[Metadata::new(
|
||||||
|
tracked_data::piglin::DATA_IS_CHARGING_CROSSBOW,
|
||||||
|
is_charging,
|
||||||
|
)],
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_dancing(&self) -> bool {
|
||||||
|
self.is_dancing.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_dancing(&self, is_dancing: bool) {
|
||||||
|
self.is_dancing.store(is_dancing, Ordering::Relaxed);
|
||||||
|
self.mob_entity.living_entity.entity.send_meta_data(
|
||||||
|
&[Metadata::new(
|
||||||
|
tracked_data::piglin::DATA_IS_DANCING,
|
||||||
|
is_dancing,
|
||||||
|
)],
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn can_hunt(&self) -> bool {
|
||||||
|
!self.cannot_hunt.load(Ordering::Relaxed)
|
||||||
|
&& self.hunt_cooldown_timer.load(Ordering::Relaxed) <= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_cannot_hunt(&self, cannot_hunt: bool) {
|
||||||
|
self.cannot_hunt.store(cannot_hunt, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_admiring(&self) -> bool {
|
||||||
|
self.admire_timer.load(Ordering::Relaxed) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_admiring_disabled(&self) -> bool {
|
||||||
|
self.admiring_disabled_timer.load(Ordering::Relaxed) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn has_eaten_recently(&self) -> bool {
|
||||||
|
self.eat_cooldown_timer.load(Ordering::Relaxed) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn start_admiring(&self, item: ItemStack) {
|
||||||
|
self.admire_timer
|
||||||
|
.store(PiglinAi::ADMIRE_DURATION, Ordering::Relaxed);
|
||||||
|
*self.admiring_item.lock().await = Some(item.clone());
|
||||||
|
|
||||||
|
let mut equip = self.mob_entity.living_entity.entity_equipment.lock().await;
|
||||||
|
equip.put(&EquipmentSlot::OFF_HAND, item);
|
||||||
|
|
||||||
|
let entity = &self.mob_entity.living_entity.entity;
|
||||||
|
let pos = entity.pos.load();
|
||||||
|
entity.world.load().play_sound(
|
||||||
|
Sound::EntityPiglinAdmiringItem,
|
||||||
|
SoundCategory::Hostile,
|
||||||
|
&pos,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn stop_holding_off_hand_item(&self, bartering_enabled: bool) {
|
||||||
|
let admired_item = {
|
||||||
|
let mut guard = self.admiring_item.lock().await;
|
||||||
|
guard.take()
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = {
|
||||||
|
let mut equip = self.mob_entity.living_entity.entity_equipment.lock().await;
|
||||||
|
equip.put(&EquipmentSlot::OFF_HAND, ItemStack::EMPTY.clone())
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(item) = admired_item else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.is_adult() {
|
||||||
|
let is_barter = PiglinAi::is_barter_currency(&item);
|
||||||
|
if bartering_enabled && is_barter {
|
||||||
|
let outcomes = PiglinAi::get_barter_response_items();
|
||||||
|
let entity = &self.mob_entity.living_entity.entity;
|
||||||
|
|
||||||
|
let mut event =
|
||||||
|
crate::plugin::api::events::entity::piglin_barter::PiglinBarterEvent::new(
|
||||||
|
entity.entity_id,
|
||||||
|
item,
|
||||||
|
outcomes,
|
||||||
|
);
|
||||||
|
if let Some(server) = entity.world.load().server.upgrade() {
|
||||||
|
server.plugin_manager.fire(&server, &mut event).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !event.cancelled {
|
||||||
|
PiglinAi::throw_items(self, event.outcome, None).await;
|
||||||
|
}
|
||||||
|
} else if !is_barter {
|
||||||
|
let remainder = self.add_to_inventory(item).await;
|
||||||
|
if let Some(rem) = remainder {
|
||||||
|
PiglinAi::throw_items(self, vec![rem], None).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let remainder = self.add_to_inventory(item).await;
|
||||||
|
if let Some(rem) = remainder {
|
||||||
|
PiglinAi::throw_items(self, vec![rem], None).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cancel_admiring(&self) {
|
||||||
|
if self.is_admiring() {
|
||||||
|
self.admire_timer.store(0, Ordering::Relaxed);
|
||||||
|
let item = {
|
||||||
|
let mut guard = self.admiring_item.lock().await;
|
||||||
|
guard.take()
|
||||||
|
};
|
||||||
|
if let Some(item) = item {
|
||||||
|
let _ = {
|
||||||
|
let mut equip = self.mob_entity.living_entity.entity_equipment.lock().await;
|
||||||
|
equip.put(&EquipmentSlot::OFF_HAND, ItemStack::EMPTY.clone())
|
||||||
|
};
|
||||||
|
PiglinAi::throw_items(self, vec![item], None).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn was_hurt_by(&self, attacker: Option<&dyn EntityBase>) {
|
||||||
|
self.cancel_admiring().await;
|
||||||
|
self.set_dancing(false);
|
||||||
|
self.celebration_timer.store(0, Ordering::Relaxed);
|
||||||
|
|
||||||
|
if let Some(attacker_entity) = attacker
|
||||||
|
&& attacker_entity.get_entity().entity_type.id == EntityType::PLAYER.id
|
||||||
|
{
|
||||||
|
self.admiring_disabled_timer
|
||||||
|
.store(PiglinAi::ADMIRING_DISABLED_DURATION, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn add_to_inventory(&self, item: ItemStack) -> Option<ItemStack> {
|
||||||
|
let mut inv = self.inventory.lock().await;
|
||||||
|
if inv.len() < Self::INVENTORY_SIZE {
|
||||||
|
inv.push(item);
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn drop_inventory(&self) {
|
||||||
|
let items = {
|
||||||
|
let mut inv = self.inventory.lock().await;
|
||||||
|
std::mem::take(&mut *inv)
|
||||||
|
};
|
||||||
|
let entity = &self.mob_entity.living_entity.entity;
|
||||||
|
let world = entity.world.load();
|
||||||
|
let pos = entity.pos.load();
|
||||||
|
for item in items {
|
||||||
|
if !item.is_empty() {
|
||||||
|
let item_entity = crate::entity::item::ItemEntity::new(
|
||||||
|
Entity::new(world.clone(), pos, &EntityType::ITEM),
|
||||||
|
item,
|
||||||
|
);
|
||||||
|
world.spawn_entity(Arc::new(item_entity)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn check_piglin_spawn_rules(world: &World, pos: &BlockPos) -> bool {
|
||||||
|
let below = BlockPos::new(pos.0.x, pos.0.y - 1, pos.0.z);
|
||||||
|
let state = world.get_block_state(&below);
|
||||||
|
state.id != Block::NETHER_WART_BLOCK.default_state.id
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn convert_to_zombified(&self) {
|
||||||
|
let entity = &self.mob_entity.living_entity.entity;
|
||||||
|
let world = entity.world.load();
|
||||||
|
let pos = entity.pos.load();
|
||||||
|
|
||||||
|
if world.level_info.load().difficulty != pumpkin_util::Difficulty::Peaceful {
|
||||||
|
world.play_sound(
|
||||||
|
Sound::EntityPiglinConvertedToZombified,
|
||||||
|
SoundCategory::Hostile,
|
||||||
|
&pos,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.drop_inventory().await;
|
||||||
|
|
||||||
|
let zombified = crate::entity::r#type::from_type(
|
||||||
|
&EntityType::ZOMBIFIED_PIGLIN,
|
||||||
|
pos,
|
||||||
|
&world,
|
||||||
|
uuid::Uuid::new_v4(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let zombified_base = zombified.get_entity();
|
||||||
|
zombified_base.set_rotation(entity.yaw.load(), entity.pitch.load());
|
||||||
|
zombified_base.head_yaw.store(entity.head_yaw.load());
|
||||||
|
zombified_base.velocity.store(entity.velocity.load());
|
||||||
|
|
||||||
|
if let Some(living) = zombified.get_living_entity() {
|
||||||
|
living.set_health(self.mob_entity.living_entity.health.load());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(custom_name) = &**entity.custom_name.load() {
|
||||||
|
zombified_base.set_custom_name(custom_name.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let src_equip = self.mob_entity.living_entity.entity_equipment.lock().await;
|
||||||
|
if let Some(living) = zombified.get_living_entity() {
|
||||||
|
let mut dst_equip = living.entity_equipment.lock().await;
|
||||||
|
for (slot, item) in &src_equip.equipment {
|
||||||
|
dst_equip.put(slot, item.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
world.spawn_entity(zombified).await;
|
||||||
|
entity.remove().await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Mob for PiglinEntity {
|
impl Mob for PiglinEntity {
|
||||||
fn get_mob_entity(&self) -> &MobEntity {
|
fn get_mob_entity(&self) -> &MobEntity {
|
||||||
&self.mob_entity
|
&self.mob_entity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn populate_default_equipment_slots<'a>(
|
||||||
|
&'a self,
|
||||||
|
_world: &'a Arc<World>,
|
||||||
|
_difficulty: &'a RegionalDifficulty,
|
||||||
|
) -> EntityBaseFuture<'a, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
if !self.is_baby.load(Ordering::Relaxed) {
|
||||||
|
let living = &self.mob_entity.living_entity;
|
||||||
|
let mut equipment = living.entity_equipment.lock().await;
|
||||||
|
|
||||||
|
// Spawn weapon: 50% crossbow, 5% golden spear (10% of remaining 50%), 45% golden sword
|
||||||
|
let weapon = if rand::random::<f32>() < 0.5 {
|
||||||
|
&Item::CROSSBOW
|
||||||
|
} else if rand::random_range(0..10) == 0 {
|
||||||
|
&Item::GOLDEN_SPEAR
|
||||||
|
} else {
|
||||||
|
&Item::GOLDEN_SWORD
|
||||||
|
};
|
||||||
|
equipment.put(&EquipmentSlot::MAIN_HAND, ItemStack::new(1, weapon));
|
||||||
|
|
||||||
|
// Armor: 10% chance per piece for golden armor
|
||||||
|
if rand::random::<f32>() < 0.1 {
|
||||||
|
equipment.put(
|
||||||
|
&EquipmentSlot::HEAD,
|
||||||
|
ItemStack::new(1, &Item::GOLDEN_HELMET),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if rand::random::<f32>() < 0.1 {
|
||||||
|
equipment.put(
|
||||||
|
&EquipmentSlot::CHEST,
|
||||||
|
ItemStack::new(1, &Item::GOLDEN_CHESTPLATE),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if rand::random::<f32>() < 0.1 {
|
||||||
|
equipment.put(
|
||||||
|
&EquipmentSlot::LEGS,
|
||||||
|
ItemStack::new(1, &Item::GOLDEN_LEGGINGS),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if rand::random::<f32>() < 0.1 {
|
||||||
|
equipment.put(&EquipmentSlot::FEET, ItemStack::new(1, &Item::GOLDEN_BOOTS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mob_init_data_tracker(&self) -> EntityBaseFuture<'_, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
let entity = self.get_entity();
|
||||||
|
let mut meta = Vec::new();
|
||||||
|
if self.is_immune_to_zombification() {
|
||||||
|
meta.push(Metadata::new(
|
||||||
|
tracked_data::piglin::DATA_IMMUNE_TO_ZOMBIFICATION,
|
||||||
|
true,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if self.is_baby() {
|
||||||
|
meta.push(Metadata::new(tracked_data::piglin::DATA_BABY_ID, true));
|
||||||
|
}
|
||||||
|
if self.is_charging_crossbow() {
|
||||||
|
meta.push(Metadata::new(
|
||||||
|
tracked_data::piglin::DATA_IS_CHARGING_CROSSBOW,
|
||||||
|
true,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if self.is_dancing() {
|
||||||
|
meta.push(Metadata::new(tracked_data::piglin::DATA_IS_DANCING, true));
|
||||||
|
}
|
||||||
|
if !meta.is_empty() {
|
||||||
|
entity.send_meta_data(&meta, None);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mob_write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
if self.is_immune_to_zombification() {
|
||||||
|
nbt.put_bool("IsImmuneToZombification", true);
|
||||||
|
}
|
||||||
|
let time_in_overworld = self.time_in_overworld.load(Ordering::Relaxed);
|
||||||
|
if time_in_overworld > 0 {
|
||||||
|
nbt.put_int("TimeInOverworld", time_in_overworld);
|
||||||
|
}
|
||||||
|
nbt.put_bool("CanPickUpLoot", true);
|
||||||
|
if self.is_baby() {
|
||||||
|
nbt.put_bool("IsBaby", true);
|
||||||
|
}
|
||||||
|
if !self.can_hunt() {
|
||||||
|
nbt.put_bool("CannotHunt", true);
|
||||||
|
}
|
||||||
|
if self.is_charging_crossbow() {
|
||||||
|
nbt.put_bool("IsChargingCrossbow", true);
|
||||||
|
}
|
||||||
|
if self.is_dancing() {
|
||||||
|
nbt.put_bool("IsDancing", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
let inv = self.inventory.lock().await;
|
||||||
|
if !inv.is_empty() {
|
||||||
|
let mut items_tag = Vec::new();
|
||||||
|
for item in inv.iter() {
|
||||||
|
if !item.is_empty() {
|
||||||
|
let mut item_nbt = NbtCompound::new();
|
||||||
|
item.write_item_stack(&mut item_nbt);
|
||||||
|
items_tag.push(NbtTag::Compound(item_nbt));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !items_tag.is_empty() {
|
||||||
|
nbt.put_list("Inventory", items_tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mob_read_nbt<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
if let Some(immune) = nbt.get_bool("IsImmuneToZombification") {
|
||||||
|
self.set_immune_to_zombification(immune);
|
||||||
|
}
|
||||||
|
if let Some(time) = nbt.get_int("TimeInOverworld") {
|
||||||
|
self.time_in_overworld.store(time, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
if let Some(baby) = nbt.get_bool("IsBaby") {
|
||||||
|
self.set_baby(baby);
|
||||||
|
}
|
||||||
|
if let Some(cannot_hunt) = nbt.get_bool("CannotHunt") {
|
||||||
|
self.set_cannot_hunt(cannot_hunt);
|
||||||
|
}
|
||||||
|
if let Some(charging) = nbt.get_bool("IsChargingCrossbow") {
|
||||||
|
self.set_charging_crossbow(charging);
|
||||||
|
}
|
||||||
|
if let Some(dancing) = nbt.get_bool("IsDancing") {
|
||||||
|
self.set_dancing(dancing);
|
||||||
|
}
|
||||||
|
if let Some(inv_list) = nbt.get_list("Inventory") {
|
||||||
|
let mut inv = self.inventory.lock().await;
|
||||||
|
inv.clear();
|
||||||
|
for tag in inv_list {
|
||||||
|
if let Some(compound) = tag.extract_compound()
|
||||||
|
&& let Some(stack) = ItemStack::read_item_stack(compound)
|
||||||
|
{
|
||||||
|
inv.push(stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mob_interact<'a>(
|
||||||
|
&'a self,
|
||||||
|
player: &'a Arc<Player>,
|
||||||
|
item_stack: &'a mut ItemStack,
|
||||||
|
) -> EntityBaseFuture<'a, bool> {
|
||||||
|
Box::pin(async move {
|
||||||
|
if PiglinAi::can_admire(self, item_stack) {
|
||||||
|
let mut given = item_stack.clone();
|
||||||
|
given.item_count = 1;
|
||||||
|
if player.gamemode.load() != pumpkin_util::GameMode::Creative {
|
||||||
|
item_stack.item_count -= 1;
|
||||||
|
}
|
||||||
|
self.start_admiring(given).await;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
self.mob_entity.mob_interact(player, item_stack).await
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mob_tick<'a>(&'a self, _caller: &'a Arc<dyn EntityBase>) -> EntityBaseFuture<'a, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
let entity = &self.mob_entity.living_entity.entity;
|
||||||
|
if !entity.is_alive() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let world = entity.world.load();
|
||||||
|
if self.is_converting(&world) {
|
||||||
|
let time = self.time_in_overworld.fetch_add(1, Ordering::Relaxed) + 1;
|
||||||
|
if time > Self::CONVERSION_TIME {
|
||||||
|
self.convert_to_zombified().await;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.time_in_overworld.store(0, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.admiring_disabled_timer.load(Ordering::Relaxed) > 0 {
|
||||||
|
self.admiring_disabled_timer.fetch_sub(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
if self.eat_cooldown_timer.load(Ordering::Relaxed) > 0 {
|
||||||
|
self.eat_cooldown_timer.fetch_sub(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
if self.hunt_cooldown_timer.load(Ordering::Relaxed) > 0 {
|
||||||
|
self.hunt_cooldown_timer.fetch_sub(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
if self.celebration_timer.load(Ordering::Relaxed) > 0 {
|
||||||
|
let remaining = self.celebration_timer.fetch_sub(1, Ordering::Relaxed) - 1;
|
||||||
|
if remaining <= 0 {
|
||||||
|
self.set_dancing(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.admire_timer.load(Ordering::Relaxed) > 0 {
|
||||||
|
let remaining = self.admire_timer.fetch_sub(1, Ordering::Relaxed) - 1;
|
||||||
|
if remaining <= 0 {
|
||||||
|
self.stop_holding_off_hand_item(true).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_damage<'a>(
|
||||||
|
&'a self,
|
||||||
|
_damage_type: pumpkin_data::damage::DamageType,
|
||||||
|
source: Option<&'a dyn EntityBase>,
|
||||||
|
) -> EntityBaseFuture<'a, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
if self.mob_entity.living_entity.dead.load(Ordering::Relaxed) {
|
||||||
|
self.drop_inventory().await;
|
||||||
|
} else {
|
||||||
|
self.was_hurt_by(source).await;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_crossbow_attack_mob(&self) -> Option<&dyn CrossbowAttackMob> {
|
||||||
|
Some(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_base_experience_reward(&self) -> u32 {
|
||||||
|
Self::XP_REWARD
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CrossbowAttackMob for PiglinEntity {
|
||||||
|
fn set_charging_crossbow(&self, is_charging: bool) {
|
||||||
|
self.set_charging_crossbow(is_charging);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_charging_crossbow(&self) -> bool {
|
||||||
|
self.is_charging_crossbow()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
186
crates/pumpkin/src/entity/mob/piglin_ai.rs
Normal file
186
crates/pumpkin/src/entity/mob/piglin_ai.rs
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use pumpkin_data::data_component_impl::EquipmentSlot;
|
||||||
|
use pumpkin_data::entity::EntityType;
|
||||||
|
use pumpkin_data::item::Item;
|
||||||
|
use pumpkin_data::item_stack::ItemStack;
|
||||||
|
use pumpkin_data::tag::{self, Taggable};
|
||||||
|
use pumpkin_util::math::vector3::Vector3;
|
||||||
|
|
||||||
|
use crate::entity::Entity;
|
||||||
|
use crate::entity::living::LivingEntity;
|
||||||
|
use crate::entity::mob::piglin::PiglinEntity;
|
||||||
|
|
||||||
|
/// Vanilla Piglin AI logic, bartering, reactions, and utilities.
|
||||||
|
/// Mirrors `net.minecraft.world.entity.monster.piglin.PiglinAi`.
|
||||||
|
pub struct PiglinAi;
|
||||||
|
|
||||||
|
impl PiglinAi {
|
||||||
|
pub const REPELLENT_DETECTION_RANGE_HORIZONTAL: i32 = 8;
|
||||||
|
pub const REPELLENT_DETECTION_RANGE_VERTICAL: i32 = 4;
|
||||||
|
pub const BARTERING_ITEM: &'static Item = &Item::GOLD_INGOT;
|
||||||
|
pub const PLAYER_ANGER_RANGE: f64 = 16.0;
|
||||||
|
pub const ANGER_DURATION: i32 = 600;
|
||||||
|
pub const ADMIRE_DURATION: i32 = 119;
|
||||||
|
pub const ADMIRING_DISABLED_DURATION: i32 = 400;
|
||||||
|
pub const EAT_COOLDOWN: i32 = 200;
|
||||||
|
pub const BABY_FLEE_DURATION: i32 = 100;
|
||||||
|
pub const CELEBRATION_TIME: i32 = 300;
|
||||||
|
pub const MIN_TIME_BETWEEN_HUNTS: i32 = 600;
|
||||||
|
pub const MAX_TIME_BETWEEN_HUNTS: i32 = 2400;
|
||||||
|
pub const DESIRED_DISTANCE_FROM_ZOMBIFIED: f64 = 6.0;
|
||||||
|
pub const PROBABILITY_OF_CELEBRATION_DANCE: f32 = 0.1;
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn is_barter_currency(item_stack: &ItemStack) -> bool {
|
||||||
|
item_stack.item.id == Self::BARTERING_ITEM.id
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_loved_item(item_stack: &ItemStack) -> bool {
|
||||||
|
item_stack.item.has_tag(&tag::Item::MINECRAFT_PIGLIN_LOVED)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_food(item_stack: &ItemStack) -> bool {
|
||||||
|
item_stack.item.has_tag(&tag::Item::MINECRAFT_PIGLIN_FOOD)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn is_zombified(entity_type: &EntityType) -> bool {
|
||||||
|
entity_type.id == EntityType::ZOMBIFIED_PIGLIN.id || entity_type.id == EntityType::ZOGLIN.id
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn wants_to_dance(killed_target_type: &EntityType) -> bool {
|
||||||
|
if killed_target_type.id != EntityType::HOGLIN.id {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
rand::random::<f32>() < Self::PROBABILITY_OF_CELEBRATION_DANCE
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn is_wearing_safe_armor(target: &LivingEntity) -> bool {
|
||||||
|
let equipment = target.entity_equipment.lock().await;
|
||||||
|
[
|
||||||
|
EquipmentSlot::HEAD,
|
||||||
|
EquipmentSlot::CHEST,
|
||||||
|
EquipmentSlot::LEGS,
|
||||||
|
EquipmentSlot::FEET,
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.any(|slot| {
|
||||||
|
let stack = equipment.get(slot);
|
||||||
|
!stack.is_empty()
|
||||||
|
&& (stack.item.has_tag(&tag::Item::MINECRAFT_PIGLIN_SAFE_ARMOR)
|
||||||
|
|| stack.item.has_tag(&tag::Item::MINECRAFT_PIGLIN_LOVED))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn can_admire(piglin: &PiglinEntity, item_stack: &ItemStack) -> bool {
|
||||||
|
!piglin.is_admiring_disabled()
|
||||||
|
&& !piglin.is_admiring()
|
||||||
|
&& piglin.is_adult()
|
||||||
|
&& Self::is_barter_currency(item_stack)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn wants_to_pickup(piglin: &PiglinEntity, item_stack: &ItemStack) -> bool {
|
||||||
|
if piglin.is_baby()
|
||||||
|
&& item_stack
|
||||||
|
.item
|
||||||
|
.has_tag(&tag::Item::MINECRAFT_IGNORED_BY_PIGLIN_BABIES)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if item_stack
|
||||||
|
.item
|
||||||
|
.has_tag(&tag::Item::MINECRAFT_PIGLIN_REPELLENTS)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if piglin.is_admiring_disabled() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if Self::is_barter_currency(item_stack) {
|
||||||
|
return !piglin.is_admiring();
|
||||||
|
}
|
||||||
|
if Self::is_food(item_stack) {
|
||||||
|
return !piglin.has_eaten_recently();
|
||||||
|
}
|
||||||
|
Self::is_loved_item(item_stack)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates randomized barter drop items matching vanilla `BuiltInLootTables.PIGLIN_BARTERING`.
|
||||||
|
#[must_use]
|
||||||
|
pub fn get_barter_response_items() -> Vec<ItemStack> {
|
||||||
|
let roll = rand::random_range(0..459);
|
||||||
|
match roll {
|
||||||
|
0..5 => vec![ItemStack::new(1, &Item::ENCHANTED_BOOK)],
|
||||||
|
5..13 => vec![ItemStack::new(1, &Item::IRON_BOOTS)],
|
||||||
|
13..21 => vec![ItemStack::new(1, &Item::SPLASH_POTION)],
|
||||||
|
21..39 => vec![ItemStack::new(1, &Item::POTION)], // Potion / Water bottle
|
||||||
|
39..49 => vec![ItemStack::new(
|
||||||
|
rand::random_range(10..=36),
|
||||||
|
&Item::IRON_NUGGET,
|
||||||
|
)],
|
||||||
|
49..59 => vec![ItemStack::new(
|
||||||
|
rand::random_range(2..=4),
|
||||||
|
&Item::ENDER_PEARL,
|
||||||
|
)],
|
||||||
|
59..79 => vec![ItemStack::new(rand::random_range(3..=9), &Item::STRING)],
|
||||||
|
79..99 => vec![ItemStack::new(rand::random_range(5..=12), &Item::QUARTZ)],
|
||||||
|
99..139 => vec![ItemStack::new(1, &Item::OBSIDIAN)],
|
||||||
|
139..179 => vec![ItemStack::new(
|
||||||
|
rand::random_range(1..=3),
|
||||||
|
&Item::CRYING_OBSIDIAN,
|
||||||
|
)],
|
||||||
|
179..219 => vec![ItemStack::new(1, &Item::FIRE_CHARGE)],
|
||||||
|
219..259 => vec![ItemStack::new(rand::random_range(2..=4), &Item::LEATHER)],
|
||||||
|
259..299 => vec![ItemStack::new(rand::random_range(2..=8), &Item::SOUL_SAND)],
|
||||||
|
299..339 => vec![ItemStack::new(
|
||||||
|
rand::random_range(2..=8),
|
||||||
|
&Item::NETHER_BRICK,
|
||||||
|
)],
|
||||||
|
339..379 => vec![ItemStack::new(
|
||||||
|
rand::random_range(6..=12),
|
||||||
|
&Item::SPECTRAL_ARROW,
|
||||||
|
)],
|
||||||
|
379..419 => vec![ItemStack::new(rand::random_range(8..=16), &Item::GRAVEL)],
|
||||||
|
_ => vec![ItemStack::new(
|
||||||
|
rand::random_range(8..=16),
|
||||||
|
&Item::BLACKSTONE,
|
||||||
|
)],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn throw_items(
|
||||||
|
piglin: &PiglinEntity,
|
||||||
|
items: Vec<ItemStack>,
|
||||||
|
target_pos: Option<Vector3<f64>>,
|
||||||
|
) {
|
||||||
|
let entity = &piglin.mob_entity.living_entity.entity;
|
||||||
|
let world = entity.world.load();
|
||||||
|
let pos = entity.pos.load();
|
||||||
|
let spawn_pos = Vector3::new(pos.x, pos.y + 1.0, pos.z);
|
||||||
|
|
||||||
|
for item in items {
|
||||||
|
if !item.is_empty() {
|
||||||
|
let item_entity = crate::entity::item::ItemEntity::new(
|
||||||
|
Entity::new(world.clone(), spawn_pos, &EntityType::ITEM),
|
||||||
|
item,
|
||||||
|
);
|
||||||
|
if let Some(target) = target_pos {
|
||||||
|
let vel = Vector3::new(target.x - pos.x, target.y - pos.y, target.z - pos.z)
|
||||||
|
.normalize();
|
||||||
|
item_entity.get_entity().velocity.store(Vector3::new(
|
||||||
|
vel.x * 0.3,
|
||||||
|
0.3,
|
||||||
|
vel.z * 0.3,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
world.spawn_entity(Arc::new(item_entity)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,25 +1,45 @@
|
|||||||
use std::sync::{Arc, Weak};
|
use std::sync::{
|
||||||
|
Arc, Weak,
|
||||||
|
atomic::{AtomicBool, AtomicI32, Ordering},
|
||||||
|
};
|
||||||
|
|
||||||
|
use pumpkin_data::Block;
|
||||||
|
use pumpkin_data::dimension::Dimension;
|
||||||
use pumpkin_data::entity::EntityType;
|
use pumpkin_data::entity::EntityType;
|
||||||
|
use pumpkin_data::sound::{Sound, SoundCategory};
|
||||||
|
use pumpkin_data::tracked_data;
|
||||||
|
use pumpkin_nbt::compound::NbtCompound;
|
||||||
|
use pumpkin_protocol::java::client::play::Metadata;
|
||||||
|
use pumpkin_util::math::position::BlockPos;
|
||||||
|
|
||||||
use crate::entity::{
|
use crate::entity::{
|
||||||
Entity,
|
Entity, EntityBase, EntityBaseFuture, NbtFuture,
|
||||||
ai::goal::{
|
ai::goal::{
|
||||||
active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal,
|
active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal,
|
||||||
look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, open_door::OpenDoorGoal,
|
look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, open_door::OpenDoorGoal,
|
||||||
swim::SwimGoal, wander_around::WanderAroundGoal,
|
revenge::RevengeGoal, swim::SwimGoal, wander_around::WanderAroundGoal,
|
||||||
},
|
},
|
||||||
mob::{Mob, MobEntity},
|
mob::{Mob, MobEntity},
|
||||||
};
|
};
|
||||||
|
use crate::world::World;
|
||||||
|
|
||||||
pub struct PiglinBruteEntity {
|
pub struct PiglinBruteEntity {
|
||||||
pub mob_entity: MobEntity,
|
pub mob_entity: MobEntity,
|
||||||
|
pub immune_to_zombification: AtomicBool,
|
||||||
|
pub time_in_overworld: AtomicI32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PiglinBruteEntity {
|
impl PiglinBruteEntity {
|
||||||
|
pub const CONVERSION_TIME: i32 = 300;
|
||||||
|
pub const XP_REWARD: u32 = 20;
|
||||||
|
|
||||||
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 piglin = Self { mob_entity };
|
let piglin = Self {
|
||||||
|
mob_entity,
|
||||||
|
immune_to_zombification: AtomicBool::new(false),
|
||||||
|
time_in_overworld: AtomicI32::new(0),
|
||||||
|
};
|
||||||
let mob_arc = Arc::new(piglin);
|
let mob_arc = Arc::new(piglin);
|
||||||
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();
|
||||||
@ -48,12 +68,14 @@ impl PiglinBruteEntity {
|
|||||||
.target_selector
|
.target_selector
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||||
|
target_selector.add_goal(1, Box::new(RevengeGoal::new(true)));
|
||||||
|
// Piglin brutes are always hostile to players (even with gold armor)
|
||||||
target_selector.add_goal(
|
target_selector.add_goal(
|
||||||
1,
|
2,
|
||||||
ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::PLAYER, true),
|
ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::PLAYER, true),
|
||||||
);
|
);
|
||||||
target_selector.add_goal(
|
target_selector.add_goal(
|
||||||
2,
|
3,
|
||||||
ActiveTargetGoal::with_default(
|
ActiveTargetGoal::with_default(
|
||||||
&mob_arc.mob_entity,
|
&mob_arc.mob_entity,
|
||||||
&EntityType::WITHER_SKELETON,
|
&EntityType::WITHER_SKELETON,
|
||||||
@ -61,17 +83,157 @@ impl PiglinBruteEntity {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
target_selector.add_goal(
|
target_selector.add_goal(
|
||||||
2,
|
3,
|
||||||
ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::WITHER, true),
|
ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::WITHER, true),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
mob_arc
|
mob_arc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_immune_to_zombification(&self) -> bool {
|
||||||
|
self.immune_to_zombification.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_immune_to_zombification(&self, immune: bool) {
|
||||||
|
self.immune_to_zombification
|
||||||
|
.store(immune, Ordering::Relaxed);
|
||||||
|
self.mob_entity.living_entity.entity.send_meta_data(
|
||||||
|
&[Metadata::new(
|
||||||
|
tracked_data::piglin_brute::DATA_IMMUNE_TO_ZOMBIFICATION,
|
||||||
|
immune,
|
||||||
|
)],
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_converting(&self, world: &World) -> bool {
|
||||||
|
!self.is_immune_to_zombification()
|
||||||
|
&& !self.mob_entity.is_no_ai()
|
||||||
|
&& world.dimension.minecraft_name != Dimension::THE_NETHER.minecraft_name
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn check_piglin_brute_spawn_rules(world: &World, pos: &BlockPos) -> bool {
|
||||||
|
let below = BlockPos::new(pos.0.x, pos.0.y - 1, pos.0.z);
|
||||||
|
let state = world.get_block_state(&below);
|
||||||
|
state.id != Block::NETHER_WART_BLOCK.default_state.id
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn convert_to_zombified(&self) {
|
||||||
|
let entity = &self.mob_entity.living_entity.entity;
|
||||||
|
let world = entity.world.load();
|
||||||
|
let pos = entity.pos.load();
|
||||||
|
|
||||||
|
if world.level_info.load().difficulty != pumpkin_util::Difficulty::Peaceful {
|
||||||
|
world.play_sound(
|
||||||
|
Sound::EntityPiglinBruteConvertedToZombified,
|
||||||
|
SoundCategory::Hostile,
|
||||||
|
&pos,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let zombified = crate::entity::r#type::from_type(
|
||||||
|
&EntityType::ZOMBIFIED_PIGLIN,
|
||||||
|
pos,
|
||||||
|
&world,
|
||||||
|
uuid::Uuid::new_v4(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let zombified_base = zombified.get_entity();
|
||||||
|
zombified_base.set_rotation(entity.yaw.load(), entity.pitch.load());
|
||||||
|
zombified_base.head_yaw.store(entity.head_yaw.load());
|
||||||
|
zombified_base.velocity.store(entity.velocity.load());
|
||||||
|
|
||||||
|
if let Some(living) = zombified.get_living_entity() {
|
||||||
|
living.set_health(self.mob_entity.living_entity.health.load());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(custom_name) = &**entity.custom_name.load() {
|
||||||
|
zombified_base.set_custom_name(custom_name.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let src_equip = self.mob_entity.living_entity.entity_equipment.lock().await;
|
||||||
|
if let Some(living) = zombified.get_living_entity() {
|
||||||
|
let mut dst_equip = living.entity_equipment.lock().await;
|
||||||
|
for (slot, item) in &src_equip.equipment {
|
||||||
|
dst_equip.put(slot, item.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
world.spawn_entity(zombified).await;
|
||||||
|
entity.remove().await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Mob for PiglinBruteEntity {
|
impl Mob for PiglinBruteEntity {
|
||||||
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();
|
||||||
|
if self.is_immune_to_zombification() {
|
||||||
|
entity.send_meta_data(
|
||||||
|
&[Metadata::new(
|
||||||
|
tracked_data::piglin_brute::DATA_IMMUNE_TO_ZOMBIFICATION,
|
||||||
|
true,
|
||||||
|
)],
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mob_write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
if self.is_immune_to_zombification() {
|
||||||
|
nbt.put_bool("IsImmuneToZombification", true);
|
||||||
|
}
|
||||||
|
let time_in_overworld = self.time_in_overworld.load(Ordering::Relaxed);
|
||||||
|
if time_in_overworld > 0 {
|
||||||
|
nbt.put_int("TimeInOverworld", time_in_overworld);
|
||||||
|
}
|
||||||
|
nbt.put_bool("CanPickUpLoot", true);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mob_read_nbt<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
if let Some(immune) = nbt.get_bool("IsImmuneToZombification") {
|
||||||
|
self.set_immune_to_zombification(immune);
|
||||||
|
}
|
||||||
|
if let Some(time) = nbt.get_int("TimeInOverworld") {
|
||||||
|
self.time_in_overworld.store(time, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
let world = entity.world.load();
|
||||||
|
if self.is_converting(&world) {
|
||||||
|
let time = self.time_in_overworld.fetch_add(1, Ordering::Relaxed) + 1;
|
||||||
|
if time > Self::CONVERSION_TIME {
|
||||||
|
self.convert_to_zombified().await;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.time_in_overworld.store(0, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_base_experience_reward(&self) -> u32 {
|
||||||
|
Self::XP_REWARD
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,18 +1,27 @@
|
|||||||
use std::sync::{Arc, Weak};
|
use std::sync::{
|
||||||
|
Arc, Weak,
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
};
|
||||||
|
|
||||||
use pumpkin_data::entity::EntityType;
|
use pumpkin_data::entity::EntityType;
|
||||||
|
use pumpkin_data::item_stack::ItemStack;
|
||||||
use pumpkin_data::sound::Sound;
|
use pumpkin_data::sound::Sound;
|
||||||
|
use pumpkin_data::tracked_data;
|
||||||
use pumpkin_nbt::compound::NbtCompound;
|
use pumpkin_nbt::compound::NbtCompound;
|
||||||
|
use pumpkin_nbt::tag::NbtTag;
|
||||||
|
use pumpkin_protocol::java::client::play::Metadata;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
use crate::entity::{
|
use crate::entity::{
|
||||||
Entity, NbtFuture,
|
Entity, EntityBase, EntityBaseFuture, 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, ranged_crossbow_attack::RangedCrossbowAttackGoal,
|
||||||
wander_around::WanderAroundGoal,
|
swim::SwimGoal, wander_around::WanderAroundGoal,
|
||||||
},
|
},
|
||||||
mob::{
|
mob::{
|
||||||
Mob, MobEntity,
|
Mob, MobEntity,
|
||||||
|
crossbow_attack_mob::CrossbowAttackMob,
|
||||||
patrol::{LongDistancePatrolGoal, PatrolData, PatrollingMonster},
|
patrol::{LongDistancePatrolGoal, PatrolData, PatrollingMonster},
|
||||||
raider::{
|
raider::{
|
||||||
HoldGroundAttackGoal, ObtainRaidLeaderBannerGoal, PathfindToRaidGoal, Raider,
|
HoldGroundAttackGoal, ObtainRaidLeaderBannerGoal, PathfindToRaidGoal, Raider,
|
||||||
@ -24,14 +33,20 @@ use crate::entity::{
|
|||||||
pub struct PillagerEntity {
|
pub struct PillagerEntity {
|
||||||
pub mob_entity: MobEntity,
|
pub mob_entity: MobEntity,
|
||||||
pub raider_data: RaiderData,
|
pub raider_data: RaiderData,
|
||||||
|
pub is_charging_crossbow: AtomicBool,
|
||||||
|
pub inventory: Mutex<Vec<ItemStack>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PillagerEntity {
|
impl PillagerEntity {
|
||||||
|
pub const INVENTORY_SIZE: usize = 5;
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new(entity: Entity) -> Arc<Self> {
|
pub fn new(entity: Entity) -> Arc<Self> {
|
||||||
let mob_arc = Arc::new(Self {
|
let mob_arc = Arc::new(Self {
|
||||||
mob_entity: MobEntity::new(entity),
|
mob_entity: MobEntity::new(entity),
|
||||||
raider_data: RaiderData::default(),
|
raider_data: RaiderData::default(),
|
||||||
|
is_charging_crossbow: AtomicBool::new(false),
|
||||||
|
inventory: Mutex::new(Vec::new()),
|
||||||
});
|
});
|
||||||
|
|
||||||
let mob_weak: Weak<dyn Mob> = {
|
let mob_weak: Weak<dyn Mob> = {
|
||||||
@ -49,8 +64,7 @@ impl PillagerEntity {
|
|||||||
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(ObtainRaidLeaderBannerGoal));
|
goal_selector.add_goal(1, Box::new(ObtainRaidLeaderBannerGoal));
|
||||||
goal_selector.add_goal(2, Box::new(HoldGroundAttackGoal::new(10.0)));
|
goal_selector.add_goal(2, Box::new(HoldGroundAttackGoal::new(10.0)));
|
||||||
// Pillagers use crossbows, but for now we give them melee
|
goal_selector.add_goal(3, Box::new(RangedCrossbowAttackGoal::new(1.0, 8.0)));
|
||||||
goal_selector.add_goal(3, Box::new(MeleeAttackGoal::new(1.0, true)));
|
|
||||||
goal_selector.add_goal(4, Box::new(LongDistancePatrolGoal::new(0.7, 0.595)));
|
goal_selector.add_goal(4, Box::new(LongDistancePatrolGoal::new(0.7, 0.595)));
|
||||||
goal_selector.add_goal(4, Box::new(RaiderMoveThroughVillageGoal::new(1.05)));
|
goal_selector.add_goal(4, Box::new(RaiderMoveThroughVillageGoal::new(1.05)));
|
||||||
goal_selector.add_goal(4, Box::new(PathfindToRaidGoal::default()));
|
goal_selector.add_goal(4, Box::new(PathfindToRaidGoal::default()));
|
||||||
@ -83,6 +97,52 @@ impl PillagerEntity {
|
|||||||
|
|
||||||
mob_arc
|
mob_arc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_charging_crossbow(&self) -> bool {
|
||||||
|
self.is_charging_crossbow.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_charging_crossbow(&self, is_charging: bool) {
|
||||||
|
self.is_charging_crossbow
|
||||||
|
.store(is_charging, Ordering::Relaxed);
|
||||||
|
self.mob_entity.living_entity.entity.send_meta_data(
|
||||||
|
&[Metadata::new(
|
||||||
|
tracked_data::pillager::IS_CHARGING_CROSSBOW,
|
||||||
|
is_charging,
|
||||||
|
)],
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn add_to_inventory(&self, item: ItemStack) -> Option<ItemStack> {
|
||||||
|
let mut inv = self.inventory.lock().await;
|
||||||
|
if inv.len() < Self::INVENTORY_SIZE {
|
||||||
|
inv.push(item);
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn drop_inventory(&self) {
|
||||||
|
let items = {
|
||||||
|
let mut inv = self.inventory.lock().await;
|
||||||
|
std::mem::take(&mut *inv)
|
||||||
|
};
|
||||||
|
let entity = &self.mob_entity.living_entity.entity;
|
||||||
|
let world = entity.world.load();
|
||||||
|
let pos = entity.pos.load();
|
||||||
|
for item in items {
|
||||||
|
if !item.is_empty() {
|
||||||
|
let item_entity = crate::entity::item::ItemEntity::new(
|
||||||
|
Entity::new(world.clone(), pos, &EntityType::ITEM),
|
||||||
|
item,
|
||||||
|
);
|
||||||
|
world.spawn_entity(Arc::new(item_entity)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Mob for PillagerEntity {
|
impl Mob for PillagerEntity {
|
||||||
@ -98,15 +158,74 @@ impl Mob for PillagerEntity {
|
|||||||
Some(self)
|
Some(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn as_crossbow_attack_mob(&self) -> Option<&dyn CrossbowAttackMob> {
|
||||||
|
Some(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mob_init_data_tracker(&self) -> EntityBaseFuture<'_, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
let entity = self.get_entity();
|
||||||
|
if self.is_charging_crossbow() {
|
||||||
|
entity.send_meta_data(
|
||||||
|
&[Metadata::new(
|
||||||
|
tracked_data::pillager::IS_CHARGING_CROSSBOW,
|
||||||
|
true,
|
||||||
|
)],
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn mob_write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
|
fn mob_write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
self.write_raider_nbt(nbt);
|
self.write_raider_nbt(nbt);
|
||||||
|
nbt.put_bool("CanPickUpLoot", true);
|
||||||
|
|
||||||
|
let inv = self.inventory.lock().await;
|
||||||
|
if !inv.is_empty() {
|
||||||
|
let mut items_tag = Vec::new();
|
||||||
|
for item in inv.iter() {
|
||||||
|
if !item.is_empty() {
|
||||||
|
let mut item_nbt = NbtCompound::new();
|
||||||
|
item.write_item_stack(&mut item_nbt);
|
||||||
|
items_tag.push(NbtTag::Compound(item_nbt));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !items_tag.is_empty() {
|
||||||
|
nbt.put_list("Inventory", items_tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mob_read_nbt<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
|
fn mob_read_nbt<'a>(&'a self, nbt: &'a NbtCompound) -> NbtFuture<'a, ()> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
self.read_raider_nbt(nbt);
|
self.read_raider_nbt(nbt);
|
||||||
|
|
||||||
|
if let Some(inv_list) = nbt.get_list("Inventory") {
|
||||||
|
let mut inv = self.inventory.lock().await;
|
||||||
|
inv.clear();
|
||||||
|
for tag in inv_list {
|
||||||
|
if let Some(compound) = tag.extract_compound()
|
||||||
|
&& let Some(stack) = ItemStack::read_item_stack(compound)
|
||||||
|
{
|
||||||
|
inv.push(stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_damage<'a>(
|
||||||
|
&'a self,
|
||||||
|
_damage_type: pumpkin_data::damage::DamageType,
|
||||||
|
_source: Option<&'a dyn EntityBase>,
|
||||||
|
) -> EntityBaseFuture<'a, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
if self.mob_entity.living_entity.dead.load(Ordering::Relaxed) {
|
||||||
|
self.drop_inventory().await;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -126,3 +245,13 @@ impl Raider for PillagerEntity {
|
|||||||
Sound::EntityPillagerCelebrate
|
Sound::EntityPillagerCelebrate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl CrossbowAttackMob for PillagerEntity {
|
||||||
|
fn set_charging_crossbow(&self, is_charging: bool) {
|
||||||
|
self.set_charging_crossbow(is_charging);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_charging_crossbow(&self) -> bool {
|
||||||
|
self.is_charging_crossbow()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,17 +1,22 @@
|
|||||||
use std::sync::{Arc, Weak};
|
use std::sync::{Arc, Weak};
|
||||||
|
|
||||||
|
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::item_stack::ItemStack;
|
||||||
|
use pumpkin_util::Difficulty;
|
||||||
|
|
||||||
use crate::entity::{
|
use crate::entity::{
|
||||||
Entity,
|
Entity, EntityBaseFuture,
|
||||||
ai::goal::{
|
ai::goal::{
|
||||||
active_target::ActiveTargetGoal, bow_attack::BowAttackGoal,
|
active_target::ActiveTargetGoal, bow_attack::BowAttackGoal,
|
||||||
look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal,
|
look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal,
|
||||||
melee_attack::MeleeAttackGoal, revenge::RevengeGoal, swim::SwimGoal,
|
melee_attack::MeleeAttackGoal, revenge::RevengeGoal, swim::SwimGoal,
|
||||||
wander_around::WanderAroundGoal,
|
wander_around::WanderAroundGoal,
|
||||||
},
|
},
|
||||||
mob::{Mob, MobEntity},
|
mob::{Mob, MobEntity, equipment::RegionalDifficulty},
|
||||||
};
|
};
|
||||||
|
use crate::world::World;
|
||||||
|
|
||||||
pub mod bogged;
|
pub mod bogged;
|
||||||
pub mod parched;
|
pub mod parched;
|
||||||
@ -70,4 +75,52 @@ impl Mob for SkeletonEntityBase {
|
|||||||
fn get_mob_entity(&self) -> &MobEntity {
|
fn get_mob_entity(&self) -> &MobEntity {
|
||||||
&self.mob_entity
|
&self.mob_entity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn populate_default_equipment_slots<'a>(
|
||||||
|
&'a self,
|
||||||
|
_world: &'a Arc<World>,
|
||||||
|
difficulty: &'a RegionalDifficulty,
|
||||||
|
) -> EntityBaseFuture<'a, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
// Default armor slots (super.populateDefaultEquipmentSlots)
|
||||||
|
if rand::random::<f32>()
|
||||||
|
< MobEntity::MAX_WEARING_ARMOR_CHANCE * difficulty.special_multiplier
|
||||||
|
{
|
||||||
|
let mut armor_type = rand::random_range(0..3);
|
||||||
|
for _ in 1..=3 {
|
||||||
|
if rand::random::<f32>() < MobEntity::WEARING_ARMOR_UPGRADE_MATERIAL_CHANCE {
|
||||||
|
armor_type += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let partial_chance = if difficulty.base_difficulty == Difficulty::Hard {
|
||||||
|
0.1f32
|
||||||
|
} else {
|
||||||
|
0.25f32
|
||||||
|
};
|
||||||
|
|
||||||
|
let living = &self.mob_entity.living_entity;
|
||||||
|
let mut equipment = living.entity_equipment.lock().await;
|
||||||
|
let mut first = true;
|
||||||
|
|
||||||
|
for slot in &MobEntity::EQUIPMENT_POPULATION_ORDER {
|
||||||
|
let current = equipment.get(slot);
|
||||||
|
if !first && rand::random::<f32>() < partial_chance {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
if current.is_empty()
|
||||||
|
&& let Some(item) = MobEntity::get_equipment_for_slot(slot, armor_type)
|
||||||
|
{
|
||||||
|
equipment.put(slot, ItemStack::new(1, item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AbstractSkeleton sets BOW on MAIN_HAND
|
||||||
|
let living = &self.mob_entity.living_entity;
|
||||||
|
let mut equipment = living.entity_equipment.lock().await;
|
||||||
|
equipment.put(&EquipmentSlot::MAIN_HAND, ItemStack::new(1, &Item::BOW));
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::entity::{
|
use crate::entity::{
|
||||||
Entity,
|
Entity, EntityBaseFuture,
|
||||||
mob::{Mob, MobEntity, skeleton::SkeletonEntityBase},
|
mob::{Mob, MobEntity, equipment::RegionalDifficulty, skeleton::SkeletonEntityBase},
|
||||||
};
|
};
|
||||||
|
use crate::world::World;
|
||||||
|
|
||||||
pub struct SkeletonEntity {
|
pub struct SkeletonEntity {
|
||||||
entity: Arc<SkeletonEntityBase>,
|
entity: Arc<SkeletonEntityBase>,
|
||||||
@ -21,4 +22,21 @@ impl Mob for SkeletonEntity {
|
|||||||
fn get_mob_entity(&self) -> &MobEntity {
|
fn get_mob_entity(&self) -> &MobEntity {
|
||||||
&self.entity.mob_entity
|
&self.entity.mob_entity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn populate_default_equipment_slots<'a>(
|
||||||
|
&'a self,
|
||||||
|
world: &'a Arc<World>,
|
||||||
|
difficulty: &'a RegionalDifficulty,
|
||||||
|
) -> EntityBaseFuture<'a, ()> {
|
||||||
|
self.entity
|
||||||
|
.populate_default_equipment_slots(world, difficulty)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn populate_default_equipment_enchantments<'a>(
|
||||||
|
&'a self,
|
||||||
|
difficulty: &'a RegionalDifficulty,
|
||||||
|
) -> EntityBaseFuture<'a, ()> {
|
||||||
|
self.entity
|
||||||
|
.populate_default_equipment_enchantments(difficulty)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,11 +11,19 @@ use crate::entity::{
|
|||||||
Entity,
|
Entity,
|
||||||
ai::goal::{Goal, active_target::ActiveTargetGoal, look_at_entity::LookAtEntityGoal},
|
ai::goal::{Goal, active_target::ActiveTargetGoal, look_at_entity::LookAtEntityGoal},
|
||||||
};
|
};
|
||||||
|
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::item_stack::ItemStack;
|
||||||
use pumpkin_nbt::compound::NbtCompound;
|
use pumpkin_nbt::compound::NbtCompound;
|
||||||
|
use pumpkin_util::Difficulty;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::{Arc, Weak};
|
use std::sync::{Arc, Weak};
|
||||||
|
|
||||||
|
use crate::entity::EntityBaseFuture;
|
||||||
|
use crate::entity::mob::equipment::RegionalDifficulty;
|
||||||
|
use crate::world::World;
|
||||||
|
|
||||||
pub mod drowned;
|
pub mod drowned;
|
||||||
pub mod husk;
|
pub mod husk;
|
||||||
#[allow(clippy::module_inception)]
|
#[allow(clippy::module_inception)]
|
||||||
@ -127,6 +135,66 @@ impl Mob for ZombieEntityBase {
|
|||||||
&self.mob_entity
|
&self.mob_entity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn populate_default_equipment_slots<'a>(
|
||||||
|
&'a self,
|
||||||
|
_world: &'a Arc<World>,
|
||||||
|
difficulty: &'a RegionalDifficulty,
|
||||||
|
) -> EntityBaseFuture<'a, ()> {
|
||||||
|
Box::pin(async move {
|
||||||
|
// Default armor slots (super.populateDefaultEquipmentSlots)
|
||||||
|
if rand::random::<f32>()
|
||||||
|
< MobEntity::MAX_WEARING_ARMOR_CHANCE * difficulty.special_multiplier
|
||||||
|
{
|
||||||
|
let mut armor_type = rand::random_range(0..3);
|
||||||
|
for _ in 1..=3 {
|
||||||
|
if rand::random::<f32>() < MobEntity::WEARING_ARMOR_UPGRADE_MATERIAL_CHANCE {
|
||||||
|
armor_type += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let partial_chance = if difficulty.base_difficulty == Difficulty::Hard {
|
||||||
|
0.1f32
|
||||||
|
} else {
|
||||||
|
0.25f32
|
||||||
|
};
|
||||||
|
|
||||||
|
let living = &self.mob_entity.living_entity;
|
||||||
|
let mut equipment = living.entity_equipment.lock().await;
|
||||||
|
let mut first = true;
|
||||||
|
|
||||||
|
for slot in &MobEntity::EQUIPMENT_POPULATION_ORDER {
|
||||||
|
let current = equipment.get(slot);
|
||||||
|
if !first && rand::random::<f32>() < partial_chance {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
first = false;
|
||||||
|
if current.is_empty()
|
||||||
|
&& let Some(item) = MobEntity::get_equipment_for_slot(slot, armor_type)
|
||||||
|
{
|
||||||
|
equipment.put(slot, ItemStack::new(1, item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let weapon_chance = if difficulty.base_difficulty == Difficulty::Hard {
|
||||||
|
0.05f32
|
||||||
|
} else {
|
||||||
|
0.01f32
|
||||||
|
};
|
||||||
|
if rand::random::<f32>() < weapon_chance {
|
||||||
|
let r = rand::random_range(0..6);
|
||||||
|
let weapon_item = match r {
|
||||||
|
0 => &Item::IRON_SWORD,
|
||||||
|
1 => &Item::IRON_SPEAR,
|
||||||
|
_ => &Item::IRON_SHOVEL,
|
||||||
|
};
|
||||||
|
let living = &self.mob_entity.living_entity;
|
||||||
|
let mut equipment = living.entity_equipment.lock().await;
|
||||||
|
equipment.put(&EquipmentSlot::MAIN_HAND, ItemStack::new(1, weapon_item));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn mob_write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
|
fn mob_write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
if self.can_break_doors() {
|
if self.can_break_doors() {
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
|
use crate::entity::mob::equipment::RegionalDifficulty;
|
||||||
use crate::entity::mob::zombie::ZombieEntityBase;
|
use crate::entity::mob::zombie::ZombieEntityBase;
|
||||||
use crate::entity::mob::{Mob, MobEntity};
|
use crate::entity::mob::{Mob, MobEntity};
|
||||||
use crate::entity::{Entity, NbtFuture};
|
use crate::entity::{Entity, EntityBaseFuture, NbtFuture};
|
||||||
|
use crate::world::World;
|
||||||
use pumpkin_nbt::compound::NbtCompound;
|
use pumpkin_nbt::compound::NbtCompound;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
@ -28,6 +30,23 @@ impl Mob for ZombieEntity {
|
|||||||
&self.entity.mob_entity
|
&self.entity.mob_entity
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn populate_default_equipment_slots<'a>(
|
||||||
|
&'a self,
|
||||||
|
world: &'a Arc<World>,
|
||||||
|
difficulty: &'a RegionalDifficulty,
|
||||||
|
) -> EntityBaseFuture<'a, ()> {
|
||||||
|
self.entity
|
||||||
|
.populate_default_equipment_slots(world, difficulty)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn populate_default_equipment_enchantments<'a>(
|
||||||
|
&'a self,
|
||||||
|
difficulty: &'a RegionalDifficulty,
|
||||||
|
) -> EntityBaseFuture<'a, ()> {
|
||||||
|
self.entity
|
||||||
|
.populate_default_equipment_enchantments(difficulty)
|
||||||
|
}
|
||||||
|
|
||||||
fn mob_write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
|
fn mob_write_nbt<'a>(&'a self, nbt: &'a mut NbtCompound) -> NbtFuture<'a, ()> {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
self.entity.mob_write_nbt(nbt).await;
|
self.entity.mob_write_nbt(nbt).await;
|
||||||
|
|||||||
@ -1784,8 +1784,29 @@ impl Player {
|
|||||||
|
|
||||||
let respawn_guard = self.respawn_point.lock().await;
|
let respawn_guard = self.respawn_point.lock().await;
|
||||||
let respawn_point = respawn_guard.as_ref()?;
|
let respawn_point = respawn_guard.as_ref()?;
|
||||||
let world = self.world();
|
let world = if self.world().dimension == respawn_point.dimension {
|
||||||
|
self.world()
|
||||||
|
} else if let Some(server) = self.world().server.upgrade() {
|
||||||
|
server.get_world_from_dimension(&respawn_point.dimension)
|
||||||
|
} else {
|
||||||
|
self.world()
|
||||||
|
};
|
||||||
let pos = &respawn_point.position;
|
let pos = &respawn_point.position;
|
||||||
|
|
||||||
|
// Ensure chunks around the spawn position are fetched
|
||||||
|
let min_chunk_x = (pos.0.x - 2) >> 4;
|
||||||
|
let max_chunk_x = (pos.0.x + 2) >> 4;
|
||||||
|
let min_chunk_z = (pos.0.z - 2) >> 4;
|
||||||
|
let max_chunk_z = (pos.0.z + 2) >> 4;
|
||||||
|
for cx in min_chunk_x..=max_chunk_x {
|
||||||
|
for cz in min_chunk_z..=max_chunk_z {
|
||||||
|
world
|
||||||
|
.level
|
||||||
|
.get_or_fetch_chunk(Vector2::new(cx, cz), |_| ())
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let (block, state_id) = world.get_block_and_state_id(pos);
|
let (block, state_id) = world.get_block_and_state_id(pos);
|
||||||
|
|
||||||
// If force is set (from /spawnpoint command), validate position is safe
|
// If force is set (from /spawnpoint command), validate position is safe
|
||||||
|
|||||||
@ -14,3 +14,15 @@ pub struct PiglinBarterEvent {
|
|||||||
/// The outcome item stacks produced by the barter.
|
/// The outcome item stacks produced by the barter.
|
||||||
pub outcome: Vec<ItemStack>,
|
pub outcome: Vec<ItemStack>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PiglinBarterEvent {
|
||||||
|
#[must_use]
|
||||||
|
pub const fn new(entity_id: i32, input_item: ItemStack, outcome: Vec<ItemStack>) -> Self {
|
||||||
|
Self {
|
||||||
|
cancelled: false,
|
||||||
|
entity_id,
|
||||||
|
input_item,
|
||||||
|
outcome,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -3945,11 +3945,18 @@ impl World {
|
|||||||
|
|
||||||
let data_kept = u8::from(alive);
|
let data_kept = u8::from(alive);
|
||||||
|
|
||||||
// Copy spawn info from level_info to avoid holding lock across await
|
let server = self.server.upgrade();
|
||||||
let (spawn_x, spawn_z, spawn_yaw, spawn_pitch, keep_inventory) = {
|
let default_world = server.as_ref().map_or_else(
|
||||||
let info = self.level_info.load();
|
|| self.clone(),
|
||||||
|
|s| s.get_world_from_dimension(&Dimension::OVERWORLD),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Copy spawn info from default world level_info to avoid holding lock across await
|
||||||
|
let (spawn_x, spawn_y, spawn_z, spawn_yaw, spawn_pitch, keep_inventory) = {
|
||||||
|
let info = default_world.level_info.load();
|
||||||
(
|
(
|
||||||
info.spawn_x,
|
info.spawn_x,
|
||||||
|
info.spawn_y,
|
||||||
info.spawn_z,
|
info.spawn_z,
|
||||||
info.spawn_yaw,
|
info.spawn_yaw,
|
||||||
info.spawn_pitch,
|
info.spawn_pitch,
|
||||||
@ -3958,8 +3965,9 @@ impl World {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Get respawn position and dimension
|
// Get respawn position and dimension
|
||||||
let (position, yaw, pitch, respawn_dimension) =
|
let (position, yaw, pitch, respawn_dimension) = if let Some(respawn) =
|
||||||
if let Some(respawn) = player.calculate_respawn_point().await {
|
player.calculate_respawn_point().await
|
||||||
|
{
|
||||||
(
|
(
|
||||||
respawn.position,
|
respawn.position,
|
||||||
respawn.yaw,
|
respawn.yaw,
|
||||||
@ -3967,27 +3975,43 @@ impl World {
|
|||||||
respawn.dimension,
|
respawn.dimension,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// No valid respawn point - send notification and use world spawn
|
// No valid respawn point - send notification if player had one set
|
||||||
|
if player.respawn_point.lock().await.is_some() {
|
||||||
player
|
player
|
||||||
.send_client_packet(&CGameEvent::new(GameEvent::NoRespawnBlockAvailable, 0.0))
|
.send_client_packet(&CGameEvent::new(GameEvent::NoRespawnBlockAvailable, 0.0))
|
||||||
.await;
|
.await;
|
||||||
|
let mut guard = player.respawn_point.lock().await;
|
||||||
|
if let Some(point) = guard.as_ref()
|
||||||
|
&& !point.force
|
||||||
|
{
|
||||||
|
*guard = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// FIXME: This spawn position calculation is incorrect. Should use vanilla's
|
// FIXME: This spawn position calculation is incorrect. Should use vanilla's
|
||||||
// proper spawn position calculation (see #1381). The y-level calculation
|
// proper spawn position calculation (see #1381). The y-level calculation
|
||||||
// needs to account for spawn radius and find a safe spawn position.
|
// needs to account for spawn radius and find a safe spawn position.
|
||||||
let chunk_pos = Vector2::new(spawn_x >> 4, spawn_z >> 4);
|
let chunk_pos = Vector2::new(spawn_x >> 4, spawn_z >> 4);
|
||||||
self.level.get_or_fetch_chunk(chunk_pos, |_| ()).await;
|
default_world
|
||||||
let top = self.get_top_block(Vector2::new(spawn_x, spawn_z));
|
.level
|
||||||
|
.get_or_fetch_chunk(chunk_pos, |_| ())
|
||||||
|
.await;
|
||||||
|
let top = default_world.get_top_block(Vector2::new(spawn_x, spawn_z));
|
||||||
|
let pos_y = if top > default_world.dimension.min_y {
|
||||||
|
top + 1
|
||||||
|
} else {
|
||||||
|
spawn_y
|
||||||
|
};
|
||||||
|
|
||||||
(
|
(
|
||||||
Vector3::new(
|
Vector3::new(
|
||||||
f64::from(spawn_x) + 0.5,
|
f64::from(spawn_x) + 0.5,
|
||||||
(top + 1).into(),
|
f64::from(pos_y),
|
||||||
f64::from(spawn_z) + 0.5,
|
f64::from(spawn_z) + 0.5,
|
||||||
),
|
),
|
||||||
spawn_yaw,
|
spawn_yaw,
|
||||||
spawn_pitch,
|
spawn_pitch,
|
||||||
self.dimension.clone(),
|
default_world.dimension.clone(),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -3995,11 +4019,8 @@ impl World {
|
|||||||
player.clone(),
|
player.clone(),
|
||||||
position,
|
position,
|
||||||
);
|
);
|
||||||
if let Some(server) = self.server.upgrade() {
|
if let Some(ref s) = server {
|
||||||
server
|
s.plugin_manager.fire(s, &mut spawn_loc_event).await;
|
||||||
.plugin_manager
|
|
||||||
.fire(&server, &mut spawn_loc_event)
|
|
||||||
.await;
|
|
||||||
}
|
}
|
||||||
let position = spawn_loc_event.spawn_pos;
|
let position = spawn_loc_event.spawn_pos;
|
||||||
|
|
||||||
@ -4007,13 +4028,13 @@ impl World {
|
|||||||
let candidate_world = if respawn_dimension == self.dimension {
|
let candidate_world = if respawn_dimension == self.dimension {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
self.server.upgrade().map_or_else(
|
server.as_ref().map_or_else(
|
||||||
|| {
|
|| {
|
||||||
warn!("Could not get server for cross-dimension respawn");
|
warn!("Could not get server for cross-dimension respawn");
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
|server| {
|
|s| {
|
||||||
let worlds = server.worlds.load();
|
let worlds = s.worlds.load();
|
||||||
worlds
|
worlds
|
||||||
.iter()
|
.iter()
|
||||||
.find(|w| w.dimension == respawn_dimension)
|
.find(|w| w.dimension == respawn_dimension)
|
||||||
@ -4025,7 +4046,7 @@ impl World {
|
|||||||
// Fire PlayerChangeWorldEvent (cancellable) before the transfer; it runs before
|
// Fire PlayerChangeWorldEvent (cancellable) before the transfer; it runs before
|
||||||
// the non-cancellable PlayerRespawnEvent, which observes the resolved world.
|
// the non-cancellable PlayerRespawnEvent, which observes the resolved world.
|
||||||
let (resolved_world, position, yaw, pitch) = if let Some(new_world) = candidate_world {
|
let (resolved_world, position, yaw, pitch) = if let Some(new_world) = candidate_world {
|
||||||
if let Some(server) = self.server.upgrade() {
|
if let Some(ref s) = server {
|
||||||
let mut event = PlayerChangeWorldEvent {
|
let mut event = PlayerChangeWorldEvent {
|
||||||
player: player.clone(),
|
player: player.clone(),
|
||||||
previous_world: self.clone(),
|
previous_world: self.clone(),
|
||||||
@ -4035,7 +4056,7 @@ impl World {
|
|||||||
pitch,
|
pitch,
|
||||||
cancelled: false,
|
cancelled: false,
|
||||||
};
|
};
|
||||||
server.plugin_manager.fire(&server, &mut event).await;
|
s.plugin_manager.fire(s, &mut event).await;
|
||||||
|
|
||||||
if event.cancelled {
|
if event.cancelled {
|
||||||
(None, position, yaw, pitch)
|
(None, position, yaw, pitch)
|
||||||
@ -4087,23 +4108,10 @@ impl World {
|
|||||||
|
|
||||||
// Cancelled or unresolved cross-dimension respawns fall back to the current
|
// Cancelled or unresolved cross-dimension respawns fall back to the current
|
||||||
// world's spawn below; otherwise the resolved values from the event apply.
|
// world's spawn below; otherwise the resolved values from the event apply.
|
||||||
let (target_world, position, yaw, pitch) = if let Some(ref new_world) = resolved_world {
|
let (target_world, position, yaw, pitch) = resolved_world.as_ref().map_or_else(
|
||||||
(new_world.clone(), position, yaw, pitch)
|
|| (self.clone(), position, yaw, pitch),
|
||||||
} else if respawn_dimension != self.dimension {
|
|new_world| (new_world.clone(), position, yaw, pitch),
|
||||||
// FIXME: This spawn position calculation is incorrect. Should use vanilla's
|
|
||||||
// proper spawn position calculation (see #1381).
|
|
||||||
let chunk_pos = Vector2::new(spawn_x >> 4, spawn_z >> 4);
|
|
||||||
self.level.get_or_fetch_chunk(chunk_pos, |_| ()).await;
|
|
||||||
let top = self.get_top_block(Vector2::new(spawn_x, spawn_z));
|
|
||||||
let fallback_pos = Vector3::new(
|
|
||||||
f64::from(spawn_x) + 0.5,
|
|
||||||
(top + 1).into(),
|
|
||||||
f64::from(spawn_z) + 0.5,
|
|
||||||
);
|
);
|
||||||
(self.clone(), fallback_pos, spawn_yaw, spawn_pitch)
|
|
||||||
} else {
|
|
||||||
(self.clone(), position, yaw, pitch)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Notify plugins that the player has respawned (non-cancellable).
|
// Notify plugins that the player has respawned (non-cancellable).
|
||||||
if let Some(server) = self.server.upgrade() {
|
if let Some(server) = self.server.upgrade() {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user