diff --git a/FightSystem_Core/build.gradle b/FightSystem_Core/build.gradle index 70ac285..b9d32c2 100644 --- a/FightSystem_Core/build.gradle +++ b/FightSystem_Core/build.gradle @@ -47,6 +47,6 @@ dependencies { compileOnly 'io.netty:netty-all:4.1.68.Final' compileOnly 'com.mojang:authlib:1.5.25' - compileOnly swdep("FastAsyncWorldEdit-1.18") + compileOnly swdep("WorldEdit-1.15") compileOnly swdep("SpigotCore") } diff --git a/FightSystem_Core/src/de/steamwar/fightsystem/ai/AI.java b/FightSystem_Core/src/de/steamwar/fightsystem/ai/AI.java index 56748c1..3d894a5 100644 --- a/FightSystem_Core/src/de/steamwar/fightsystem/ai/AI.java +++ b/FightSystem_Core/src/de/steamwar/fightsystem/ai/AI.java @@ -58,6 +58,7 @@ import java.util.logging.Level; public abstract class AI { + public static final int MOVEMENT_DELAY = 4; public static final double INTERACTION_RANGE = 5.0; private static final Map ais = new HashMap<>(); @@ -209,7 +210,7 @@ public abstract class AI { } public void move(Vector pos) { - queue.add(new Action(4) { + queue.add(new Action(MOVEMENT_DELAY) { @Override public void run() { Location location = entity.getLocation(); diff --git a/FightSystem_Core/src/de/steamwar/fightsystem/ai/LixfelAI.java b/FightSystem_Core/src/de/steamwar/fightsystem/ai/LixfelAI.java index 2116936..42e9191 100644 --- a/FightSystem_Core/src/de/steamwar/fightsystem/ai/LixfelAI.java +++ b/FightSystem_Core/src/de/steamwar/fightsystem/ai/LixfelAI.java @@ -20,51 +20,41 @@ package de.steamwar.fightsystem.ai; import com.sk89q.worldedit.extent.clipboard.Clipboard; -import com.sk89q.worldedit.registry.state.BooleanProperty; -import com.sk89q.worldedit.world.block.BaseBlock; +import com.sk89q.worldedit.registry.state.Property; +import com.sk89q.worldedit.world.block.BlockState; import de.steamwar.fightsystem.Config; -import de.steamwar.fightsystem.FightSystem; +import de.steamwar.fightsystem.ai.lixfel.PlanResult; import de.steamwar.fightsystem.fight.FightTeam; import de.steamwar.fightsystem.states.FightState; import de.steamwar.sql.SchematicData; import de.steamwar.sql.SchematicNode; import de.steamwar.sql.SteamwarUser; -import org.bukkit.Bukkit; import org.bukkit.Material; import org.bukkit.Particle; import org.bukkit.block.data.BlockData; import org.bukkit.block.data.Waterlogged; -import org.bukkit.scheduler.BukkitTask; import org.bukkit.util.Vector; import java.io.IOException; import java.util.*; -import java.util.function.Consumer; -import java.util.stream.Collectors; +import java.util.function.Supplier; public class LixfelAI extends AI { - private static final Random random = new Random(); + private final Random random = new Random(); + private final List> plans = new ArrayList<>(); - private final BukkitTask timerTask; - private int currentTime; private LixfelPathplanner pathplanner; - private List setup; - private int preRunningStart; - private List timedStart; - private List cannons; - private Cannon currentCannon; + private Vector plannedPosition; public LixfelAI(FightTeam team, String user) { super(team, SteamwarUser.get(user)); - timerTask = Bukkit.getScheduler().runTaskTimer(FightSystem.getPlugin(), () -> currentTime++, 1, 1); } @Override public SchematicNode chooseSchematic() { List publics = SchematicNode.getAllSchematicsOfType(0, Config.SchematicType.toDB()); - SchematicNode schem = publics.get(random.nextInt(publics.size())); - schem = publics.stream().filter(s -> s.getName().equals("TheUnderground")).findAny().orElse(schem); + SchematicNode schem = publics.stream().filter(s -> s.getName().equals("TheUnderground")).findAny().orElseGet(() -> publics.get(random.nextInt(publics.size()))); Clipboard clipboard; try { clipboard = new SchematicData(schem).load(); @@ -73,9 +63,13 @@ public class LixfelAI extends AI { } pathplanner = new LixfelPathplanner(clipboard); - setup = new ArrayList<>(Collections.singletonList(new Vector(25, 15, 25))); - timedStart = new ArrayList<>(Collections.singletonList(new TimedVector(420L, new Vector(21, 15, 24)))); - cannons = new ArrayList<>(Arrays.asList( + + plans.clear(); + plans.add(new MovementEmergency()); + plans.add(new ReadyPlan()); + plans.add(new TimedInteraction(200.0, -1, new Vector(25, 15, 25))); + plans.add(new TimedInteraction(210.0, 420, new Vector(21, 15, 24))); + List cannons = new ArrayList<>(Arrays.asList( new Cannon(new Vector(11,25,11), 10,23,14, 12,23,14, 10,23,12, 12,23,12, 12,24,15, 10,24,14, 12,23,15, 12,24,14, 11,24,12, 10,24,15, 12,24,12, 10,23,15, 10,24,12), new Cannon(new Vector(39,25,11), 38,24,15, 38,23,12, 40,23,14, 40,24,12, 40,23,15, 40,24,14, 39,24,12, 38,23,14, 40,24,15, 40,23,12, 38,23,15, 38,24,12, 38,24,14), new Cannon(new Vector(12,18,11), 13,17,15, 13,17,16, 13,18,16, 13,18,14, 14,18,14, 14,17,15, 14,17,16, 14,18,16, 13,17,14, 14,17,14, 13,18,15, 14,18,15), @@ -84,125 +78,120 @@ public class LixfelAI extends AI { new Cannon(null, 23,5,9, 23,6,9, 23,7,9, 23,8,9), new Cannon(null, 27,5,9, 27,6,9, 27,7,9, 27,8,9) )); - assignWater(clipboard); - chooseCannon(); + assignWater(cannons, clipboard); + plans.addAll(cannons); return schem; } @Override - protected void plan() { - switch (FightState.getFightState()) { - case POST_SCHEM_SETUP: - if(prepareSchem() && scanEnemy()) - setReady(); - break; - case PRE_RUNNING: - if(start() && scanEnemy()) - ensureInRange(currentCannon.tnt.keySet().iterator().next()); - break; - case RUNNING: - while(currentCannon.shoot() && chooseCannon() && scanEnemy()) {} - break; - default: - break; - } + public void move(Vector pos) { + plannedPosition = pos; + super.move(pos); } @Override - public void stop() { - if(!timerTask.isCancelled()) - timerTask.cancel(); - - super.stop(); + protected void plan() { + plans.stream().map(Supplier::get).filter(result -> result != PlanResult.EMPTY).max(Comparator.comparingDouble(PlanResult::getRating)).ifPresent(PlanResult::act); } - private void assignWater(Clipboard clipboard) { - BooleanProperty waterlogged = new BooleanProperty("waterlogged", Arrays.asList(false, true)); - + private void assignWater(List cannons, Clipboard clipboard) { List waterSources = new ArrayList<>(); clipboard.getRegion().forEach(block -> { - BaseBlock state = clipboard.getFullBlock(block); - if(state.getBlockType().getMaterial().isLiquid() || Boolean.TRUE.equals(state.getState(waterlogged))) + BlockState state = clipboard.getBlock(block); + Property waterlogged = state.getBlockType().getPropertyMap().get("waterlogged"); + if(state.getBlockType().getMaterial().isLiquid() || (waterlogged != null && Boolean.TRUE.equals(state.getState(waterlogged)))) waterSources.add(pathplanner.clipboardToSchem(block)); }); for(Vector water : waterSources) { - cannons.stream().min(Comparator.comparingDouble(c -> c.waterDistance(water))).ifPresent(cannon -> cannon.addWater(water)); + cannons.stream().filter(cannon -> cannon.getTNTMinY() >= water.getBlockY()).min(Comparator.comparingDouble(c -> c.waterDistance(water))).ifPresent(cannon -> cannon.addWater(water)); } } - private boolean prepareSchem() { - return doWith(setup, this::interact); + private PlanResult inRange(double rating, Vector target, Supplier plan) { + return inRangeAndTime(rating, 0, target, plan); } - private boolean scanEnemy() { - //TODO - return true; - } - - private boolean start() { - if(preRunningStart == 0) - preRunningStart = currentTime; - - if(timedStart.isEmpty()) - return true; - - long time = currentTime - preRunningStart; - TimedVector vector = timedStart.get(0); - if(!ensureInRange(vector.vector)) - return false; - - if(time >= vector.getTime()) { - interact(timedStart.remove(0).vector); - } else { - scanEnemy(); - } - return false; - } - - private boolean chooseCannon() { - List availableCannons = cannons.stream().filter(Cannon::available).collect(Collectors.toList()); - if(availableCannons.isEmpty()) - return false; - - currentCannon = availableCannons.get(random.nextInt(availableCannons.size())); - return true; - } - - private boolean doWith(List targets, Consumer method) { - if(targets.isEmpty()) - return true; - - if(ensureInRange(targets.get(0))) - method.accept(targets.remove(0)); - return false; - } - - private boolean ensureInRange(Vector target) { + private PlanResult inRangeAndTime(double rating, int time, Vector target, Supplier plan) { Vector position = getPosition(); - boolean inRange = new Vector(0, getEntity().getEyeHeight(), 0).add(position).distance(target) <= 5; + boolean inRange = new Vector(0, getEntity().getEyeHeight(), 0).add(position).distance(target) <= AI.INTERACTION_RANGE; if(inRange) - return true; + return time <= 0 ? plan.get() : PlanResult.EMPTY; List path = new ArrayList<>(pathplanner.planToRange(position, new Vector(0, -getEntity().getEyeHeight(), 0).add(target), 5.0)); for(Vector v : path) Config.world.spawnParticle(Particle.DRIP_LAVA, translate(v, false), 1); - if(path.isEmpty()) { - move(new Vector(0, 1.2, 0).add(position)); - setTNT(position); - } else { - move(path.get(0)); - } + if(path.isEmpty() || time > path.size()*AI.MOVEMENT_DELAY) + return PlanResult.EMPTY; - return false; + return new PlanResult(rating, () -> move(path.get(0))); } - private class Cannon { + private class ReadyPlan implements Supplier { + @Override + public PlanResult get() { + return new PlanResult(Double.MIN_VALUE, () -> { + setReady(); + plans.remove(this); + }); + } + } + + private class MovementEmergency implements Supplier { + + @Override + public PlanResult get() { + //TODO defunct ladder + if(plannedPosition == null || getPosition().equals(plannedPosition) || pathplanner.getLadders().contains(plannedPosition)) + return PlanResult.EMPTY; + + pathplanner.getWalkable().remove(plannedPosition); //TODO neighbour-table update + if(getEntity().getVelocity().getY() < 0) { + return new PlanResult(1000.0, () -> setTNT(getPosition().subtract(new Vector(0, 1.0, 0)))); + } else { + pathplanner.getWalkable().add(getPosition()); //TODO idealized position, neighbour-table update + //TODO handle offgrid location + //TODO update grid + return new PlanResult(10.0, () -> { + + }); + } + } + } + + private class TimedInteraction implements Supplier { + + private final double rating; + private final int time; + private final Vector vector; + + private int timeReference = 0; + public TimedInteraction(double rating, int time, Vector vector) { + this.rating = rating; + this.vector = vector; + this.time = time; + } + + @Override + public PlanResult get() { + if(!FightState.ingame()) + timeReference = getEntity().getTicksLived(); + + int timeRemaining = time - (getEntity().getTicksLived() - timeReference); + return inRangeAndTime(rating, timeRemaining, vector, () -> new PlanResult(rating, () -> { + interact(vector); + plans.remove(this); + })); + } + } + + private class Cannon implements Supplier { private final List water = new ArrayList<>(); private final Vector activator; private final Map tnt = new HashMap<>(); + private final int minY; private int freeAt; private int lastCannonCheck = -1; @@ -212,6 +201,12 @@ public class LixfelAI extends AI { for(int i = 0; i < tntpos.length; i+=3) { tnt.put(new Vector(tntpos[i], tntpos[i+1], tntpos[i+2]), false); } + + minY = tnt.keySet().stream().min(Comparator.comparingInt(Vector::getBlockY)).orElse(new Vector()).getBlockY(); + } + + public int getTNTMinY() { + return minY; } public void addWater(Vector vector) { @@ -219,75 +214,47 @@ public class LixfelAI extends AI { } public double waterDistance(Vector vector) { - return tnt.keySet().stream().mapToDouble(t -> { - double distance = t.distance(vector); - if(t.getY() < vector.getY()) - distance += 10.0; //It is unlikely that TNT is loaded from below into the cannon - return distance; - }).min().orElse(Double.MAX_VALUE); + return tnt.keySet().stream().mapToDouble(t -> t.distance(vector)).min().orElse(Double.MAX_VALUE); } - public boolean available() { - return currentTime >= freeAt && !water.isEmpty(); - } - - public boolean shoot() { - for(Vector w : water) - Config.world.spawnParticle(Particle.VILLAGER_HAPPY, translate(w, true).add(0.5, 0.5, 0.5), 1); - - if(currentTime < freeAt || water.isEmpty()) - return true; + @Override + public PlanResult get() { + //TODO smarter ratings + if(getEntity().getTicksLived() < freeAt || water.isEmpty()) + return PlanResult.EMPTY; if(lastCannonCheck < freeAt) { - Vector w = water.get(random.nextInt(water.size())); - BlockData data = getBlockData(w); - if(data.getMaterial() == Material.WATER || (data instanceof Waterlogged && ((Waterlogged)data).isWaterlogged())) - lastCannonCheck = freeAt; - else - water.remove(w); - return false; + return new PlanResult(80.0, () -> { + Vector w = water.get(random.nextInt(water.size())); + BlockData data = getBlockData(w); + if(data.getMaterial() == Material.WATER || (data instanceof Waterlogged && ((Waterlogged)data).isWaterlogged())) + lastCannonCheck = freeAt; + else + water.remove(w); + }); } - for(Map.Entry entry : tnt.entrySet()) { - if(!entry.getValue()) { - if(ensureInRange(entry.getKey())) { - setTNT(entry.getKey()); - entry.setValue(true); - } - return false; - } - } + return tnt.entrySet().stream().filter(entry -> !entry.getValue()).map(entry -> inRangeAndTime(90.0, FightState.infight() ? 0 : 1, entry.getKey(), () -> new PlanResult(100.0, () -> { + setTNT(entry.getKey()); + entry.setValue(true); - if(activator != null) { - if(!ensureInRange(activator)) - return false; + if(activator == null && tnt.values().stream().allMatch(b -> b)) + fired(); + }))).max(Comparator.comparingDouble(PlanResult::getRating)).orElseGet(() -> { + if(activator == null) + return PlanResult.EMPTY; - interact(activator); - } + return inRange(110.0, activator, () -> new PlanResult(120.0, () -> { + interact(activator); + fired(); + })); + }); + } - freeAt = currentTime + 80; + private void fired() { + freeAt = getEntity().getTicksLived() + 80; for(Map.Entry entry : tnt.entrySet()) entry.setValue(false); - - return false; - } - } - - public static class TimedVector { - private final long time; - private final Vector vector; - - public TimedVector(long time, Vector vector) { - this.time = time; - this.vector = vector; - } - - public long getTime() { - return time; - } - - public Vector getVector() { - return vector; } } } diff --git a/FightSystem_Core/src/de/steamwar/fightsystem/ai/LixfelPathplanner.java b/FightSystem_Core/src/de/steamwar/fightsystem/ai/LixfelPathplanner.java index 992c7c9..16f092a 100644 --- a/FightSystem_Core/src/de/steamwar/fightsystem/ai/LixfelPathplanner.java +++ b/FightSystem_Core/src/de/steamwar/fightsystem/ai/LixfelPathplanner.java @@ -78,6 +78,7 @@ public class LixfelPathplanner { private final Vector clipboardToSchem; private final BlockVector3 diff; + private final List ladders = new ArrayList<>(); private final List walkable = new ArrayList<>(); private final Map neighbours = new HashMap<>(); @@ -95,6 +96,9 @@ public class LixfelPathplanner { return toBukkit(vector.subtract(diff), 0.0, 0.0); } + public List getLadders() { + return ladders; + } public List getWalkable() { return walkable; } @@ -117,10 +121,10 @@ public class LixfelPathplanner { if(height >= 1.0 && region.contains(above)) return; + Vector block = toBukkit(vector.subtract(diff), 0.5, Math.max(height, 0.0)); + walkable.add(block); if(height < 0.0) - height = 0.0; - - walkable.add(toBukkit(vector.subtract(diff), 0.5, height)); + ladders.add(block); }); for(Vector vector : walkable) { diff --git a/FightSystem_Core/src/de/steamwar/fightsystem/ai/lixfel/PlanResult.java b/FightSystem_Core/src/de/steamwar/fightsystem/ai/lixfel/PlanResult.java new file mode 100644 index 0000000..0c63793 --- /dev/null +++ b/FightSystem_Core/src/de/steamwar/fightsystem/ai/lixfel/PlanResult.java @@ -0,0 +1,41 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2024 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.lixfel; + + +public class PlanResult { + public static final PlanResult EMPTY = new PlanResult(Double.NEGATIVE_INFINITY, () -> {}); + + private final double rating; + private final Runnable action; + + public PlanResult(double rating, Runnable action) { + this.rating = rating; + this.action = action; + } + + public double getRating() { + return rating; + } + + public void act() { + action.run(); + } +}