Implemented new AI behavior patterns #1
@ -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();
|
||||
}
|
||||
if (this.movementTarget != null) {
|
||||
this.movementTarget.cancel();
|
||||
}
|
||||
|
||||
public CompletableFuture<PathState> setPathTo(Point point) {
|
||||
var bb = entity.getBoundingBox();
|
||||
var centerToCorner = Math.sqrt(bb.width() * bb.width() + bb.depth() * bb.depth()) / 2;
|
||||
return setPathTo(point, centerToCorner);
|
||||
this.movementTarget = movementTarget;
|
||||
|
||||
if (movementTarget != null) {
|
||||
movementTarget.start();
|
||||
}
|
||||
|
||||
public CompletableFuture<PathState> setPathTo(Point point, double arrivalDistance) {
|
||||
return setPathTo(point, arrivalDistance, 50, 20);
|
||||
}
|
||||
|
||||
public CompletableFuture<PathState> 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 (entity.getDistanceSquared(point) <= arrivalDistance * arrivalDistance) {
|
||||
return CompletableFuture.completedFuture(PathState.COMPLETED);
|
||||
}
|
||||
|
||||
// Проверка находится ли точка слишком далеко
|
||||
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<PathState> 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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
package ru.dragonestia.msb3.api.ai;
|
||||
|
||||
public interface ArrivalResultListener {
|
||||
|
||||
/**
|
||||
* Отправляет завершенное состояние пути. Может быть положительным (моб дошел до нужной точки), либо негативным (потерялся)
|
||||
* @param resultState Состояние полученное в конце пути
|
||||
*/
|
||||
void result(FollowerState resultState);
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
/**
|
||||
* Путь не задан.
|
||||
@ -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) {}
|
||||
@ -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) {
|
||||
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 == PathState.DEADLOCKED) {
|
||||
if (result == FollowerState.DEADLOCKED) {
|
||||
entity.teleport(Pos.fromPoint(nextPos))
|
||||
.thenRun(() -> walkToNextPos(entity));
|
||||
return;
|
||||
}
|
||||
|
||||
walkToNextPos(entity);
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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<FollowerState> 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<FollowerState> setPathTo(Point point, double arrivalDistance) {
|
||||
return setPathTo(point, arrivalDistance, 50, 20);
|
||||
}
|
||||
|
||||
public CompletableFuture<FollowerState> 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<FollowerState> 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);
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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<PathState> future) {}
|
||||
@ -0,0 +1,8 @@
|
||||
package ru.dragonestia.msb3.api.ai.navigator;
|
||||
|
||||
import net.minestom.server.coordinate.Point;
|
||||
|
||||
public interface Navigator {
|
||||
|
||||
NavigatorPath findPath(Point point);
|
||||
}
|
||||
@ -0,0 +1,4 @@
|
||||
package ru.dragonestia.msb3.api.ai.navigator;
|
||||
|
||||
public record NavigatorPath() {
|
||||
}
|
||||
@ -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<PathNode> nodes = new ArrayList<>();
|
||||
private final AtomicReference<PathState> state = new AtomicReference<>(PathState.FOLLOWING);
|
||||
private final AtomicReference<FollowerState> 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);
|
||||
}
|
||||
|
||||
|
||||
@ -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<PathNode>(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) {
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
if (!isDead()) {
|
||||
ai.tick(time);
|
||||
}
|
||||
|
||||
super.update(time);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user