forked from SteamWar/SteamWar
Add NavMesh
This commit is contained in:
@@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<Pos> 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<REntity> 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<Pos, Double> floorBlock = new HashMap<>();
|
||||||
|
private Map<Pos, Double> ceilingOffset = new HashMap<>();
|
||||||
|
private Map<Pos, Set<Pos>> neighbourConnections = new HashMap<>();
|
||||||
|
private Map<Pos, Set<Pos>> 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<Pos> 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<Vector, Double> 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<Vector> getAllWalkableBlocks() {
|
||||||
|
return floorBlock.keySet().stream().map(Pos::toVector).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Vector> getWalkableBlocks(Vector fromVector) {
|
||||||
|
Pos from = toPos(fromVector);
|
||||||
|
if (from == null) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<Pos> checked = new HashSet<>();
|
||||||
|
List<Pos> 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<Vector> 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<Pos> checking = new ArrayList<>(Arrays.asList(to));
|
||||||
|
Map<Pos, Pos> route = new HashMap<>();
|
||||||
|
while (!checking.isEmpty()) {
|
||||||
|
Set<Pos> toCheck = new HashSet<>();
|
||||||
|
for (Pos pos : checking) {
|
||||||
|
boolean foundFrom = false;
|
||||||
|
Set<Pos> 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<Pos> 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<Vector> 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<Vector> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user