From bab2c22a519d269e569c18c6237d9dd397230497 Mon Sep 17 00:00:00 2001 From: Johnnywoof Date: Tue, 14 Jun 2022 13:07:42 -0400 Subject: [PATCH] Use predicate for entity targeting (#1099) * Use predicate for entity targeting * Change range variable in deprecated constructor from double to float * Use distance squared --- .../entity/ai/target/ClosestEntityTarget.java | 124 ++++++------------ .../entity/ai/ClosestEntityTargetTest.java | 62 +++++++++ 2 files changed, 103 insertions(+), 83 deletions(-) create mode 100644 src/test/java/net/minestom/server/entity/ai/ClosestEntityTargetTest.java diff --git a/src/main/java/net/minestom/server/entity/ai/target/ClosestEntityTarget.java b/src/main/java/net/minestom/server/entity/ai/target/ClosestEntityTarget.java index a41de8bd4..3775f1bf3 100644 --- a/src/main/java/net/minestom/server/entity/ai/target/ClosestEntityTarget.java +++ b/src/main/java/net/minestom/server/entity/ai/target/ClosestEntityTarget.java @@ -4,111 +4,69 @@ import net.minestom.server.entity.Entity; import net.minestom.server.entity.EntityCreature; import net.minestom.server.entity.LivingEntity; import net.minestom.server.entity.ai.TargetSelector; -import net.minestom.server.instance.Chunk; import net.minestom.server.instance.Instance; -import net.minestom.server.utils.chunk.ChunkUtils; import org.jetbrains.annotations.NotNull; -import java.util.ArrayList; -import java.util.List; -import java.util.Set; +import java.util.Comparator; +import java.util.function.Predicate; /** - * Target the closest targetable entity (based on the class array) + * Target the closest targetable entity (based on the target predicate) */ public class ClosestEntityTarget extends TargetSelector { - private final float range; - private final Class[] entitiesTarget; + private final double range; + private final Predicate targetPredicate; + /** + * @param entityCreature the entity (self) + * @param range the maximum range the entity can target others within + * @param entitiesTarget the entities to target by class + * @deprecated Use {@link #ClosestEntityTarget(EntityCreature, double, Predicate)} + */ @SafeVarargs + @Deprecated public ClosestEntityTarget(@NotNull EntityCreature entityCreature, float range, @NotNull Class... entitiesTarget) { + this(entityCreature, range, ent -> { + Class clazz = ent.getClass(); + for (Class targetClass : entitiesTarget) { + if (targetClass.isAssignableFrom(clazz)) { + return true; + } + } + return false; + }); + } + + /** + * @param entityCreature the entity (self) + * @param range the maximum range the entity can target others within + * @param targetPredicate the predicate used to check if the other entity can be targeted + */ + public ClosestEntityTarget(@NotNull EntityCreature entityCreature, double range, + @NotNull Predicate targetPredicate) { super(entityCreature); this.range = range; - this.entitiesTarget = entitiesTarget; + this.targetPredicate = targetPredicate; } @Override public Entity findTarget() { - final Instance instance = getEntityCreature().getInstance(); - final Chunk currentChunk = instance.getChunkAt(entityCreature.getPosition()); - if (currentChunk == null) { + + Instance instance = entityCreature.getInstance(); + + if (instance == null) { return null; } - final List chunks = getNeighbours(instance, currentChunk.getChunkX(), currentChunk.getChunkZ()); + return instance.getNearbyEntities(entityCreature.getPosition(), range).stream() + // Don't target our self and make sure entity is valid + .filter(ent -> !entityCreature.equals(ent) && !ent.isRemoved()) + .filter(targetPredicate) + .min(Comparator.comparingDouble(e -> e.getDistanceSquared(entityCreature))) + .orElse(null); - Entity entity = null; - double distance = Double.MAX_VALUE; - - for (Chunk chunk : chunks) { - final Set entities = instance.getChunkEntities(chunk); - - for (Entity ent : entities) { - - // Only target living entities - if (!(ent instanceof LivingEntity)) { - continue; - } - - // Don't target itself - if (ent.equals(entityCreature)) { - continue; - } - - if (ent.isRemoved()) { - // Entity not valid - return null; - } - - // Check if the entity type can be targeted - final Class clazz = ent.getClass(); - boolean correct = false; - for (Class targetClass : entitiesTarget) { - if (targetClass.isAssignableFrom(clazz)) { - correct = true; - break; - } - } - - if (!correct) { - continue; - } - - // Check distance - final double d = entityCreature.getDistance(ent); - if ((entity == null || d < distance) && d < range) { - entity = ent; - distance = d; - continue; - } - } - } - - return entity; - } - - private List getNeighbours(Instance instance, int chunkX, int chunkZ) { - List chunks = new ArrayList<>(); - // Constants used to loop through the neighbors - final int[] posX = {1, 0, -1}; - final int[] posZ = {1, 0, -1}; - - for (int x : posX) { - for (int z : posZ) { - - final int targetX = chunkX + x; - final int targetZ = chunkZ + z; - final Chunk chunk = instance.getChunk(targetX, targetZ); - if (ChunkUtils.isLoaded(chunk)) { - // Chunk is loaded, add it - chunks.add(chunk); - } - - } - } - return chunks; } } diff --git a/src/test/java/net/minestom/server/entity/ai/ClosestEntityTargetTest.java b/src/test/java/net/minestom/server/entity/ai/ClosestEntityTargetTest.java new file mode 100644 index 000000000..cb75f376b --- /dev/null +++ b/src/test/java/net/minestom/server/entity/ai/ClosestEntityTargetTest.java @@ -0,0 +1,62 @@ +package net.minestom.server.entity.ai; + +import net.minestom.server.api.Env; +import net.minestom.server.api.EnvTest; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.entity.EntityCreature; +import net.minestom.server.entity.EntityType; +import net.minestom.server.entity.ai.target.ClosestEntityTarget; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +@EnvTest +public class ClosestEntityTargetTest { + + @Test + public void validFindTarget(Env env) { + var instance = env.createFlatInstance(); + + var self = new EntityCreature(EntityType.ZOMBIE); + self.setInstance(instance, new Pos(0, 42, 0)).join(); + + var spider = new EntityCreature(EntityType.SPIDER); + spider.setInstance(instance, new Pos(-3, 42, -3)).join(); + + var secondSpider = new EntityCreature(EntityType.SPIDER); + secondSpider.setInstance(instance, new Pos(-4, 42, -4)).join(); + + var skeleton = new EntityCreature(EntityType.SKELETON); + skeleton.setInstance(instance, new Pos(5, 42, 5)).join(); + + var zombie = new EntityCreature(EntityType.ZOMBIE); + zombie.setInstance(instance, new Pos(10, 42, -10)).join(); + + assertEquals(5, instance.getEntities().size(), "Not all entities are in the instance"); + + assertNull( + new ClosestEntityTarget(self, 1, e -> true).findTarget(), + "Entity targets it self" + ); + + assertEquals(spider, + new ClosestEntityTarget(self, 20, e -> e.getEntityType() == EntityType.SPIDER).findTarget(), + "The closest spider was not selected" + ); + + assertNull( + new ClosestEntityTarget(self, 2, e -> e.getEntityType() == EntityType.SPIDER).findTarget(), + "Range distance is not being considered" + ); + + zombie.remove(); + + assertNull( + new ClosestEntityTarget(self, 20, e -> e.getEntityType() == EntityType.ZOMBIE).findTarget(), + "Removed entities are included in target selection" + ); + + } + +}