From bf26dfed5627a4e38594206cf95f4f5d6a06e13a Mon Sep 17 00:00:00 2001 From: YoyoNow Date: Thu, 16 Apr 2026 19:44:06 +0200 Subject: [PATCH] Fix NavMesh and implement some initial stuff --- .../src/de/steamwar/fightsystem/ai/AI.java | 18 +- .../de/steamwar/fightsystem/ai/AIManager.java | 5 +- .../de/steamwar/fightsystem/ai/NavMesh.java | 431 +++++++++--------- .../fightsystem/ai/schematic/Bridge.java | 45 ++ .../fightsystem/ai/schematic/Cannon.java | 45 ++ .../fightsystem/ai/schematic/WarMachine.java | 63 +++ .../ai/schematic/impl/MiniWarGear20.java | 64 +++ .../fightsystem/ai/yoyonow/YoyoNowAI.java | 102 +++++ FightSystem/build.gradle.kts | 12 +- 9 files changed, 560 insertions(+), 225 deletions(-) create mode 100644 FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/schematic/Bridge.java create mode 100644 FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/schematic/Cannon.java create mode 100644 FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/schematic/WarMachine.java create mode 100644 FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/schematic/impl/MiniWarGear20.java create mode 100644 FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/yoyonow/YoyoNowAI.java diff --git a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/AI.java b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/AI.java index 7152f40e..3c8b6e6d 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/AI.java +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/AI.java @@ -136,18 +136,22 @@ public abstract class AI { public Vector getPosition() { Location location = entity.getLocation(); + return untranslate(location.toVector()); + } + + public Vector untranslate(Vector vector) { Region extend = team.getExtendRegion(); if(Fight.getUnrotated() == team) return new Vector( - location.getX() - extend.getMinX(), - location.getY() - team.getSchemRegion().getMinY(), - location.getZ() - extend.getMinZ() + vector.getX() - extend.getMinX(), + vector.getY() - team.getSchemRegion().getMinY(), + vector.getZ() - extend.getMinZ() ); else return new Vector( - extend.getMaxX() - location.getX(), - location.getY() - team.getSchemRegion().getMinY(), - extend.getMaxZ() - location.getZ() + extend.getMaxX() - vector.getX(), + vector.getY() - team.getSchemRegion().getMinY(), + extend.getMaxZ() - vector.getZ() ); } @@ -228,7 +232,7 @@ public abstract class AI { } Location target = translate(pos); - if(Math.abs(location.getX() - target.getX()) > 1.0 || Math.abs(location.getY() - target.getY()) > 1.5 || Math.abs(location.getZ() - target.getZ()) > 1.0) { + if(Math.abs(location.getX() - target.getX()) > 1.5 || Math.abs(location.getY() - target.getY()) > 1.5 || Math.abs(location.getZ() - target.getZ()) > 1.5) { FightSystem.getPlugin().getLogger().log(Level.INFO, () -> entity.getName() + ": Overdistance movement " + location.toVector() + " " + target.toVector()); return; } diff --git a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/AIManager.java b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/AIManager.java index 1ed59d98..83e746de 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/AIManager.java +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/AIManager.java @@ -20,8 +20,10 @@ package de.steamwar.fightsystem.ai; import de.steamwar.Reflection; +import de.steamwar.core.Core; import de.steamwar.fightsystem.ArenaMode; import de.steamwar.fightsystem.Config; +import de.steamwar.fightsystem.ai.yoyonow.YoyoNowAI; import de.steamwar.fightsystem.fight.FightTeam; import lombok.AllArgsConstructor; import lombok.Getter; @@ -35,7 +37,8 @@ import java.util.stream.Collectors; @AllArgsConstructor public class AIManager { private static final List AIs = Arrays.asList( - new AIManager(DummyAI.class, Material.STONE, () -> ArenaMode.Test.contains(Config.mode)) + new AIManager(DummyAI.class, Material.STONE, () -> ArenaMode.Test.contains(Config.mode)), + new AIManager(YoyoNowAI.class, Material.SLIME_BLOCK, () -> Config.GameModeConfig.Schematic.Type.toDB().equals("miniwargear") && Core.getVersion() >= 14) ); public static List availableAIs() { diff --git a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/NavMesh.java b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/NavMesh.java index 021ce215..8531b41e 100644 --- a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/NavMesh.java +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/NavMesh.java @@ -1,7 +1,7 @@ /* * This file is a part of the SteamWar software. * - * Copyright (C) 2023 SteamWar.de-Serverteam + * Copyright (C) 2026 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 @@ -19,7 +19,7 @@ package de.steamwar.fightsystem.ai; -import de.steamwar.entity.RArmorStand; +import de.steamwar.entity.RBlockDisplay; import de.steamwar.entity.REntity; import de.steamwar.entity.REntityServer; import de.steamwar.fightsystem.ArenaMode; @@ -27,92 +27,125 @@ import de.steamwar.fightsystem.FightSystem; import de.steamwar.fightsystem.fight.FightTeam; import de.steamwar.fightsystem.states.FightState; import de.steamwar.fightsystem.states.OneShotStateDependent; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.RequiredArgsConstructor; 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.Transformation; import org.bukkit.util.Vector; import org.bukkit.util.VoxelShape; +import org.joml.AxisAngle4f; +import org.joml.Vector3f; import java.util.*; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.BiConsumer; import java.util.stream.Collectors; +import java.util.stream.Stream; -public class NavMesh implements Listener { +public class NavMesh { private static final World WORLD = Bukkit.getWorlds().get(0); + private static final double PLAYER_FALL_DISTANCE = 3; 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<>(); + private static final double PLAYER_HEIGHT = 1.8; + private static final Cuboid PLAYER_SHADOW = new Cuboid(0.2, 0, 0.2, 0.6, 1, 0.6); + + private static final Set RELATIVE_BLOCKS = new HashSet<>(); + protected static final org.bukkit.block.data.BlockData PATH_BLOCK = Material.LIME_STAINED_GLASS.createBlockData(); + protected static final Vector MIDDLE_VECTOR = new Vector(0.5, 0, 0.5); 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))); + for (int y = (int) -Math.ceil(PLAYER_FALL_DISTANCE); y <= Math.ceil(PLAYER_JUMP_HEIGHT); y++) { + RELATIVE_BLOCKS.add(new Pos(-1, y, 0)); + RELATIVE_BLOCKS.add(new Pos(1, y, 0)); + RELATIVE_BLOCKS.add(new Pos(0, y, -1)); + RELATIVE_BLOCKS.add(new Pos(0, y, 1)); } - RELATIVE_BLOCKS_TO_CHECK.add(new Pos(BlockFace.UP.getDirection())); - RELATIVE_BLOCKS_TO_CHECK.add(new Pos(BlockFace.DOWN.getDirection())); + RELATIVE_BLOCKS.add(new Pos(0, 1, 0)); + RELATIVE_BLOCKS.add(new Pos(0, -1, 0)); } private FightTeam fightTeam; - private List rEntities = new ArrayList<>(); - private REntityServer entityServer; + private REntityServer server = new REntityServer(); - public NavMesh(FightTeam fightTeam, REntityServer entityServer) { + private Map walkable = new HashMap<>(); + private Map> connections = new HashMap<>(); // Reverse connections! + + @Getter + private boolean ready = false; + + public NavMesh(FightTeam fightTeam) { this.fightTeam = fightTeam; - this.entityServer = entityServer; new OneShotStateDependent(ArenaMode.All, FightState.PostSchemSetup, () -> { + Bukkit.getOnlinePlayers().forEach(server::addPlayer); + 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); + calculatePosition(new Pos(x, y, z), walkable); }); - 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)); - }); - */ + + walkable.values().forEach(this::checkNeighbouring); + + System.out.println("NavMesh2: " + walkable.size() + " " + connections.size()); + + System.out.println("NavMesh initialized in " + (System.currentTimeMillis() - time) + " ms"); + ready = true; }, 20); }); new OneShotStateDependent(ArenaMode.All, FightState.Spectate, () -> { - floorBlock.clear(); + ready = false; }); } + private void calculatePosition(Pos pos, Map walkable) { + Block block = WORLD.getBlockAt(pos.x, pos.y, pos.z); + // Air is not walkable + if (block.isPassable()) return; + + // Ignore Ladders from floor calculation! + if (block.getType() != Material.LADDER) { + // Floor Position + pos.floor = playerCollisionMax(block.getCollisionShape()); + // If no walkable block remove + if (pos.floor == 0) return; + } else { + pos.floor = 0.5; + } + + // Ceiling Position + pos.ceiling = fightTeam.getExtendRegion().getMaxY() - pos.y; + for (int y = pos.y + 1; y <= fightTeam.getExtendRegion().getMaxY() + 3; y++) { + block = WORLD.getBlockAt(pos.x, y, pos.z); + if (block.isPassable()) continue; + double min = playerCollisionMin(block.getCollisionShape()); + if (min >= 1.0) continue; + pos.ceiling = y + min - pos.y; + break; + } + // If player cannot fit remove + if (pos.ceiling - pos.floor < PLAYER_HEIGHT) return; + + walkable.put(pos, pos); + } + + @RequiredArgsConstructor 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(); - } + private final int x; + private final int y; + private final int z; + private double floor = 0; + private double ceiling = 0; @Override public String toString() { - return x + "," + y + "," + z; + return x + "," + y + "(" + floor + ".." + ceiling + ")" + "," + z; } @Override @@ -128,142 +161,90 @@ public class NavMesh implements Listener { 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); + public Vector toVector() { + return new Vector(x, y + floor, z); + } + + public double floorPosition() { + return y + floor; + } + + public double ceilingPosition() { + return y + ceiling; } } - private Map floorBlock = new HashMap<>(); - private Map ceilingOffset = new HashMap<>(); - private Map> neighbourConnections = new HashMap<>(); - private Map> reverseNeighbourConnections = new HashMap<>(); + private double playerCollisionMax(VoxelShape voxelShape) { + List yList = voxelShape.getBoundingBoxes().stream() + .flatMap(boundingBox -> Stream.of(boundingBox.getMinY(), boundingBox.getMaxY())) + .sorted() + .collect(Collectors.toList()); - 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); - } + double collisionY = 0; + for (double y : yList) { + if (voxelShape.getBoundingBoxes() + .stream() + .noneMatch(boundingBox -> collides(y, boundingBox))) { + collisionY = y; + break; } } - 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; + return collisionY; + } - 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); - } - } + private double playerCollisionMin(VoxelShape voxelShape) { + List yList = voxelShape.getBoundingBoxes().stream() + .flatMap(boundingBox -> Stream.of(boundingBox.getMinY(), boundingBox.getMaxY())) + .sorted(Comparator.reverseOrder()) + .collect(Collectors.toList()); + + double collisionY = 1.5; + for (double y : yList) { + if (voxelShape.getBoundingBoxes() + .stream() + .noneMatch(boundingBox -> collides(y - 1, boundingBox))) { + collisionY = y; + break; } - 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); + return collisionY; } - 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); - }); + private boolean collides(double y, BoundingBox boundingBox) { + Cuboid second = new Cuboid(boundingBox.getMinX(), boundingBox.getMinY(), boundingBox.getMinZ(), boundingBox.getWidthX(), boundingBox.getHeight(), boundingBox.getWidthZ()); + return PLAYER_SHADOW.setY(y).intersects(second); + } + + @AllArgsConstructor + private static class Cuboid { + private double x; + private double y; + private double z; + private double dx; + private double dy; + private double dz; + + public boolean intersects(Cuboid cuboid) { + double minx = x - cuboid.dx; + double miny = y - cuboid.dy; + double minz = z - cuboid.dz; + double maxx = minx + dx + cuboid.dx; + double maxy = miny + dy + cuboid.dy; + double maxz = minz + dz + cuboid.dz; + return maxx > cuboid.x && maxy > cuboid.y && maxz > cuboid.z && minx < cuboid.x && miny < cuboid.y && minz < cuboid.z; } - 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); - } + public Cuboid setY(double y) { + return new Cuboid(x, y, z, dx, dy, dz); } } - 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; @@ -271,44 +252,66 @@ public class NavMesh implements Listener { 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); + Pos current = new Pos(pos.x + x, y, pos.z + z); + walkable.remove(current); + calculatePosition(current, walkable); } } } - floorBlock.forEach(this::checkNeighbouring); + + walkable.values().forEach(this::checkNeighbouring); } - public List getAllWalkableBlocks() { - return floorBlock.keySet().stream().map(Pos::toVector).collect(Collectors.toList()); + private void checkNeighbouring(Pos pos) { + for (Set value : connections.values()) { + value.remove(pos); + } + + for (Pos relative : RELATIVE_BLOCKS) { + Pos other = walkable.get(pos.add(relative)); + if (other == null) continue; + + Block thisBlock = WORLD.getBlockAt(pos.x, pos.y, pos.z); + Block otherBlock = WORLD.getBlockAt(other.x, other.y, other.z); + + // TODO: Ladder connections! + if (thisBlock.getType() != Material.LADDER && otherBlock.getType() == Material.LADDER && pos.y > other.y) continue; + + // Check if jumpable vertical distance + if (otherBlock.getType() != Material.LADDER && other.floorPosition() - pos.floorPosition() > PLAYER_JUMP_HEIGHT) continue; + // Check if Ceiling is high enough for player walking down! + if (other.ceilingPosition() < pos.floorPosition() + PLAYER_HEIGHT) continue; + + connections.computeIfAbsent(other, __ -> new LinkedHashSet<>()).add(pos); + } } - public List getWalkableBlocks(Vector fromVector) { - Pos from = toPos(fromVector); - if (from == null) { - return Collections.emptyList(); - } + private Pos toPos(Vector vector) { + Pos pos = new Pos(vector.getBlockX(), vector.getBlockY(), vector.getBlockZ()); + if (walkable.containsKey(pos)) return walkable.get(pos); + pos = new Pos(pos.x, pos.y - 1, pos.z); + if (walkable.containsKey(pos)) return walkable.get(pos); + return null; + } - Set checked = new HashSet<>(); - List checking = new ArrayList<>(); - checking.add(from); - while (!checking.isEmpty()) { - Pos pos = checking.remove(0); - checked.add(pos); + public List walkable() { + return walkable.values().stream().map(Pos::toVector).collect(Collectors.toList()); + } - 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); - }); - } + public List pathToNearest(Vector fromVector, Vector toVector) { + Pos to = toPos(toVector); + if (walkable.containsKey(to)) return path(fromVector, toVector); - return checked.stream().map(Pos::toVector).collect(Collectors.toList()); + Pos nearestPos = walkable.values() + .stream() + .min(Comparator.comparingDouble(pos -> pos.toVector().distanceSquared(toVector))) + .orElse(null); + if (nearestPos == null) return Collections.emptyList(); + return path(fromVector, nearestPos.toVector()); } public List path(Vector fromVector, Vector toVector) { - rEntities.forEach(REntity::die); - rEntities.clear(); + server.getEntities().forEach(REntity::die); Pos from = toPos(fromVector); Pos to = toPos(toVector); @@ -325,7 +328,10 @@ public class NavMesh implements Listener { Set toCheck = new HashSet<>(); for (Pos pos : checking) { boolean foundFrom = false; - Set successors = reverseNeighbourConnections.get(pos); + Set successors = connections.get(pos); + if (successors == null) { + return Collections.emptyList(); + } for (Pos p : successors) { if (route.containsKey(p)) continue; route.put(p, pos); @@ -342,39 +348,32 @@ public class NavMesh implements Listener { 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)); - } + for (int i = 0; i < path.size(); i++) { + path.set(i, walkable.get(path.get(i))); } - 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()); + int i = 0; + while (i < path.size() - 1) { + Pos current = path.get(i); + Pos next = path.get(i + 1); - AtomicReference last = new AtomicReference<>(); + if (current.floorPosition() > next.floorPosition() + 1) { + Pos between = new Pos(next.x, current.y, next.z); + between.floor = current.floor; + path.add(i + 1, between); + i++; + } + i++; + } + + List vectors = path.stream().skip(1).map(Pos::toVector).collect(Collectors.toList()); 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); + RBlockDisplay block = new RBlockDisplay(server, vector.toLocation(WORLD)); + block.setBlock(PATH_BLOCK); + block.setTransform(new Transformation(new Vector3f(0, 0, 0), new AxisAngle4f(0, 0, 0, 0), new Vector3f(1, 0.001F, 1), new AxisAngle4f(0, 0, 0, 0))); }); + vectors.forEach(vector -> vector.add(MIDDLE_VECTOR)); return vectors; } } diff --git a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/schematic/Bridge.java b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/schematic/Bridge.java new file mode 100644 index 00000000..36deb30e --- /dev/null +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/schematic/Bridge.java @@ -0,0 +1,45 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 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.schematic; + +import lombok.Getter; +import org.bukkit.util.Vector; + +import java.util.HashSet; +import java.util.Set; + +@Getter +public class Bridge { + + private final Set shieldActivators = new HashSet<>(); + private Vector automaticActivator = null; + private int automaticActivatorTime = 0; + + public Bridge addShieldActivator(Vector shieldActivator) { + this.shieldActivators.add(shieldActivator); + return this; + } + + public Bridge addAutomaticActivator(Vector automaticActivator, int automaticActivatorTime) { + this.automaticActivator = automaticActivator; + this.automaticActivatorTime = automaticActivatorTime; + return this; + } +} diff --git a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/schematic/Cannon.java b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/schematic/Cannon.java new file mode 100644 index 00000000..0084c0fc --- /dev/null +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/schematic/Cannon.java @@ -0,0 +1,45 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 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.schematic; + +import lombok.Getter; +import org.bukkit.util.Vector; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@Getter +public class Cannon { + + private final List tntPositions = new ArrayList<>(); + private final Set triggerPositions = new HashSet<>(); + + public Cannon addTnt(Vector tnt) { + tntPositions.add(tnt); + return this; + } + + public Cannon addTrigger(Vector trigger) { + triggerPositions.add(trigger); + return this; + } +} diff --git a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/schematic/WarMachine.java b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/schematic/WarMachine.java new file mode 100644 index 00000000..c27b2ee4 --- /dev/null +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/schematic/WarMachine.java @@ -0,0 +1,63 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 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.schematic; + +import lombok.Getter; +import org.bukkit.util.Consumer; +import org.bukkit.util.Vector; + +import java.util.ArrayList; +import java.util.List; + +@Getter +public class WarMachine { + + private int schematicId = 0; + private Bridge bridge = new Bridge(); + private List cannons = new ArrayList<>(); + private List tntChests = new ArrayList<>(); + + public WarMachine setSchematicId(int schematicId) { + this.schematicId = schematicId; + return this; + } + + public WarMachine editBridge(Consumer initializer) { + initializer.accept(bridge); + return this; + } + + public WarMachine addCannon(Consumer initializer) { + Cannon cannon = new Cannon(); + initializer.accept(cannon); + cannons.add(cannon); + return this; + } + + public WarMachine addTnTChest(Vector chest) { + tntChests.add(chest); + return this; + } + + public WarMachine finish(List list) { + list.add(this); + return this; + } +} diff --git a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/schematic/impl/MiniWarGear20.java b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/schematic/impl/MiniWarGear20.java new file mode 100644 index 00000000..62a55725 --- /dev/null +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/schematic/impl/MiniWarGear20.java @@ -0,0 +1,64 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 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.schematic.impl; + +import de.steamwar.fightsystem.ai.schematic.WarMachine; +import org.bukkit.util.Vector; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +public class MiniWarGear20 { + + private static final Random RANDOM = new Random(); + private static final List MiniWarGear20 = new ArrayList<>(); + + public static WarMachine select() { + return MiniWarGear20.get(RANDOM.nextInt(MiniWarGear20.size())); + } + + public static final WarMachine DPR_PV1_Reaper = new WarMachine() + .setSchematicId(135745) + .editBridge(bridge -> { + bridge.addShieldActivator(new Vector(23, 16, 19)); + bridge.addShieldActivator(new Vector(32, 4, 13)); + bridge.addShieldActivator(new Vector(20, 4, 13)); + }) + .addCannon(cannon -> { + cannon.addTnt(new Vector(30, 2, 22)); + cannon.addTnt(new Vector(30, 2, 23)); + cannon.addTnt(new Vector(30, 2, 24)); + cannon.addTnt(new Vector(31, 2, 23)); + cannon.addTnt(new Vector(31, 2, 24)); + cannon.addTnt(new Vector(30, 3, 25)); + cannon.addTnt(new Vector(31, 3, 25)); + cannon.addTnt(new Vector(30, 3, 26)); + cannon.addTnt(new Vector(31, 3, 26)); + cannon.addTnt(new Vector(30, 4, 25)); + cannon.addTnt(new Vector(31, 4, 25)); + cannon.addTnt(new Vector(30, 4, 26)); + cannon.addTnt(new Vector(31, 4, 26)); + cannon.addTrigger(new Vector(31, 4, 20)); + cannon.addTrigger(new Vector(32, 4, 19)); + cannon.addTrigger(new Vector(32, 4, 21)); + }) + .finish(MiniWarGear20); +} diff --git a/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/yoyonow/YoyoNowAI.java b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/yoyonow/YoyoNowAI.java new file mode 100644 index 00000000..2cc6186b --- /dev/null +++ b/FightSystem/FightSystem_Core/src/de/steamwar/fightsystem/ai/yoyonow/YoyoNowAI.java @@ -0,0 +1,102 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 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.yoyonow; + +import com.sk89q.worldedit.extent.clipboard.Clipboard; +import de.steamwar.entity.REntityServer; +import de.steamwar.fightsystem.ArenaMode; +import de.steamwar.fightsystem.ai.AI; +import de.steamwar.fightsystem.ai.NavMesh; +import de.steamwar.fightsystem.ai.schematic.WarMachine; +import de.steamwar.fightsystem.ai.schematic.impl.MiniWarGear20; +import de.steamwar.fightsystem.fight.FightTeam; +import de.steamwar.fightsystem.states.FightState; +import de.steamwar.fightsystem.states.OneShotStateDependent; +import de.steamwar.sql.SchematicNode; +import de.steamwar.sql.SteamwarUser; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.util.Vector; + +import java.util.List; +import java.util.Random; + +public class YoyoNowAI extends AI { + + private static final World WORLD = Bukkit.getWorlds().get(0); + + private WarMachine selectedSchematic; + + private final REntityServer entityServer = new REntityServer(); + private final NavMesh navMesh; + + protected YoyoNowAI(FightTeam team) { + super(team, SteamwarUser.get("YoyoNow.AI")); + getEntity().setGlowing(true); + navMesh = new NavMesh(team); + + new OneShotStateDependent(ArenaMode.All, FightState.PostSchemSetup, () -> { + Bukkit.getOnlinePlayers().forEach(entityServer::addPlayer); + }); + } + + @Override + public SchematicNode chooseSchematic() { + selectedSchematic = MiniWarGear20.select(); + return SchematicNode.getSchematicNode(selectedSchematic.getSchematicId()); + } + + @Override + public void schematic(Clipboard clipboard) { + setReady(); + } + + private Random random = new Random(); + private Vector destination = null; + + @Override + protected void plan() { + if (!navMesh.isReady()) return; + + navMesh.update(getEntity().getLocation().toVector()); + + if (this.destination == null) { + List walkable = navMesh.walkable(); + Vector destination = walkable.get(random.nextInt(walkable.size())); + List path = navMesh.pathToNearest(getEntity().getLocation().toVector(), destination); + if (path.isEmpty()) return; + this.destination = untranslate(path.getLast()); + return; + } + + Location location = translate(destination); + List path = navMesh.pathToNearest(getEntity().getLocation().toVector(), location.toVector()); + + if (path.isEmpty()) { + destination = null; + return; + } + move(untranslate(path.get(0))); + if (path.get(0).equals(destination)) { + destination = null; + } + } +} diff --git a/FightSystem/build.gradle.kts b/FightSystem/build.gradle.kts index b999bdef..46430585 100644 --- a/FightSystem/build.gradle.kts +++ b/FightSystem/build.gradle.kts @@ -91,4 +91,14 @@ tasks.register("QuickGear20") { template = "QuickGear20" worldName = "arenas/WarGearPark" config = "QuickGear20.yml" -} \ No newline at end of file +} + +tasks.register("MiniWarGear20") { + group = "run" + description = "Run a MiniWarGear 1.20 Fight Server" + dependsOn(":SpigotCore:shadowJar") + dependsOn(":FightSystem:shadowJar") + template = "MiniWarGear20" + worldName = "arenas/NightTown" + config = "MiniWarGear20.yml" +}