001package squidpony.squidgrid; 002 003import squidpony.ArrayTools; 004import squidpony.squidgrid.mapping.DungeonUtility; 005import squidpony.squidmath.Coord; 006import squidpony.squidmath.GreasedRegion; 007import squidpony.squidmath.MathExtras; 008import squidpony.squidmath.NumberTools; 009 010import java.io.Serializable; 011import java.util.*; 012 013/** 014 * This class provides methods for calculating Field of View in grids. Field of 015 * View (FOV) algorithms determine how much area surrounding a point can be 016 * seen. They return a 2D array of doubles, representing the amount of view 017 * (typically sight, but perhaps sound, smell, etc.) which the origin has of 018 * each cell. In the returned 2D array, 1.0 is always "fully seen," while 0.0 019 * is always "unseen." Values in between are much more common, and they enable 020 * this class to be used for lighting effects. 021 * <br> 022 * The input resistanceMap is considered the percent of opacity. This resistance 023 * is on top of the resistance applied from the light spreading out. You can 024 * obtain a resistance map easily with the {@link #generateResistances(char[][])} 025 * method, which uses defaults for common chars used in SquidLib, but you may 026 * also want to create a resistance map manually if a given char means something 027 * very different in your game. This is easy enough to do by looping over all the 028 * x,y positions in your char[][] map and running a switch statement on each char, 029 * assigning a double to the same x,y position in a double[][]. The value should 030 * be between 0.0 (unblocked) for things light passes through, 1.0 (blocked) for 031 * things light can't pass at all, and possibly other values if you have 032 * translucent obstacles. There's {@link #generateSimpleResistances(char[][])} as 033 * well, which only returns 1.0 (fully blocked) or 0.0 (passable), and 3x3 subcell 034 * variants, which produce a resistance map that is 3 times wider and 3 times 035 * taller than the input map. The subcell variants have especially useful behavior 036 * when using {@link DungeonUtility#hashesToLines(char[][])} to draw a map with 037 * box-drawing characters, since these 3x3 resistance maps will line up blocking 038 * cells to where a box-drawing line is. 039 * <br> 040 * The returned light map is considered the percent of light in the cells. 041 * <br> 042 * All implementations for FOV here (that is, Ripple FOV and Shadow FOV) provide 043 * percentage levels for partially-lit or partially-seen cells. This leads to a 044 * straightforward implementation of soft lighting using an FOV result -- just 045 * mix the background or floor color of a cell, however you represent it, with 046 * a very light color (like pastel yellow), with the percentage of the light 047 * color to mix in equal to the percent of light in the FOV map. 048 * <br> 049 * All solvers perform bounds checking so solid borders in the map are not 050 * required. 051 * <br> 052 * For calculating FOV maps, this class provides both instance methods, which 053 * attempt to reuse the same 2D array for light stored in the object, and static 054 * methods, which take a light 2D array as an argument and edit it in-place. 055 * In older versions of SquidLib, constantly allocating and returning 2D double 056 * arrays on each call dragged performance down, but both of the new methods 057 * should perform well. 058 * <br> 059 * Static methods are provided to add together FOV maps in the simple way 060 * (disregarding visibility of distant FOV from a given cell), or the more 061 * practical way for roguelikes (where a cell needs to be within line-of-sight 062 * in the first place for a distant light to illuminate it). The second method 063 * relies on an LOS map, which is essentially the same as a very-high-radius 064 * FOV map and can be easily obtained with calculateLOSMap(). 065 * <br> 066 * If you want to iterate through cells that are visible in a double[][] returned 067 * by FOV, you can pass that double[][] to the constructor for GreasedRegion, and 068 * you can use the GreasedRegion as a reliably-ordered Collection of Coord (among 069 * other things). The order GreasedRegion iterates in is somewhat strange, and 070 * doesn't, for example, start at the center of an FOV map, but it will be the 071 * same every time you create a GreasedRegion with the same FOV map (or the same 072 * visible Coords). 073 * <br> 074 * This class is not thread-safe. This is generally true for most of SquidLib. 075 * 076 * @author Eben Howard - http://squidpony.com - howard@squidpony.com 077 */ 078public class FOV implements Serializable { 079 private static final long serialVersionUID = 3258723684733275798L; 080 /** 081 * Performs FOV by pushing values outwards from the source location. 082 * It will go around corners a bit. This corresponds to a 083 * {@code rippleLooseness} of 2 in {@link #reuseRippleFOV(double[][], double[][], int, int, int, double, Radius)}. 084 */ 085 public static final int RIPPLE = 1; 086 /** 087 * Performs FOV by pushing values outwards from the source location. 088 * It will spread around edges like smoke or water, but maintain a 089 * tendency to curl towards the start position when going around 090 * edges. This corresponds to a {@code rippleLooseness} of 3 091 * in {@link #reuseRippleFOV(double[][], double[][], int, int, int, double, Radius)}. 092 */ 093 public static final int RIPPLE_LOOSE = 2; 094 /** 095 * Performs FOV by pushing values outwards from the source location. 096 * It will only go around corners slightly. This corresponds to a 097 * {@code rippleLooseness} of 1 in {@link #reuseRippleFOV(double[][], double[][], int, int, int, double, Radius)}. 098 */ 099 public static final int RIPPLE_TIGHT = 3; 100 /** 101 * Performs FOV by pushing values outwards from the source location. 102 * It will go around corners massively. This corresponds to a 103 * {@code rippleLooseness} of 6 in {@link #reuseRippleFOV(double[][], double[][], int, int, int, double, Radius)}. 104 */ 105 public static final int RIPPLE_VERY_LOOSE = 4; 106 /** 107 * Uses Shadow Casting FOV algorithm. Returns a percentage from 1.0 (center of 108 * FOV) to 0.0 (outside of FOV). 109 */ 110 public static final int SHADOW = 5; 111 private int type = SHADOW; 112 113 /** 114 * Data allocated in the previous calls to the public API, if any. Used to 115 * save allocations when multiple calls are done on the same instance. 116 */ 117 protected double[][] light; 118 /** 119 * Data allocated in the previous calls to the public API, if any. Used to 120 * save allocations when multiple calls are done on the same instance. 121 */ 122 protected GreasedRegion nearLight; 123 124 protected static final Direction[] ccw = new Direction[] 125 {Direction.UP_RIGHT, Direction.UP_LEFT, Direction.DOWN_LEFT, Direction.DOWN_RIGHT, Direction.UP_RIGHT}, 126 ccw_full = new Direction[]{Direction.RIGHT, Direction.UP_RIGHT, Direction.UP, Direction.UP_LEFT, 127 Direction.LEFT, Direction.DOWN_LEFT, Direction.DOWN, Direction.DOWN_RIGHT}; 128 private static final ArrayDeque<Coord> dq = new ArrayDeque<>(); 129 private static final GreasedRegion lightWorkspace = new GreasedRegion(64, 64); 130 131 /** 132 * Creates a solver which will use the default SHADOW solver. 133 */ 134 public FOV() { 135 } 136 137 /** 138 * Creates a solver which will use the provided FOV solver type, typically one of {@link #SHADOW} (the default), 139 * {@link #RIPPLE}, {@link #RIPPLE_TIGHT}, {@link #RIPPLE_LOOSE}, or {@link #RIPPLE_VERY_LOOSE}. 140 * @param type 141 */ 142 public FOV(int type) { 143 this.type = type; 144 } 145 146 /** 147 * Calculates the Field Of View for the provided map from the given x, y 148 * coordinates. Returns a light map where the values represent a percentage 149 * of fully lit. 150 * 151 * The starting point for the calculation is considered to be at the center 152 * of the origin cell. Radius determinations based on Euclidean 153 * calculations. The light will be treated as having infinite possible 154 * radius. 155 * 156 * @param resistanceMap the grid of cells to calculate on; the kind made by {@link #generateResistances(char[][])} 157 * @param startx the horizontal component of the starting location 158 * @param starty the vertical component of the starting location 159 * @return the computed light grid 160 */ 161 public double[][] calculateFOV(double[][] resistanceMap, int startx, int starty) { 162 return calculateFOV(resistanceMap, startx, starty, Integer.MAX_VALUE); 163 } 164 165 /** 166 * Calculates the Field Of View for the provided map from the given x, y 167 * coordinates. Returns a light map where the values represent a percentage 168 * of fully lit. 169 * 170 * The starting point for the calculation is considered to be at the center 171 * of the origin cell. Radius determinations based on Euclidean 172 * calculations. 173 * 174 * @param resistanceMap the grid of cells to calculate on; the kind made by {@link #generateResistances(char[][])} 175 * @param startx the horizontal component of the starting location 176 * @param starty the vertical component of the starting location 177 * @param radius the distance the light will extend to 178 * @return the computed light grid 179 */ 180 public double[][] calculateFOV(double[][] resistanceMap, int startx, int starty, double radius) { 181 return calculateFOV(resistanceMap, startx, starty, radius, Radius.CIRCLE); 182 } 183 184 /** 185 * Calculates the Field Of View for the provided map from the given x, y 186 * coordinates. Returns a light map where the values represent a percentage 187 * of fully lit. 188 * 189 * The starting point for the calculation is considered to be at the center 190 * of the origin cell. Radius determinations are determined by the provided 191 * RadiusStrategy. 192 * 193 * @param resistanceMap the grid of cells to calculate on; the kind made by {@link #generateResistances(char[][])} 194 * @param startX the horizontal component of the starting location 195 * @param startY the vertical component of the starting location 196 * @param radius the distance the light will extend to 197 * @param radiusTechnique provides a means to calculate the radius as desired 198 * @return the computed light grid 199 */ 200 public double[][] calculateFOV(double[][] resistanceMap, int startX, int startY, double radius, Radius radiusTechnique) { 201 double decay = 1.0 / radius; 202 203 int width = resistanceMap.length; 204 int height = resistanceMap[0].length; 205 206 initializeLightMap(width, height); 207 light[startX][startY] = Math.min(1.0, radius);//make the starting space full power unless radius is tiny 208 209 switch (type) { 210 case RIPPLE: 211 case RIPPLE_LOOSE: 212 case RIPPLE_TIGHT: 213 case RIPPLE_VERY_LOOSE: 214 initializeNearLight(width, height); 215 doRippleFOV(light, rippleValue(type), startX, startY, decay, radius, resistanceMap, nearLight, radiusTechnique); 216 break; 217 default: 218 for (Direction d : Direction.DIAGONALS) { 219 shadowCast(0, d.deltaX, d.deltaY, 0, radius, startX, startY, decay, light, resistanceMap, radiusTechnique); 220 shadowCast(d.deltaX, 0, 0, d.deltaY, radius, startX, startY, decay, light, resistanceMap, radiusTechnique); 221 } 222 break; 223 } 224 return light; 225 } 226 227 /** 228 * Calculates the Field Of View for the provided map from the given x, y 229 * coordinates. Returns a light map where the values represent a percentage 230 * of fully lit. 231 * 232 * The starting point for the calculation is considered to be at the center 233 * of the origin cell. Radius determinations are determined by the provided 234 * RadiusStrategy. A conical section of FOV is lit by this method if 235 * span is greater than 0. 236 * 237 * @param resistanceMap the grid of cells to calculate on; the kind made by {@link #generateResistances(char[][])} 238 * @param startX the horizontal component of the starting location 239 * @param startY the vertical component of the starting location 240 * @param radius the distance the light will extend to 241 * @param radiusTechnique provides a means to calculate the radius as desired 242 * @param angle the angle in degrees that will be the center of the FOV cone, 0 points right 243 * @param span the angle in degrees that measures the full arc contained in the FOV cone 244 * @return the computed light grid 245 */ 246 public double[][] calculateFOV(double[][] resistanceMap, int startX, int startY, double radius, 247 Radius radiusTechnique, double angle, double span) { 248 double decay = 1.0 / radius; 249 angle = ((angle >= 360.0 || angle < 0.0) 250 ? MathExtras.remainder(angle, 360.0) : angle) * 0.002777777777777778; 251 span = span * 0.002777777777777778; 252 int width = resistanceMap.length; 253 int height = resistanceMap[0].length; 254 255 initializeLightMap(width, height); 256 light[startX][startY] = Math.min(1.0, radius);//make the starting space full power unless radius is tiny 257 258 switch (type) { 259 case RIPPLE: 260 case RIPPLE_LOOSE: 261 case RIPPLE_TIGHT: 262 case RIPPLE_VERY_LOOSE: 263 initializeNearLight(width, height); 264 doRippleFOV(light, rippleValue(type), startX, startY, decay, radius, resistanceMap, nearLight, radiusTechnique, angle, span); 265 break; 266 default: 267 light = shadowCastLimited(1, 1.0, 0.0, 0, 1, 1, 0, radius, startX, startY, decay, light, resistanceMap, radiusTechnique, angle, span); 268 light = shadowCastLimited(1, 1.0, 0.0, 1, 0, 0, 1, radius, startX, startY, decay, light, resistanceMap, radiusTechnique, angle, span); 269 270 light = shadowCastLimited(1, 1.0, 0.0, 0, -1, 1, 0, radius, startX, startY, decay, light, resistanceMap, radiusTechnique, angle, span); 271 light = shadowCastLimited(1, 1.0, 0.0, -1, 0, 0, 1, radius, startX, startY, decay, light, resistanceMap, radiusTechnique, angle, span); 272 273 light = shadowCastLimited(1, 1.0, 0.0, 0, -1, -1, 0, radius, startX, startY, decay, light, resistanceMap, radiusTechnique, angle, span); 274 light = shadowCastLimited(1, 1.0, 0.0, -1, 0, 0, -1, radius, startX, startY, decay, light, resistanceMap, radiusTechnique, angle, span); 275 276 light = shadowCastLimited(1, 1.0, 0.0, 0, 1, -1, 0, radius, startX, startY, decay, light, resistanceMap, radiusTechnique, angle, span); 277 light = shadowCastLimited(1, 1.0, 0.0, 1, 0, 0, -1, radius, startX, startY, decay, light, resistanceMap, radiusTechnique, angle, span); 278 break; 279 } 280 281 return light; 282 } 283 284 285 /** 286 * Calculates the Field Of View for the provided map from the given x, y 287 * coordinates. Assigns to, and returns, a light map where the values 288 * represent a percentage of fully lit. Always uses shadowcasting FOV, 289 * which allows this method to be static since it doesn't need to keep any 290 * state around, and can reuse the state the user gives it via the 291 * {@code light} parameter. The values in light are always cleared before 292 * this is run, because prior state can make this give incorrect results. 293 * <br> 294 * The starting point for the calculation is considered to be at the center 295 * of the origin cell. Radius determinations based on Euclidean 296 * calculations. The light will be treated as having infinite possible 297 * radius. 298 * <br> 299 * This static method is equivalent to the static method {@link #reuseFOV(double[][], double[][], int, int)}, 300 * but all of the overloads are called reuseFOV(), so this method name is discouraged for new code. Both delegate to 301 * {@link #reuseFOV(double[][], double[][], int, int, double, Radius)} anyway. 302 * 303 * @param resistanceMap the grid of cells to calculate on; the kind made by {@link #generateResistances(char[][])} 304 * @param light a non-null 2D double array that will have its contents overwritten, modified, and returned 305 * @param startx the horizontal component of the starting location 306 * @param starty the vertical component of the starting location 307 * @return the computed light grid (the same as {@code light}) 308 */ 309 public static double[][] calculateFOV(double[][] resistanceMap, double[][] light, int startx, int starty) { 310 return reuseFOV(resistanceMap, light, startx, starty, Integer.MAX_VALUE, Radius.CIRCLE); 311 } 312 313 /** 314 * Calculates the Field Of View for the provided map from the given x, y 315 * coordinates. Assigns to, and returns, a light map where the values 316 * represent a percentage of fully lit. Always uses shadowcasting FOV, 317 * which allows this method to be static since it doesn't need to keep any 318 * state around, and can reuse the state the user gives it via the 319 * {@code light} parameter. The values in light are always cleared before 320 * this is run, because prior state can make this give incorrect results. 321 * <br> 322 * The starting point for the calculation is considered to be at the center 323 * of the origin cell. Radius determinations based on Euclidean 324 * calculations. The light will be treated as having infinite possible 325 * radius. 326 * <br> 327 * This static method is equivalent to the static method {@link #calculateFOV(double[][], double[][], int, int)}, 328 * but all of the overloads are called reuseFOV(), so this method name is preferred in new code. Both delegate to 329 * {@link #reuseFOV(double[][], double[][], int, int, double, Radius)} anyway. 330 * 331 * @param resistanceMap the grid of cells to calculate on; the kind made by {@link #generateResistances(char[][])} 332 * @param light a non-null 2D double array that will have its contents overwritten, modified, and returned 333 * @param startx the horizontal component of the starting location 334 * @param starty the vertical component of the starting location 335 * @return the computed light grid (the same as {@code light}) 336 */ 337 public static double[][] reuseFOV(double[][] resistanceMap, double[][] light, int startx, int starty) { 338 return reuseFOV(resistanceMap, light, startx, starty, Integer.MAX_VALUE, Radius.CIRCLE); 339 } 340 341 /** 342 * Calculates the Field Of View for the provided map from the given x, y 343 * coordinates. Assigns to, and returns, a light map where the values 344 * represent a percentage of fully lit. Always uses shadowcasting FOV, 345 * which allows this method to be static since it doesn't need to keep any 346 * state around, and can reuse the state the user gives it via the 347 * {@code light} parameter. The values in light are always cleared before 348 * this is run, because prior state can make this give incorrect results. 349 * <br> 350 * The starting point for the calculation is considered to be at the center 351 * of the origin cell. Radius determinations based on Euclidean 352 * calculations. 353 * 354 * @param resistanceMap the grid of cells to calculate on; the kind made by {@link #generateResistances(char[][])} 355 * @param startx the horizontal component of the starting location 356 * @param starty the vertical component of the starting location 357 * @param radius the distance the light will extend to 358 * @return the computed light grid 359 */ 360 public static double[][] reuseFOV(double[][] resistanceMap, double[][] light, int startx, int starty, double radius) { 361 return reuseFOV(resistanceMap, light, startx, starty, radius, Radius.CIRCLE); 362 } 363 364 365 /** 366 * Calculates the Field Of View for the provided map from the given x, y 367 * coordinates. Assigns to, and returns, a light map where the values 368 * represent a percentage of fully lit. Always uses shadowcasting FOV, 369 * which allows this method to be static since it doesn't need to keep any 370 * state around, and can reuse the state the user gives it via the 371 * {@code light} parameter. The values in light are always cleared before 372 * this is run, because prior state can make this give incorrect results. 373 * <br> 374 * The starting point for the calculation is considered to be at the center 375 * of the origin cell. Radius determinations are determined by the provided 376 * RadiusStrategy. 377 * @param resistanceMap the grid of cells to calculate on; the kind made by {@link #generateResistances(char[][])} 378 * @param light the grid of cells to assign to; may have existing values, and 0.0 is used to mean "unlit" 379 * @param startX the horizontal component of the starting location 380 * @param startY the vertical component of the starting location 381 * @param radius the distance the light will extend to 382 * @param radiusTechnique provides a means to calculate the radius as desired 383 * @return the computed light grid, which is the same 2D array as the value assigned to {@code light} 384 */ 385 public static double[][] reuseFOV(double[][] resistanceMap, double[][] light, int startX, int startY, double radius, Radius radiusTechnique) 386 { 387 double decay = 1.0 / radius; 388 ArrayTools.fill(light, 0); 389 light[startX][startY] = Math.min(1.0, radius);//make the starting space full power unless radius is tiny 390 391 392 shadowCast(0, 1, 1, 0, radius, startX, startY, decay, light, resistanceMap, radiusTechnique); 393 shadowCast(1, 0, 0, 1, radius, startX, startY, decay, light, resistanceMap, radiusTechnique); 394 395 shadowCast(0, 1, -1, 0, radius, startX, startY, decay, light, resistanceMap, radiusTechnique); 396 shadowCast(1, 0, 0, -1, radius, startX, startY, decay, light, resistanceMap, radiusTechnique); 397 398 shadowCast(0, -1, -1, 0, radius, startX, startY, decay, light, resistanceMap, radiusTechnique); 399 shadowCast(-1, 0, 0, -1, radius, startX, startY, decay, light, resistanceMap, radiusTechnique); 400 401 shadowCast(0, -1, 1, 0, radius, startX, startY, decay, light, resistanceMap, radiusTechnique); 402 shadowCast(-1, 0, 0, 1, radius, startX, startY, decay, light, resistanceMap, radiusTechnique); 403 return light; 404 } 405 /** 406 * Calculates the Field Of View for the provided map from the given x, y 407 * coordinates. Assigns to, and returns, a light map where the values 408 * represent a percentage of fully lit. Always uses shadowcasting FOV, 409 * which allows this method to be static since it doesn't need to keep any 410 * state around, and can reuse the state the user gives it via the 411 * {@code light} parameter. The values in light are always cleared before 412 * this is run, because prior state can make this give incorrect results. 413 * <br> 414 * The starting point for the calculation is considered to be at the center 415 * of the origin cell. Radius determinations are determined by the provided 416 * RadiusStrategy. 417 * @param resistanceMap the grid of cells to calculate on; the kind made by {@link #generateResistances(char[][])} 418 * @param light the grid of cells to assign to; may have existing values, and 0.0 is used to mean "unlit" 419 * @param startX the horizontal component of the starting location 420 * @param startY the vertical component of the starting location 421 * @param radius the distance the light will extend to 422 * @param radiusTechnique provides a means to calculate the radius as desired 423 * @return the computed light grid, which is the same 2D array as the value assigned to {@code light} 424 */ 425 public static double[][] reuseFOVSymmetrical(double[][] resistanceMap, double[][] light, int startX, int startY, double radius, Radius radiusTechnique) 426 { 427 double decay = 1.0 / radius; 428 ArrayTools.fill(light, 0); 429 light[startX][startY] = Math.min(1.0, radius);//make the starting space full power unless radius is tiny 430 431 432 shadowCast(0, 1, 1, 0, radius, startX, startY, decay, light, resistanceMap, radiusTechnique); 433 for (int row = 0; row <= radius + 1.0; row++) { 434 for (int col = Math.max(1,row); col <= radius + 1.0; col++) { 435 if(startX - col >= 0 && startY - row >= 0 && resistanceMap[startX - col][startY - row] < 1.0 && 436 !shadowCastCheck(1, 1.0, 0.0, 0, -1, -1, 0, radius, startX - col, startY - row, decay, light, resistanceMap, radiusTechnique, 0, 0, light.length, light[0].length, startX, startY)) 437 light[startX - col][startY - row] = 0.0; 438 } 439 } 440 shadowCast(1, 0, 0, 1, radius, startX, startY, decay, light, resistanceMap, radiusTechnique); 441 for (int col = 0; col <= radius + 1.0; col++) { 442 for (int row = Math.max(1,col); row <= radius + 1.0; row++) { 443 if(startX - col >= 0 && startY - row >= 0 && resistanceMap[startX - col][startY - row] < 1.0 && 444 !shadowCastCheck(1, 1.0, 0.0, -1, 0, 0, -1, radius, startX - col, startY - row, decay, light, resistanceMap, radiusTechnique, 0, 0, light.length, light[0].length, startX, startY)) 445 light[startX - col][startY - row] = 0.0; 446 } 447 } 448 449 shadowCast(0, 1, -1, 0, radius, startX, startY, decay, light, resistanceMap, radiusTechnique); 450 for (int row = 0; row <= radius + 1.0; row++) { 451 for (int col = Math.max(1,row); col <= radius + 1.0; col++) { 452 if(startX - col >= 0 && startY + row < light[0].length && resistanceMap[startX - col][startY + row] < 1.0 && 453 !shadowCastCheck(1, 1.0, 0.0, 0, -1, 1, 0, radius, startX - col, startY + row, decay, light, resistanceMap, radiusTechnique, 0, 0, light.length, light[0].length, startX, startY)) 454 light[startX - col][startY + row] = 0.0; 455 } 456 } 457 shadowCast(1, 0, 0, -1, radius, startX, startY, decay, light, resistanceMap, radiusTechnique); 458 for (int col = 0; col <= radius + 1.0; col++) { 459 for (int row = Math.max(1,col); row <= radius + 1.0; row++) { 460 if(startX - col >= 0 && startY + row < light[0].length && resistanceMap[startX - col][startY + row] < 1.0 && 461 !shadowCastCheck(1, 1.0, 0.0, -1, 0, 0, 1, radius, startX - col, startY + row, decay, light, resistanceMap, radiusTechnique, 0, 0, light.length, light[0].length, startX, startY)) 462 light[startX - col][startY + row] = 0.0; 463 } 464 } 465 466 shadowCast(0, -1, -1, 0, radius, startX, startY, decay, light, resistanceMap, radiusTechnique); 467 for (int row = 0; row <= radius + 1.0; row++) { 468 for (int col = Math.max(1,row); col <= radius + 1.0; col++) { 469 if(startX + col < light.length && startY + row < light[0].length && resistanceMap[startX + col][startY + row] < 1.0 && 470 !shadowCastCheck(1, 1.0, 0.0, 0, 1, 1, 0, radius, startX + col, startY + row, decay, light, resistanceMap, radiusTechnique, 0, 0, light.length, light[0].length, startX, startY)) 471 light[startX + col][startY + row] = 0.0; 472 } 473 } 474 shadowCast(-1, 0, 0, -1, radius, startX, startY, decay, light, resistanceMap, radiusTechnique); 475 for (int col = 0; col <= radius + 1.0; col++) { 476 for (int row = Math.max(1,col); row <= radius + 1.0; row++) { 477 if(startX + col < light.length && startY + row < light[0].length && resistanceMap[startX + col][startY + row] < 1.0 && 478 !shadowCastCheck(1, 1.0, 0.0, 1, 0, 0, 1, radius, startX + col, startY + row, decay, light, resistanceMap, radiusTechnique, 0, 0, light.length, light[0].length, startX, startY)) 479 light[startX + col][startY + row] = 0.0; 480 } 481 } 482 483 shadowCast(0, -1, 1, 0, radius, startX, startY, decay, light, resistanceMap, radiusTechnique); 484 for (int row = 0; row <= radius + 1.0 && startY + row < light[0].length; row++) { 485 for (int col = Math.max(1,row); col <= radius + 1.0; col++) { 486 if(startX + col < light.length && startY - row >= 0 && resistanceMap[startX + col][startY - row] < 1.0 && 487 !shadowCastCheck(1, 1.0, 0.0, 0, 1, -1, 0, radius, startX + col, startY - row, decay, light, resistanceMap, radiusTechnique, 0, 0, light.length, light[0].length, startX, startY)) 488 light[startX + col][startY - row] = 0.0; 489 } 490 } 491 shadowCast(-1, 0, 0, 1, radius, startX, startY, decay, light, resistanceMap, radiusTechnique); 492 for (int col = 0; col <= radius + 1.0; col++) { 493 for (int row = Math.max(1,col); row <= radius + 1.0; row++) { 494 if(startX + col < light.length && startY - row >= 0 && resistanceMap[startX + col][startY - row] < 1.0 && 495 !shadowCastCheck(1, 1.0, 0.0, 1, 0, 0, -1, radius, startX + col, startY - row, decay, light, resistanceMap, radiusTechnique, 0, 0, light.length, light[0].length, startX, startY)) 496 light[startX + col][startY - row] = 0.0; 497 } 498 } 499 return light; 500 } 501 /** 502 * Calculates which cells have line of sight from the given x, y coordinates. 503 * Assigns to, and returns, a light map where the values 504 * are always either 0.0 for "not in line of sight" or 1.0 for "in line of 505 * sight," which doesn't mean a cell is actually visible if there's no light 506 * in that cell. Always uses shadowcasting FOV, which allows this method to 507 * be static since it doesn't need to keep any state around, and can reuse the 508 * state the user gives it via the {@code light} parameter. The values in light 509 * are always cleared before this is run, because prior state can make this give 510 * incorrect results. 511 * <br> 512 * The starting point for the calculation is considered to be at the center 513 * of the origin cell. Radius determinations are pretty much irrelevant because 514 * the distance doesn't matter, only the presence of a clear line, but this uses 515 * {@link Radius#SQUARE} if it matters. 516 * @param resistanceMap the grid of cells to calculate on; the kind made by {@link #generateResistances(char[][])} 517 * @param light the grid of cells to assign to; may have existing values, and 0.0 is used to mean "no line" 518 * @param startX the horizontal component of the starting location 519 * @param startY the vertical component of the starting location 520 * @return the computed light grid, which is the same 2D array as the value assigned to {@code light} 521 */ 522 public static double[][] reuseLOS(double[][] resistanceMap, double[][] light, int startX, int startY) 523 { 524 return reuseLOS(resistanceMap, light, startX, startY, 0, 0, light.length, light[0].length); 525 } 526 /** 527 * Calculates which cells have line of sight from the given x, y coordinates. 528 * Assigns to, and returns, a light map where the values 529 * are always either 0.0 for "not in line of sight" or 1.0 for "in line of 530 * sight," which doesn't mean a cell is actually visible if there's no light 531 * in that cell. Always uses shadowcasting FOV, which allows this method to 532 * be static since it doesn't need to keep any state around, and can reuse the 533 * state the user gives it via the {@code light} parameter. The values in light 534 * are always cleared before this is run, because prior state can make this give 535 * incorrect results. 536 * <br> 537 * The starting point for the calculation is considered to be at the center 538 * of the origin cell. Radius determinations are pretty much irrelevant because 539 * the distance doesn't matter, only the presence of a clear line, but this uses 540 * {@link Radius#SQUARE} if it matters. 541 * @param resistanceMap the grid of cells to calculate on; the kind made by {@link #generateResistances(char[][])} 542 * @param light the grid of cells to assign to; may have existing values, and 0.0 is used to mean "no line" 543 * @param startX the horizontal component of the starting location 544 * @param startY the vertical component of the starting location 545 * @return the computed light grid, which is the same 2D array as the value assigned to {@code light} 546 */ 547 public static double[][] reuseLOS(double[][] resistanceMap, double[][] light, int startX, int startY, 548 int minX, int minY, int maxX, int maxY) 549 { 550 double radius = light.length + light[0].length; 551 double decay = 1.0 / radius; 552 ArrayTools.fill(light, 0); 553 light[startX][startY] = 1;//make the starting space full power 554 555 shadowCastBinary(1, 1.0, 0.0, 0, 1, 1, 0, radius, startX, startY, decay, light, resistanceMap, Radius.SQUARE, minX, minY, maxX, maxY); 556 shadowCastBinary(1, 1.0, 0.0, 1, 0, 0, 1, radius, startX, startY, decay, light, resistanceMap, Radius.SQUARE, minX, minY, maxX, maxY); 557 shadowCastBinary(1, 1.0, 0.0, 0, 1, -1, 0, radius, startX, startY, decay, light, resistanceMap, Radius.SQUARE, minX, minY, maxX, maxY); 558 shadowCastBinary(1, 1.0, 0.0, 1, 0, 0, -1, radius, startX, startY, decay, light, resistanceMap, Radius.SQUARE, minX, minY, maxX, maxY); 559 shadowCastBinary(1, 1.0, 0.0, 0, -1, -1, 0, radius, startX, startY, decay, light, resistanceMap, Radius.SQUARE, minX, minY, maxX, maxY); 560 shadowCastBinary(1, 1.0, 0.0, -1, 0, 0, -1, radius, startX, startY, decay, light, resistanceMap, Radius.SQUARE, minX, minY, maxX, maxY); 561 shadowCastBinary(1, 1.0, 0.0, 0, -1, 1, 0, radius, startX, startY, decay, light, resistanceMap, Radius.SQUARE, minX, minY, maxX, maxY); 562 shadowCastBinary(1, 1.0, 0.0, -1, 0, 0, 1, radius, startX, startY, decay, light, resistanceMap, Radius.SQUARE, minX, minY, maxX, maxY); 563 564 return light; 565 } 566 /** 567 * Calculates the Field Of View for the provided map from the given x, y 568 * coordinates, lighting at the given angle in degrees and covering a span 569 * centered on that angle, also in degrees. Assigns to, and returns, a light 570 * map where the values represent a percentage of fully lit. Always uses 571 * shadowcasting FOV, which allows this method to be static since it doesn't 572 * need to keep any state around, and can reuse the state the user gives it 573 * via the {@code light} parameter. The values in light are cleared before 574 * this is run, because prior state can make this give incorrect results. 575 * <br> 576 * The starting point for the calculation is considered to be at the center 577 * of the origin cell. Radius determinations are determined by the provided 578 * RadiusStrategy. A conical section of FOV is lit by this method if 579 * span is greater than 0. 580 * 581 * @param resistanceMap the grid of cells to calculate on; the kind made by {@link #generateResistances(char[][])} 582 * @param light the grid of cells to assign to; may have existing values, and 0.0 is used to mean "unlit" 583 * @param startX the horizontal component of the starting location 584 * @param startY the vertical component of the starting location 585 * @param radius the distance the light will extend to 586 * @param radiusTechnique provides a means to shape the FOV by changing distance calculation (circle, square, etc.) 587 * @param angle the angle in degrees that will be the center of the FOV cone, 0 points right 588 * @param span the angle in degrees that measures the full arc contained in the FOV cone 589 * @return the computed light grid 590 */ 591 public static double[][] reuseFOV(double[][] resistanceMap, double[][] light, int startX, int startY, 592 double radius, Radius radiusTechnique, double angle, double span) { 593 double decay = 1.0 / radius; 594 ArrayTools.fill(light, 0); 595 light[startX][startY] = Math.min(1.0, radius);//make the starting space full power unless radius is tiny 596 angle = ((angle >= 360.0 || angle < 0.0) 597 ? MathExtras.remainder(angle, 360.0) : angle) * 0.002777777777777778; 598 span = span * 0.002777777777777778; 599 600 601 light = shadowCastLimited(1, 1.0, 0.0, 0, 1, 1, 0, radius, startX, startY, decay, light, resistanceMap, radiusTechnique, angle, span); 602 light = shadowCastLimited(1, 1.0, 0.0, 1, 0, 0, 1, radius, startX, startY, decay, light, resistanceMap, radiusTechnique, angle, span); 603 604 light = shadowCastLimited(1, 1.0, 0.0, 0, -1, 1, 0, radius, startX, startY, decay, light, resistanceMap, radiusTechnique, angle, span); 605 light = shadowCastLimited(1, 1.0, 0.0, -1, 0, 0, 1, radius, startX, startY, decay, light, resistanceMap, radiusTechnique, angle, span); 606 607 light = shadowCastLimited(1, 1.0, 0.0, 0, -1, -1, 0, radius, startX, startY, decay, light, resistanceMap, radiusTechnique, angle, span); 608 light = shadowCastLimited(1, 1.0, 0.0, -1, 0, 0, -1, radius, startX, startY, decay, light, resistanceMap, radiusTechnique, angle, span); 609 610 light = shadowCastLimited(1, 1.0, 0.0, 0, 1, -1, 0, radius, startX, startY, decay, light, resistanceMap, radiusTechnique, angle, span); 611 light = shadowCastLimited(1, 1.0, 0.0, 1, 0, 0, -1, radius, startX, startY, decay, light, resistanceMap, radiusTechnique, angle, span); 612 return light; 613 } 614 615 /** 616 * Like the {@link #reuseFOV(double[][], double[][], int, int, double, Radius)} method, but this uses Ripple FOV 617 * with a configurable tightness/looseness (between 1, tightest, and 6, loosest). Other parameters are similar; you 618 * can get a resistance map from {@link #generateResistances(char[][])}, {@code light} will be modified and returned 619 * (it will be overwritten, but its size should be the same as the resistance map), there's a starting x,y position, 620 * a radius in cells, and a {@link Radius} enum constant to choose the distance measurement. 621 * @param resistanceMap probably calculated with {@link #generateResistances(char[][])}; 1.0 blocks light, 0.0 allows it 622 * @param light will be overwritten! Should be initialized with the same size as {@code resistanceMap} 623 * @param rippleLooseness affects spread; between 1 and 6, inclusive; 1 is tightest, 2 is normal, and 6 is loosest 624 * @param x starting x position to look from 625 * @param y starting y position to look from 626 * @param radius the distance to extend from the starting x,y position 627 * @param radiusTechnique how to measure distance; typically {@link Radius#CIRCLE}. 628 * @return {@code light}, after writing the FOV map into it; 1.0 is fully lit and 0.0 is unseen 629 */ 630 public static double[][] reuseRippleFOV(double[][] resistanceMap, double[][] light, int rippleLooseness, int x, int y, double radius, Radius radiusTechnique) { 631 ArrayTools.fill(light, 0); 632 light[x][y] = Math.min(1.0, radius);//make the starting space full power unless radius is tiny 633 lightWorkspace.resizeAndEmpty(light.length, light[0].length); 634 doRippleFOV(light, MathExtras.clamp(rippleLooseness, 1, 6), x, y, 1.0 / radius, radius, resistanceMap, lightWorkspace, radiusTechnique); 635 return light; 636 } 637 638 /** 639 * Like the {@link #reuseFOV(double[][], double[][], int, int, double, Radius, double, double)} method, but this 640 * uses Ripple FOV with a configurable tightness/looseness (between 1, tightest, and 6, loosest). Other parameters 641 * are similar; you can get a resistance map from {@link #generateResistances(char[][])}, {@code light} will be 642 * modified and returned (it will be overwritten, but its size should be the same as the resistance map), there's 643 * starting x,y position, a radius in cells, a {@link Radius} enum constant to choose the distance measurement, and 644 * the angle/span combination to specify a conical section of FOV (span is the total in degrees, centered on angle). 645 * @param resistanceMap probably calculated with {@link #generateResistances(char[][])}; 1.0 blocks light, 0.0 allows it 646 * @param light will be overwritten! Should be initialized with the same size as {@code resistanceMap} 647 * @param rippleLooseness affects spread; between 1 and 6, inclusive; 1 is tightest, 2 is normal, and 6 is loosest 648 * @param x starting x position to look from 649 * @param y starting y position to look from 650 * @param radius the distance to extend from the starting x,y position 651 * @param radiusTechnique how to measure distance; typically {@link Radius#CIRCLE}. 652 * @param angle the angle to center the conical FOV on 653 * @param span the total span in degrees for the conical FOV to cover 654 * @return {@code light}, after writing the FOV map into it; 1.0 is fully lit and 0.0 is unseen 655 */ 656 public static double[][] reuseRippleFOV(double[][] resistanceMap, double[][] light, int rippleLooseness, int x, int y, double radius, Radius radiusTechnique, double angle, double span) { 657 ArrayTools.fill(light, 0); 658 light[x][y] = Math.min(1.0, radius);//make the starting space full power unless radius is tiny 659 lightWorkspace.resizeAndEmpty(light.length, light[0].length); 660 angle = ((angle >= 360.0 || angle < 0.0) 661 ? MathExtras.remainder(angle, 360.0) : angle) * 0.002777777777777778; 662 span = span * 0.002777777777777778; 663 doRippleFOV(light, MathExtras.clamp(rippleLooseness, 1, 6), x, y, 1.0 / radius, radius, resistanceMap, lightWorkspace, radiusTechnique, angle, span); 664 return light; 665 } 666 667 /** 668 * Reuses the existing light 2D array and fills it with a straight-line bouncing path of light that reflects its way 669 * through the given resistanceMap from startX, startY until it uses up the given distance. The angle the path 670 * takes is given in degrees, and the angle used can change as obstacles are hit (reflecting backwards if it hits a 671 * corner pointing directly into or away from its path). This can be used something like an LOS method, but because 672 * the path can be traveled back over, an array or Queue becomes somewhat more complex, and the decreasing numbers 673 * for a straight line that stack may make more sense for how this could be used (especially with visual effects). 674 * This currently allows the path to pass through single-cell wall-like obstacles without changing direction, e.g. 675 * it passes through pillars, but will bounce if it hits a bigger wall. 676 * @param resistanceMap the grid of cells to calculate on; the kind made by {@link #generateResistances(char[][])} 677 * @param light the grid of cells to assign to; may have existing values, and 0.0 is used to mean "unlit" 678 * @param startX the horizontal component of the starting location 679 * @param startY the vertical component of the starting location 680 * @param distance the distance the light will extend to 681 * @param angle in degrees, the angle to start the path traveling in 682 * @return the given light parameter, after modifications 683 */ 684 public static double[][] bouncingLine(double[][] resistanceMap, double[][] light, int startX, int startY, double distance, double angle) 685 { 686 double rad = Math.max(1, distance); 687 double decay = 1.0 / rad; 688 ArrayTools.fill(light, 0); 689 light[startX][startY] = 1;//make the starting space full power 690 angle = Math.toRadians((angle > 360.0 || angle < 0.0) 691 ? MathExtras.remainder(angle, 360.0) : angle); 692 float s = (float) NumberTools.sin(angle), 693 c = (float) NumberTools.cos(angle); 694 double deteriorate = 1.0; 695 int dx, dy, width = resistanceMap.length, height = resistanceMap[0].length; 696 for (int d = 1; d <= rad; ) { 697 dx = startX + Math.round(c * d); 698 if(dx < 0 || dx > width) 699 break; 700 dy = startY + Math.round(s * d); 701 if(dy < 0 || dy > height) 702 break; 703 deteriorate -= decay; 704 //check if it's within the lightable area and light if needed 705 if (deteriorate > 0.0) { 706 light[dx][dy] = Math.min(light[dx][dy] + deteriorate, 1.0); 707 if (resistanceMap[dx][dy] >= 1 && deteriorate > decay) 708 { 709 startX = dx; 710 startY = dy; 711 d = 1; 712 double flipX = resistanceMap[startX + Math.round(-c * d)][dy], 713 flipY = resistanceMap[dx][startY + Math.round(-s * d)]; 714 if(flipX >= 1.0) 715 s = -s; 716 if(flipY >= 1.0) 717 c = -c; 718 } 719 else ++d; 720 721 } 722 else break; 723 } 724 725 return light; 726 } 727 /** 728 * @param width 729 * The width that {@link #light} should have. 730 * @param height 731 * The height that {@link #light} should have. 732 */ 733 private void initializeLightMap(int width, int height) { 734 if (light == null) 735 light = new double[width][height]; 736 else { 737 if (light.length != width || light[0].length != height) 738 /* Size changed */ 739 light = new double[width][height]; 740 else { 741 /* 742 * Size did not change, we simply need to erase the previous 743 * result 744 */ 745 ArrayTools.fill(light, 0.0); 746 } 747 } 748 } 749 750 /** 751 * @param width 752 * The width that {@link #nearLight} should have. 753 * @param height 754 * The height that {@link #nearLight} should have. 755 */ 756 private void initializeNearLight(int width, int height) { 757 if (nearLight == null) { 758 /* Can't do much with null */ 759 nearLight = new GreasedRegion(width, height); 760 } 761 else { 762 if (nearLight.width != width || nearLight.height != height) { 763 /* Size changed */ 764 nearLight.resizeAndEmpty(width, height); 765 } 766 else { 767 /* 768 * Size did not change, we simply need to erase the previous 769 * result 770 */ 771 nearLight.clear(); 772 } 773 } 774 } 775 776 private static int rippleValue(int type) { 777 switch (type) { 778 case RIPPLE: 779 return 2; 780 case RIPPLE_LOOSE: 781 return 3; 782 case RIPPLE_TIGHT: 783 return 1; 784 case RIPPLE_VERY_LOOSE: 785 return 6; 786 default: 787 System.err.println("Unrecognized ripple type: " + type + ". Defaulting to RIPPLE"); 788 return 2; 789 } 790 } 791 792 private static void doRippleFOV(double[][] lightMap, int ripple, int x, int y, double decay, double radius, double[][] map, GreasedRegion indirect, Radius radiusStrategy) { 793 dq.clear(); 794 int width = lightMap.length; 795 int height = lightMap[0].length; 796 dq.offer(Coord.get(x, y)); 797 while (!dq.isEmpty()) { 798 Coord p = dq.removeFirst(); 799 if (lightMap[p.x][p.y] <= 0 || indirect.contains(p)) { 800 continue;//no light to spread 801 } 802 803 for (Direction dir : Direction.OUTWARDS) { 804 int x2 = p.x + dir.deltaX; 805 int y2 = p.y + dir.deltaY; 806 if (x2 < 0 || x2 >= width || y2 < 0 || y2 >= height //out of bounds 807 || radiusStrategy.radius(x, y, x2, y2) >= radius + 1) {//+1 to cover starting tile 808 continue; 809 } 810 811 double surroundingLight = nearRippleLight(x2, y2, ripple, x, y, decay, lightMap, map, indirect, radiusStrategy); 812 if (lightMap[x2][y2] < surroundingLight) { 813 lightMap[x2][y2] = surroundingLight; 814 if (map[x2][y2] < 1) {//make sure it's not a wall 815 dq.offer(Coord.get(x2, y2));//redo neighbors since this one's light changed 816 } 817 } 818 } 819 } 820 } 821 822 823 824 private static void doRippleFOV(double[][] lightMap, int ripple, int x, int y, double decay, double radius, double[][] map, GreasedRegion indirect, Radius radiusStrategy, double angle, double span) { 825 dq.clear(); 826 int width = lightMap.length; 827 int height = lightMap[0].length; 828 dq.offer(Coord.get(x, y)); 829 while (!dq.isEmpty()) { 830 Coord p = dq.removeFirst(); 831 if (lightMap[p.x][p.y] <= 0 || indirect.contains(p)) { 832 continue;//no light to spread 833 } 834 835 for (Direction dir : ccw_full) { 836 int x2 = p.x + dir.deltaX; 837 int y2 = p.y + dir.deltaY; 838 if (x2 < 0 || x2 >= width || y2 < 0 || y2 >= height //out of bounds 839 || radiusStrategy.radius(x, y, x2, y2) >= radius + 1) {//+1 to cover starting tile 840 continue; 841 } 842 double newAngle = NumberTools.atan2_(y2 - y, x2 - x); 843 if (newAngle > span * 0.5 && newAngle < 1.0 - span * 0.5) 844 continue; 845//if (Math.abs(MathExtras.remainder(angle - newAngle, Math.PI * 2)) > span * 0.5) 846 847 double surroundingLight = nearRippleLight(x2, y2, ripple, x, y, decay, lightMap, map, indirect, radiusStrategy ); 848 if (lightMap[x2][y2] < surroundingLight) { 849 lightMap[x2][y2] = surroundingLight; 850 if (map[x2][y2] < 1) {//make sure it's not a wall 851 dq.offer(Coord.get(x2, y2));//redo neighbors since this one's light changed 852 } 853 } 854 } 855 } 856 } 857 858 private static double nearRippleLight(int x, int y, int rippleNeighbors, int startx, int starty, double decay, double[][] lightMap, double[][] map, GreasedRegion indirect, Radius radiusStrategy) { 859 if (x == startx && y == starty) { 860 return 1; 861 } 862 int width = lightMap.length; 863 int height = lightMap[0].length; 864 List<Coord> neighbors = new ArrayList<>(); 865 double tmpDistance = 0, testDistance; 866 Coord c; 867 for (Direction di : Direction.OUTWARDS) { 868 int x2 = x + di.deltaX; 869 int y2 = y + di.deltaY; 870 if (x2 >= 0 && x2 < width && y2 >= 0 && y2 < height) { 871 tmpDistance = radiusStrategy.radius(startx, starty, x2, y2); 872 int idx = 0; 873 for(int i = 0; i < neighbors.size() && i <= rippleNeighbors; i++) 874 { 875 c = neighbors.get(i); 876 testDistance = radiusStrategy.radius(startx, starty, c.x, c.y); 877 if(tmpDistance < testDistance) { 878 break; 879 } 880 idx++; 881 } 882 neighbors.add(idx, Coord.get(x2, y2)); 883 } 884 } 885 886 if (neighbors.isEmpty()) { 887 return 0; 888 } 889 neighbors = neighbors.subList(0, Math.min(neighbors.size(), rippleNeighbors)); 890/* 891 while (neighbors.size() > rippleNeighbors) { 892 Coord p = neighbors.remove(0); 893 double dist = radiusStrategy.radius(startx, starty, p.x, p.y); 894 double dist2 = 0; 895 for (Coord p2 : neighbors) { 896 dist2 = Math.max(dist2, radiusStrategy.radius(startx, starty, p2.x, p2.y)); 897 } 898 if (dist < dist2) {//not the largest, put it back 899 neighbors.add(p); 900 } 901 } 902*/ 903 double light = 0; 904 int lit = 0, indirects = 0; 905 for (Coord p : neighbors) { 906 if (lightMap[p.x][p.y] > 0) { 907 lit++; 908 if (indirect.contains(p)) { 909 indirects++; 910 } 911 double dist = radiusStrategy.radius(x, y, p.x, p.y); 912 light = Math.max(light, lightMap[p.x][p.y] - dist * decay - map[p.x][p.y]); 913 } 914 } 915 916 if (map[x][y] >= 1 || indirects >= lit) { 917 indirect.insert(x, y); 918 } 919 return light; 920 } 921 922 private static void shadowCast(int xx, int xy, int yx, int yy, 923 double radius, int startx, int starty, double decay, double[][] lightMap, 924 double[][] map, Radius radiusStrategy) { 925 shadowCast(1, 1.0, 0.0, xx, xy, yx, yy, radius, startx, starty, decay, lightMap, map, radiusStrategy, 926 0, 0, lightMap.length, lightMap[0].length); 927 } 928 929 private static void shadowCastBinary(int row, double start, double end, int xx, int xy, int yx, int yy, 930 double radius, int startx, int starty, double decay, double[][] lightMap, 931 double[][] map, Radius radiusStrategy, 932 int minX, int minY, int maxX, int maxY) { 933 double newStart = 0; 934 if (start < end) { 935 return; 936 } 937 938 boolean blocked = false; 939 for (int distance = row; distance <= radius && distance < maxX - minX + maxY - minY && !blocked; distance++) { 940 int deltaY = -distance; 941 for (int deltaX = -distance; deltaX <= 0; deltaX++) { 942 int currentX = startx + deltaX * xx + deltaY * xy; 943 int currentY = starty + deltaX * yx + deltaY * yy; 944 double leftSlope = (deltaX - 0.5f) / (deltaY + 0.5f); 945 double rightSlope = (deltaX + 0.5f) / (deltaY - 0.5f); 946 947 if (!(currentX >= minX && currentY >= minY && currentX < maxX && currentY < maxY) || start < rightSlope) { 948 continue; 949 } else if (end > leftSlope) { 950 break; 951 } 952 953 lightMap[currentX][currentY] = 1.0; 954 955 if (blocked) { //previous cell was a blocking one 956 if (map[currentX][currentY] >= 1) {//hit a wall 957 newStart = rightSlope; 958 } else { 959 blocked = false; 960 start = newStart; 961 } 962 } else { 963 if (map[currentX][currentY] >= 1 && distance < radius) {//hit a wall within sight line 964 blocked = true; 965 shadowCastBinary(distance + 1, start, leftSlope, xx, xy, yx, yy, radius, startx, starty, decay, 966 lightMap, map, radiusStrategy, minX, minY, maxX, maxY); 967 newStart = rightSlope; 968 } 969 } 970 } 971 } 972 } 973 974 private static boolean shadowCastCheck(int row, double start, double end, int xx, int xy, int yx, int yy, 975 double radius, int startx, int starty, double decay, double[][] lightMap, 976 double[][] map, Radius radiusStrategy, 977 int minX, int minY, int maxX, int maxY, int targetX, int targetY) { 978 double newStart = 0; 979 if (start < end) { 980 return false; 981 } 982 983 boolean blocked = false; 984 for (int distance = row; distance <= radius && distance < maxX - minX + maxY - minY && !blocked; distance++) { 985 int deltaY = -distance; 986 for (int deltaX = -distance; deltaX <= 0; deltaX++) { 987 int currentX = startx + deltaX * xx + deltaY * xy; 988 int currentY = starty + deltaX * yx + deltaY * yy; 989 double leftSlope = (deltaX - 0.5f) / (deltaY + 0.5f); 990 double rightSlope = (deltaX + 0.5f) / (deltaY - 0.5f); 991 992 if (!(currentX >= minX && currentY >= minY && currentX < maxX && currentY < maxY) || start < rightSlope) { 993 continue; 994 } else if (end > leftSlope) { 995 break; 996 } 997 998 if(currentX == targetX && currentY == targetY) return true; 999 1000 if (blocked) { //previous cell was a blocking one 1001 if (map[currentX][currentY] >= 1.0) {//hit a wall 1002 newStart = rightSlope; 1003 } else { 1004 blocked = false; 1005 start = newStart; 1006 } 1007 } else { 1008 if (map[currentX][currentY] >= 1.0 && distance < radius) {//hit a wall within sight line 1009 blocked = true; 1010 if(shadowCastCheck(distance + 1, start, leftSlope, xx, xy, yx, yy, radius, startx, starty, decay, 1011 lightMap, map, radiusStrategy, minX, minY, maxX, maxY, targetX, targetY)) 1012 return true; 1013 newStart = rightSlope; 1014 } 1015 } 1016 } 1017 } 1018 return false; 1019 } 1020 1021 private static void shadowCast(int row, double start, double end, int xx, int xy, int yx, int yy, 1022 double radius, int startx, int starty, double decay, double[][] lightMap, 1023 double[][] map, Radius radiusStrategy, 1024 int minX, int minY, int maxX, int maxY) { 1025 double newStart = 0; 1026 if (start < end) { 1027 return; 1028 } 1029 1030 boolean blocked = false; 1031 for (int distance = row; distance <= radius && distance < maxX - minX + maxY - minY && !blocked; distance++) { 1032 int deltaY = -distance; 1033 for (int deltaX = -distance; deltaX <= 0; deltaX++) { 1034 int currentX = startx + deltaX * xx + deltaY * xy; 1035 int currentY = starty + deltaX * yx + deltaY * yy; 1036 double leftSlope = (deltaX - 0.5f) / (deltaY + 0.5f); 1037 double rightSlope = (deltaX + 0.5f) / (deltaY - 0.5f); 1038 1039 if (!(currentX >= minX && currentY >= minY && currentX < maxX && currentY < maxY) || start < rightSlope) { 1040 continue; 1041 } else if (end > leftSlope) { 1042 break; 1043 } 1044 double deltaRadius = radiusStrategy.radius(deltaX, deltaY); 1045 //check if it's within the lightable area and light if needed 1046 if (deltaRadius <= radius) { 1047 lightMap[currentX][currentY] = 1.0 - decay * deltaRadius; 1048 } 1049 1050 if (blocked) { //previous cell was a blocking one 1051 if (map[currentX][currentY] >= 1) {//hit a wall 1052 newStart = rightSlope; 1053 } else { 1054 blocked = false; 1055 start = newStart; 1056 } 1057 } else { 1058 if (map[currentX][currentY] >= 1 && distance < radius) {//hit a wall within sight line 1059 blocked = true; 1060 shadowCast(distance + 1, start, leftSlope, xx, xy, yx, yy, radius, startx, starty, decay, 1061 lightMap, map, radiusStrategy, minX, minY, maxX, maxY); 1062 newStart = rightSlope; 1063 } 1064 } 1065 } 1066 } 1067 } 1068 private static double[][] shadowCastLimited(int row, double start, double end, int xx, int xy, int yx, int yy, 1069 double radius, int startx, int starty, double decay, double[][] lightMap, 1070 double[][] map, Radius radiusStrategy, double angle, double span) { 1071 double newStart = 0; 1072 if (start < end) { 1073 return lightMap; 1074 } 1075 int width = lightMap.length; 1076 int height = lightMap[0].length; 1077 1078 boolean blocked = false; 1079 for (int distance = row; distance <= radius && distance < width + height && !blocked; distance++) { 1080 int deltaY = -distance; 1081 for (int deltaX = -distance; deltaX <= 0; deltaX++) { 1082 int currentX = startx + deltaX * xx + deltaY * xy; 1083 int currentY = starty + deltaX * yx + deltaY * yy; 1084 double leftSlope = (deltaX - 0.5f) / (deltaY + 0.5f); 1085 double rightSlope = (deltaX + 0.5f) / (deltaY - 0.5f); 1086 1087 if (!(currentX >= 0 && currentY >= 0 && currentX < width && currentY < height) || start < rightSlope) { 1088 continue; 1089 } else if (end > leftSlope) { 1090 break; 1091 } 1092 double deltaRadius = radiusStrategy.radius(deltaX, deltaY), 1093 at2 = Math.abs(angle - NumberTools.atan2_(currentY - starty, currentX - startx));// + 1.0) % 1.0; 1094 //check if it's within the lightable area and light if needed 1095 if (deltaRadius <= radius 1096 && (at2 <= span * 0.5 1097 || at2 >= 1.0 - span * 0.5)) { 1098 double bright = 1 - decay * deltaRadius; 1099 lightMap[currentX][currentY] = bright; 1100 } 1101 1102 if (blocked) { //previous cell was a blocking one 1103 if (map[currentX][currentY] >= 1) {//hit a wall 1104 newStart = rightSlope; 1105 } else { 1106 blocked = false; 1107 start = newStart; 1108 } 1109 } else { 1110 if (map[currentX][currentY] >= 1 && distance < radius) {//hit a wall within sight line 1111 blocked = true; 1112 lightMap = shadowCastLimited(distance + 1, start, leftSlope, xx, xy, yx, yy, radius, startx, starty, decay, lightMap, map, radiusStrategy, angle, span); 1113 newStart = rightSlope; 1114 } 1115 } 1116 } 1117 } 1118 return lightMap; 1119 } 1120 1121 private static final double[] directionRanges = new double[8]; 1122 /** 1123 * Calculates the Field Of View for the provided map from the given x, y 1124 * coordinates, lighting with the view "pointed at" the given {@code angle} in degrees, 1125 * extending to different ranges based on the direction the light is traveling. 1126 * The direction ranges are {@code forward}, {@code sideForward}, {@code side}, 1127 * {@code sideBack}, and {@code back}; all are multiplied by {@code radius}. 1128 * Assigns to, and returns, a light map where the values represent a percentage of fully 1129 * lit. The values in light are cleared before this is run, because prior state can make 1130 * this give incorrect results. You can use {@link #addFOVsInto(double[][], double[][]...)} 1131 * if you want to mix FOV results, which works as an alternative to using the prior light state. 1132 * <br> 1133 * The starting point for the calculation is considered to be at the center 1134 * of the origin cell. Radius determinations are determined by the provided 1135 * RadiusStrategy. If all direction ranges are the same, this acts like 1136 * {@link #reuseFOV(double[][], double[][], int, int, double, Radius)}; otherwise 1137 * may produce conical shapes (potentially more than one, or overlapping ones). 1138 * 1139 * @param resistanceMap the grid of cells to calculate on; the kind made by {@link #generateResistances(char[][])} 1140 * @param light the grid of cells to assign to; may have existing values, and 0.0 is used to mean "unlit" 1141 * @param startX the horizontal component of the starting location 1142 * @param startY the vertical component of the starting location 1143 * @param radius the distance the light will extend to (roughly); direction ranges will be multiplied by this 1144 * @param radiusTechnique provides a means to shape the FOV by changing distance calculation (circle, square, etc.) 1145 * @param angle the angle in degrees that will be the center of the FOV cone, 0 points right 1146 * @param forward the range to extend when the light is within 22.5 degrees of angle; will be interpolated with sideForward 1147 * @param sideForward the range to extend when the light is between 22.5 and 67.5 degrees of angle; will be interpolated with forward or side 1148 * @param side the range to extend when the light is between 67.5 and 112.5 degrees of angle; will be interpolated with sideForward or sideBack 1149 * @param sideBack the range to extend when the light is between 112.5 and 157.5 degrees of angle; will be interpolated with side or back 1150 * @param back the range to extend when the light is more than 157.5 degrees away from angle; will be interpolated with sideBack 1151 * @return the computed light grid (the same as {@code light}) 1152 */ 1153 public static double[][] reuseFOV(double[][] resistanceMap, double[][] light, int startX, int startY, 1154 double radius, Radius radiusTechnique, double angle, 1155 double forward, double sideForward, double side, double sideBack, double back) { 1156 directionRanges[0] = forward * radius; 1157 directionRanges[7] = directionRanges[1] = sideForward * radius; 1158 directionRanges[6] = directionRanges[2] = side * radius; 1159 directionRanges[5] = directionRanges[3] = sideBack * radius; 1160 directionRanges[4] = back * radius; 1161 1162 radius = Math.max(1, radius); 1163 ArrayTools.fill(light, 0); 1164 light[startX][startY] = 1;//make the starting space full power 1165 angle = ((angle >= 360.0 || angle < 0.0) 1166 ? MathExtras.remainder(angle, 360.0) : angle) * 0.002777777777777778; 1167 1168 1169 light = shadowCastPersonalized(1, 1.0, 0.0, 0, 1, 1, 0, radius, startX, startY, light, resistanceMap, radiusTechnique, angle, directionRanges); 1170 light = shadowCastPersonalized(1, 1.0, 0.0, 1, 0, 0, 1, radius, startX, startY, light, resistanceMap, radiusTechnique, angle, directionRanges); 1171 light = shadowCastPersonalized(1, 1.0, 0.0, 0, -1, 1, 0, radius, startX, startY, light, resistanceMap, radiusTechnique, angle, directionRanges); 1172 light = shadowCastPersonalized(1, 1.0, 0.0, -1, 0, 0, 1, radius, startX, startY, light, resistanceMap, radiusTechnique, angle, directionRanges); 1173 light = shadowCastPersonalized(1, 1.0, 0.0, 0, -1, -1, 0, radius, startX, startY, light, resistanceMap, radiusTechnique, angle, directionRanges); 1174 light = shadowCastPersonalized(1, 1.0, 0.0, -1, 0, 0, -1, radius, startX, startY, light, resistanceMap, radiusTechnique, angle, directionRanges); 1175 light = shadowCastPersonalized(1, 1.0, 0.0, 0, 1, -1, 0, radius, startX, startY, light, resistanceMap, radiusTechnique, angle, directionRanges); 1176 light = shadowCastPersonalized(1, 1.0, 0.0, 1, 0, 0, -1, radius, startX, startY, light, resistanceMap, radiusTechnique, angle, directionRanges); 1177 return light; 1178 } 1179 1180 private static double[][] shadowCastPersonalized(int row, double start, double end, int xx, int xy, int yx, int yy, 1181 double radius, int startx, int starty, double[][] lightMap, 1182 double[][] map, Radius radiusStrategy, double angle, final double[] directionRanges) { 1183 double newStart = 0; 1184 if (start < end) { 1185 return lightMap; 1186 } 1187 int width = lightMap.length; 1188 int height = lightMap[0].length; 1189 1190 boolean blocked = false; 1191 for (int distance = row; distance <= radius && distance < width + height && !blocked; distance++) { 1192 int deltaY = -distance; 1193 for (int deltaX = -distance; deltaX <= 0; deltaX++) { 1194 int currentX = startx + deltaX * xx + deltaY * xy; 1195 int currentY = starty + deltaX * yx + deltaY * yy; 1196 double leftSlope = (deltaX - 0.5f) / (deltaY + 0.5f); 1197 double rightSlope = (deltaX + 0.5f) / (deltaY - 0.5f); 1198 1199 if (!(currentX >= 0 && currentY >= 0 && currentX < width && currentY < height) || start < rightSlope) { 1200 continue; 1201 } else if (end > leftSlope) { 1202 break; 1203 } 1204 double at2 = Math.abs(angle - NumberTools.atan2_(currentY - starty, currentX - startx)) * 8.0, 1205 deltaRadius = radiusStrategy.radius(deltaX, deltaY); 1206 int ia = (int)(at2), low = ia & 7, high = ia + 1 & 7; 1207 double a = at2 - ia, adjRadius = (1.0 - a) * directionRanges[low] + a * directionRanges[high]; 1208 //check if it's within the lightable area and light if needed 1209 if (deltaRadius <= adjRadius) { 1210 lightMap[currentX][currentY] = 1.0 - (deltaRadius / (adjRadius + 1.0)); // how bright the tile is 1211 } 1212 1213 if (blocked) { //previous cell was a blocking one 1214 if (map[currentX][currentY] >= 1) {//hit a wall 1215 newStart = rightSlope; 1216 } else { 1217 blocked = false; 1218 start = newStart; 1219 } 1220 } else { 1221 if (map[currentX][currentY] >= 1 && distance < adjRadius) {//hit a wall within sight line 1222 blocked = true; 1223 lightMap = shadowCastPersonalized(distance + 1, start, leftSlope, xx, xy, yx, yy, radius, startx, starty, lightMap, map, radiusStrategy, angle, directionRanges); 1224 newStart = rightSlope; 1225 } 1226 } 1227 } 1228 } 1229 return lightMap; 1230 } 1231 1232 /** 1233 * Adds an FOV map to another in the simplest way possible; does not check line-of-sight between FOV maps. 1234 * Clamps the highest value for any single position at 1.0. Modifies the basis parameter in-place and makes no 1235 * allocations; this is different from {@link #addFOVs(double[][][])}, which creates a new 2D array. 1236 * @param basis a 2D double array, which can be empty or returned by calculateFOV() or reuseFOV(); modified! 1237 * @param addend another 2D double array that will be added into basis; this one will not be modified 1238 * @return the sum of the 2D double arrays passed, using the dimensions of basis if they don't match 1239 */ 1240 public static double[][] addFOVsInto(double[][] basis, double[][] addend) 1241 { 1242 for (int x = 0; x < basis.length && x < addend.length; x++) { 1243 for (int y = 0; y < basis[x].length && y < addend[x].length; y++) { 1244 basis[x][y] = Math.min(1.0, basis[x][y] + addend[x][y]); 1245 } 1246 } 1247 return basis; 1248 } 1249 /** 1250 * Adds multiple FOV maps together in the simplest way possible; does not check line-of-sight between FOV maps. 1251 * Clamps the highest value for any single position at 1.0. Allocates a new 2D double array and returns it. 1252 * @param maps an array or vararg of 2D double arrays, each usually returned by calculateFOV() 1253 * @return the sum of all the 2D double arrays passed, using the dimensions of the first if they don't all match 1254 */ 1255 public static double[][] addFOVs(double[][]... maps) 1256 { 1257 if(maps == null || maps.length == 0) 1258 return new double[0][0]; 1259 double[][] map = ArrayTools.copy(maps[0]); 1260 for(int i = 1; i < maps.length; i++) 1261 { 1262 for (int x = 0; x < map.length && x < maps[i].length; x++) { 1263 for (int y = 0; y < map[x].length && y < maps[i][x].length; y++) { 1264 map[x][y] += maps[i][x][y]; 1265 } 1266 } 1267 } 1268 for (int x = 0; x < map.length; x++) { 1269 for (int y = 0; y < map[x].length; y++) { 1270 if(map[x][y] > 1.0) map[x][y] = 1.0; 1271 } 1272 } 1273 return map; 1274 } 1275 /** 1276 * Adds multiple FOV maps to basis cell-by-cell, modifying basis; does not check line-of-sight between FOV maps. 1277 * Clamps the highest value for any single position at 1.0. Returns basis without allocating new objects. 1278 * @param basis a 2D double array that will be modified by adding values in maps to it and clamping to 1.0 or less 1279 * @param maps an array or vararg of 2D double arrays, each usually returned by calculateFOV() 1280 * @return basis, with all elements in all of maps added to the corresponding cells and clamped 1281 */ 1282 public static double[][] addFOVsInto(double[][] basis, double[][]... maps) { 1283 if (maps == null || maps.length == 0) 1284 return basis; 1285 for (int i = 1; i < maps.length; i++) { 1286 for (int x = 0; x < basis.length && x < maps[i].length; x++) { 1287 for (int y = 0; y < basis[x].length && y < maps[i][x].length; y++) { 1288 basis[x][y] += maps[i][x][y]; 1289 } 1290 } 1291 } 1292 for (int x = 0; x < basis.length; x++) { 1293 for (int y = 0; y < basis[x].length; y++) { 1294 if (basis[x][y] > 1.0) basis[x][y] = 1.0; 1295 } 1296 } 1297 return basis; 1298 } 1299 1300 /** 1301 * Adds multiple FOV maps together in the simplest way possible; does not check line-of-sight between FOV maps. 1302 * Clamps the highest value for any single position at 1.0. Allocates a new 2D double array and returns it. 1303 * @param maps an Iterable of 2D double arrays (most collections implement Iterable), 1304 * each usually returned by calculateFOV() 1305 * @return the sum of all the 2D double arrays passed, using the dimensions of the first if they don't all match 1306 */ 1307 public static double[][] addFOVs(Iterable<double[][]> maps) 1308 { 1309 if(maps == null) 1310 return new double[0][0]; 1311 Iterator<double[][]> it = maps.iterator(); 1312 if(!it.hasNext()) 1313 return new double[0][0]; 1314 double[][] map = ArrayTools.copy(it.next()), t; 1315 while (it.hasNext()) 1316 { 1317 t = it.next(); 1318 for (int x = 0; x < map.length && x < t.length; x++) { 1319 for (int y = 0; y < map[x].length && y < t[x].length; y++) { 1320 map[x][y] += t[x][y]; 1321 } 1322 } 1323 } 1324 for (int x = 0; x < map.length; x++) { 1325 for (int y = 0; y < map[x].length; y++) { 1326 if(map[x][y] > 1.0) map[x][y] = 1.0; 1327 } 1328 } 1329 return map; 1330 } 1331 1332 /** 1333 * Adds together multiple FOV maps, but only adds to a position if it is visible in the given LOS map. Useful if 1334 * you want distant lighting to be visible only if the player has line-of-sight to a lit cell. Typically the LOS map 1335 * is calculated by {@link #reuseLOS(double[][], double[][], int, int)}, using the same resistance map used to 1336 * calculate the FOV maps. Clamps the highest value for any single position at 1.0. 1337 * @param losMap an LOS map such as one generated by {@link #reuseLOS(double[][], double[][], int, int)} 1338 * @param maps an array or vararg of 2D double arrays, each usually returned by calculateFOV() 1339 * @return the sum of all the 2D double arrays in maps where a cell was visible in losMap 1340 */ 1341 public static double[][] mixVisibleFOVs(double[][] losMap, double[][]... maps) 1342 { 1343 if(losMap == null || losMap.length == 0) 1344 return addFOVs(maps); 1345 final int width = losMap.length, height = losMap[0].length; 1346 double[][] map = new double[width][height]; 1347 if(maps == null || maps.length == 0) 1348 return map; 1349 for(int i = 0; i < maps.length; i++) 1350 { 1351 for (int x = 0; x < width && x < maps[i].length; x++) { 1352 for (int y = 0; y < height && y < maps[i][x].length; y++) { 1353 if(losMap[x][y] > 0.0001) { 1354 map[x][y] += maps[i][x][y]; 1355 } 1356 } 1357 } 1358 } 1359 for (int x = 0; x < width; x++) { 1360 for (int y = 0; y < height; y++) { 1361 if(map[x][y] > 1.0) map[x][y] = 1.0; 1362 } 1363 } 1364 1365 return map; 1366 } 1367 /** 1368 * Adds together multiple FOV maps, but only adds to a position if it is visible in the given LOS map. Useful if 1369 * you want distant lighting to be visible only if the player has line-of-sight to a lit cell. Typically the LOS map 1370 * is calculated by {@link #reuseLOS(double[][], double[][], int, int)}, using the same resistance map used to 1371 * calculate the FOV maps. Clamps the highest value for any single position at 1.0. 1372 * @param losMap an LOS map such as one generated by {@link #reuseLOS(double[][], double[][], int, int)} 1373 * @param basis an existing 2D double array that should have matching width and height to losMap; will be modified 1374 * @param maps an array or vararg of 2D double arrays, each usually returned by calculateFOV() 1375 * @return the sum of all the 2D double arrays in maps where a cell was visible in losMap 1376 */ 1377 public static double[][] mixVisibleFOVsInto(double[][] losMap, double[][] basis, double[][]... maps) 1378 1379 { 1380 if(losMap == null || losMap.length <= 0 || losMap[0].length <= 0) 1381 return addFOVsInto(basis, maps); 1382 final int width = losMap.length, height = losMap[0].length; 1383 double[][] map = new double[width][height]; 1384 if(maps == null || maps.length == 0) 1385 return map; 1386 for(int i = 0; i < maps.length; i++) 1387 { 1388 for (int x = 0; x < width && x < maps[i].length; x++) { 1389 for (int y = 0; y < height && y < maps[i][x].length; y++) { 1390 if(losMap[x][y] > 0.0001) { 1391 map[x][y] += maps[i][x][y]; 1392 } 1393 } 1394 } 1395 } 1396 for (int x = 0; x < width; x++) { 1397 for (int y = 0; y < height; y++) { 1398 if(map[x][y] > 1.0) map[x][y] = 1.0; 1399 } 1400 } 1401 return map; 1402 } 1403 1404 /** 1405 * Adds together multiple FOV maps, but only adds to a position if it is visible in the given LOS map. Useful if 1406 * you want distant lighting to be visible only if the player has line-of-sight to a lit cell. Typically the LOS map 1407 * is calculated by {@link #reuseLOS(double[][], double[][], int, int)}, using the same resistance map used to 1408 * calculate the FOV maps. Clamps the highest value for any single position at 1.0. 1409 * @param losMap an LOS map such as one generated by {@link #reuseLOS(double[][], double[][], int, int)} 1410 * @param maps an Iterable of 2D double arrays, each usually returned by calculateFOV() 1411 * @return the sum of all the 2D double arrays in maps where a cell was visible in losMap 1412 */ 1413 public static double[][] mixVisibleFOVs(double[][] losMap, Iterable<double[][]> maps) 1414 { 1415 if(losMap == null || losMap.length == 0) 1416 return addFOVs(maps); 1417 final int width = losMap.length, height = losMap[0].length; 1418 double[][] map = new double[width][height]; 1419 if(maps == null) 1420 return map; 1421 for (double[][] map1 : maps) { 1422 for (int x = 0; x < width && x < map1.length; x++) { 1423 for (int y = 0; y < height && y < map1[x].length; y++) { 1424 if (losMap[x][y] > 0.0001) { 1425 map[x][y] += map1[x][y]; 1426 if (map[x][y] > 1.0) map[x][y] = 1.0; 1427 } 1428 } 1429 } 1430 } 1431 return map; 1432 } 1433 1434 /** 1435 * Calculates what cells are visible from (startX,startY) using the given resistanceMap; this can be given to 1436 * mixVisibleFOVs() to limit extra light sources to those visible from the starting point. Just like calling 1437 * calculateFOV(), this creates a new double[][]; there doesn't appear to be a way to work with Ripple FOV and avoid 1438 * needing an empty double[][] every time, since it uses previously-placed light to determine how it should spread. 1439 * @param resistanceMap the grid of cells to calculate on; the kind made by {@link #generateResistances(char[][])} 1440 * @param startX the center of the LOS map; typically the player's x-position 1441 * @param startY the center of the LOS map; typically the player's y-position 1442 * @return an LOS map with the given starting point 1443 */ 1444 public double[][] calculateLOSMap(double[][] resistanceMap, int startX, int startY) 1445 { 1446 if(resistanceMap == null || resistanceMap.length <= 0 || resistanceMap[0].length <= 0) 1447 return new double[0][0]; 1448 1449 int width = resistanceMap.length; 1450 int height = resistanceMap[0].length; 1451 double rad = width + height; 1452 double decay = 1.0 / rad; 1453 1454 initializeLightMap(width, height); 1455 light[startX][startY] = 1;//make the starting space full power 1456 1457 switch (type) { 1458 case RIPPLE: 1459 case RIPPLE_LOOSE: 1460 case RIPPLE_TIGHT: 1461 case RIPPLE_VERY_LOOSE: 1462 initializeNearLight(width, height); 1463 doRippleFOV(light, rippleValue(type), startX, startY, decay, rad, resistanceMap, nearLight, Radius.SQUARE); 1464 for (int x = 0; x < width; x++) { 1465 for (int y = 0; y < height; y++) { 1466 if(light[x][y] > 0.0001) 1467 light[x][y] = 1.0; 1468 } 1469 } 1470 break; 1471 default: 1472 for (Direction d : Direction.DIAGONALS) { 1473 shadowCastBinary(1, 1.0, 0.0, 0, d.deltaX, d.deltaY, 0, rad, startX, startY, decay, light, resistanceMap, Radius.SQUARE, 0, 0, width, height); 1474 shadowCastBinary(1, 1.0, 0.0, d.deltaX, 0, 0, d.deltaY, rad, startX, startY, decay, light, resistanceMap, Radius.SQUARE, 0, 0, width, height); 1475 } 1476 break; 1477 } 1478 return light; 1479 } 1480 1481 1482 1483 /** 1484 * Given a char[][] for the map, produces a double[][] that can be used with most of the methods in FOV, like 1485 * {@link #reuseFOV(double[][], double[][], int, int, double)}. It expects any doors to be represented by '+' if 1486 * closed or '/' if open (which can be caused by calling {@link DungeonUtility#closeDoors(char[][])}), any walls to 1487 * be '#' or box drawing characters, and it doesn't care what other chars are used (only doors, including open ones, 1488 * and walls obscure light and thus have a resistance by default). 1489 * 1490 * @param map a dungeon, width by height, with any closed doors as '+' and open doors as '/' as per closeDoors() 1491 * @return a resistance map suitable for use with the FOV class, with clear cells assigned 0.0 and blocked ones 1.0 1492 */ 1493 public static double[][] generateResistances(char[][] map) { 1494 int width = map.length; 1495 int height = map[0].length; 1496 double[][] portion = new double[width][height]; 1497 for (int i = 0; i < width; i++) { 1498 for (int j = 0; j < height; j++) { 1499 switch (map[i][j]) { 1500 case '\1': 1501 case '├': 1502 case '┤': 1503 case '┴': 1504 case '┬': 1505 case '┌': 1506 case '┐': 1507 case '└': 1508 case '┘': 1509 case '│': 1510 case '─': 1511 case '┼': 1512 case '#': 1513 portion[i][j] = 1.0; 1514 break; 1515 case '/': 1516 case '"': 1517 portion[i][j] = 0.15; 1518 break; 1519 case '+': 1520 portion[i][j] = 0.95; 1521 break; 1522 case '.': 1523 case ',': 1524 case '~': 1525 case '^': 1526 default: 1527 portion[i][j] = 0.0; 1528 } 1529 } 1530 } 1531 return portion; 1532 } 1533 1534 /** 1535 * Given a char[][] for the map that should use box drawing characters (as produced by 1536 * {@link DungeonUtility#hashesToLines(char[][], boolean)}), produces a double[][] with triple width and triple 1537 * height that can be used with FOV methods like {@link #reuseFOV(double[][], double[][], int, int, double)} in 1538 * classes that use subcell lighting. Importantly, this only considers a "thin line" of wall to be blocking 1539 * (matching the box drawing character), instead of the whole 3x3 area. This expects any doors to be represented by 1540 * '+' if closed or '/' if open (which can be caused by calling {@link DungeonUtility#closeDoors(char[][])}), thick 1541 * vegetation or other concealing obstructions to be '"', any normal walls to be box drawing characters, any cells 1542 * that block all subcells to be '#', and it doesn't care what other chars are used (only doors, including open 1543 * ones, vegetation, and walls obscure light and thus have a resistance normally). 1544 * 1545 * @param map a dungeon, width by height, with any closed doors as '+' and open doors as '/' as per closeDoors() 1546 * @return a resistance map suitable for use with the FOV class and subcell lighting, with triple width/height 1547 */ 1548 public static double[][] generateResistances3x3(char[][] map) { 1549 int width = map.length; 1550 int height = map[0].length; 1551 double[][] portion = new double[width * 3][height * 3]; 1552 for (int i = 0, x = 0; i < width; i++, x+=3) { 1553 for (int j = 0, y = 0; j < height; j++, y+=3) { 1554 switch (map[i][j]) { 1555 case '\1': 1556 case '#': 1557 portion[x][y] = portion[x+1][y] = portion[x+2][y] = 1558 portion[x][y+1] = portion[x+1][y+1] = portion[x+2][y+1] = 1559 portion[x][y+2] = portion[x+1][y+2] = portion[x+2][y+2] = 1.0; 1560 break; 1561 case '├': 1562 /*portion[x][y] =*/ portion[x+1][y] = /*portion[x+2][y] =*/ 1563 /*portion[x][y+1] =*/ portion[x+1][y+1] = portion[x+2][y+1] = 1564 /*portion[x][y+2] =*/ portion[x+1][y+2] = /*portion[x+2][y+2] =*/ 1.0; 1565 break; 1566 case '┤': 1567 /*portion[x][y] =*/ portion[x+1][y] = /*portion[x+2][y] =*/ 1568 portion[x][y+1] = portion[x+1][y+1] = /*portion[x+2][y+1] =*/ 1569 /*portion[x][y+2] =*/ portion[x+1][y+2] = /*portion[x+2][y+2] =*/ 1.0; 1570 break; 1571 case '┴': 1572 /*portion[x][y] =*/ portion[x+1][y] = /*portion[x+2][y] =*/ 1573 portion[x][y+1] = portion[x+1][y+1] = portion[x+2][y+1] = 1574 /*portion[x][y+2] = portion[x+1][y+2] = portion[x+2][y+2] =*/ 1.0; 1575 break; 1576 case '┬': 1577 /*portion[x][y] = portion[x+1][y] = portion[x+2][y] =*/ 1578 portion[x][y+1] = portion[x+1][y+1] = portion[x+2][y+1] = 1579 /*portion[x][y+2] =*/ portion[x+1][y+2] = /*portion[x+2][y+2] =*/ 1.0; 1580 break; 1581 case '┌': 1582 /*portion[x][y] = portion[x+1][y] = portion[x+2][y] =*/ 1583 /*portion[x][y+1] =*/ portion[x+1][y+1] = portion[x+2][y+1] = 1584 /*portion[x][y+2] =*/ portion[x+1][y+2] = /*portion[x+2][y+2] =*/ 1.0; 1585 break; 1586 case '┐': 1587 /*portion[x][y] = portion[x+1][y] = portion[x+2][y] =*/ 1588 portion[x][y+1] = portion[x+1][y+1] = /*portion[x+2][y+1] =*/ 1589 /*portion[x][y+2] =*/ portion[x+1][y+2] = /*portion[x+2][y+2] =*/ 1.0; 1590 break; 1591 case '└': 1592 /*portion[x][y] =*/ portion[x+1][y] = /*portion[x+2][y] =*/ 1593 /*portion[x][y+1] =*/ portion[x+1][y+1] = portion[x+2][y+1] = 1594 /*portion[x][y+2] = portion[x+1][y+2] = portion[x+2][y+2] =*/ 1.0; 1595 break; 1596 case '┘': 1597 /*portion[x][y] =*/ portion[x+1][y] = /*portion[x+2][y] =*/ 1598 portion[x][y+1] = portion[x+1][y+1] = /*portion[x+2][y+1] =*/ 1599 /*portion[x][y+2] = portion[x+1][y+2] = portion[x+2][y+2] =*/ 1.0; 1600 break; 1601 case '│': 1602 /*portion[x][y] =*/ portion[x+1][y] = /*portion[x+2][y] =*/ 1603 /*portion[x][y+1] =*/ portion[x+1][y+1] = /*portion[x+2][y+1] =*/ 1604 /*portion[x][y+2] =*/ portion[x+1][y+2] = /*portion[x+2][y+2] =*/ 1.0; 1605 break; 1606 case '─': 1607 portion[x][y+1] = portion[x+1][y+1] = portion[x+2][y+1] = 1.0; 1608 break; 1609 case '╴': 1610 portion[x][y+1] = portion[x+1][y+1] = 1.0; 1611 break; 1612 case '╵': 1613 /*portion[x][y] =*/ portion[x+1][y] = /*portion[x+2][y] =*/ 1614 /*portion[x][y+1] =*/ portion[x+1][y+1] = /*portion[x+2][y+1] =*/ 1.0; 1615 break; 1616 case '╶': 1617 portion[x+1][y+1] = portion[x+2][y+1] = 1.0; 1618 break; 1619 case '╷': 1620 /*portion[x][y+1] =*/ portion[x+1][y+1] = /*portion[x+2][y+1] =*/ 1621 /*portion[x][y+2] =*/ portion[x+1][y+2] = /*portion[x+2][y+2] =*/ 1.0; 1622 break; 1623 case '┼': 1624 /*portion[x][y] =*/ portion[x+1][y] = /*portion[x+2][y] =*/ 1625 portion[x][y+1] = portion[x+1][y+1] = portion[x+2][y+1] = 1626 /*portion[x][y+2] =*/ portion[x+1][y+2] = /*portion[x+2][y+2] =*/ 1.0; 1627 break; 1628 case '/': 1629 case '"': 1630 portion[x][y] = portion[x+1][y] = portion[x+2][y] = 1631 portion[x][y+1] = portion[x+1][y+1] = portion[x+2][y+1] = 1632 portion[x][y+2] = portion[x+1][y+2] = portion[x+2][y+2] = 0.15; 1633 break; 1634 case '+': 1635 portion[x][y] = portion[x+1][y] = portion[x+2][y] = 1636 portion[x][y+1] = portion[x+1][y+1] = portion[x+2][y+1] = 1637 portion[x][y+2] = portion[x+1][y+2] = portion[x+2][y+2] = 0.95; 1638 break; 1639 } 1640 } 1641 } 1642 return portion; 1643 } 1644 1645 /** 1646 * Given a char[][] for the map, produces a double[][] that can be used with any FOV methods that expect a 1647 * resistance map (like {@link #reuseFOV(double[][], double[][], int, int, double)}), but does not treat 1648 * any cells as partly transparent, only fully-blocking or fully-permitting light. This is mainly useful if you 1649 * expect the FOV radius to be very high or (effectively) infinite, since anything less than complete blockage would 1650 * be passed through by infinite-radius FOV. This expects any doors to be represented by '+' if closed or '/' if 1651 * open (most door placement defaults to a mix of '+' and '/', so by calling 1652 * {@link DungeonUtility#closeDoors(char[][])} you can close all doors at the start), and any walls to be '#' or 1653 * box drawing characters. This will assign 1.0 resistance to walls and closed doors or 0.0 for any other cell. 1654 * 1655 * @param map a dungeon, width by height, with any closed doors as '+' and open doors as '/' as per closeDoors() 1656 * @return a resistance map suitable for use with the FOV class, but with no partially transparent cells 1657 */ 1658 public static double[][] generateSimpleResistances(char[][] map) { 1659 int width = map.length; 1660 int height = map[0].length; 1661 double[][] portion = new double[width][height]; 1662 for (int i = 0; i < width; i++) { 1663 for (int j = 0; j < height; j++) { 1664 switch (map[i][j]) { 1665 case '\1': 1666 case '├': 1667 case '┤': 1668 case '┴': 1669 case '┬': 1670 case '┌': 1671 case '┐': 1672 case '└': 1673 case '┘': 1674 case '│': 1675 case '─': 1676 case '┼': 1677 case '#': 1678 case '+': 1679 portion[i][j] = 1.0; 1680 break; 1681 default: 1682 portion[i][j] = 0.0; 1683 } 1684 } 1685 } 1686 return portion; 1687 } 1688 1689 /** 1690 * Given a char[][] for the map that should use box drawing characters (as produced by 1691 * {@link DungeonUtility#hashesToLines(char[][], boolean)}), produces a double[][] with triple width and triple 1692 * height that can be used with FOV's methods that expect a resistance map (like 1693 * {@link #reuseFOV(double[][], double[][], int, int, double)}) in classes that use subcell lighting. This expects 1694 * any doors to be represented by '+' if closed or '/' if open (most door placement defaults to a mix of '+' and 1695 * '/', so by calling {@link DungeonUtility#closeDoors(char[][])} you can close all doors at the start), any walls 1696 * to be box drawing characters, and any cells that block all subcells within their area to be '#'. This will assig 1697 * 1.0 resistance to walls and closed doors where a line of the box drawing char would block light, or 0.0 for an 1698 * other subcell. 1699 * 1700 * @param map a dungeon, width by height, with any closed doors as '+' and open doors as '/' as per closeDoors() 1701 * @return a resistance map suitable for use with the FOV class and subcell lighting, with triple width/height 1702 */ 1703 public static double[][] generateSimpleResistances3x3(char[][] map) { 1704 int width = map.length; 1705 int height = map[0].length; 1706 double[][] portion = new double[width * 3][height * 3]; 1707 for (int i = 0, x = 0; i < width; i++, x+=3) { 1708 for (int j = 0, y = 0; j < height; j++, y+=3) { 1709 switch (map[i][j]) { 1710 case '\1': 1711 case '#': 1712 case '+': 1713 portion[x][y] = portion[x+1][y] = portion[x+2][y] = 1714 portion[x][y+1] = portion[x+1][y+1] = portion[x+2][y+1] = 1715 portion[x][y+2] = portion[x+1][y+2] = portion[x+2][y+2] = 1.0; 1716 break; 1717 case '├': 1718 /*portion[x][y] =*/ portion[x+1][y] = /*portion[x+2][y] =*/ 1719 /*portion[x][y+1] =*/ portion[x+1][y+1] = portion[x+2][y+1] = 1720 /*portion[x][y+2] =*/ portion[x+1][y+2] = /*portion[x+2][y+2] =*/ 1.0; 1721 break; 1722 case '┤': 1723 /*portion[x][y] =*/ portion[x+1][y] = /*portion[x+2][y] =*/ 1724 portion[x][y+1] = portion[x+1][y+1] = /*portion[x+2][y+1] =*/ 1725 /*portion[x][y+2] =*/ portion[x+1][y+2] = /*portion[x+2][y+2] =*/ 1.0; 1726 break; 1727 case '┴': 1728 /*portion[x][y] =*/ portion[x+1][y] = /*portion[x+2][y] =*/ 1729 portion[x][y+1] = portion[x+1][y+1] = portion[x+2][y+1] = 1730 /*portion[x][y+2] = portion[x+1][y+2] = portion[x+2][y+2] =*/ 1.0; 1731 break; 1732 case '┬': 1733 /*portion[x][y] = portion[x+1][y] = portion[x+2][y] =*/ 1734 portion[x][y+1] = portion[x+1][y+1] = portion[x+2][y+1] = 1735 /*portion[x][y+2] =*/ portion[x+1][y+2] = /*portion[x+2][y+2] =*/ 1.0; 1736 break; 1737 case '┌': 1738 /*portion[x][y] = portion[x+1][y] = portion[x+2][y] =*/ 1739 /*portion[x][y+1] =*/ portion[x+1][y+1] = portion[x+2][y+1] = 1740 /*portion[x][y+2] =*/ portion[x+1][y+2] = /*portion[x+2][y+2] =*/ 1.0; 1741 break; 1742 case '┐': 1743 /*portion[x][y] = portion[x+1][y] = portion[x+2][y] =*/ 1744 portion[x][y+1] = portion[x+1][y+1] = /*portion[x+2][y+1] =*/ 1745 /*portion[x][y+2] =*/ portion[x+1][y+2] = /*portion[x+2][y+2] =*/ 1.0; 1746 break; 1747 case '└': 1748 /*portion[x][y] =*/ portion[x+1][y] = /*portion[x+2][y] =*/ 1749 /*portion[x][y+1] =*/ portion[x+1][y+1] = portion[x+2][y+1] = 1750 /*portion[x][y+2] = portion[x+1][y+2] = portion[x+2][y+2] =*/ 1.0; 1751 break; 1752 case '┘': 1753 /*portion[x][y] =*/ portion[x+1][y] = /*portion[x+2][y] =*/ 1754 portion[x][y+1] = portion[x+1][y+1] = /*portion[x+2][y+1] =*/ 1755 /*portion[x][y+2] = portion[x+1][y+2] = portion[x+2][y+2] =*/ 1.0; 1756 break; 1757 case '│': 1758 /*portion[x][y] =*/ portion[x+1][y] = /*portion[x+2][y] =*/ 1759 /*portion[x][y+1] =*/ portion[x+1][y+1] = /*portion[x+2][y+1] =*/ 1760 /*portion[x][y+2] =*/ portion[x+1][y+2] = /*portion[x+2][y+2] =*/ 1.0; 1761 break; 1762 case '─': 1763 portion[x][y+1] = portion[x+1][y+1] = portion[x+2][y+1] = 1.0; 1764 break; 1765 case '╴': 1766 portion[x][y+1] = portion[x+1][y+1] = 1.0; 1767 break; 1768 case '╵': 1769 /*portion[x][y] =*/ portion[x+1][y] = /*portion[x+2][y] =*/ 1770 /*portion[x][y+1] =*/ portion[x+1][y+1] = /*portion[x+2][y+1] =*/ 1.0; 1771 break; 1772 case '╶': 1773 portion[x+1][y+1] = portion[x+2][y+1] = 1.0; 1774 break; 1775 case '╷': 1776 /*portion[x][y+1] =*/ portion[x+1][y+1] = /*portion[x+2][y+1] =*/ 1777 /*portion[x][y+2] =*/ portion[x+1][y+2] = /*portion[x+2][y+2] =*/ 1.0; 1778 break; 1779 case '┼': 1780 /*portion[x][y] =*/ portion[x+1][y] = /*portion[x+2][y] =*/ 1781 portion[x][y+1] = portion[x+1][y+1] = portion[x+2][y+1] = 1782 /*portion[x][y+2] =*/ portion[x+1][y+2] = /*portion[x+2][y+2] =*/ 1.0; 1783 break; 1784 } 1785 } 1786 } 1787 return portion; 1788 } 1789 1790}