fix: respawn in nether

This commit is contained in:
Alexander Medvedev
2026-08-25 13:42:47 +02:00
parent dafc2b2000
commit d8c7c23c0b
21 changed files with 2555 additions and 271 deletions

View File

@@ -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,

View File

@@ -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;
}
})
}
}

View File

@@ -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(&center) <= 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();

View File

@@ -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;

View 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
}
}

View File

@@ -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,
);
}
})
}

View 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) {}
}

View File

@@ -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,

View File

@@ -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
}
}

View File

@@ -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 {})
}

View File

@@ -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()
}
}

View 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;
}
}
}
}

View File

@@ -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
}
}

View File

@@ -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()
}
}

View File

@@ -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));
})
}
}

View File

@@ -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)
}
}

View File

@@ -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() {

View File

@@ -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;

View File

@@ -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

View File

@@ -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,
}
}
}

View File

@@ -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() {