diff --git a/pumpkin/src/entity/ai/goal/active_target.rs b/pumpkin/src/entity/ai/goal/active_target.rs index 845f67801..30b1e3c5e 100644 --- a/pumpkin/src/entity/ai/goal/active_target.rs +++ b/pumpkin/src/entity/ai/goal/active_target.rs @@ -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() }) } diff --git a/pumpkin/src/entity/ai/goal/revenge.rs b/pumpkin/src/entity/ai/goal/revenge.rs index 1fdf71e97..3fae4a8f3 100644 --- a/pumpkin/src/entity/ai/goal/revenge.rs +++ b/pumpkin/src/entity/ai/goal/revenge.rs @@ -58,6 +58,7 @@ impl Goal for RevengeGoal { if !self .target_predicate .test(&world, Some(&mob_entity.living_entity), attacker_living) + .await { return false; } diff --git a/pumpkin/src/entity/ai/goal/teleport_towards_player.rs b/pumpkin/src/entity/ai/goal/teleport_towards_player.rs index ff03dfc1d..96f989535 100644 --- a/pumpkin/src/entity/ai/goal/teleport_towards_player.rs +++ b/pumpkin/src/entity/ai/goal/teleport_towards_player.rs @@ -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; } diff --git a/pumpkin/src/entity/ai/goal/track_target.rs b/pumpkin/src/entity/ai/goal/track_target.rs index 2fc262321..2d300ee7f 100644 --- a/pumpkin/src/entity/ai/goal/track_target.rs +++ b/pumpkin/src/entity/ai/goal/track_target.rs @@ -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); + } +} diff --git a/pumpkin/src/entity/ai/target_predicate.rs b/pumpkin/src/entity/ai/target_predicate.rs index d45ff7e5d..fa69f6141 100644 --- a/pumpkin/src/entity/ai/target_predicate.rs +++ b/pumpkin/src/entity/ai/target_predicate.rs @@ -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