fix(ai): respect target visibility (#2701)

This commit is contained in:
Eshan I.
2026-08-05 02:50:41 -04:00
committed by GitHub
parent 09d23bd15f
commit f0da5f330b
5 changed files with 88 additions and 23 deletions

View File

@@ -79,7 +79,7 @@ impl ActiveTargetGoal {
self.target = target;
}
fn find_closest_target(&mut self, mob: &MobEntity) {
async fn find_closest_target(&mut self, mob: &MobEntity) {
let follow_range = mob
.living_entity
.get_attribute_value(&Attributes::FOLLOW_RANGE);
@@ -103,6 +103,7 @@ impl ActiveTargetGoal {
&& self
.target_predicate
.test(&world, Some(&mob.living_entity), living)
.await
{
self.target = Some(potential_entity);
return;
@@ -116,6 +117,7 @@ impl ActiveTargetGoal {
&& self
.target_predicate
.test(&world, Some(&mob.living_entity), living)
.await
{
self.target = Some(potential_entity);
return;
@@ -133,7 +135,7 @@ impl Goal for ActiveTargetGoal {
{
return false;
}
self.find_closest_target(mob.get_mob_entity());
self.find_closest_target(mob.get_mob_entity()).await;
self.target.is_some()
})
}

View File

@@ -58,6 +58,7 @@ impl Goal for RevengeGoal {
if !self
.target_predicate
.test(&world, Some(&mob_entity.living_entity), attacker_living)
.await
{
return false;
}

View File

@@ -58,11 +58,15 @@ impl TeleportTowardsPlayerGoal {
}
let living = player.get_living_entity()?;
if !self.target_predicate.test(
&world,
Some(&self.enderman.mob_entity.living_entity),
living,
) {
if !self
.target_predicate
.test(
&world,
Some(&self.enderman.mob_entity.living_entity),
living,
)
.await
{
return None;
}

View File

@@ -34,7 +34,7 @@ impl TrackTargetGoal {
check_can_navigate_cooldown: AtomicI32::new(0),
time_without_visibility: AtomicI32::new(0),
max_time_without_visibility: 60,
target_predicate: TargetPredicate::create_attackable(),
target_predicate: TargetPredicate::create_attackable().ignore_visibility(),
}
}
@@ -55,8 +55,18 @@ impl TrackTargetGoal {
false
}
fn remembers_visible_target(&self, has_line_of_sight: bool) -> bool {
if has_line_of_sight {
self.time_without_visibility.store(0, Ordering::Relaxed);
true
} else {
let unseen_ticks = self.time_without_visibility.fetch_add(1, Ordering::Relaxed) + 1;
unseen_ticks <= to_goal_ticks(self.max_time_without_visibility)
}
}
/// Equivalent to Vanilla's `canAttack` check inside `TargetGoal`
pub fn can_track(
pub async fn can_track(
&self,
mob: &dyn Mob,
target: Option<&LivingEntity>,
@@ -69,7 +79,10 @@ impl TrackTargetGoal {
let mob_entity = mob.get_mob_entity();
let world = mob_entity.living_entity.entity.world.load();
if !target_predicate.test(&world, Some(&mob_entity.living_entity), target) {
if !target_predicate
.test(&world, Some(&mob_entity.living_entity), target)
.await
{
return false;
}
@@ -119,7 +132,10 @@ impl Goal for TrackTargetGoal {
return false;
}
if !self.can_track(mob, Some(target), &self.target_predicate) {
if !self
.can_track(mob, Some(target), &self.target_predicate)
.await
{
return false;
}
@@ -142,17 +158,18 @@ impl Goal for TrackTargetGoal {
}
if self.check_visibility {
// TODO: mob.getSensing().hasLineOfSight(target)
let has_line_of_sight = true;
let world = mob_entity.living_entity.entity.world.load();
let has_line_of_sight = world
.raycast(
mob_entity.living_entity.entity.get_eye_pos(),
target.entity.get_eye_pos(),
async |block_pos, world| world.get_block_state(block_pos).is_solid(),
)
.await
.is_none();
if has_line_of_sight {
self.time_without_visibility.store(0, Ordering::Relaxed);
} else {
let unseen_ticks =
self.time_without_visibility.fetch_add(1, Ordering::Relaxed) + 1;
if unseen_ticks > to_goal_ticks(self.max_time_without_visibility) {
return false;
}
if !self.remembers_visible_target(has_line_of_sight) {
return false;
}
}
@@ -179,3 +196,28 @@ impl Goal for TrackTargetGoal {
self.goal_control
}
}
#[cfg(test)]
mod tests {
use super::{TrackTargetGoal, to_goal_ticks};
use std::sync::atomic::Ordering;
#[test]
fn forgets_unseen_target_after_vanilla_memory_window() {
let goal = TrackTargetGoal::with_default(true);
let memory_ticks = to_goal_ticks(goal.max_time_without_visibility);
for _ in 0..memory_ticks {
assert!(goal.remembers_visible_target(false));
}
assert!(!goal.remembers_visible_target(false));
}
#[test]
fn seeing_target_resets_unseen_memory() {
let goal = TrackTargetGoal::with_default(true);
assert!(goal.remembers_visible_target(false));
assert!(goal.remembers_visible_target(true));
assert_eq!(goal.time_without_visibility.load(Ordering::Relaxed), 0);
}
}

View File

@@ -91,7 +91,7 @@ impl TargetPredicate {
));
}
pub fn test(
pub async fn test(
&self,
world: &World,
tester: Option<&LivingEntity>,
@@ -126,7 +126,23 @@ impl TargetPredicate {
if dist_sq > max_dist * max_dist {
return false;
}
// TODO: visibility check (needs world raycast)
}
if self.respects_visibility
&& let Some(tester_ent) = tester
&& tester_ent
.entity
.world
.load_full()
.raycast(
tester_ent.entity.get_eye_pos(),
target.entity.get_eye_pos(),
async |block_pos, world| world.get_block_state(block_pos).is_solid(),
)
.await
.is_some()
{
return false;
}
true