diff --git a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/NavMesh.java b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/NavMesh.java
new file mode 100644
index 00000000..021ce215
--- /dev/null
+++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/NavMesh.java
@@ -0,0 +1,388 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2023 SteamWar.de-Serverteam
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.steamwar.fightsystem.ai;
+
+import de.steamwar.entity.RArmorStand;
+import de.steamwar.entity.REntity;
+import de.steamwar.entity.REntityServer;
+import de.steamwar.fightsystem.ArenaMode;
+import de.steamwar.fightsystem.FightSystem;
+import de.steamwar.fightsystem.fight.FightTeam;
+import de.steamwar.fightsystem.states.FightState;
+import de.steamwar.fightsystem.states.OneShotStateDependent;
+import org.bukkit.Bukkit;
+import org.bukkit.Material;
+import org.bukkit.World;
+import org.bukkit.block.Block;
+import org.bukkit.block.BlockFace;
+import org.bukkit.event.Listener;
+import org.bukkit.util.BoundingBox;
+import org.bukkit.util.Vector;
+import org.bukkit.util.VoxelShape;
+
+import java.util.*;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.BiConsumer;
+import java.util.stream.Collectors;
+
+public class NavMesh implements Listener {
+
+ private static final World WORLD = Bukkit.getWorlds().get(0);
+ private static final double PLAYER_JUMP_HEIGHT = 1.25;
+ private static final double PLAYER_HEIGHT = 1.8125;
+ private static final BoundingBox PLAYER_SHADOW = new BoundingBox(0.2, 0, 0.2, 0.8, 1, 0.8);
+ private static final Set RELATIVE_BLOCKS_TO_CHECK = new HashSet<>();
+
+ static {
+ for (int y = -2; y <= 2; y++) {
+ RELATIVE_BLOCKS_TO_CHECK.add(new Pos(BlockFace.NORTH.getDirection().setY(y)));
+ RELATIVE_BLOCKS_TO_CHECK.add(new Pos(BlockFace.SOUTH.getDirection().setY(y)));
+ RELATIVE_BLOCKS_TO_CHECK.add(new Pos(BlockFace.EAST.getDirection().setY(y)));
+ RELATIVE_BLOCKS_TO_CHECK.add(new Pos(BlockFace.WEST.getDirection().setY(y)));
+ }
+ RELATIVE_BLOCKS_TO_CHECK.add(new Pos(BlockFace.UP.getDirection()));
+ RELATIVE_BLOCKS_TO_CHECK.add(new Pos(BlockFace.DOWN.getDirection()));
+ }
+
+ private FightTeam fightTeam;
+ private List rEntities = new ArrayList<>();
+ private REntityServer entityServer;
+
+ public NavMesh(FightTeam fightTeam, REntityServer entityServer) {
+ this.fightTeam = fightTeam;
+ this.entityServer = entityServer;
+
+ new OneShotStateDependent(ArenaMode.All, FightState.PostSchemSetup, () -> {
+ Bukkit.getScheduler().runTaskLater(FightSystem.getPlugin(), () -> {
+ long time = System.currentTimeMillis();
+ fightTeam.getExtendRegion().forEach((x, y, z) -> {
+ if (y < fightTeam.getSchemRegion().getMinY()) return;
+ checkWalkable(x, y, z);
+ });
+ floorBlock.forEach(this::checkNeighbouring);
+ System.out.println(System.currentTimeMillis() - time + " ms");
+ /*
+ iterateWalkableBlocks((vector, ceilingOffset) -> {
+ RArmorStand armorStand = new RArmorStand(entityServer, vector.toLocation(WORLD), RArmorStand.Size.MARKER);
+ armorStand.setNoGravity(true);
+ armorStand.setInvisible(true);
+ armorStand.setDisplayName("+" + (ceilingOffset == null ? "∞" : ceilingOffset));
+ });
+ */
+ }, 20);
+ });
+ new OneShotStateDependent(ArenaMode.All, FightState.Spectate, () -> {
+ floorBlock.clear();
+ });
+ }
+
+ private static class Pos {
+ private int x;
+ private int y;
+ private int z;
+
+ public Pos(int x, int y, int z) {
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ }
+
+ public Pos(Vector vector) {
+ this.x = vector.getBlockX();
+ this.y = vector.getBlockY();
+ this.z = vector.getBlockZ();
+ }
+
+ @Override
+ public String toString() {
+ return x + "," + y + "," + z;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Pos)) return false;
+ Pos pos = (Pos) o;
+ return x == pos.x && y == pos.y && z == pos.z;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(x, y, z);
+ }
+
+ public Vector toVector() {
+ return new Vector(x, y, z);
+ }
+
+ public Pos add(Pos pos) {
+ return new Pos(x + pos.x, y + pos.y, z + pos.z);
+ }
+
+ public Pos subtract(Pos pos) {
+ return new Pos(x - pos.x, y - pos.y, z - pos.z);
+ }
+ }
+
+ private Map floorBlock = new HashMap<>();
+ private Map ceilingOffset = new HashMap<>();
+ private Map> neighbourConnections = new HashMap<>();
+ private Map> reverseNeighbourConnections = new HashMap<>();
+
+ private void checkWalkable(int x, int y, int z) {
+ Pos pos = new Pos(x, y, z);
+ floorBlock.remove(pos);
+ ceilingOffset.remove(pos);
+
+ Block block = WORLD.getBlockAt(x, y, z);
+ VoxelShape floor = block.getCollisionShape();
+ if (block.getType() != Material.LADDER && !overlaps(floor, 0, 0, 0)) return;
+
+ Double floorHeight = null;
+ for (BoundingBox box : floor.getBoundingBoxes()) {
+ double by = box.getMaxY();
+ if (!overlaps(floor, 0, by, 0)) {
+ if (floorHeight == null) {
+ floorHeight = by;
+ } else {
+ floorHeight = Math.min(floorHeight, by);
+ }
+ }
+ }
+ if (floorHeight == null) return;
+
+ Double ceilingOffset = null;
+ for (int cy = y + 1; cy < fightTeam.getExtendRegion().getMaxY(); cy++) {
+ VoxelShape current = WORLD.getBlockAt(x, cy, z).getCollisionShape();
+ if (!overlaps(current, 0, 0, 0)) continue;
+
+ Double ceilingHeight = null;
+ for (BoundingBox box : current.getBoundingBoxes()) {
+ double by = box.getMinY();
+ if (!overlaps(current, 0, -(1 - by), 0)) {
+ if (ceilingHeight == null) {
+ ceilingHeight = by;
+ } else {
+ ceilingHeight = Math.max(ceilingHeight, by);
+ }
+ }
+ }
+ if (ceilingHeight != null) {
+ ceilingOffset = cy - y + ceilingHeight - floorHeight;
+ }
+ break;
+ }
+
+ if (ceilingOffset != null && ceilingOffset < PLAYER_HEIGHT) return;
+
+ floorBlock.put(pos, y + floorHeight);
+ this.ceilingOffset.put(pos, ceilingOffset);
+ }
+
+ private void checkNeighbouring(Pos pos, double posFloorHeight) {
+ Set connections = neighbourConnections.remove(pos);
+ if (connections != null) {
+ connections.forEach(p -> {
+ reverseNeighbourConnections.getOrDefault(pos.add(p), Collections.emptySet()).remove(pos);
+ });
+ }
+
+ for (Pos relativeCheck : RELATIVE_BLOCKS_TO_CHECK) {
+ Pos other = new Pos(pos.x + relativeCheck.x, pos.y + relativeCheck.y, pos.z + relativeCheck.z);
+ Double otherFloorHeight = floorBlock.get(other);
+ if (otherFloorHeight == null) continue;
+ if (otherFloorHeight > posFloorHeight && otherFloorHeight - posFloorHeight > PLAYER_JUMP_HEIGHT) continue;
+ // double floorDiff = Math.abs(posFloorHeight - otherFloorHeight);
+ // if (floorDiff > PLAYER_JUMP_HEIGHT) continue;
+
+ Double posCeilingOffset = ceilingOffset.get(pos);
+ Double otherCeilingOffset = ceilingOffset.get(other);
+ if (posCeilingOffset == null && otherCeilingOffset == null) {
+ neighbourConnections.computeIfAbsent(pos, __ -> new LinkedHashSet<>()).add(relativeCheck);
+ reverseNeighbourConnections.computeIfAbsent(pos.add(relativeCheck), __ -> new LinkedHashSet<>()).add(pos);
+ continue;
+ }
+
+ if (posCeilingOffset != null && otherCeilingOffset == null) {
+ if (posFloorHeight + posCeilingOffset - otherFloorHeight >= PLAYER_HEIGHT) {
+ neighbourConnections.computeIfAbsent(pos, __ -> new LinkedHashSet<>()).add(relativeCheck);
+ reverseNeighbourConnections.computeIfAbsent(pos.add(relativeCheck), __ -> new LinkedHashSet<>()).add(pos);
+ }
+ continue;
+ }
+ if (otherCeilingOffset != null && posCeilingOffset == null) {
+ if (otherFloorHeight + otherCeilingOffset - posFloorHeight >= PLAYER_HEIGHT) {
+ neighbourConnections.computeIfAbsent(pos, __ -> new LinkedHashSet<>()).add(relativeCheck);
+ reverseNeighbourConnections.computeIfAbsent(pos.add(relativeCheck), __ -> new LinkedHashSet<>()).add(pos);
+ }
+ continue;
+ }
+
+ if (posFloorHeight + posCeilingOffset - otherFloorHeight >= PLAYER_HEIGHT && otherFloorHeight + otherCeilingOffset - posFloorHeight >= PLAYER_HEIGHT) {
+ neighbourConnections.computeIfAbsent(pos, __ -> new LinkedHashSet<>()).add(relativeCheck);
+ reverseNeighbourConnections.computeIfAbsent(pos.add(relativeCheck), __ -> new LinkedHashSet<>()).add(pos);
+ }
+ }
+ }
+
+ private boolean overlaps(VoxelShape voxelShape, double x, double y, double z) {
+ PLAYER_SHADOW.shift(x, y, z);
+ boolean overlaps = voxelShape.overlaps(PLAYER_SHADOW);
+ PLAYER_SHADOW.shift(-x, -y, -z);
+ return overlaps;
+ }
+
+ private void iterateWalkableBlocks(BiConsumer consumer) {
+ floorBlock.forEach((pos, aDouble) -> {
+ Vector vector = new Vector(pos.x + 0.5, aDouble, pos.z + 0.5);
+ consumer.accept(vector, ceilingOffset.get(pos));
+ });
+ }
+
+ private Pos toPos(Vector vector) {
+ Pos pos = new Pos(vector);
+ if (floorBlock.containsKey(pos)) return pos;
+ pos = new Pos(pos.x, pos.y - 1, pos.z);
+ if (floorBlock.containsKey(pos)) return pos;
+ return null;
+ }
+
+ public void update(Vector posVector) {
+ Pos pos = toPos(posVector);
+ if (pos == null) return;
+
+ for (int x = -2; x <= 2; x++) {
+ for (int z = -2; z <= 2; z++) {
+ for (int y = fightTeam.getSchemRegion().getMinY(); y <= pos.y + 2; y++) {
+ checkWalkable(pos.x + x, y, pos.z + z);
+ }
+ }
+ }
+ floorBlock.forEach(this::checkNeighbouring);
+ }
+
+ public List getAllWalkableBlocks() {
+ return floorBlock.keySet().stream().map(Pos::toVector).collect(Collectors.toList());
+ }
+
+ public List getWalkableBlocks(Vector fromVector) {
+ Pos from = toPos(fromVector);
+ if (from == null) {
+ return Collections.emptyList();
+ }
+
+ Set checked = new HashSet<>();
+ List checking = new ArrayList<>();
+ checking.add(from);
+ while (!checking.isEmpty()) {
+ Pos pos = checking.remove(0);
+ checked.add(pos);
+
+ neighbourConnections.getOrDefault(pos, new HashSet<>()).forEach(p -> {
+ Pos n = pos.add(p);
+ if (checked.contains(n)) return;
+ if (checking.contains(n)) return;
+ checking.add(n);
+ });
+ }
+
+ return checked.stream().map(Pos::toVector).collect(Collectors.toList());
+ }
+
+ public List path(Vector fromVector, Vector toVector) {
+ rEntities.forEach(REntity::die);
+ rEntities.clear();
+
+ Pos from = toPos(fromVector);
+ Pos to = toPos(toVector);
+ if (from == null || to == null) {
+ return Collections.emptyList();
+ }
+ if (from.equals(to)) {
+ return Collections.emptyList();
+ }
+
+ List checking = new ArrayList<>(Arrays.asList(to));
+ Map route = new HashMap<>();
+ while (!checking.isEmpty()) {
+ Set toCheck = new HashSet<>();
+ for (Pos pos : checking) {
+ boolean foundFrom = false;
+ Set successors = reverseNeighbourConnections.get(pos);
+ for (Pos p : successors) {
+ if (route.containsKey(p)) continue;
+ route.put(p, pos);
+ toCheck.add(p);
+ foundFrom = p.equals(from);
+ if (foundFrom) break;
+ }
+
+ if (foundFrom) {
+ List path = new ArrayList<>();
+ path.add(from);
+
+ while (path.get(path.size() - 1) != to) {
+ path.add(route.get(path.get(path.size() - 1)));
+ }
+
+ for (int i = path.size() - 1; i > 0; i--) {
+ Pos current = path.get(i);
+ Pos last = path.get(i - 1);
+ if (last.y > current.y) {
+ path.set(i, new Pos(current.x, last.y, current.z));
+ }
+ }
+
+ List vectors = path.stream().map(p -> {
+ Double floorHeight = floorBlock.get(p);
+ if (p.y > (floorHeight == null ? p.y : floorHeight)) floorHeight = null;
+ return new Vector(p.x + 0.5, floorHeight == null ? p.y : floorHeight, p.z + 0.5);
+ }).collect(Collectors.toList());
+
+ AtomicReference last = new AtomicReference<>();
+ vectors.forEach(vector -> {
+ RArmorStand armorStand = new RArmorStand(entityServer, vector.toLocation(WORLD), RArmorStand.Size.MARKER);
+ armorStand.setInvisible(true);
+ armorStand.setNoGravity(true);
+ armorStand.setDisplayName("+");
+ rEntities.add(armorStand);
+
+ if (true) return;
+ Vector lastVector = last.getAndSet(vector);
+ if (lastVector == null) return;
+ lastVector = lastVector.clone().add(vector).divide(new Vector(2, 2, 2));
+ armorStand = new RArmorStand(entityServer, lastVector.toLocation(WORLD), RArmorStand.Size.MARKER);
+ armorStand.setInvisible(true);
+ armorStand.setNoGravity(true);
+ armorStand.setDisplayName("+");
+ rEntities.add(armorStand);
+ });
+
+ return vectors;
+ }
+ }
+
+ checking.clear();
+ checking.addAll(toCheck);
+ }
+
+ return Collections.emptyList();
+ }
+}