001package squidpony.squidgrid; 002 003import squidpony.squidmath.Coord; 004import squidpony.squidmath.GWTRNG; 005import squidpony.squidmath.IRNG; 006import squidpony.squidmath.OrderedMap; 007 008import java.util.Map; 009import java.util.Set; 010 011/** 012 * This class is used to determine when a sound is audible on a map and at what positions. 013 * Created by Tommy Ettinger on 4/4/2015. 014 */ 015public class SoundMap 016{ 017 018 /** 019 * This affects how sound travels on diagonal directions vs. orthogonal directions. MANHATTAN should form a diamond 020 * shape on a featureless map, while CHEBYSHEV will form a square. 021 */ 022 public Measurement measurement = Measurement.MANHATTAN; 023 024 025 /** 026 * Stores which parts of the map are accessible and which are not. Should not be changed unless the actual physical 027 * terrain has changed. You should call initialize() with a new map instead of changing this directly. 028 */ 029 public double[][] physicalMap; 030 /** 031 * The frequently-changing values that are often the point of using this class; cells producing sound will have a 032 * value greater than 0, cells that cannot possibly be reached by a sound will have a value of exactly 0, and walls 033 * will have a value equal to the WALL constant (a negative number). 034 */ 035 public double[][] gradientMap; 036 /** 037 * Height of the map. Exciting stuff. Don't change this, instead call initialize(). 038 */ 039 public int height; 040 /** 041 * Width of the map. Exciting stuff. Don't change this, instead call initialize(). 042 */ 043 public int width; 044 /** 045 * The latest results of findAlerted(), with Coord keys representing the positions of creatures that were alerted 046 * and Double values representing how loud the sound was when it reached them. 047 */ 048 public OrderedMap<Coord, Double> alerted; 049 /** 050 * Cells with no sound are always marked with 0. 051 */ 052 public static final double SILENT = 0.0; 053 /** 054 * Walls, which are solid no-entry cells, are marked with a significant negative number equal to -999500.0 . 055 */ 056 public static final double WALL = -999500.0; 057 /** 058 * Sources of sound on the map; keys are positions, values are how loud the noise is (10.0 should spread 10 cells 059 * away, with diminishing values assigned to further positions). 060 */ 061 public OrderedMap<Coord, Double> sounds; 062 private OrderedMap<Coord, Double> fresh; 063 /** 064 * The RNG used to decide which one of multiple equally-short paths to take. 065 */ 066 public IRNG rng; 067 068 private boolean initialized; 069 /** 070 * Construct a SoundMap without a level to actually scan. If you use this constructor, you must call an 071 * initialize() method before using this class. 072 */ 073 public SoundMap() { 074 rng = new GWTRNG(); 075 alerted = new OrderedMap<>(); 076 fresh = new OrderedMap<>(); 077 sounds = new OrderedMap<>(); 078 } 079 080 /** 081 * Construct a SoundMap without a level to actually scan. This constructor allows you to specify an RNG before it is 082 * used. If you use this constructor, you must call an initialize() method before using this class. 083 */ 084 public SoundMap(IRNG random) { 085 rng = random; 086 alerted = new OrderedMap<>(); 087 fresh = new OrderedMap<>(); 088 sounds = new OrderedMap<>(); 089 } 090 091 /** 092 * Used to construct a SoundMap from the output of another. Any sounds will need to be assigned again. 093 * @param level 094 */ 095 public SoundMap(final double[][] level) { 096 rng = new GWTRNG(); 097 alerted = new OrderedMap<>(); 098 fresh = new OrderedMap<>(); 099 sounds = new OrderedMap<>(); 100 initialize(level); 101 } 102 /** 103 * Used to construct a DijkstraMap from the output of another, specifying a distance calculation. 104 * @param level 105 * @param measurement 106 */ 107 public SoundMap(final double[][] level, Measurement measurement) { 108 rng = new GWTRNG(); 109 this.measurement = measurement; 110 alerted = new OrderedMap<>(); 111 fresh = new OrderedMap<>(); 112 sounds = new OrderedMap<>(); 113 initialize(level); 114 } 115 116 /** 117 * Constructor meant to take a char[][] returned by DungeonBoneGen.generate(), or any other 118 * char[][] where '#' means a wall and anything else is a walkable tile. If you only have 119 * a map that uses box-drawing characters, use DungeonUtility.linesToHashes() to get a 120 * map that can be used here. 121 * 122 * @param level 123 */ 124 public SoundMap(final char[][] level) { 125 rng = new GWTRNG(); 126 alerted = new OrderedMap<>(); 127 fresh = new OrderedMap<>(); 128 sounds = new OrderedMap<>(); 129 initialize(level); 130 } 131 /** 132 * Constructor meant to take a char[][] returned by DungeonBoneGen.generate(), or any other 133 * char[][] where one char means a wall and anything else is a walkable tile. If you only have 134 * a map that uses box-drawing characters, use DungeonUtility.linesToHashes() to get a 135 * map that can be used here. You can specify the character used for walls. 136 * 137 * @param level 138 */ 139 public SoundMap(final char[][] level, char alternateWall) { 140 rng = new GWTRNG(); 141 alerted = new OrderedMap<>(); 142 fresh = new OrderedMap<>(); 143 sounds = new OrderedMap<>(); 144 initialize(level, alternateWall); 145 } 146 147 /** 148 * Constructor meant to take a char[][] returned by DungeonBoneGen.generate(), or any other 149 * char[][] where '#' means a wall and anything else is a walkable tile. If you only have 150 * a map that uses box-drawing characters, use DungeonUtility.linesToHashes() to get a 151 * map that can be used here. This constructor specifies a distance measurement. 152 * 153 * @param level 154 * @param measurement 155 */ 156 public SoundMap(final char[][] level, Measurement measurement) { 157 rng = new GWTRNG(); 158 this.measurement = measurement; 159 alerted = new OrderedMap<>(); 160 fresh = new OrderedMap<>(); 161 sounds = new OrderedMap<>(); 162 initialize(level); 163 } 164 165 /** 166 * Used to initialize or re-initialize a SoundMap that needs a new PhysicalMap because it either wasn't given 167 * one when it was constructed, or because the contents of the terrain have changed permanently. 168 * @param level 169 * @return 170 */ 171 public SoundMap initialize(final double[][] level) { 172 width = level.length; 173 height = level[0].length; 174 gradientMap = new double[width][height]; 175 physicalMap = new double[width][height]; 176 for (int y = 0; y < height; y++) { 177 for (int x = 0; x < width; x++) { 178 gradientMap[x][y] = level[x][y]; 179 physicalMap[x][y] = level[x][y]; 180 } 181 } 182 initialized = true; 183 return this; 184 } 185 186 /** 187 * Used to initialize or re-initialize a SoundMap that needs a new PhysicalMap because it either wasn't given 188 * one when it was constructed, or because the contents of the terrain have changed permanently. 189 * @param level 190 * @return 191 */ 192 public SoundMap initialize(final char[][] level) { 193 width = level.length; 194 height = level[0].length; 195 gradientMap = new double[width][height]; 196 physicalMap = new double[width][height]; 197 for (int y = 0; y < height; y++) { 198 for (int x = 0; x < width; x++) { 199 double t = (level[x][y] == '#') ? WALL : SILENT; 200 gradientMap[x][y] = t; 201 physicalMap[x][y] = t; 202 } 203 } 204 initialized = true; 205 return this; 206 } 207 208 /** 209 * Used to initialize or re-initialize a SoundMap that needs a new PhysicalMap because it either wasn't given 210 * one when it was constructed, or because the contents of the terrain have changed permanently. This 211 * initialize() method allows you to specify an alternate wall char other than the default character, '#' . 212 * @param level 213 * @param alternateWall 214 * @return 215 */ 216 public SoundMap initialize(final char[][] level, char alternateWall) { 217 width = level.length; 218 height = level[0].length; 219 gradientMap = new double[width][height]; 220 physicalMap = new double[width][height]; 221 for (int y = 0; y < height; y++) { 222 for (int x = 0; x < width; x++) { 223 double t = (level[x][y] == alternateWall) ? WALL : SILENT; 224 gradientMap[x][y] = t; 225 physicalMap[x][y] = t; 226 } 227 } 228 initialized = true; 229 return this; 230 } 231 232 /** 233 * Resets the gradientMap to its original value from physicalMap. Does not remove sounds (they will still affect 234 * scan() normally). 235 */ 236 public void resetMap() { 237 if(!initialized) return; 238 for (int y = 0; y < height; y++) { 239 for (int x = 0; x < width; x++) { 240 gradientMap[x][y] = physicalMap[x][y]; 241 } 242 } 243 } 244 245 /** 246 * Resets this SoundMap to a state with no sounds, no alerted creatures, and no changes made to gradientMap 247 * relative to physicalMap. 248 */ 249 public void reset() { 250 resetMap(); 251 alerted.clear(); 252 fresh.clear(); 253 sounds.clear(); 254 } 255 256 /** 257 * Marks a cell as producing a sound with the given loudness; this can be placed on a wall or unreachable area, 258 * but that may cause the sound to be un-hear-able. A sound emanating from a cell on one side of a 2-cell-thick 259 * wall will only radiate sound on one side, which can be used for certain effects. A sound emanating from a cell 260 * in a 1-cell-thick wall will radiate on both sides. 261 * @param x 262 * @param y 263 * @param loudness The number of cells the sound should spread away using the current measurement. 264 */ 265 public void setSound(int x, int y, double loudness) { 266 if(!initialized) return; 267 Coord pt = Coord.get(x, y); 268 if(sounds.containsKey(pt) && sounds.get(pt) >= loudness) 269 return; 270 sounds.put(pt, loudness); 271 } 272 273 /** 274 * Marks a cell as producing a sound with the given loudness; this can be placed on a wall or unreachable area, 275 * but that may cause the sound to be un-hear-able. A sound emanating from a cell on one side of a 2-cell-thick 276 * wall will only radiate sound on one side, which can be used for certain effects. A sound emanating from a cell 277 * in a 1-cell-thick wall will radiate on both sides. 278 * @param pt 279 * @param loudness The number of cells the sound should spread away using the current measurement. 280 */ 281 public void setSound(Coord pt, double loudness) { 282 if(!initialized) return; 283 if(sounds.containsKey(pt) && sounds.get(pt) >= loudness) 284 return; 285 sounds.put(pt, loudness); 286 } 287 288 /** 289 * If a sound is being produced at a given (x, y) location, this removes it. 290 * @param x 291 * @param y 292 */ 293 public void removeSound(int x, int y) { 294 if(!initialized) return; 295 Coord pt = Coord.get(x, y); 296 sounds.remove(pt); 297 } 298 299 /** 300 * If a sound is being produced at a given location (a Coord), this removes it. 301 * @param pt 302 */ 303 public void removeSound(Coord pt) { 304 if(!initialized) return; 305 sounds.remove(pt); 306 } 307 308 /** 309 * Marks a specific cell in gradientMap as a wall, which makes sounds potentially unable to pass through it. 310 * @param x 311 * @param y 312 */ 313 public void setOccupied(int x, int y) { 314 if(!initialized) return; 315 gradientMap[x][y] = WALL; 316 } 317 318 /** 319 * Reverts a cell to the value stored in the original state of the level as known by physicalMap. 320 * @param x 321 * @param y 322 */ 323 public void resetCell(int x, int y) { 324 if(!initialized) return; 325 gradientMap[x][y] = physicalMap[x][y]; 326 } 327 328 /** 329 * Reverts a cell to the value stored in the original state of the level as known by physicalMap. 330 * @param pt 331 */ 332 public void resetCell(Coord pt) { 333 if(!initialized) return; 334 gradientMap[pt.x][pt.y] = physicalMap[pt.x][pt.y]; 335 } 336 337 /** 338 * Used to remove all sounds. 339 */ 340 public void clearSounds() { 341 if(!initialized) return; 342 sounds.clear(); 343 } 344 345 protected void setFresh(int x, int y, double counter) { 346 if(!initialized) return; 347 gradientMap[x][y] = counter; 348 fresh.put(Coord.get(x, y), counter); 349 } 350 351 protected void setFresh(final Coord pt, double counter) { 352 if(!initialized) return; 353 gradientMap[pt.x][pt.y] = counter; 354 fresh.put(Coord.get(pt.x, pt.y), counter); 355 } 356 357 /** 358 * Recalculate the sound map and return it. Cells that were marked as goals with setSound will have 359 * a value greater than 0 (higher numbers are louder sounds), the cells adjacent to sounds will have a value 1 less 360 * than the loudest adjacent cell, and cells progressively further from sounds will have a value equal to the 361 * loudness of the nearest sound minus the distance from it, to a minimum of 0. The exceptions are walls, 362 * which will have a value defined by the WALL constant in this class. Like sound itself, the sound map 363 * allows some passage through walls; specifically, 1 cell thick of wall can be passed through, with reduced 364 * loudness, before the fill cannot go further. This uses the current measurement. 365 * 366 * @return A 2D double[width][height] using the width and height of what this knows about the physical map. 367 */ 368 public double[][] scan() { 369 if(!initialized) return null; 370 371 for (Map.Entry<Coord, Double> entry : sounds.entrySet()) { 372 gradientMap[entry.getKey().x][entry.getKey().y] = entry.getValue(); 373 if(fresh.containsKey(entry.getKey()) && fresh.get(entry.getKey()) > entry.getValue()) 374 { 375 } 376 else 377 { 378 fresh.put(entry.getKey(), entry.getValue()); 379 } 380 381 } 382 int numAssigned = fresh.size(); 383 384 Direction[] dirs = (measurement == Measurement.MANHATTAN) ? Direction.CARDINALS : Direction.OUTWARDS; 385 386 while (numAssigned > 0) { 387 numAssigned = 0; 388 OrderedMap<Coord, Double> fresh2 = new OrderedMap<>(fresh.size()); 389 fresh2.putAll(fresh); 390 fresh.clear(); 391 392 for (Map.Entry<Coord, Double> cell : fresh2.entrySet()) { 393 if(cell.getValue() <= 1) //We shouldn't assign values lower than 1. 394 continue; 395 for (int d = 0; d < dirs.length; d++) { 396 Coord adj = cell.getKey().translate(dirs[d].deltaX, dirs[d].deltaY); 397 if(adj.x < 0 || adj.x >= width || adj.y < 0 || adj.y >= height) 398 continue; 399 if(physicalMap[cell.getKey().x][cell.getKey().y] == WALL && physicalMap[adj.x][adj.y] == WALL) 400 continue; 401 if (gradientMap[cell.getKey().x][cell.getKey().y] > gradientMap[adj.x][adj.y] + 1) { 402 double v = cell.getValue() - 1 - ((physicalMap[adj.x][adj.y] == WALL) ? 1 : 0); 403 if (v > 0) { 404 gradientMap[adj.x][adj.y] = v; 405 fresh.put(Coord.get(adj.x, adj.y), v); 406 ++numAssigned; 407 } 408 } 409 } 410 } 411 } 412 413 for (int y = 0; y < height; y++) { 414 for (int x = 0; x < width; x++) { 415 if (physicalMap[x][y] == WALL) { 416 gradientMap[x][y] = WALL; 417 } 418 } 419 } 420 421 return gradientMap; 422 } 423 424 /** 425 * Scans the dungeon using SoundMap.scan, adding any positions in extraSounds to the group of known sounds before 426 * scanning. The creatures passed to this function as a Set of Points will have the loudness of all sounds at 427 * their position put as the value in alerted corresponding to their Coord position. 428 * 429 * @param creatures 430 * @param extraSounds 431 * @return 432 */ 433 public OrderedMap<Coord, Double> findAlerted(Set<Coord> creatures, Map<Coord, Double> extraSounds) { 434 if(!initialized) return null; 435 alerted = new OrderedMap<>(creatures.size()); 436 437 resetMap(); 438 for (Map.Entry<Coord, Double> sound : extraSounds.entrySet()) { 439 setSound(sound.getKey(), sound.getValue()); 440 } 441 scan(); 442 for(Coord critter : creatures) 443 { 444 if(critter.x < 0 || critter.x >= width || critter.y < 0 || critter.y >= height) 445 continue; 446 alerted.put(Coord.get(critter.x, critter.y), gradientMap[critter.x][critter.y]); 447 } 448 return alerted; 449 } 450}