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