Volver al índice
src/main/java/ar/com/companeros/world/NavigationTask.java
package ar.com.companeros.world;
import ar.com.companeros.body.PlayerSessions;
import java.util.*;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
/** Navegación acotada sobre terreno observado; emite movimiento, nunca teletransporta. */
public final class NavigationTask {
public enum State { RUNNING, SUCCEEDED, BLOCKED, CANCELLED }
// Objetivo y presupuesto. La búsqueda se reparte entre ticks del servidor.
private final ServerPlayer player;
private final BlockPos target;
private final double arrivalDistance;
private final long startedTick;
private State state = State.RUNNING;
private String reason = "";
private final ArrayDeque<BlockPos> frontier = new ArrayDeque<>();
private final Map<BlockPos, BlockPos> parents = new HashMap<>();
private final ArrayDeque<BlockPos> route = new ArrayDeque<>();
private BlockPos searchOrigin;
private Vec3 lastPosition;
private int unmovedTicks;
private int searches;
public NavigationTask(ServerPlayer player, BlockPos target, double arrivalDistance, long startedTick) {
this.player = Objects.requireNonNull(player);
this.target = target.immutable();
this.arrivalDistance = Math.max(.65, Math.min(4, arrivalDistance));
this.startedTick = startedTick;
lastPosition = player.position();
beginSearch();
}
public State state() { return state; }
public String reason() { return reason; }
public double progress() {
return Math.max(0, 1 - Math.sqrt(player.position().distanceToSqr(Vec3.atCenterOf(target))) / 64);
}
// Ejecución: detectar llegada, resolver camino y enviar una intención física.
public State tick(long tick) {
if (state != State.RUNNING) return state;
if (!player.isAlive() || player.isRemoved()) return stop(State.BLOCKED, "actor_unavailable");
if (tick - startedTick > 2400) return stop(State.BLOCKED, "navigation_timeout");
if (arrived(player.blockPosition())) return stop(State.SUCCEEDED, "");
if (route.isEmpty()) {
searchSlice();
if (state != State.RUNNING) return state;
if (route.isEmpty()) { intent(0, false); return state; }
}
BlockPos next = route.peek();
if (!safe(next)) {
if (++searches > 4) return stop(State.BLOCKED, "path_changed");
beginSearch(); intent(0, false); return state;
}
Vec3 center = new Vec3(next.getX() + .5, next.getY(), next.getZ() + .5);
double horizontal = Math.pow(player.getX() - center.x, 2) + Math.pow(player.getZ() - center.z, 2);
if (horizontal < .16 && Math.abs(player.getY() - center.y) < .65) {
route.remove(); intent(0, false); return state;
}
Vec3 direction = center.subtract(player.position());
float yaw = (float) Math.toDegrees(Math.atan2(-direction.x, direction.z));
player.setYRot(yaw); player.setYHeadRot(yaw); player.setYBodyRot(yaw);
boolean jump = next.getY() > player.blockPosition().getY() && player.onGround();
intent(1, jump);
if (player.position().distanceToSqr(lastPosition) < .0004) unmovedTicks++;
else unmovedTicks = 0;
lastPosition = player.position();
if (unmovedTicks > 60) {
if (++searches > 4) return stop(State.BLOCKED, "movement_blocked");
beginSearch(); unmovedTicks = 0;
}
return state;
}
public void cancel() { stop(State.CANCELLED, "cancelled"); }
private State stop(State value, String why) {
state = value; reason = why; intent(0, false); return state;
}
private void intent(float forward, boolean jump) {
PlayerSessions.setMovementIntent(player.server, player.getUUID(), 0, forward, jump);
}
// Busca solamente chunks cargados y hasta 1.024 nodos, 48 por tick.
private void beginSearch() {
route.clear(); frontier.clear(); parents.clear();
searchOrigin = player.blockPosition().immutable();
frontier.add(searchOrigin); parents.put(searchOrigin, searchOrigin);
}
private void searchSlice() {
for (int count = 0; count < 48 && !frontier.isEmpty(); count++) {
BlockPos current = frontier.remove();
if (arrived(current)) { reconstruct(current); return; }
if (parents.size() >= 1024) { stop(State.BLOCKED, "path_budget_exceeded"); return; }
for (Direction dir : List.of(Direction.NORTH, Direction.SOUTH, Direction.EAST, Direction.WEST)) {
for (int dy : new int[]{0, 1, -1}) {
BlockPos next = current.relative(dir).offset(0, dy, 0).immutable();
if (parents.containsKey(next) || Math.abs(next.getX() - searchOrigin.getX()) > 48
|| Math.abs(next.getZ() - searchOrigin.getZ()) > 48 || !safe(next)) continue;
if (dy == 1 && !player.serverLevel().getBlockState(current.above(2)).getCollisionShape(player.serverLevel(), current.above(2)).isEmpty()) continue;
parents.put(next, current); frontier.add(next); break;
}
}
}
if (frontier.isEmpty()) stop(State.BLOCKED, "no_safe_path");
}
private void reconstruct(BlockPos end) {
for (BlockPos node = end; !node.equals(searchOrigin); node = parents.get(node)) route.addFirst(node);
if (route.isEmpty() && !arrived(player.blockPosition())) stop(State.BLOCKED, "no_safe_path");
}
private boolean arrived(BlockPos pos) {
return Math.abs(pos.getY() - target.getY()) <= 1
&& Math.pow(pos.getX() - target.getX(), 2) + Math.pow(pos.getZ() - target.getZ(), 2) <= arrivalDistance * arrivalDistance;
}
// Reflejo preventivo: el plan no autoriza saltos letales ni andar sobre peligros visibles.
private boolean safe(BlockPos pos) {
var level = player.serverLevel();
if (!level.hasChunkAt(pos) || !level.getWorldBorder().isWithinBounds(pos)) return false;
for (BlockPos check : List.of(pos, pos.above(), pos.below())) {
var block = level.getBlockState(check);
if (!block.getFluidState().isEmpty() || block.is(Blocks.FIRE) || block.is(Blocks.SOUL_FIRE)
|| block.is(Blocks.CACTUS) || block.is(Blocks.MAGMA_BLOCK) || block.is(Blocks.POWDER_SNOW)) return false;
}
if (!level.getBlockState(pos.below()).isCollisionShapeFullBlock(level, pos.below())) return false;
AABB box = player.getBoundingBox().move(pos.getX() + .5 - player.getX(), pos.getY() - player.getY(), pos.getZ() + .5 - player.getZ());
return level.noCollision(player, box);
}
}