001package squidpony.squidai; 002 003import squidpony.squidgrid.FOV; 004import squidpony.squidgrid.Radius; 005import squidpony.squidgrid.mapping.DungeonUtility; 006import squidpony.squidmath.Bresenham; 007import squidpony.squidmath.Coord; 008import squidpony.squidmath.GreasedRegion; 009import squidpony.squidmath.OrderedMap; 010 011import java.io.Serializable; 012import java.util.ArrayList; 013import java.util.Collection; 014import java.util.Set; 015 016/** 017 * A simple struct-like class that stores various public fields which describe the targeting properties of a skill, 018 * spell, tech, or any other game-specific term for a targeted (typically offensive) ability we call a Technique. 019 * <br> 020 * The typical usage of a Technique is: 021 * <ul> 022 * <li>Construct any AOE implementation the Technique would use (if the Technique affects multiple grid cells).</li> 023 * <li>Construct the Technique (passing the AOE as a parameter if needed).</li> 024 * <li>Call {@link #setMap(char[][])} before considering the Technique if it has not been called yet, if the physical 025 * map (including doors and obstacles) has changed since setMap() was last called, or simply on every Technique every 026 * time the map changes if there are few enemies with few Techniques. PERFORMING ANY SUBSEQUENT STEPS WITHOUT SETTING 027 * THE MAP TO THE CURRENT ACTUAL PHYSICAL MAP WILL HAVE BAD CONSEQUENCES FOR LOGIC AND MAY CAUSE CRASHING BUGS DUE TO 028 * ARRAY BOUNDS VIOLATIONS IF YOU HAVEN'T SET THE MAP ARRAY IN THE FIRST PLACE. The map should be bounded by wall chars 029 * ('#'), which is done automatically by {@link squidpony.squidgrid.mapping.DungeonGenerator} and similar classes, and 030 * can be done with {@link squidpony.squidgrid.mapping.DungeonUtility#wallWrap(char[][])} as well.</li> 031 * <li>When the Technique is being considered by an AI, call {@link #idealLocations(Coord, Set, Set, Set)} with the 032 * values of targets, lesserTargets, and/or priorityTargets set to beings that the AI can see (likely using FOV) and 033 * wants to affect with this Technique (enemies for offensive Techniques, allies for supporting ones), and 034 * requiredExclusions typically set to allies for offensive Techniques that can cause friendly-fire damage, or to null 035 * for supporting ones or Techniques that don't affect allies.</li> 036 * <li>If the Technique is being used for a player action, you can show the player what cells are valid targets with 037 * {@link #possibleTargets(Coord)} or its other overload, or can validate choices without telling the player beforehand 038 * using {@link #canTarget(Coord, Coord)} (this may be useful for cases where the player is activating an unknown magic 039 * item like a scroll, and the actual range hasn't been revealed).</li> 040 * <li>When an ideal location has been determined from the previous step, and the player or AI decides to use this 041 * Technique on a specific target point, call {@link #apply(Coord, Coord)} with the user position as a Coord and the 042 * chosen Coord, and proceed to process the effects of the Technique as fitting for your game on the returned Map of 043 * Coord keys to Double values denoting how affected (from 0.0 for unaffected to 1.0 for full power) that Coord is. 044 * An AI might decide which Technique to use by an aggression ("aggro" or "hatred") level tracked per-enemy, by weights 045 * on Techniques for different situations, by choosing at random, or some combination of factors.</li> 046 * </ul> 047 * <br> 048 * A Technique always has an AOE implementation that it uses to determine which cells it actually affects, and 049 * Techniques that do not actually affect an area use the default single-cell "Area of Effect" implementation, PointAOE. 050 * You typically will need to construct the implementing class of the AOE interface in a different way for each 051 * implementation; BeamAOE, LineAOE, and ConeAOE depend on the user's position, BurstAOE and BlastAOE treat radii 052 * differently from BeamAOE and LineAOE, and CloudAOE has a random component that can be given a seed. 053 * <br> 054 * A Technique has a String name, which typically should be in a form that can be presented to a user, and a 055 * String id, which defaults to the same value as name but can be given some value not meant for users that records 056 * any additional identifying characteristics the game needs for things like comparisons. 057 * 058 * Created by Tommy Ettinger on 7/27/2015. 059 */ 060public class Technique implements Serializable { 061 private static final long serialVersionUID = 2L; 062 063 public String name; 064 public String id; 065 public AOE aoe; 066 protected char[][] dungeon; 067 068 private static final Coord DEFAULT_POINT = Coord.get(0, 0); 069 070 /** 071 * Creates a Technique that can target any adjacent single Coord, using 072 * Chebyshev (8-way square) distance. 073 */ 074 public Technique() { 075 name = "Default Technique"; 076 id = name; 077 aoe = new PointAOE(DEFAULT_POINT); 078 } 079 080 /** 081 * Creates a Technique that can target any adjacent single Coord, 082 * using Chebyshev (8-way square) distance. 083 * @param name An identifier that may be displayed to the user. Also used for id. 084 */ 085 public Technique(String name) { 086 this.name = name; 087 id = name; 088 aoe = new PointAOE(DEFAULT_POINT); 089 } 090 091 /** 092 * Creates a Technique that can target a Coord at a range specified by the given AOE's minRange and maxRange, 093 * using a distance metric from the AOE, and use that target Coord for the given AOE. 094 * @param name An identifier that may be displayed to the user. Also used for id. 095 * @param aoe An implementation of the AOE interface; typically needs construction beforehand. 096 */ 097 public Technique(String name, AOE aoe) { 098 this.name = name; 099 id = name; 100 this.aoe = aoe; 101 } 102 103 104 /** 105 * Creates a Technique that can target a Coord at a range specified by the given AOE's minRange and maxRange, 106 * using a distance metric from the AOE, and use that target Coord for the given AOE. Takes an id parameter. 107 * @param name An identifier that may be displayed to the user. 108 * @param id An identifier that should always be internal, and will probably never be shown to the user. 109 * @param aoe An implementation of the AOE interface; typically needs construction beforehand. 110 */ 111 public Technique(String name, String id, AOE aoe) { 112 this.name = name; 113 this.id = id; 114 this.aoe = aoe; 115 } 116 117 118 /** 119 * VITAL: Call this method before any calls to idealLocations() or apply(), and call it again if the map changes. 120 * 121 * This simple method sets the map that this Technique can find targets in to a given char 2D array with '#' for 122 * walls and any other character (including characters for open and closed doors) treated as a floor for most 123 * purposes (certain AOE implementations may treat open and closed doors differently, specifically any that use 124 * FOV internally and can yield values other than 1.0 from their findArea() method, like BlastAOE and ConeAOE). 125 * @param map A char 2D array like one generated by squidpony.squidgrid.mapping.DungeonGenerator, with '#' for walls and bounded edges. 126 */ 127 public void setMap(char[][] map) 128 { 129 dungeon = map; 130 aoe.setMap(map); 131 } 132 133 /** 134 * Get a mapping of Coord keys representing locations to apply this Technique to, to ArrayList of Coord values 135 * representing which targets (by their location) are affected by choosing that Coord. All targets with this method 136 * are valued equally, and the ideal location affects as many as possible without hitting any requiredExclusions. 137 * 138 * YOU MUST CALL setMap() with the current map status at some point before using this method, and call it again if 139 * the map changes. Failure to do so can cause serious bugs, from logic errors where monsters consider a door 140 * closed when it is open or vice versa, to an ArrayIndexOutOfBoundsException being thrown if the player moved to a 141 * differently-sized map and the Technique tries to use the previous map with coordinates from the new one. 142 * 143 * @param user The location of the user of this Technique 144 * @param targets Set of Coord of desirable targets to include in the area of this Technique, as many as possible. 145 * @param requiredExclusions Set of Coord where each value is something this Technique will really try to avoid. 146 * @return OrderedMap of Coord keys representing target points to pass to apply, to ArrayList of Coord values representing what targets' locations will be affected. 147 */ 148 public OrderedMap<Coord, ArrayList<Coord>> idealLocations(Coord user, Collection<Coord> targets, Collection<Coord> requiredExclusions) { 149 aoe.setOrigin(user); 150 return aoe.idealLocations(targets, requiredExclusions); 151 152 } 153 154 /** 155 * Get a mapping of Coord keys representing locations to apply this Technique to, to ArrayList of Coord values 156 * representing which targets (by their location) are effected by choosing that Coord. This method will strongly 157 * prefer including priorityTargets in its area, especially multiple one if possible, and primarily uses 158 * lesserTargets as a tiebreaker if two locations have the same number of included priorityTargets but one has more 159 * lesserTargets in its area. 160 * 161 * YOU MUST CALL setMap() with the current map status at some point before using this method, and call it again if 162 * the map changes. Failure to do so can cause serious bugs, from logic errors where monsters consider a door 163 * closed when it is open or vice versa, to an ArrayIndexOutOfBoundsException being thrown if the player moved to a 164 * differently-sized map and the Technique tries to use the previous map with coordinates from the new one. 165 * 166 * @param user The location of the user of this Technique 167 * @param priorityTargets Set of Coord of important targets to include in the area of this Technique, preferring to target a single priorityTarget over four lesserTargets. 168 * @param lesserTargets Set of Coord of desirable targets to include in the area of this Technique, as many as possible without excluding priorityTargets. 169 * @param requiredExclusions Set of Coord where each value is something this Technique will really try to avoid. 170 * @return OrderedMap of Coord keys representing target points to pass to apply, to ArrayList of Coord values representing what targets' locations will be affected. 171 */ 172 public OrderedMap<Coord, ArrayList<Coord>> idealLocations(Coord user, Set<Coord> priorityTargets, Set<Coord> lesserTargets, Set<Coord> requiredExclusions) { 173 aoe.setOrigin(user); 174 return aoe.idealLocations(priorityTargets, lesserTargets, requiredExclusions); 175 } 176 177 /** 178 * This does one last validation of the location aimAt (checking that it is within the valid range for this 179 * Technique) before getting the area affected by the AOE targeting that cell. It considers the origin of the AOE 180 * to be the Coord parameter user, for purposes of directional limitations and for AOE implementations that need 181 * the user's location, such as ConeAOE and LineAOE. 182 * 183 * YOU MUST CALL setMap() with the current map status at some point before using this method, and call it again if 184 * the map changes. Failure to do so can cause serious bugs, from logic errors where monsters consider a door 185 * closed when it is open or vice versa, to an ArrayIndexOutOfBoundsException being thrown if the player moved to a 186 * differently-sized map and the Technique tries to use the previous map with coordinates from the new one. 187 * 188 * @param user The position of the Technique's user, x first, y second. 189 * @param aimAt A target Coord typically obtained from idealLocations that determines how to position the AOE. 190 * @return a HashMap of Coord keys to Double values from 1.0 (fully affected) to 0.0 (unaffected). 191 */ 192 public OrderedMap<Coord, Double> apply(Coord user, Coord aimAt) 193 { 194 aoe.setOrigin(user); 195 aoe.shift(aimAt); 196 return aoe.findArea(); 197 } 198 199 /** 200 * A quick yes-or-no check for whether a {@code user} at a given Coord can use this Technique to target the given 201 * Coord of a {@code possibleTarget}. There doesn't need to be a creature in the possibleTarget cell, since area of 202 * effect Techniques could still affect nearby creatures. Returns true if possibleTarget is a viable target cell for 203 * the given user Coord with this Technique, or false otherwise. 204 * @param user the Coord of the starting cell of this Technique, usually the position of the Technique's user 205 * @param possibleTarget a Coord that could maybe be a viable target 206 * @return true if this Technique can be used to target {@code possibleTarget} from the position {@code user}, or false otherwise 207 */ 208 public boolean canTarget(Coord user, Coord possibleTarget) 209 { 210 if(aoe == null || user == null || possibleTarget == null || 211 !AreaUtils.verifyReach(aoe.getReach(), user, possibleTarget)) return false; 212 Radius radiusStrategy = aoe.getMetric(); 213 Coord[] path = Bresenham.line2D_(user.x, user.y, possibleTarget.x, possibleTarget.y); 214 double rad = radiusStrategy.radius(user.x, user.y, possibleTarget.x, possibleTarget.y); 215 if(rad < aoe.getMinRange() || rad > aoe.getMaxRange()) { 216 return false; 217 } 218 Coord p; 219 for (int i = 0; i < path.length; i++) { 220 p = path[i]; 221 if (p.x == possibleTarget.x && p.y == possibleTarget.y) { 222 return true;//reached the end 223 } 224 if ((p.x != user.x || p.y != user.y) && dungeon[p.x][p.y] == '#') { 225 // if this isn't the starting cell and the map has a wall here, then stop and return false 226 return false; 227 } 228 } 229 return false;//never got to the target point 230 } 231 /** 232 * Gets all possible target-able Coords when using this technique from the given Coord {@code user}, returning them 233 * in a GreasedRegion. Note that a GreasedRegion can be used as a Collection of Coord with a fairly fast 234 * {@link GreasedRegion#contains(Coord)} method, or at least faster than an ArrayList of Coord. GreasedRegion values 235 * are mutable, like arrays, so if you want to edit the returned value you can do so without constructing additional 236 * objects. This works by getting a FOV map (using shadowcasting and the same metric/radius type as the {@link #aoe} 237 * field on this Technique) to figure out what cells are visible, then eliminating cells that don't match the 238 * minimum range on the AOE or aren't legal targets because of its AimLimit. 239 * <br> 240 * This method isn't especially efficient because it needs to construct a temporary 2D array for the FOV to use as 241 * well as an additional temporary 2D array for resistances. This generates its resistance map with 242 * {@link DungeonUtility#generateSimpleResistances(char[][])} every time; if you want other resistances, you should 243 * use {@link #possibleTargets(Coord, double[][])} instead. 244 * @param user the position of the user of this Technique 245 * @return all possible Coord values that can be used as targets for this Technique from the given starting Coord, as a GreasedRegion 246 */ 247 public GreasedRegion possibleTargets(Coord user) 248 { 249 return possibleTargets(user, DungeonUtility.generateSimpleResistances(dungeon)); 250 } 251 /** 252 * Gets all possible target-able Coords when using this technique from the given Coord {@code user}, returning them 253 * in a GreasedRegion. This takes a 2D double array as a resistance map, the same kind used by {@link FOV}, which 254 * can be obtained with {@link DungeonUtility#generateResistances(char[][])} or 255 * {@link DungeonUtility#generateSimpleResistances(char[][])}. Note that a GreasedRegion can be used as a Collection 256 * of Coord with a fairly fast {@link GreasedRegion#contains(Coord)} method, or at least faster than an ArrayList of 257 * Coord. GreasedRegion values are mutable, like arrays, so if you want to edit the returned value you can do so 258 * without constructing additional objects. This works by getting a FOV map (using shadowcasting and the same 259 * metric/radius type as the {@link #aoe} field on this Technique) to figure out what cells are visible, then 260 * eliminating cells that don't match the minimum range on the AOE or aren't legal targets because of its AimLimit. 261 * <br> 262 * This method isn't especially efficient because it needs to construct a temporary 2D array for the FOV to use, but 263 * it is more efficient than {@link #possibleTargets(Coord)}, which needs to construct an additional temporary 2D 264 * array. You may also want to reuse an existing resistance map, and you can with this method. 265 * @param user the position of the user of this Technique 266 * @param resistanceMap a 2D double array where walls are 1.0 and other values are less; often produced by {@link DungeonUtility#generateSimpleResistances(char[][])} 267 * @return all possible Coord values that can be used as targets for this Technique from the given starting Coord, as a GreasedRegion 268 */ 269 public GreasedRegion possibleTargets(Coord user, double[][] resistanceMap) { 270 if (aoe.getMaxRange() <= 0) return new GreasedRegion(user, dungeon.length, dungeon[0].length); 271 double[][] fovmap = new double[dungeon.length][dungeon[0].length]; 272 FOV.reuseFOV(resistanceMap, fovmap, user.x, user.y, aoe.getMaxRange(), aoe.getMetric()); 273 double rangeBound = 1.0001 - ((double) aoe.getMinRange() / aoe.getMaxRange()); 274 AimLimit limit = aoe.getLimitType(); 275 if (limit == null) limit = AimLimit.FREE; 276 switch (limit) { 277 case ORTHOGONAL: { 278 // Thanks, SillyNameRandomNumber, for finding this fix 279 return new GreasedRegion(fovmap, 0.0001, rangeBound) 280 .removeRectangle(0, 0, user.x, user.y) 281 .removeRectangle(0, user.y + 1, user.x, dungeon[0].length - user.y - 1) 282 .removeRectangle(user.x + 1, 0, dungeon.length - user.x - 1, user.y) 283 .removeRectangle(user.x + 1, user.y + 1, dungeon.length - user.x - 1, dungeon[0].length - user.y - 1); 284 } 285 case DIAGONAL: { 286 GreasedRegion all = new GreasedRegion(fovmap, 0.0001, rangeBound), used = new GreasedRegion(dungeon.length, dungeon[0].length); 287 for (Coord c : all) { 288 if (Math.abs(c.x - user.x) == Math.abs(c.y - user.y)) 289 used.insert(c); 290 } 291 return used; 292 } 293 case EIGHT_WAY: { 294 GreasedRegion all = new GreasedRegion(fovmap, 0.0001, rangeBound), used = new GreasedRegion(dungeon.length, dungeon[0].length); 295 for (Coord c : all) { 296 if (Math.abs(c.x - user.x) == Math.abs(c.y - user.y) || c.x == user.x || c.y == user.y) 297 used.insert(c); 298 } 299 return used; 300 } 301 default: 302 return new GreasedRegion(fovmap, 0.0001, rangeBound); 303 } 304 } 305}