package squidpony.performance.alternate;
import squidpony.GwtCompatibility;
import squidpony.annotation.GwtIncompatible;
import squidpony.squidai.Technique;
import squidpony.squidai.Threat;
import squidpony.squidgrid.*;
import squidpony.squidmath.*;
import java.io.Serializable;
import java.util.*;
/**
* An alternative to AStarSearch when you want to fully explore a search space, or when you want a gradient floodfill.
* It's currently significantly faster that AStarSearch, and also supports pathfinding to the nearest of multiple
* goals, which is not possible with AStarSearch. This last feature enables a whole host of others, like pathfinding
* for creatures that can attack targets between a specified minimum and maximum distance, and there's also the
* standard uses of Dijkstra Maps such as finding ideal paths to run away.
* As a bit of introduction, the article http://www.roguebasin.com/index.php?title=Dijkstra_Maps_Visualized can
* provide some useful information on how these work and how to visualize the information they can produce, while
* http://www.roguebasin.com/index.php?title=The_Incredible_Power_of_Dijkstra_Maps is an inspiring list of the
* various features Dijkstra Maps can enable.
* <br>
* 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 OldDijkstraMap implements Serializable {
private static final long serialVersionUID = -2456306898212944440L;
private static final double root2 = Math.sqrt(2.0);
/**
* 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;
public double heuristic(Direction target) {
switch (this) {
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;
}
public int directionCount() {
switch (this) {
case MANHATTAN:
return 4;
default:
return 8;
}
}
}
/**
* 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<>();
public boolean cutShort = false;
/**
* 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.
*/
protected IntDoubleOrderedMap goals;
private IntDoubleOrderedMap 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 Direction[] reuse = new Direction[9];
private boolean initialized = false;
private int mappedCount = 0;
private int blockingRequirement = 2;
/**
* 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 OldDijkstraMap() {
rng = new RNG(new LightRNG());
path = new ArrayList<>();
goals = new IntDoubleOrderedMap();
fresh = new IntDoubleOrderedMap();
closed = new IntDoubleOrderedMap();
open = new IntDoubleOrderedMap();
}
/**
* 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 OldDijkstraMap(RNG random) {
rng = random;
path = new ArrayList<>();
goals = new IntDoubleOrderedMap();
fresh = new IntDoubleOrderedMap();
closed = new IntDoubleOrderedMap();
open = new IntDoubleOrderedMap();
}
/**
* Used to construct a DijkstraMap from the output of another.
*
* @param level
*/
public OldDijkstraMap(final double[][] level) {
this(level, Measurement.MANHATTAN);
}
/**
* Used to construct a DijkstraMap from the output of another, specifying a distance calculation.
*
* @param level
* @param measurement
*/
public OldDijkstraMap(final double[][] level, Measurement measurement) {
rng = new RNG();
this.measurement = measurement;
path = new ArrayList<>();
goals = new IntDoubleOrderedMap();
fresh = new IntDoubleOrderedMap();
closed = new IntDoubleOrderedMap();
open = new IntDoubleOrderedMap();
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 OldDijkstraMap(final char[][] level) {
this(level, Measurement.MANHATTAN, new RNG());
}
/**
* 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 OldDijkstraMap(final char[][] level, RNG rng) {
this(level, Measurement.MANHATTAN, rng);
}
/**
* 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 OldDijkstraMap(final char[][] level, char alternateWall) {
rng = new RNG();
path = new ArrayList<>();
goals = new IntDoubleOrderedMap();
fresh = new IntDoubleOrderedMap();
closed = new IntDoubleOrderedMap();
open = new IntDoubleOrderedMap();
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 OldDijkstraMap(final char[][] level, Measurement measurement) {
this(level, measurement, new RNG());
}
/**
* 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 OldDijkstraMap(final char[][] level, Measurement measurement, RNG rng) {
this.rng = rng;
path = new ArrayList<>();
this.measurement = measurement;
goals = new IntDoubleOrderedMap();
fresh = new IntDoubleOrderedMap();
closed = new IntDoubleOrderedMap();
open = new IntDoubleOrderedMap();
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 OldDijkstraMap 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 OldDijkstraMap 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 OldDijkstraMap 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 OldDijkstraMap 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 OldDijkstraMap 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 OldDijkstraMap 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 Measurement.CHEBYSHEV;
else if (radius.equals2D(Radius.DIAMOND))
return Measurement.MANHATTAN;
else
return 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 || x < 0 || x >= width || y < 0 || y >= height) return;
if (physicalMap[x][y] > FLOOR) {
return;
}
goals.put(Coord.pureEncode(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 || !pt.isWithin(width, height)) return;
if (physicalMap[pt.x][pt.y] > FLOOR) {
return;
}
goals.put(pt.encode(), GOAL);
}
/**
* Marks many cells as goals for pathfinding, ignoring cells in walls or unreachable areas. More efficient than
* a loop that calls {@link #setGoal(Coord)} over and over, since this doesn't need to do a bounds check. The
* GreasedRegion passed to this should have the same width and height as this DijkstraMap.
*
* @param pts a GreasedRegion containing "on" cells to treat as goals; should have the same width and height as this
*/
public void setGoals(GreasedRegion pts) {
if (!initialized || pts.width > width || pts.height > height) return;
int[] enc = new GreasedRegion(physicalMap, FLOOR).and(pts).asEncoded();
double[] gls = new double[enc.length];
Arrays.fill(gls, GOAL);
goals.putAll(enc, gls);
}
/**
* Marks many cells as goals for pathfinding, ignoring cells in walls or unreachable areas. Simply loops through
* pts and calls {@link #setGoal(Coord)} on each Coord in pts.
* If you have a GreasedRegion, you should use it with {@link #setGoals(GreasedRegion)}, which is faster.
*
* @param pts any Iterable of Coord, which can be a List, Set, Queue, etc. of Coords to mark as goals
*/
public void setGoals(Iterable<Coord> pts) {
if (!initialized) return;
for (Coord c : pts) {
setGoal(c);
}
}
/**
* 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 || !pt.isWithin(width, height)) 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 || x < 0 || x >= width || y < 0 || y >= height) 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 || x < 0 || x >= width || y < 0 || y >= height) 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 || x < 0 || x >= width || y < 0 || y >= height) 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 || !pt.isWithin(width, height)) 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;
IntDoubleOrderedMap.KeyIterator ki = goals.keySet().iterator();
while (ki.hasNext())
resetCell(Coord.decode(ki.nextInt()));
goals.clear();
}
protected void setFresh(int x, int y, double counter) {
if (!initialized || x < 0 || x >= width || y < 0 || y >= height) return;
gradientMap[x][y] = counter;
fresh.put(Coord.pureEncode(x, y), counter);
}
protected void setFresh(final Coord pt, double counter) {
if (!initialized || !pt.isWithin(width, height)) return;
gradientMap[pt.x][pt.y] = counter;
fresh.put(pt.encode(), 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];
if (c.isWithin(width, height))
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];
if (c.isWithin(width, height)) {
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(Collection<Coord> impassable) {
if (!initialized) return null;
if (impassable != null && !impassable.isEmpty()) {
for (Coord pt : impassable) {
closed.put(pt.encode(), WALL);
}
}
Coord dec, adj, cen;
int enc;
for (IntDoubleOrderedMap.MapEntry entry : goals.mapEntrySet()) {
//if (closed.containsKey(entry.getIntKey()))
// continue;
// closed.remove(entry.getIntKey());
dec = Coord.decode(entry.getIntKey());
gradientMap[dec.x][dec.y] = entry.getDoubleValue();
}
double currentLowest = 999000;
IntDoubleOrderedMap lowest = new IntDoubleOrderedMap();
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
if (gradientMap[x][y] > FLOOR && !goals.containsKey(Coord.pureEncode(x, y)))
closed.put(Coord.pureEncode(x, y), physicalMap[x][y]);
else if (gradientMap[x][y] < currentLowest) {
currentLowest = gradientMap[x][y];
lowest.clear();
lowest.put(Coord.pureEncode(x, y), currentLowest);
} else if (gradientMap[x][y] == currentLowest) {
lowest.put(Coord.pureEncode(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 (IntDoubleOrderedMap.MapEntry cell : open.mapEntrySet()) {
cen = Coord.decode(cell.getIntKey());
for (int d = 0; d < dirs.length; d++) {
adj = cen.translate(dirs[d].deltaX, dirs[d].deltaY);
if (adj.x < 0 || adj.y < 0 || width <= adj.x || height <= adj.y)
/* Outside the map */
continue;
if (d >= 4 && blockingRequirement > 0) // diagonal
{
if ((gradientMap[adj.x][cen.y] > FLOOR ? 1 : 0)
+ (gradientMap[cen.x][adj.y] > FLOOR ? 1 : 0)
>= blockingRequirement) {
continue;
}
}
enc = adj.encode();
double h = measurement.heuristic(dirs[d]);
if (!closed.containsKey(enc) && !open.containsKey(enc) && gradientMap[cen.x][cen.y] + h * costMap[adj.x][adj.y] < gradientMap[adj.x][adj.y]) {
setFresh(adj, cell.getDoubleValue() + h * costMap[adj.x][adj.y]);
++numAssigned;
++mappedCount;
}
}
}
// closed.putAll(open);
open.clear();
open.putAll(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, Collection<Coord> impassable) {
if (!initialized) return null;
if (impassable != null && !impassable.isEmpty()) {
for (Coord pt : impassable) {
closed.put(pt.encode(), WALL);
}
}
Coord dec, adj, cen;
int enc;
for (IntDoubleOrderedMap.MapEntry entry : goals.mapEntrySet()) {
//if (closed.containsKey(entry.getIntKey()))
// closed.remove(entry.getIntKey());
dec = Coord.decode(entry.getIntKey());
gradientMap[dec.x][dec.y] = entry.getDoubleValue();
}
double currentLowest = 999000;
IntDoubleOrderedMap lowest = new IntDoubleOrderedMap();
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
if (gradientMap[x][y] > FLOOR && !goals.containsKey(Coord.pureEncode(x, y)))
closed.put(Coord.pureEncode(x, y), physicalMap[x][y]);
else if (gradientMap[x][y] < currentLowest) {
currentLowest = gradientMap[x][y];
lowest.clear();
lowest.put(Coord.pureEncode(x, y), currentLowest);
} else if (gradientMap[x][y] == currentLowest) {
lowest.put(Coord.pureEncode(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) {
numAssigned = 0;
for (IntDoubleOrderedMap.MapEntry cell : open.mapEntrySet()) {
cen = Coord.decode(cell.getIntKey());
for (int d = 0; d < dirs.length; d++) {
adj = cen.translate(dirs[d].deltaX, dirs[d].deltaY);
if (adj.x < 0 || adj.y < 0 || width <= adj.x || height <= adj.y)
/* Outside the map */
continue;
if (d >= 4 && blockingRequirement > 0) // diagonal
{
if ((gradientMap[cen.x + dirs[d].deltaX][cen.y] > FLOOR ? 1 : 0)
+ (gradientMap[cen.x][cen.y + dirs[d].deltaY] > FLOOR ? 1 : 0)
>= blockingRequirement) {
continue;
}
}
enc = adj.encode();
double h = measurement.heuristic(dirs[d]);
if (!closed.containsKey(enc) && !open.containsKey(enc) &&
gradientMap[cen.x][cen.y] + h * costMap[adj.x][adj.y] < gradientMap[adj.x][adj.y]) {
setFresh(adj, cell.getDoubleValue() + h * costMap[adj.x][adj.y]);
++numAssigned;
++mappedCount;
}
}
}
// closed.putAll(open);
open = new IntDoubleOrderedMap(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 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, Collection<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.encode()))
closed.remove(start2.encode());
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.pureEncode(x, y)))
closed.put(Coord.pureEncode(x, y), physicalMap[x][y]);
}
}
int numAssigned = 1;
mappedCount = 1;
open.put(start2.encode(), 0.0);
Coord dec, adj, cen;
int enc;
Direction[] dirs = (measurement == Measurement.MANHATTAN) ? Direction.CARDINALS : Direction.OUTWARDS;
while (numAssigned > 0) {
// ++iter;
numAssigned = 0;
for (IntDoubleOrderedMap.MapEntry cell : open.mapEntrySet()) {
cen = Coord.decode(cell.getIntKey());
for (int d = 0; d < dirs.length; d++) {
adj = cen.translate(dirs[d].deltaX, dirs[d].deltaY);
if (adj.x < 0 || adj.y < 0 || width <= adj.x || height <= adj.y)
/* Outside the map */
continue;
enc = adj.encode();
double h = measurement.heuristic(dirs[d]);
if (!closed.containsKey(enc) && !open.containsKey(enc) &&
gradientMap[cen.x][cen.y] + h * costMap[adj.x][adj.y] < gradientMap[adj.x][adj.y]) {
setFresh(adj, cell.getDoubleValue() + 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 IntDoubleOrderedMap(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) {
return findNearest(start, new OrderedSet<>(targets));
}
/**
* 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) {
cutShort = true;
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 = appendDirToShuffle(rng);
int choice = rng.nextInt(measurement.directionCount() + 1);
for (int d = 0; d < measurement.directionCount() + 1; d++) {
Coord pt = Coord.get(currentPos.x + dirs[d].deltaX, currentPos.y + dirs[d].deltaY);
if (!pt.isWithin(width, height))
continue;
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) {
cutShort = true;
frustration = 0;
return new ArrayList<>(path);
}
currentPos = currentPos.translate(dirs[choice].deltaX, dirs[choice].deltaY);
if (gradientMap[currentPos.x][currentPos.y] == 0)
break;
path.add(currentPos);
frustration++;
}
frustration = 0;
cutShort = false;
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, Collection<Coord> targets) {
if (!initialized) return null;
ArrayList<Coord> found = new ArrayList<>(limit);
if (targets == null)
return found;
if (targets.contains(start))
return found;
Coord start2 = start, adj, cen;
int enc;
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.encode()))
closed.remove(start2.encode());
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.pureEncode(x, y)))
closed.put(Coord.pureEncode(x, y), physicalMap[x][y]);
}
}
int numAssigned = 1;
mappedCount = 1;
open.put(start2.encode(), 0.0);
Direction[] dirs = (measurement == Measurement.MANHATTAN) ? Direction.CARDINALS : Direction.OUTWARDS;
while (numAssigned > 0) {
// ++iter;
numAssigned = 0;
for (IntDoubleOrderedMap.MapEntry cell : open.mapEntrySet()) {
cen = Coord.decode(cell.getIntKey());
for (int d = 0; d < dirs.length; d++) {
adj = cen.translate(dirs[d].deltaX, dirs[d].deltaY);
if (adj.x < 0 || adj.y < 0 || width <= adj.x || height <= adj.y)
/* Outside the map */
continue;
enc = adj.encode();
double h = measurement.heuristic(dirs[d]);
if (!closed.containsKey(enc) && !open.containsKey(enc) &&
gradientMap[cen.x][cen.y] + h * costMap[adj.x][adj.y] < gradientMap[adj.x][adj.y]) {
setFresh(adj, cell.getDoubleValue() + 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 IntDoubleOrderedMap(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(Collection<Coord> impassable, int size) {
if (!initialized) return null;
if (impassable == null)
impassable = new OrderedSet<>();
IntDoubleOrderedMap blocking = new IntDoubleOrderedMap(impassable.size());
for (Coord pt : impassable) {
blocking.put(pt.encode(), WALL);
for (int x = 0; x < size; x++) {
for (int y = 0; y < size; y++) {
if (x + y != 0 && gradientMap[pt.x - x][pt.y - y] <= FLOOR)
blocking.put(Coord.pureEncode(pt.x - x, pt.y - y), DARK);
}
}
}
closed.putAll(blocking);
Coord dec, cen, adj;
int enc;
for (IntDoubleOrderedMap.MapEntry entry : goals.mapEntrySet()) {
//if (closed.containsKey(entry.getIntKey()))
// closed.remove(entry.getIntKey());
dec = Coord.decode(entry.getIntKey());
gradientMap[dec.x][dec.y] = entry.getDoubleValue();
}
mappedCount = goals.size();
double currentLowest = 999000;
IntDoubleOrderedMap lowest = new IntDoubleOrderedMap();
int temp, p;
for (int y = 0; y < height; y++) {
I_AM_BECOME_DEATH_DESTROYER_OF_WORLDS:
for (int x = 0; x < width; x++) {
p = Coord.pureEncode(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.pureEncode(x - i, y - j);
if (y - j < 0 || closed.containsKey(temp))
continue;
if (gradientMap[x - i][y - j] <= FLOOR && !goals.containsKey(temp))
closed.put(temp, 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.pureEncode(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.pureEncode(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.pureEncode(x + i, y + j);
if (y + j >= height || closed.containsKey(temp))
continue I_AM_BECOME_DEATH_DESTROYER_OF_WORLDS;
}
}
lowest.put(p, currentLowest);
}
}
}
}
int numAssigned = lowest.size();
open.putAll(lowest);
Direction[] dirs = (measurement == Measurement.MANHATTAN) ? Direction.CARDINALS : Direction.OUTWARDS;
while (numAssigned > 0) {
numAssigned = 0;
for (IntDoubleOrderedMap.MapEntry cell : open.mapEntrySet()) {
cen = Coord.decode(cell.getIntKey());
for (int d = 0; d < dirs.length; d++) {
adj = cen.translate(dirs[d].deltaX, dirs[d].deltaY);
if (adj.x < 0 || adj.y < 0 || width <= adj.x || height <= adj.y)
/* Outside the map */
continue;
enc = adj.encode();
double h = measurement.heuristic(dirs[d]);
if (!closed.containsKey(enc) && !open.containsKey(enc) && gradientMap[cen.x][cen.y] + h * costMap[adj.x][adj.y] < gradientMap[adj.x][adj.y]) {
setFresh(adj, cell.getDoubleValue() + h * costMap[adj.x][adj.y]);
++numAssigned;
++mappedCount;
}
}
}
// closed.putAll(open);
open = new IntDoubleOrderedMap(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, which represents movement in a system where
* a single move can be multiple cells if length is greater than 1 and should usually be 1 in standard roguelikes;
* 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-cell-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. This overload always scans the whole map; use
* {@link #findPath(int, int, Collection, Collection, Coord, Coord...)} to scan a smaller area for performance reasons.
* <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, Collection<Coord> impassable,
Collection<Coord> onlyPassable, Coord start, Coord... targets) {
return findPath(length, -1, impassable, onlyPassable, start, targets);
}
/**
* Scans the dungeon using DijkstraMap.scan or DijkstraMap.partialScan 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, which represents
* movement in a system where a single move can be multiple cells if length is greater than 1 and should usually
* be 1 in standard roguelikes; 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-cell-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 full map will only be scanned if scanLimit is 0 or less; for positive scanLimit values this will scan only
* that distance out from each goal, which can save processing time on maps where only a small part matters.
* Generally, scanLimit should be significantly greater than length.
* <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 scanLimit how many cells away from a goal to actually process; negative to process whole map
* @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, int scanLimit, Collection<Coord> impassable,
Collection<Coord> onlyPassable, Coord start, Coord... targets) {
if (!initialized) return null;
path.clear();
Collection<Coord> impassable2;
if (impassable == null)
impassable2 = Collections.emptySet();
else
impassable2 = new GreasedRegion(width, height, impassable);
if (onlyPassable == null)
onlyPassable = Collections.emptySet();
resetMap();
for (Coord goal : targets) {
setGoal(goal.x, goal.y);
}
if (goals.isEmpty()) {
cutShort = true;
return new ArrayList<>(path);
}
if (length < 0)
length = 0;
if (scanLimit <= 0 || scanLimit < length)
scan(impassable2);
else
partialScan(scanLimit, 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 = appendDirToShuffle(rng);
int choice = rng.nextInt(measurement.directionCount() + 1);
for (int d = 0; d <= measurement.directionCount(); d++) {
Coord pt = Coord.get(currentPos.x + dirs[d].deltaX, currentPos.y + dirs[d].deltaY);
if (!pt.isWithin(width, height))
continue;
if (gradientMap[pt.x][pt.y] < best && !impassable2.contains(pt)) {
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) {
cutShort = true;
frustration = 0;
return new ArrayList<>(path);
}
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.encode(), WALL);
impassable2.add(currentPos);
return findPath(length, scanLimit, impassable2, onlyPassable, start, targets);
}
break;
}
if (gradientMap[currentPos.x][currentPos.y] == 0)
break;
}
cutShort = false;
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, Collection<Coord> impassable,
Collection<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,
Collection<Coord> impassable, Collection<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();
Collection<Coord> impassable2;
if (impassable == null)
impassable2 = Collections.emptySet();
else
impassable2 = new GreasedRegion(width, height, impassable);
if (onlyPassable == null)
onlyPassable = Collections.emptySet();
resetMap();
for (Coord goal : targets) {
setGoal(goal.x, goal.y);
}
if (goals.isEmpty()) {
cutShort = true;
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 = appendDirToShuffle(rng);
int choice = rng.nextInt(measurement.directionCount() + 1);
for (int d = 0; d <= measurement.directionCount(); d++) {
Coord pt = Coord.get(currentPos.x + dirs[d].deltaX, currentPos.y + dirs[d].deltaY);
if (!pt.isWithin(width, height))
continue;
if (gradientMap[pt.x][pt.y] < best && !impassable2.contains(pt)) {
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) {
cutShort = true;
frustration = 0;
return new ArrayList<>(path);
}
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.encode(), WALL);
impassable2.add(currentPos);
return findAttackPath(moveLength, minPreferredRange, maxPreferredRange, los, impassable2,
onlyPassable, start, targets);
}
break;
}
if (gradientMap[currentPos.x][currentPos.y] == 0)
break;
}
cutShort = false;
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,
Collection<Coord> impassable, Collection<Coord> allies, Coord start, Collection<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;
OrderedSet<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) {
cutShort = true;
return new ArrayList<>(path);
}
Collection<Coord> impassable2;
if (impassable == null)
impassable2 = Collections.emptySet();
else
impassable2 = new GreasedRegion(width, height, impassable);
if (allies == null)
friends = new OrderedSet<>();
else {
friends = new OrderedSet<>(allies);
friends.remove(start);
}
resetMap();
setGoal(start);
userDistanceMap = scan(impassable2);
clearGoals();
resetMap();
for (Coord goal : targets) {
setGoal(goal.x, goal.y);
}
if (goals.isEmpty()) {
cutShort = true;
return new ArrayList<>(path);
}
Measurement mess = measurement;
/*
if(measurement == Measurement.EUCLIDEAN)
{
measurement = Measurement.CHEBYSHEV;
}
*/
scan(impassable2);
clearGoals();
Coord tempPt = Coord.get(0, 0);
OrderedMap<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);
if (!ideal.isEmpty()) {
targetMap[x][y] = ideal.keyAt(0);
worthMap[x][y] = ideal.getAt(0).size();
setGoal(x, y);
gradientMap[x][y] = 0;
}
continue CELL;
}
}
gradientMap[x][y] = FLOOR;
} else
gradientMap[x][y] = FLOOR;
}
}
scan(impassable2);
double currentDistance = gradientMap[start.x][start.y];
if (currentDistance <= moveLength) {
int[] g_arr = goals.keySet().toIntArray();
goals.clear();
setGoal(start);
scan(impassable2);
goals.clear();
gradientMap[start.x][start.y] = moveLength;
Coord dec;
for (int g, ig = 0; ig < g_arr.length; ig++) {
g = g_arr[ig];
dec = Coord.decode(g);
if (gradientMap[dec.x][dec.y] <= moveLength && worthMap[dec.x][dec.y] > 0) {
goals.put(g, 0.0 - worthMap[dec.x][dec.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 = appendDirToShuffle(rng);
int choice = rng.nextInt(measurement.directionCount() + 1);
for (int d = 0; d <= measurement.directionCount(); d++) {
Coord pt = Coord.get(currentPos.x + dirs[d].deltaX, currentPos.y + dirs[d].deltaY);
if (!pt.isWithin(width, height))
continue;
if (gradientMap[pt.x][pt.y] < best && !impassable2.contains(pt)) {
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.encode(), 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) {
cutShort = true;
frustration = 0;
return new ArrayList<>(path);
}
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.encode(), WALL);
impassable2.add(currentPos);
return findTechniquePath(moveLength, tech, dungeon, los, impassable2,
friends, start, targets);
}
break;
}
// if(gradientMap[currentPos.x][currentPos.y] == 0)
// break;
}
cutShort = false;
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, Collection<Coord> impassable,
Collection<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,
Collection<Coord> impassable, Collection<Coord> onlyPassable, Coord start, Coord... targets) {
if (!initialized) return null;
if (minPreferredRange < 0) minPreferredRange = 0;
if (maxPreferredRange < minPreferredRange) maxPreferredRange = minPreferredRange;
path.clear();
Collection<Coord> impassable2;
if (impassable == null)
impassable2 = Collections.emptySet();
else
impassable2 = new GreasedRegion(width, height, impassable);
if (onlyPassable == null)
onlyPassable = Collections.emptySet();
resetMap();
for (Coord goal : targets) {
setGoal(goal.x, goal.y);
}
if (goals.isEmpty()) {
cutShort = true;
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 = appendDirToShuffle(rng);
int choice = rng.nextInt(measurement.directionCount() + 1);
for (int d = 0; d <= measurement.directionCount(); d++) {
Coord pt = Coord.get(currentPos.x + dirs[d].deltaX, currentPos.y + dirs[d].deltaY);
if (!pt.isWithin(width, height))
continue;
if (gradientMap[pt.x][pt.y] < best && !impassable2.contains(pt)) {
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) {
cutShort = true;
frustration = 0;
return new ArrayList<>(path);
}
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.encode(), WALL);
impassable2.add(currentPos);
return findAttackPath(moveLength, minPreferredRange, maxPreferredRange, cache, impassable2,
onlyPassable, start, targets);
}
break;
}
if (gradientMap[currentPos.x][currentPos.y] == 0)
break;
}
cutShort = false;
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, Collection<Coord> impassable,
Collection<Coord> onlyPassable, Iterable<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>();
Collection<Coord> impassable2;
if (impassable == null)
impassable2 = new OrderedSet<Coord>();
else
impassable2 = new OrderedSet<Coord>(impassable);
if (onlyPassable == null)
onlyPassable = new OrderedSet<Coord>();
resetMap();
for (Coord goal : targets) {
setGoal(goal.x, goal.y);
}
if (goals.isEmpty()) {
cutShort = true;
return new ArrayList<>(path);
}
Measurement mess = measurement;
if (measurement == Measurement.EUCLIDEAN) {
measurement = Measurement.CHEBYSHEV;
}
scan(impassable2);
goals.clear();
IntDoubleOrderedMap cachedGoals = new IntDoubleOrderedMap();
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.pureEncode(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 = appendDirToShuffle(rng);
int choice = rng.nextInt(measurement.directionCount() + 1);
for (int d = 0; d <= measurement.directionCount(); d++) {
Coord pt = Coord.get(currentPos.x + dirs[d].deltaX, currentPos.y + dirs[d].deltaY);
if (!pt.isWithin(width, height))
continue;
if (gradientMap[pt.x][pt.y] < best && !impassable2.contains(pt)) {
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.encode(), WALL);
impassable2.add(currentPos);
return findCoveredAttackPath(moveLength, minPreferredRange, maxPreferredRange, coverPreference,
impassable2, onlyPassable, threats, start, targets);
}
cutShort = false;
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, Collection<Coord> impassable,
Collection<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, Collection<Coord> impassable,
Collection<Coord> onlyPassable, Iterable<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>();
Collection<Coord> impassable2;
if (impassable == null)
impassable2 = new OrderedSet<Coord>();
else
impassable2 = new OrderedSet<Coord>(impassable);
if (onlyPassable == null)
onlyPassable = new OrderedSet<Coord>();
resetMap();
for (Coord goal : targets) {
setGoal(goal.x, goal.y);
}
if (goals.isEmpty()) {
cutShort = true;
return new ArrayList<>(path);
}
Measurement mess = measurement;
if (measurement == Measurement.EUCLIDEAN) {
measurement = Measurement.CHEBYSHEV;
}
scan(impassable2);
goals.clear();
IntDoubleOrderedMap cachedGoals = new IntDoubleOrderedMap();
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.pureEncode(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 = appendDirToShuffle(rng);
int choice = rng.nextInt(measurement.directionCount() + 1);
for (int d = 0; d <= measurement.directionCount(); d++) {
Coord pt = Coord.get(currentPos.x + dirs[d].deltaX, currentPos.y + dirs[d].deltaY);
if (!pt.isWithin(width, height))
continue;
if (gradientMap[pt.x][pt.y] < best && !impassable2.contains(pt)) {
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.encode(), WALL);
impassable2.add(currentPos);
return findCoveredAttackPath(moveLength, minPreferredRange, maxPreferredRange, coverPreference,
fov, seekDistantGoals, impassable2, onlyPassable, threats, start, targets);
}
cutShort = false;
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, Collection<Coord> impassable,
Collection<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, Collection<Coord> impassable,
Collection<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();
Collection<Coord> impassable2;
if (impassable == null)
impassable2 = Collections.emptySet();
else
impassable2 = new GreasedRegion(width, height, impassable);
if (onlyPassable == null)
onlyPassable = Collections.emptySet();
resetMap();
for (Coord goal : targets) {
setGoal(goal.x, goal.y);
}
if (goals.isEmpty()) {
cutShort = true;
return new ArrayList<>(path);
}
Measurement mess = measurement;
if (measurement == Measurement.EUCLIDEAN) {
measurement = Measurement.CHEBYSHEV;
}
scan(impassable2);
goals.clear();
IntDoubleOrderedMap cachedGoals = new IntDoubleOrderedMap();
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.pureEncode(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 = appendDirToShuffle(rng);
int choice = rng.nextInt(measurement.directionCount() + 1);
for (int d = 0; d <= measurement.directionCount(); d++) {
Coord pt = Coord.get(currentPos.x + dirs[d].deltaX, currentPos.y + dirs[d].deltaY);
if (!pt.isWithin(width, height))
continue;
if (gradientMap[pt.x][pt.y] < best && !impassable2.contains(pt)) {
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.encode(), WALL);
impassable2.add(currentPos);
return findCoveredAttackPath(moveLength, minPreferredRange, maxPreferredRange, coverPreference,
fov, seekDistantGoals, impassable2, onlyPassable, threats, start, targets);
}
cutShort = false;
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 (or any Collection) 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,
Collection<Coord> impassable, Collection<Coord> allies, Coord start, Collection<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;
Collection<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) {
cutShort = true;
return new ArrayList<>(path);
}
Collection<Coord> impassable2;
if (impassable == null)
impassable2 = Collections.emptySet();
else
impassable2 = new GreasedRegion(width, height, impassable);
if (allies == null)
friends = Collections.emptySet();
else {
friends = new GreasedRegion(width, height, allies);
friends.remove(start);
}
resetMap();
setGoal(start);
userDistanceMap = scan(impassable2);
clearGoals();
resetMap();
for (Coord goal : targets) {
setGoal(goal.x, goal.y);
}
if (goals.isEmpty()) {
cutShort = true;
return new ArrayList<>(path);
}
Measurement mess = measurement;
/*
if(measurement == Measurement.EUCLIDEAN)
{
measurement = Measurement.CHEBYSHEV;
}
*/
scan(impassable2);
clearGoals();
Coord tempPt = Coord.get(0, 0);
OrderedMap<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);
if (!ideal.isEmpty()) {
targetMap[x][y] = ideal.keyAt(0);
worthMap[x][y] = ideal.getAt(0).size();
setGoal(x, y);
gradientMap[x][y] = 0;
}
continue CELL;
}
}
gradientMap[x][y] = FLOOR;
} else
gradientMap[x][y] = FLOOR;
}
}
scan(impassable2);
double currentDistance = gradientMap[start.x][start.y];
if (currentDistance <= moveLength) {
int[] g_arr = goals.keySet().toIntArray();
Coord dec;
goals.clear();
setGoal(start);
scan(impassable2);
goals.clear();
gradientMap[start.x][start.y] = moveLength;
for (int g : g_arr) {
dec = Coord.decode(g);
if (gradientMap[dec.x][dec.y] <= moveLength && worthMap[dec.x][dec.y] > 0) {
goals.put(g, 0.0 - worthMap[dec.x][dec.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 = appendDirToShuffle(rng);
int choice = rng.nextInt(measurement.directionCount() + 1);
for (int d = 0; d <= measurement.directionCount(); d++) {
Coord pt = Coord.get(currentPos.x + dirs[d].deltaX, currentPos.y + dirs[d].deltaY);
if (!pt.isWithin(width, height))
continue;
if (gradientMap[pt.x][pt.y] < best && !impassable2.contains(pt)) {
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.encode(), 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) {
cutShort = true;
frustration = 0;
return new ArrayList<>(path);
}
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.encode(), WALL);
impassable2.add(currentPos);
return findTechniquePath(moveLength, tech, dungeon, cache, impassable2,
friends, start, targets);
}
break;
}
// if(gradientMap[currentPos.x][currentPos.y] == 0)
// break;
}
cutShort = false;
frustration = 0;
goals.clear();
return new ArrayList<>(path);
}
private double cachedLongerPaths = 1.2;
private Collection<Coord> cachedImpassable = new OrderedSet<>();
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, Collection<Coord> impassable,
Collection<Coord> onlyPassable, Coord start, Coord... fearSources) {
return findFleePath(length, -1, preferLongerPaths, impassable, onlyPassable, start, fearSources);
}
/**
* Scans the dungeon using DijkstraMap.scan or DijkstraMap.partialScan with the listed fearSources and start
* point, and returns a list of Coord positions (using this DijkstraMap's metric) needed to get further from
* the closest fearSources, meant for running away. The maximum length of the returned list is given by length,
* which represents movement in a system where a single move can be multiple cells if length is greater than 1 and
* should usually be 1 in standard roguelikes; 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-cell-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. However, scanLimit is not cached; if you use scanLimit then it is assumed you are using some
* value for it that shouldn't change relative to the other parameters (like twice the length).
* The full map will only be scanned if scanLimit is 0 or less; for positive scanLimit values this will scan only
* that distance out from each goal, which can save processing time on maps where only a small part matters.
* Generally, scanLimit should be significantly greater than length.
* <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 scanLimit how many steps away from a fear source to calculate; negative scans the whole map
* @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, int scanLimit, double preferLongerPaths, Collection<Coord> impassable,
Collection<Coord> onlyPassable, Coord start, Coord... fearSources) {
if (!initialized) return null;
path.clear();
Collection<Coord> impassable2;
if (impassable == null)
impassable2 = Collections.emptySet();
else
impassable2 = new GreasedRegion(width, height, impassable);
if (onlyPassable == null)
onlyPassable = Collections.emptySet();
if (fearSources == null || fearSources.length < 1) {
cutShort = true;
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 OrderedSet<>(impassable2);
cachedFearSources = GwtCompatibility.cloneCoords(fearSources);
cachedSize = 1;
resetMap();
for (Coord goal : fearSources) {
setGoal(goal.x, goal.y);
}
if (goals.isEmpty()) {
cutShort = true;
return new ArrayList<>(path);
}
if (length < 0) length = 0;
if (scanLimit <= 0 || scanLimit < length)
cachedFleeMap = scan(impassable2);
else
cachedFleeMap = partialScan(scanLimit, 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 : -preferLongerPaths;
}
}
if (scanLimit <= 0 || scanLimit < length)
cachedFleeMap = scan(impassable2);
else
cachedFleeMap = partialScan(scanLimit, 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 = appendDirToShuffle(rng);
int choice = rng.nextInt(measurement.directionCount() + 1);
for (int d = 0; d <= measurement.directionCount(); d++) {
Coord pt = Coord.get(currentPos.x + dirs[d].deltaX, currentPos.y + dirs[d].deltaY);
if (!pt.isWithin(width, height))
continue;
if (gradientMap[pt.x][pt.y] < best && !impassable2.contains(pt)) {
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) {
cutShort = true;
frustration = 0;
return new ArrayList<>(path);
}
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.encode(), WALL);
impassable2.add(currentPos);
return findFleePath(length, scanLimit, preferLongerPaths, impassable2, onlyPassable, start, fearSources);
}
break;
}
}
cutShort = false;
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, Collection<Coord> impassable,
Collection<Coord> onlyPassable, Coord start, Coord... targets) {
if (!initialized) return null;
path.clear();
Collection<Coord> impassable2;
if (impassable == null)
impassable2 = Collections.emptySet();
else
impassable2 = new GreasedRegion(width, height, impassable);
if (onlyPassable == null)
onlyPassable = Collections.emptySet();
resetMap();
for (Coord goal : targets) {
setGoal(goal.x, goal.y);
}
if (goals.isEmpty()) {
cutShort = true;
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 = appendDirToShuffle(rng);
int choice = rng.nextInt(measurement.directionCount() + 1);
for (int d = 0; d <= measurement.directionCount(); d++) {
Coord pt = Coord.get(currentPos.x + dirs[d].deltaX, currentPos.y + dirs[d].deltaY);
if (!pt.isWithin(width, height))
continue;
if (gradientMap[pt.x][pt.y] < best && !impassable2.contains(pt)) {
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) {
cutShort = true;
frustration = 0;
return new ArrayList<>(path);
}
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.encode(), WALL);
impassable2.add(currentPos);
return findPathLarge(size, length, impassable2, onlyPassable, start, targets);
}
break;
}
if (gradientMap[currentPos.x][currentPos.y] == 0)
break;
}
cutShort = false;
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, Collection<Coord> impassable,
Collection<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();
Collection<Coord> impassable2;
if (impassable == null)
impassable2 = Collections.emptySet();
else
impassable2 = new GreasedRegion(width, height, impassable);
if (onlyPassable == null)
onlyPassable = Collections.emptySet();
resetMap();
for (Coord goal : targets) {
setGoal(goal.x, goal.y);
}
if (goals.isEmpty()) {
cutShort = true;
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 = appendDirToShuffle(rng);
int choice = rng.nextInt(measurement.directionCount() + 1);
for (int d = 0; d <= measurement.directionCount(); d++) {
Coord pt = Coord.get(currentPos.x + dirs[d].deltaX, currentPos.y + dirs[d].deltaY);
if (!pt.isWithin(width, height))
continue;
if (gradientMap[pt.x][pt.y] < best && !impassable2.contains(pt)) {
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) {
cutShort = true;
frustration = 0;
return new ArrayList<>(path);
}
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.encode(), WALL);
impassable2.add(currentPos);
return findAttackPathLarge(size, moveLength, preferredRange, los, impassable2, onlyPassable, start, targets);
}
break;
}
if (gradientMap[currentPos.x][currentPos.y] == 0)
break;
}
cutShort = false;
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,
Collection<Coord> impassable, Collection<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();
Collection<Coord> impassable2;
if (impassable == null)
impassable2 = Collections.emptySet();
else
impassable2 = new GreasedRegion(width, height, impassable);
if (onlyPassable == null)
onlyPassable = Collections.emptySet();
resetMap();
for (Coord goal : targets) {
setGoal(goal);
}
if (goals.isEmpty()) {
cutShort = true;
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 = appendDirToShuffle(rng);
int choice = rng.nextInt(measurement.directionCount() + 1);
for (int d = 0; d <= measurement.directionCount(); d++) {
Coord pt = Coord.get(currentPos.x + dirs[d].deltaX, currentPos.y + dirs[d].deltaY);
if (!pt.isWithin(width, height))
continue;
if (gradientMap[pt.x][pt.y] < best && !impassable2.contains(pt)) {
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) {
cutShort = true;
frustration = 0;
return new ArrayList<>(path);
}
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.encode(), WALL);
impassable2.add(currentPos);
return findAttackPathLarge(size, moveLength, minPreferredRange, maxPreferredRange, los, impassable2,
onlyPassable, start, targets);
}
break;
}
if (gradientMap[currentPos.x][currentPos.y] == 0)
break;
}
cutShort = false;
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, Collection<Coord> impassable,
Collection<Coord> onlyPassable, Coord start, Coord... fearSources) {
if (!initialized) return null;
path.clear();
Collection<Coord> impassable2;
if (impassable == null)
impassable2 = Collections.emptySet();
else
impassable2 = new GreasedRegion(width, height, impassable);
if (onlyPassable == null)
onlyPassable = Collections.emptySet();
if (fearSources == null || fearSources.length < 1) {
cutShort = true;
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 OrderedSet<>(impassable2);
cachedFearSources = GwtCompatibility.cloneCoords(fearSources);
cachedSize = size;
resetMap();
for (Coord goal : fearSources) {
setGoal(goal.x, goal.y);
}
if (goals.isEmpty()) {
cutShort = true;
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 = appendDirToShuffle(rng);
int choice = rng.nextInt(measurement.directionCount() + 1);
for (int d = 0; d <= measurement.directionCount(); d++) {
Coord pt = Coord.get(currentPos.x + dirs[d].deltaX, currentPos.y + dirs[d].deltaY);
if (!pt.isWithin(width, height))
continue;
if (gradientMap[pt.x][pt.y] < best && !impassable2.contains(pt)) {
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) {
cutShort = true;
frustration = 0;
return new ArrayList<>(path);
}
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.encode(), WALL);
impassable2.add(currentPos);
return findFleePathLarge(size, length, preferLongerPaths, impassable2, onlyPassable, start, fearSources);
}
break;
}
}
cutShort = false;
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) {
path.clear();
if (!initialized || goals == null || goals.isEmpty()) return path;
Coord currentPos = target;
if (gradientMap[currentPos.x][currentPos.y] <= FLOOR)
path.add(currentPos);
else
return path;
RNG rng2 = new StatefulRNG(0xf00d);
while (true) {
if (frustration > 2000) {
path.clear();
break;
}
double best = gradientMap[currentPos.x][currentPos.y];
final Direction[] dirs = appendDirToShuffle(rng2);
int choice = rng2.nextInt(measurement.directionCount() + 1);
for (int d = 0; d <= measurement.directionCount(); d++) {
Coord pt = Coord.get(currentPos.x + dirs[d].deltaX, currentPos.y + dirs[d].deltaY);
if (!pt.isWithin(width, height))
continue;
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) {
cutShort = true;
frustration = 0;
return new ArrayList<>(path);
}
currentPos = currentPos.translate(dirs[choice].deltaX, dirs[choice].deltaY);
path.add(0, currentPos);
frustration++;
if (gradientMap[currentPos.x][currentPos.y] == 0)
break;
}
cutShort = false;
frustration = 0;
return new ArrayList<>(path);
}
/**
* A simple limited flood-fill that returns a OrderedMap 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 OrderedMap of Coord keys to Double values; the starts are included in this with the value 0.0.
*/
public Map<Coord, Double> floodFill(int radius, Coord... starts) {
if (!initialized) return null;
OrderedMap<Coord, Double> fill = new OrderedMap<>();
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;
}
public int getMappedCount() {
return mappedCount;
}
/**
* If you want obstacles present in orthogonal cells to prevent pathfinding along the diagonal between them, this
* can be used to make thin diagonal walls non-viable to move through, or even to prevent diagonal movement if any
* one obstacle is orthogonally adjacent to both the start and target cell of a diagonal move.
* <br>
* If this is 0, as a special case no orthogonal obstacles will block diagonal moves.
* <br>
* If this is 1, having one orthogonal obstacle adjacent to both the current cell and the cell the pathfinder is
* trying to diagonally enter will block diagonal moves. This generally blocks movement around corners, the "hard
* corner" rule used in some games.
* <br>
* If this is 2, having two orthogonal obstacles adjacent to both the current cell and the cell the pathfinder is
* trying to diagonally enter will block diagonal moves. As an example, if there is a wall to the north and a wall
* to the east, then the pathfinder won't be able to move northeast even if there is a floor there.
*
* @return the current level of blocking required to stop a diagonal move
*/
public int getBlockingRequirement() {
return blockingRequirement;
}
/**
* If you want obstacles present in orthogonal cells to prevent pathfinding along the diagonal between them, this
* can be used to make thin diagonal walls non-viable to move through, or even to prevent diagonal movement if any
* one obstacle is orthogonally adjacent to both the start and target cell of a diagonal move.
* <br>
* If this is 0, as a special case no orthogonal obstacles will block diagonal moves.
* <br>
* If this is 1, having one orthogonal obstacle adjacent to both the current cell and the cell the pathfinder is
* trying to diagonally enter will block diagonal moves. This generally blocks movement around corners, the "hard
* corner" rule used in some games.
* <br>
* If this is 2, having two orthogonal obstacles adjacent to both the current cell and the cell the pathfinder is
* trying to diagonally enter will block diagonal moves. As an example, if there is a wall to the north and a wall
* to the east, then the pathfinder won't be able to move northeast even if there is a floor there.
*
* @param blockingRequirement the desired level of blocking required to stop a diagonal move
*/
public void setBlockingRequirement(int blockingRequirement) {
this.blockingRequirement = blockingRequirement > 2 ? 2 : blockingRequirement < 0 ? 0 : blockingRequirement;
}
/* For Gwt compatibility */
private Direction[] shuffleDirs(RNG rng) {
final Direction[] src = measurement == Measurement.MANHATTAN
? Direction.CARDINALS : Direction.OUTWARDS;
return rng.randomPortion(src, reuse);
}
/* For Gwt compatibility */
private Direction[] appendDirToShuffle(RNG rng) {
//appendDir(shuffleDirs(rng), Direction.NONE)
shuffleDirs(rng);
reuse[measurement.directionCount()] = Direction.NONE;
return reuse;
}
}