From b90c646f1fa54ab8902ad6f661724974aca85c63 Mon Sep 17 00:00:00 2001 From: ScarletRedMan Date: Wed, 30 Apr 2025 14:15:12 +0700 Subject: [PATCH 01/16] feat: implemented debug broadcast messages --- .../msb3/api/debug/DebugMessage.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/api/src/main/java/ru/dragonestia/msb3/api/debug/DebugMessage.java b/api/src/main/java/ru/dragonestia/msb3/api/debug/DebugMessage.java index d728a52..6bb560a 100644 --- a/api/src/main/java/ru/dragonestia/msb3/api/debug/DebugMessage.java +++ b/api/src/main/java/ru/dragonestia/msb3/api/debug/DebugMessage.java @@ -5,6 +5,7 @@ import net.kyori.adventure.sound.Sound; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.TextColor; import net.kyori.adventure.text.format.TextDecoration; +import net.minestom.server.MinecraftServer; import net.minestom.server.entity.Player; import net.minestom.server.sound.SoundEvent; @@ -32,4 +33,26 @@ public class DebugMessage { .append(Component.text(message, TextColor.color(0xFF6A6B))) .build()); } + + public void broadcast(String message) { + if (!Debug.isEnabled()) return; + + for (var player: MinecraftServer.getConnectionManager().getOnlinePlayers()) { + player.sendMessage(Component.text() + .append(Component.text("[DEBUG] ", TextColor.color(0xFFC909), TextDecoration.BOLD)) + .append(Component.text(message, TextColor.color(0xFFD297))) + .build()); + } + } + + public void broadcastError(String message) { + if (!Debug.isEnabled()) return; + for (var player: MinecraftServer.getConnectionManager().getOnlinePlayers()) { + player.sendMessage(Component.text() + .append(Component.text("[DEBUG] ", TextColor.color(0xFFC909), TextDecoration.BOLD)) + .append(Component.text("Error: ", TextColor.color(0xFF3F3F))) + .append(Component.text(message, TextColor.color(0xFF6A6B))) + .build()); + } + } } -- 2.47.2 From f25df4ad9b179c0426b867381e9b881503cc22f6 Mon Sep 17 00:00:00 2001 From: ScarletRedMan Date: Wed, 30 Apr 2025 18:18:43 +0700 Subject: [PATCH 02/16] feat: implemented ChunkPreloader --- .../msb3/api/util/ChunkPreloader.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 api/src/main/java/ru/dragonestia/msb3/api/util/ChunkPreloader.java diff --git a/api/src/main/java/ru/dragonestia/msb3/api/util/ChunkPreloader.java b/api/src/main/java/ru/dragonestia/msb3/api/util/ChunkPreloader.java new file mode 100644 index 0000000..4e85bb3 --- /dev/null +++ b/api/src/main/java/ru/dragonestia/msb3/api/util/ChunkPreloader.java @@ -0,0 +1,21 @@ +package ru.dragonestia.msb3.api.util; + +import lombok.experimental.UtilityClass; +import net.minestom.server.instance.InstanceContainer; + +@UtilityClass +public class ChunkPreloader { + + public void preload(InstanceContainer instance, int startChunkX, int startChunkZ, int endChunkX, int endChunkZ) { + record Point(int x, int z) {} + + var min = new Point(Math.min(startChunkX, endChunkX), Math.min(startChunkZ, endChunkZ)); + var max = new Point(Math.max(startChunkX, endChunkX), Math.max(startChunkZ, endChunkZ)); + + for (int chunkX = min.x; chunkX <= max.x; chunkX++) { + for (int chunkZ = min.z; chunkZ <= max.z; chunkZ++) { + instance.loadChunk(chunkX, chunkZ).join(); + } + } + } +} -- 2.47.2 From 770fe6c49684bd4a336e87c948e0e17a5b7d891e Mon Sep 17 00:00:00 2001 From: ScarletRedMan Date: Wed, 30 Apr 2025 18:18:59 +0700 Subject: [PATCH 03/16] feat: attempt to reimplement entity AI --- .../java/ru/dragonestia/msb3/api/ai/AI.java | 174 ++++++++++++++++++ .../ru/dragonestia/msb3/api/ai/Actor.java | 81 ++++++++ .../msb3/api/ai/action/Action.java | 43 +++++ .../action/LookClosePlayersAction.java} | 51 +++-- .../msb3/api/ai/action/PatrolAction.java | 105 +++++++++++ .../api/ai/navigator/DestinationPoint.java | 7 + .../msb3/api/ai/navigator/Path.java | 60 ++++++ .../msb3/api/ai/navigator/PathGenerator.java | 134 ++++++++++++++ .../msb3/api/ai/navigator/PathNode.java | 125 +++++++++++++ .../msb3/api/ai/navigator/PathState.java | 50 +++++ .../follower/GroundNodeFollower.java | 66 +++++++ .../ai/navigator/follower/NodeFollower.java | 15 ++ .../navigator/node/GroundNodeGenerator.java | 128 +++++++++++++ .../api/ai/navigator/node/NodeGenerator.java | 46 +++++ .../msb3/api/boot/DebugAiBootstrap.java | 50 +++++ .../dragonestia/msb3/api/entity/EntityAI.java | 78 ++++++++ .../ru/dragonestia/msb3/api/entity/Human.java | 3 +- .../player/defaults/DebugParamsContext.java | 10 +- .../script/defaults/SpawnTestActorScript.java | 40 ++++ 19 files changed, 1233 insertions(+), 33 deletions(-) create mode 100644 api/src/main/java/ru/dragonestia/msb3/api/ai/AI.java create mode 100644 api/src/main/java/ru/dragonestia/msb3/api/ai/Actor.java create mode 100644 api/src/main/java/ru/dragonestia/msb3/api/ai/action/Action.java rename api/src/main/java/ru/dragonestia/msb3/api/{entity/goal/LookCloseGoal.java => ai/action/LookClosePlayersAction.java} (55%) create mode 100644 api/src/main/java/ru/dragonestia/msb3/api/ai/action/PatrolAction.java create mode 100644 api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/DestinationPoint.java create mode 100644 api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/Path.java create mode 100644 api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/PathGenerator.java create mode 100644 api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/PathNode.java create mode 100644 api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/PathState.java create mode 100644 api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/follower/GroundNodeFollower.java create mode 100644 api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/follower/NodeFollower.java create mode 100644 api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/node/GroundNodeGenerator.java create mode 100644 api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/node/NodeGenerator.java create mode 100644 api/src/main/java/ru/dragonestia/msb3/api/boot/DebugAiBootstrap.java create mode 100644 api/src/main/java/ru/dragonestia/msb3/api/entity/EntityAI.java create mode 100644 api/src/main/java/ru/dragonestia/msb3/api/script/defaults/SpawnTestActorScript.java diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/AI.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/AI.java new file mode 100644 index 0000000..f64927c --- /dev/null +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/AI.java @@ -0,0 +1,174 @@ +package ru.dragonestia.msb3.api.ai; + +import lombok.Getter; +import lombok.Setter; +import net.minestom.server.coordinate.Point; +import net.minestom.server.coordinate.Pos; +import net.minestom.server.coordinate.Vec; +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.navigator.node.GroundNodeGenerator; +import ru.dragonestia.msb3.api.ai.navigator.node.NodeGenerator; +import ru.dragonestia.msb3.api.entity.EntityAI; +import ru.dragonestia.msb3.api.util.UncheckedRunnable; + +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +@Getter +public class AI { + + private final EntityAI entity; + private DestinationPoint destinationData; + @Setter private NodeGenerator nodeGenerator = new GroundNodeGenerator(); + @Setter private NodeFollower nodeFollower; + @Getter private final Actor actor; + + public AI(EntityAI entity) { + this.entity = Objects.requireNonNull(entity, "AI is null"); + nodeFollower = new GroundNodeFollower(entity); + actor = new Actor(entity); + } + + public PathState getCurrentPathState() { + synchronized (this) { + if (destinationData == null) return PathState.NONE; + return destinationData.path().getState(); + } + } + + public CompletableFuture setPathTo(Point point) { + var bb = entity.getBoundingBox(); + var centerToCorner = Math.sqrt(bb.width() * bb.width() + bb.depth() * bb.depth()) / 2; + return setPathTo(point, centerToCorner); + } + + public CompletableFuture setPathTo(Point point, double arrivalDistance) { + return setPathTo(point, arrivalDistance, 50, 20); + } + + public CompletableFuture setPathTo(Point point, double arrivalDistance, double maxDistance, double pathVariance) { + Objects.requireNonNull(point, "Point is null"); + var instance = Objects.requireNonNull(entity.getInstance(), "Entity is not spawned"); + if (!instance.getWorldBorder().inBounds(point)) + throw new IllegalStateException("Destination point is not in world bounds"); + if (!instance.isChunkLoaded(point)) + throw new IllegalStateException("Chunk is not loaded for destination point"); + + + synchronized (this) { + // Завершаем активное следование к точке + if (destinationData != null) { + var data = destinationData; + destinationData = null; + + UncheckedRunnable.runIgnoreException(() -> data.future().complete(PathState.TERMINATED)); + } + + // Игрок находится рядом с точкой, куда нужно прийти + if (entity.getDistanceSquared(point) <= arrivalDistance * arrivalDistance) { + return CompletableFuture.completedFuture(PathState.COMPLETED); + } + + // Проверка находится ли точка слишком далеко + if (entity.getDistanceSquared(point) > Math.pow(maxDistance - arrivalDistance / 2, 2)) { + return CompletableFuture.completedFuture(PathState.TOO_FAR); + } + + // Начинаем просчет пути до новой точки + var path = PathGenerator.generate(instance, + entity.getPosition(), + point, + arrivalDistance, + maxDistance, + pathVariance, + entity.getBoundingBox(), + entity.isOnGround(), + nodeGenerator); + + // Если на время просчета пути уже известен результат действия + if (path.getState().isCompleted()) { + return CompletableFuture.completedFuture(path.getState()); + } + + return (destinationData = new DestinationPoint(Vec.fromPoint(point), arrivalDistance, path, new CompletableFuture<>())) + .future().thenApply(result -> { + resetDestinationData(); + return result; + }); + } + } + + public void tick(long delta) { + synchronized (this) { + if (entity.isDead()) return; + + var action = actor.getCurrentAction(); + if (action != null) action.tick(actor, entity, delta); + + if (destinationData != null) tickNavigator(); + } + } + + 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(path); + 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: хождение по неполноценным блокам - огромная проблема. Entity попросту застревает в них + + if (nodeFollower.isAtPoint(currentTarget)) path.next(); + else if (path.getCurrentType() == PathNode.Type.JUMP) nodeFollower.jump(currentTarget, nextTarget); + checkFinishedPath(path, destinationData.future()); + } + + private void recalculatePath(Path originalPath) { + var newPath = PathGenerator.generate(entity.getInstance(), + entity.getPosition(), + destinationData.position(), + destinationData.arrivalDistance(), originalPath.getMaxDistance(), + originalPath.getPathVariance(), entity.getBoundingBox(), entity.isOnGround(), nodeGenerator); + + destinationData = new DestinationPoint(destinationData.position(), destinationData.arrivalDistance(), newPath, destinationData.future()); + checkFinishedPath(newPath, destinationData.future()); + } + + private void checkFinishedPath(Path path, CompletableFuture future) { + var state = path.getState(); + if (state.isCompleted()) { + UncheckedRunnable.runIgnoreException(() -> future.complete(state)); + } + } + + public void resetDestinationData() { + synchronized (this) { + if (destinationData == null) return; + + var future = destinationData.future(); + destinationData = null; + if (!future.isDone()) future.complete(PathState.TERMINATED); + } + } + + public boolean isCompleted() { + return getCurrentPathState().isCompleted(); + } +} diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/Actor.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/Actor.java new file mode 100644 index 0000000..db09758 --- /dev/null +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/Actor.java @@ -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 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 getPlannedActions() { + return plannedActions; + } +} diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/action/Action.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/action/Action.java new file mode 100644 index 0000000..eb1a429 --- /dev/null +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/action/Action.java @@ -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); +} diff --git a/api/src/main/java/ru/dragonestia/msb3/api/entity/goal/LookCloseGoal.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/action/LookClosePlayersAction.java similarity index 55% rename from api/src/main/java/ru/dragonestia/msb3/api/entity/goal/LookCloseGoal.java rename to api/src/main/java/ru/dragonestia/msb3/api/ai/action/LookClosePlayersAction.java index de6f120..dec717a 100644 --- a/api/src/main/java/ru/dragonestia/msb3/api/entity/goal/LookCloseGoal.java +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/action/LookClosePlayersAction.java @@ -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; diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/action/PatrolAction.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/action/PatrolAction.java new file mode 100644 index 0000000..e48eca8 --- /dev/null +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/action/PatrolAction.java @@ -0,0 +1,105 @@ +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.navigator.PathState; +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 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) { + entity.getAi().resetDestinationData(); + this.entity = null; + } + + @Override + public boolean canStart(Actor actor, EntityAI entity) { + return findNearestPos(entity) != null; + } + + public void setPath(List 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 navigator = 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; + } + + walkToNextPos(entity); + }); + } +} diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/DestinationPoint.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/DestinationPoint.java new file mode 100644 index 0000000..0cb22d2 --- /dev/null +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/DestinationPoint.java @@ -0,0 +1,7 @@ +package ru.dragonestia.msb3.api.ai.navigator; + +import net.minestom.server.coordinate.Vec; + +import java.util.concurrent.CompletableFuture; + +public record DestinationPoint(Vec position, double arrivalDistance, Path path, CompletableFuture future) {} diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/Path.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/Path.java new file mode 100644 index 0000000..bba183b --- /dev/null +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/Path.java @@ -0,0 +1,60 @@ +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 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 nodes = new ArrayList<>(); + private final AtomicReference state = new AtomicReference<>(PathState.FOLLOWING); + private int index = 0; + + public Path(double maxDistance, double pathVariance) { + this.maxDistance = maxDistance; + this.pathVariance = pathVariance; + } + + public PathState getState() { + return state.get(); + } + + public void setState(PathState 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()); + } +} diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/PathGenerator.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/PathGenerator.java new file mode 100644 index 0000000..83a8410 --- /dev/null +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/PathGenerator.java @@ -0,0 +1,134 @@ +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.navigator.node.NodeGenerator; + +import java.util.*; + +@UtilityClass +public class PathGenerator { + + private final Comparator 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 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(maxSize); + + while (!open.isEmpty() && closed.size64() < maxSize) { + if (path.getState() == PathState.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 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(PathState.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(PathState.DEADLOCKED); + path.getNodes().clear(); + return; + } + + if (path.getNodes().isEmpty()) { + path.setState(PathState.DEADLOCKED); + return; + } + + var lastNode = path.getNodes().getLast(); + if (getDistanceSquared(lastNode.x(), lastNode.y(), lastNode.z(), target) > (closeDistance * closeDistance)) { + path.setState(PathState.DEADLOCKED); + return; + } + + PathNode pEnd = new PathNode(target, 0, 0, PathNode.Type.WALK, null); + path.getNodes().add(pEnd); + path.setState(PathState.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; + } +} diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/PathNode.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/PathNode.java new file mode 100644 index 0000000..239ec09 --- /dev/null +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/PathNode.java @@ -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 + } +} diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/PathState.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/PathState.java new file mode 100644 index 0000000..11b5038 --- /dev/null +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/PathState.java @@ -0,0 +1,50 @@ +package ru.dragonestia.msb3.api.ai.navigator; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum PathState { + + /** + * Путь не задан. + * @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; +} diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/follower/GroundNodeFollower.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/follower/GroundNodeFollower.java new file mode 100644 index 0000000..9231202 --- /dev/null +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/follower/GroundNodeFollower.java @@ -0,0 +1,66 @@ +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, 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(4f); + } + } + + @Override + public boolean isAtPoint(Point point) { + return entity.getPosition().distanceSquared(point) < 0.5 * 0.5; + } + + public void jump(float height) { + entity.setVelocity(new Vec(0, height * 2.5f, 0)); + } + + @Override + public double movementSpeed() { + return entity.getAttribute(Attribute.MOVEMENT_SPEED).getValue(); + } +} diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/follower/NodeFollower.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/follower/NodeFollower.java new file mode 100644 index 0000000..84a4dcd --- /dev/null +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/follower/NodeFollower.java @@ -0,0 +1,15 @@ +package ru.dragonestia.msb3.api.ai.navigator.follower; + +import net.minestom.server.coordinate.Point; +import org.jetbrains.annotations.Nullable; + +public interface NodeFollower { + + void moveTowards(Point target, double speed, Point lookAt); + + void jump(@Nullable Point point, @Nullable Point target); + + boolean isAtPoint(Point point); + + double movementSpeed(); +} diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/node/GroundNodeGenerator.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/node/GroundNodeGenerator.java new file mode 100644 index 0000000..ffdebb9 --- /dev/null +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/node/GroundNodeGenerator.java @@ -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 getWalkable(Block.Getter getter, Set visited, PathNode current, Point goal, BoundingBox boundingBox) { + Collection 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 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 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(); + } +} diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/node/NodeGenerator.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/node/NodeGenerator.java new file mode 100644 index 0000000..0269412 --- /dev/null +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/node/NodeGenerator.java @@ -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 getWalkable(Block.Getter getter, Set 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); + } +} diff --git a/api/src/main/java/ru/dragonestia/msb3/api/boot/DebugAiBootstrap.java b/api/src/main/java/ru/dragonestia/msb3/api/boot/DebugAiBootstrap.java new file mode 100644 index 0000000..01aba31 --- /dev/null +++ b/api/src/main/java/ru/dragonestia/msb3/api/boot/DebugAiBootstrap.java @@ -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", "msb3 server - AI test"); + + // Скачать мир можно здесь: 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); + } +} diff --git a/api/src/main/java/ru/dragonestia/msb3/api/entity/EntityAI.java b/api/src/main/java/ru/dragonestia/msb3/api/entity/EntityAI.java new file mode 100644 index 0000000..4b69a73 --- /dev/null +++ b/api/src/main/java/ru/dragonestia/msb3/api/entity/EntityAI.java @@ -0,0 +1,78 @@ +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 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 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); + getAttribute(Attribute.MOVEMENT_SPEED).setBaseValue(0.2); + heal(); + } + + @Override + public void update(long time) { + ai.tick(time); + super.update(time); + } + + @Override + public CompletableFuture setInstance(@NotNull Instance instance, @NotNull Pos spawnPosition) { + ai.resetDestinationData(); + 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); + } +} diff --git a/api/src/main/java/ru/dragonestia/msb3/api/entity/Human.java b/api/src/main/java/ru/dragonestia/msb3/api/entity/Human.java index c0e9286..a93e0db 100644 --- a/api/src/main/java/ru/dragonestia/msb3/api/entity/Human.java +++ b/api/src/main/java/ru/dragonestia/msb3/api/entity/Human.java @@ -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 npcTeams = new ConcurrentHashMap<>(); diff --git a/api/src/main/java/ru/dragonestia/msb3/api/player/defaults/DebugParamsContext.java b/api/src/main/java/ru/dragonestia/msb3/api/player/defaults/DebugParamsContext.java index b5cb773..270939f 100644 --- a/api/src/main/java/ru/dragonestia/msb3/api/player/defaults/DebugParamsContext.java +++ b/api/src/main/java/ru/dragonestia/msb3/api/player/defaults/DebugParamsContext.java @@ -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; @@ -39,10 +39,10 @@ public class DebugParamsContext extends PlayerContext { 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); } diff --git a/api/src/main/java/ru/dragonestia/msb3/api/script/defaults/SpawnTestActorScript.java b/api/src/main/java/ru/dragonestia/msb3/api/script/defaults/SpawnTestActorScript.java new file mode 100644 index 0000000..dfd2b5a --- /dev/null +++ b/api/src/main/java/ru/dragonestia/msb3/api/script/defaults/SpawnTestActorScript.java @@ -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); + } +} -- 2.47.2 From 44166ff462193be42d692dcb6129dd2e188c730b Mon Sep 17 00:00:00 2001 From: ScarletRedMan Date: Wed, 30 Apr 2025 18:22:44 +0700 Subject: [PATCH 04/16] chore: version=3.0.3-r1 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index da97924..b1bacb8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ MSB3_MAVEN_REPOSITORY=http://git.dragonestia.ru/api/packages/MSB/maven -MSB3_VERSION=3.0.2 \ No newline at end of file +MSB3_VERSION=3.0.3-r1 \ No newline at end of file -- 2.47.2 From cad9eb6a3a52acbb8d3a7a97bf30831454067085 Mon Sep 17 00:00:00 2001 From: ScarletRedMan Date: Wed, 30 Apr 2025 18:24:11 +0700 Subject: [PATCH 05/16] fixup! chore: version=3.0.2_ai_r1 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index b1bacb8..745b425 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ MSB3_MAVEN_REPOSITORY=http://git.dragonestia.ru/api/packages/MSB/maven -MSB3_VERSION=3.0.3-r1 \ No newline at end of file +MSB3_VERSION=3.0.2_ai_r1 \ No newline at end of file -- 2.47.2 From 4668616e48abed039c3aae02c668b60e1c206bb8 Mon Sep 17 00:00:00 2001 From: ScarletRedMan Date: Fri, 2 May 2025 13:58:24 +0700 Subject: [PATCH 06/16] fix: fixed action handlers --- .../msb3/api/dialog/action/DialogDialogActionHandler.java | 2 +- .../msb3/api/dialog/action/ScriptDialogActionHandler.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/api/src/main/java/ru/dragonestia/msb3/api/dialog/action/DialogDialogActionHandler.java b/api/src/main/java/ru/dragonestia/msb3/api/dialog/action/DialogDialogActionHandler.java index e43b828..37a5139 100644 --- a/api/src/main/java/ru/dragonestia/msb3/api/dialog/action/DialogDialogActionHandler.java +++ b/api/src/main/java/ru/dragonestia/msb3/api/dialog/action/DialogDialogActionHandler.java @@ -14,7 +14,7 @@ public class DialogDialogActionHandler implements DialogActionHandler { public void handle(DialogButtonClick click, Params params) { var player = click.player(); - if (params.contains("dialogId")) { + if (!params.contains("dialogId")) { DebugMessage.sendError(player, "Отсутствует обязательный параметр dialogId для команды dialog"); return; } diff --git a/api/src/main/java/ru/dragonestia/msb3/api/dialog/action/ScriptDialogActionHandler.java b/api/src/main/java/ru/dragonestia/msb3/api/dialog/action/ScriptDialogActionHandler.java index b93219a..14a1723 100644 --- a/api/src/main/java/ru/dragonestia/msb3/api/dialog/action/ScriptDialogActionHandler.java +++ b/api/src/main/java/ru/dragonestia/msb3/api/dialog/action/ScriptDialogActionHandler.java @@ -14,7 +14,7 @@ public class ScriptDialogActionHandler implements DialogActionHandler { public void handle(DialogButtonClick click, Params params) { var player = click.player(); - if (params.contains("scriptId")) { + if (!params.contains("scriptId")) { DebugMessage.sendError(player, "Отсутствует обязательный параметр scriptId для команды script"); return; } @@ -27,6 +27,7 @@ public class ScriptDialogActionHandler implements DialogActionHandler { return; } + click.renderer().close(false); ScriptService.ofPlayer(player).start(script.get(), params); } } -- 2.47.2 From b1a39ee646b4d08dfac9fd6fe021a6100a719a63 Mon Sep 17 00:00:00 2001 From: ScarletRedMan Date: Fri, 2 May 2025 13:58:31 +0700 Subject: [PATCH 07/16] fix: fixed navigator --- .../msb3/api/player/defaults/NavigatorContext.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/api/src/main/java/ru/dragonestia/msb3/api/player/defaults/NavigatorContext.java b/api/src/main/java/ru/dragonestia/msb3/api/player/defaults/NavigatorContext.java index 0193181..1c6c9e8 100644 --- a/api/src/main/java/ru/dragonestia/msb3/api/player/defaults/NavigatorContext.java +++ b/api/src/main/java/ru/dragonestia/msb3/api/player/defaults/NavigatorContext.java @@ -21,8 +21,8 @@ public class NavigatorContext extends PlayerContext implements Navigator { private final static Component EMPTY = Component.text(" "); private BossBar bossBar; - private Point target; - private String text; + private volatile Point target; + private volatile String text; private MinecraftFont.Text cachedText; private boolean prevTargetEmpty = true; private boolean enabled = true; @@ -71,6 +71,7 @@ public class NavigatorContext extends PlayerContext implements Navigator { prevTargetEmpty = this.target == null; this.target = target; this.text = text; + cachedText = null; update(); } -- 2.47.2 From 7c9495429fc9e63ec4fab06060b61eeb62e4d377 Mon Sep 17 00:00:00 2001 From: ScarletRedMan Date: Fri, 2 May 2025 13:59:11 +0700 Subject: [PATCH 08/16] fix: tried to fix semi-blocks walking. Working but bad --- api/src/main/java/ru/dragonestia/msb3/api/ai/AI.java | 6 ++++++ .../msb3/api/ai/navigator/follower/GroundNodeFollower.java | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/AI.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/AI.java index f64927c..f05cb13 100644 --- a/api/src/main/java/ru/dragonestia/msb3/api/ai/AI.java +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/AI.java @@ -130,7 +130,13 @@ public class AI { if (nextTarget == null) nextTarget = destinationData.position(); boolean nextIsRePath = nextTarget.sameBlock(Pos.ZERO); + var prevPos = entity.getPosition(); nodeFollower.moveTowards(currentTarget, nodeFollower.movementSpeed(), nextIsRePath ? currentTarget : nextTarget); + var delta = entity.getPosition().sub(prevPos); + if (delta.isZero()) { + nodeFollower.jump(currentTarget, nextTarget); + } + // TODO: исправить баг, в данном месте, когда Entity не движется вообще. Сделать хорошую проверку на застревание // TODO: присутствует баг, что если как-то помешать Entity следовать до точки, например поставить перед ним блок, то он перестанет идти // TODO: хождение по неполноценным блокам - огромная проблема. Entity попросту застревает в них diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/follower/GroundNodeFollower.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/follower/GroundNodeFollower.java index 9231202..e20e524 100644 --- a/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/follower/GroundNodeFollower.java +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/follower/GroundNodeFollower.java @@ -52,7 +52,8 @@ public class GroundNodeFollower implements NodeFollower { @Override public boolean isAtPoint(Point point) { - return entity.getPosition().distanceSquared(point) < 0.5 * 0.5; + var d = entity.getPosition().sub(point); + return d.x() * d.x() + d.z() * d.z() < 0.5 * 0.5; } public void jump(float height) { -- 2.47.2 From 6bbd44be85b511d6638c352140f72d12ce44e4a4 Mon Sep 17 00:00:00 2001 From: ScarletRedMan Date: Fri, 2 May 2025 13:59:25 +0700 Subject: [PATCH 09/16] chore: version=3.0.2_ai_r6 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 745b425..27fa4ba 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ MSB3_MAVEN_REPOSITORY=http://git.dragonestia.ru/api/packages/MSB/maven -MSB3_VERSION=3.0.2_ai_r1 \ No newline at end of file +MSB3_VERSION=3.0.2_ai_r6 \ No newline at end of file -- 2.47.2 From 8125d040131e631f6772c75b9dbddf15d2ab95fc Mon Sep 17 00:00:00 2001 From: ScarletRedMan Date: Wed, 7 May 2025 02:36:42 +0700 Subject: [PATCH 10/16] fix: tried fix not full blocks for path follower --- api/src/main/java/ru/dragonestia/msb3/api/ai/AI.java | 8 ++------ .../api/ai/navigator/follower/GroundNodeFollower.java | 9 ++++----- .../java/ru/dragonestia/msb3/api/entity/EntityAI.java | 4 ++++ 3 files changed, 10 insertions(+), 11 deletions(-) diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/AI.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/AI.java index f05cb13..f5ca042 100644 --- a/api/src/main/java/ru/dragonestia/msb3/api/ai/AI.java +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/AI.java @@ -130,16 +130,12 @@ public class AI { if (nextTarget == null) nextTarget = destinationData.position(); boolean nextIsRePath = nextTarget.sameBlock(Pos.ZERO); - var prevPos = entity.getPosition(); nodeFollower.moveTowards(currentTarget, nodeFollower.movementSpeed(), nextIsRePath ? currentTarget : nextTarget); - var delta = entity.getPosition().sub(prevPos); - if (delta.isZero()) { - nodeFollower.jump(currentTarget, nextTarget); - } // TODO: исправить баг, в данном месте, когда Entity не движется вообще. Сделать хорошую проверку на застревание // TODO: присутствует баг, что если как-то помешать Entity следовать до точки, например поставить перед ним блок, то он перестанет идти - // TODO: хождение по неполноценным блокам - огромная проблема. Entity попросту застревает в них + // TODO: Бля, этот поиск пути говнище полное! Он не учитывает ни заборы, ни полублоки, ни ковры и остальные неполноценные блоки. Трава - это вообще пиздец. + // TODO: использовать CollisionUtils для просчета поиска путей if (nodeFollower.isAtPoint(currentTarget)) path.next(); else if (path.getCurrentType() == PathNode.Type.JUMP) nodeFollower.jump(currentTarget, nextTarget); diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/follower/GroundNodeFollower.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/follower/GroundNodeFollower.java index e20e524..5155234 100644 --- a/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/follower/GroundNodeFollower.java +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/follower/GroundNodeFollower.java @@ -37,8 +37,7 @@ public class GroundNodeFollower implements NodeFollower { 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, 0, speedZ)); + 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)); } @@ -46,7 +45,7 @@ public class GroundNodeFollower implements NodeFollower { @Override public void jump(@Nullable Point point, @Nullable Point target) { if (entity.isOnGround()) { - jump(4f); + jump(entity.getJumpHeight()); } } @@ -56,8 +55,8 @@ public class GroundNodeFollower implements NodeFollower { return d.x() * d.x() + d.z() * d.z() < 0.5 * 0.5; } - public void jump(float height) { - entity.setVelocity(new Vec(0, height * 2.5f, 0)); + public void jump(double height) { + entity.setVelocity(new Vec(0, height, 0)); } @Override diff --git a/api/src/main/java/ru/dragonestia/msb3/api/entity/EntityAI.java b/api/src/main/java/ru/dragonestia/msb3/api/entity/EntityAI.java index 4b69a73..7cbd540 100644 --- a/api/src/main/java/ru/dragonestia/msb3/api/entity/EntityAI.java +++ b/api/src/main/java/ru/dragonestia/msb3/api/entity/EntityAI.java @@ -75,4 +75,8 @@ public class EntityAI extends LivingEntity { super.remove(permanent); ai.getActor().setActive(false); } + + public double getJumpHeight() { + return 2.1; + } } -- 2.47.2 From 0f047692b0d10c53b6c9e9c28b552d46faccbcc9 Mon Sep 17 00:00:00 2001 From: ScarletRedMan Date: Thu, 22 May 2025 15:48:25 +0700 Subject: [PATCH 11/16] feat: implemented Puppet subcommand for /debug_ai --- .../msb3/api/command/DebugAICommand.java | 1 + .../msb3/api/command/PuppeteerSubcommand.java | 90 +++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 api/src/main/java/ru/dragonestia/msb3/api/command/PuppeteerSubcommand.java diff --git a/api/src/main/java/ru/dragonestia/msb3/api/command/DebugAICommand.java b/api/src/main/java/ru/dragonestia/msb3/api/command/DebugAICommand.java index 7c2bea1..09ab1aa 100644 --- a/api/src/main/java/ru/dragonestia/msb3/api/command/DebugAICommand.java +++ b/api/src/main/java/ru/dragonestia/msb3/api/command/DebugAICommand.java @@ -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() { diff --git a/api/src/main/java/ru/dragonestia/msb3/api/command/PuppeteerSubcommand.java b/api/src/main/java/ru/dragonestia/msb3/api/command/PuppeteerSubcommand.java new file mode 100644 index 0000000..1da0624 --- /dev/null +++ b/api/src/main/java/ru/dragonestia/msb3/api/command/PuppeteerSubcommand.java @@ -0,0 +1,90 @@ +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.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::teleport, ArgumentType.Literal("teleport")); + } + + 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(); + 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().setPathTo(player.getPosition()).thenAccept(result -> { + player.sendMessage(Component.text("Entity path target result: " + result, NamedTextColor.YELLOW)); + }); + } + + 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 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; + } +} -- 2.47.2 From 0e5a82d02f66db8f7ea9c27e00a0f64f88f1e760 Mon Sep 17 00:00:00 2001 From: ScarletRedMan Date: Thu, 22 May 2025 18:04:25 +0700 Subject: [PATCH 12/16] fix: fixed removing puppet --- .../ru/dragonestia/msb3/api/command/PuppeteerSubcommand.java | 1 + 1 file changed, 1 insertion(+) diff --git a/api/src/main/java/ru/dragonestia/msb3/api/command/PuppeteerSubcommand.java b/api/src/main/java/ru/dragonestia/msb3/api/command/PuppeteerSubcommand.java index 1da0624..9fb186a 100644 --- a/api/src/main/java/ru/dragonestia/msb3/api/command/PuppeteerSubcommand.java +++ b/api/src/main/java/ru/dragonestia/msb3/api/command/PuppeteerSubcommand.java @@ -50,6 +50,7 @@ public class PuppeteerSubcommand extends Command { if (isNotCreated(sender)) return; entity.remove(); + entity = null; sender.sendMessage(Component.text("Entity removed!")); } -- 2.47.2 From c0a6b807423fd6dab2f0d9330ff86d022702ca8e Mon Sep 17 00:00:00 2001 From: ScarletRedMan Date: Thu, 29 May 2025 13:53:38 +0700 Subject: [PATCH 13/16] refactor: started refactoring NodeFollower --- .../java/ru/dragonestia/msb3/api/ai/AI.java | 45 +++++++++- .../ai/movement/GroundMovementFollower.java | 85 +++++++++++++++++++ .../api/ai/movement/MovementFollower.java | 44 ++++++++++ .../msb3/api/command/PuppeteerSubcommand.java | 20 +++++ .../dragonestia/msb3/api/entity/EntityAI.java | 2 +- 5 files changed, 193 insertions(+), 3 deletions(-) create mode 100644 api/src/main/java/ru/dragonestia/msb3/api/ai/movement/GroundMovementFollower.java create mode 100644 api/src/main/java/ru/dragonestia/msb3/api/ai/movement/MovementFollower.java diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/AI.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/AI.java index f5ca042..604b5e9 100644 --- a/api/src/main/java/ru/dragonestia/msb3/api/ai/AI.java +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/AI.java @@ -2,20 +2,26 @@ 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.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 { @@ -23,11 +29,14 @@ public class AI { 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; public AI(EntityAI entity) { this.entity = Objects.requireNonNull(entity, "AI is null"); nodeFollower = new GroundNodeFollower(entity); + movementFollower = new GroundMovementFollower(entity); actor = new Actor(entity); } @@ -108,9 +117,34 @@ public class AI { if (action != null) action.tick(actor, entity, delta); if (destinationData != null) tickNavigator(); + if (destinationPoint != null) tickNewNavigator(); } } + 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(); @@ -123,7 +157,7 @@ public class AI { var nextTarget = path.getNext(); if (currentTarget == null || path.getCurrentType() == PathNode.Type.REPATH || path.getCurrentType() == null) { - recalculatePath(path); + recalculatePath(); return; } @@ -142,7 +176,10 @@ public class AI { checkFinishedPath(path, destinationData.future()); } - private void recalculatePath(Path originalPath) { + public void recalculatePath() { + if (destinationData == null) return; + + var originalPath = destinationData.path(); var newPath = PathGenerator.generate(entity.getInstance(), entity.getPosition(), destinationData.position(), @@ -173,4 +210,8 @@ public class AI { public boolean isCompleted() { return getCurrentPathState().isCompleted(); } + + public double getMovementSpeed() { + return entity.getAttribute(Attribute.MOVEMENT_SPEED).getValue(); + } } diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/movement/GroundMovementFollower.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/movement/GroundMovementFollower.java new file mode 100644 index 0000000..135ada1 --- /dev/null +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/movement/GroundMovementFollower.java @@ -0,0 +1,85 @@ +package ru.dragonestia.msb3.api.ai.movement; + +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)); + } +} diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/movement/MovementFollower.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/movement/MovementFollower.java new file mode 100644 index 0000000..6f0f8ac --- /dev/null +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/movement/MovementFollower.java @@ -0,0 +1,44 @@ +package ru.dragonestia.msb3.api.ai.movement; + +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); + } +} \ No newline at end of file diff --git a/api/src/main/java/ru/dragonestia/msb3/api/command/PuppeteerSubcommand.java b/api/src/main/java/ru/dragonestia/msb3/api/command/PuppeteerSubcommand.java index 9fb186a..00ecba9 100644 --- a/api/src/main/java/ru/dragonestia/msb3/api/command/PuppeteerSubcommand.java +++ b/api/src/main/java/ru/dragonestia/msb3/api/command/PuppeteerSubcommand.java @@ -8,6 +8,7 @@ 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.entity.EntityAI; public class PuppeteerSubcommand extends Command { @@ -21,7 +22,9 @@ public class PuppeteerSubcommand extends Command { 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) { @@ -65,6 +68,14 @@ public class PuppeteerSubcommand extends Command { }); } + 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().setDestinationPoint(player.getPosition()); + } + private void teleport(CommandSender sender, CommandContext ctx) { if (isNotCreated(sender)) return; @@ -74,6 +85,15 @@ public class PuppeteerSubcommand extends Command { }); } + 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) { diff --git a/api/src/main/java/ru/dragonestia/msb3/api/entity/EntityAI.java b/api/src/main/java/ru/dragonestia/msb3/api/entity/EntityAI.java index 7cbd540..9f4953a 100644 --- a/api/src/main/java/ru/dragonestia/msb3/api/entity/EntityAI.java +++ b/api/src/main/java/ru/dragonestia/msb3/api/entity/EntityAI.java @@ -77,6 +77,6 @@ public class EntityAI extends LivingEntity { } public double getJumpHeight() { - return 2.1; + return 1.1; } } -- 2.47.2 From e50ee8a3d67e97a5d3f3f91ec1fce88ab7d0da40 Mon Sep 17 00:00:00 2001 From: ScarletRedMan Date: Mon, 14 Jul 2025 19:21:41 +0700 Subject: [PATCH 14/16] !refactor: reimplemented movement target --- .../java/ru/dragonestia/msb3/api/ai/AI.java | 191 ++---------------- .../msb3/api/ai/ArrivalResultListener.java | 10 + .../PathState.java => FollowerState.java} | 4 +- .../msb3/api/ai/FollowerTarget.java | 7 + .../msb3/api/ai/action/PatrolAction.java | 41 ++-- .../GroundMovementFollower.java | 2 +- .../{ => follower}/MovementFollower.java | 2 +- .../ai/movement/target/MovementTarget.java | 75 +++++++ .../target/WithPathFinderMovementTarget.java | 138 +++++++++++++ .../WithoutPathFinderMovementTarget.java | 45 +++++ .../api/ai/navigator/DestinationPoint.java | 7 - .../msb3/api/ai/navigator/Navigator.java | 8 + .../msb3/api/ai/navigator/NavigatorPath.java | 4 + .../msb3/api/ai/navigator/Path.java | 7 +- .../msb3/api/ai/navigator/PathGenerator.java | 13 +- .../follower/GroundNodeFollower.java | 66 ------ .../ai/navigator/follower/NodeFollower.java | 15 -- .../msb3/api/command/PuppeteerSubcommand.java | 12 +- .../dragonestia/msb3/api/entity/EntityAI.java | 29 ++- .../player/defaults/DebugParamsContext.java | 2 + 20 files changed, 375 insertions(+), 303 deletions(-) create mode 100644 api/src/main/java/ru/dragonestia/msb3/api/ai/ArrivalResultListener.java rename api/src/main/java/ru/dragonestia/msb3/api/ai/{navigator/PathState.java => FollowerState.java} (95%) create mode 100644 api/src/main/java/ru/dragonestia/msb3/api/ai/FollowerTarget.java rename api/src/main/java/ru/dragonestia/msb3/api/ai/movement/{ => follower}/GroundMovementFollower.java (98%) rename api/src/main/java/ru/dragonestia/msb3/api/ai/movement/{ => follower}/MovementFollower.java (96%) create mode 100644 api/src/main/java/ru/dragonestia/msb3/api/ai/movement/target/MovementTarget.java create mode 100644 api/src/main/java/ru/dragonestia/msb3/api/ai/movement/target/WithPathFinderMovementTarget.java create mode 100644 api/src/main/java/ru/dragonestia/msb3/api/ai/movement/target/WithoutPathFinderMovementTarget.java delete mode 100644 api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/DestinationPoint.java create mode 100644 api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/Navigator.java create mode 100644 api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/NavigatorPath.java delete mode 100644 api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/follower/GroundNodeFollower.java delete mode 100644 api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/follower/NodeFollower.java diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/AI.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/AI.java index 604b5e9..6d2236c 100644 --- a/api/src/main/java/ru/dragonestia/msb3/api/ai/AI.java +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/AI.java @@ -3,215 +3,54 @@ package ru.dragonestia.msb3.api.ai; import lombok.Getter; import lombok.Setter; import lombok.extern.log4j.Log4j2; -import net.minestom.server.coordinate.Point; -import net.minestom.server.coordinate.Pos; -import net.minestom.server.coordinate.Vec; -import net.minestom.server.entity.attribute.Attribute; -import ru.dragonestia.msb3.api.ai.movement.GroundMovementFollower; -import ru.dragonestia.msb3.api.ai.movement.MovementFollower; -import ru.dragonestia.msb3.api.ai.navigator.*; -import ru.dragonestia.msb3.api.ai.navigator.follower.GroundNodeFollower; -import ru.dragonestia.msb3.api.ai.navigator.follower.NodeFollower; +import ru.dragonestia.msb3.api.ai.movement.target.MovementTarget; +import ru.dragonestia.msb3.api.ai.movement.follower.GroundMovementFollower; +import ru.dragonestia.msb3.api.ai.movement.follower.MovementFollower; import ru.dragonestia.msb3.api.ai.navigator.node.GroundNodeGenerator; import ru.dragonestia.msb3.api.ai.navigator.node.NodeGenerator; -import ru.dragonestia.msb3.api.debug.DebugMessage; import ru.dragonestia.msb3.api.entity.EntityAI; -import ru.dragonestia.msb3.api.util.UncheckedRunnable; import java.util.Objects; -import java.util.concurrent.CompletableFuture; +/** + * Класс для управления поведением сущности + */ @Log4j2 @Getter public class AI { private final EntityAI entity; - private DestinationPoint destinationData; @Setter private NodeGenerator nodeGenerator = new GroundNodeGenerator(); - @Setter private NodeFollower nodeFollower; private final MovementFollower movementFollower; @Getter private final Actor actor; - @Setter private Point destinationPoint; + @Getter private MovementTarget movementTarget; public AI(EntityAI entity) { this.entity = Objects.requireNonNull(entity, "AI is null"); - nodeFollower = new GroundNodeFollower(entity); movementFollower = new GroundMovementFollower(entity); actor = new Actor(entity); } - public PathState getCurrentPathState() { + public void setMovementTarget(MovementTarget movementTarget) { synchronized (this) { - if (destinationData == null) return PathState.NONE; - return destinationData.path().getState(); - } - } - - public CompletableFuture setPathTo(Point point) { - var bb = entity.getBoundingBox(); - var centerToCorner = Math.sqrt(bb.width() * bb.width() + bb.depth() * bb.depth()) / 2; - return setPathTo(point, centerToCorner); - } - - public CompletableFuture setPathTo(Point point, double arrivalDistance) { - return setPathTo(point, arrivalDistance, 50, 20); - } - - public CompletableFuture setPathTo(Point point, double arrivalDistance, double maxDistance, double pathVariance) { - Objects.requireNonNull(point, "Point is null"); - var instance = Objects.requireNonNull(entity.getInstance(), "Entity is not spawned"); - if (!instance.getWorldBorder().inBounds(point)) - throw new IllegalStateException("Destination point is not in world bounds"); - if (!instance.isChunkLoaded(point)) - throw new IllegalStateException("Chunk is not loaded for destination point"); - - - synchronized (this) { - // Завершаем активное следование к точке - if (destinationData != null) { - var data = destinationData; - destinationData = null; - - UncheckedRunnable.runIgnoreException(() -> data.future().complete(PathState.TERMINATED)); + if (this.movementTarget != null) { + this.movementTarget.cancel(); } - // Игрок находится рядом с точкой, куда нужно прийти - if (entity.getDistanceSquared(point) <= arrivalDistance * arrivalDistance) { - return CompletableFuture.completedFuture(PathState.COMPLETED); + this.movementTarget = movementTarget; + + if (movementTarget != null) { + movementTarget.start(); } - - // Проверка находится ли точка слишком далеко - if (entity.getDistanceSquared(point) > Math.pow(maxDistance - arrivalDistance / 2, 2)) { - return CompletableFuture.completedFuture(PathState.TOO_FAR); - } - - // Начинаем просчет пути до новой точки - var path = PathGenerator.generate(instance, - entity.getPosition(), - point, - arrivalDistance, - maxDistance, - pathVariance, - entity.getBoundingBox(), - entity.isOnGround(), - nodeGenerator); - - // Если на время просчета пути уже известен результат действия - if (path.getState().isCompleted()) { - return CompletableFuture.completedFuture(path.getState()); - } - - return (destinationData = new DestinationPoint(Vec.fromPoint(point), arrivalDistance, path, new CompletableFuture<>())) - .future().thenApply(result -> { - resetDestinationData(); - return result; - }); } } public void tick(long delta) { synchronized (this) { - if (entity.isDead()) return; - var action = actor.getCurrentAction(); if (action != null) action.tick(actor, entity, delta); - if (destinationData != null) tickNavigator(); - if (destinationPoint != null) tickNewNavigator(); + if (movementTarget != null) movementTarget.tick(); } } - - private void tickNewNavigator() { - if (checkDestinationPointCompleted()) return; - - var speed = Math.min(getMovementSpeed(), Math.sqrt(movementFollower.squaredDistance(destinationPoint))); - var prevPosition = entity.getPosition(); - movementFollower.moveTo(destinationPoint, speed, destinationPoint); - var movementDistance = movementFollower.squaredDistance(prevPosition); - if (movementDistance == 0) { - // Deadlocked? - } - - checkDestinationPointCompleted(); - } - - private boolean checkDestinationPointCompleted() { - double distSquared = movementFollower.squaredDistance(destinationPoint); - if (distSquared < 0.01 * 0.01) { - destinationPoint = null; - DebugMessage.broadcast("Destination point completed"); - return true; - } - return false; - } - - private void tickNavigator() { - if (entity.getPosition().distance(destinationData.position()) < destinationData.arrivalDistance()) { - var future = destinationData.future(); - UncheckedRunnable.runIgnoreException(() -> future.complete(PathState.COMPLETED)); - return; - } - - var path = destinationData.path(); - var currentTarget = path.getCurrent(); - var nextTarget = path.getNext(); - - if (currentTarget == null || path.getCurrentType() == PathNode.Type.REPATH || path.getCurrentType() == null) { - recalculatePath(); - return; - } - - if (nextTarget == null) nextTarget = destinationData.position(); - - boolean nextIsRePath = nextTarget.sameBlock(Pos.ZERO); - nodeFollower.moveTowards(currentTarget, nodeFollower.movementSpeed(), nextIsRePath ? currentTarget : nextTarget); - - // TODO: исправить баг, в данном месте, когда Entity не движется вообще. Сделать хорошую проверку на застревание - // TODO: присутствует баг, что если как-то помешать Entity следовать до точки, например поставить перед ним блок, то он перестанет идти - // TODO: Бля, этот поиск пути говнище полное! Он не учитывает ни заборы, ни полублоки, ни ковры и остальные неполноценные блоки. Трава - это вообще пиздец. - // TODO: использовать CollisionUtils для просчета поиска путей - - if (nodeFollower.isAtPoint(currentTarget)) path.next(); - else if (path.getCurrentType() == PathNode.Type.JUMP) nodeFollower.jump(currentTarget, nextTarget); - checkFinishedPath(path, destinationData.future()); - } - - public void recalculatePath() { - if (destinationData == null) return; - - var originalPath = destinationData.path(); - var newPath = PathGenerator.generate(entity.getInstance(), - entity.getPosition(), - destinationData.position(), - destinationData.arrivalDistance(), originalPath.getMaxDistance(), - originalPath.getPathVariance(), entity.getBoundingBox(), entity.isOnGround(), nodeGenerator); - - destinationData = new DestinationPoint(destinationData.position(), destinationData.arrivalDistance(), newPath, destinationData.future()); - checkFinishedPath(newPath, destinationData.future()); - } - - private void checkFinishedPath(Path path, CompletableFuture future) { - var state = path.getState(); - if (state.isCompleted()) { - UncheckedRunnable.runIgnoreException(() -> future.complete(state)); - } - } - - public void resetDestinationData() { - synchronized (this) { - if (destinationData == null) return; - - var future = destinationData.future(); - destinationData = null; - if (!future.isDone()) future.complete(PathState.TERMINATED); - } - } - - public boolean isCompleted() { - return getCurrentPathState().isCompleted(); - } - - public double getMovementSpeed() { - return entity.getAttribute(Attribute.MOVEMENT_SPEED).getValue(); - } } diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/ArrivalResultListener.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/ArrivalResultListener.java new file mode 100644 index 0000000..4a6767f --- /dev/null +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/ArrivalResultListener.java @@ -0,0 +1,10 @@ +package ru.dragonestia.msb3.api.ai; + +public interface ArrivalResultListener { + + /** + * Отправляет завершенное состояние пути. Может быть положительным (моб дошел до нужной точки), либо негативным (потерялся) + * @param resultState Состояние полученное в конце пути + */ + void result(FollowerState resultState); +} diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/PathState.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/FollowerState.java similarity index 95% rename from api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/PathState.java rename to api/src/main/java/ru/dragonestia/msb3/api/ai/FollowerState.java index 11b5038..eb78ad2 100644 --- a/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/PathState.java +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/FollowerState.java @@ -1,11 +1,11 @@ -package ru.dragonestia.msb3.api.ai.navigator; +package ru.dragonestia.msb3.api.ai; import lombok.Getter; import lombok.RequiredArgsConstructor; @Getter @RequiredArgsConstructor -public enum PathState { +public enum FollowerState { /** * Путь не задан. diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/FollowerTarget.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/FollowerTarget.java new file mode 100644 index 0000000..a482547 --- /dev/null +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/FollowerTarget.java @@ -0,0 +1,7 @@ +package ru.dragonestia.msb3.api.ai; + +import net.minestom.server.coordinate.Point; + +import java.util.function.Consumer; + +public record FollowerTarget(Point point, ArrivalResultListener callback) {} diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/action/PatrolAction.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/action/PatrolAction.java index e48eca8..f06e4f3 100644 --- a/api/src/main/java/ru/dragonestia/msb3/api/ai/action/PatrolAction.java +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/action/PatrolAction.java @@ -5,7 +5,8 @@ import lombok.Setter; import net.minestom.server.coordinate.Pos; import net.minestom.server.coordinate.Vec; import ru.dragonestia.msb3.api.ai.Actor; -import ru.dragonestia.msb3.api.ai.navigator.PathState; +import ru.dragonestia.msb3.api.ai.FollowerState; +import ru.dragonestia.msb3.api.ai.movement.target.MovementTarget; import ru.dragonestia.msb3.api.entity.EntityAI; import ru.dragonestia.msb3.api.scheduler.Scheduler; @@ -41,7 +42,8 @@ public class PatrolAction implements Action { @Override public void stop(Actor actor, EntityAI entity) { - entity.getAi().resetDestinationData(); + var ai = entity.getAi(); + if (ai.getMovementTarget() != null) ai.getMovementTarget().cancel(); this.entity = null; } @@ -81,25 +83,24 @@ public class PatrolAction implements Action { private void walkToNextPos(EntityAI entity) { var nextPos = useNextPos(); - var navigator = entity.getAi(); + var ai = entity.getAi(); - navigator.setPathTo(nextPos, destinationRadius) - .thenAccept(result -> { - if (result == PathState.TERMINATED) return; - if (result == PathState.TOO_FAR) { - Scheduler.ofEntity(entity).delayedTask(() -> { - entity.teleport(Pos.fromPoint(nextPos)) - .thenRun(() -> walkToNextPos(entity)); - }, Duration.ofSeconds(1)); - return; - } - if (result == PathState.DEADLOCKED) { - entity.teleport(Pos.fromPoint(nextPos)) - .thenRun(() -> walkToNextPos(entity)); - return; - } + ai.setMovementTarget(MovementTarget.withPathFinder(entity, nextPos, result -> { + if (result == FollowerState.TERMINATED) return; + if (result == FollowerState.TOO_FAR) { + Scheduler.ofEntity(entity).delayedTask(() -> { + entity.teleport(Pos.fromPoint(nextPos)) + .thenRun(() -> walkToNextPos(entity)); + }, Duration.ofSeconds(1)); + return; + } + if (result == FollowerState.DEADLOCKED) { + entity.teleport(Pos.fromPoint(nextPos)) + .thenRun(() -> walkToNextPos(entity)); + return; + } - walkToNextPos(entity); - }); + walkToNextPos(entity); + })); } } diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/movement/GroundMovementFollower.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/movement/follower/GroundMovementFollower.java similarity index 98% rename from api/src/main/java/ru/dragonestia/msb3/api/ai/movement/GroundMovementFollower.java rename to api/src/main/java/ru/dragonestia/msb3/api/ai/movement/follower/GroundMovementFollower.java index 135ada1..0a3aacd 100644 --- a/api/src/main/java/ru/dragonestia/msb3/api/ai/movement/GroundMovementFollower.java +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/movement/follower/GroundMovementFollower.java @@ -1,4 +1,4 @@ -package ru.dragonestia.msb3.api.ai.movement; +package ru.dragonestia.msb3.api.ai.movement.follower; import net.minestom.server.collision.CollisionUtils; import net.minestom.server.collision.PhysicsResult; diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/movement/MovementFollower.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/movement/follower/MovementFollower.java similarity index 96% rename from api/src/main/java/ru/dragonestia/msb3/api/ai/movement/MovementFollower.java rename to api/src/main/java/ru/dragonestia/msb3/api/ai/movement/follower/MovementFollower.java index 6f0f8ac..56e1bff 100644 --- a/api/src/main/java/ru/dragonestia/msb3/api/ai/movement/MovementFollower.java +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/movement/follower/MovementFollower.java @@ -1,4 +1,4 @@ -package ru.dragonestia.msb3.api.ai.movement; +package ru.dragonestia.msb3.api.ai.movement.follower; import lombok.Getter; import lombok.RequiredArgsConstructor; diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/movement/target/MovementTarget.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/movement/target/MovementTarget.java new file mode 100644 index 0000000..ed46ca3 --- /dev/null +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/movement/target/MovementTarget.java @@ -0,0 +1,75 @@ +package ru.dragonestia.msb3.api.ai.movement.target; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import net.minestom.server.coordinate.Point; +import ru.dragonestia.msb3.api.ai.ArrivalResultListener; +import ru.dragonestia.msb3.api.ai.FollowerState; +import ru.dragonestia.msb3.api.entity.EntityAI; + +@Getter +@RequiredArgsConstructor +public abstract class MovementTarget { + + private final EntityAI entity; + private final Point destinationPoint; + private final ArrivalResultListener onArrive; + private boolean started = false; + private boolean completed = false; + + public final void start() { + synchronized (this) { + if (started) return; + completed = false; + started = true; + + start0(); + } + } + + protected void start0() {} + + public final void tick() { + synchronized (this) { + if (!started) return; + + tick0(); + } + } + + protected void tick0() {} + + public final void cancel() { + synchronized (this) { + if (!started || completed) return; + started = false; + completed = true; + } + + if (cancel0()) { + complete(FollowerState.TERMINATED); + } + } + + protected boolean cancel0() { + return true; + } + + protected synchronized final void complete(FollowerState state) { + if (!state.isCompleted()) { + throw new IllegalStateException("Invalid state taken for completed movement. Taken: %s".formatted(state)); + } + + completed = true; + entity.getAi().setMovementTarget(null); + onArrive.result(state); + } + + public static MovementTarget justMoveTo(EntityAI entity, Point destination, ArrivalResultListener onArrive) { + return new WithoutPathFinderMovementTarget(entity, destination, onArrive); + } + + public static MovementTarget withPathFinder(EntityAI entity, Point destination, ArrivalResultListener onArrive) { + return new WithPathFinderMovementTarget(entity, destination, onArrive); + } +} diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/movement/target/WithPathFinderMovementTarget.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/movement/target/WithPathFinderMovementTarget.java new file mode 100644 index 0000000..c9d4dd5 --- /dev/null +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/movement/target/WithPathFinderMovementTarget.java @@ -0,0 +1,138 @@ +package ru.dragonestia.msb3.api.ai.movement.target; + +import net.minestom.server.coordinate.Point; +import ru.dragonestia.msb3.api.ai.ArrivalResultListener; +import ru.dragonestia.msb3.api.entity.EntityAI; + +public class WithPathFinderMovementTarget extends MovementTarget{ + + WithPathFinderMovementTarget(EntityAI entity, Point destinationPoint, ArrivalResultListener onArrive) { + super(entity, destinationPoint, onArrive); + } + + /* TODO: implement it + public CompletableFuture setPathTo(Point point) { + var bb = entity.getBoundingBox(); + var centerToCorner = Math.sqrt(bb.width() * bb.width() + bb.depth() * bb.depth()) / 2; + return setPathTo(point, centerToCorner); + } + + public CompletableFuture setPathTo(Point point, double arrivalDistance) { + return setPathTo(point, arrivalDistance, 50, 20); + } + + public CompletableFuture setPathTo(Point point, double arrivalDistance, double maxDistance, double pathVariance) { + Objects.requireNonNull(point, "Point is null"); + var instance = Objects.requireNonNull(entity.getInstance(), "Entity is not spawned"); + if (!instance.getWorldBorder().inBounds(point)) + throw new IllegalStateException("Destination point is not in world bounds"); + if (!instance.isChunkLoaded(point)) + throw new IllegalStateException("Chunk is not loaded for destination point"); + + + synchronized (this) { + // Завершаем активное следование к точке + if (destinationData != null) { + var data = destinationData; + destinationData = null; + + UncheckedRunnable.runIgnoreException(() -> data.future().complete(FollowerState.TERMINATED)); + } + + // Игрок находится рядом с точкой, куда нужно прийти + if (entity.getDistanceSquared(point) <= arrivalDistance * arrivalDistance) { + return CompletableFuture.completedFuture(FollowerState.COMPLETED); + } + + // Проверка находится ли точка слишком далеко + if (entity.getDistanceSquared(point) > Math.pow(maxDistance - arrivalDistance / 2, 2)) { + return CompletableFuture.completedFuture(FollowerState.TOO_FAR); + } + + // Начинаем просчет пути до новой точки + var path = PathGenerator.generate(instance, + entity.getPosition(), + point, + arrivalDistance, + maxDistance, + pathVariance, + entity.getBoundingBox(), + entity.isOnGround(), + nodeGenerator); + + // Если на время просчета пути уже известен результат действия + if (path.getState().isCompleted()) { + return CompletableFuture.completedFuture(path.getState()); + } + + return (destinationData = new DestinationPoint(Vec.fromPoint(point), arrivalDistance, path, new CompletableFuture<>())) + .future().thenApply(result -> { + resetDestinationData(); + return result; + }); + } + } + + private void tickNavigator() { + if (entity.getPosition().distance(destinationData.position()) < destinationData.arrivalDistance()) { + var future = destinationData.future(); + UncheckedRunnable.runIgnoreException(() -> future.complete(FollowerState.COMPLETED)); + return; + } + + var path = destinationData.path(); + var currentTarget = path.getCurrent(); + var nextTarget = path.getNext(); + + if (currentTarget == null || path.getCurrentType() == PathNode.Type.REPATH || path.getCurrentType() == null) { + recalculatePath(); + return; + } + + if (nextTarget == null) nextTarget = destinationData.position(); + + boolean nextIsRePath = nextTarget.sameBlock(Pos.ZERO); + nodeFollower.moveTowards(currentTarget, nodeFollower.movementSpeed(), nextIsRePath ? currentTarget : nextTarget); + + // TODO: исправить баг, в данном месте, когда Entity не движется вообще. Сделать хорошую проверку на застревание + // TODO: присутствует баг, что если как-то помешать Entity следовать до точки, например поставить перед ним блок, то он перестанет идти + // TODO: Бля, этот поиск пути говнище полное! Он не учитывает ни заборы, ни полублоки, ни ковры и остальные неполноценные блоки. Трава - это вообще пиздец. + // TODO: использовать CollisionUtils для просчета поиска путей + + if (nodeFollower.isAtPoint(currentTarget)) path.next(); + else if (path.getCurrentType() == PathNode.Type.JUMP) nodeFollower.jump(currentTarget, nextTarget); + checkFinishedPath(path, destinationData.future()); + } + + public void recalculatePath() { + if (destinationData == null) return; + + var originalPath = destinationData.path(); + var newPath = PathGenerator.generate(entity.getInstance(), + entity.getPosition(), + destinationData.position(), + destinationData.arrivalDistance(), originalPath.getMaxDistance(), + originalPath.getPathVariance(), entity.getBoundingBox(), entity.isOnGround(), nodeGenerator); + + destinationData = new DestinationPoint(destinationData.position(), destinationData.arrivalDistance(), newPath, destinationData.future()); + checkFinishedPath(newPath, destinationData.future()); + } + + private void checkFinishedPath(Path path, CompletableFuture future) { + var state = path.getState(); + if (state.isCompleted()) { + UncheckedRunnable.runIgnoreException(() -> future.complete(state)); + } + } + + public void resetDestinationData() { + synchronized (this) { + if (destinationData == null) return; + + var future = destinationData.future(); + destinationData = null; + if (!future.isDone()) future.complete(FollowerState.TERMINATED); + } + } + */ +} diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/movement/target/WithoutPathFinderMovementTarget.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/movement/target/WithoutPathFinderMovementTarget.java new file mode 100644 index 0000000..3b11b36 --- /dev/null +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/movement/target/WithoutPathFinderMovementTarget.java @@ -0,0 +1,45 @@ +package ru.dragonestia.msb3.api.ai.movement.target; + +import net.minestom.server.coordinate.Point; +import ru.dragonestia.msb3.api.ai.ArrivalResultListener; +import ru.dragonestia.msb3.api.ai.FollowerState; +import ru.dragonestia.msb3.api.ai.movement.follower.MovementFollower; +import ru.dragonestia.msb3.api.entity.EntityAI; + +public class WithoutPathFinderMovementTarget extends MovementTarget { + + private MovementFollower movementFollower; + + WithoutPathFinderMovementTarget(EntityAI entity, Point destinationPoint, ArrivalResultListener onArrive) { + super(entity, destinationPoint, onArrive); + } + + @Override + protected void start0() { + movementFollower = getEntity().getMovementFollower(); + } + + @Override + protected void tick0() { + if (checkDestinationPointCompleted()) return; + + var speed = Math.min(getEntity().getMovementSpeed(), Math.sqrt(movementFollower.squaredDistance(getDestinationPoint()))); + var prevPosition = getEntity().getPosition(); + movementFollower.moveTo(getDestinationPoint(), speed, getDestinationPoint()); + var movementDistance = movementFollower.squaredDistance(prevPosition); + if (movementDistance == 0) { // TODO: catch deadlock condition + // Deadlocked? + } + + checkDestinationPointCompleted(); + } + + private boolean checkDestinationPointCompleted() { + double distSquared = movementFollower.squaredDistance(getDestinationPoint()); + if (distSquared < 0.01 * 0.01) { + complete(FollowerState.COMPLETED); + return true; + } + return false; + } +} diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/DestinationPoint.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/DestinationPoint.java deleted file mode 100644 index 0cb22d2..0000000 --- a/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/DestinationPoint.java +++ /dev/null @@ -1,7 +0,0 @@ -package ru.dragonestia.msb3.api.ai.navigator; - -import net.minestom.server.coordinate.Vec; - -import java.util.concurrent.CompletableFuture; - -public record DestinationPoint(Vec position, double arrivalDistance, Path path, CompletableFuture future) {} diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/Navigator.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/Navigator.java new file mode 100644 index 0000000..3f276ab --- /dev/null +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/Navigator.java @@ -0,0 +1,8 @@ +package ru.dragonestia.msb3.api.ai.navigator; + +import net.minestom.server.coordinate.Point; + +public interface Navigator { + + NavigatorPath findPath(Point point); +} diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/NavigatorPath.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/NavigatorPath.java new file mode 100644 index 0000000..15e25f0 --- /dev/null +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/NavigatorPath.java @@ -0,0 +1,4 @@ +package ru.dragonestia.msb3.api.ai.navigator; + +public record NavigatorPath() { +} diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/Path.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/Path.java index bba183b..f78138f 100644 --- a/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/Path.java +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/Path.java @@ -4,6 +4,7 @@ import lombok.Getter; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Vec; import org.jetbrains.annotations.Nullable; +import ru.dragonestia.msb3.api.ai.FollowerState; import java.util.ArrayList; import java.util.List; @@ -14,7 +15,7 @@ public class Path { @Getter private final double maxDistance; @Getter private final double pathVariance; @Getter private final List nodes = new ArrayList<>(); - private final AtomicReference state = new AtomicReference<>(PathState.FOLLOWING); + private final AtomicReference state = new AtomicReference<>(FollowerState.FOLLOWING); private int index = 0; public Path(double maxDistance, double pathVariance) { @@ -22,11 +23,11 @@ public class Path { this.pathVariance = pathVariance; } - public PathState getState() { + public FollowerState getState() { return state.get(); } - public void setState(PathState newState) { + public void setState(FollowerState newState) { state.set(newState); } diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/PathGenerator.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/PathGenerator.java index 83a8410..f876289 100644 --- a/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/PathGenerator.java +++ b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/PathGenerator.java @@ -7,6 +7,7 @@ import net.minestom.server.collision.BoundingBox; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Pos; import net.minestom.server.instance.block.Block; +import ru.dragonestia.msb3.api.ai.FollowerState; import ru.dragonestia.msb3.api.ai.navigator.node.NodeGenerator; import java.util.*; @@ -50,7 +51,7 @@ public class PathGenerator { var closed = new ObjectOpenHashBigSet(maxSize); while (!open.isEmpty() && closed.size64() < maxSize) { - if (path.getState() == PathState.TERMINATED) { + if (path.getState() == FollowerState.TERMINATED) { return; } @@ -81,7 +82,7 @@ public class PathGenerator { if (current == null || !withinDistance(current, target, closeDistance)) { if (closestFoundNodes.isEmpty()) { - path.setState(PathState.DEADLOCKED); + path.setState(FollowerState.DEADLOCKED); return; } @@ -100,25 +101,25 @@ public class PathGenerator { Collections.reverse(path.getNodes()); if (path.getCurrentType() == PathNode.Type.REPATH) { - path.setState(PathState.DEADLOCKED); + path.setState(FollowerState.DEADLOCKED); path.getNodes().clear(); return; } if (path.getNodes().isEmpty()) { - path.setState(PathState.DEADLOCKED); + path.setState(FollowerState.DEADLOCKED); return; } var lastNode = path.getNodes().getLast(); if (getDistanceSquared(lastNode.x(), lastNode.y(), lastNode.z(), target) > (closeDistance * closeDistance)) { - path.setState(PathState.DEADLOCKED); + path.setState(FollowerState.DEADLOCKED); return; } PathNode pEnd = new PathNode(target, 0, 0, PathNode.Type.WALK, null); path.getNodes().add(pEnd); - path.setState(PathState.FOLLOWING); + path.setState(FollowerState.FOLLOWING); } private boolean withinDistance(PathNode point, Point target, double closeDistance) { diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/follower/GroundNodeFollower.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/follower/GroundNodeFollower.java deleted file mode 100644 index 5155234..0000000 --- a/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/follower/GroundNodeFollower.java +++ /dev/null @@ -1,66 +0,0 @@ -package ru.dragonestia.msb3.api.ai.navigator.follower; - -import lombok.RequiredArgsConstructor; -import net.minestom.server.collision.CollisionUtils; -import net.minestom.server.coordinate.Point; -import net.minestom.server.coordinate.Pos; -import net.minestom.server.coordinate.Vec; -import net.minestom.server.entity.attribute.Attribute; -import net.minestom.server.utils.position.PositionUtils; -import org.jetbrains.annotations.Nullable; -import ru.dragonestia.msb3.api.entity.EntityAI; - -@RequiredArgsConstructor -public class GroundNodeFollower implements NodeFollower { - - private final EntityAI entity; - - @Override - public void moveTowards(Point direction, double speed, Point lookAt) { - var position = entity.getPosition(); - var dx = direction.x() - position.x(); - var dy = direction.y() - position.y(); - var dz = direction.z() - position.z(); - - var dxLook = lookAt.x() - position.x(); - var dyLook = lookAt.y() - position.y(); - var dzLook = lookAt.z() - position.z(); - - // the purpose of these few lines is to slow down entities when they reach their destination - final double distSquared = dx * dx + dy * dy + dz * dz; - if (speed > distSquared) { - speed = distSquared; - } - - var radians = Math.atan2(dz, dx); - var speedX = Math.cos(radians) * speed; - var speedZ = Math.sin(radians) * speed; - var yaw = PositionUtils.getLookYaw(dxLook, dzLook); - var pitch = PositionUtils.getLookPitch(dxLook, dyLook, dzLook); - var physicsResult = CollisionUtils.handlePhysics(entity, new Vec(speedX, dy > 0? (dy + 0.1) : 0, speedZ)); - var newPosition = Pos.fromPoint(physicsResult.newPosition()); - entity.refreshPosition(newPosition.withView(yaw, pitch)); - } - - @Override - public void jump(@Nullable Point point, @Nullable Point target) { - if (entity.isOnGround()) { - jump(entity.getJumpHeight()); - } - } - - @Override - public boolean isAtPoint(Point point) { - var d = entity.getPosition().sub(point); - return d.x() * d.x() + d.z() * d.z() < 0.5 * 0.5; - } - - public void jump(double height) { - entity.setVelocity(new Vec(0, height, 0)); - } - - @Override - public double movementSpeed() { - return entity.getAttribute(Attribute.MOVEMENT_SPEED).getValue(); - } -} diff --git a/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/follower/NodeFollower.java b/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/follower/NodeFollower.java deleted file mode 100644 index 84a4dcd..0000000 --- a/api/src/main/java/ru/dragonestia/msb3/api/ai/navigator/follower/NodeFollower.java +++ /dev/null @@ -1,15 +0,0 @@ -package ru.dragonestia.msb3.api.ai.navigator.follower; - -import net.minestom.server.coordinate.Point; -import org.jetbrains.annotations.Nullable; - -public interface NodeFollower { - - void moveTowards(Point target, double speed, Point lookAt); - - void jump(@Nullable Point point, @Nullable Point target); - - boolean isAtPoint(Point point); - - double movementSpeed(); -} diff --git a/api/src/main/java/ru/dragonestia/msb3/api/command/PuppeteerSubcommand.java b/api/src/main/java/ru/dragonestia/msb3/api/command/PuppeteerSubcommand.java index 00ecba9..8efcb17 100644 --- a/api/src/main/java/ru/dragonestia/msb3/api/command/PuppeteerSubcommand.java +++ b/api/src/main/java/ru/dragonestia/msb3/api/command/PuppeteerSubcommand.java @@ -8,7 +8,9 @@ import net.minestom.server.command.builder.CommandContext; import net.minestom.server.command.builder.arguments.ArgumentType; import net.minestom.server.entity.EntityType; import net.minestom.server.entity.Player; -import ru.dragonestia.msb3.api.ai.movement.GroundMovementFollower; +import ru.dragonestia.msb3.api.ai.movement.target.MovementTarget; +import ru.dragonestia.msb3.api.ai.movement.follower.GroundMovementFollower; +import ru.dragonestia.msb3.api.debug.DebugMessage; import ru.dragonestia.msb3.api.entity.EntityAI; public class PuppeteerSubcommand extends Command { @@ -63,9 +65,9 @@ public class PuppeteerSubcommand extends Command { var player = (Player) sender; sender.sendMessage(Component.text("Set entity path target.")); - entity.getAi().setPathTo(player.getPosition()).thenAccept(result -> { + entity.getAi().setMovementTarget(MovementTarget.withPathFinder(entity, player.getPosition(), result -> { player.sendMessage(Component.text("Entity path target result: " + result, NamedTextColor.YELLOW)); - }); + })); } private void justComeHere(CommandSender sender, CommandContext ctx) { @@ -73,7 +75,9 @@ public class PuppeteerSubcommand extends Command { var player = (Player) sender; sender.sendMessage(Component.text("Set entity path target without PathFinder.")); - entity.getAi().setDestinationPoint(player.getPosition()); + entity.getAi().setMovementTarget(MovementTarget.justMoveTo(entity, player.getPosition(), result -> { + DebugMessage.broadcast("Follower target result: " + result); + })); } private void teleport(CommandSender sender, CommandContext ctx) { diff --git a/api/src/main/java/ru/dragonestia/msb3/api/entity/EntityAI.java b/api/src/main/java/ru/dragonestia/msb3/api/entity/EntityAI.java index 9f4953a..2f4675f 100644 --- a/api/src/main/java/ru/dragonestia/msb3/api/entity/EntityAI.java +++ b/api/src/main/java/ru/dragonestia/msb3/api/entity/EntityAI.java @@ -13,6 +13,8 @@ import net.minestom.server.instance.Instance; import net.minestom.server.utils.time.TimeUnit; import org.jetbrains.annotations.NotNull; import ru.dragonestia.msb3.api.ai.AI; +import ru.dragonestia.msb3.api.ai.movement.follower.GroundMovementFollower; +import ru.dragonestia.msb3.api.ai.movement.follower.MovementFollower; import java.time.Duration; import java.util.Objects; @@ -23,6 +25,7 @@ import java.util.concurrent.CompletableFuture; public class EntityAI extends LivingEntity { @Setter private int removalAnimationDelay = 1000; + private final MovementFollower movementFollower; private final AI ai = new AI(this); public EntityAI(@NotNull EntityType entityType) { @@ -31,19 +34,25 @@ public class EntityAI extends LivingEntity { public EntityAI(@NotNull EntityType entityType, @NotNull UUID uuid) { super(entityType, uuid); + + movementFollower = createMovementFollower(); + getAttribute(Attribute.MOVEMENT_SPEED).setBaseValue(0.2); heal(); } @Override public void update(long time) { - ai.tick(time); + if (!isDead()) { + ai.tick(time); + } + super.update(time); } @Override public CompletableFuture setInstance(@NotNull Instance instance, @NotNull Pos spawnPosition) { - ai.resetDestinationData(); + if (ai.getMovementTarget() != null) ai.getMovementTarget().cancel(); return super.setInstance(instance, spawnPosition).thenRun(() -> { var action = ai.getActor().getCurrentAction(); if (action != null) { @@ -76,7 +85,23 @@ public class EntityAI extends LivingEntity { ai.getActor().setActive(false); } + /** + * Получить высоту прыжка в блоках + * @return Высота прыжка + */ public double getJumpHeight() { return 1.1; } + + /** + * Получить скорость передвижения сущности. Сколько блоков в тик + * @return Скорость сущности + */ + public double getMovementSpeed() { + return getAttribute(Attribute.MOVEMENT_SPEED).getValue(); + } + + protected MovementFollower createMovementFollower() { + return new GroundMovementFollower(this); + } } diff --git a/api/src/main/java/ru/dragonestia/msb3/api/player/defaults/DebugParamsContext.java b/api/src/main/java/ru/dragonestia/msb3/api/player/defaults/DebugParamsContext.java index 270939f..0346ba4 100644 --- a/api/src/main/java/ru/dragonestia/msb3/api/player/defaults/DebugParamsContext.java +++ b/api/src/main/java/ru/dragonestia/msb3/api/player/defaults/DebugParamsContext.java @@ -37,6 +37,7 @@ public class DebugParamsContext extends PlayerContext { if (value) { if (taskDebugRendererAiPathFinder != null) return; + /* TODO: fix it taskDebugRendererAiPathFinder = Scheduler.ofPlayer(getPlayer()).repeatingTask(() -> { for (var entity: getPlayer().getInstance().getNearbyEntities(getPlayer().getPosition(), 32)) { if (entity instanceof EntityAI creature) { @@ -49,6 +50,7 @@ public class DebugParamsContext extends PlayerContext { } } }, Duration.ofMillis(500)); + */ return; } -- 2.47.2 From 071c2b7ed9d36461440902169dcacdf8ddcb3145 Mon Sep 17 00:00:00 2001 From: ScarletRedMan Date: Tue, 19 Aug 2025 22:18:13 +0700 Subject: [PATCH 15/16] !feat: added quest context --- .../msb3/api/boot/ServerBootstrap.java | 1 + .../msb3/api/command/DebugCommands.java | 1 + .../msb3/api/command/DebugQuestCommand.java | 115 ++++++++++++++++++ .../player/defaults/PlayerQuestContext.java | 21 ++++ 4 files changed, 138 insertions(+) create mode 100644 api/src/main/java/ru/dragonestia/msb3/api/command/DebugQuestCommand.java create mode 100644 api/src/main/java/ru/dragonestia/msb3/api/player/defaults/PlayerQuestContext.java diff --git a/api/src/main/java/ru/dragonestia/msb3/api/boot/ServerBootstrap.java b/api/src/main/java/ru/dragonestia/msb3/api/boot/ServerBootstrap.java index 5dcf30b..8071cc8 100644 --- a/api/src/main/java/ru/dragonestia/msb3/api/boot/ServerBootstrap.java +++ b/api/src/main/java/ru/dragonestia/msb3/api/boot/ServerBootstrap.java @@ -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() { diff --git a/api/src/main/java/ru/dragonestia/msb3/api/command/DebugCommands.java b/api/src/main/java/ru/dragonestia/msb3/api/command/DebugCommands.java index 80c9852..e783abc 100644 --- a/api/src/main/java/ru/dragonestia/msb3/api/command/DebugCommands.java +++ b/api/src/main/java/ru/dragonestia/msb3/api/command/DebugCommands.java @@ -32,6 +32,7 @@ public class DebugCommands { DebugRendererCommand.register(); DebugAICommand.register(); ScriptCommand.register(); + DebugQuestCommand.register(); log.info("Registered debug commands"); } diff --git a/api/src/main/java/ru/dragonestia/msb3/api/command/DebugQuestCommand.java b/api/src/main/java/ru/dragonestia/msb3/api/command/DebugQuestCommand.java new file mode 100644 index 0000000..d956f4d --- /dev/null +++ b/api/src/main/java/ru/dragonestia/msb3/api/command/DebugQuestCommand.java @@ -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 - View quest details"); + player.sendMessage("/debug_quest complete - Force complete quest"); + player.sendMessage("/debug_quest cancel - Force cancel quest"); + player.sendMessage("/debug_quest fail - 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 + } +} diff --git a/api/src/main/java/ru/dragonestia/msb3/api/player/defaults/PlayerQuestContext.java b/api/src/main/java/ru/dragonestia/msb3/api/player/defaults/PlayerQuestContext.java new file mode 100644 index 0000000..4c2c4bb --- /dev/null +++ b/api/src/main/java/ru/dragonestia/msb3/api/player/defaults/PlayerQuestContext.java @@ -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() { + + } +} -- 2.47.2 From 760b3551502ee8e04d3af14e5798177abde31813 Mon Sep 17 00:00:00 2001 From: ScarletRedMan Date: Tue, 19 Aug 2025 23:49:01 +0700 Subject: [PATCH 16/16] chore: update project configuration --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 27fa4ba..e61d24b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ -MSB3_MAVEN_REPOSITORY=http://git.dragonestia.ru/api/packages/MSB/maven +MSB3_MAVEN_REPOSITORY=https://git.dragonestia.ru/api/packages/Dragonestia/maven MSB3_VERSION=3.0.2_ai_r6 \ No newline at end of file -- 2.47.2