From e50ee8a3d67e97a5d3f3f91ec1fce88ab7d0da40 Mon Sep 17 00:00:00 2001 From: ScarletRedMan Date: Mon, 14 Jul 2025 19:21:41 +0700 Subject: [PATCH] !refactor: reimplemented movement target --- .../java/ru/dragonestia/msb3/api/ai/AI.java | 191 ++---------------- .../msb3/api/ai/ArrivalResultListener.java | 10 + .../PathState.java => FollowerState.java} | 4 +- .../msb3/api/ai/FollowerTarget.java | 7 + .../msb3/api/ai/action/PatrolAction.java | 41 ++-- .../GroundMovementFollower.java | 2 +- .../{ => follower}/MovementFollower.java | 2 +- .../ai/movement/target/MovementTarget.java | 75 +++++++ .../target/WithPathFinderMovementTarget.java | 138 +++++++++++++ .../WithoutPathFinderMovementTarget.java | 45 +++++ .../api/ai/navigator/DestinationPoint.java | 7 - .../msb3/api/ai/navigator/Navigator.java | 8 + .../msb3/api/ai/navigator/NavigatorPath.java | 4 + .../msb3/api/ai/navigator/Path.java | 7 +- .../msb3/api/ai/navigator/PathGenerator.java | 13 +- .../follower/GroundNodeFollower.java | 66 ------ .../ai/navigator/follower/NodeFollower.java | 15 -- .../msb3/api/command/PuppeteerSubcommand.java | 12 +- .../dragonestia/msb3/api/entity/EntityAI.java | 29 ++- .../player/defaults/DebugParamsContext.java | 2 + 20 files changed, 375 insertions(+), 303 deletions(-) create mode 100644 api/src/main/java/ru/dragonestia/msb3/api/ai/ArrivalResultListener.java rename api/src/main/java/ru/dragonestia/msb3/api/ai/{navigator/PathState.java => FollowerState.java} (95%) create mode 100644 api/src/main/java/ru/dragonestia/msb3/api/ai/FollowerTarget.java rename api/src/main/java/ru/dragonestia/msb3/api/ai/movement/{ => follower}/GroundMovementFollower.java (98%) rename api/src/main/java/ru/dragonestia/msb3/api/ai/movement/{ => follower}/MovementFollower.java (96%) create mode 100644 api/src/main/java/ru/dragonestia/msb3/api/ai/movement/target/MovementTarget.java create mode 100644 api/src/main/java/ru/dragonestia/msb3/api/ai/movement/target/WithPathFinderMovementTarget.java create mode 100644 api/src/main/java/ru/dragonestia/msb3/api/ai/movement/target/WithoutPathFinderMovementTarget.java delete mode 100644 api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/DestinationPoint.java create mode 100644 api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/Navigator.java create mode 100644 api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/NavigatorPath.java delete mode 100644 api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/follower/GroundNodeFollower.java delete mode 100644 api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/follower/NodeFollower.java diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/AI.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/AI.java index 604b5e9..6d2236c 100644 --- a/api/src/main/java/ru/dragonestia/msb3/api/ai/AI.java +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/AI.java @@ -3,215 +3,54 @@ package ru.dragonestia.msb3.api.ai; import lombok.Getter; import lombok.Setter; import lombok.extern.log4j.Log4j2; -import net.minestom.server.coordinate.Point; -import net.minestom.server.coordinate.Pos; -import net.minestom.server.coordinate.Vec; -import net.minestom.server.entity.attribute.Attribute; -import ru.dragonestia.msb3.api.ai.movement.GroundMovementFollower; -import ru.dragonestia.msb3.api.ai.movement.MovementFollower; -import ru.dragonestia.msb3.api.ai.navigator.*; -import ru.dragonestia.msb3.api.ai.navigator.follower.GroundNodeFollower; -import ru.dragonestia.msb3.api.ai.navigator.follower.NodeFollower; +import ru.dragonestia.msb3.api.ai.movement.target.MovementTarget; +import ru.dragonestia.msb3.api.ai.movement.follower.GroundMovementFollower; +import ru.dragonestia.msb3.api.ai.movement.follower.MovementFollower; import ru.dragonestia.msb3.api.ai.navigator.node.GroundNodeGenerator; import ru.dragonestia.msb3.api.ai.navigator.node.NodeGenerator; -import ru.dragonestia.msb3.api.debug.DebugMessage; import ru.dragonestia.msb3.api.entity.EntityAI; -import ru.dragonestia.msb3.api.util.UncheckedRunnable; import java.util.Objects; -import java.util.concurrent.CompletableFuture; +/** + * Класс для управления поведением сущности + */ @Log4j2 @Getter public class AI { private final EntityAI entity; - private DestinationPoint destinationData; @Setter private NodeGenerator nodeGenerator = new GroundNodeGenerator(); - @Setter private NodeFollower nodeFollower; private final MovementFollower movementFollower; @Getter private final Actor actor; - @Setter private Point destinationPoint; + @Getter private MovementTarget movementTarget; public AI(EntityAI entity) { this.entity = Objects.requireNonNull(entity, "AI is null"); - nodeFollower = new GroundNodeFollower(entity); movementFollower = new GroundMovementFollower(entity); actor = new Actor(entity); } - public PathState getCurrentPathState() { + public void setMovementTarget(MovementTarget movementTarget) { synchronized (this) { - if (destinationData == null) return PathState.NONE; - return destinationData.path().getState(); - } - } - - public CompletableFuture setPathTo(Point point) { - var bb = entity.getBoundingBox(); - var centerToCorner = Math.sqrt(bb.width() * bb.width() + bb.depth() * bb.depth()) / 2; - return setPathTo(point, centerToCorner); - } - - public CompletableFuture setPathTo(Point point, double arrivalDistance) { - return setPathTo(point, arrivalDistance, 50, 20); - } - - public CompletableFuture setPathTo(Point point, double arrivalDistance, double maxDistance, double pathVariance) { - Objects.requireNonNull(point, "Point is null"); - var instance = Objects.requireNonNull(entity.getInstance(), "Entity is not spawned"); - if (!instance.getWorldBorder().inBounds(point)) - throw new IllegalStateException("Destination point is not in world bounds"); - if (!instance.isChunkLoaded(point)) - throw new IllegalStateException("Chunk is not loaded for destination point"); - - - synchronized (this) { - // Завершаем активное следование к точке - if (destinationData != null) { - var data = destinationData; - destinationData = null; - - UncheckedRunnable.runIgnoreException(() -> data.future().complete(PathState.TERMINATED)); + if (this.movementTarget != null) { + this.movementTarget.cancel(); } - // Игрок находится рядом с точкой, куда нужно прийти - if (entity.getDistanceSquared(point) <= arrivalDistance * arrivalDistance) { - return CompletableFuture.completedFuture(PathState.COMPLETED); + this.movementTarget = movementTarget; + + if (movementTarget != null) { + movementTarget.start(); } - - // Проверка находится ли точка слишком далеко - if (entity.getDistanceSquared(point) > Math.pow(maxDistance - arrivalDistance / 2, 2)) { - return CompletableFuture.completedFuture(PathState.TOO_FAR); - } - - // Начинаем просчет пути до новой точки - var path = PathGenerator.generate(instance, - entity.getPosition(), - point, - arrivalDistance, - maxDistance, - pathVariance, - entity.getBoundingBox(), - entity.isOnGround(), - nodeGenerator); - - // Если на время просчета пути уже известен результат действия - if (path.getState().isCompleted()) { - return CompletableFuture.completedFuture(path.getState()); - } - - return (destinationData = new DestinationPoint(Vec.fromPoint(point), arrivalDistance, path, new CompletableFuture<>())) - .future().thenApply(result -> { - resetDestinationData(); - return result; - }); } } public void tick(long delta) { synchronized (this) { - if (entity.isDead()) return; - var action = actor.getCurrentAction(); if (action != null) action.tick(actor, entity, delta); - if (destinationData != null) tickNavigator(); - if (destinationPoint != null) tickNewNavigator(); + if (movementTarget != null) movementTarget.tick(); } } - - private void tickNewNavigator() { - if (checkDestinationPointCompleted()) return; - - var speed = Math.min(getMovementSpeed(), Math.sqrt(movementFollower.squaredDistance(destinationPoint))); - var prevPosition = entity.getPosition(); - movementFollower.moveTo(destinationPoint, speed, destinationPoint); - var movementDistance = movementFollower.squaredDistance(prevPosition); - if (movementDistance == 0) { - // Deadlocked? - } - - checkDestinationPointCompleted(); - } - - private boolean checkDestinationPointCompleted() { - double distSquared = movementFollower.squaredDistance(destinationPoint); - if (distSquared < 0.01 * 0.01) { - destinationPoint = null; - DebugMessage.broadcast("Destination point completed"); - return true; - } - return false; - } - - private void tickNavigator() { - if (entity.getPosition().distance(destinationData.position()) < destinationData.arrivalDistance()) { - var future = destinationData.future(); - UncheckedRunnable.runIgnoreException(() -> future.complete(PathState.COMPLETED)); - return; - } - - var path = destinationData.path(); - var currentTarget = path.getCurrent(); - var nextTarget = path.getNext(); - - if (currentTarget == null || path.getCurrentType() == PathNode.Type.REPATH || path.getCurrentType() == null) { - recalculatePath(); - return; - } - - if (nextTarget == null) nextTarget = destinationData.position(); - - boolean nextIsRePath = nextTarget.sameBlock(Pos.ZERO); - nodeFollower.moveTowards(currentTarget, nodeFollower.movementSpeed(), nextIsRePath ? currentTarget : nextTarget); - - // TODO: исправить баг, в данном месте, когда Entity не движется вообще. Сделать хорошую проверку на застревание - // TODO: присутствует баг, что если как-то помешать Entity следовать до точки, например поставить перед ним блок, то он перестанет идти - // TODO: Бля, этот поиск пути говнище полное! Он не учитывает ни заборы, ни полублоки, ни ковры и остальные неполноценные блоки. Трава - это вообще пиздец. - // TODO: использовать CollisionUtils для просчета поиска путей - - if (nodeFollower.isAtPoint(currentTarget)) path.next(); - else if (path.getCurrentType() == PathNode.Type.JUMP) nodeFollower.jump(currentTarget, nextTarget); - checkFinishedPath(path, destinationData.future()); - } - - public void recalculatePath() { - if (destinationData == null) return; - - var originalPath = destinationData.path(); - var newPath = PathGenerator.generate(entity.getInstance(), - entity.getPosition(), - destinationData.position(), - destinationData.arrivalDistance(), originalPath.getMaxDistance(), - originalPath.getPathVariance(), entity.getBoundingBox(), entity.isOnGround(), nodeGenerator); - - destinationData = new DestinationPoint(destinationData.position(), destinationData.arrivalDistance(), newPath, destinationData.future()); - checkFinishedPath(newPath, destinationData.future()); - } - - private void checkFinishedPath(Path path, CompletableFuture future) { - var state = path.getState(); - if (state.isCompleted()) { - UncheckedRunnable.runIgnoreException(() -> future.complete(state)); - } - } - - public void resetDestinationData() { - synchronized (this) { - if (destinationData == null) return; - - var future = destinationData.future(); - destinationData = null; - if (!future.isDone()) future.complete(PathState.TERMINATED); - } - } - - public boolean isCompleted() { - return getCurrentPathState().isCompleted(); - } - - public double getMovementSpeed() { - return entity.getAttribute(Attribute.MOVEMENT_SPEED).getValue(); - } } diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/ArrivalResultListener.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/ArrivalResultListener.java new file mode 100644 index 0000000..4a6767f --- /dev/null +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/ArrivalResultListener.java @@ -0,0 +1,10 @@ +package ru.dragonestia.msb3.api.ai; + +public interface ArrivalResultListener { + + /** + * Отправляет завершенное состояние пути. Может быть положительным (моб дошел до нужной точки), либо негативным (потерялся) + * @param resultState Состояние полученное в конце пути + */ + void result(FollowerState resultState); +} diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/PathState.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/FollowerState.java similarity index 95% rename from api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/PathState.java rename to api/src/main/java/ru/dragonestia/msb3/api/ai/FollowerState.java index 11b5038..eb78ad2 100644 --- a/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/PathState.java +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/FollowerState.java @@ -1,11 +1,11 @@ -package ru.dragonestia.msb3.api.ai.navigator; +package ru.dragonestia.msb3.api.ai; import lombok.Getter; import lombok.RequiredArgsConstructor; @Getter @RequiredArgsConstructor -public enum PathState { +public enum FollowerState { /** * Путь не задан. diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/FollowerTarget.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/FollowerTarget.java new file mode 100644 index 0000000..a482547 --- /dev/null +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/FollowerTarget.java @@ -0,0 +1,7 @@ +package ru.dragonestia.msb3.api.ai; + +import net.minestom.server.coordinate.Point; + +import java.util.function.Consumer; + +public record FollowerTarget(Point point, ArrivalResultListener callback) {} diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/action/PatrolAction.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/action/PatrolAction.java index e48eca8..f06e4f3 100644 --- a/api/src/main/java/ru/dragonestia/msb3/api/ai/action/PatrolAction.java +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/action/PatrolAction.java @@ -5,7 +5,8 @@ import lombok.Setter; import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; import ru.dragonestia.msb3.api.ai.Actor; -import ru.dragonestia.msb3.api.ai.navigator.PathState; +import ru.dragonestia.msb3.api.ai.FollowerState; +import ru.dragonestia.msb3.api.ai.movement.target.MovementTarget; import ru.dragonestia.msb3.api.entity.EntityAI; import ru.dragonestia.msb3.api.scheduler.Scheduler; @@ -41,7 +42,8 @@ public class PatrolAction implements Action { @Override public void stop(Actor actor, EntityAI entity) { - entity.getAi().resetDestinationData(); + var ai = entity.getAi(); + if (ai.getMovementTarget() != null) ai.getMovementTarget().cancel(); this.entity = null; } @@ -81,25 +83,24 @@ public class PatrolAction implements Action { private void walkToNextPos(EntityAI entity) { var nextPos = useNextPos(); - var navigator = entity.getAi(); + var ai = entity.getAi(); - navigator.setPathTo(nextPos, destinationRadius) - .thenAccept(result -> { - if (result == PathState.TERMINATED) return; - if (result == PathState.TOO_FAR) { - Scheduler.ofEntity(entity).delayedTask(() -> { - entity.teleport(Pos.fromPoint(nextPos)) - .thenRun(() -> walkToNextPos(entity)); - }, Duration.ofSeconds(1)); - return; - } - if (result == PathState.DEADLOCKED) { - entity.teleport(Pos.fromPoint(nextPos)) - .thenRun(() -> walkToNextPos(entity)); - return; - } + ai.setMovementTarget(MovementTarget.withPathFinder(entity, nextPos, result -> { + if (result == FollowerState.TERMINATED) return; + if (result == FollowerState.TOO_FAR) { + Scheduler.ofEntity(entity).delayedTask(() -> { + entity.teleport(Pos.fromPoint(nextPos)) + .thenRun(() -> walkToNextPos(entity)); + }, Duration.ofSeconds(1)); + return; + } + if (result == FollowerState.DEADLOCKED) { + entity.teleport(Pos.fromPoint(nextPos)) + .thenRun(() -> walkToNextPos(entity)); + return; + } - walkToNextPos(entity); - }); + walkToNextPos(entity); + })); } } diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/movement/GroundMovementFollower.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/movement/follower/GroundMovementFollower.java similarity index 98% rename from api/src/main/java/ru/dragonestia/msb3/api/ai/movement/GroundMovementFollower.java rename to api/src/main/java/ru/dragonestia/msb3/api/ai/movement/follower/GroundMovementFollower.java index 135ada1..0a3aacd 100644 --- a/api/src/main/java/ru/dragonestia/msb3/api/ai/movement/GroundMovementFollower.java +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/movement/follower/GroundMovementFollower.java @@ -1,4 +1,4 @@ -package ru.dragonestia.msb3.api.ai.movement; +package ru.dragonestia.msb3.api.ai.movement.follower; import net.minestom.server.collision.CollisionUtils; import net.minestom.server.collision.PhysicsResult; diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/movement/MovementFollower.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/movement/follower/MovementFollower.java similarity index 96% rename from api/src/main/java/ru/dragonestia/msb3/api/ai/movement/MovementFollower.java rename to api/src/main/java/ru/dragonestia/msb3/api/ai/movement/follower/MovementFollower.java index 6f0f8ac..56e1bff 100644 --- a/api/src/main/java/ru/dragonestia/msb3/api/ai/movement/MovementFollower.java +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/movement/follower/MovementFollower.java @@ -1,4 +1,4 @@ -package ru.dragonestia.msb3.api.ai.movement; +package ru.dragonestia.msb3.api.ai.movement.follower; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/movement/target/MovementTarget.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/movement/target/MovementTarget.java new file mode 100644 index 0000000..ed46ca3 --- /dev/null +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/movement/target/MovementTarget.java @@ -0,0 +1,75 @@ +package ru.dragonestia.msb3.api.ai.movement.target; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.minestom.server.coordinate.Point; +import ru.dragonestia.msb3.api.ai.ArrivalResultListener; +import ru.dragonestia.msb3.api.ai.FollowerState; +import ru.dragonestia.msb3.api.entity.EntityAI; + +@Getter +@RequiredArgsConstructor +public abstract class MovementTarget { + + private final EntityAI entity; + private final Point destinationPoint; + private final ArrivalResultListener onArrive; + private boolean started = false; + private boolean completed = false; + + public final void start() { + synchronized (this) { + if (started) return; + completed = false; + started = true; + + start0(); + } + } + + protected void start0() {} + + public final void tick() { + synchronized (this) { + if (!started) return; + + tick0(); + } + } + + protected void tick0() {} + + public final void cancel() { + synchronized (this) { + if (!started || completed) return; + started = false; + completed = true; + } + + if (cancel0()) { + complete(FollowerState.TERMINATED); + } + } + + protected boolean cancel0() { + return true; + } + + protected synchronized final void complete(FollowerState state) { + if (!state.isCompleted()) { + throw new IllegalStateException("Invalid state taken for completed movement. Taken: %s".formatted(state)); + } + + completed = true; + entity.getAi().setMovementTarget(null); + onArrive.result(state); + } + + public static MovementTarget justMoveTo(EntityAI entity, Point destination, ArrivalResultListener onArrive) { + return new WithoutPathFinderMovementTarget(entity, destination, onArrive); + } + + public static MovementTarget withPathFinder(EntityAI entity, Point destination, ArrivalResultListener onArrive) { + return new WithPathFinderMovementTarget(entity, destination, onArrive); + } +} diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/movement/target/WithPathFinderMovementTarget.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/movement/target/WithPathFinderMovementTarget.java new file mode 100644 index 0000000..c9d4dd5 --- /dev/null +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/movement/target/WithPathFinderMovementTarget.java @@ -0,0 +1,138 @@ +package ru.dragonestia.msb3.api.ai.movement.target; + +import net.minestom.server.coordinate.Point; +import ru.dragonestia.msb3.api.ai.ArrivalResultListener; +import ru.dragonestia.msb3.api.entity.EntityAI; + +public class WithPathFinderMovementTarget extends MovementTarget{ + + WithPathFinderMovementTarget(EntityAI entity, Point destinationPoint, ArrivalResultListener onArrive) { + super(entity, destinationPoint, onArrive); + } + + /* TODO: implement it + public CompletableFuture setPathTo(Point point) { + var bb = entity.getBoundingBox(); + var centerToCorner = Math.sqrt(bb.width() * bb.width() + bb.depth() * bb.depth()) / 2; + return setPathTo(point, centerToCorner); + } + + public CompletableFuture setPathTo(Point point, double arrivalDistance) { + return setPathTo(point, arrivalDistance, 50, 20); + } + + public CompletableFuture setPathTo(Point point, double arrivalDistance, double maxDistance, double pathVariance) { + Objects.requireNonNull(point, "Point is null"); + var instance = Objects.requireNonNull(entity.getInstance(), "Entity is not spawned"); + if (!instance.getWorldBorder().inBounds(point)) + throw new IllegalStateException("Destination point is not in world bounds"); + if (!instance.isChunkLoaded(point)) + throw new IllegalStateException("Chunk is not loaded for destination point"); + + + synchronized (this) { + // Завершаем активное следование к точке + if (destinationData != null) { + var data = destinationData; + destinationData = null; + + UncheckedRunnable.runIgnoreException(() -> data.future().complete(FollowerState.TERMINATED)); + } + + // Игрок находится рядом с точкой, куда нужно прийти + if (entity.getDistanceSquared(point) <= arrivalDistance * arrivalDistance) { + return CompletableFuture.completedFuture(FollowerState.COMPLETED); + } + + // Проверка находится ли точка слишком далеко + if (entity.getDistanceSquared(point) > Math.pow(maxDistance - arrivalDistance / 2, 2)) { + return CompletableFuture.completedFuture(FollowerState.TOO_FAR); + } + + // Начинаем просчет пути до новой точки + var path = PathGenerator.generate(instance, + entity.getPosition(), + point, + arrivalDistance, + maxDistance, + pathVariance, + entity.getBoundingBox(), + entity.isOnGround(), + nodeGenerator); + + // Если на время просчета пути уже известен результат действия + if (path.getState().isCompleted()) { + return CompletableFuture.completedFuture(path.getState()); + } + + return (destinationData = new DestinationPoint(Vec.fromPoint(point), arrivalDistance, path, new CompletableFuture<>())) + .future().thenApply(result -> { + resetDestinationData(); + return result; + }); + } + } + + private void tickNavigator() { + if (entity.getPosition().distance(destinationData.position()) < destinationData.arrivalDistance()) { + var future = destinationData.future(); + UncheckedRunnable.runIgnoreException(() -> future.complete(FollowerState.COMPLETED)); + return; + } + + var path = destinationData.path(); + var currentTarget = path.getCurrent(); + var nextTarget = path.getNext(); + + if (currentTarget == null || path.getCurrentType() == PathNode.Type.REPATH || path.getCurrentType() == null) { + recalculatePath(); + return; + } + + if (nextTarget == null) nextTarget = destinationData.position(); + + boolean nextIsRePath = nextTarget.sameBlock(Pos.ZERO); + nodeFollower.moveTowards(currentTarget, nodeFollower.movementSpeed(), nextIsRePath ? currentTarget : nextTarget); + + // TODO: исправить баг, в данном месте, когда Entity не движется вообще. Сделать хорошую проверку на застревание + // TODO: присутствует баг, что если как-то помешать Entity следовать до точки, например поставить перед ним блок, то он перестанет идти + // TODO: Бля, этот поиск пути говнище полное! Он не учитывает ни заборы, ни полублоки, ни ковры и остальные неполноценные блоки. Трава - это вообще пиздец. + // TODO: использовать CollisionUtils для просчета поиска путей + + if (nodeFollower.isAtPoint(currentTarget)) path.next(); + else if (path.getCurrentType() == PathNode.Type.JUMP) nodeFollower.jump(currentTarget, nextTarget); + checkFinishedPath(path, destinationData.future()); + } + + public void recalculatePath() { + if (destinationData == null) return; + + var originalPath = destinationData.path(); + var newPath = PathGenerator.generate(entity.getInstance(), + entity.getPosition(), + destinationData.position(), + destinationData.arrivalDistance(), originalPath.getMaxDistance(), + originalPath.getPathVariance(), entity.getBoundingBox(), entity.isOnGround(), nodeGenerator); + + destinationData = new DestinationPoint(destinationData.position(), destinationData.arrivalDistance(), newPath, destinationData.future()); + checkFinishedPath(newPath, destinationData.future()); + } + + private void checkFinishedPath(Path path, CompletableFuture future) { + var state = path.getState(); + if (state.isCompleted()) { + UncheckedRunnable.runIgnoreException(() -> future.complete(state)); + } + } + + public void resetDestinationData() { + synchronized (this) { + if (destinationData == null) return; + + var future = destinationData.future(); + destinationData = null; + if (!future.isDone()) future.complete(FollowerState.TERMINATED); + } + } + */ +} diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/movement/target/WithoutPathFinderMovementTarget.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/movement/target/WithoutPathFinderMovementTarget.java new file mode 100644 index 0000000..3b11b36 --- /dev/null +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/movement/target/WithoutPathFinderMovementTarget.java @@ -0,0 +1,45 @@ +package ru.dragonestia.msb3.api.ai.movement.target; + +import net.minestom.server.coordinate.Point; +import ru.dragonestia.msb3.api.ai.ArrivalResultListener; +import ru.dragonestia.msb3.api.ai.FollowerState; +import ru.dragonestia.msb3.api.ai.movement.follower.MovementFollower; +import ru.dragonestia.msb3.api.entity.EntityAI; + +public class WithoutPathFinderMovementTarget extends MovementTarget { + + private MovementFollower movementFollower; + + WithoutPathFinderMovementTarget(EntityAI entity, Point destinationPoint, ArrivalResultListener onArrive) { + super(entity, destinationPoint, onArrive); + } + + @Override + protected void start0() { + movementFollower = getEntity().getMovementFollower(); + } + + @Override + protected void tick0() { + if (checkDestinationPointCompleted()) return; + + var speed = Math.min(getEntity().getMovementSpeed(), Math.sqrt(movementFollower.squaredDistance(getDestinationPoint()))); + var prevPosition = getEntity().getPosition(); + movementFollower.moveTo(getDestinationPoint(), speed, getDestinationPoint()); + var movementDistance = movementFollower.squaredDistance(prevPosition); + if (movementDistance == 0) { // TODO: catch deadlock condition + // Deadlocked? + } + + checkDestinationPointCompleted(); + } + + private boolean checkDestinationPointCompleted() { + double distSquared = movementFollower.squaredDistance(getDestinationPoint()); + if (distSquared < 0.01 * 0.01) { + complete(FollowerState.COMPLETED); + return true; + } + return false; + } +} diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/DestinationPoint.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/DestinationPoint.java deleted file mode 100644 index 0cb22d2..0000000 --- a/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/DestinationPoint.java +++ /dev/null @@ -1,7 +0,0 @@ -package ru.dragonestia.msb3.api.ai.navigator; - -import net.minestom.server.coordinate.Vec; - -import java.util.concurrent.CompletableFuture; - -public record DestinationPoint(Vec position, double arrivalDistance, Path path, CompletableFuture future) {} diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/Navigator.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/Navigator.java new file mode 100644 index 0000000..3f276ab --- /dev/null +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/Navigator.java @@ -0,0 +1,8 @@ +package ru.dragonestia.msb3.api.ai.navigator; + +import net.minestom.server.coordinate.Point; + +public interface Navigator { + + NavigatorPath findPath(Point point); +} diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/NavigatorPath.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/NavigatorPath.java new file mode 100644 index 0000000..15e25f0 --- /dev/null +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/NavigatorPath.java @@ -0,0 +1,4 @@ +package ru.dragonestia.msb3.api.ai.navigator; + +public record NavigatorPath() { +} diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/Path.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/Path.java index bba183b..f78138f 100644 --- a/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/Path.java +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/Path.java @@ -4,6 +4,7 @@ import lombok.Getter; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Vec; import org.jetbrains.annotations.Nullable; +import ru.dragonestia.msb3.api.ai.FollowerState; import java.util.ArrayList; import java.util.List; @@ -14,7 +15,7 @@ public class Path { @Getter private final double maxDistance; @Getter private final double pathVariance; @Getter private final List nodes = new ArrayList<>(); - private final AtomicReference state = new AtomicReference<>(PathState.FOLLOWING); + private final AtomicReference state = new AtomicReference<>(FollowerState.FOLLOWING); private int index = 0; public Path(double maxDistance, double pathVariance) { @@ -22,11 +23,11 @@ public class Path { this.pathVariance = pathVariance; } - public PathState getState() { + public FollowerState getState() { return state.get(); } - public void setState(PathState newState) { + public void setState(FollowerState newState) { state.set(newState); } diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/PathGenerator.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/PathGenerator.java index 83a8410..f876289 100644 --- a/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/PathGenerator.java +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/PathGenerator.java @@ -7,6 +7,7 @@ import net.minestom.server.collision.BoundingBox; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Pos; import net.minestom.server.instance.block.Block; +import ru.dragonestia.msb3.api.ai.FollowerState; import ru.dragonestia.msb3.api.ai.navigator.node.NodeGenerator; import java.util.*; @@ -50,7 +51,7 @@ public class PathGenerator { var closed = new ObjectOpenHashBigSet(maxSize); while (!open.isEmpty() && closed.size64() < maxSize) { - if (path.getState() == PathState.TERMINATED) { + if (path.getState() == FollowerState.TERMINATED) { return; } @@ -81,7 +82,7 @@ public class PathGenerator { if (current == null || !withinDistance(current, target, closeDistance)) { if (closestFoundNodes.isEmpty()) { - path.setState(PathState.DEADLOCKED); + path.setState(FollowerState.DEADLOCKED); return; } @@ -100,25 +101,25 @@ public class PathGenerator { Collections.reverse(path.getNodes()); if (path.getCurrentType() == PathNode.Type.REPATH) { - path.setState(PathState.DEADLOCKED); + path.setState(FollowerState.DEADLOCKED); path.getNodes().clear(); return; } if (path.getNodes().isEmpty()) { - path.setState(PathState.DEADLOCKED); + path.setState(FollowerState.DEADLOCKED); return; } var lastNode = path.getNodes().getLast(); if (getDistanceSquared(lastNode.x(), lastNode.y(), lastNode.z(), target) > (closeDistance * closeDistance)) { - path.setState(PathState.DEADLOCKED); + path.setState(FollowerState.DEADLOCKED); return; } PathNode pEnd = new PathNode(target, 0, 0, PathNode.Type.WALK, null); path.getNodes().add(pEnd); - path.setState(PathState.FOLLOWING); + path.setState(FollowerState.FOLLOWING); } private boolean withinDistance(PathNode point, Point target, double closeDistance) { diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/follower/GroundNodeFollower.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/follower/GroundNodeFollower.java deleted file mode 100644 index 5155234..0000000 --- a/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/follower/GroundNodeFollower.java +++ /dev/null @@ -1,66 +0,0 @@ -package ru.dragonestia.msb3.api.ai.navigator.follower; - -import lombok.RequiredArgsConstructor; -import net.minestom.server.collision.CollisionUtils; -import net.minestom.server.coordinate.Point; -import net.minestom.server.coordinate.Pos; -import net.minestom.server.coordinate.Vec; -import net.minestom.server.entity.attribute.Attribute; -import net.minestom.server.utils.position.PositionUtils; -import org.jetbrains.annotations.Nullable; -import ru.dragonestia.msb3.api.entity.EntityAI; - -@RequiredArgsConstructor -public class GroundNodeFollower implements NodeFollower { - - private final EntityAI entity; - - @Override - public void moveTowards(Point direction, double speed, Point lookAt) { - var position = entity.getPosition(); - var dx = direction.x() - position.x(); - var dy = direction.y() - position.y(); - var dz = direction.z() - position.z(); - - var dxLook = lookAt.x() - position.x(); - var dyLook = lookAt.y() - position.y(); - var dzLook = lookAt.z() - position.z(); - - // the purpose of these few lines is to slow down entities when they reach their destination - final double distSquared = dx * dx + dy * dy + dz * dz; - if (speed > distSquared) { - speed = distSquared; - } - - var radians = Math.atan2(dz, dx); - var speedX = Math.cos(radians) * speed; - var speedZ = Math.sin(radians) * speed; - var yaw = PositionUtils.getLookYaw(dxLook, dzLook); - var pitch = PositionUtils.getLookPitch(dxLook, dyLook, dzLook); - var physicsResult = CollisionUtils.handlePhysics(entity, new Vec(speedX, dy > 0? (dy + 0.1) : 0, speedZ)); - var newPosition = Pos.fromPoint(physicsResult.newPosition()); - entity.refreshPosition(newPosition.withView(yaw, pitch)); - } - - @Override - public void jump(@Nullable Point point, @Nullable Point target) { - if (entity.isOnGround()) { - jump(entity.getJumpHeight()); - } - } - - @Override - public boolean isAtPoint(Point point) { - var d = entity.getPosition().sub(point); - return d.x() * d.x() + d.z() * d.z() < 0.5 * 0.5; - } - - public void jump(double height) { - entity.setVelocity(new Vec(0, height, 0)); - } - - @Override - public double movementSpeed() { - return entity.getAttribute(Attribute.MOVEMENT_SPEED).getValue(); - } -} diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/follower/NodeFollower.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/follower/NodeFollower.java deleted file mode 100644 index 84a4dcd..0000000 --- a/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/follower/NodeFollower.java +++ /dev/null @@ -1,15 +0,0 @@ -package ru.dragonestia.msb3.api.ai.navigator.follower; - -import net.minestom.server.coordinate.Point; -import org.jetbrains.annotations.Nullable; - -public interface NodeFollower { - - void moveTowards(Point target, double speed, Point lookAt); - - void jump(@Nullable Point point, @Nullable Point target); - - boolean isAtPoint(Point point); - - double movementSpeed(); -} diff --git a/api/src/main/java/ru/dragonestia/msb3/api/command/PuppeteerSubcommand.java b/api/src/main/java/ru/dragonestia/msb3/api/command/PuppeteerSubcommand.java index 00ecba9..8efcb17 100644 --- a/api/src/main/java/ru/dragonestia/msb3/api/command/PuppeteerSubcommand.java +++ b/api/src/main/java/ru/dragonestia/msb3/api/command/PuppeteerSubcommand.java @@ -8,7 +8,9 @@ import net.minestom.server.command.builder.CommandContext; import net.minestom.server.command.builder.arguments.ArgumentType; import net.minestom.server.entity.EntityType; import net.minestom.server.entity.Player; -import ru.dragonestia.msb3.api.ai.movement.GroundMovementFollower; +import ru.dragonestia.msb3.api.ai.movement.target.MovementTarget; +import ru.dragonestia.msb3.api.ai.movement.follower.GroundMovementFollower; +import ru.dragonestia.msb3.api.debug.DebugMessage; import ru.dragonestia.msb3.api.entity.EntityAI; public class PuppeteerSubcommand extends Command { @@ -63,9 +65,9 @@ public class PuppeteerSubcommand extends Command { var player = (Player) sender; sender.sendMessage(Component.text("Set entity path target.")); - entity.getAi().setPathTo(player.getPosition()).thenAccept(result -> { + entity.getAi().setMovementTarget(MovementTarget.withPathFinder(entity, player.getPosition(), result -> { player.sendMessage(Component.text("Entity path target result: " + result, NamedTextColor.YELLOW)); - }); + })); } private void justComeHere(CommandSender sender, CommandContext ctx) { @@ -73,7 +75,9 @@ public class PuppeteerSubcommand extends Command { var player = (Player) sender; sender.sendMessage(Component.text("Set entity path target without PathFinder.")); - entity.getAi().setDestinationPoint(player.getPosition()); + entity.getAi().setMovementTarget(MovementTarget.justMoveTo(entity, player.getPosition(), result -> { + DebugMessage.broadcast("Follower target result: " + result); + })); } private void teleport(CommandSender sender, CommandContext ctx) { diff --git a/api/src/main/java/ru/dragonestia/msb3/api/entity/EntityAI.java b/api/src/main/java/ru/dragonestia/msb3/api/entity/EntityAI.java index 9f4953a..2f4675f 100644 --- a/api/src/main/java/ru/dragonestia/msb3/api/entity/EntityAI.java +++ b/api/src/main/java/ru/dragonestia/msb3/api/entity/EntityAI.java @@ -13,6 +13,8 @@ import net.minestom.server.instance.Instance; import net.minestom.server.utils.time.TimeUnit; import org.jetbrains.annotations.NotNull; import ru.dragonestia.msb3.api.ai.AI; +import ru.dragonestia.msb3.api.ai.movement.follower.GroundMovementFollower; +import ru.dragonestia.msb3.api.ai.movement.follower.MovementFollower; import java.time.Duration; import java.util.Objects; @@ -23,6 +25,7 @@ import java.util.concurrent.CompletableFuture; public class EntityAI extends LivingEntity { @Setter private int removalAnimationDelay = 1000; + private final MovementFollower movementFollower; private final AI ai = new AI(this); public EntityAI(@NotNull EntityType entityType) { @@ -31,19 +34,25 @@ public class EntityAI extends LivingEntity { public EntityAI(@NotNull EntityType entityType, @NotNull UUID uuid) { super(entityType, uuid); + + movementFollower = createMovementFollower(); + getAttribute(Attribute.MOVEMENT_SPEED).setBaseValue(0.2); heal(); } @Override public void update(long time) { - ai.tick(time); + if (!isDead()) { + ai.tick(time); + } + super.update(time); } @Override public CompletableFuture setInstance(@NotNull Instance instance, @NotNull Pos spawnPosition) { - ai.resetDestinationData(); + if (ai.getMovementTarget() != null) ai.getMovementTarget().cancel(); return super.setInstance(instance, spawnPosition).thenRun(() -> { var action = ai.getActor().getCurrentAction(); if (action != null) { @@ -76,7 +85,23 @@ public class EntityAI extends LivingEntity { ai.getActor().setActive(false); } + /** + * Получить высоту прыжка в блоках + * @return Высота прыжка + */ public double getJumpHeight() { return 1.1; } + + /** + * Получить скорость передвижения сущности. Сколько блоков в тик + * @return Скорость сущности + */ + public double getMovementSpeed() { + return getAttribute(Attribute.MOVEMENT_SPEED).getValue(); + } + + protected MovementFollower createMovementFollower() { + return new GroundMovementFollower(this); + } } diff --git a/api/src/main/java/ru/dragonestia/msb3/api/player/defaults/DebugParamsContext.java b/api/src/main/java/ru/dragonestia/msb3/api/player/defaults/DebugParamsContext.java index 270939f..0346ba4 100644 --- a/api/src/main/java/ru/dragonestia/msb3/api/player/defaults/DebugParamsContext.java +++ b/api/src/main/java/ru/dragonestia/msb3/api/player/defaults/DebugParamsContext.java @@ -37,6 +37,7 @@ public class DebugParamsContext extends PlayerContext { if (value) { if (taskDebugRendererAiPathFinder != null) return; + /* TODO: fix it taskDebugRendererAiPathFinder = Scheduler.ofPlayer(getPlayer()).repeatingTask(() -> { for (var entity: getPlayer().getInstance().getNearbyEntities(getPlayer().getPosition(), 32)) { if (entity instanceof EntityAI creature) { @@ -49,6 +50,7 @@ public class DebugParamsContext extends PlayerContext { } } }, Duration.ofMillis(500)); + */ return; }