From e68cef7505c55e8436461cb4460021dce8e74a4c Mon Sep 17 00:00:00 2001 From: xRookieFight Date: Sun, 9 Aug 2026 16:11:36 +0300 Subject: [PATCH] fix: make skeletons shoot arrows with their bow (#2818) --- .../pumpkin/src/entity/ai/goal/bow_attack.rs | 211 ++++++++++++++++++ crates/pumpkin/src/entity/ai/goal/mod.rs | 1 + crates/pumpkin/src/entity/mob/skeleton/mod.rs | 10 +- 3 files changed, 218 insertions(+), 4 deletions(-) create mode 100644 crates/pumpkin/src/entity/ai/goal/bow_attack.rs diff --git a/crates/pumpkin/src/entity/ai/goal/bow_attack.rs b/crates/pumpkin/src/entity/ai/goal/bow_attack.rs new file mode 100644 index 000000000..22e6b593e --- /dev/null +++ b/crates/pumpkin/src/entity/ai/goal/bow_attack.rs @@ -0,0 +1,211 @@ +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 std::sync::Arc; +use tokio::sync::Mutex; + +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}; + +/// Ranged bow attack used by skeletons and their variants. +/// Mirrors vanilla `RangedBowAttackGoal`: the mob keeps its distance, draws the bow +/// and releases an arrow once it has been drawn long enough. +pub struct BowAttackGoal { + goal_control: Controls, + speed: f64, + attack_interval: i32, + squared_range: f64, + cooldown: i32, + draw_ticks: i32, + drawing: bool, +} + +impl BowAttackGoal { + /// Ticks the bow has to be drawn before the arrow is released. + const DRAW_TIME: i32 = 20; + /// Vanilla arrow speed for mob shots. + const ARROW_SPEED: f64 = 1.6; + + #[must_use] + pub fn new(speed: f64, attack_interval: i32, range: f32) -> Self { + Self { + goal_control: Controls::MOVE | Controls::LOOK, + speed, + attack_interval, + squared_range: f64::from(range * range), + cooldown: -1, + draw_ticks: 0, + drawing: false, + } + } + + async fn main_hand_item(mob: &dyn Mob) -> Arc> { + mob.get_mob_entity() + .living_entity + .entity_equipment + .lock() + .await + .get(&EquipmentSlot::MAIN_HAND) + } + + async fn is_holding_bow(mob: &dyn Mob) -> bool { + let held = Self::main_hand_item(mob).await; + let item = held.lock().await; + item.item.id == Item::BOW.id + } + + async fn stop_drawing(&mut self, mob: &dyn Mob) { + if self.drawing { + mob.get_mob_entity().living_entity.clear_active_hand().await; + self.drawing = false; + self.draw_ticks = 0; + } + } + + /// Spawns the arrow, matching vanilla `AbstractSkeleton::performRangedAttack`. + async fn shoot(mob: &dyn Mob, target: &Arc) { + let entity = mob.get_entity(); + let world = entity.world.load(); + + let arrow_entity = Entity::new(world.clone(), entity.pos.load(), &EntityType::ARROW); + let projectile = ItemStack::new(1, &Item::ARROW); + let arrow = ArrowEntity::new_shot(arrow_entity, entity, &projectile, ArrowPickup::Allowed); + + let mob_pos = entity.pos.load(); + let target_entity = target.get_entity(); + let target_pos = target_entity.pos.load(); + + 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); + + // Vanilla scales the spread with the world difficulty: 14 - difficulty * 4. + 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::EntityArrowShoot, SoundCategory::Hostile, &mob_pos); + + let arrow: Arc = Arc::new(arrow); + world.spawn_entity(arrow).await; + } +} + +impl Goal for BowAttackGoal { + 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_bow(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_bow(mob).await + }) + } + + fn start<'a>(&'a mut self, _mob: &'a dyn Mob) -> GoalFuture<'a, ()> { + Box::pin(async move { + self.cooldown = -1; + self.draw_ticks = 0; + self.drawing = false; + }) + } + + fn stop<'a>(&'a mut self, mob: &'a dyn Mob) -> GoalFuture<'a, ()> { + Box::pin(async move { + self.stop_drawing(mob).await; + self.cooldown = -1; + mob.get_mob_entity().navigator.lock().unwrap().stop(); + }) + } + + 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); + + mob.get_mob_entity() + .look_control + .lock() + .unwrap() + .look_at_entity_with_range(&target, 30.0, 30.0); + + // Close the gap while out of shooting range, otherwise hold position. + { + let mut navigator = mob.get_mob_entity().navigator.lock().unwrap(); + if distance_sq > self.squared_range { + navigator.set_progress(NavigatorGoal { + current_progress: mob_pos, + destination: target_pos, + speed: self.speed, + }); + } else { + navigator.stop(); + } + } + + if self.drawing { + self.draw_ticks += 1; + if self.draw_ticks >= Self::DRAW_TIME { + self.stop_drawing(mob).await; + Self::shoot(mob, &target).await; + self.cooldown = self.attack_interval; + } + return; + } + + self.cooldown -= 1; + if self.cooldown <= 0 && distance_sq <= self.squared_range { + let held = Self::main_hand_item(mob).await; + let stack = held.lock().await.clone(); + mob.get_mob_entity() + .living_entity + .set_active_hand(Hand::Right, stack, i32::MAX) + .await; + self.drawing = true; + self.draw_ticks = 0; + } + }) + } + + fn should_run_every_tick(&self) -> bool { + true + } + + fn controls(&self) -> Controls { + self.goal_control + } +} diff --git a/crates/pumpkin/src/entity/ai/goal/mod.rs b/crates/pumpkin/src/entity/ai/goal/mod.rs index 3165bd2db..1b054ed3f 100644 --- a/crates/pumpkin/src/entity/ai/goal/mod.rs +++ b/crates/pumpkin/src/entity/ai/goal/mod.rs @@ -6,6 +6,7 @@ pub mod ambient_stand; pub mod avoid_entity; pub mod beg; pub mod blaze_attack; +pub mod bow_attack; pub mod breed; pub mod chase_player; pub mod creeper_ignite; diff --git a/crates/pumpkin/src/entity/mob/skeleton/mod.rs b/crates/pumpkin/src/entity/mob/skeleton/mod.rs index afac382b2..282f8eabb 100644 --- a/crates/pumpkin/src/entity/mob/skeleton/mod.rs +++ b/crates/pumpkin/src/entity/mob/skeleton/mod.rs @@ -5,9 +5,10 @@ use pumpkin_data::entity::EntityType; use crate::entity::{ Entity, NBTStorage, NbtFuture, ai::goal::{ - active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal, - look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, revenge::RevengeGoal, - swim::SwimGoal, wander_around::WanderAroundGoal, + 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}, }; @@ -46,7 +47,8 @@ impl SkeletonEntityBase { .unwrap_or_else(std::sync::PoisonError::into_inner); goal_selector.add_goal(0, Box::new(SwimGoal::default())); - goal_selector.add_goal(2, Box::new(MeleeAttackGoal::new(1.2, false))); + goal_selector.add_goal(2, Box::new(BowAttackGoal::new(1.0, 20, 15.0))); + goal_selector.add_goal(3, Box::new(MeleeAttackGoal::new(1.2, false))); goal_selector.add_goal(7, Box::new(WanderAroundGoal::new(1.0))); goal_selector.add_goal( 8,