Implemented new AI behavior patterns #1

Open
ScarletRedMan wants to merge 17 commits from feat/ai into master
20 changed files with 375 additions and 303 deletions
Showing only changes of commit e50ee8a3d6 - Show all commits

View File

@ -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<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);
}
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 (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<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();
}
}

View File

@ -0,0 +1,10 @@
package ru.dragonestia.msb3.api.ai;
public interface ArrivalResultListener {
/**
* Отправляет завершенное состояние пути. Может быть положительным (моб дошел до нужной точки), либо негативным (потерялся)
* @param resultState Состояние полученное в конце пути
*/
void result(FollowerState resultState);
}

View File

@ -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 {
/**
* Путь не задан.

View File

@ -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) {}

View File

@ -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);
}));
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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);
}
}
*/
}

View File

@ -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;
}
}

View File

@ -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) {}

View File

@ -0,0 +1,8 @@
package ru.dragonestia.msb3.api.ai.navigator;
import net.minestom.server.coordinate.Point;
public interface Navigator {
NavigatorPath findPath(Point point);
}

View File

@ -0,0 +1,4 @@
package ru.dragonestia.msb3.api.ai.navigator;
public record NavigatorPath() {
}

View File

@ -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);
}

View File

@ -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) {

View File

@ -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();
}
}

View File

@ -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();
}

View File

@ -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) {

View File

@ -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<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);
}
}

View File

@ -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;
}