fix: make skeletons shoot arrows with their bow (#2818)

This commit is contained in:
xRookieFight
2026-08-09 16:11:36 +03:00
committed by GitHub
parent ee984ea555
commit e68cef7505
3 changed files with 218 additions and 4 deletions

View File

@@ -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<Mutex<ItemStack>> {
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<dyn EntityBase>) {
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<dyn EntityBase> = 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
}
}

View File

@@ -6,6 +6,7 @@ pub mod ambient_stand;
pub mod avoid_entity; pub mod avoid_entity;
pub mod beg; pub mod beg;
pub mod blaze_attack; pub mod blaze_attack;
pub mod bow_attack;
pub mod breed; pub mod breed;
pub mod chase_player; pub mod chase_player;
pub mod creeper_ignite; pub mod creeper_ignite;

View File

@@ -5,9 +5,10 @@ use pumpkin_data::entity::EntityType;
use crate::entity::{ use crate::entity::{
Entity, NBTStorage, NbtFuture, Entity, NBTStorage, NbtFuture,
ai::goal::{ ai::goal::{
active_target::ActiveTargetGoal, look_around::RandomLookAroundGoal, active_target::ActiveTargetGoal, bow_attack::BowAttackGoal,
look_at_entity::LookAtEntityGoal, melee_attack::MeleeAttackGoal, revenge::RevengeGoal, look_around::RandomLookAroundGoal, look_at_entity::LookAtEntityGoal,
swim::SwimGoal, wander_around::WanderAroundGoal, melee_attack::MeleeAttackGoal, revenge::RevengeGoal, swim::SwimGoal,
wander_around::WanderAroundGoal,
}, },
mob::{Mob, MobEntity}, mob::{Mob, MobEntity},
}; };
@@ -46,7 +47,8 @@ impl SkeletonEntityBase {
.unwrap_or_else(std::sync::PoisonError::into_inner); .unwrap_or_else(std::sync::PoisonError::into_inner);
goal_selector.add_goal(0, Box::new(SwimGoal::default())); goal_selector.add_goal(0, Box::new(SwimGoal::default()));
goal_selector.add_goal(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(7, Box::new(WanderAroundGoal::new(1.0)));
goal_selector.add_goal( goal_selector.add_goal(
8, 8,