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 frontier = new ArrayDeque<>(); private final Map parents = new HashMap<>(); private final ArrayDeque 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); } }