Implemented new AI behavior patterns #1
56
api/src/main/java/ru/dragonestia/msb3/api/ai/AI.java
Normal file
56
api/src/main/java/ru/dragonestia/msb3/api/ai/AI.java
Normal file
@ -0,0 +1,56 @@
|
||||
package ru.dragonestia.msb3.api.ai;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.log4j.Log4j2;
|
||||
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.entity.EntityAI;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Класс для управления поведением сущности
|
||||
*/
|
||||
@Log4j2
|
||||
@Getter
|
||||
public class AI {
|
||||
|
||||
private final EntityAI entity;
|
||||
@Setter private NodeGenerator nodeGenerator = new GroundNodeGenerator();
|
||||
private final MovementFollower movementFollower;
|
||||
@Getter private final Actor actor;
|
||||
@Getter private MovementTarget movementTarget;
|
||||
|
||||
public AI(EntityAI entity) {
|
||||
this.entity = Objects.requireNonNull(entity, "AI is null");
|
||||
movementFollower = new GroundMovementFollower(entity);
|
||||
actor = new Actor(entity);
|
||||
}
|
||||
|
||||
public void setMovementTarget(MovementTarget movementTarget) {
|
||||
synchronized (this) {
|
||||
if (this.movementTarget != null) {
|
||||
this.movementTarget.cancel();
|
||||
}
|
||||
|
||||
this.movementTarget = movementTarget;
|
||||
|
||||
if (movementTarget != null) {
|
||||
movementTarget.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void tick(long delta) {
|
||||
synchronized (this) {
|
||||
var action = actor.getCurrentAction();
|
||||
if (action != null) action.tick(actor, entity, delta);
|
||||
|
||||
if (movementTarget != null) movementTarget.tick();
|
||||
}
|
||||
}
|
||||
}
|
||||
81
api/src/main/java/ru/dragonestia/msb3/api/ai/Actor.java
Normal file
81
api/src/main/java/ru/dragonestia/msb3/api/ai/Actor.java
Normal file
@ -0,0 +1,81 @@
|
||||
package ru.dragonestia.msb3.api.ai;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import ru.dragonestia.msb3.api.ai.action.Action;
|
||||
import ru.dragonestia.msb3.api.entity.EntityAI;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedList;
|
||||
|
||||
/**
|
||||
* Класс для управления планированием расписания действий для сущности
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
public class Actor {
|
||||
|
||||
@Getter private final EntityAI entity;
|
||||
@Getter @Setter private boolean active = false;
|
||||
@Getter private Action currentAction;
|
||||
private final LinkedList<Action> plannedActions = new LinkedList<>();
|
||||
|
||||
private void tryStartAction(Action action) {
|
||||
if (isActive()) action.start(this, entity);
|
||||
}
|
||||
|
||||
private void tryEndAction(Action action) {
|
||||
if (isActive()) action.stop(this, entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Принудительно установить действие для исполнения сущностью. Данная установка игнорирует проверки начала действия.
|
||||
* @param newAction Новое действие
|
||||
* @apiNote Если передать null, то текущее действие прервется и приостановится выполнение действий из плана
|
||||
*/
|
||||
public void setCurrentAction(Action newAction) {
|
||||
if (currentAction != null) tryEndAction(currentAction);
|
||||
currentAction = newAction;
|
||||
if (newAction != null) tryStartAction(newAction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Принудительно установить действие для исполнения сущностью и при этом текущее действие помещается в
|
||||
* план действий в самое начало. Использовать, для того чтобы прервать выполнение текущего действия,
|
||||
* переключиться на другое и снова вернуться к той задаче, которую он изначально выполнял
|
||||
* @param newAction Новое действие
|
||||
*/
|
||||
public void setCurrentActionAndRememberPrev(Action newAction) {
|
||||
if (currentAction != null) plannedActions.addFirst(currentAction);
|
||||
setCurrentAction(newAction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Завершить текущее действие и перейти к следующему исходя из плана
|
||||
*/
|
||||
public void useNextAction() {
|
||||
while (true) {
|
||||
var action = plannedActions.poll();
|
||||
if (action == null) break; // TODO: добавлять действия из генератора расписаний
|
||||
if (!action.canStart(this, entity)) continue; // Действие не может начаться, так как условие не выполняется
|
||||
setCurrentAction(action);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавить действие в план выполнения. Если у сущности нет текущей задачи, то оно приступит к задаче сразу же
|
||||
* @param action Действие
|
||||
*/
|
||||
public void addNextAction(Action action) {
|
||||
plannedActions.add(action);
|
||||
if (currentAction == null) useNextAction();
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить план действий для сущности
|
||||
* @return План действий
|
||||
*/
|
||||
public Collection<Action> getPlannedActions() {
|
||||
return plannedActions;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
package ru.dragonestia.msb3.api.ai;
|
||||
|
||||
public interface ArrivalResultListener {
|
||||
|
||||
/**
|
||||
* Отправляет завершенное состояние пути. Может быть положительным (моб дошел до нужной точки), либо негативным (потерялся)
|
||||
* @param resultState Состояние полученное в конце пути
|
||||
*/
|
||||
void result(FollowerState resultState);
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
package ru.dragonestia.msb3.api.ai;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum FollowerState {
|
||||
|
||||
/**
|
||||
* Путь не задан.
|
||||
* @apiNote Можно получить только при получении текущего состояния пути Entity
|
||||
*/
|
||||
NONE(true, true),
|
||||
|
||||
/**
|
||||
* Entity находится в состоянии пути к какой-то точке, либо же идет просчет пути до точки
|
||||
* @apiNote Можно получить только при получении текущего состояния пути Entity
|
||||
*/
|
||||
FOLLOWING(false, true),
|
||||
|
||||
/**
|
||||
* Путь успешно пройден.
|
||||
* @apiNote Может являться результатом для CompletableFuture
|
||||
*/
|
||||
COMPLETED(true, true),
|
||||
|
||||
/**
|
||||
* Entity зашел в тупик и не может найти путь.
|
||||
* @apiNote Может являться результатом для CompletableFuture
|
||||
*/
|
||||
DEADLOCKED(true, false),
|
||||
|
||||
/**
|
||||
* Путь был принудительно отклонен.
|
||||
* @apiNote Может являться результатом для CompletableFuture
|
||||
*/
|
||||
TERMINATED(true, false),
|
||||
|
||||
/**
|
||||
* Entity не может дойти до указанной точки так как она превышает лимит длинны пути.
|
||||
* @apiNote Может являться результатом для CompletableFuture
|
||||
*/
|
||||
TOO_FAR(true, false),
|
||||
|
||||
;
|
||||
|
||||
private final boolean completed;
|
||||
private final boolean success;
|
||||
}
|
||||
@ -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) {}
|
||||
@ -0,0 +1,43 @@
|
||||
package ru.dragonestia.msb3.api.ai.action;
|
||||
|
||||
import ru.dragonestia.msb3.api.ai.Actor;
|
||||
import ru.dragonestia.msb3.api.entity.EntityAI;
|
||||
|
||||
public interface Action {
|
||||
|
||||
/**
|
||||
* Получить имя действия, которое примерно описывает что должна делать сущность
|
||||
* @return Имя действия
|
||||
*/
|
||||
String getName();
|
||||
|
||||
/**
|
||||
* Действие начало выполнение
|
||||
* @param actor Действующее лицо
|
||||
* @param entity Действующая сущность
|
||||
*/
|
||||
void start(Actor actor, EntityAI entity);
|
||||
|
||||
/**
|
||||
* Выполнение действия раз в тик
|
||||
* @param actor Действующее лицо
|
||||
* @param entity Действующая сущность
|
||||
* @param delta Сколько времени (в миллисекундах) прошло с предыдущего тика
|
||||
*/
|
||||
void tick(Actor actor, EntityAI entity, long delta);
|
||||
|
||||
/**
|
||||
* Остановка действия
|
||||
* @param actor Действующее лицо
|
||||
* @param entity Действующая сущность
|
||||
*/
|
||||
void stop(Actor actor, EntityAI entity);
|
||||
|
||||
/**
|
||||
* Может ли действие начаться? Данная проверка выполняется только, если действие находится в очереди действий
|
||||
* @param actor Действующее лицо
|
||||
* @param entity Действующая сущность
|
||||
* @return Результат проверки
|
||||
*/
|
||||
boolean canStart(Actor actor, EntityAI entity);
|
||||
}
|
||||
@ -1,43 +1,42 @@
|
||||
package ru.dragonestia.msb3.api.entity.goal;
|
||||
package ru.dragonestia.msb3.api.ai.action;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import net.minestom.server.entity.EntityCreature;
|
||||
import net.minestom.server.entity.Player;
|
||||
import net.minestom.server.entity.ai.GoalSelector;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import ru.dragonestia.msb3.api.ai.Actor;
|
||||
import ru.dragonestia.msb3.api.entity.EntityAI;
|
||||
|
||||
public class LookCloseGoal extends GoalSelector {
|
||||
/**
|
||||
* Entity смотрит на ближайшего игрока в радиусе 7 блоков
|
||||
*/
|
||||
public class LookClosePlayersAction implements Action {
|
||||
|
||||
private final static int MAX_DIST = 7 * 7;
|
||||
|
||||
@Setter @Getter private volatile boolean enabled = false;
|
||||
|
||||
private boolean lastSeePlayer = false;
|
||||
@Getter private float defaultYaw;
|
||||
@Getter private float defaultPitch;
|
||||
|
||||
public LookCloseGoal(@NotNull EntityCreature entityCreature) {
|
||||
super(entityCreature);
|
||||
|
||||
defaultYaw = entityCreature.getPosition().yaw();
|
||||
defaultPitch = entityCreature.getPosition().pitch();
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Смотрит на ближайших игроков";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldStart() {
|
||||
return true;
|
||||
public void start(Actor actor, EntityAI entity) {
|
||||
defaultYaw = entity.getPosition().yaw();
|
||||
defaultPitch = entity.getPosition().pitch();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {}
|
||||
|
||||
@Override
|
||||
public void tick(long l) {
|
||||
if ((l & 0b11) == 0) return;
|
||||
public void tick(Actor actor, EntityAI entity, long delta) {
|
||||
if ((delta & 0b11) == 0) return;
|
||||
|
||||
if (!enabled) {
|
||||
if (lastSeePlayer) {
|
||||
entityCreature.setView(defaultYaw, defaultPitch);
|
||||
entity.setView(defaultYaw, defaultPitch);
|
||||
lastSeePlayer = false;
|
||||
}
|
||||
return;
|
||||
@ -46,8 +45,8 @@ public class LookCloseGoal extends GoalSelector {
|
||||
Player closestPlayer = null;
|
||||
double closestDistance = Double.MAX_VALUE;
|
||||
|
||||
for (var player: entityCreature.getViewers()) {
|
||||
var distance = player.getPosition().distanceSquared(entityCreature.getPosition());
|
||||
for (var player: entity.getViewers()) {
|
||||
var distance = player.getPosition().distanceSquared(entity.getPosition());
|
||||
if (distance > MAX_DIST) continue;
|
||||
|
||||
if (distance < closestDistance) {
|
||||
@ -57,24 +56,24 @@ public class LookCloseGoal extends GoalSelector {
|
||||
}
|
||||
|
||||
if (closestPlayer != null) {
|
||||
entityCreature.lookAt(closestPlayer);
|
||||
entity.lookAt(closestPlayer);
|
||||
lastSeePlayer = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastSeePlayer) {
|
||||
entityCreature.setView(defaultYaw, defaultPitch);
|
||||
entity.setView(defaultYaw, defaultPitch);
|
||||
lastSeePlayer = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldEnd() {
|
||||
return false;
|
||||
}
|
||||
public void stop(Actor actor, EntityAI entity) {}
|
||||
|
||||
@Override
|
||||
public void end() {}
|
||||
public boolean canStart(Actor actor, EntityAI entity) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void setDefaultRotation(float yaw, float pitch) {
|
||||
defaultYaw = yaw;
|
||||
@ -0,0 +1,106 @@
|
||||
package ru.dragonestia.msb3.api.ai.action;
|
||||
|
||||
import lombok.Getter;
|
||||
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.FollowerState;
|
||||
import ru.dragonestia.msb3.api.ai.movement.target.MovementTarget;
|
||||
import ru.dragonestia.msb3.api.entity.EntityAI;
|
||||
import ru.dragonestia.msb3.api.scheduler.Scheduler;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Entity патрулирует местность по заданному маршруту
|
||||
*/
|
||||
public class PatrolAction implements Action {
|
||||
|
||||
@Getter private final List<Vec> path = new ArrayList<>();
|
||||
@Getter @Setter private double destinationRadius = 1;
|
||||
@Getter private Vec currentPos;
|
||||
private boolean increment = true;
|
||||
private int posNumber = 0;
|
||||
private EntityAI entity;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Патрулирование территории";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Actor actor, EntityAI entity) {
|
||||
walkToNextPos(entity);
|
||||
this.entity = entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick(Actor actor, EntityAI entity, long delta) {}
|
||||
|
||||
@Override
|
||||
public void stop(Actor actor, EntityAI entity) {
|
||||
var ai = entity.getAi();
|
||||
if (ai.getMovementTarget() != null) ai.getMovementTarget().cancel();
|
||||
this.entity = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canStart(Actor actor, EntityAI entity) {
|
||||
return findNearestPos(entity) != null;
|
||||
}
|
||||
|
||||
public void setPath(List<Vec> newPath) {
|
||||
posNumber = 0;
|
||||
increment = true;
|
||||
path.clear();
|
||||
path.addAll(newPath);
|
||||
|
||||
if (entity != null) walkToNextPos(entity);
|
||||
}
|
||||
|
||||
private Vec findNearestPos(EntityAI entity) {
|
||||
if (entity == null) return path.getFirst();
|
||||
|
||||
Vec nearest = null;
|
||||
double dist = Double.MAX_VALUE;
|
||||
for (var pos: path) {
|
||||
var currentDist = entity.getDistance(pos);
|
||||
if (currentDist < dist) nearest = pos;
|
||||
}
|
||||
return nearest;
|
||||
}
|
||||
|
||||
private Vec useNextPos() {
|
||||
int nextIndex = posNumber + (increment? 1 : -1);
|
||||
if (nextIndex >= path.size()) increment = false;
|
||||
if (nextIndex < 0) increment = true;
|
||||
posNumber = posNumber + (increment? 1 : -1);
|
||||
return currentPos = path.get(posNumber);
|
||||
}
|
||||
|
||||
private void walkToNextPos(EntityAI entity) {
|
||||
var nextPos = useNextPos();
|
||||
var ai = entity.getAi();
|
||||
|
||||
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);
|
||||
}));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,85 @@
|
||||
package ru.dragonestia.msb3.api.ai.movement.follower;
|
||||
|
||||
import net.minestom.server.collision.CollisionUtils;
|
||||
import net.minestom.server.collision.PhysicsResult;
|
||||
import net.minestom.server.coordinate.Point;
|
||||
import net.minestom.server.coordinate.Vec;
|
||||
import ru.dragonestia.msb3.api.entity.EntityAI;
|
||||
|
||||
public class GroundMovementFollower extends MovementFollower {
|
||||
|
||||
private PhysicsResult lastPhysicResult;
|
||||
|
||||
public GroundMovementFollower(EntityAI entity) {
|
||||
super(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public double squaredDistance(Point destination) {
|
||||
return getEntity().getPosition().withY(0).distanceSquared(destination.withY(0));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PhysicsResult moveResult(Point direction, double speed) {
|
||||
var radians = Math.atan2(direction.z(), direction.x());
|
||||
var speedX = Math.cos(radians) * speed;
|
||||
var speedZ = Math.sin(radians) * speed;
|
||||
|
||||
double speedY = 0;
|
||||
if (lastPhysicResult != null) {
|
||||
double highestDeltaCollision = 0;
|
||||
|
||||
for (int i = 0; i < lastPhysicResult.collisionShapes().length; i++) {
|
||||
var shape = lastPhysicResult.collisionShapes()[i];
|
||||
var shapePos = lastPhysicResult.collisionShapePositions()[i];
|
||||
if (shape == null || shapePos == null) continue;
|
||||
|
||||
var point = shapePos.add(shape.relativeEnd());
|
||||
var deltaH = point.y() - getEntity().getPosition().y();
|
||||
if (deltaH > getMaxHeightStep() && deltaH > getJumpHeight()) continue;
|
||||
highestDeltaCollision = Math.max(highestDeltaCollision, deltaH);
|
||||
}
|
||||
|
||||
if (highestDeltaCollision > 0 && getEntity().isOnGround()) {
|
||||
speedY = highestDeltaCollision + 0.08;
|
||||
}
|
||||
}
|
||||
|
||||
return lastPhysicResult = CollisionUtils.handlePhysics(getEntity(), new Vec(speedX, speedY, speedZ));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PhysicsResult move(Point direction, double speed) {
|
||||
return super.move(direction, speed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PhysicsResult move(Point direction, double speed, Point lookDirection) {
|
||||
return super.move(direction, speed, lookDirection);
|
||||
}
|
||||
|
||||
public double getJumpHeight() {
|
||||
return 1.1;
|
||||
}
|
||||
|
||||
public double getMaxHeightStep() {
|
||||
return 0.6;
|
||||
}
|
||||
|
||||
public void jump() {
|
||||
jump(0, 0);
|
||||
}
|
||||
|
||||
public void jump(Point direction) {
|
||||
jump(direction.x(), direction.y(), direction.z());
|
||||
}
|
||||
|
||||
public void jump(double x, double z) {
|
||||
jump(x, getEntity().getJumpHeight(), z);
|
||||
}
|
||||
|
||||
public void jump(double x, double y, double z) {
|
||||
if (!getEntity().isOnGround()) return;
|
||||
getEntity().setVelocity(new Vec(x, y, z).mul(7.1));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
package ru.dragonestia.msb3.api.ai.movement.follower;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import net.minestom.server.collision.PhysicsResult;
|
||||
import net.minestom.server.coordinate.Point;
|
||||
import net.minestom.server.coordinate.Pos;
|
||||
import net.minestom.server.utils.position.PositionUtils;
|
||||
import ru.dragonestia.msb3.api.entity.EntityAI;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public abstract class MovementFollower {
|
||||
|
||||
private final EntityAI entity;
|
||||
|
||||
public double squaredDistance(Point destination) {
|
||||
return entity.getDistanceSquared(destination);
|
||||
}
|
||||
|
||||
public abstract PhysicsResult moveResult(Point direction, double speed);
|
||||
|
||||
public PhysicsResult move(Point direction, double speed) {
|
||||
var physicResult = moveResult(direction, speed);
|
||||
var newPosition = entity.getPosition().withCoord(physicResult.newPosition());
|
||||
entity.refreshPosition(newPosition);
|
||||
return physicResult;
|
||||
}
|
||||
|
||||
public PhysicsResult move(Point direction, double speed, Point lookDirection) {
|
||||
var yaw = PositionUtils.getLookYaw(lookDirection.x(), direction.z());
|
||||
var pitch = PositionUtils.getLookPitch(lookDirection.x(), lookDirection.y(), direction.z());
|
||||
var physicResult = moveResult(direction, speed);
|
||||
var newPosition = Pos.fromPoint(physicResult.newPosition()).withView(yaw, pitch);
|
||||
entity.refreshPosition(newPosition);
|
||||
return physicResult;
|
||||
}
|
||||
|
||||
public PhysicsResult moveTo(Point destination, double speed, Point lookAt) {
|
||||
var direction = destination.sub(entity.getPosition());
|
||||
var lookDirection = lookAt.sub(entity.getPosition());
|
||||
return move(direction, speed, lookDirection);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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() {
|
||||
}
|
||||
@ -0,0 +1,61 @@
|
||||
package ru.dragonestia.msb3.api.ai.navigator;
|
||||
|
||||
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;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
public class Path {
|
||||
|
||||
@Getter private final double maxDistance;
|
||||
@Getter private final double pathVariance;
|
||||
@Getter private final List<PathNode> nodes = new ArrayList<>();
|
||||
private final AtomicReference<FollowerState> state = new AtomicReference<>(FollowerState.FOLLOWING);
|
||||
private int index = 0;
|
||||
|
||||
public Path(double maxDistance, double pathVariance) {
|
||||
this.maxDistance = maxDistance;
|
||||
this.pathVariance = pathVariance;
|
||||
}
|
||||
|
||||
public FollowerState getState() {
|
||||
return state.get();
|
||||
}
|
||||
|
||||
public void setState(FollowerState newState) {
|
||||
state.set(newState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return nodes.toString();
|
||||
}
|
||||
|
||||
public @Nullable PathNode.Type getCurrentType() {
|
||||
if (index >= nodes.size()) return null;
|
||||
var current = nodes.get(index);
|
||||
return current.getType();
|
||||
}
|
||||
|
||||
public @Nullable Point getCurrent() {
|
||||
if (index >= nodes.size()) return null;
|
||||
var current = nodes.get(index);
|
||||
return new Vec(current.x(), current.y(), current.z());
|
||||
}
|
||||
|
||||
public void next() {
|
||||
if (index >= nodes.size()) return;
|
||||
index++;
|
||||
}
|
||||
|
||||
public Point getNext() {
|
||||
if (index + 1 >= nodes.size()) return null;
|
||||
var current = nodes.get(index + 1);
|
||||
return new Vec(current.x(), current.y(), current.z());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,135 @@
|
||||
package ru.dragonestia.msb3.api.ai.navigator;
|
||||
|
||||
import it.unimi.dsi.fastutil.objects.ObjectHeapPriorityQueue;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectOpenHashBigSet;
|
||||
import lombok.experimental.UtilityClass;
|
||||
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.*;
|
||||
|
||||
@UtilityClass
|
||||
public class PathGenerator {
|
||||
|
||||
private final Comparator<PathNode> pathNodeComparator = (first, second) -> (int) (((first.g() + first.h()) - (second.g() + second.h())) * 1000);
|
||||
|
||||
public static Path generate(Block.Getter getter, Pos orgStart, Point orgTarget, double closeDistance, double maxDistance, double pathVariance, BoundingBox boundingBox, boolean isOnGround, NodeGenerator generator) {
|
||||
final Point start = (!isOnGround && generator.hasGravitySnap())
|
||||
? orgStart.withY(generator.gravitySnap(getter, orgStart.x(), orgStart.y(), orgStart.z(), boundingBox, 100).orElse(orgStart.y()))
|
||||
: orgStart;
|
||||
|
||||
final Point target = (generator.hasGravitySnap())
|
||||
? orgTarget.withY(generator.gravitySnap(getter, orgTarget.x(), orgTarget.y(), orgTarget.z(), boundingBox, 100).orElse(orgTarget.y()))
|
||||
: Pos.fromPoint(orgTarget);
|
||||
|
||||
var path = new Path(maxDistance, pathVariance);
|
||||
computePath(getter, start, target, closeDistance, maxDistance, pathVariance, boundingBox, path, generator);
|
||||
return path;
|
||||
}
|
||||
|
||||
private static PathNode buildRepathNode(PathNode parent) {
|
||||
return new PathNode(0, 0, 0, 0, 0, PathNode.Type.REPATH, parent);
|
||||
}
|
||||
|
||||
private void computePath(Block.Getter getter, Point start, Point target, double closeDistance, double maxDistance, double pathVariance, BoundingBox boundingBox, Path path, NodeGenerator generator) {
|
||||
double closestDistance = Double.MAX_VALUE;
|
||||
double straightDistance = generator.heuristic(start, target);
|
||||
int maxSize = (int) Math.floor(maxDistance * 10);
|
||||
|
||||
closeDistance = Math.max(0.8, closeDistance);
|
||||
List<PathNode> closestFoundNodes = List.of();
|
||||
|
||||
var pStart = new PathNode(start, 0, generator.heuristic(start, target), PathNode.Type.WALK, null);
|
||||
|
||||
var open = new ObjectHeapPriorityQueue<>(pathNodeComparator);
|
||||
open.enqueue(pStart);
|
||||
|
||||
var closed = new ObjectOpenHashBigSet<PathNode>(maxSize);
|
||||
|
||||
while (!open.isEmpty() && closed.size64() < maxSize) {
|
||||
if (path.getState() == FollowerState.TERMINATED) {
|
||||
return;
|
||||
}
|
||||
|
||||
var current = open.dequeue();
|
||||
|
||||
if (((current.g() + current.h()) - straightDistance) > pathVariance) continue;
|
||||
if (!withinDistance(current, start, maxDistance)) continue;
|
||||
if (withinDistance(current, target, closeDistance)) {
|
||||
open.enqueue(current);
|
||||
break;
|
||||
}
|
||||
|
||||
if (current.h() < closestDistance) {
|
||||
closestDistance = current.h();
|
||||
closestFoundNodes = List.of(current);
|
||||
}
|
||||
|
||||
Collection<? extends PathNode> found = generator.getWalkable(getter, closed, current, target, boundingBox);
|
||||
found.forEach(p -> {
|
||||
if (getDistanceSquared(p.x(), p.y(), p.z(), start) <= (maxDistance * maxDistance)) {
|
||||
open.enqueue(p);
|
||||
closed.add(p);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
PathNode current = open.isEmpty() ? null : open.dequeue();
|
||||
|
||||
if (current == null || !withinDistance(current, target, closeDistance)) {
|
||||
if (closestFoundNodes.isEmpty()) {
|
||||
path.setState(FollowerState.DEADLOCKED);
|
||||
return;
|
||||
}
|
||||
|
||||
current = closestFoundNodes.getFirst();
|
||||
|
||||
if (!open.isEmpty()) {
|
||||
current = buildRepathNode(current);
|
||||
}
|
||||
}
|
||||
|
||||
while (Objects.requireNonNull(current).parent() != null) {
|
||||
path.getNodes().add(current);
|
||||
current = current.parent();
|
||||
}
|
||||
|
||||
Collections.reverse(path.getNodes());
|
||||
|
||||
if (path.getCurrentType() == PathNode.Type.REPATH) {
|
||||
path.setState(FollowerState.DEADLOCKED);
|
||||
path.getNodes().clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.getNodes().isEmpty()) {
|
||||
path.setState(FollowerState.DEADLOCKED);
|
||||
return;
|
||||
}
|
||||
|
||||
var lastNode = path.getNodes().getLast();
|
||||
if (getDistanceSquared(lastNode.x(), lastNode.y(), lastNode.z(), target) > (closeDistance * closeDistance)) {
|
||||
path.setState(FollowerState.DEADLOCKED);
|
||||
return;
|
||||
}
|
||||
|
||||
PathNode pEnd = new PathNode(target, 0, 0, PathNode.Type.WALK, null);
|
||||
path.getNodes().add(pEnd);
|
||||
path.setState(FollowerState.FOLLOWING);
|
||||
}
|
||||
|
||||
private boolean withinDistance(PathNode point, Point target, double closeDistance) {
|
||||
return getDistanceSquared(point.x(), point.y(), point.z(), target) < (closeDistance * closeDistance);
|
||||
}
|
||||
|
||||
private double getDistanceSquared(double x, double y, double z, Point target) {
|
||||
var dx = x - target.x();
|
||||
var dy = y - target.y();
|
||||
var dz = z - target.z();
|
||||
return dx * dx + dy * dy + dz * dz;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,125 @@
|
||||
package ru.dragonestia.msb3.api.ai.navigator;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import net.minestom.server.coordinate.Point;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public class PathNode {
|
||||
|
||||
@Setter private double g;
|
||||
@Setter private double h;
|
||||
private PathNode parent;
|
||||
private double pointX;
|
||||
private double pointY;
|
||||
private double pointZ;
|
||||
private int hashCode;
|
||||
@Setter @Getter private Type type;
|
||||
|
||||
public PathNode(double px, double py, double pz, double g, double h, @Nullable PathNode parent) {
|
||||
this(px, py, pz, g, h, Type.WALK, parent);
|
||||
}
|
||||
|
||||
public PathNode(double px, double py, double pz, double g, double h, Type type, @Nullable PathNode parent) {
|
||||
this.g = g;
|
||||
this.h = h;
|
||||
this.parent = parent;
|
||||
this.type = type;
|
||||
this.pointX = px;
|
||||
this.pointY = py;
|
||||
this.pointZ = pz;
|
||||
this.hashCode = cantor((int) Math.floor(px), cantor((int) Math.floor(py), (int) Math.floor(pz)));
|
||||
}
|
||||
|
||||
public PathNode(Point point, double g, double h, Type walk, @Nullable PathNode parent) {
|
||||
this(point.x(), point.y(), point.z(), g, h, walk, parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return hashCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == null) return false;
|
||||
if (obj == this) return true;
|
||||
if (!(obj instanceof PathNode other)) return false;
|
||||
return this.hashCode == other.hashCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PathNode{point=(%s, %s, %s), d=%s, type=%s}".formatted(
|
||||
pointX,
|
||||
pointY,
|
||||
pointZ,
|
||||
g + h,
|
||||
type
|
||||
);
|
||||
}
|
||||
|
||||
public double x() {
|
||||
return pointX;
|
||||
}
|
||||
|
||||
public double y() {
|
||||
return pointY;
|
||||
}
|
||||
|
||||
public double z() {
|
||||
return pointZ;
|
||||
}
|
||||
|
||||
public int blockX() {
|
||||
return (int) Math.floor(pointX);
|
||||
}
|
||||
|
||||
public int blockY() {
|
||||
return (int) Math.floor(pointY);
|
||||
}
|
||||
|
||||
public int blockZ() {
|
||||
return (int) Math.floor(pointZ);
|
||||
}
|
||||
|
||||
public double g() {
|
||||
return g;
|
||||
}
|
||||
|
||||
public double h() {
|
||||
return h;
|
||||
}
|
||||
|
||||
public void setPoint(double px, double py, double pz) {
|
||||
this.pointX = px;
|
||||
this.pointY = py;
|
||||
this.pointZ = pz;
|
||||
this.hashCode = cantor((int) Math.floor(px), cantor((int) Math.floor(py), (int) Math.floor(pz)));
|
||||
}
|
||||
|
||||
public @Nullable PathNode parent() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
public void setParent(@Nullable PathNode current) {
|
||||
this.parent = current;
|
||||
}
|
||||
|
||||
private static int cantor(int a, int b) {
|
||||
int ca = a >= 0 ? 2 * a : -2 * a - 1;
|
||||
int cb = b >= 0 ? 2 * b : -2 * b - 1;
|
||||
return (ca + cb + 1) * (ca + cb) / 2 + cb;
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
WALK,
|
||||
JUMP,
|
||||
FALL,
|
||||
CLIMB,
|
||||
CLIMB_WALL,
|
||||
SWIM,
|
||||
FLY,
|
||||
REPATH
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,128 @@
|
||||
package ru.dragonestia.msb3.api.ai.navigator.node;
|
||||
|
||||
import net.minestom.server.collision.BoundingBox;
|
||||
import net.minestom.server.coordinate.Point;
|
||||
import net.minestom.server.coordinate.Vec;
|
||||
import net.minestom.server.instance.block.Block;
|
||||
import ru.dragonestia.msb3.api.ai.navigator.PathNode;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.OptionalDouble;
|
||||
import java.util.Set;
|
||||
|
||||
public class GroundNodeGenerator implements NodeGenerator {
|
||||
|
||||
private final static int MAX_FALL_DISTANCE = 5;
|
||||
|
||||
private final BoundingBox.PointIterator pointIterator = new BoundingBox.PointIterator();
|
||||
private PathNode tempNode = null;
|
||||
|
||||
@Override
|
||||
public Collection<? extends PathNode> getWalkable(Block.Getter getter, Set<PathNode> visited, PathNode current, Point goal, BoundingBox boundingBox) {
|
||||
Collection<PathNode> nearby = new ArrayList<>();
|
||||
tempNode = new PathNode(0, 0, 0, 0, 0, current);
|
||||
|
||||
int stepSize = (int) Math.max(Math.floor(boundingBox.width() / 2), 1);
|
||||
if (stepSize < 1) stepSize = 1;
|
||||
|
||||
for (int x = -stepSize; x <= stepSize; ++x) {
|
||||
for (int z = -stepSize; z <= stepSize; ++z) {
|
||||
if (x == 0 && z == 0) continue;
|
||||
double cost = Math.sqrt(x * x + z * z) * 0.98;
|
||||
|
||||
double floorPointX = current.blockX() + 0.5 + x;
|
||||
double floorPointY = current.blockY();
|
||||
double floorPointZ = current.blockZ() + 0.5 + z;
|
||||
|
||||
var optionalFloorPointY = gravitySnap(getter, floorPointX, floorPointY, floorPointZ, boundingBox, MAX_FALL_DISTANCE);
|
||||
if (optionalFloorPointY.isEmpty()) continue;
|
||||
floorPointY = optionalFloorPointY.getAsDouble();
|
||||
|
||||
var floorPoint = new Vec(floorPointX, floorPointY, floorPointZ);
|
||||
|
||||
var nodeWalk = createWalk(getter, floorPoint, boundingBox, cost, current, goal, visited);
|
||||
if (nodeWalk != null && !visited.contains(nodeWalk)) nearby.add(nodeWalk);
|
||||
|
||||
for (int i = 1; i == 1; ++i) {
|
||||
Point jumpPoint = new Vec(current.blockX() + 0.5 + x, current.blockY() + i, current.blockZ() + 0.5 + z);
|
||||
OptionalDouble jumpPointY = gravitySnap(getter, jumpPoint.x(), jumpPoint.y(), jumpPoint.z(), boundingBox, MAX_FALL_DISTANCE);
|
||||
if (jumpPointY.isEmpty()) continue;
|
||||
jumpPoint = jumpPoint.withY(jumpPointY.getAsDouble());
|
||||
|
||||
if (!floorPoint.sameBlock(jumpPoint)) {
|
||||
var nodeJump = createJump(getter, jumpPoint, boundingBox, cost + 0.2, current, goal, visited);
|
||||
if (nodeJump != null && !visited.contains(nodeJump)) nearby.add(nodeJump);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nearby;
|
||||
}
|
||||
|
||||
private PathNode createWalk(Block.Getter getter, Point point, BoundingBox boundingBox, double cost, PathNode start, Point goal, Set<PathNode> closed) {
|
||||
var n = newNode(start, cost, point, goal);
|
||||
if (closed.contains(n)) return null;
|
||||
|
||||
if (Math.abs(point.y() - start.y()) > Vec.EPSILON && point.y() < start.y()) {
|
||||
if (start.y() - point.y() > MAX_FALL_DISTANCE) return null;
|
||||
if (!canMoveTowards(getter, new Vec(start.x(), start.y(), start.z()), point.withY(start.y()), boundingBox))
|
||||
return null;
|
||||
n.setType(PathNode.Type.FALL);
|
||||
} else {
|
||||
if (!canMoveTowards(getter, new Vec(start.x(), start.y(), start.z()), point, boundingBox)) return null;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
private PathNode createJump(Block.Getter getter, Point point, BoundingBox boundingBox, double cost, PathNode start, Point goal, Set<PathNode> closed) {
|
||||
if (Math.abs(point.y() - start.y()) < Vec.EPSILON) return null;
|
||||
if (point.y() - start.y() > 2) return null;
|
||||
if (point.blockX() != start.blockX() && point.blockZ() != start.blockZ()) return null;
|
||||
|
||||
var n = newNode(start, cost, point, goal);
|
||||
if (closed.contains(n)) return null;
|
||||
|
||||
if (pointInvalid(getter, point, boundingBox)) return null;
|
||||
if (pointInvalid(getter, new Vec(start.x(), start.y() + 1, start.z()), boundingBox)) return null;
|
||||
|
||||
n.setType(PathNode.Type.JUMP);
|
||||
return n;
|
||||
}
|
||||
|
||||
private PathNode newNode(PathNode current, double cost, Point point, Point goal) {
|
||||
tempNode.setG(current.g() + cost);
|
||||
tempNode.setH(heuristic(point, goal));
|
||||
tempNode.setPoint(point.x(), point.y(), point.z());
|
||||
|
||||
var newNode = tempNode;
|
||||
tempNode = new PathNode(0, 0, 0, 0, 0, PathNode.Type.WALK, current);
|
||||
|
||||
return newNode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasGravitySnap() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OptionalDouble gravitySnap(Block.Getter getter, double pointOrgX, double pointOrgY, double pointOrgZ, BoundingBox boundingBox, double maxFall) {
|
||||
var pointX = (int) Math.floor(pointOrgX) + 0.5;
|
||||
var pointY = (int) Math.floor(pointOrgY);
|
||||
var pointZ = (int) Math.floor(pointOrgZ) + 0.5;
|
||||
|
||||
for (int axis = 1; axis <= maxFall; ++axis) {
|
||||
pointIterator.reset(boundingBox, pointX, pointY, pointZ, BoundingBox.AxisMask.Y, -axis);
|
||||
|
||||
while (pointIterator.hasNext()) {
|
||||
var block = pointIterator.next();
|
||||
if (getter.getBlock(block.blockX(), block.blockY(), block.blockZ(), Block.Getter.Condition.TYPE).isSolid()) {
|
||||
return OptionalDouble.of(block.blockY() + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
return OptionalDouble.empty();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
package ru.dragonestia.msb3.api.ai.navigator.node;
|
||||
|
||||
import net.minestom.server.collision.BoundingBox;
|
||||
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.instance.block.Block;
|
||||
import ru.dragonestia.msb3.api.ai.navigator.PathNode;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.OptionalDouble;
|
||||
import java.util.Set;
|
||||
|
||||
public interface NodeGenerator {
|
||||
|
||||
Collection<? extends PathNode> getWalkable(Block.Getter getter, Set<PathNode> visited, PathNode current, Point goal, BoundingBox boundingBox);
|
||||
|
||||
boolean hasGravitySnap();
|
||||
|
||||
OptionalDouble gravitySnap(Block.Getter getter, double pointX, double pointY, double pointZ, BoundingBox boundingBox, double maxFall);
|
||||
|
||||
default boolean canMoveTowards(Block.Getter getter, Point start, Point end, BoundingBox boundingBox) {
|
||||
final Point diff = end.sub(start);
|
||||
|
||||
if (getter.getBlock(end) != Block.AIR) return false;
|
||||
var res = CollisionUtils.handlePhysics(getter, boundingBox, Pos.fromPoint(start), Vec.fromPoint(diff), null, false);
|
||||
return !res.collisionZ() && !res.collisionY() && !res.collisionX();
|
||||
}
|
||||
|
||||
default boolean pointInvalid(Block.Getter getter, Point point, BoundingBox boundingBox) {
|
||||
var iterator = boundingBox.getBlocks(point);
|
||||
while (iterator.hasNext()) {
|
||||
var block = iterator.next();
|
||||
if (getter.getBlock(block.blockX(), block.blockY(), block.blockZ(), Block.Getter.Condition.TYPE).isSolid()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
default double heuristic(Point node, Point target) {
|
||||
return node.distance(target);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
package ru.dragonestia.msb3.api.boot;
|
||||
|
||||
import net.minestom.server.coordinate.Pos;
|
||||
import net.minestom.server.entity.GameMode;
|
||||
import ru.dragonestia.msb3.api.module.FlatWorldModule;
|
||||
import ru.dragonestia.msb3.api.module.MotdModule;
|
||||
import ru.dragonestia.msb3.api.module.ResourcePackRepositoryModule;
|
||||
import ru.dragonestia.msb3.api.script.ScriptRegistry;
|
||||
import ru.dragonestia.msb3.api.script.defaults.SpawnTestActorScript;
|
||||
import ru.dragonestia.msb3.api.util.ChunkPreloader;
|
||||
import ru.dragonestia.msb3.api.world.WorldFactory;
|
||||
import team.unnamed.creative.ResourcePack;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class DebugAiBootstrap extends ServerInitializer {
|
||||
|
||||
public static void main(String[] args) {
|
||||
ServerBootstrap.start("0.0.0.0", 25565, new DebugAiBootstrap());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoad() {}
|
||||
|
||||
@Override
|
||||
public void onDefaultModulesLoaded() {
|
||||
MotdModule.init("logo.png", "<gradient:#ff0059:#e06806><bold>msb3 server - AI test</bold></gradient>");
|
||||
|
||||
// Скачать мир можно здесь: http://files.dragonestia.ru/f/fd754add490b42929130/
|
||||
FlatWorldModule.init(GameMode.CREATIVE, WorldFactory.anvil(new File("./debug_ai")).createWorldSync(), new Pos(0.5, 61, 0.5, -135, 0));
|
||||
|
||||
ScriptRegistry.register(SpawnTestActorScript::new);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInitializeResources(ResourcePack resourcePack) {}
|
||||
|
||||
@Override
|
||||
public void onResourcePackCompiled(ResourcePack resourcePack) {
|
||||
ResourcePackRepositoryModule.init("0.0.0.0", 7270);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServerStarted() {
|
||||
var instance = FlatWorldModule.getWorld().getInstance();
|
||||
ChunkPreloader.preload(instance, -1, -16, 18, 0);
|
||||
instance.setTimeRate(0);
|
||||
instance.setTime(5000);
|
||||
}
|
||||
}
|
||||
@ -195,6 +195,7 @@ public final class ServerBootstrap {
|
||||
PlayerContextManager.registerContext(TalksContext.class, TalksContext::new);
|
||||
PlayerContextManager.registerContext(DebugParamsContext.class, DebugParamsContext::new);
|
||||
PlayerContextManager.registerContext(PlayerScriptContext.class, PlayerScriptContext::new);
|
||||
PlayerContextManager.registerContext(PlayerQuestContext.class, PlayerQuestContext::new);
|
||||
}
|
||||
|
||||
private void initDefaultSkins() {
|
||||
|
||||
@ -22,6 +22,7 @@ public class DebugAICommand extends Command {
|
||||
setDefaultExecutor(this::defaultExecutor);
|
||||
|
||||
addSyntax(this::executeShowPath, ArgumentType.Literal("show_path"), argShowPathValue);
|
||||
addSubcommand(new PuppeteerSubcommand());
|
||||
}
|
||||
|
||||
public static void register() {
|
||||
|
||||
@ -32,6 +32,7 @@ public class DebugCommands {
|
||||
DebugRendererCommand.register();
|
||||
DebugAICommand.register();
|
||||
ScriptCommand.register();
|
||||
DebugQuestCommand.register();
|
||||
|
||||
log.info("Registered debug commands");
|
||||
}
|
||||
|
||||
@ -0,0 +1,115 @@
|
||||
package ru.dragonestia.msb3.api.command;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.minestom.server.MinecraftServer;
|
||||
import net.minestom.server.command.CommandSender;
|
||||
import net.minestom.server.command.builder.Command;
|
||||
import net.minestom.server.command.builder.CommandContext;
|
||||
import net.minestom.server.command.builder.arguments.ArgumentString;
|
||||
import net.minestom.server.command.builder.arguments.ArgumentType;
|
||||
import net.minestom.server.entity.Player;
|
||||
import ru.dragonestia.msb3.api.player.PlayerContext;
|
||||
import ru.dragonestia.msb3.api.player.defaults.PlayerQuestContext;
|
||||
|
||||
public class DebugQuestCommand extends Command {
|
||||
|
||||
private final ArgumentString argQuestId = new ArgumentString("Quest ID");
|
||||
|
||||
public DebugQuestCommand() {
|
||||
super("debug_quest");
|
||||
|
||||
setDefaultExecutor(this::defaultHandler);
|
||||
addSyntax(this::listAllReceivedQuests, ArgumentType.Literal("list"));
|
||||
addSyntax(this::listAllCompletedQuests, ArgumentType.Literal("completed"));
|
||||
addSyntax(this::viewQuestDetails, ArgumentType.Literal("details"), argQuestId);
|
||||
addSyntax(this::completeQuest, ArgumentType.Literal("complete"), argQuestId);
|
||||
addSyntax(this::cancelQuest, ArgumentType.Literal("cancel"), argQuestId);
|
||||
addSyntax(this::failQuest, ArgumentType.Literal("fail"), argQuestId);
|
||||
}
|
||||
|
||||
public static void register() {
|
||||
MinecraftServer.getCommandManager().register(new DebugQuestCommand());
|
||||
}
|
||||
|
||||
private void defaultHandler(CommandSender sender, CommandContext ctx) {
|
||||
var player = (Player) sender;
|
||||
|
||||
player.sendMessage("Debug Quest commands:");
|
||||
player.sendMessage("/debug_quest list - List all received quests");
|
||||
player.sendMessage("/debug_quest completed - List all completed, cancelled and failed quests");
|
||||
player.sendMessage("/debug_quest details <questId> - View quest details");
|
||||
player.sendMessage("/debug_quest complete <questId> - Force complete quest");
|
||||
player.sendMessage("/debug_quest cancel <questId> - Force cancel quest");
|
||||
player.sendMessage("/debug_quest fail <questId> - Force fail quest");
|
||||
}
|
||||
|
||||
private void listAllReceivedQuests(CommandSender sender, CommandContext ctx) {
|
||||
var player = (Player) sender;
|
||||
var service = PlayerContext.of(player, PlayerQuestContext.class);
|
||||
|
||||
var output = Component.text()
|
||||
.append(Component.text("Received quests(%s): %s"));
|
||||
|
||||
// TODO
|
||||
|
||||
player.sendMessage(output.build());
|
||||
}
|
||||
|
||||
private void listAllCompletedQuests(CommandSender sender, CommandContext ctx) {
|
||||
var player = (Player) sender;
|
||||
var service = PlayerContext.of(player, PlayerQuestContext.class);
|
||||
|
||||
var outputCompleted = Component.text()
|
||||
.append(Component.text("Completed quests(%s): %s"));
|
||||
|
||||
// TODO
|
||||
|
||||
player.sendMessage(outputCompleted.build());
|
||||
|
||||
var outputCancelled = Component.text()
|
||||
.append(Component.text("Cancelled quests(%s): %s"));
|
||||
|
||||
// TODO
|
||||
|
||||
player.sendMessage(outputCancelled.build());
|
||||
|
||||
var outputFailed = Component.text()
|
||||
.append(Component.text("Failed quests(%s): %s"));
|
||||
|
||||
// TODO
|
||||
|
||||
player.sendMessage(outputFailed.build());
|
||||
}
|
||||
|
||||
private void viewQuestDetails(CommandSender sender, CommandContext ctx) {
|
||||
var player = (Player) sender;
|
||||
var service = PlayerContext.of(player, PlayerQuestContext.class);
|
||||
var questId = ctx.get(argQuestId);
|
||||
|
||||
// TODO
|
||||
}
|
||||
|
||||
private void completeQuest(CommandSender sender, CommandContext ctx) {
|
||||
var player = (Player) sender;
|
||||
var service = PlayerContext.of(player, PlayerQuestContext.class);
|
||||
var questId = ctx.get(argQuestId);
|
||||
|
||||
// TODO
|
||||
}
|
||||
|
||||
private void cancelQuest(CommandSender sender, CommandContext ctx) {
|
||||
var player = (Player) sender;
|
||||
var service = PlayerContext.of(player, PlayerQuestContext.class);
|
||||
var questId = ctx.get(argQuestId);
|
||||
|
||||
// TODO
|
||||
}
|
||||
|
||||
private void failQuest(CommandSender sender, CommandContext ctx) {
|
||||
var player = (Player) sender;
|
||||
var service = PlayerContext.of(player, PlayerQuestContext.class);
|
||||
var questId = ctx.get(argQuestId);
|
||||
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,115 @@
|
||||
package ru.dragonestia.msb3.api.command;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.minestom.server.command.CommandSender;
|
||||
import net.minestom.server.command.builder.Command;
|
||||
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.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 {
|
||||
|
||||
private EntityAI entity;
|
||||
|
||||
public PuppeteerSubcommand() {
|
||||
super("puppeteer");
|
||||
|
||||
setDefaultExecutor(this::defaultExecutor);
|
||||
addSyntax(this::createEntity, ArgumentType.Literal("create"));
|
||||
addSyntax(this::removeEntity, ArgumentType.Literal("remove"));
|
||||
addSyntax(this::comeHere, ArgumentType.Literal("come_here"));
|
||||
addSyntax(this::justComeHere, ArgumentType.Literal("just_come_here"));
|
||||
addSyntax(this::teleport, ArgumentType.Literal("teleport"));
|
||||
addSyntax(this::jump, ArgumentType.Literal("jump"));
|
||||
}
|
||||
|
||||
private void defaultExecutor(CommandSender sender, CommandContext ctx) {
|
||||
var player = (Player) sender;
|
||||
|
||||
player.sendMessage("Puppeteer commands:");
|
||||
}
|
||||
|
||||
private void createEntity(CommandSender sender, CommandContext ctx) {
|
||||
if (entity != null) {
|
||||
sender.sendMessage(Component.text("Entity is already created!", NamedTextColor.RED));
|
||||
return;
|
||||
}
|
||||
|
||||
var player = (Player) sender;
|
||||
|
||||
var e = new EntityAI(EntityType.SKELETON);
|
||||
e.setCustomName(Component.text("Puppet"));
|
||||
e.setInstance(player.getInstance(), player.getPosition());
|
||||
entity = e;
|
||||
|
||||
sender.sendMessage(Component.text("Puppeteer created!", NamedTextColor.YELLOW));
|
||||
}
|
||||
|
||||
private void removeEntity(CommandSender sender, CommandContext ctx) {
|
||||
if (isNotCreated(sender)) return;
|
||||
|
||||
entity.remove();
|
||||
entity = null;
|
||||
sender.sendMessage(Component.text("Entity removed!"));
|
||||
}
|
||||
|
||||
private void comeHere(CommandSender sender, CommandContext ctx) {
|
||||
if (isNotCreated(sender)) return;
|
||||
|
||||
var player = (Player) sender;
|
||||
|
||||
sender.sendMessage(Component.text("Set entity path target."));
|
||||
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) {
|
||||
if (isNotCreated(sender)) return;
|
||||
|
||||
var player = (Player) sender;
|
||||
sender.sendMessage(Component.text("Set entity path target without PathFinder."));
|
||||
entity.getAi().setMovementTarget(MovementTarget.justMoveTo(entity, player.getPosition(), result -> {
|
||||
DebugMessage.broadcast("Follower target result: " + result);
|
||||
}));
|
||||
}
|
||||
|
||||
private void teleport(CommandSender sender, CommandContext ctx) {
|
||||
if (isNotCreated(sender)) return;
|
||||
|
||||
var player = (Player) sender;
|
||||
entity.teleport(player.getPosition()).thenRun(() -> {
|
||||
sender.sendMessage(Component.text("Teleported.", NamedTextColor.YELLOW));
|
||||
});
|
||||
}
|
||||
|
||||
private void jump(CommandSender sender, CommandContext ctx) {
|
||||
if (isNotCreated(sender)) return;
|
||||
|
||||
if (entity.getAi().getMovementFollower() instanceof GroundMovementFollower follower) {
|
||||
follower.jump();
|
||||
sender.sendMessage(Component.text("Jumped.", NamedTextColor.YELLOW));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isNotCreated(CommandSender sender) {
|
||||
boolean failed = false;
|
||||
if (entity == null) {
|
||||
failed = true;
|
||||
} else if (entity.isDead() || entity.isRemoved()) {
|
||||
entity.remove();
|
||||
entity = null;
|
||||
}
|
||||
|
||||
if (failed) {
|
||||
sender.sendMessage(Component.text("Entity is not created!", NamedTextColor.RED));
|
||||
}
|
||||
return failed;
|
||||
}
|
||||
}
|
||||
107
api/src/main/java/ru/dragonestia/msb3/api/entity/EntityAI.java
Normal file
107
api/src/main/java/ru/dragonestia/msb3/api/entity/EntityAI.java
Normal file
@ -0,0 +1,107 @@
|
||||
package ru.dragonestia.msb3.api.entity;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import net.minestom.server.coordinate.Pos;
|
||||
import net.minestom.server.entity.Entity;
|
||||
import net.minestom.server.entity.EntityType;
|
||||
import net.minestom.server.entity.LivingEntity;
|
||||
import net.minestom.server.entity.attribute.Attribute;
|
||||
import net.minestom.server.event.EventDispatcher;
|
||||
import net.minestom.server.event.entity.EntityAttackEvent;
|
||||
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;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@Getter
|
||||
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) {
|
||||
this(entityType, UUID.randomUUID());
|
||||
}
|
||||
|
||||
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) {
|
||||
if (ai.getMovementTarget() != null) ai.getMovementTarget().cancel();
|
||||
return super.setInstance(instance, spawnPosition).thenRun(() -> {
|
||||
var action = ai.getActor().getCurrentAction();
|
||||
if (action != null) {
|
||||
action.start(ai.getActor(), this);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void kill() {
|
||||
super.kill();
|
||||
|
||||
if (removalAnimationDelay > 0) scheduleRemove(Duration.of(removalAnimationDelay, TimeUnit.MILLISECOND));
|
||||
else remove();
|
||||
}
|
||||
|
||||
public void attack(Entity target, boolean swingHand) {
|
||||
if (swingHand) swingMainHand();
|
||||
EntityAttackEvent attackEvent = new EntityAttackEvent(this, Objects.requireNonNull(target));
|
||||
EventDispatcher.call(attackEvent);
|
||||
}
|
||||
|
||||
public void attack(Entity target) {
|
||||
attack(target, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void remove(boolean permanent) {
|
||||
super.remove(permanent);
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,6 @@ package ru.dragonestia.msb3.api.entity;
|
||||
import lombok.Getter;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import net.minestom.server.MinecraftServer;
|
||||
import net.minestom.server.coordinate.Point;
|
||||
import net.minestom.server.coordinate.Pos;
|
||||
@ -27,7 +26,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
public class Human extends EntityCreature {
|
||||
public class Human extends EntityAI {
|
||||
|
||||
private static final AtomicInteger freeId = new AtomicInteger(0);
|
||||
private static final Map<String, Team> npcTeams = new ConcurrentHashMap<>();
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
package ru.dragonestia.msb3.api.player.defaults;
|
||||
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.minestom.server.entity.EntityCreature;
|
||||
import net.minestom.server.network.packet.server.play.ParticlePacket;
|
||||
import net.minestom.server.particle.Particle;
|
||||
import net.minestom.server.timer.Task;
|
||||
import ru.dragonestia.msb3.api.entity.EntityAI;
|
||||
import ru.dragonestia.msb3.api.player.MsbPlayer;
|
||||
import ru.dragonestia.msb3.api.player.PlayerContext;
|
||||
import ru.dragonestia.msb3.api.scheduler.Scheduler;
|
||||
@ -37,18 +37,20 @@ 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 EntityCreature creature) {
|
||||
var nodes = creature.getNavigator().getNodes();
|
||||
if (nodes == null) continue;
|
||||
for (var point: nodes) {
|
||||
if (entity instanceof EntityAI creature) {
|
||||
var destinationData = creature.getAi().getDestinationData();
|
||||
if (destinationData == null) continue;
|
||||
for (var point: destinationData.path().getNodes()) {
|
||||
var packet = new ParticlePacket(PARTICLE_AI_PATH, point.x(), point.y() + 0.5, point.z(), 0, 0, 0, 0, 1);
|
||||
entity.sendPacketToViewers(packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, Duration.ofMillis(500));
|
||||
*/
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
package ru.dragonestia.msb3.api.player.defaults;
|
||||
|
||||
import ru.dragonestia.msb3.api.player.MsbPlayer;
|
||||
import ru.dragonestia.msb3.api.player.PlayerContext;
|
||||
|
||||
public class PlayerQuestContext extends PlayerContext {
|
||||
|
||||
public PlayerQuestContext(MsbPlayer player) {
|
||||
super(player);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
package ru.dragonestia.msb3.api.script.defaults;
|
||||
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.minestom.server.coordinate.Vec;
|
||||
import net.minestom.server.entity.EntityType;
|
||||
import ru.dragonestia.msb3.api.ai.action.PatrolAction;
|
||||
import ru.dragonestia.msb3.api.entity.EntityAI;
|
||||
import ru.dragonestia.msb3.api.player.MsbPlayer;
|
||||
import ru.dragonestia.msb3.api.script.InstantScript;
|
||||
import ru.dragonestia.msb3.api.util.Params;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class SpawnTestActorScript extends InstantScript {
|
||||
|
||||
public SpawnTestActorScript() {
|
||||
super("test_actor");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(MsbPlayer player, Params params) {
|
||||
var instance = player.getInstance();
|
||||
var spawnPos = player.getPosition();
|
||||
|
||||
var entity = new EntityAI(EntityType.SKELETON);
|
||||
entity.setCustomName(Component.text("Test actor"));
|
||||
entity.setCustomNameVisible(true);
|
||||
|
||||
var action = new PatrolAction();
|
||||
action.setPath(List.of(
|
||||
new Vec(10.22, 46.00, -6.13),
|
||||
new Vec(12.99, 46.00, -22.02),
|
||||
new Vec(11.82, 46.00, -39.27),
|
||||
new Vec(47.15, 47.00, -39.14)
|
||||
));
|
||||
entity.getAi().getActor().setCurrentAction(action);
|
||||
|
||||
entity.setInstance(instance, spawnPos);
|
||||
}
|
||||
}
|
||||
@ -1,2 +1,2 @@
|
||||
MSB3_MAVEN_REPOSITORY=https://git.dragonestia.ru/api/packages/Dragonestia/maven
|
||||
MSB3_VERSION=3.0.2
|
||||
MSB3_VERSION=3.0.2_ai_r6
|
||||
Loading…
x
Reference in New Issue
Block a user