package squidpony.performance.alternate; import squidpony.GwtCompatibility; import squidpony.annotation.GwtIncompatible; import squidpony.squidai.Threat; import squidpony.squidgrid.*; import squidpony.squidmath.*; import java.util.*; /** * An alternative to AStarSearch when you want to fully explore a search space, or when you want a gradient floodfill. * If you can't remember how to spell this, just remember: Does It Just Know Stuff? That's Really Awesome! * Created by Tommy Ettinger on 4/4/2015. */ public class DijkstraMap { /** * The type of heuristic to use. */ public enum Measurement { /** * The distance it takes when only the four primary directions can be * moved in. The default. */ MANHATTAN, /** * The distance it takes when diagonal movement costs the same as * cardinal movement. */ CHEBYSHEV, /** * The distance it takes as the crow flies. This will NOT affect movement cost when calculating a path, * only the preferred squares to travel to (resulting in drastically more reasonable-looking paths). */ EUCLIDEAN } /** * This affects how distance is measured on diagonal directions vs. orthogonal directions. MANHATTAN should form a * diamond shape on a featureless map, while CHEBYSHEV and EUCLIDEAN will form a square. EUCLIDEAN does not affect * the length of paths, though it will change the DijkstraMap's gradientMap to have many non-integer values, and * that in turn will make paths this finds much more realistic and smooth (favoring orthogonal directions unless a * diagonal one is a better option). */ public Measurement measurement = Measurement.MANHATTAN; /** * Stores which parts of the map are accessible and which are not. Should not be changed unless the actual physical * terrain has changed. You should call initialize() with a new map instead of changing this directly. */ public double[][] physicalMap; /** * The frequently-changing values that are often the point of using this class; goals will have a value of 0, and * any cells that can have a character reach a goal in n steps will have a value of n. Cells that cannot be * entered because they are solid will have a very high value equal to the WALL constant in this class, and cells * that cannot be entered because they cannot reach a goal will have a different very high value equal to the * DARK constant in this class. */ public double[][] gradientMap; /** * A 2D array of modifiers to apply to the perceived safety of an area; modifiers go up when deteriorate() is * called, which makes the cells specified in that method call more dangerous (usually because staying in one place * is perceived as risky). */ public double[][] safetyMap; /** * This stores the entry cost multipliers for each cell; that is, a value of 1.0 is a normal, unmodified cell, but * a value of 0.5 can be entered easily (two cells of its cost can be entered for the cost of one 1.0 cell), and a * value of 2.0 can only be entered with difficulty (one cell of its cost can be entered for the cost of two 1.0 * cells). Unlike the measurement field, this does affect the length of paths, as well as the numbers assigned * to gradientMap during a scan. The values for walls are identical to the value used by gradientMap, that is, this * class' WALL static final field. Floors, however, are never given FLOOR as a value, and default to 1.0 . */ public double[][] costMap = null; /** * Height of the map. Exciting stuff. Don't change this, instead call initialize(). */ public int height; /** * Width of the map. Exciting stuff. Don't change this, instead call initialize(). */ public int width; /** * The latest path that was obtained by calling findPath(). It will not contain the value passed as a starting * cell; only steps that require movement will be included, and so if the path has not been found or a valid * path toward a goal is impossible, this ArrayList will be empty. */ public ArrayList<Coord> path = new ArrayList<>(); /** * Goals are always marked with 0. */ public static final double GOAL = 0.0; /** * Floor cells, which include any walkable cell, are marked with a high number equal to 999200.0 . */ public static final double FLOOR = 999200.0; /** * Walls, which are solid no-entry cells, are marked with a high number equal to 999500.0 . */ public static final double WALL = 999500.0; /** * This is used to mark cells that the scan couldn't reach, and these dark cells are marked with a high number * equal to 999800.0 . */ public static final double DARK = 999800.0; /** * Goals that pathfinding will seek out. The Double value should almost always be 0.0 , the same as the static GOAL * constant in this class. */ public LinkedHashMap<Coord, Double> goals; private LinkedHashMap<Coord, Double> fresh, closed, open; /** * The RNG used to decide which one of multiple equally-short paths to take. */ public RNG rng; private int frustration = 0; public Coord[][] targetMap; private boolean initialized = false; private int mappedCount = 0; public int getMappedCount() { return mappedCount; } /** * Construct a DijkstraMap without a level to actually scan. If you use this constructor, you must call an * initialize() method before using this class. */ public DijkstraMap() { rng = new RNG(new LightRNG()); path = new ArrayList<>(); goals = new LinkedHashMap<>(); fresh = new LinkedHashMap<>(); closed = new LinkedHashMap<>(); open = new LinkedHashMap<>(); } /** * Construct a DijkstraMap without a level to actually scan. This constructor allows you to specify an RNG before * it is ever used in this class. If you use this constructor, you must call an initialize() method before using * any other methods in the class. */ public DijkstraMap(RNG random) { rng = random; path = new ArrayList<>(); goals = new LinkedHashMap<>(); fresh = new LinkedHashMap<>(); closed = new LinkedHashMap<>(); open = new LinkedHashMap<>(); } /** * Used to construct a DijkstraMap from the output of another. * * @param level */ public DijkstraMap(final double[][] level) { rng = new RNG(new LightRNG()); path = new ArrayList<>(); goals = new LinkedHashMap<>(); fresh = new LinkedHashMap<>(); closed = new LinkedHashMap<>(); open = new LinkedHashMap<>(); initialize(level); } /** * Used to construct a DijkstraMap from the output of another, specifying a distance calculation. * * @param level * @param measurement */ public DijkstraMap(final double[][] level, Measurement measurement) { rng = new RNG(new LightRNG()); this.measurement = measurement; path = new ArrayList<>(); goals = new LinkedHashMap<>(); fresh = new LinkedHashMap<>(); closed = new LinkedHashMap<>(); open = new LinkedHashMap<>(); initialize(level); } /** * Constructor meant to take a char[][] returned by DungeonBoneGen.generate(), or any other * char[][] where '#' means a wall and anything else is a walkable tile. If you only have * a map that uses box-drawing characters, use DungeonUtility.linesToHashes() to get a * map that can be used here. * * @param level */ public DijkstraMap(final char[][] level) { rng = new RNG(new LightRNG()); path = new ArrayList<>(); goals = new LinkedHashMap<>(); fresh = new LinkedHashMap<>(); closed = new LinkedHashMap<>(); open = new LinkedHashMap<>(); initialize(level); } /** * Constructor meant to take a char[][] returned by DungeonBoneGen.generate(), or any other * char[][] where '#' means a wall and anything else is a walkable tile. If you only have * a map that uses box-drawing characters, use DungeonUtility.linesToHashes() to get a * map that can be used here. Also takes an RNG that ensures predictable path choices given * otherwise identical inputs and circumstances. * * @param level * @param rng The RNG to use for certain decisions; only affects find* methods like findPath, not scan. */ public DijkstraMap(final char[][] level, RNG rng) { this.rng = rng; path = new ArrayList<>(); goals = new LinkedHashMap<>(); fresh = new LinkedHashMap<>(); closed = new LinkedHashMap<>(); open = new LinkedHashMap<>(); initialize(level); } /** * Constructor meant to take a char[][] returned by DungeonBoneGen.generate(), or any other * char[][] where one char means a wall and anything else is a walkable tile. If you only have * a map that uses box-drawing characters, use DungeonUtility.linesToHashes() to get a * map that can be used here. You can specify the character used for walls. * * @param level */ public DijkstraMap(final char[][] level, char alternateWall) { rng = new RNG(new LightRNG()); path = new ArrayList<>(); goals = new LinkedHashMap<>(); fresh = new LinkedHashMap<>(); closed = new LinkedHashMap<>(); open = new LinkedHashMap<>(); initialize(level, alternateWall); } /** * Constructor meant to take a char[][] returned by DungeonBoneGen.generate(), or any other * char[][] where '#' means a wall and anything else is a walkable tile. If you only have * a map that uses box-drawing characters, use DungeonUtility.linesToHashes() to get a * map that can be used here. This constructor specifies a distance measurement. * * @param level * @param measurement */ public DijkstraMap(final char[][] level, Measurement measurement) { rng = new RNG(new LightRNG()); path = new ArrayList<>(); this.measurement = measurement; goals = new LinkedHashMap<>(); fresh = new LinkedHashMap<>(); closed = new LinkedHashMap<>(); open = new LinkedHashMap<>(); initialize(level); } /** * Constructor meant to take a char[][] returned by DungeonBoneGen.generate(), or any other * char[][] where '#' means a wall and anything else is a walkable tile. If you only have * a map that uses box-drawing characters, use DungeonUtility.linesToHashes() to get a * map that can be used here. Also takes a distance measurement and an RNG that ensures * predictable path choices given otherwise identical inputs and circumstances. * * @param level * @param rng The RNG to use for certain decisions; only affects find* methods like findPath, not scan. */ public DijkstraMap(final char[][] level, Measurement measurement, RNG rng) { this.rng = rng; path = new ArrayList<>(); this.measurement = measurement; goals = new LinkedHashMap<>(); fresh = new LinkedHashMap<>(); closed = new LinkedHashMap<>(); open = new LinkedHashMap<>(); initialize(level); } /** * Used to initialize or re-initialize a DijkstraMap that needs a new PhysicalMap because it either wasn't given * one when it was constructed, or because the contents of the terrain have changed permanently (not if a * creature moved; for that you pass the positions of creatures that block paths to scan() or findPath() ). * * @param level * @return */ public DijkstraMap initialize(final double[][] level) { width = level.length; height = level[0].length; gradientMap = new double[width][height]; safetyMap = new double[width][height]; physicalMap = new double[width][height]; costMap = new double[width][height]; targetMap = new Coord[width][height]; for (int x = 0; x < width; x++) { System.arraycopy(level[x], 0, gradientMap[x], 0, height); System.arraycopy(level[x], 0, physicalMap[x], 0, height); Arrays.fill(costMap[x], 1.0); } initialized = true; return this; } /** * Used to initialize or re-initialize a DijkstraMap that needs a new PhysicalMap because it either wasn't given * one when it was constructed, or because the contents of the terrain have changed permanently (not if a * creature moved; for that you pass the positions of creatures that block paths to scan() or findPath() ). * * @param level * @return */ public DijkstraMap initialize(final char[][] level) { width = level.length; height = level[0].length; gradientMap = new double[width][height]; safetyMap = new double[width][height]; physicalMap = new double[width][height]; costMap = new double[width][height]; targetMap = new Coord[width][height]; for (int x = 0; x < width; x++) { Arrays.fill(costMap[x], 1.0); for (int y = 0; y < height; y++) { double t = (level[x][y] == '#') ? WALL : FLOOR; gradientMap[x][y] = t; physicalMap[x][y] = t; } } initialized = true; return this; } /** * Used to initialize or re-initialize a DijkstraMap that needs a new PhysicalMap because it either wasn't given * one when it was constructed, or because the contents of the terrain have changed permanently (not if a * creature moved; for that you pass the positions of creatures that block paths to scan() or findPath() ). This * initialize() method allows you to specify an alternate wall char other than the default character, '#' . * * @param level * @param alternateWall * @return */ public DijkstraMap initialize(final char[][] level, char alternateWall) { width = level.length; height = level[0].length; gradientMap = new double[width][height]; safetyMap = new double[width][height]; physicalMap = new double[width][height]; costMap = new double[width][height]; targetMap = new Coord[width][height]; for (int x = 0; x < width; x++) { Arrays.fill(costMap[x], 1.0); for (int y = 0; y < height; y++) { double t = (level[x][y] == alternateWall) ? WALL : FLOOR; gradientMap[x][y] = t; physicalMap[x][y] = t; } } initialized = true; return this; } /** * Used to initialize the entry cost modifiers for games that require variable costs to enter squares. This expects * a char[][] of the same exact dimensions as the 2D array that was used to previously initialize() this * DijkstraMap, treating the '#' char as a wall (impassable) and anything else as having a normal cost to enter. * The costs can be accessed later by using costMap directly (which will have a valid value when this does not * throw an exception), or by calling setCost(). * * @param level a 2D char array that uses '#' for walls * @return this DijkstraMap for chaining. */ public DijkstraMap initializeCost(final char[][] level) { if (!initialized) throw new IllegalStateException("DijkstraMap must be initialized first!"); for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { costMap[x][y] = (level[x][y] == '#') ? WALL : 1.0; } } return this; } /** * Used to initialize the entry cost modifiers for games that require variable costs to enter squares. This expects * a char[][] of the same exact dimensions as the 2D array that was used to previously initialize() this * DijkstraMap, treating the '#' char as a wall (impassable) and anything else as having a normal cost to enter. * The costs can be accessed later by using costMap directly (which will have a valid value when this does not * throw an exception), or by calling setCost(). * <p/> * This method allows you to specify an alternate wall char other than the default character, '#' . * * @param level a 2D char array that uses alternateChar for walls. * @param alternateWall a char to use to represent walls. * @return this DijkstraMap for chaining. */ public DijkstraMap initializeCost(final char[][] level, char alternateWall) { if (!initialized) throw new IllegalStateException("DijkstraMap must be initialized first!"); for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { costMap[x][y] = (level[x][y] == alternateWall) ? WALL : 1.0; } } return this; } /** * Used to initialize the entry cost modifiers for games that require variable costs to enter squares. This expects * a double[][] of the same exact dimensions as the 2D array that was used to previously initialize() this * DijkstraMap, using the exact values given in costs as the values to enter cells, even if they aren't what this * class would assign normally -- walls and other impassable values should be given WALL as a value, however. * The costs can be accessed later by using costMap directly (which will have a valid value when this does not * throw an exception), or by calling setCost(). * <p/> * This method should be slightly more efficient than the other initializeCost methods. * * @param costs a 2D double array that already has the desired cost values * @return this DijkstraMap for chaining. */ public DijkstraMap initializeCost(final double[][] costs) { if (!initialized) throw new IllegalStateException("DijkstraMap must be initialized first!"); costMap = new double[width][height]; for (int x = 0; x < width; x++) { System.arraycopy(costs[x], 0, costMap[x], 0, height); } return this; } /** * Gets the appropriate DijkstraMap.Measurement to pass to a constructor if you already have a Radius. * Matches SQUARE or CUBE to CHEBYSHEV, DIAMOND or OCTAHEDRON to MANHATTAN, and CIRCLE or SPHERE to EUCLIDEAN. * * @param radius the Radius to find the corresponding Measurement for * @return a DijkstraMap.Measurement that matches radius; SQUARE to CHEBYSHEV, DIAMOND to MANHATTAN, etc. */ public static Measurement findMeasurement(Radius radius) { if (radius.equals2D(Radius.SQUARE)) return DijkstraMap.Measurement.CHEBYSHEV; else if (radius.equals2D(Radius.DIAMOND)) return DijkstraMap.Measurement.MANHATTAN; else return DijkstraMap.Measurement.EUCLIDEAN; } /** * Gets the appropriate Radius corresponding to a DijkstraMap.Measurement. * Matches CHEBYSHEV to SQUARE, MANHATTAN to DIAMOND, and EUCLIDEAN to CIRCLE. * * @param measurement the Measurement to find the corresponding Radius for * @return a DijkstraMap.Measurement that matches radius; CHEBYSHEV to SQUARE, MANHATTAN to DIAMOND, etc. */ public static Radius findRadius(Measurement measurement) { switch (measurement) { case CHEBYSHEV: return Radius.SQUARE; case EUCLIDEAN: return Radius.CIRCLE; default: return Radius.DIAMOND; } } /** * Resets the gradientMap to its original value from physicalMap. */ public void resetMap() { if (!initialized) return; for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { gradientMap[x][y] = physicalMap[x][y]; } } } /** * Resets the targetMap (which is only assigned in the first place if you use findTechniquePath() ). */ public void resetTargetMap() { if (!initialized) return; for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { targetMap[x][y] = null; } } } /** * Resets the targetMap (which is only assigned in the first place if you use findTechniquePath() ). */ public void resetSafetyMap() { if (!initialized) return; for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { safetyMap[x][y] = 0.0; } } } /** * Resets this DijkstraMap to a state with no goals, no discovered path, and no changes made to gradientMap * relative to physicalMap. */ public void reset() { resetMap(); resetTargetMap(); goals.clear(); path.clear(); closed.clear(); fresh.clear(); open.clear(); frustration = 0; } /** * Marks a cell as a goal for pathfinding, unless the cell is a wall or unreachable area (then it does nothing). * * @param x * @param y */ public void setGoal(int x, int y) { if (!initialized) return; if (physicalMap[x][y] > FLOOR) { return; } goals.put(Coord.get(x, y), GOAL); } /** * Marks a cell as a goal for pathfinding, unless the cell is a wall or unreachable area (then it does nothing). * * @param pt */ public void setGoal(Coord pt) { if (!initialized) return; if (physicalMap[pt.x][pt.y] > FLOOR) { return; } goals.put(pt, GOAL); } /** * Marks a cell's cost for pathfinding as cost, unless the cell is a wall or unreachable area (then it always sets * the cost to the value of the WALL field). * * @param pt * @param cost */ public void setCost(Coord pt, double cost) { if (!initialized) return; if (physicalMap[pt.x][pt.y] > FLOOR) { costMap[pt.x][pt.y] = WALL; return; } costMap[pt.x][pt.y] = cost; } /** * Marks a cell's cost for pathfinding as cost, unless the cell is a wall or unreachable area (then it always sets * the cost to the value of the WALL field). * * @param x * @param y * @param cost */ public void setCost(int x, int y, double cost) { if (!initialized) return; if (physicalMap[x][y] > FLOOR) { costMap[x][y] = WALL; return; } costMap[x][y] = cost; } /** * Marks a specific cell in gradientMap as completely impossible to enter. * * @param x * @param y */ public void setOccupied(int x, int y) { if (!initialized) return; gradientMap[x][y] = WALL; } /** * Reverts a cell to the value stored in the original state of the level as known by physicalMap. * * @param x * @param y */ public void resetCell(int x, int y) { if (!initialized) return; gradientMap[x][y] = physicalMap[x][y]; } /** * Reverts a cell to the value stored in the original state of the level as known by physicalMap. * * @param pt */ public void resetCell(Coord pt) { if (!initialized) return; gradientMap[pt.x][pt.y] = physicalMap[pt.x][pt.y]; } /** * Used to remove all goals and undo any changes to gradientMap made by having a goal present. */ public void clearGoals() { if (!initialized) return; for (Coord entry : goals.keySet()) { resetCell(entry); } goals.clear(); } protected void setFresh(int x, int y, double counter) { if (!initialized) return; gradientMap[x][y] = counter; fresh.put(Coord.get(x, y), counter); } protected void setFresh(final Coord pt, double counter) { if (!initialized) return; gradientMap[pt.x][pt.y] = counter; fresh.put(pt, counter); } /** * Used in conjunction with methods that depend on finding cover, like findCoveredAttackPath(), this method causes * specified risky points to be considered less safe, and will encourage a pathfinder to keep moving toward a goal * instead of just staying in cover forever (or until an enemy moves around the cover and ambushes the pathfinder). * Typically, you call deteriorate() with the current Coord position of the pathfinder and any Coords they stayed at * earlier along a path, and you do this once every turn or once every few turns, depending on how aggressively the * pathfinder should seek a goal. * * @param riskyPoints a List of Coord that should be considered more risky to stay at with each call. * @return the current safetyMap. */ public double[][] deteriorate(List<Coord> riskyPoints) { return deteriorate(riskyPoints.toArray(new Coord[riskyPoints.size()])); } /** * Used in conjunction with methods that depend on finding cover, like findCoveredAttackPath(), this method causes * specified risky points to be considered less safe, and will encourage a pathfinder to keep moving toward a goal * instead of just staying in cover forever (or until an enemy moves around the cover and ambushes the pathfinder). * Typically, you call deteriorate() with the current Coord position of the pathfinder and any Coords they stayed at * earlier along a path, and you do this once every turn or once every few turns, depending on how aggressively the * pathfinder should seek a goal. * * @param riskyPoints a vararg or array of Coord that should be considered more risky to stay at with each call. * @return the current safetyMap. */ public double[][] deteriorate(Coord... riskyPoints) { if (!initialized) return null; Coord c; for (int i = 0; i < riskyPoints.length; i++) { c = riskyPoints[i]; safetyMap[c.x][c.y] += 1.0; } return safetyMap; } /** * Used in conjunction with methods that depend on finding cover, like findCoveredAttackPath(), this method causes * specified safer points to be considered more safe, and will make a pathfinder more likely to enter those places * if they were considered dangerous earlier (due to calling deteriorate()). * <p/> * Typically, you call relax() with previous Coords a pathfinder stayed at that should be safer now than they were * at some previous point in time, and you might do this when no one has been attacked in a while or when the AI is * sure that a threat has been neutralized or no longer threatens a safer point. * * @param saferPoints a List of Coord that should be considered less risky to stay at with each call. * @return the current safetyMap. */ public double[][] relax(List<Coord> saferPoints) { return relax(saferPoints.toArray(new Coord[saferPoints.size()])); } /** * Used in conjunction with methods that depend on finding cover, like findCoveredAttackPath(), this method causes * specified safer points to be considered more safe, and will make a pathfinder more likely to enter those places * if they were considered dangerous earlier (due to calling deteriorate()). * <p/> * Typically, you call relax() with previous Coords a pathfinder stayed at that should be safer now than they were * at some previous point in time, and you might do this when no one has been attacked in a while or when the AI is * sure that a threat has been neutralized or no longer threatens a safer point. * * @param saferPoints a vararg or array of Coord that should be considered less risky to stay at with each call. * @return the current safetyMap. */ public double[][] relax(Coord... saferPoints) { if (!initialized) return null; Coord c; for (int i = 0; i < saferPoints.length; i++) { c = saferPoints[i]; safetyMap[c.x][c.y] -= 1.0; if (safetyMap[c.x][c.y] < 0.0) safetyMap[c.x][c.y] = 0.0; } return safetyMap; } /** * Recalculate the Dijkstra map and return it. Cells that were marked as goals with setGoal will have * a value of 0, the cells adjacent to goals will have a value of 1, and cells progressively further * from goals will have a value equal to the distance from the nearest goal. The exceptions are walls, * which will have a value defined by the WALL constant in this class, and areas that the scan was * unable to reach, which will have a value defined by the DARK constant in this class (typically, * these areas should not be used to place NPCs or items and should be filled with walls). This uses the * current measurement. * * @param impassable A Set of Position keys representing the locations of enemies or other moving obstacles to a * path that cannot be moved through; this can be null if there are no such obstacles. * @return A 2D double[width][height] using the width and height of what this knows about the physical map. */ public double[][] scan(Set<Coord> impassable) { if (!initialized) return null; if (impassable == null) impassable = new LinkedHashSet<>(); LinkedHashMap<Coord, Double> blocking = new LinkedHashMap<>(impassable.size()); for (Coord pt : impassable) { blocking.put(pt, WALL); } closed.putAll(blocking); for (Map.Entry<Coord, Double> entry : goals.entrySet()) { if (closed.containsKey(entry.getKey())) closed.remove(entry.getKey()); gradientMap[entry.getKey().x][entry.getKey().y] = entry.getValue(); } double currentLowest = 999000; LinkedHashMap<Coord, Double> lowest = new LinkedHashMap<>(); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { if (gradientMap[x][y] > FLOOR && !goals.containsKey(Coord.get(x, y))) closed.put(Coord.get(x, y), physicalMap[x][y]); else if (gradientMap[x][y] < currentLowest) { currentLowest = gradientMap[x][y]; lowest.clear(); lowest.put(Coord.get(x, y), currentLowest); } else if (gradientMap[x][y] == currentLowest) { lowest.put(Coord.get(x, y), currentLowest); } } } int numAssigned = lowest.size(); mappedCount = goals.size(); open.putAll(lowest); Direction[] dirs = (measurement == Measurement.MANHATTAN) ? Direction.CARDINALS : Direction.OUTWARDS; while (numAssigned > 0) { // ++iter; numAssigned = 0; for (Map.Entry<Coord, Double> cell : open.entrySet()) { for (int d = 0; d < dirs.length; d++) { Coord adj = cell.getKey().translate(dirs[d].deltaX, dirs[d].deltaY); if (adj.x < 0 || adj.y < 0 || width <= adj.x || height <= adj.y) /* Outside the map */ continue; double h = heuristic(dirs[d]); if (!closed.containsKey(adj) && !open.containsKey(adj) && gradientMap[cell.getKey().x][cell.getKey().y] + h * costMap[adj.x][adj.y] < gradientMap[adj.x][adj.y]) { setFresh(adj, cell.getValue() + h * costMap[adj.x][adj.y]); ++numAssigned; ++mappedCount; } } } // closed.putAll(open); open = new LinkedHashMap<>(fresh); fresh.clear(); } closed.clear(); open.clear(); double[][] gradientClone = new double[width][height]; for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { if (gradientMap[x][y] == FLOOR) { gradientMap[x][y] = DARK; } } System.arraycopy(gradientMap[x], 0, gradientClone[x], 0, height); } return gradientClone; } /** * Recalculate the Dijkstra map up to a limit and return it. Cells that were marked as goals with setGoal will have * a value of 0, the cells adjacent to goals will have a value of 1, and cells progressively further * from goals will have a value equal to the distance from the nearest goal. If a cell would take more steps to * reach than the given limit, it will have a value of DARK if it was passable instead of the distance. The * exceptions are walls, which will have a value defined by the WALL constant in this class, and areas that the scan * was unable to reach, which will have a value defined by the DARK constant in this class. This uses the * current measurement. * * @param limit The maximum number of steps to scan outward from a goal. * @param impassable A Set of Position keys representing the locations of enemies or other moving obstacles to a * path that cannot be moved through; this can be null if there are no such obstacles. * @return A 2D double[width][height] using the width and height of what this knows about the physical map. */ public double[][] partialScan(int limit, Set<Coord> impassable) { if (!initialized) return null; if (impassable == null) impassable = new LinkedHashSet<>(); LinkedHashMap<Coord, Double> blocking = new LinkedHashMap<>(impassable.size()); for (Coord pt : impassable) { blocking.put(pt, WALL); } closed.putAll(blocking); for (Map.Entry<Coord, Double> entry : goals.entrySet()) { if (closed.containsKey(entry.getKey())) closed.remove(entry.getKey()); gradientMap[entry.getKey().x][entry.getKey().y] = entry.getValue(); } double currentLowest = 999000; LinkedHashMap<Coord, Double> lowest = new LinkedHashMap<>(); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { if (gradientMap[x][y] > FLOOR && !goals.containsKey(Coord.get(x, y))) closed.put(Coord.get(x, y), physicalMap[x][y]); else if (gradientMap[x][y] < currentLowest) { currentLowest = gradientMap[x][y]; lowest.clear(); lowest.put(Coord.get(x, y), currentLowest); } else if (gradientMap[x][y] == currentLowest) { lowest.put(Coord.get(x, y), currentLowest); } } } int numAssigned = lowest.size(); mappedCount = goals.size(); open.putAll(lowest); Direction[] dirs = (measurement == Measurement.MANHATTAN) ? Direction.CARDINALS : Direction.OUTWARDS; int iter = 0; while (numAssigned > 0 && iter < limit) { // ++iter; numAssigned = 0; for (Map.Entry<Coord, Double> cell : open.entrySet()) { for (int d = 0; d < dirs.length; d++) { Coord adj = cell.getKey().translate(dirs[d].deltaX, dirs[d].deltaY); double h = heuristic(dirs[d]); if (!closed.containsKey(adj) && !open.containsKey(adj) && gradientMap[cell.getKey().x][cell.getKey().y] + h * costMap[adj.x][adj.y] < gradientMap[adj.x][adj.y]) { setFresh(adj, cell.getValue() + h * costMap[adj.x][adj.y]); ++numAssigned; ++mappedCount; } } } // closed.putAll(open); open = new LinkedHashMap<>(fresh); fresh.clear(); ++iter; } closed.clear(); open.clear(); double[][] gradientClone = new double[width][height]; for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { if (gradientMap[x][y] == FLOOR) { gradientMap[x][y] = DARK; } } System.arraycopy(gradientMap[x], 0, gradientClone[x], 0, height); } return gradientClone; } /** * Recalculate the Dijkstra map until it reaches a Coord in targets, then returns the first target found. * This uses the current measurement. * * @param start the cell to use as the origin for finding the nearest target * @param targets the Coords that this is trying to find; it will stop once it finds one * @return the Coord that it found first. */ public Coord findNearest(Coord start, Set<Coord> targets) { if (!initialized) return null; if (targets == null) return null; if (targets.contains(start)) return start; resetMap(); Coord start2 = start; int xShift = width / 8, yShift = height / 8; while (physicalMap[start.x][start.y] >= WALL && frustration < 50) { start2 = Coord.get(Math.min(Math.max(1, start.x + rng.nextInt(1 + xShift * 2) - xShift), width - 2), Math.min(Math.max(1, start.y + rng.nextInt(1 + yShift * 2) - yShift), height - 2)); } if (closed.containsKey(start2)) closed.remove(start2); gradientMap[start2.x][start2.y] = 0.0; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { if (gradientMap[x][y] > FLOOR && !goals.containsKey(Coord.get(x, y))) closed.put(Coord.get(x, y), physicalMap[x][y]); } } int numAssigned = 1; mappedCount = 1; open.put(start2, 0.0); Direction[] dirs = (measurement == Measurement.MANHATTAN) ? Direction.CARDINALS : Direction.OUTWARDS; while (numAssigned > 0) { // ++iter; numAssigned = 0; for (Map.Entry<Coord, Double> cell : open.entrySet()) { for (int d = 0; d < dirs.length; d++) { Coord adj = cell.getKey().translate(dirs[d].deltaX, dirs[d].deltaY); double h = heuristic(dirs[d]); if (!closed.containsKey(adj) && !open.containsKey(adj) && gradientMap[cell.getKey().x][cell.getKey().y] + h * costMap[adj.x][adj.y] < gradientMap[adj.x][adj.y]) { setFresh(adj, cell.getValue() + h * costMap[adj.x][adj.y]); ++numAssigned; ++mappedCount; if (targets.contains(adj)) { fresh.clear(); closed.clear(); open.clear(); return adj; } } } } // closed.putAll(open); open = new LinkedHashMap<>(fresh); fresh.clear(); } closed.clear(); open.clear(); return null; } /** * Recalculate the Dijkstra map until it reaches a Coord in targets, then returns the first target found. * This uses the current measurement. * * @param start the cell to use as the origin for finding the nearest target * @param targets the Coords that this is trying to find; it will stop once it finds one * @return the Coord that it found first. */ public Coord findNearest(Coord start, Coord... targets) { LinkedHashSet<Coord> tgts = new LinkedHashSet<>(targets.length); Collections.addAll(tgts, targets); return findNearest(start, tgts); } /** * If you have a target or group of targets you want to pathfind to without scanning the full map, this can be good. * It may find sub-optimal paths in the presence of costs to move into cells. It is useful when you want to move in * a straight line to a known nearby goal. * * @param start your starting location * @param targets an array or vararg of Coords to pathfind to the nearest of * @return an ArrayList of Coord that goes from a cell adjacent to start and goes to one of the targets. Copy of path. */ public ArrayList<Coord> findShortcutPath(Coord start, Coord... targets) { if (targets.length == 0) { path.clear(); return new ArrayList<>(path); } Coord currentPos = findNearest(start, targets); while (true) { if (frustration > 500) { path.clear(); break; } double best = gradientMap[currentPos.x][currentPos.y]; final Direction[] dirs = appendDir(shuffleDirs(rng), Direction.NONE); int choice = rng.nextInt(dirs.length); for (int d = 0; d < dirs.length; d++) { Coord pt = Coord.get(currentPos.x + dirs[d].deltaX, currentPos.y + dirs[d].deltaY); if (gradientMap[pt.x][pt.y] < best) { if (dirs[choice] == Direction.NONE || !path.contains(pt)) { best = gradientMap[pt.x][pt.y]; choice = d; } } } if (best >= gradientMap[currentPos.x][currentPos.y] || physicalMap[currentPos.x + dirs[choice].deltaX][currentPos.y + dirs[choice].deltaY] > FLOOR) { path.clear(); break; } currentPos = currentPos.translate(dirs[choice].deltaX, dirs[choice].deltaY); if (gradientMap[currentPos.x][currentPos.y] == 0) break; path.add(currentPos); frustration++; } frustration = 0; Collections.reverse(path); return new ArrayList<>(path); } /** * Recalculate the Dijkstra map until it reaches a Coord in targets, then returns the first several targets found, * up to limit or less if the map is fully searched without finding enough. * This uses the current measurement. * * @param start the cell to use as the origin for finding the nearest targets * @param limit the maximum number of targets to find before returning * @param targets the Coords that this is trying to find; it will stop once it finds enough (based on limit) * @return the Coords that it found first. */ public ArrayList<Coord> findNearestMultiple(Coord start, int limit, Set<Coord> targets) { if (!initialized) return null; if (targets == null) return null; ArrayList<Coord> found = new ArrayList<>(limit); if (targets.contains(start)) return found; Coord start2 = start; while (physicalMap[start.x][start.y] >= WALL && frustration < 50) { start2 = Coord.get(Math.min(Math.max(1, start.x + rng.nextInt(15) - 7), width - 2), Math.min(Math.max(1, start.y + rng.nextInt(15) - 7), height - 2)); frustration++; } if (closed.containsKey(start2)) closed.remove(start2); gradientMap[start2.x][start2.y] = 0.0; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { if (gradientMap[x][y] > FLOOR && !goals.containsKey(Coord.get(x, y))) closed.put(Coord.get(x, y), physicalMap[x][y]); } } int numAssigned = 1; mappedCount = 1; open.put(start2, 0.0); Direction[] dirs = (measurement == Measurement.MANHATTAN) ? Direction.CARDINALS : Direction.OUTWARDS; while (numAssigned > 0) { // ++iter; numAssigned = 0; for (Map.Entry<Coord, Double> cell : open.entrySet()) { for (int d = 0; d < dirs.length; d++) { Coord adj = cell.getKey().translate(dirs[d].deltaX, dirs[d].deltaY); double h = heuristic(dirs[d]); if (!closed.containsKey(adj) && !open.containsKey(adj) && gradientMap[cell.getKey().x][cell.getKey().y] + h * costMap[adj.x][adj.y] < gradientMap[adj.x][adj.y]) { setFresh(adj, cell.getValue() + h * costMap[adj.x][adj.y]); ++numAssigned; ++mappedCount; if (targets.contains(adj)) { found.add(adj); if (found.size() >= limit) { fresh.clear(); open.clear(); closed.clear(); return found; } } } } } // closed.putAll(open); open = new LinkedHashMap<>(fresh); fresh.clear(); } closed.clear(); open.clear(); return found; } /** * Recalculate the Dijkstra map for a creature that is potentially larger than 1x1 cell and return it. The value of * a cell in the returned Dijkstra map assumes that a creature is square, with a side length equal to the passed * size, that its minimum-x, minimum-y cell is the starting cell, and that any cell with a distance number * represents the distance for the creature's minimum-x, minimum-y cell to reach it. Cells that cannot be entered * by the minimum-x, minimum-y cell because of sizing (such as a floor cell next to a maximum-x and/or maximum-y * wall if size is > 1) will be marked as DARK. Cells that were marked as goals with setGoal will have * a value of 0, the cells adjacent to goals will have a value of 1, and cells progressively further * from goals will have a value equal to the distance from the nearest goal. The exceptions are walls, * which will have a value defined by the WALL constant in this class, and areas that the scan was * unable to reach, which will have a value defined by the DARK constant in this class. (typically, * these areas should not be used to place NPCs or items and should be filled with walls). This uses the * current measurement. * * @param impassable A Set of Position keys representing the locations of enemies or other moving obstacles to a * path that cannot be moved through; this can be null if there are no such obstacles. * @param size The length of one side of a square creature using this to find a path, i.e. 2 for a 2x2 cell * creature. Non-square creatures are not supported because turning is really hard. * @return A 2D double[width][height] using the width and height of what this knows about the physical map. */ public double[][] scan(Set<Coord> impassable, int size) { if (!initialized) return null; if (impassable == null) impassable = new LinkedHashSet<>(); LinkedHashMap<Coord, Double> blocking = new LinkedHashMap<>(impassable.size()); for (Coord pt : impassable) { blocking.put(pt, WALL); for (int x = 0; x < size; x++) { for (int y = 0; y < size; y++) { if (x + y == 0) continue; if (gradientMap[pt.x - x][pt.y - y] <= FLOOR) blocking.put(Coord.get(pt.x - x, pt.y - y), DARK); } } } closed.putAll(blocking); for (Map.Entry<Coord, Double> entry : goals.entrySet()) { if (closed.containsKey(entry.getKey())) closed.remove(entry.getKey()); gradientMap[entry.getKey().x][entry.getKey().y] = entry.getValue(); } mappedCount = goals.size(); double currentLowest = 999000; LinkedHashMap<Coord, Double> lowest = new LinkedHashMap<>(); Coord p = Coord.get(0, 0), temp = Coord.get(0, 0); for (int y = 0; y < height; y++) { I_AM_BECOME_DEATH_DESTROYER_OF_WORLDS: for (int x = 0; x < width; x++) { p = Coord.get(x, y); if (gradientMap[x][y] > FLOOR && !goals.containsKey(p)) { closed.put(p, physicalMap[x][y]); if (gradientMap[x][y] == WALL) { for (int i = 0; i < size; i++) { if (x - i < 0) continue; for (int j = 0; j < size; j++) { temp = Coord.get(x - i, y - j); if (y - j < 0 || closed.containsKey(temp)) continue; if (gradientMap[temp.x][temp.y] <= FLOOR && !goals.containsKey(temp)) closed.put(Coord.get(temp.x, temp.y), DARK); } } } } else if (gradientMap[x][y] < currentLowest && !closed.containsKey(p)) { for (int i = 0; i < size; i++) { if (x + i >= width) continue I_AM_BECOME_DEATH_DESTROYER_OF_WORLDS; for (int j = 0; j < size; j++) { temp = Coord.get(x + i, y + j); if (y + j >= height || closed.containsKey(temp)) continue I_AM_BECOME_DEATH_DESTROYER_OF_WORLDS; } } currentLowest = gradientMap[x][y]; lowest.clear(); lowest.put(Coord.get(x, y), currentLowest); } else if (gradientMap[x][y] == currentLowest && !closed.containsKey(p)) { if (!closed.containsKey(p)) { for (int i = 0; i < size; i++) { if (x + i >= width) continue I_AM_BECOME_DEATH_DESTROYER_OF_WORLDS; for (int j = 0; j < size; j++) { temp = Coord.get(x + i, y + j); if (y + j >= height || closed.containsKey(temp)) continue I_AM_BECOME_DEATH_DESTROYER_OF_WORLDS; } } lowest.put(Coord.get(x, y), currentLowest); } } } } int numAssigned = lowest.size(); open.putAll(lowest); Direction[] dirs = (measurement == Measurement.MANHATTAN) ? Direction.CARDINALS : Direction.OUTWARDS; while (numAssigned > 0) { numAssigned = 0; for (Map.Entry<Coord, Double> cell : open.entrySet()) { for (int d = 0; d < dirs.length; d++) { Coord adj = cell.getKey().translate(dirs[d].deltaX, dirs[d].deltaY); double h = heuristic(dirs[d]); if (!closed.containsKey(adj) && !open.containsKey(adj) && gradientMap[cell.getKey().x][cell.getKey().y] + h * costMap[adj.x][adj.y] < gradientMap[adj.x][adj.y]) { setFresh(adj, cell.getValue() + h * costMap[adj.x][adj.y]); ++numAssigned; ++mappedCount; } } } // closed.putAll(open); open = new LinkedHashMap<>(fresh); fresh.clear(); } closed.clear(); open.clear(); double[][] gradientClone = new double[width][height]; for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { if (gradientMap[x][y] == FLOOR) { gradientMap[x][y] = DARK; } } System.arraycopy(gradientMap[x], 0, gradientClone[x], 0, height); } return gradientClone; } /** * Scans the dungeon using DijkstraMap.scan with the listed goals and start point, and returns a list * of Coord positions (using the current measurement) needed to get closer to the closest reachable * goal. The maximum length of the returned list is given by length; if moving the full length of * the list would place the mover in a position shared by one of the positions in onlyPassable * (which is typically filled with friendly units that can be passed through in multi-tile- * movement scenarios), it will recalculate a move so that it does not pass into that cell. * The keys in impassable should be the positions of enemies and obstacles that cannot be moved * through, and will be ignored if there is a goal overlapping one. * <br> * This caches its result in a member field, path, which can be fetched after finding a path and will change with * each call to a pathfinding method. * @param length the length of the path to calculate * @param impassable a Set of impassable Coord positions that may change (not constant like walls); can be null * @param onlyPassable a Set of Coord positions that this pathfinder cannot end a path occupying (typically allies); can be null * @param start the start of the path, should correspond to the minimum-x, minimum-y position of the pathfinder * @param targets a vararg or array of Coord that this will try to pathfind toward * @return an ArrayList of Coord that will contain the locations of this creature as it goes toward a target. Copy of path. */ public ArrayList<Coord> findPath(int length, Set<Coord> impassable, Set<Coord> onlyPassable, Coord start, Coord... targets) { if (!initialized) return null; path.clear(); LinkedHashSet<Coord> impassable2; if (impassable == null) impassable2 = new LinkedHashSet<>(); else impassable2 = new LinkedHashSet<>(impassable); if (onlyPassable == null) onlyPassable = new LinkedHashSet<>(); resetMap(); for (Coord goal : targets) { setGoal(goal.x, goal.y); } if (goals.isEmpty()) return new ArrayList<>(path); scan(impassable2); Coord currentPos = start; double paidLength = 0.0; while (true) { if (frustration > 500) { path.clear(); break; } double best = gradientMap[currentPos.x][currentPos.y]; final Direction[] dirs = appendDir(shuffleDirs(rng), Direction.NONE); int choice = rng.nextInt(dirs.length); for (int d = 0; d < dirs.length; d++) { Coord pt = Coord.get(currentPos.x + dirs[d].deltaX, currentPos.y + dirs[d].deltaY); if (gradientMap[pt.x][pt.y] < best) { if (dirs[choice] == Direction.NONE || !path.contains(pt)) { best = gradientMap[pt.x][pt.y]; choice = d; } } } if (best >= gradientMap[currentPos.x][currentPos.y] || physicalMap[currentPos.x + dirs[choice].deltaX][currentPos.y + dirs[choice].deltaY] > FLOOR) { path.clear(); break; } currentPos = currentPos.translate(dirs[choice].deltaX, dirs[choice].deltaY); path.add(currentPos); paidLength += costMap[currentPos.x][currentPos.y]; frustration++; if (paidLength > length - 1.0) { if (onlyPassable.contains(currentPos)) { closed.put(currentPos, WALL); impassable2.add(currentPos); return findPath(length, impassable2, onlyPassable, start, targets); } break; } if (gradientMap[currentPos.x][currentPos.y] == 0) break; } frustration = 0; goals.clear(); return new ArrayList<>(path); } /** * Scans the dungeon using DijkstraMap.scan with the listed goals and start point, and returns a list * of Coord positions (using the current measurement) needed to get closer to a goal, until preferredRange is * reached, or further from a goal if the preferredRange has not been met at the current distance. * The maximum length of the returned list is given by moveLength; if moving the full length of * the list would place the mover in a position shared by one of the positions in onlyPassable * (which is typically filled with friendly units that can be passed through in multi-tile- * movement scenarios), it will recalculate a move so that it does not pass into that cell. * The keys in impassable should be the positions of enemies and obstacles that cannot be moved * through, and will be ignored if there is a goal overlapping one. * <br> * This caches its result in a member field, path, which can be fetched after finding a path and will change with * each call to a pathfinding method. * * @param moveLength the length of the path to calculate * @param preferredRange the distance this unit will try to keep from a target * @param los a squidgrid.LOS object if the preferredRange should try to stay in line of sight, or null if LoS * should be disregarded. * @param impassable a Set of impassable Coord positions that may change (not constant like walls); can be null * @param onlyPassable a Set of Coord positions that this pathfinder cannot end a path occupying (typically allies); can be null * @param start the start of the path, should correspond to the minimum-x, minimum-y position of the pathfinder * @param targets a vararg or array of Coord that this will try to pathfind toward * @return an ArrayList of Coord that will contain the locations of this creature as it goes toward a target. Copy of path. */ public ArrayList<Coord> findAttackPath(int moveLength, int preferredRange, LOS los, Set<Coord> impassable, Set<Coord> onlyPassable, Coord start, Coord... targets) { return findAttackPath(moveLength, preferredRange, preferredRange, los, impassable, onlyPassable, start, targets); } /** * Scans the dungeon using DijkstraMap.scan with the listed goals and start point, and returns a list * of Coord positions (using the current measurement) needed to get closer to a goal, until a cell is reached with * a distance from a goal that is at least equal to minPreferredRange and no more than maxPreferredRange, * which may go further from a goal if the minPreferredRange has not been met at the current distance. * The maximum length of the returned list is given by moveLength; if moving the full length of * the list would place the mover in a position shared by one of the positions in onlyPassable * (which is typically filled with friendly units that can be passed through in multi-tile- * movement scenarios), it will recalculate a move so that it does not pass into that cell. * The keys in impassable should be the positions of enemies and obstacles that cannot be moved * through, and will be ignored if there is a goal overlapping one. * <br> * This caches its result in a member field, path, which can be fetched after finding a path and will change with * each call to a pathfinding method. * * @param moveLength the length of the path to calculate * @param minPreferredRange the (inclusive) lower bound of the distance this unit will try to keep from a target * @param maxPreferredRange the (inclusive) upper bound of the distance this unit will try to keep from a target * @param los a squidgrid.LOS object if the preferredRange should try to stay in line of sight, or null if LoS * should be disregarded. * @param impassable a Set of impassable Coord positions that may change (not constant like walls); can be null * @param onlyPassable a Set of Coord positions that this pathfinder cannot end a path occupying (typically allies); can be null * @param start the start of the path, should correspond to the minimum-x, minimum-y position of the pathfinder * @param targets a vararg or array of Coord that this will try to pathfind toward * @return an ArrayList of Coord that will contain the locations of this creature as it goes toward a target. Copy of path. */ public ArrayList<Coord> findAttackPath(int moveLength, int minPreferredRange, int maxPreferredRange, LOS los, Set<Coord> impassable, Set<Coord> onlyPassable, Coord start, Coord... targets) { if (!initialized) return null; if (minPreferredRange < 0) minPreferredRange = 0; if (maxPreferredRange < minPreferredRange) maxPreferredRange = minPreferredRange; double[][] resMap = new double[width][height]; if (los != null) { for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { resMap[x][y] = (physicalMap[x][y] == WALL) ? 1.0 : 0.0; } } } path.clear(); LinkedHashSet<Coord> impassable2; if (impassable == null) impassable2 = new LinkedHashSet<>(); else impassable2 = new LinkedHashSet<>(impassable); if (onlyPassable == null) onlyPassable = new LinkedHashSet<>(); resetMap(); for (Coord goal : targets) { setGoal(goal.x, goal.y); } if (goals.isEmpty()) return new ArrayList<>(path); Measurement mess = measurement; if (measurement == Measurement.EUCLIDEAN) { measurement = Measurement.CHEBYSHEV; } scan(impassable2); goals.clear(); for (int x = 0; x < width; x++) { CELL: for (int y = 0; y < height; y++) { if (gradientMap[x][y] == WALL || gradientMap[x][y] == DARK) continue; if (gradientMap[x][y] >= minPreferredRange && gradientMap[x][y] <= maxPreferredRange) { for (Coord goal : targets) { if (los == null || los.isReachable(resMap, x, y, goal.x, goal.y)) { setGoal(x, y); gradientMap[x][y] = 0; continue CELL; } } gradientMap[x][y] = FLOOR; } else gradientMap[x][y] = FLOOR; } } measurement = mess; scan(impassable2); Coord currentPos = start; double paidLength = 0.0; while (true) { if (frustration > 500) { path.clear(); break; } double best = gradientMap[currentPos.x][currentPos.y]; final Direction[] dirs = appendDir(shuffleDirs(rng), Direction.NONE); int choice = rng.nextInt(dirs.length); for (int d = 0; d < dirs.length; d++) { Coord pt = Coord.get(currentPos.x + dirs[d].deltaX, currentPos.y + dirs[d].deltaY); if (gradientMap[pt.x][pt.y] < best) { if (dirs[choice] == Direction.NONE || !path.contains(pt)) { best = gradientMap[pt.x][pt.y]; choice = d; } } } if (best >= gradientMap[currentPos.x][currentPos.y] || physicalMap[currentPos.x + dirs[choice].deltaX][currentPos.y + dirs[choice].deltaY] > FLOOR) { path.clear(); break; } currentPos = currentPos.translate(dirs[choice].deltaX, dirs[choice].deltaY); path.add(Coord.get(currentPos.x, currentPos.y)); paidLength += costMap[currentPos.x][currentPos.y]; frustration++; if (paidLength > moveLength - 1.0) { if (onlyPassable.contains(currentPos)) { closed.put(currentPos, WALL); impassable2.add(currentPos); return findAttackPath(moveLength, minPreferredRange, maxPreferredRange, los, impassable2, onlyPassable, start, targets); } break; } if (gradientMap[currentPos.x][currentPos.y] == 0) break; } frustration = 0; goals.clear(); return new ArrayList<>(path); } // /** // * Scans the dungeon using DijkstraMap.scan with the listed goals and start point, and returns a list // * of Coord positions (using the current measurement) needed to get closer to a goal, where goals are // * considered valid if they are at a valid range for the given Technique to hit at least one target // * and ideal if that Technique can affect as many targets as possible from a cell that can be moved // * to with at most movelength steps. // * <br> // * The return value of this method is the path to get to a location to attack, but on its own it // * does not tell the user how to perform the attack. It does set the targetMap 2D Coord array field // * so that if your position at the end of the returned path is non-null in targetMap, it will be // * a Coord that can be used as a target position for Technique.apply() . If your position at the end // * of the returned path is null, then an ideal attack position was not reachable by the path. // * <br> // * This needs a char[][] dungeon as an argument because DijkstraMap does not always have a char[][] // * version of the map available to it, and certain AOE implementations that a Technique uses may // * need a char[][] specifically to determine what they affect. // * <br> // * The maximum length of the returned list is given by moveLength; if moving the full length of // * the list would place the mover in a position shared by one of the positions in allies // * (which is typically filled with friendly units that can be passed through in multi-tile- // * movement scenarios, and is also used considered an undesirable thing to affect for the Technique), // * it will recalculate a move so that it does not pass into that cell. // * <br> // * The keys in impassable should be the positions of enemies and obstacles that cannot be moved // * through, and will be ignored if there is a target overlapping one. // * <br> // * This caches its result in a member field, path, which can be fetched after finding a path and will change with // * each call to a pathfinding method. // * // * @param moveLength the maximum distance to try to pathfind out to; if a spot to use a Technique can be found // * while moving no more than this distance, then the targetMap field in this object will have a // * target Coord that is ideal for the given Technique at the x, y indices corresponding to the // * last Coord in the returned path. // * @param tech a Technique that we will try to find an ideal place to use, and/or a path toward that place. // * @param dungeon a char 2D array with '#' for walls. // * @param los a squidgrid.LOS object if the preferred range should try to stay in line of sight, or null if LoS // * should be disregarded. // * @param impassable locations of enemies or mobile hazards/obstacles that aren't in the map as walls // * @param allies called onlyPassable in other methods, here it also represents allies for Technique things // * @param start the Coord the pathfinder starts at. // * @param targets a Set of Coord, not an array of Coord or variable argument list as in other methods. // * @return an ArrayList of Coord that represents a path to travel to get to an ideal place to use tech. Copy of path. // */ // public ArrayList<Coord> findTechniquePath(int moveLength, Technique tech, char[][] dungeon, LOS los, // Set<Coord> impassable, Set<Coord> allies, Coord start, Set<Coord> targets) { // if (!initialized) return null; // tech.setMap(dungeon); // double[][] resMap = new double[width][height]; // double[][] worthMap = new double[width][height]; // double[][] userDistanceMap; // double paidLength = 0.0; // // LinkedHashSet<Coord> friends; // // // for (int x = 0; x < width; x++) { // for (int y = 0; y < height; y++) { // resMap[x][y] = (physicalMap[x][y] == WALL) ? 1.0 : 0.0; // targetMap[x][y] = null; // } // } // // path.clear(); // if (targets == null || targets.size() == 0) // return new ArrayList<>(path); // LinkedHashSet<Coord> impassable2; // if (impassable == null) // impassable2 = new LinkedHashSet<>(); // else // impassable2 = new LinkedHashSet<>(impassable); // // if (allies == null) // friends = new LinkedHashSet<>(); // else { // friends = new LinkedHashSet<>(allies); // friends.remove(start); // } // // resetMap(); // setGoal(start); // userDistanceMap = scan(impassable2); // clearGoals(); // resetMap(); // for (Coord goal : targets) { // setGoal(goal.x, goal.y); // } // if (goals.isEmpty()) // return new ArrayList<>(path); // // Measurement mess = measurement; // /* // if(measurement == Measurement.EUCLIDEAN) // { // measurement = Measurement.CHEBYSHEV; // } // */ // scan(impassable2); // clearGoals(); // // Coord tempPt = Coord.get(0, 0); // LinkedHashMap<Coord, ArrayList<Coord>> ideal; // // generate an array of the single best location to attack when you are in a given cell. // for (int x = 0; x < width; x++) { // CELL: // for (int y = 0; y < height; y++) { // tempPt = Coord.get(x, y); // if (gradientMap[x][y] == WALL || gradientMap[x][y] == DARK || userDistanceMap[x][y] > moveLength * 2.0) // continue; // if (gradientMap[x][y] >= tech.aoe.getMinRange() && gradientMap[x][y] <= tech.aoe.getMaxRange()) { // for (Coord tgt : targets) { // if (los == null || los.isReachable(resMap, x, y, tgt.x, tgt.y)) { // ideal = tech.idealLocations(tempPt, targets, friends); // // this is weird but it saves the trouble of getting the iterator and checking hasNext() . // for (Map.Entry<Coord, ArrayList<Coord>> ip : ideal.entrySet()) { // targetMap[x][y] = ip.getKey(); // worthMap[x][y] = ip.getValue().size(); // setGoal(x, y); // gradientMap[x][y] = 0; // break; // } // continue CELL; // } // } // gradientMap[x][y] = FLOOR; // } else // gradientMap[x][y] = FLOOR; // } // } // scan(impassable2); // // double currentDistance = gradientMap[start.x][start.y]; // if (currentDistance <= moveLength) { // Coord[] g_arr = new Coord[goals.size()]; // g_arr = goals.keySet().toArray(g_arr); // // goals.clear(); // setGoal(start); // scan(impassable2); // goals.clear(); // gradientMap[start.x][start.y] = moveLength; // // for (Coord g : g_arr) { // if (gradientMap[g.x][g.y] <= moveLength && worthMap[g.x][g.y] > 0) { // goals.put(g, 0.0 - worthMap[g.x][g.y]); // } // } // resetMap(); // /* for(Coord g : goals.keySet()) // { // gradientMap[g.x][g.y] = 0.0 - worthMap[g.x][g.y]; // }*/ // scan(impassable2); // // } // // measurement = mess; // // Coord currentPos = Coord.get(start.x, start.y); // while (true) { // if (frustration > 500) { // path.clear(); // break; // } // double best = gradientMap[currentPos.x][currentPos.y]; // final Direction[] dirs = appendDir(shuffleDirs(rng), Direction.NONE); // int choice = rng.nextInt(dirs.length); // // for (int d = 0; d < dirs.length; d++) { // Coord pt = Coord.get(currentPos.x + dirs[d].deltaX, currentPos.y + dirs[d].deltaY); // if (gradientMap[pt.x][pt.y] < best) { // if (dirs[choice] == Direction.NONE || !path.contains(pt)) { // best = gradientMap[pt.x][pt.y]; // choice = d; // } // } // } // if (best >= gradientMap[currentPos.x][currentPos.y]) { // if (friends.contains(currentPos)) { // closed.put(currentPos, WALL); // impassable2.add(currentPos); // return findTechniquePath(moveLength, tech, dungeon, los, impassable2, // friends, start, targets); // } // break; // } // if (best > gradientMap[start.x][start.y] || physicalMap[currentPos.x + dirs[choice].deltaX][currentPos.y + dirs[choice].deltaY] > FLOOR) { // path.clear(); // break; // } // currentPos = currentPos.translate(dirs[choice].deltaX, dirs[choice].deltaY); // path.add(currentPos); // paidLength += costMap[currentPos.x][currentPos.y]; // frustration++; // if (paidLength > moveLength - 1.0) { // if (friends.contains(currentPos)) { // closed.put(currentPos, WALL); // impassable2.add(currentPos); // return findTechniquePath(moveLength, tech, dungeon, los, impassable2, // friends, start, targets); // } // break; // } //// if(gradientMap[currentPos.x][currentPos.y] == 0) //// break; // } // frustration = 0; // goals.clear(); // return new ArrayList<>(path); // } /** * Scans the dungeon using DijkstraMap.scan with the listed goals and start point, and returns a list * of Coord positions (using the current measurement) needed to get closer to a goal, until preferredRange is * reached, or further from a goal if the preferredRange has not been met at the current distance. * The maximum length of the returned list is given by moveLength; if moving the full length of * the list would place the mover in a position shared by one of the positions in onlyPassable * (which is typically filled with friendly units that can be passed through in multi-tile- * movement scenarios), it will recalculate a move so that it does not pass into that cell. * The keys in impassable should be the positions of enemies and obstacles that cannot be moved * through, and will be ignored if there is a goal overlapping one. * <br> * This caches its result in a member field, path, which can be fetched after finding a path and will change with * each call to a pathfinding method. * * @param moveLength the length of the path to calculate * @param preferredRange the distance this unit will try to keep from a target * @param cache a FOVCache that has completed its calculations, and will be used for LOS work, may be null * @param impassable a Set of impassable Coord positions that may change (not constant like walls); can be null * @param onlyPassable a Set of Coord positions that this pathfinder cannot end a path occupying (typically allies); can be null * @param start the start of the path, should correspond to the minimum-x, minimum-y position of the pathfinder * @param targets a vararg or array of Coord that this will try to pathfind toward * @return an ArrayList of Coord that will contain the locations of this creature as it goes toward a target. Copy of path. */ @GwtIncompatible public ArrayList<Coord> findAttackPath(int moveLength, int preferredRange, FOVCache cache, Set<Coord> impassable, Set<Coord> onlyPassable, Coord start, Coord... targets) { return findAttackPath(moveLength, preferredRange, preferredRange, cache, impassable, onlyPassable, start, targets); } /** * Scans the dungeon using DijkstraMap.scan with the listed goals and start point, and returns a list * of Coord positions (using the current measurement) needed to get closer to a goal, until a cell is reached with * a distance from a goal that is at least equal to minPreferredRange and no more than maxPreferredRange, * which may go further from a goal if the minPreferredRange has not been met at the current distance. * The maximum length of the returned list is given by moveLength; if moving the full length of * the list would place the mover in a position shared by one of the positions in onlyPassable * (which is typically filled with friendly units that can be passed through in multi-tile- * movement scenarios), it will recalculate a move so that it does not pass into that cell. * The keys in impassable should be the positions of enemies and obstacles that cannot be moved * through, and will be ignored if there is a goal overlapping one. * <br> * This caches its result in a member field, path, which can be fetched after finding a path and will change with * each call to a pathfinding method. * * @param moveLength the length of the path to calculate * @param minPreferredRange the (inclusive) lower bound of the distance this unit will try to keep from a target * @param maxPreferredRange the (inclusive) upper bound of the distance this unit will try to keep from a target * @param cache a FOVCache that has completed its calculations, and will be used for LOS work, may be null * @param impassable a Set of impassable Coord positions that may change (not constant like walls); can be null * @param onlyPassable a Set of Coord positions that this pathfinder cannot end a path occupying (typically allies); can be null * @param start the start of the path, should correspond to the minimum-x, minimum-y position of the pathfinder * @param targets a vararg or array of Coord that this will try to pathfind toward * @return an ArrayList of Coord that will contain the locations of this creature as it goes toward a target. Copy of path. */ @GwtIncompatible public ArrayList<Coord> findAttackPath(int moveLength, int minPreferredRange, int maxPreferredRange, FOVCache cache, Set<Coord> impassable, Set<Coord> onlyPassable, Coord start, Coord... targets) { if (!initialized) return null; if (minPreferredRange < 0) minPreferredRange = 0; if (maxPreferredRange < minPreferredRange) maxPreferredRange = minPreferredRange; path.clear(); LinkedHashSet<Coord> impassable2; if (impassable == null) impassable2 = new LinkedHashSet<>(); else impassable2 = new LinkedHashSet<>(impassable); if (onlyPassable == null) onlyPassable = new LinkedHashSet<>(); resetMap(); for (Coord goal : targets) { setGoal(goal.x, goal.y); } if (goals.isEmpty()) return new ArrayList<>(path); Measurement mess = measurement; if (measurement == Measurement.EUCLIDEAN) { measurement = Measurement.CHEBYSHEV; } scan(impassable2); goals.clear(); for (int x = 0; x < width; x++) { CELL: for (int y = 0; y < height; y++) { if (gradientMap[x][y] == WALL || gradientMap[x][y] == DARK) continue; if (gradientMap[x][y] >= minPreferredRange && gradientMap[x][y] <= maxPreferredRange) { for (Coord goal : targets) { if (cache == null || cache.queryLOS(x, y, goal.x, goal.y)) { setGoal(x, y); gradientMap[x][y] = 0; continue CELL; } } gradientMap[x][y] = FLOOR; } else gradientMap[x][y] = FLOOR; } } measurement = mess; scan(impassable2); Coord currentPos = start; double paidLength = 0.0; while (true) { if (frustration > 500) { path.clear(); break; } double best = gradientMap[currentPos.x][currentPos.y]; final Direction[] dirs = appendDir(shuffleDirs(rng), Direction.NONE); int choice = rng.nextInt(dirs.length); for (int d = 0; d < dirs.length; d++) { Coord pt = Coord.get(currentPos.x + dirs[d].deltaX, currentPos.y + dirs[d].deltaY); if (gradientMap[pt.x][pt.y] < best) { if (dirs[choice] == Direction.NONE || !path.contains(pt)) { best = gradientMap[pt.x][pt.y]; choice = d; } } } if (best >= gradientMap[currentPos.x][currentPos.y] || physicalMap[currentPos.x + dirs[choice].deltaX][currentPos.y + dirs[choice].deltaY] > FLOOR) { path.clear(); break; } currentPos = currentPos.translate(dirs[choice].deltaX, dirs[choice].deltaY); path.add(Coord.get(currentPos.x, currentPos.y)); paidLength += costMap[currentPos.x][currentPos.y]; frustration++; if (paidLength > moveLength - 1.0) { if (onlyPassable.contains(currentPos)) { closed.put(currentPos, WALL); impassable2.add(currentPos); return findAttackPath(moveLength, minPreferredRange, maxPreferredRange, cache, impassable2, onlyPassable, start, targets); } break; } if (gradientMap[currentPos.x][currentPos.y] == 0) break; } frustration = 0; goals.clear(); return new ArrayList<>(path); } /** * Scans the dungeon using DijkstraMap.scan with the listed goals and start point, and returns a list of Coord * positions (using the current measurement) needed to get closer to a goal while staying in areas that none of the * given threats are able to see (which should prevent them from attacking), until a cell is reached with * a distance from a goal that is at least equal to minPreferredRange and no more than maxPreferredRange, * which may go further from a goal if the minPreferredRange has not been met at the current distance. * <p/> * Essentially, this method is for finding ways to approach enemies who can attack at range without constantly being * attacked by them. You are expected to call deteriorate() and possible relax() at points when a position becomes * riskier to stay at (then you call deteriorate()) or a position starts to seem like a safer place (then, relax()). * <p/> * The maximum length of the returned list is given by moveLength; if moving the full length of * the list would place the mover in a position shared by one of the positions in onlyPassable * (which is typically filled with friendly units that can be passed through in multi-tile- * movement scenarios), it will recalculate a move so that it does not pass into that cell. * The keys in impassable should be the positions of enemies and obstacles that cannot be moved * through, and will be ignored if there is a goal overlapping one. * <br> * This caches its result in a member field, path, which can be fetched after finding a path and will change with * each call to a pathfinding method. * * @param moveLength the length of the path to calculate * @param minPreferredRange the (inclusive) lower bound of the distance this unit will try to keep from a target * @param maxPreferredRange the (inclusive) upper bound of the distance this unit will try to keep from a target * @param coverPreference positive, typically around 1.0, higher numbers make the pathfinder stay behind cover * more, lower numbers make the pathfinder move more aggressively toward targets * @param impassable a Set of impassable Coord positions that may change (not constant like walls); can be null * @param onlyPassable a Set of Coord positions that this pathfinder cannot end a path occupying (typically allies); can be null * @param threats a List of Threat objects that store a position, min and max threatening distance * @param start the start of the path, should correspond to the minimum-x, minimum-y position of the pathfinder * @param targets a vararg or array of Coord that this will try to pathfind toward * @return an ArrayList of Coord that will contain the locations of this creature as it goes toward a target. Copy of path. */ public ArrayList<Coord> findCoveredAttackPath(int moveLength, int minPreferredRange, int maxPreferredRange, double coverPreference, Set<Coord> impassable, Set<Coord> onlyPassable, List<Threat> threats, Coord start, Coord... targets) { if (!initialized) return null; if (minPreferredRange < 0) minPreferredRange = 0; if (maxPreferredRange < minPreferredRange) maxPreferredRange = minPreferredRange; double[][] resMap = new double[width][height]; for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { resMap[x][y] = (physicalMap[x][y] == WALL) ? 1.0 : 0.0; } } path = new ArrayList<Coord>(); LinkedHashSet<Coord> impassable2; if (impassable == null) impassable2 = new LinkedHashSet<Coord>(); else impassable2 = new LinkedHashSet<Coord>(impassable); if (onlyPassable == null) onlyPassable = new LinkedHashSet<Coord>(); resetMap(); for (Coord goal : targets) { setGoal(goal.x, goal.y); } if (goals.isEmpty()) return new ArrayList<>(path); Measurement mess = measurement; if (measurement == Measurement.EUCLIDEAN) { measurement = Measurement.CHEBYSHEV; } scan(impassable2); goals.clear(); LinkedHashMap<Coord, Double> cachedGoals = new LinkedHashMap<Coord, Double>(); for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { if (gradientMap[x][y] == WALL || gradientMap[x][y] == DARK) continue; if (gradientMap[x][y] >= minPreferredRange && gradientMap[x][y] <= maxPreferredRange) { gradientMap[x][y] = 0.001 * (maxPreferredRange - gradientMap[x][y]); cachedGoals.put(Coord.get(x, y), gradientMap[x][y]); } else gradientMap[x][y] = FLOOR; } } measurement = mess; double[][] storedScan = scan(impassable2); if(storedScan[start.x][start.y] > moveLength) { clearGoals(); resetMap(); double[][] seen; short[] packed = CoordPacker.ALL_WALL, floors = CoordPacker.pack(physicalMap, FLOOR), tempPacked; for (Threat t : threats) { packed = CoordPacker.unionPacked( packed, CoordPacker.reachable(floors, CoordPacker.packOne(t.position), t.reach)); } short[] unseen = CoordPacker.differencePacked(CoordPacker.rectangle(width, height), CoordPacker.expand(packed, 1, width, height)); Coord[] safe = CoordPacker.allPacked(unseen); for (int i = 0; i < safe.length; i++) { setGoal(safe[i]); } safetyMap = scan(impassable2); for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { if (storedScan[x][y] < FLOOR) { gradientMap[x][y] = storedScan[x][y] * 2.0 * (moveLength+1) + safetyMap[x][y] * coverPreference; } //safeMap[x][y] = Math.pow(safeMap[x][y] + safetyMap[x][y], 1.5); } } goals = cachedGoals; scan(impassable2); //gradientMap = storedScan; } Coord currentPos = start; double paidLength = 0.0; while (true) { if (frustration > 500) { path = new ArrayList<Coord>(); break; } double best = gradientMap[currentPos.x][currentPos.y]; final Direction[] dirs = appendDir(shuffleDirs(rng), Direction.NONE); int choice = rng.nextInt(dirs.length); for (int d = 0; d < dirs.length; d++) { Coord pt = Coord.get(currentPos.x + dirs[d].deltaX, currentPos.y + dirs[d].deltaY); if (gradientMap[pt.x][pt.y] < best) { if (dirs[choice] == Direction.NONE || !path.contains(pt)) { best = gradientMap[pt.x][pt.y]; choice = d; } } } if (best >= gradientMap[currentPos.x][currentPos.y] || physicalMap[currentPos.x + dirs[choice].deltaX][currentPos.y + dirs[choice].deltaY] > FLOOR) { break; } currentPos = currentPos.translate(dirs[choice]); path.add(Coord.get(currentPos.x, currentPos.y)); paidLength += costMap[currentPos.x][currentPos.y]; frustration++; if(paidLength > moveLength - 1.0) break; if (gradientMap[currentPos.x][currentPos.y] == 0) break; } goals.clear(); if (onlyPassable.contains(currentPos) || impassable2.contains(currentPos)) { closed.put(currentPos, WALL); impassable2.add(currentPos); return findCoveredAttackPath(moveLength, minPreferredRange, maxPreferredRange, coverPreference, impassable2, onlyPassable, threats, start, targets); } frustration = 0; return new ArrayList<>(path); } /** * Scans the dungeon using DijkstraMap.scan with the listed goals and start point, and returns a list of Coord * positions (using the current measurement) needed to get closer to a goal while staying in areas that none of the * given threats are able to see (which should prevent them from attacking), until a cell is reached with * a distance from a goal that is at equal to preferredRange, * which may go further from a goal if the preferredRange has not been met at the current distance. * <p/> * Essentially, this method is for finding ways to approach enemies who can attack at range without constantly being * attacked by them. You are expected to call deteriorate() and possible relax() at points when a position becomes * riskier to stay at (then you call deteriorate()) or a position starts to seem like a safer place (then, relax()). * <p/> * The maximum length of the returned list is given by moveLength; if moving the full length of * the list would place the mover in a position shared by one of the positions in onlyPassable * (which is typically filled with friendly units that can be passed through in multi-tile- * movement scenarios), it will recalculate a move so that it does not pass into that cell. * The keys in impassable should be the positions of enemies and obstacles that cannot be moved * through, and will be ignored if there is a goal overlapping one. * <br> * This caches its result in a member field, path, which can be fetched after finding a path and will change with * each call to a pathfinding method. * * @param moveLength the length of the path to calculate * @param preferredRange the distance this unit will try to keep from a target * @param fov a FOV that will be used for LOS work, must not be null * @param seekDistantGoals true if this should pathfind to goals that it cannot see, false if FOV restricts pathfinding * @param impassable a Set of impassable Coord positions that may change (not constant like walls); can be null * @param onlyPassable a Set of Coord positions that this pathfinder cannot end a path occupying (typically allies); can be null * @param threats a List of Threat objects that store a position, min and max threatening distance * @param start the start of the path, should correspond to the minimum-x, minimum-y position of the pathfinder * @param targets a vararg or array of Coord that this will try to pathfind toward * @return an ArrayList of Coord that will contain the locations of this creature as it goes toward a target. Copy of path. */ public ArrayList<Coord> findCoveredAttackPath(int moveLength, int preferredRange, double coverPreference, FOV fov, boolean seekDistantGoals, Set<Coord> impassable, Set<Coord> onlyPassable, List<Threat> threats, Coord start, Coord... targets) { return findCoveredAttackPath(moveLength, preferredRange, preferredRange, coverPreference, fov, seekDistantGoals, impassable, onlyPassable, threats, start, targets); } /** * Scans the dungeon using DijkstraMap.scan with the listed goals and start point, and returns a list of Coord * positions (using the current measurement) needed to get closer to a goal while staying in areas that none of the * given threats are able to see (which should prevent them from attacking), until a cell is reached with * a distance from a goal that is at least equal to minPreferredRange and no more than maxPreferredRange, * which may go further from a goal if the minPreferredRange has not been met at the current distance. * <p/> * Essentially, this method is for finding ways to approach enemies who can attack at range without constantly being * attacked by them. You are expected to call deteriorate() and possible relax() at points when a position becomes * riskier to stay at (then you call deteriorate()) or a position starts to seem like a safer place (then, relax()). * <p/> * The maximum length of the returned list is given by moveLength; if moving the full length of * the list would place the mover in a position shared by one of the positions in onlyPassable * (which is typically filled with friendly units that can be passed through in multi-tile- * movement scenarios), it will recalculate a move so that it does not pass into that cell. * The keys in impassable should be the positions of enemies and obstacles that cannot be moved * through, and will be ignored if there is a goal overlapping one. * <br> * This caches its result in a member field, path, which can be fetched after finding a path and will change with * each call to a pathfinding method. * * @param moveLength the length of the path to calculate * @param minPreferredRange the (inclusive) lower bound of the distance this unit will try to keep from a target * @param maxPreferredRange the (inclusive) upper bound of the distance this unit will try to keep from a target * @param coverPreference positive, typically around 1.0, higher numbers make the pathfinder stay behind cover * more, lower numbers make the pathfinder move more aggressively toward targets * @param fov a FOV that will be used for LOS work, MUST NOT be null * @param seekDistantGoals true if this should pathfind to goals that it cannot see, false if FOV restricts pathfinding * @param impassable a Set of impassable Coord positions that may change (not constant like walls); can be null * @param onlyPassable a Set of Coord positions that this pathfinder cannot end a path occupying (typically allies); can be null * @param threats a List of Threat objects that store a position, min and max threatening distance * @param start the start of the path, should correspond to the minimum-x, minimum-y position of the pathfinder * @param targets a vararg or array of Coord that this will try to pathfind toward * @return an ArrayList of Coord that will contain the locations of this creature as it goes toward a target. Copy of path. */ public ArrayList<Coord> findCoveredAttackPath(int moveLength, int minPreferredRange, int maxPreferredRange, double coverPreference, FOV fov, boolean seekDistantGoals, Set<Coord> impassable, Set<Coord> onlyPassable, List<Threat> threats, Coord start, Coord... targets) { if (!initialized) return null; if(fov == null) { return findCoveredAttackPath(moveLength, minPreferredRange, maxPreferredRange, coverPreference, impassable, onlyPassable, threats, start, targets); } if (minPreferredRange < 0) minPreferredRange = 0; if (maxPreferredRange < minPreferredRange) maxPreferredRange = minPreferredRange; double[][] resMap = new double[width][height]; for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { resMap[x][y] = (physicalMap[x][y] == WALL) ? 1.0 : 0.0; } } path = new ArrayList<Coord>(); LinkedHashSet<Coord> impassable2; if (impassable == null) impassable2 = new LinkedHashSet<Coord>(); else impassable2 = new LinkedHashSet<Coord>(impassable); if (onlyPassable == null) onlyPassable = new LinkedHashSet<Coord>(); resetMap(); for (Coord goal : targets) { setGoal(goal.x, goal.y); } if (goals.isEmpty()) return new ArrayList<>(path); Measurement mess = measurement; if (measurement == Measurement.EUCLIDEAN) { measurement = Measurement.CHEBYSHEV; } scan(impassable2); goals.clear(); LinkedHashMap<Coord, Double> cachedGoals = new LinkedHashMap<Coord, Double>(); for (int x = 0; x < width; x++) { CELL: for (int y = 0; y < height; y++) { if (gradientMap[x][y] == WALL || gradientMap[x][y] == DARK) continue; if (gradientMap[x][y] >= minPreferredRange && gradientMap[x][y] <= maxPreferredRange) { double[][] results = new double[width][height]; if (!seekDistantGoals) results = fov.calculateFOV(resMap, x, y, maxPreferredRange, findRadius(mess)); for (Coord goal : targets) { if (seekDistantGoals || results[goal.x][goal.y] > 0.0) { gradientMap[x][y] = 0.001 * (maxPreferredRange - gradientMap[x][y]); cachedGoals.put(Coord.get(x, y), gradientMap[x][y]); continue CELL; } } gradientMap[x][y] = FLOOR; } else gradientMap[x][y] = FLOOR; } } measurement = mess; double[][] storedScan = scan(impassable2); if(storedScan[start.x][start.y] > moveLength) { clearGoals(); resetMap(); double[][] seen; short[] packed = CoordPacker.ALL_WALL, tempPacked; for (Threat t : threats) { seen = fov.calculateFOV(resMap, t.position.x, t.position.y, t.reach.maxDistance, findRadius(measurement)); tempPacked = CoordPacker.pack(seen); if (t.reach.minDistance > 0) { seen = fov.calculateFOV(resMap, t.position.x, t.position.y, t.reach.minDistance, findRadius(measurement)); tempPacked = CoordPacker.differencePacked(tempPacked, CoordPacker.pack(seen)); } packed = CoordPacker.unionPacked(packed, tempPacked); } short[] unseen = CoordPacker.differencePacked(CoordPacker.rectangle(width, height), CoordPacker.expand(packed, 1, width, height)); Coord[] safe = CoordPacker.allPacked(unseen); for (int i = 0; i < safe.length; i++) { setGoal(safe[i]); } safetyMap = scan(impassable2); for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { if (storedScan[x][y] < FLOOR) { gradientMap[x][y] = storedScan[x][y] * 2.0 * (moveLength+1) + safetyMap[x][y] * coverPreference; } //safeMap[x][y] = Math.pow(safeMap[x][y] + safetyMap[x][y], 1.5); } } goals = cachedGoals; scan(impassable2); //gradientMap = storedScan; } Coord currentPos = start; double paidLength = 0.0; while (true) { if (frustration > 500) { path = new ArrayList<Coord>(); break; } double best = gradientMap[currentPos.x][currentPos.y]; final Direction[] dirs = appendDir(shuffleDirs(rng), Direction.NONE); int choice = rng.nextInt(dirs.length); for (int d = 0; d < dirs.length; d++) { Coord pt = Coord.get(currentPos.x + dirs[d].deltaX, currentPos.y + dirs[d].deltaY); if (gradientMap[pt.x][pt.y] < best) { if (dirs[choice] == Direction.NONE || !path.contains(pt)) { best = gradientMap[pt.x][pt.y]; choice = d; } } } if (best >= gradientMap[currentPos.x][currentPos.y] || physicalMap[currentPos.x + dirs[choice].deltaX][currentPos.y + dirs[choice].deltaY] > FLOOR) { break; } currentPos = currentPos.translate(dirs[choice]); path.add(Coord.get(currentPos.x, currentPos.y)); paidLength += costMap[currentPos.x][currentPos.y]; frustration++; if(paidLength > moveLength - 1.0) break; if (gradientMap[currentPos.x][currentPos.y] == 0) break; } goals.clear(); if (onlyPassable.contains(currentPos) || impassable2.contains(currentPos)) { closed.put(currentPos, WALL); impassable2.add(currentPos); return findCoveredAttackPath(moveLength, minPreferredRange, maxPreferredRange, coverPreference, fov, seekDistantGoals, impassable2, onlyPassable, threats, start, targets); } frustration = 0; return new ArrayList<>(path); } /** * Scans the dungeon using DijkstraMap.scan with the listed goals and start point, and returns a list of Coord * positions (using the current measurement) needed to get closer to a goal while staying in areas that none of the * given threats are able to see (which should prevent them from attacking), until a cell is reached with * a distance from a goal that is at equal to preferredRange, * which may go further from a goal if the preferredRange has not been met at the current distance. * <p/> * Essentially, this method is for finding ways to approach enemies who can attack at range without constantly being * attacked by them. You are expected to call deteriorate() and possible relax() at points when a position becomes * riskier to stay at (then you call deteriorate()) or a position starts to seem like a safer place (then, relax()). * <p/> * The maximum length of the returned list is given by moveLength; if moving the full length of * the list would place the mover in a position shared by one of the positions in onlyPassable * (which is typically filled with friendly units that can be passed through in multi-tile- * movement scenarios), it will recalculate a move so that it does not pass into that cell. * The keys in impassable should be the positions of enemies and obstacles that cannot be moved * through, and will be ignored if there is a goal overlapping one. * <br> * This caches its result in a member field, path, which can be fetched after finding a path and will change with * each call to a pathfinding method. * * @param moveLength the length of the path to calculate * @param preferredRange the distance this unit will try to keep from a target * @param fov a FOVCache that has completed its calculations, and will be used for LOS work, may be null * @param seekDistantGoals true if this should pathfind to goals that it cannot see, false if FOV restricts pathfinding * @param impassable a Set of impassable Coord positions that may change (not constant like walls); can be null * @param onlyPassable a Set of Coord positions that this pathfinder cannot end a path occupying (typically allies); can be null * @param threats a List of Threat objects that store a position, min and max threatening distance * @param start the start of the path, should correspond to the minimum-x, minimum-y position of the pathfinder * @param targets a vararg or array of Coord that this will try to pathfind toward * @return an ArrayList of Coord that will contain the locations of this creature as it goes toward a target. Copy of path. */ @GwtIncompatible public ArrayList<Coord> findCoveredAttackPath(int moveLength, int preferredRange, double coverPreference, FOVCache fov, boolean seekDistantGoals, Set<Coord> impassable, Set<Coord> onlyPassable, List<Threat> threats, Coord start, Coord... targets) { return findCoveredAttackPath(moveLength, preferredRange, preferredRange, coverPreference, fov, seekDistantGoals, impassable, onlyPassable, threats, start, targets); } /** * Scans the dungeon using DijkstraMap.scan with the listed goals and start point, and returns a list of Coord * positions (using the current measurement) needed to get closer to a goal while staying in areas that none of the * given threats are able to see (which should prevent them from attacking), until a cell is reached with * a distance from a goal that is at least equal to minPreferredRange and no more than maxPreferredRange, * which may go further from a goal if the minPreferredRange has not been met at the current distance. * <p/> * Essentially, this method is for finding ways to approach enemies who can attack at range without constantly being * attacked by them. You are expected to call deteriorate() and possible relax() at points when a position becomes * riskier to stay at (then you call deteriorate()) or a position starts to seem like a safer place (then, relax()). * <p/> * The maximum length of the returned list is given by moveLength; if moving the full length of * the list would place the mover in a position shared by one of the positions in onlyPassable * (which is typically filled with friendly units that can be passed through in multi-tile- * movement scenarios), it will recalculate a move so that it does not pass into that cell. * The keys in impassable should be the positions of enemies and obstacles that cannot be moved * through, and will be ignored if there is a goal overlapping one. * <br> * This caches its result in a member field, path, which can be fetched after finding a path and will change with * each call to a pathfinding method. * * @param moveLength the length of the path to calculate * @param minPreferredRange the (inclusive) lower bound of the distance this unit will try to keep from a target * @param maxPreferredRange the (inclusive) upper bound of the distance this unit will try to keep from a target * @param coverPreference positive, typically around 1.0, higher numbers make the pathfinder stay behind cover * more, lower numbers make the pathfinder move more aggressively toward targets * @param fov a FOVCache that has completed its calculations, and will be used for LOS work * @param seekDistantGoals true if this should pathfind to goals that it cannot see, false if FOV restricts pathfinding * @param impassable a Set of impassable Coord positions that may change (not constant like walls); can be null * @param onlyPassable a Set of Coord positions that this pathfinder cannot end a path occupying (typically allies); can be null * @param threats a List of Threat objects that store a position, min and max threatening distance * @param start the start of the path, should correspond to the minimum-x, minimum-y position of the pathfinder * @param targets a vararg or array of Coord that this will try to pathfind toward * @return an ArrayList of Coord that will contain the locations of this creature as it goes toward a target. Copy of path. */ @GwtIncompatible public ArrayList<Coord> findCoveredAttackPath(int moveLength, int minPreferredRange, int maxPreferredRange, double coverPreference, FOVCache fov, boolean seekDistantGoals, Set<Coord> impassable, Set<Coord> onlyPassable, List<Threat> threats, Coord start, Coord... targets) { if (!initialized) return null; if(fov == null) { return findCoveredAttackPath(moveLength, minPreferredRange, maxPreferredRange, coverPreference, impassable, onlyPassable, threats, start, targets); } if (minPreferredRange < 0) minPreferredRange = 0; if (maxPreferredRange < minPreferredRange) maxPreferredRange = minPreferredRange; double[][] resMap = new double[width][height]; for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { resMap[x][y] = (physicalMap[x][y] == WALL) ? 1.0 : 0.0; } } path.clear(); LinkedHashSet<Coord> impassable2; if (impassable == null) impassable2 = new LinkedHashSet<>(); else impassable2 = new LinkedHashSet<>(impassable); if (onlyPassable == null) onlyPassable = new LinkedHashSet<>(); resetMap(); for (Coord goal : targets) { setGoal(goal.x, goal.y); } if (goals.isEmpty()) return new ArrayList<>(path); Measurement mess = measurement; if (measurement == Measurement.EUCLIDEAN) { measurement = Measurement.CHEBYSHEV; } scan(impassable2); goals.clear(); LinkedHashMap<Coord, Double> cachedGoals = new LinkedHashMap<>(); for (int x = 0; x < width; x++) { CELL: for (int y = 0; y < height; y++) { if (gradientMap[x][y] == WALL || gradientMap[x][y] == DARK) continue; if (gradientMap[x][y] >= minPreferredRange && gradientMap[x][y] <= maxPreferredRange) { double[][] results = new double[width][height]; if (!seekDistantGoals) results = fov.calculateFOV(resMap, x, y, maxPreferredRange, findRadius(mess)); for (Coord goal : targets) { if (seekDistantGoals || results[goal.x][goal.y] > 0.0) { gradientMap[x][y] = 0.001 * (maxPreferredRange - gradientMap[x][y]); cachedGoals.put(Coord.get(x, y), gradientMap[x][y]); continue CELL; } } gradientMap[x][y] = FLOOR; } else gradientMap[x][y] = FLOOR; } } measurement = mess; double[][] storedScan = scan(impassable2); if(storedScan[start.x][start.y] > moveLength) { clearGoals(); resetMap(); double[][] seen; short[] packed = CoordPacker.ALL_WALL, tempPacked; for (Threat t : threats) { tempPacked = fov.getCacheEntry(t.position.x, t.position.y, t.reach.maxDistance); if (t.reach.minDistance > 0) { tempPacked = CoordPacker.differencePacked(tempPacked, fov.getCacheEntry(t.position.x, t.position.y, t.reach.minDistance)); } packed = CoordPacker.unionPacked(packed, tempPacked); } short[] unseen = CoordPacker.differencePacked(CoordPacker.rectangle(width, height), CoordPacker.expand(packed, 1, width, height)); Coord[] safe = CoordPacker.allPacked(unseen); for (int i = 0; i < safe.length; i++) { setGoal(safe[i]); } safetyMap = scan(impassable2); for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { if (storedScan[x][y] < FLOOR) { gradientMap[x][y] = storedScan[x][y] * 2.0 * (moveLength+1) + safetyMap[x][y] * coverPreference; } //safeMap[x][y] = Math.pow(safeMap[x][y] + safetyMap[x][y], 1.5); } } goals = cachedGoals; scan(impassable2); //gradientMap = storedScan; } Coord currentPos = start; double paidLength = 0.0; while (true) { if (frustration > 500) { path.clear(); break; } double best = gradientMap[currentPos.x][currentPos.y]; final Direction[] dirs = appendDir(shuffleDirs(rng), Direction.NONE); int choice = rng.nextInt(dirs.length); for (int d = 0; d < dirs.length; d++) { Coord pt = Coord.get(currentPos.x + dirs[d].deltaX, currentPos.y + dirs[d].deltaY); if (gradientMap[pt.x][pt.y] < best) { if (dirs[choice] == Direction.NONE || !path.contains(pt)) { best = gradientMap[pt.x][pt.y]; choice = d; } } } if (best >= gradientMap[currentPos.x][currentPos.y] || physicalMap[currentPos.x + dirs[choice].deltaX][currentPos.y + dirs[choice].deltaY] > FLOOR) { break; } currentPos = currentPos.translate(dirs[choice]); path.add(Coord.get(currentPos.x, currentPos.y)); paidLength += costMap[currentPos.x][currentPos.y]; frustration++; if(paidLength > moveLength - 1.0) break; if (gradientMap[currentPos.x][currentPos.y] == 0) break; } goals.clear(); if (onlyPassable.contains(currentPos) || impassable2.contains(currentPos)) { closed.put(currentPos, WALL); impassable2.add(currentPos); return findCoveredAttackPath(moveLength, minPreferredRange, maxPreferredRange, coverPreference, fov, seekDistantGoals, impassable2, onlyPassable, threats, start, targets); } frustration = 0; return new ArrayList<>(path); } // /** // * Scans the dungeon using DijkstraMap.scan with the listed goals and start point, and returns a list // * of Coord positions (using the current measurement) needed to get closer to a goal, where goals are // * considered valid if they are at a valid range for the given Technique to hit at least one target // * and ideal if that Technique can affect as many targets as possible from a cell that can be moved // * to with at most movelength steps. // * <p/> // * The return value of this method is the path to get to a location to attack, but on its own it // * does not tell the user how to perform the attack. It does set the targetMap 2D Coord array field // * so that if your position at the end of the returned path is non-null in targetMap, it will be // * a Coord that can be used as a target position for Technique.apply() . If your position at the end // * of the returned path is null, then an ideal attack position was not reachable by the path. // * <p/> // * This needs a char[][] dungeon as an argument because DijkstraMap does not always have a char[][] // * version of the map available to it, and certain AOE implementations that a Technique uses may // * need a char[][] specifically to determine what they affect. // * <p/> // * The maximum length of the returned list is given by moveLength; if moving the full length of // * the list would place the mover in a position shared by one of the positions in allies // * (which is typically filled with friendly units that can be passed through in multi-tile- // * movement scenarios, and is also used considered an undesirable thing to affect for the Technique), // * it will recalculate a move so that it does not pass into that cell. // * <p/> // * The keys in impassable should be the positions of enemies and obstacles that cannot be moved // * through, and will be ignored if there is a target overlapping one. // * <br> // * This caches its result in a member field, path, which can be fetched after finding a path and will change with // * each call to a pathfinding method. // * // * @param moveLength the maximum distance to try to pathfind out to; if a spot to use a Technique can be found // * while moving no more than this distance, then the targetMap field in this object will have a // * target Coord that is ideal for the given Technique at the x, y indices corresponding to the // * last Coord in the returned path. // * @param tech a Technique that we will try to find an ideal place to use, and/or a path toward that place. // * @param dungeon a char 2D array with '#' for walls. // * @param cache a FOVCache that has completed its calculations, and will be used for LOS and Technique work, may be null // * @param impassable locations of enemies or mobile hazards/obstacles that aren't in the map as walls // * @param allies called onlyPassable in other methods, here it also represents allies for Technique things // * @param start the Coord the pathfinder starts at. // * @param targets a Set of Coord, not an array of Coord or variable argument list as in other methods. // * @return an ArrayList of Coord that represents a path to travel to get to an ideal place to use tech. Copy of path. // */ // @GwtIncompatible // public ArrayList<Coord> findTechniquePath(int moveLength, Technique tech, char[][] dungeon, FOVCache cache, // Set<Coord> impassable, Set<Coord> allies, Coord start, Set<Coord> targets) { // if (!initialized) return null; // tech.setMap(dungeon); // if (cache != null) // tech.aoe.setCache(cache); // double[][] resMap = new double[width][height]; // double[][] worthMap = new double[width][height]; // double[][] userDistanceMap; // double paidLength = 0.0; // // LinkedHashSet<Coord> friends; // // // for (int x = 0; x < width; x++) { // for (int y = 0; y < height; y++) { // resMap[x][y] = (physicalMap[x][y] == WALL) ? 1.0 : 0.0; // targetMap[x][y] = null; // } // } // // path.clear(); // if (targets == null || targets.size() == 0) // return new ArrayList<>(path); // LinkedHashSet<Coord> impassable2; // if (impassable == null) // impassable2 = new LinkedHashSet<>(); // else // impassable2 = new LinkedHashSet<>(impassable); // // if (allies == null) // friends = new LinkedHashSet<>(); // else { // friends = new LinkedHashSet<>(allies); // friends.remove(start); // } // // resetMap(); // setGoal(start); // userDistanceMap = scan(impassable2); // clearGoals(); // resetMap(); // for (Coord goal : targets) { // setGoal(goal.x, goal.y); // } // if (goals.isEmpty()) // return new ArrayList<>(path); // // Measurement mess = measurement; // /* // if(measurement == Measurement.EUCLIDEAN) // { // measurement = Measurement.CHEBYSHEV; // } // */ // scan(impassable2); // clearGoals(); // // Coord tempPt = Coord.get(0, 0); // LinkedHashMap<Coord, ArrayList<Coord>> ideal; // // generate an array of the single best location to attack when you are in a given cell. // for (int x = 0; x < width; x++) { // CELL: // for (int y = 0; y < height; y++) { // tempPt = Coord.get(x, y); // if (gradientMap[x][y] == WALL || gradientMap[x][y] == DARK || userDistanceMap[x][y] > moveLength * 2.0) // continue; // if (gradientMap[x][y] >= tech.aoe.getMinRange() && gradientMap[x][y] <= tech.aoe.getMaxRange()) { // for (Coord tgt : targets) { // if (cache == null || cache.queryLOS(x, y, tgt.x, tgt.y)) { // ideal = tech.idealLocations(tempPt, targets, friends); // // this is weird but it saves the trouble of getting the iterator and checking hasNext() . // for (Map.Entry<Coord, ArrayList<Coord>> ip : ideal.entrySet()) { // targetMap[x][y] = ip.getKey(); // worthMap[x][y] = ip.getValue().size(); // setGoal(x, y); // gradientMap[x][y] = 0; // break; // } // continue CELL; // } // } // gradientMap[x][y] = FLOOR; // } else // gradientMap[x][y] = FLOOR; // } // } // scan(impassable2); // // double currentDistance = gradientMap[start.x][start.y]; // if (currentDistance <= moveLength) { // Coord[] g_arr = new Coord[goals.size()]; // g_arr = goals.keySet().toArray(g_arr); // // goals.clear(); // setGoal(start); // scan(impassable2); // goals.clear(); // gradientMap[start.x][start.y] = moveLength; // // for (Coord g : g_arr) { // if (gradientMap[g.x][g.y] <= moveLength && worthMap[g.x][g.y] > 0) { // goals.put(g, 0.0 - worthMap[g.x][g.y]); // } // } // resetMap(); // /* for(Coord g : goals.keySet()) // { // gradientMap[g.x][g.y] = 0.0 - worthMap[g.x][g.y]; // }*/ // scan(impassable2); // // } // // measurement = mess; // // Coord currentPos = Coord.get(start.x, start.y); // while (true) { // if (frustration > 500) { // path.clear(); // break; // } // double best = gradientMap[currentPos.x][currentPos.y]; // final Direction[] dirs = appendDir(shuffleDirs(rng), Direction.NONE); // int choice = rng.nextInt(dirs.length); // // for (int d = 0; d < dirs.length; d++) { // Coord pt = Coord.get(currentPos.x + dirs[d].deltaX, currentPos.y + dirs[d].deltaY); // if (gradientMap[pt.x][pt.y] < best) { // if (dirs[choice] == Direction.NONE || !path.contains(pt)) { // best = gradientMap[pt.x][pt.y]; // choice = d; // } // } // } // if (best >= gradientMap[currentPos.x][currentPos.y]) { // if (friends.contains(currentPos)) { // closed.put(currentPos, WALL); // impassable2.add(currentPos); // return findTechniquePath(moveLength, tech, dungeon, cache, impassable2, // friends, start, targets); // } // break; // } // if (best > gradientMap[start.x][start.y] || physicalMap[currentPos.x + dirs[choice].deltaX][currentPos.y + dirs[choice].deltaY] > FLOOR) { // path.clear(); // break; // } // currentPos = currentPos.translate(dirs[choice].deltaX, dirs[choice].deltaY); // path.add(currentPos); // paidLength += costMap[currentPos.x][currentPos.y]; // frustration++; // if (paidLength > moveLength - 1.0) { // if (friends.contains(currentPos)) { // closed.put(currentPos, WALL); // impassable2.add(currentPos); // return findTechniquePath(moveLength, tech, dungeon, cache, impassable2, // friends, start, targets); // } // break; // } //// if(gradientMap[currentPos.x][currentPos.y] == 0) //// break; // } // frustration = 0; // goals.clear(); // return new ArrayList<>(path); // } private double cachedLongerPaths = 1.2; private Set<Coord> cachedImpassable = new LinkedHashSet<>(); private Coord[] cachedFearSources; private double[][] cachedFleeMap; private int cachedSize = 1; /** * Scans the dungeon using DijkstraMap.scan with the listed fearSources and start point, and returns a list * of Coord positions (using Manhattan distance) needed to get further from the closest fearSources, meant * for running away. The maximum length of the returned list is given by length; if moving the full * length of the list would place the mover in a position shared by one of the positions in onlyPassable * (which is typically filled with friendly units that can be passed through in multi-tile- * movement scenarios), it will recalculate a move so that it does not pass into that cell. * The keys in impassable should be the positions of enemies and obstacles that cannot be moved * through, and will be ignored if there is a fearSource overlapping one. The preferLongerPaths parameter * is meant to be tweaked and adjusted; higher values should make creatures prefer to escape out of * doorways instead of hiding in the closest corner, and a value of 1.2 should be typical for many maps. * The parameters preferLongerPaths, impassable, and the varargs used for fearSources will be cached, and * any subsequent calls that use the same values as the last values passed will avoid recalculating * unnecessary scans. * <br> * This caches its result in a member field, path, which can be fetched after finding a path and will change with * each call to a pathfinding method. * * @param length the length of the path to calculate * @param preferLongerPaths Set this to 1.2 if you aren't sure; it will probably need tweaking for different maps. * @param impassable a Set of impassable Coord positions that may change (not constant like walls); can be null * @param onlyPassable a Set of Coord positions that this pathfinder cannot end a path occupying (typically allies); can be null * @param start the start of the path, should correspond to the minimum-x, minimum-y position of the pathfinder * @param fearSources a vararg or array of Coord positions to run away from * @return an ArrayList of Coord that will contain the locations of this creature as it goes away from fear sources. Copy of path. */ public ArrayList<Coord> findFleePath(int length, double preferLongerPaths, Set<Coord> impassable, Set<Coord> onlyPassable, Coord start, Coord... fearSources) { if (!initialized) return null; path.clear(); LinkedHashSet<Coord> impassable2; if (impassable == null) impassable2 = new LinkedHashSet<>(); else impassable2 = new LinkedHashSet<>(impassable); if (onlyPassable == null) onlyPassable = new LinkedHashSet<>(); if (fearSources == null || fearSources.length < 1) { path.clear(); return new ArrayList<>(path); } if (cachedSize == 1 && preferLongerPaths == cachedLongerPaths && impassable2.equals(cachedImpassable) && Arrays.equals(fearSources, cachedFearSources)) { gradientMap = cachedFleeMap; } else { cachedLongerPaths = preferLongerPaths; cachedImpassable = new LinkedHashSet<>(impassable2); cachedFearSources = GwtCompatibility.cloneCoords(fearSources); cachedSize = 1; resetMap(); for (Coord goal : fearSources) { setGoal(goal.x, goal.y); } if (goals.isEmpty()) return new ArrayList<>(path); scan(impassable2); for (int x = 0; x < gradientMap.length; x++) { for (int y = 0; y < gradientMap[x].length; y++) { gradientMap[x][y] *= (gradientMap[x][y] >= FLOOR) ? 1.0 : (0.0 - preferLongerPaths); } } cachedFleeMap = scan(impassable2); } Coord currentPos = start; double paidLength = 0.0; while (true) { if (frustration > 500) { path.clear(); break; } double best = gradientMap[currentPos.x][currentPos.y]; final Direction[] dirs = appendDir(shuffleDirs(rng), Direction.NONE); int choice = rng.nextInt(dirs.length); for (int d = 0; d < dirs.length; d++) { Coord pt = Coord.get(currentPos.x + dirs[d].deltaX, currentPos.y + dirs[d].deltaY); if (gradientMap[pt.x][pt.y] < best) { if (dirs[choice] == Direction.NONE || !path.contains(pt)) { best = gradientMap[pt.x][pt.y]; choice = d; } } } if (best >= gradientMap[start.x][start.y] || physicalMap[currentPos.x + dirs[choice].deltaX][currentPos.y + dirs[choice].deltaY] > FLOOR) { path.clear(); break; } currentPos = currentPos.translate(dirs[choice].deltaX, dirs[choice].deltaY); if (path.size() > 0) { Coord last = path.get(path.size() - 1); if (gradientMap[last.x][last.y] <= gradientMap[currentPos.x][currentPos.y]) break; } path.add(currentPos); frustration++; paidLength += costMap[currentPos.x][currentPos.y]; if (paidLength > length - 1.0) { if (onlyPassable.contains(currentPos)) { closed.put(currentPos, WALL); impassable2.add(currentPos); return findFleePath(length, preferLongerPaths, impassable2, onlyPassable, start, fearSources); } break; } } frustration = 0; goals.clear(); return new ArrayList<>(path); } /** * Scans the dungeon using DijkstraMap.scan with the listed goals and start point, and returns a list * of Coord positions (using the current measurement) needed to get closer to the closest reachable * goal. The maximum length of the returned list is given by length; if moving the full length of * the list would place the mover in a position shared by one of the positions in onlyPassable * (which is typically filled with friendly units that can be passed through in multi-tile- * movement scenarios), it will recalculate a move so that it does not pass into that cell. * The keys in impassable should be the positions of enemies and obstacles that cannot be moved * through, and will be ignored if there is a goal overlapping one. * The parameter size refers to the side length of a square unit, such as 2 for a 2x2 unit. The * parameter start must refer to the minimum-x, minimum-y cell of that unit if size is > 1, and * all positions in the returned path will refer to movement of the minimum-x, minimum-y cell. * <br> * This caches its result in a member field, path, which can be fetched after finding a path and will change with * each call to a pathfinding method. * * @param size the side length of the creature trying to find a path * @param length the length of the path to calculate * @param impassable a Set of impassable Coord positions that may change (not constant like walls); can be null * @param onlyPassable a Set of Coord positions that this pathfinder cannot end a path occupying (typically allies); can be null * @param start the start of the path, should correspond to the minimum-x, minimum-y position of the pathfinder * @param targets a vararg or array of Coord that this will try to pathfind toward * @return an ArrayList of Coord that will contain the min-x, min-y locations of this creature as it goes toward a target. Copy of path. */ public ArrayList<Coord> findPathLarge(int size, int length, Set<Coord> impassable, Set<Coord> onlyPassable, Coord start, Coord... targets) { if (!initialized) return null; path.clear(); LinkedHashSet<Coord> impassable2; if (impassable == null) impassable2 = new LinkedHashSet<>(); else impassable2 = new LinkedHashSet<>(impassable); if (onlyPassable == null) onlyPassable = new LinkedHashSet<>(); resetMap(); for (Coord goal : targets) { setGoal(goal.x, goal.y); } if (goals.isEmpty()) return new ArrayList<>(path); scan(impassable2, size); Coord currentPos = start; double paidLength = 0.0; while (true) { if (frustration > 500) { path.clear(); break; } double best = gradientMap[currentPos.x][currentPos.y]; final Direction[] dirs = appendDir(shuffleDirs(rng), Direction.NONE); int choice = rng.nextInt(dirs.length); for (int d = 0; d < dirs.length; d++) { Coord pt = Coord.get(currentPos.x + dirs[d].deltaX, currentPos.y + dirs[d].deltaY); if (gradientMap[pt.x][pt.y] < best) { if (dirs[choice] == Direction.NONE || !path.contains(pt)) { best = gradientMap[pt.x][pt.y]; choice = d; } } } if (best >= gradientMap[currentPos.x][currentPos.y] || physicalMap[currentPos.x + dirs[choice].deltaX][currentPos.y + dirs[choice].deltaY] > FLOOR) { path.clear(); break; } currentPos = currentPos.translate(dirs[choice].deltaX, dirs[choice].deltaY); path.add(currentPos); paidLength += costMap[currentPos.x][currentPos.y]; frustration++; if (paidLength > length - 1.0) { if (onlyPassable.contains(currentPos)) { closed.put(currentPos, WALL); impassable2.add(currentPos); return findPathLarge(size, length, impassable2, onlyPassable, start, targets); } break; } if (gradientMap[currentPos.x][currentPos.y] == 0) break; } frustration = 0; goals.clear(); return new ArrayList<>(path); } /** * Scans the dungeon using DijkstraMap.scan with the listed goals and start point, and returns a list * of Coord positions (using the current measurement) needed to get closer to a goal, until preferredRange is * reached, or further from a goal if the preferredRange has not been met at the current distance. * The maximum length of the returned list is given by moveLength; if moving the full length of * the list would place the mover in a position shared by one of the positions in onlyPassable * (which is typically filled with friendly units that can be passed through in multi-tile- * movement scenarios), it will recalculate a move so that it does not pass into that cell. * The keys in impassable should be the positions of enemies and obstacles that cannot be moved * through, and will be ignored if there is a goal overlapping one. * The parameter size refers to the side length of a square unit, such as 2 for a 2x2 unit. The * parameter start must refer to the minimum-x, minimum-y cell of that unit if size is > 1, and * all positions in the returned path will refer to movement of the minimum-x, minimum-y cell. * <br> * This caches its result in a member field, path, which can be fetched after finding a path and will change with * each call to a pathfinding method. * * @param size the side length of the creature trying to find a path * @param moveLength the length of the path to calculate * @param preferredRange the distance this unit will try to keep from a target * @param los a squidgrid.LOS object if the preferredRange should try to stay in line of sight, or null if LoS * should be disregarded. * @param impassable a Set of impassable Coord positions that may change (not constant like walls); can be null * @param onlyPassable a Set of Coord positions that this pathfinder cannot end a path occupying (typically allies); can be null * @param start the start of the path, should correspond to the minimum-x, minimum-y position of the pathfinder * @param targets a vararg or array of Coord that this will try to pathfind toward * @return an ArrayList of Coord that will contain the min-x, min-y locations of this creature as it goes toward a target. Copy of path. */ public ArrayList<Coord> findAttackPathLarge(int size, int moveLength, int preferredRange, LOS los, Set<Coord> impassable, Set<Coord> onlyPassable, Coord start, Coord... targets) { if (!initialized) return null; if (preferredRange < 0) preferredRange = 0; double[][] resMap = new double[width][height]; if (los != null) { for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { resMap[x][y] = (physicalMap[x][y] == WALL) ? 1.0 : 0.0; } } } path.clear(); LinkedHashSet<Coord> impassable2; if (impassable == null) impassable2 = new LinkedHashSet<>(); else impassable2 = new LinkedHashSet<>(impassable); if (onlyPassable == null) onlyPassable = new LinkedHashSet<>(); resetMap(); for (Coord goal : targets) { setGoal(goal.x, goal.y); } if (goals.isEmpty()) return new ArrayList<>(path); Measurement mess = measurement; if (measurement == Measurement.EUCLIDEAN) { measurement = Measurement.CHEBYSHEV; } scan(impassable2, size); goals.clear(); for (int x = 0; x < width; x++) { CELL: for (int y = 0; y < height; y++) { if (gradientMap[x][y] == WALL || gradientMap[x][y] == DARK) continue; if (x + 2 < width && y + 2 < height && gradientMap[x][y] == preferredRange) { for (Coord goal : targets) { if (los == null || los.isReachable(resMap, x, y, goal.x, goal.y) || los.isReachable(resMap, x + 1, y, goal.x, goal.y) || los.isReachable(resMap, x, y + 1, goal.x, goal.y) || los.isReachable(resMap, x + 1, y + 1, goal.x, goal.y)) { setGoal(x, y); gradientMap[x][y] = 0; continue CELL; } } gradientMap[x][y] = FLOOR; } else gradientMap[x][y] = FLOOR; } } measurement = mess; scan(impassable2, size); Coord currentPos = start; double paidLength = 0.0; while (true) { if (frustration > 500) { path.clear(); break; } double best = gradientMap[currentPos.x][currentPos.y]; final Direction[] dirs = appendDir(shuffleDirs(rng), Direction.NONE); int choice = rng.nextInt(dirs.length); for (int d = 0; d < dirs.length; d++) { Coord pt = Coord.get(currentPos.x + dirs[d].deltaX, currentPos.y + dirs[d].deltaY); if (gradientMap[pt.x][pt.y] < best) { if (dirs[choice] == Direction.NONE || !path.contains(pt)) { best = gradientMap[pt.x][pt.y]; choice = d; } } } if (best >= gradientMap[currentPos.x][currentPos.y] || physicalMap[currentPos.x + dirs[choice].deltaX][currentPos.y + dirs[choice].deltaY] > FLOOR) { path.clear(); break; } currentPos = currentPos.translate(dirs[choice].deltaX, dirs[choice].deltaY); path.add(currentPos); frustration++; paidLength += costMap[currentPos.x][currentPos.y]; if (paidLength > moveLength - 1.0) { if (onlyPassable.contains(currentPos)) { closed.put(currentPos, WALL); impassable2.add(currentPos); return findAttackPathLarge(size, moveLength, preferredRange, los, impassable2, onlyPassable, start, targets); } break; } if (gradientMap[currentPos.x][currentPos.y] == 0) break; } frustration = 0; goals.clear(); return new ArrayList<>(path); } /** * Scans the dungeon using DijkstraMap.scan with the listed goals and start point, and returns a list * of Coord positions (using the current measurement) needed to get closer to a goal, until a cell is reached with * a distance from a goal that is at least equal to minPreferredRange and no more than maxPreferredRange, * which may go further from a goal if the minPreferredRange has not been met at the current distance. * The maximum length of the returned list is given by moveLength; if moving the full length of * the list would place the mover in a position shared by one of the positions in onlyPassable * (which is typically filled with friendly units that can be passed through in multi-tile- * movement scenarios), it will recalculate a move so that it does not pass into that cell. * The keys in impassable should be the positions of enemies and obstacles that cannot be moved * through, and will be ignored if there is a goal overlapping one. * The parameter size refers to the side length of a square unit, such as 2 for a 2x2 unit. The * parameter start must refer to the minimum-x, minimum-y cell of that unit if size is > 1, and * all positions in the returned path will refer to movement of the minimum-x, minimum-y cell. * <br> * This caches its result in a member field, path, which can be fetched after finding a path and will change with * each call to a pathfinding method. * * @param size the side length of the creature trying to find a path * @param moveLength the length of the path to calculate * @param minPreferredRange the (inclusive) lower bound of the distance this unit will try to keep from a target * @param maxPreferredRange the (inclusive) upper bound of the distance this unit will try to keep from a target * @param los a squidgrid.LOS object if the preferredRange should try to stay in line of sight, or null if LoS * should be disregarded. * @param impassable a Set of impassable Coord positions that may change (not constant like walls); can be null * @param onlyPassable a Set of Coord positions that this pathfinder cannot end a path occupying (typically allies); can be null * @param start the start of the path, should correspond to the minimum-x, minimum-y position of the pathfinder * @param targets a vararg or array of Coord that this will try to pathfind toward * @return an ArrayList of Coord that will contain the min-x, min-y locations of this creature as it goes toward a target. Copy of path. */ public ArrayList<Coord> findAttackPathLarge(int size, int moveLength, int minPreferredRange, int maxPreferredRange, LOS los, Set<Coord> impassable, Set<Coord> onlyPassable, Coord start, Coord... targets) { if (!initialized) return null; if (minPreferredRange < 0) minPreferredRange = 0; if (maxPreferredRange < minPreferredRange) maxPreferredRange = minPreferredRange; double[][] resMap = new double[width][height]; if (los != null) { for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) { resMap[x][y] = (physicalMap[x][y] == WALL) ? 1.0 : 0.0; } } } path.clear(); LinkedHashSet<Coord> impassable2; if (impassable == null) impassable2 = new LinkedHashSet<>(); else impassable2 = new LinkedHashSet<>(impassable); if (onlyPassable == null) onlyPassable = new LinkedHashSet<>(); resetMap(); for (Coord goal : targets) { setGoal(goal); } if (goals.isEmpty()) return new ArrayList<>(path); Measurement mess = measurement; if (measurement == Measurement.EUCLIDEAN) { measurement = Measurement.CHEBYSHEV; } scan(impassable2, size); goals.clear(); for (int x = 0; x < width; x++) { CELL: for (int y = 0; y < height; y++) { if (gradientMap[x][y] == WALL || gradientMap[x][y] == DARK) continue; if (x + 2 < width && y + 2 < height && gradientMap[x][y] >= minPreferredRange && gradientMap[x][y] <= maxPreferredRange) { for (Coord goal : targets) { if (los == null || los.isReachable(resMap, x, y, goal.x, goal.y) || los.isReachable(resMap, x + 1, y, goal.x, goal.y) || los.isReachable(resMap, x, y + 1, goal.x, goal.y) || los.isReachable(resMap, x + 1, y + 1, goal.x, goal.y)) { setGoal(x, y); gradientMap[x][y] = 0; continue CELL; } } gradientMap[x][y] = FLOOR; } else gradientMap[x][y] = FLOOR; } } measurement = mess; scan(impassable2, size); Coord currentPos = start; double paidLength = 0.0; while (true) { if (frustration > 500) { path.clear(); break; } double best = gradientMap[currentPos.x][currentPos.y]; final Direction[] dirs = appendDir(shuffleDirs(rng), Direction.NONE); int choice = rng.nextInt(dirs.length); for (int d = 0; d < dirs.length; d++) { Coord pt = Coord.get(currentPos.x + dirs[d].deltaX, currentPos.y + dirs[d].deltaY); if (gradientMap[pt.x][pt.y] < best) { if (dirs[choice] == Direction.NONE || !path.contains(pt)) { best = gradientMap[pt.x][pt.y]; choice = d; } } } if (best >= gradientMap[currentPos.x][currentPos.y] || physicalMap[currentPos.x + dirs[choice].deltaX][currentPos.y + dirs[choice].deltaY] > FLOOR) { path.clear(); break; } currentPos = currentPos.translate(dirs[choice].deltaX, dirs[choice].deltaY); path.add(currentPos); frustration++; paidLength += costMap[currentPos.x][currentPos.y]; if (paidLength > moveLength - 1.0) { if (onlyPassable.contains(currentPos)) { closed.put(currentPos, WALL); impassable2.add(currentPos); return findAttackPathLarge(size, moveLength, minPreferredRange, maxPreferredRange, los, impassable2, onlyPassable, start, targets); } break; } if (gradientMap[currentPos.x][currentPos.y] == 0) break; } frustration = 0; goals.clear(); return new ArrayList<>(path); } /** * Scans the dungeon using DijkstraMap.scan with the listed fearSources and start point, and returns a list * of Coord positions (using Manhattan distance) needed to get further from the closest fearSources, meant * for running away. The maximum length of the returned list is given by length; if moving the full * length of the list would place the mover in a position shared by one of the positions in onlyPassable * (which is typically filled with friendly units that can be passed through in multi-tile- * movement scenarios), it will recalculate a move so that it does not pass into that cell. * The keys in impassable should be the positions of enemies and obstacles that cannot be moved * through, and will be ignored if there is a fearSource overlapping one. The preferLongerPaths parameter * is meant to be tweaked and adjusted; higher values should make creatures prefer to escape out of * doorways instead of hiding in the closest corner, and a value of 1.2 should be typical for many maps. * The parameters size, preferLongerPaths, impassable, and the varargs used for fearSources will be cached, and * any subsequent calls that use the same values as the last values passed will avoid recalculating * unnecessary scans. Calls to findFleePath will cache as if size is 1, and may share a cache with this function. * The parameter size refers to the side length of a square unit, such as 2 for a 2x2 unit. The * parameter start must refer to the minimum-x, minimum-y cell of that unit if size is > 1, and * all positions in the returned path will refer to movement of the minimum-x, minimum-y cell. * <br> * This caches its result in a member field, path, which can be fetched after finding a path and will change with * each call to a pathfinding method. * * @param size the side length of the creature trying the find a path * @param length the length of the path to calculate * @param preferLongerPaths Set this to 1.2 if you aren't sure; it will probably need tweaking for different maps. * @param impassable a Set of impassable Coord positions that may change (not constant like walls); can be null * @param onlyPassable a Set of Coord positions that this pathfinder cannot end a path occupying (typically allies); can be null * @param start the start of the path, should correspond to the minimum-x, minimum-y position of the pathfinder * @param fearSources a vararg or array of Coord positions to run away from * @return an ArrayList of Coord that will contain the locations of this creature as it goes away from fear sources. Copy of path. */ public ArrayList<Coord> findFleePathLarge(int size, int length, double preferLongerPaths, Set<Coord> impassable, Set<Coord> onlyPassable, Coord start, Coord... fearSources) { if (!initialized) return null; path.clear(); LinkedHashSet<Coord> impassable2; if (impassable == null) impassable2 = new LinkedHashSet<>(); else impassable2 = new LinkedHashSet<>(impassable); if (onlyPassable == null) onlyPassable = new LinkedHashSet<>(); if (fearSources == null || fearSources.length < 1) { path.clear(); return new ArrayList<>(path); } if (size == cachedSize && preferLongerPaths == cachedLongerPaths && impassable2.equals(cachedImpassable) && Arrays.equals(fearSources, cachedFearSources)) { gradientMap = cachedFleeMap; } else { cachedLongerPaths = preferLongerPaths; cachedImpassable = new LinkedHashSet<>(impassable2); cachedFearSources = GwtCompatibility.cloneCoords(fearSources); cachedSize = size; resetMap(); for (Coord goal : fearSources) { setGoal(goal.x, goal.y); } if (goals.isEmpty()) return new ArrayList<>(path); scan(impassable2, size); for (int x = 0; x < gradientMap.length; x++) { for (int y = 0; y < gradientMap[x].length; y++) { gradientMap[x][y] *= (gradientMap[x][y] >= FLOOR) ? 1.0 : (0.0 - preferLongerPaths); } } cachedFleeMap = scan(impassable2, size); } Coord currentPos = start; double paidLength = 0.0; while (true) { if (frustration > 500) { path.clear(); break; } double best = gradientMap[currentPos.x][currentPos.y]; final Direction[] dirs = appendDir(shuffleDirs(rng), Direction.NONE); int choice = rng.nextInt(dirs.length); for (int d = 0; d < dirs.length; d++) { Coord pt = Coord.get(currentPos.x + dirs[d].deltaX, currentPos.y + dirs[d].deltaY); if (gradientMap[pt.x][pt.y] < best) { if (dirs[choice] == Direction.NONE || !path.contains(pt)) { best = gradientMap[pt.x][pt.y]; choice = d; } } } if (best >= gradientMap[currentPos.x][currentPos.y] || physicalMap[currentPos.x + dirs[choice].deltaX][currentPos.y + dirs[choice].deltaY] > FLOOR) { path.clear(); break; } currentPos = currentPos.translate(dirs[choice].deltaX, dirs[choice].deltaY); if (path.size() > 0) { Coord last = path.get(path.size() - 1); if (gradientMap[last.x][last.y] <= gradientMap[currentPos.x][currentPos.y]) break; } path.add(currentPos); frustration++; paidLength += costMap[currentPos.x][currentPos.y]; if (paidLength > length - 1.0) { if (onlyPassable.contains(currentPos)) { closed.put(currentPos, WALL); impassable2.add(currentPos); return findFleePathLarge(size, length, preferLongerPaths, impassable2, onlyPassable, start, fearSources); } break; } } frustration = 0; goals.clear(); return new ArrayList<>(path); } /** * Intended primarily for internal use. Needs scan() to already be called and at least one goal to already be set, * and does not restrict the length of the path or behave as if the pathfinder has allies or enemies. * <br> * This caches its result in a member field, path, which can be fetched after finding a path and will change with * each call to a pathfinding method. * * @param target the target cell * @return an ArrayList of Coord that make up the best path. Copy of path. */ public ArrayList<Coord> findPathPreScanned(Coord target) { if (!initialized || goals == null || goals.isEmpty()) return null; RNG rng2 = new StatefulRNG(new LightRNG(0xf00d)); path.clear(); Coord currentPos = target; while (true) { if (frustration > 2000) { path.clear(); break; } double best = gradientMap[currentPos.x][currentPos.y]; final Direction[] dirs = appendDir(shuffleDirs(rng2), Direction.NONE); int choice = rng2.nextInt(dirs.length); for (int d = 0; d < dirs.length; d++) { Coord pt = Coord.get(currentPos.x + dirs[d].deltaX, currentPos.y + dirs[d].deltaY); if (gradientMap[pt.x][pt.y] < best) { if (dirs[choice] == Direction.NONE || !path.contains(pt)) { best = gradientMap[pt.x][pt.y]; choice = d; } } } if (best >= gradientMap[currentPos.x][currentPos.y] || physicalMap[currentPos.x + dirs[choice].deltaX][currentPos.y + dirs[choice].deltaY] > FLOOR) { path.clear(); break; } currentPos = currentPos.translate(dirs[choice].deltaX, dirs[choice].deltaY); path.add(0, currentPos); frustration++; if (gradientMap[currentPos.x][currentPos.y] == 0) break; } frustration = 0; return new ArrayList<>(path); } /** * A simple limited flood-fill that returns a LinkedHashMap of Coord keys to the Double values in the DijkstraMap, only * calculating out to a number of steps determined by limit. This can be useful if you need many flood-fills and * don't need a large area for each, or if you want to have an effect spread to a certain number of cells away. * * @param radius the number of steps to take outward from each starting position. * @param starts a vararg group of Points to step outward from; this often will only need to be one Coord. * @return A LinkedHashMap of Coord keys to Double values; the starts are included in this with the value 0.0. */ public LinkedHashMap<Coord, Double> floodFill(int radius, Coord... starts) { if (!initialized) return null; LinkedHashMap<Coord, Double> fill = new LinkedHashMap<>(); resetMap(); for (Coord goal : starts) { setGoal(goal.x, goal.y); } if (goals.isEmpty()) return fill; partialScan(radius, null); double temp; for (int x = 1; x < width - 1; x++) { for (int y = 1; y < height - 1; y++) { temp = gradientMap[x][y]; if (temp < FLOOR) { fill.put(Coord.get(x, y), temp); } } } goals.clear(); return fill; } private static final double root2 = Math.sqrt(2.0); private double heuristic(Direction target) { switch (measurement) { case MANHATTAN: case CHEBYSHEV: return 1.0; case EUCLIDEAN: switch (target) { case DOWN_LEFT: case DOWN_RIGHT: case UP_LEFT: case UP_RIGHT: return root2; default: return 1.0; } } return 1.0; } /* For Gwt compatibility */ private Direction[] shuffleDirs(RNG rng) { final Direction[] src = measurement == Measurement.MANHATTAN ? Direction.CARDINALS : Direction.OUTWARDS; return rng.shuffle(src, new Direction[src.length]); } /* For Gwt compatibility */ private static Direction[] appendDir(Direction[] src, Direction additional) { final Direction[] result = new Direction[src.length + 1]; for (int i = 0; i < src.length; i++) result[i] = src[i]; result[result.length - 1] = additional; return result; } }