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}