mirror of
https://github.com/Pumpkin-MC/Pumpkin.git
synced 2026-08-30 20:14:23 +00:00
fix: respawn in nether
This commit is contained in:
@@ -55,7 +55,7 @@ impl BlockBehaviour for RespawnAnchorBlock {
|
||||
fn normal_use<'a>(&'a self, args: NormalUseArgs<'a>) -> BlockFuture<'a, BlockActionResult> {
|
||||
Box::pin(async move {
|
||||
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 {
|
||||
args.world
|
||||
@@ -92,15 +92,6 @@ impl BlockBehaviour for RespawnAnchorBlock {
|
||||
)
|
||||
.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(
|
||||
Sound::BlockRespawnAnchorSetSpawn,
|
||||
SoundCategory::Blocks,
|
||||
|
||||
@@ -1,18 +1,37 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::block::entities::mob_spawner::MobSpawnerBlockEntity;
|
||||
use crate::entity::experience_orb::ExperienceOrbEntity;
|
||||
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")]
|
||||
pub struct 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, ()> {
|
||||
Box::pin(async move {
|
||||
let hopper_block_entity = MobSpawnerBlockEntity::new(*args.position, None);
|
||||
args.world.add_block_entity(Arc::new(hopper_block_entity));
|
||||
let spawner_block_entity = MobSpawnerBlockEntity::new(*args.position, None);
|
||||
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,
|
||||
};
|
||||
|
||||
use crate::{block::entities::BlockEntity, world::World};
|
||||
use crate::{block::entities::BlockEntity, entity::EntityBase, world::World};
|
||||
|
||||
pub struct MobSpawnerBlockEntity {
|
||||
pub position: BlockPos,
|
||||
@@ -24,6 +24,8 @@ pub struct MobSpawnerBlockEntity {
|
||||
pub min_delay: i32,
|
||||
pub spawn_count: i32,
|
||||
pub spawn_range: i32,
|
||||
pub max_nearby_entities: i32,
|
||||
pub required_player_range: i32,
|
||||
pub entity_type: AtomicCell<Option<&'static EntityType>>,
|
||||
}
|
||||
|
||||
@@ -34,6 +36,8 @@ impl MobSpawnerBlockEntity {
|
||||
pub const DEFAULT_MIN_SPAWN_DELAY: i32 = 200;
|
||||
pub const DEFAULT_SPAWN_COUNT: 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]
|
||||
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,
|
||||
spawn_count: Self::DEFAULT_SPAWN_COUNT,
|
||||
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),
|
||||
}
|
||||
}
|
||||
@@ -55,6 +61,14 @@ impl MobSpawnerBlockEntity {
|
||||
nbt.put_int("x", position.0.x);
|
||||
nbt.put_int("y", position.0.y);
|
||||
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() {
|
||||
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>> {
|
||||
Box::pin(async move {
|
||||
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;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
if self.delay.load(Ordering::Relaxed) > 0 {
|
||||
self.delay.fetch_sub(1, Ordering::Relaxed);
|
||||
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 mut update_spawns = false;
|
||||
let mut spawned_any = false;
|
||||
for _ in 0..self.spawn_count {
|
||||
let pos = self.position.0;
|
||||
|
||||
let spawn_pos = Vector3::new(
|
||||
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,
|
||||
(pos.y + rand::random_range(0..3) - 1) 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,
|
||||
);
|
||||
// TODO: we should use getSpawnBox, but this is only modified for slimes and magma slimes
|
||||
@@ -140,11 +190,13 @@ impl BlockEntity for MobSpawnerBlockEntity {
|
||||
world,
|
||||
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.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;
|
||||
}
|
||||
}
|
||||
@@ -155,28 +207,51 @@ impl BlockEntity for MobSpawnerBlockEntity {
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let delay = nbt.get_short("Delay").unwrap_or(Self::DEFAULT_DELAY as i16) as i32;
|
||||
let min_delay = nbt
|
||||
.get_int("MinSpawnDelay")
|
||||
.unwrap_or(Self::DEFAULT_MIN_SPAWN_DELAY);
|
||||
let max_delay = nbt
|
||||
.get_int("MaxSpawnDelay")
|
||||
.unwrap_or(Self::DEFAULT_MAX_SPAWN_DELAY);
|
||||
let spawn_count = nbt
|
||||
.get_int("SpawnCount")
|
||||
.unwrap_or(Self::DEFAULT_SPAWN_COUNT);
|
||||
let spawn_range = nbt
|
||||
.get_int("SpawnRange")
|
||||
.unwrap_or(Self::DEFAULT_SPAWN_RANGE);
|
||||
let get_num = |name: &str| {
|
||||
nbt.get_short(name)
|
||||
.map(i32::from)
|
||||
.or_else(|| nbt.get_int(name))
|
||||
.or_else(|| nbt.get_byte(name).map(i32::from))
|
||||
};
|
||||
|
||||
let delay = get_num("Delay").unwrap_or(Self::DEFAULT_DELAY);
|
||||
let min_delay = get_num("MinSpawnDelay").unwrap_or(Self::DEFAULT_MIN_SPAWN_DELAY);
|
||||
let max_delay = get_num("MaxSpawnDelay").unwrap_or(Self::DEFAULT_MAX_SPAWN_DELAY);
|
||||
let spawn_count = get_num("SpawnCount").unwrap_or(Self::DEFAULT_SPAWN_COUNT);
|
||||
let spawn_range = get_num("SpawnRange").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
|
||||
.get_compound("SpawnData")
|
||||
.and_then(|data| data.get_compound("entity"))
|
||||
.and_then(|entity| entity.get_string("id"))
|
||||
.and_then(|id| {
|
||||
let name = id.strip_prefix("minecraft:").unwrap_or(id);
|
||||
EntityType::from_name(name)
|
||||
});
|
||||
.and_then(|data| {
|
||||
data.get_compound("entity")
|
||||
.and_then(|entity| entity.get_string("id"))
|
||||
.or_else(|| data.get_string("id"))
|
||||
})
|
||||
.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 {
|
||||
position,
|
||||
@@ -185,6 +260,8 @@ impl BlockEntity for MobSpawnerBlockEntity {
|
||||
min_delay,
|
||||
spawn_count,
|
||||
spawn_range,
|
||||
max_nearby_entities,
|
||||
required_player_range,
|
||||
entity_type: AtomicCell::new(entity_type),
|
||||
}
|
||||
}
|
||||
@@ -200,6 +277,14 @@ impl BlockEntity for MobSpawnerBlockEntity {
|
||||
|
||||
fn chunk_data_nbt(&self) -> Option<NbtCompound> {
|
||||
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() {
|
||||
let mut spawn_entry = NbtCompound::new();
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ pub mod pathfind_to_raid;
|
||||
pub mod pick_up_block;
|
||||
pub mod place_block;
|
||||
pub mod ranged_attack;
|
||||
pub mod ranged_crossbow_attack;
|
||||
pub mod revenge;
|
||||
pub mod step_and_destroy_block;
|
||||
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 pumpkin_data::damage::DamageType;
|
||||
use pumpkin_data::sound::Sound;
|
||||
use pumpkin_data::sound::{Sound, SoundCategory};
|
||||
use pumpkin_data::tag::{self, Taggable};
|
||||
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 pumpkin_world::chunk::ChunkHeightmapType;
|
||||
@@ -33,64 +35,197 @@ impl BatEntity {
|
||||
let bat = Self {
|
||||
mob_entity,
|
||||
hanging_position: Mutex::new(None),
|
||||
roosting: AtomicBool::new(false),
|
||||
roosting: AtomicBool::new(true),
|
||||
ambient_sound_chance: AtomicI32::new(MIN_AMBIENT_SOUND_DELAY),
|
||||
};
|
||||
Arc::new(bat)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
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)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if rand::random_bool(1.0) {
|
||||
if rand::random_bool(0.5) {
|
||||
return false;
|
||||
}
|
||||
if world.get_max_local_raw_brightness(pos) > rand::random_range(0..4) {
|
||||
return false;
|
||||
}
|
||||
if world
|
||||
.get_block(pos)
|
||||
let below_pos = BlockPos::new(pos.0.x, pos.0.y - 1, pos.0.z);
|
||||
if !world
|
||||
.get_block(&below_pos)
|
||||
.has_tag(&tag::Block::MINECRAFT_BATS_SPAWNABLE_ON)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
//TODO:check_mob_spawn_rules(entity_type, world, spawn_reason, pos).await
|
||||
true
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_roosting(&self) -> bool {
|
||||
self.roosting.load(Relaxed)
|
||||
}
|
||||
|
||||
pub fn set_roosting(&self, roosting: bool) {
|
||||
self.roosting.store(roosting, Relaxed);
|
||||
// TODO:
|
||||
// let flags = if roosting { ROOSTING_FLAG } else { 0 };
|
||||
// self.mob_entity
|
||||
// .living_entity
|
||||
// .entity
|
||||
// .send_meta_data(&[Metadata::new(
|
||||
// pumpkin_data::tracked_data::bat::ID_FLAGS,
|
||||
// MetaDataType::BYTE,
|
||||
// flags,
|
||||
// )])
|
||||
// .await;
|
||||
let flags: u8 = if roosting { ROOSTING_FLAG } else { 0 };
|
||||
self.mob_entity.living_entity.entity.send_meta_data(
|
||||
&[Metadata::new(tracked_data::bat::DATA_ID_FLAGS, flags)],
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
fn tick_ambient_sound(&self, world: &World, pos: &Vector3<f64>) {
|
||||
let chance = self.ambient_sound_chance.fetch_sub(1, Relaxed);
|
||||
if chance <= 0 {
|
||||
self.ambient_sound_chance
|
||||
.store(MIN_AMBIENT_SOUND_DELAY, Relaxed);
|
||||
if !self.is_roosting() || rand::random_range(0..4) == 0 {
|
||||
world.play_sound_fine(
|
||||
Sound::EntityBatAmbient,
|
||||
SoundCategory::Ambient,
|
||||
pos,
|
||||
0.1,
|
||||
0.95,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn tick_roosting(&self, world: &World, above_pos: &BlockPos, pos: &Vector3<f64>) {
|
||||
let entity = &self.mob_entity.living_entity.entity;
|
||||
let above_state = world.get_block_state(above_pos);
|
||||
if above_state.is_solid_block() {
|
||||
let rotate_head = {
|
||||
let mut rng = rand::rng();
|
||||
(rng.random_range(0u32..200) == 0).then(|| rng.random_range(0i32..360) as f32)
|
||||
};
|
||||
if let Some(head_yaw) = rotate_head {
|
||||
entity.head_yaw.store(head_yaw);
|
||||
}
|
||||
|
||||
if world
|
||||
.get_closest_player(*pos, CLOSE_PLAYER_DISTANCE)
|
||||
.is_some()
|
||||
{
|
||||
self.set_roosting(false);
|
||||
world.play_sound_fine(
|
||||
Sound::EntityBatTakeoff,
|
||||
SoundCategory::Ambient,
|
||||
pos,
|
||||
0.1,
|
||||
0.95,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
self.set_roosting(false);
|
||||
world.play_sound_fine(
|
||||
Sound::EntityBatTakeoff,
|
||||
SoundCategory::Ambient,
|
||||
pos,
|
||||
0.1,
|
||||
0.95,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
if let Some(hp) = *hanging_pos {
|
||||
let hp_state = world.get_block_state(&hp);
|
||||
if !hp_state.is_air() || hp.0.y <= world.dimension.min_y {
|
||||
*hanging_pos = None;
|
||||
}
|
||||
}
|
||||
|
||||
let (should_pick_new, new_target, try_roost) = {
|
||||
let mut rng = rand::rng();
|
||||
let should_pick = hanging_pos.is_none()
|
||||
|| rng.random_range(0u32..30) == 0
|
||||
|| hanging_pos.is_some_and(|hp| {
|
||||
let dx = f64::from(hp.0.x) + 0.5 - pos.x;
|
||||
let dy = f64::from(hp.0.y) + 0.1 - pos.y;
|
||||
let dz = f64::from(hp.0.z) + 0.5 - pos.z;
|
||||
dx * dx + dy * dy + dz * dz < 4.0
|
||||
});
|
||||
let new_target = should_pick.then(|| {
|
||||
BlockPos::new(
|
||||
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.z as i32 + rng.random_range(0i32..7) - rng.random_range(0i32..7),
|
||||
)
|
||||
});
|
||||
let try_roost = rng.random_range(0u32..100) == 0;
|
||||
(should_pick, new_target, try_roost)
|
||||
};
|
||||
|
||||
if should_pick_new {
|
||||
if let Some(target) = new_target {
|
||||
let target_state = world.get_block_state(&target);
|
||||
if target_state.is_air() && target.0.y > world.dimension.min_y {
|
||||
*hanging_pos = Some(target);
|
||||
} else {
|
||||
*hanging_pos = None;
|
||||
}
|
||||
} else {
|
||||
*hanging_pos = None;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(target) = *hanging_pos {
|
||||
let d = f64::from(target.0.x) + 0.5 - pos.x;
|
||||
let e = f64::from(target.0.y) + 0.1 - pos.y;
|
||||
let f = f64::from(target.0.z) + 0.5 - pos.z;
|
||||
|
||||
let velo = entity.velocity.load();
|
||||
let new_velo = Vector3::new(
|
||||
velo.x + (d.signum() * 0.5 - velo.x) * 0.1,
|
||||
velo.y + (e.signum() * 0.7 - velo.y) * 0.1,
|
||||
velo.z + (f.signum() * 0.5 - velo.z) * 0.1,
|
||||
);
|
||||
entity.velocity.store(new_velo);
|
||||
|
||||
let yaw = (new_velo.z.atan2(new_velo.x) as f32).to_degrees() - 90.0;
|
||||
let yaw_diff = pumpkin_util::math::wrap_degrees(yaw - entity.yaw.load());
|
||||
entity.yaw.store(entity.yaw.load() + yaw_diff);
|
||||
}
|
||||
drop(hanging_pos);
|
||||
|
||||
if try_roost {
|
||||
let above_state = world.get_block_state(above_pos);
|
||||
if above_state.is_solid_block() {
|
||||
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", flags as i8);
|
||||
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 = nbt.get_byte("BatFlags").unwrap_or(0) as u8;
|
||||
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);
|
||||
})
|
||||
@@ -106,110 +241,14 @@ impl Mob for BatEntity {
|
||||
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();
|
||||
|
||||
// Ambient idle sound (vanilla: MobEntity.mobTick → playAmbientSound)
|
||||
let chance = self.ambient_sound_chance.fetch_sub(1, Relaxed);
|
||||
if chance <= 0 {
|
||||
self.ambient_sound_chance
|
||||
.store(MIN_AMBIENT_SOUND_DELAY, Relaxed);
|
||||
entity.play_sound(Sound::EntityBatAmbient);
|
||||
}
|
||||
self.tick_ambient_sound(&world, &pos);
|
||||
|
||||
if self.is_roosting() {
|
||||
let above_state = world.get_block_state(&above_pos);
|
||||
if above_state.is_solid_block() {
|
||||
let rotate_head = {
|
||||
let mut rng = rand::rng();
|
||||
(rng.random_range(0u32..200) == 0)
|
||||
.then(|| rng.random_range(0i32..360) as f32)
|
||||
};
|
||||
if let Some(head_yaw) = rotate_head {
|
||||
entity.head_yaw.store(head_yaw);
|
||||
}
|
||||
|
||||
let pos = entity.pos.load();
|
||||
if world
|
||||
.get_closest_player(pos, CLOSE_PLAYER_DISTANCE)
|
||||
.is_some()
|
||||
{
|
||||
self.set_roosting(false);
|
||||
}
|
||||
} else {
|
||||
self.set_roosting(false);
|
||||
}
|
||||
self.tick_roosting(&world, &above_pos, &pos);
|
||||
} else {
|
||||
let mut hanging_pos = self.hanging_position.lock().await;
|
||||
|
||||
if let Some(hp) = *hanging_pos {
|
||||
let hp_state = world.get_block_state(&hp);
|
||||
if !hp_state.is_air() || hp.0.y <= world.dimension.min_y {
|
||||
*hanging_pos = None;
|
||||
}
|
||||
}
|
||||
|
||||
let (should_pick_new, new_target, try_roost) = {
|
||||
let mut rng = rand::rng();
|
||||
let should_pick = hanging_pos.is_none()
|
||||
|| rng.random_range(0u32..30) == 0
|
||||
|| hanging_pos.is_some_and(|hp| {
|
||||
let pos = entity.pos.load();
|
||||
let dx = f64::from(hp.0.x) + 0.5 - pos.x;
|
||||
let dy = f64::from(hp.0.y) + 0.1 - pos.y;
|
||||
let dz = f64::from(hp.0.z) + 0.5 - pos.z;
|
||||
dx * dx + dy * dy + dz * dz < 4.0
|
||||
});
|
||||
let new_target = should_pick.then(|| {
|
||||
let pos = entity.pos.load();
|
||||
BlockPos::new(
|
||||
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.z as i32 + rng.random_range(0i32..7) - rng.random_range(0i32..7),
|
||||
)
|
||||
});
|
||||
let try_roost = rng.random_range(0u32..100) == 0;
|
||||
(should_pick, new_target, try_roost)
|
||||
};
|
||||
|
||||
if should_pick_new {
|
||||
// Pre-validate: only accept targets in air (avoids water, lava, hazards)
|
||||
if let Some(target) = new_target {
|
||||
let target_state = world.get_block_state(&target);
|
||||
if target_state.is_air() && target.0.y > world.dimension.min_y {
|
||||
*hanging_pos = Some(target);
|
||||
} else {
|
||||
*hanging_pos = None;
|
||||
}
|
||||
} else {
|
||||
*hanging_pos = None;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(target) = *hanging_pos {
|
||||
let pos = entity.pos.load();
|
||||
let d = f64::from(target.0.x) + 0.5 - pos.x;
|
||||
let e = f64::from(target.0.y) + 0.1 - pos.y;
|
||||
let f = f64::from(target.0.z) + 0.5 - pos.z;
|
||||
|
||||
let velo = entity.velocity.load();
|
||||
let new_velo = Vector3::new(
|
||||
velo.x + (d.signum() * 0.5 - velo.x) * 0.1,
|
||||
velo.y + (e.signum() * 0.7 - velo.y) * 0.1,
|
||||
velo.z + (f.signum() * 0.5 - velo.z) * 0.1,
|
||||
);
|
||||
entity.velocity.store(new_velo);
|
||||
|
||||
let yaw = (new_velo.z.atan2(new_velo.x) as f32).to_degrees() - 90.0;
|
||||
let yaw_diff = pumpkin_util::math::wrap_degrees(yaw - entity.yaw.load());
|
||||
entity.yaw.store(entity.yaw.load() + yaw_diff);
|
||||
}
|
||||
drop(hanging_pos);
|
||||
|
||||
if try_roost {
|
||||
let above_state = world.get_block_state(&above_pos);
|
||||
if above_state.is_solid_block() {
|
||||
self.set_roosting(true);
|
||||
}
|
||||
}
|
||||
self.tick_flying(&world, &above_pos, &pos).await;
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -242,6 +281,15 @@ impl Mob for BatEntity {
|
||||
Box::pin(async move {
|
||||
if self.is_roosting() {
|
||||
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
|
||||
/// the level from the remaining cost. Cost is halved each iteration so
|
||||
/// later enchantments receive lower levels.
|
||||
fn apply_vanilla_enchantments(
|
||||
pub fn apply_vanilla_enchantments(
|
||||
stack: &mut ItemStack,
|
||||
slot: &EquipmentSlot,
|
||||
special_multiplier: f32,
|
||||
|
||||
@@ -1,32 +1,49 @@
|
||||
use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
|
||||
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::{
|
||||
Entity,
|
||||
Entity, EntityBase, EntityBaseFuture, NbtFuture,
|
||||
ai::goal::{Controls, Goal, GoalFuture},
|
||||
mob::{Mob, MobEntity},
|
||||
};
|
||||
use crate::world::World;
|
||||
|
||||
pub struct GhastEntity {
|
||||
pub mob_entity: MobEntity,
|
||||
pub is_charging: AtomicBool,
|
||||
pub explosion_power: AtomicU8,
|
||||
pub wanted_fly_target: Mutex<Option<Vector3<f64>>>,
|
||||
}
|
||||
|
||||
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> {
|
||||
let mob_entity = MobEntity::new(entity);
|
||||
let ghast = Self {
|
||||
mob_entity,
|
||||
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_weak: Weak<dyn Mob> = {
|
||||
let mob_arc: Arc<dyn Mob> = mob_arc.clone();
|
||||
Arc::downgrade(&mob_arc)
|
||||
};
|
||||
|
||||
{
|
||||
let mut goal_selector = mob_arc
|
||||
@@ -35,20 +52,83 @@ impl GhastEntity {
|
||||
.lock()
|
||||
.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
|
||||
}
|
||||
|
||||
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);
|
||||
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 {
|
||||
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 {
|
||||
@@ -59,21 +139,69 @@ impl Mob for GhastEntity {
|
||||
fn get_mob_gravity(&self) -> f64 {
|
||||
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 {
|
||||
goal_control: Controls,
|
||||
mob_weak: Weak<dyn Mob>,
|
||||
}
|
||||
|
||||
impl Default for GhastLookGoal {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
goal_control: Controls::LOOK,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GhastLookGoal {
|
||||
#[must_use]
|
||||
pub fn new(mob_weak: Weak<dyn Mob>) -> Self {
|
||||
Self {
|
||||
goal_control: Controls::LOOK,
|
||||
mob_weak,
|
||||
}
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +215,7 @@ impl Goal for GhastLookGoal {
|
||||
}
|
||||
|
||||
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 target_opt = mob_entity.target.lock().await.clone();
|
||||
|
||||
@@ -96,19 +224,18 @@ impl Goal for GhastLookGoal {
|
||||
let target_pos = target.get_entity().pos.load();
|
||||
|
||||
if mob_pos.squared_distance_to_vec(&target_pos) < 4096.0 {
|
||||
let mut look_control = mob_entity
|
||||
.look_control
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
look_control.look_at(mob, target_pos.x, target_pos.y, target_pos.z);
|
||||
let dx = target_pos.x - mob_pos.x;
|
||||
let dz = target_pos.z - mob_pos.z;
|
||||
let yaw = (-f64::atan2(dx, dz).to_degrees()) as f32;
|
||||
mob_entity.living_entity.entity.yaw.store(yaw);
|
||||
mob_entity.living_entity.entity.head_yaw.store(yaw);
|
||||
}
|
||||
} else {
|
||||
// If no target, face the movement direction
|
||||
let velocity = mob_entity.living_entity.entity.velocity.load();
|
||||
if velocity.x != 0.0 || velocity.z != 0.0 {
|
||||
let yaw = (-f64::atan2(velocity.x, velocity.z) * (180.0 / std::f64::consts::PI))
|
||||
as f32;
|
||||
let yaw = (-f64::atan2(velocity.x, velocity.z).to_degrees()) as f32;
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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 pumpkin_data::attributes::Attributes;
|
||||
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::tag::{self, Taggable};
|
||||
use pumpkin_data::tracked_data;
|
||||
@@ -36,6 +38,7 @@ pub mod breeze;
|
||||
pub mod cave_spider;
|
||||
pub mod creaking;
|
||||
pub mod creeper;
|
||||
pub mod crossbow_attack_mob;
|
||||
pub mod elder_guardian;
|
||||
pub mod enderman;
|
||||
pub mod endermite;
|
||||
@@ -50,6 +53,7 @@ pub mod magma_cube;
|
||||
pub mod patrol;
|
||||
pub mod phantom;
|
||||
pub mod piglin;
|
||||
pub mod piglin_ai;
|
||||
pub mod piglin_brute;
|
||||
pub mod pillager;
|
||||
pub mod raider;
|
||||
@@ -102,6 +106,65 @@ impl MobEntity {
|
||||
const ATTACKING_FLAG: u8 = 4;
|
||||
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]
|
||||
pub fn new(entity: Entity) -> Self {
|
||||
Self {
|
||||
@@ -654,6 +717,105 @@ pub trait Mob: EntityBase + Send + Sync {
|
||||
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, ()> {
|
||||
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::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::{
|
||||
Entity,
|
||||
Entity, EntityBase, EntityBaseFuture, NbtFuture,
|
||||
ai::goal::{
|
||||
active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal,
|
||||
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 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 {
|
||||
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> {
|
||||
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_weak: Weak<dyn Mob> = {
|
||||
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(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(3, Box::new(RangedCrossbowAttackGoal::new(1.0, 8.0)));
|
||||
goal_selector.add_goal(5, Box::new(WanderAroundGoal::new(1.0)));
|
||||
goal_selector.add_goal(
|
||||
6,
|
||||
@@ -49,12 +115,28 @@ impl PiglinEntity {
|
||||
.target_selector
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
target_selector.add_goal(
|
||||
1,
|
||||
ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::PLAYER, true),
|
||||
);
|
||||
|
||||
// Retaliate when attacked
|
||||
target_selector.add_goal(1, Box::new(RevengeGoal::new(true)));
|
||||
|
||||
// Hostile to players unless wearing safe gold armor
|
||||
target_selector.add_goal(
|
||||
2,
|
||||
Box::new(ActiveTargetGoal::new(
|
||||
&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(
|
||||
&mob_arc.mob_entity,
|
||||
&EntityType::WITHER_SKELETON,
|
||||
@@ -62,17 +144,561 @@ impl PiglinEntity {
|
||||
),
|
||||
);
|
||||
target_selector.add_goal(
|
||||
2,
|
||||
3,
|
||||
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
|
||||
}
|
||||
|
||||
#[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 {
|
||||
fn get_mob_entity(&self) -> &MobEntity {
|
||||
&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::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::{
|
||||
Entity,
|
||||
Entity, EntityBase, EntityBaseFuture, NbtFuture,
|
||||
ai::goal::{
|
||||
active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal,
|
||||
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},
|
||||
};
|
||||
use crate::world::World;
|
||||
|
||||
pub struct PiglinBruteEntity {
|
||||
pub mob_entity: MobEntity,
|
||||
pub immune_to_zombification: AtomicBool,
|
||||
pub time_in_overworld: AtomicI32,
|
||||
}
|
||||
|
||||
impl PiglinBruteEntity {
|
||||
pub const CONVERSION_TIME: i32 = 300;
|
||||
pub const XP_REWARD: u32 = 20;
|
||||
|
||||
pub fn new(entity: Entity) -> Arc<Self> {
|
||||
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_weak: Weak<dyn Mob> = {
|
||||
let mob_arc: Arc<dyn Mob> = mob_arc.clone();
|
||||
@@ -48,12 +68,14 @@ impl PiglinBruteEntity {
|
||||
.target_selector
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
target_selector.add_goal(1, Box::new(RevengeGoal::new(true)));
|
||||
// Piglin brutes are always hostile to players (even with gold armor)
|
||||
target_selector.add_goal(
|
||||
1,
|
||||
2,
|
||||
ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::PLAYER, true),
|
||||
);
|
||||
target_selector.add_goal(
|
||||
2,
|
||||
3,
|
||||
ActiveTargetGoal::with_default(
|
||||
&mob_arc.mob_entity,
|
||||
&EntityType::WITHER_SKELETON,
|
||||
@@ -61,17 +83,157 @@ impl PiglinBruteEntity {
|
||||
),
|
||||
);
|
||||
target_selector.add_goal(
|
||||
2,
|
||||
3,
|
||||
ActiveTargetGoal::with_default(&mob_arc.mob_entity, &EntityType::WITHER, true),
|
||||
);
|
||||
};
|
||||
|
||||
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 {
|
||||
fn get_mob_entity(&self) -> &MobEntity {
|
||||
&self.mob_entity
|
||||
}
|
||||
|
||||
fn mob_init_data_tracker(&self) -> EntityBaseFuture<'_, ()> {
|
||||
Box::pin(async move {
|
||||
let entity = self.get_entity();
|
||||
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::item_stack::ItemStack;
|
||||
use pumpkin_data::sound::Sound;
|
||||
use pumpkin_data::tracked_data;
|
||||
use pumpkin_nbt::compound::NbtCompound;
|
||||
use pumpkin_nbt::tag::NbtTag;
|
||||
use pumpkin_protocol::java::client::play::Metadata;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::entity::{
|
||||
Entity, NbtFuture,
|
||||
Entity, EntityBase, EntityBaseFuture, NbtFuture,
|
||||
ai::goal::{
|
||||
active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal,
|
||||
look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, swim::SwimGoal,
|
||||
wander_around::WanderAroundGoal,
|
||||
look_at_entity::LookAtEntityGoal, ranged_crossbow_attack::RangedCrossbowAttackGoal,
|
||||
swim::SwimGoal, wander_around::WanderAroundGoal,
|
||||
},
|
||||
mob::{
|
||||
Mob, MobEntity,
|
||||
crossbow_attack_mob::CrossbowAttackMob,
|
||||
patrol::{LongDistancePatrolGoal, PatrolData, PatrollingMonster},
|
||||
raider::{
|
||||
HoldGroundAttackGoal, ObtainRaidLeaderBannerGoal, PathfindToRaidGoal, Raider,
|
||||
@@ -24,14 +33,20 @@ use crate::entity::{
|
||||
pub struct PillagerEntity {
|
||||
pub mob_entity: MobEntity,
|
||||
pub raider_data: RaiderData,
|
||||
pub is_charging_crossbow: AtomicBool,
|
||||
pub inventory: Mutex<Vec<ItemStack>>,
|
||||
}
|
||||
|
||||
impl PillagerEntity {
|
||||
pub const INVENTORY_SIZE: usize = 5;
|
||||
|
||||
#[must_use]
|
||||
pub fn new(entity: Entity) -> Arc<Self> {
|
||||
let mob_arc = Arc::new(Self {
|
||||
mob_entity: MobEntity::new(entity),
|
||||
raider_data: RaiderData::default(),
|
||||
is_charging_crossbow: AtomicBool::new(false),
|
||||
inventory: Mutex::new(Vec::new()),
|
||||
});
|
||||
|
||||
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(1, Box::new(ObtainRaidLeaderBannerGoal));
|
||||
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(MeleeAttackGoal::new(1.0, true)));
|
||||
goal_selector.add_goal(3, Box::new(RangedCrossbowAttackGoal::new(1.0, 8.0)));
|
||||
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(PathfindToRaidGoal::default()));
|
||||
@@ -83,6 +97,52 @@ impl PillagerEntity {
|
||||
|
||||
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 {
|
||||
@@ -98,15 +158,74 @@ impl Mob for PillagerEntity {
|
||||
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, ()> {
|
||||
Box::pin(async move {
|
||||
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, ()> {
|
||||
Box::pin(async move {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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 pumpkin_data::data_component_impl::EquipmentSlot;
|
||||
use pumpkin_data::entity::EntityType;
|
||||
use pumpkin_data::item::Item;
|
||||
use pumpkin_data::item_stack::ItemStack;
|
||||
use pumpkin_util::Difficulty;
|
||||
|
||||
use crate::entity::{
|
||||
Entity,
|
||||
Entity, EntityBaseFuture,
|
||||
ai::goal::{
|
||||
active_target::ActiveTargetGoal, bow_attack::BowAttackGoal,
|
||||
look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal,
|
||||
melee_attack::MeleeAttackGoal, revenge::RevengeGoal, swim::SwimGoal,
|
||||
wander_around::WanderAroundGoal,
|
||||
},
|
||||
mob::{Mob, MobEntity},
|
||||
mob::{Mob, MobEntity, equipment::RegionalDifficulty},
|
||||
};
|
||||
use crate::world::World;
|
||||
|
||||
pub mod bogged;
|
||||
pub mod parched;
|
||||
@@ -70,4 +75,52 @@ impl Mob for SkeletonEntityBase {
|
||||
fn get_mob_entity(&self) -> &MobEntity {
|
||||
&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 crate::entity::{
|
||||
Entity,
|
||||
mob::{Mob, MobEntity, skeleton::SkeletonEntityBase},
|
||||
Entity, EntityBaseFuture,
|
||||
mob::{Mob, MobEntity, equipment::RegionalDifficulty, skeleton::SkeletonEntityBase},
|
||||
};
|
||||
use crate::world::World;
|
||||
|
||||
pub struct SkeletonEntity {
|
||||
entity: Arc<SkeletonEntityBase>,
|
||||
@@ -21,4 +22,21 @@ impl Mob for SkeletonEntity {
|
||||
fn get_mob_entity(&self) -> &MobEntity {
|
||||
&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,
|
||||
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::item::Item;
|
||||
use pumpkin_data::item_stack::ItemStack;
|
||||
use pumpkin_nbt::compound::NbtCompound;
|
||||
use pumpkin_util::Difficulty;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Weak};
|
||||
|
||||
use crate::entity::EntityBaseFuture;
|
||||
use crate::entity::mob::equipment::RegionalDifficulty;
|
||||
use crate::world::World;
|
||||
|
||||
pub mod drowned;
|
||||
pub mod husk;
|
||||
#[allow(clippy::module_inception)]
|
||||
@@ -127,6 +135,66 @@ impl Mob for ZombieEntityBase {
|
||||
&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, ()> {
|
||||
Box::pin(async move {
|
||||
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::{Mob, MobEntity};
|
||||
use crate::entity::{Entity, NbtFuture};
|
||||
use crate::entity::{Entity, EntityBaseFuture, NbtFuture};
|
||||
use crate::world::World;
|
||||
use pumpkin_nbt::compound::NbtCompound;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -28,6 +30,23 @@ impl Mob for ZombieEntity {
|
||||
&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, ()> {
|
||||
Box::pin(async move {
|
||||
self.entity.mob_write_nbt(nbt).await;
|
||||
|
||||
@@ -1784,8 +1784,29 @@ impl Player {
|
||||
|
||||
let respawn_guard = self.respawn_point.lock().await;
|
||||
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;
|
||||
|
||||
// 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);
|
||||
|
||||
// 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.
|
||||
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);
|
||||
|
||||
// Copy spawn info from level_info to avoid holding lock across await
|
||||
let (spawn_x, spawn_z, spawn_yaw, spawn_pitch, keep_inventory) = {
|
||||
let info = self.level_info.load();
|
||||
let server = self.server.upgrade();
|
||||
let default_world = server.as_ref().map_or_else(
|
||||
|| 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_y,
|
||||
info.spawn_z,
|
||||
info.spawn_yaw,
|
||||
info.spawn_pitch,
|
||||
@@ -3958,48 +3965,62 @@ impl World {
|
||||
};
|
||||
|
||||
// Get respawn position and dimension
|
||||
let (position, yaw, pitch, respawn_dimension) =
|
||||
if let Some(respawn) = player.calculate_respawn_point().await {
|
||||
(
|
||||
respawn.position,
|
||||
respawn.yaw,
|
||||
respawn.pitch,
|
||||
respawn.dimension,
|
||||
)
|
||||
} else {
|
||||
// No valid respawn point - send notification and use world spawn
|
||||
let (position, yaw, pitch, respawn_dimension) = if let Some(respawn) =
|
||||
player.calculate_respawn_point().await
|
||||
{
|
||||
(
|
||||
respawn.position,
|
||||
respawn.yaw,
|
||||
respawn.pitch,
|
||||
respawn.dimension,
|
||||
)
|
||||
} else {
|
||||
// No valid respawn point - send notification if player had one set
|
||||
if player.respawn_point.lock().await.is_some() {
|
||||
player
|
||||
.send_client_packet(&CGameEvent::new(GameEvent::NoRespawnBlockAvailable, 0.0))
|
||||
.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
|
||||
// proper spawn position calculation (see #1381). The y-level calculation
|
||||
// needs to account for spawn radius and find a safe spawn position.
|
||||
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));
|
||||
|
||||
(
|
||||
Vector3::new(
|
||||
f64::from(spawn_x) + 0.5,
|
||||
(top + 1).into(),
|
||||
f64::from(spawn_z) + 0.5,
|
||||
),
|
||||
spawn_yaw,
|
||||
spawn_pitch,
|
||||
self.dimension.clone(),
|
||||
)
|
||||
// FIXME: This spawn position calculation is incorrect. Should use vanilla's
|
||||
// proper spawn position calculation (see #1381). The y-level calculation
|
||||
// needs to account for spawn radius and find a safe spawn position.
|
||||
let chunk_pos = Vector2::new(spawn_x >> 4, spawn_z >> 4);
|
||||
default_world
|
||||
.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(
|
||||
f64::from(spawn_x) + 0.5,
|
||||
f64::from(pos_y),
|
||||
f64::from(spawn_z) + 0.5,
|
||||
),
|
||||
spawn_yaw,
|
||||
spawn_pitch,
|
||||
default_world.dimension.clone(),
|
||||
)
|
||||
};
|
||||
|
||||
let mut spawn_loc_event = crate::plugin::api::events::player::player_spawn_location::PlayerSpawnLocationEvent::new(
|
||||
player.clone(),
|
||||
position,
|
||||
);
|
||||
if let Some(server) = self.server.upgrade() {
|
||||
server
|
||||
.plugin_manager
|
||||
.fire(&server, &mut spawn_loc_event)
|
||||
.await;
|
||||
if let Some(ref s) = server {
|
||||
s.plugin_manager.fire(s, &mut spawn_loc_event).await;
|
||||
}
|
||||
let position = spawn_loc_event.spawn_pos;
|
||||
|
||||
@@ -4007,13 +4028,13 @@ impl World {
|
||||
let candidate_world = if respawn_dimension == self.dimension {
|
||||
None
|
||||
} else {
|
||||
self.server.upgrade().map_or_else(
|
||||
server.as_ref().map_or_else(
|
||||
|| {
|
||||
warn!("Could not get server for cross-dimension respawn");
|
||||
None
|
||||
},
|
||||
|server| {
|
||||
let worlds = server.worlds.load();
|
||||
|s| {
|
||||
let worlds = s.worlds.load();
|
||||
worlds
|
||||
.iter()
|
||||
.find(|w| w.dimension == respawn_dimension)
|
||||
@@ -4025,7 +4046,7 @@ impl World {
|
||||
// Fire PlayerChangeWorldEvent (cancellable) before the transfer; it runs before
|
||||
// the non-cancellable PlayerRespawnEvent, which observes the resolved 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 {
|
||||
player: player.clone(),
|
||||
previous_world: self.clone(),
|
||||
@@ -4035,7 +4056,7 @@ impl World {
|
||||
pitch,
|
||||
cancelled: false,
|
||||
};
|
||||
server.plugin_manager.fire(&server, &mut event).await;
|
||||
s.plugin_manager.fire(s, &mut event).await;
|
||||
|
||||
if event.cancelled {
|
||||
(None, position, yaw, pitch)
|
||||
@@ -4087,23 +4108,10 @@ impl World {
|
||||
|
||||
// Cancelled or unresolved cross-dimension respawns fall back to the current
|
||||
// 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 {
|
||||
(new_world.clone(), position, yaw, pitch)
|
||||
} else if respawn_dimension != self.dimension {
|
||||
// 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)
|
||||
};
|
||||
let (target_world, position, yaw, pitch) = resolved_world.as_ref().map_or_else(
|
||||
|| (self.clone(), position, yaw, pitch),
|
||||
|new_world| (new_world.clone(), position, yaw, pitch),
|
||||
);
|
||||
|
||||
// Notify plugins that the player has respawned (non-cancellable).
|
||||
if let Some(server) = self.server.upgrade() {
|
||||
|
||||
Reference in New Issue
Block a user