package squidpony.squidgrid; import squidpony.squidmath.Coord; import squidpony.squidmath.Coord3D; import squidpony.squidmath.OrderedSet; import squidpony.squidmath.RNG; import java.util.ArrayList; import java.util.List; import java.util.Set; /** * Basic radius strategy implementations likely to be used for roguelikes. * * @author Eben Howard - http://squidpony.com - howard@squidpony.com */ public enum Radius { /** * In an unobstructed area the FOV would be a square. * * This is the shape that would represent movement radius in an 8-way * movement scheme with no additional cost for diagonal movement. */ SQUARE, /** * In an unobstructed area the FOV would be a diamond. * * This is the shape that would represent movement radius in a 4-way * movement scheme. */ DIAMOND, /** * In an unobstructed area the FOV would be a circle. * * This is the shape that would represent movement radius in an 8-way * movement scheme with all movement cost the same based on distance from * the source */ CIRCLE, /** * In an unobstructed area the FOV would be a cube. * * This is the shape that would represent movement radius in an 8-way * movement scheme with no additional cost for diagonal movement. */ CUBE, /** * In an unobstructed area the FOV would be a octahedron. * * This is the shape that would represent movement radius in a 4-way * movement scheme. */ OCTAHEDRON, /** * In an unobstructed area the FOV would be a sphere. * * This is the shape that would represent movement radius in an 8-way * movement scheme with all movement cost the same based on distance from * the source */ SPHERE; private static final double PI2 = Math.PI * 2; public double radius(int startx, int starty, int startz, int endx, int endy, int endz) { return radius((double) startx, (double) starty, (double) startz, (double) endx, (double) endy, (double) endz); } public double radius(double startx, double starty, double startz, double endx, double endy, double endz) { double dx = Math.abs(startx - endx); double dy = Math.abs(starty - endy); double dz = Math.abs(startz - endz); return radius(dx, dy, dz); } public double radius(int dx, int dy, int dz) { return radius((float) dx, (float) dy, (float) dz); } public double radius(double dx, double dy, double dz) { dx = Math.abs(dx); dy = Math.abs(dy); dz = Math.abs(dz); double radius = 0; switch (this) { case SQUARE: case CUBE: radius = Math.max(dx, Math.max(dy, dz));//radius is longest axial distance break; case DIAMOND: case OCTAHEDRON: radius = dx + dy + dz;//radius is the manhattan distance break; case CIRCLE: case SPHERE: radius = Math.sqrt(dx * dx + dy * dy + dz * dz);//standard spherical radius } return radius; } public double radius(int startx, int starty, int endx, int endy) { return radius((double) startx, (double) starty, (double) endx, (double) endy); } public double radius(Coord start, Coord end) { return radius((double) start.x, (double) start.y, (double) end.x, (double) end.y); } public double radius(Coord end) { return radius(0.0, 0.0, (double) end.x, (double) end.y); } public double radius(double startx, double starty, double endx, double endy) { double dx = startx - endx; double dy = starty - endy; return radius(dx, dy); } public double radius(int dx, int dy) { return radius((double) dx, (double) dy); } public double radius(double dx, double dy) { return radius(dx, dy, 0); } public Coord onUnitShape(double distance, RNG rng) { int x = 0, y = 0; switch (this) { case SQUARE: case CUBE: x = rng.between((int) -distance, (int) distance + 1); y = rng.between((int) -distance, (int) distance + 1); break; case DIAMOND: case OCTAHEDRON: x = rng.between((int) -distance, (int) distance + 1); y = rng.between((int) -distance, (int) distance + 1); if (radius(x, y) > distance) { if (x > 0) { if (y > 0) { x = (int) (distance - x); y = (int) (distance - y); } else { x = (int) (distance - x); y = (int) (-distance - y); } } else { if (y > 0) { x = (int) (-distance - x); y = (int) (distance - y); } else { x = (int) (-distance - x); y = (int) (-distance - y); } } } break; case CIRCLE: case SPHERE: double radius = distance * Math.sqrt(rng.between(0.0, 1.0)); double theta = rng.between(0, PI2); x = (int) Math.round(Math.cos(theta) * radius); y = (int) Math.round(Math.sin(theta) * radius); } return Coord.get(x, y); } public Coord3D onUnitShape3D(double distance, RNG rng) { int x = 0, y = 0, z = 0; switch (this) { case SQUARE: case DIAMOND: case CIRCLE: Coord p = onUnitShape(distance, rng); return new Coord3D(p.x, p.y, 0);//2D strategies case CUBE: x = rng.between((int) -distance, (int) distance + 1); y = rng.between((int) -distance, (int) distance + 1); z = rng.between((int) -distance, (int) distance + 1); break; case OCTAHEDRON: case SPHERE: do { x = rng.between((int) -distance, (int) distance + 1); y = rng.between((int) -distance, (int) distance + 1); z = rng.between((int) -distance, (int) distance + 1); } while (radius(x, y, z) > distance); } return new Coord3D(x, y, z); } public double volume2D(double radiusLength) { switch (this) { case SQUARE: case CUBE: return (radiusLength * 2 + 1) * (radiusLength * 2 + 1); case DIAMOND: case OCTAHEDRON: return radiusLength * (radiusLength + 1) * 2 + 1; default: return Math.PI * radiusLength * radiusLength + 1; } } public double volume3D(double radiusLength) { switch (this) { case SQUARE: case CUBE: return (radiusLength * 2 + 1) * (radiusLength * 2 + 1) * (radiusLength * 2 + 1); case DIAMOND: case OCTAHEDRON: double total = radiusLength * (radiusLength + 1) * 2 + 1; for(double i = radiusLength - 1; i >= 0; i--) { total += (i * (i + 1) * 2 + 1) * 2; } return total; default: return Math.PI * radiusLength * radiusLength * radiusLength * 4.0 / 3.0 + 1; } } private int clamp(int n, int min, int max) { return Math.min(Math.max(min, n), max - 1); } public OrderedSet<Coord> perimeter(Coord center, int radiusLength, boolean surpassEdges, int width, int height) { OrderedSet<Coord> rim = new OrderedSet<>(4 * radiusLength); if(!surpassEdges && (center.x < 0 || center.x >= width || center.y < 0 || center.y > height)) return rim; if(radiusLength < 1) { rim.add(center); return rim; } switch (this) { case SQUARE: case CUBE: { for (int i = center.x - radiusLength; i <= center.x + radiusLength; i++) { int x = i; if(!surpassEdges) x = clamp(i, 0, width); rim.add(Coord.get(x, clamp(center.y - radiusLength, 0, height))); rim.add(Coord.get(x, clamp(center.y + radiusLength, 0, height))); } for (int j = center.y - radiusLength; j <= center.y + radiusLength; j++) { int y = j; if(!surpassEdges) y = clamp(j, 0, height); rim.add(Coord.get(clamp(center.x - radiusLength, 0, height), y)); rim.add(Coord.get(clamp(center.x + radiusLength, 0, height), y)); } } break; case DIAMOND: case OCTAHEDRON: { int xUp = center.x + radiusLength, xDown = center.x - radiusLength, yUp = center.y + radiusLength, yDown = center.y - radiusLength; if(!surpassEdges) { xDown = clamp(xDown, 0, width); xUp = clamp(xUp, 0, width); yDown = clamp(yDown, 0, height); yUp = clamp(yUp, 0, height); } rim.add(Coord.get(xDown, center.y)); rim.add(Coord.get(xUp, center.y)); rim.add(Coord.get(center.x, yDown)); rim.add(Coord.get(center.x, yUp)); for (int i = xDown + 1, c = 1; i < center.x; i++, c++) { int x = i; if(!surpassEdges) x = clamp(i, 0, width); rim.add(Coord.get(x, clamp(center.y - c, 0, height))); rim.add(Coord.get(x, clamp(center.y + c, 0, height))); } for (int i = center.x + 1, c = 1; i < center.x + radiusLength; i++, c++) { int x = i; if(!surpassEdges) x = clamp(i, 0, width); rim.add(Coord.get(x, clamp(center.y + radiusLength - c, 0, height))); rim.add(Coord.get(x, clamp(center.y - radiusLength + c, 0, height))); } } break; default: { double theta; int x, y, denom = 1; boolean anySuccesses; while(denom <= 256) { anySuccesses = false; for (int i = 1; i <= denom; i+=2) { theta = i * (PI2 / denom); x = (int) (Math.cos(theta) * (radiusLength + 0.25)) + center.x; y = (int) (Math.sin(theta) * (radiusLength + 0.25)) + center.y; if (!surpassEdges) { x = clamp(x, 0, width); y = clamp(y, 0, height); } Coord p = Coord.get(x, y); boolean test = !rim.contains(p); rim.add(p); anySuccesses = test || anySuccesses; } if(!anySuccesses) break; denom *= 2; } } } return rim; } public Coord extend(Coord center, Coord middle, int radiusLength, boolean surpassEdges, int width, int height) { if(!surpassEdges && (center.x < 0 || center.x >= width || center.y < 0 || center.y > height || middle.x < 0 || middle.x >= width || middle.y < 0 || middle.y > height)) return Coord.get(0, 0); if(radiusLength < 1) { return center; } double theta = Math.atan2(middle.y - center.y, middle.x - center.x), cosTheta = Math.cos(theta), sinTheta = Math.sin(theta); Coord end = Coord.get(middle.x, middle.y); switch (this) { case SQUARE: case CUBE: case DIAMOND: case OCTAHEDRON: { int rad2 = 0; if(surpassEdges) { while (radius(center.x, center.y, end.x, end.y) < radiusLength) { rad2++; end = Coord.get((int) Math.round(cosTheta * rad2) + center.x , (int) Math.round(sinTheta * rad2) + center.y); } } else { while (radius(center.x, center.y, end.x, end.y) < radiusLength) { rad2++; end = Coord.get(clamp((int) Math.round(cosTheta * rad2) + center.x, 0, width) , clamp((int) Math.round(sinTheta * rad2) + center.y, 0, height)); if (end.x == 0 || end.x == width - 1 || end.y == 0 || end.y == height - 1) return end; } } return end; } default: { end = Coord.get(clamp( (int) Math.round(cosTheta * radiusLength) + center.x, 0, width) , clamp( (int) Math.round(sinTheta * radiusLength) + center.y, 0, height)); if(!surpassEdges) { long edgeLength = 0; // if (end.x == 0 || end.x == width - 1 || end.y == 0 || end.y == height - 1) if (end.x < 0) { // wow, we lucked out here. the only situation where cos(angle) is 0 is if the angle aims // straight up or down, and then x cannot be < 0 or >= width. edgeLength = Math.round((0 - center.x) / cosTheta); end = end.setY(clamp((int) Math.round(sinTheta * edgeLength) + center.y, 0, height)); } else if(end.x >= width) { // wow, we lucked out here. the only situation where cos(angle) is 0 is if the angle aims // straight up or down, and then x cannot be < 0 or >= width. edgeLength = Math.round((width - 1 - center.x) / cosTheta); end = end.setY(clamp((int) Math.round(sinTheta * edgeLength) + center.y, 0, height)); } if (end.y < 0) { // wow, we lucked out here. the only situation where sin(angle) is 0 is if the angle aims // straight left or right, and then y cannot be < 0 or >= height. edgeLength = Math.round((0 - center.y) / sinTheta); end = end.setX(clamp((int) Math.round(cosTheta * edgeLength) + center.x, 0, width)); } else if(end.y >= height) { // wow, we lucked out here. the only situation where sin(angle) is 0 is if the angle aims // straight left or right, and then y cannot be < 0 or >= height. edgeLength = Math.round((height - 1 - center.y) / sinTheta); end = end.setX(clamp((int) Math.round(cosTheta * edgeLength) + center.x, 0, width)); } } return end; } } } /** * Compares two Radius enums as if they are both in a 2D plane; that is, Radius.SPHERE is treated as equal to * Radius.CIRCLE, Radius.CUBE is equal to Radius.SQUARE, and Radius.OCTAHEDRON is equal to Radius.DIAMOND. * @param other the Radius to compare this to * @return true if the 2D versions of both Radius enums are the same shape. */ public boolean equals2D(Radius other) { switch (this) { case CIRCLE: case SPHERE: return other == CIRCLE || other == SPHERE; case SQUARE: case CUBE: return other == SQUARE || other == CUBE; default: return other == DIAMOND || other == OCTAHEDRON; } } public boolean inRange(int startx, int starty, int endx, int endy, int minRange, int maxRange) { double dist = radius(startx, starty, endx, endy); return dist >= minRange - 0.001 && dist <= maxRange + 0.001; } public int roughDistance(int xPos, int yPos) { int x = Math.abs(xPos), y = Math.abs(yPos); switch (this) { case CIRCLE: case SPHERE: { if(x == y) return 3 * x; else if(x < y) return 3 * x + 2 * (y - x); else return 3 * y + 2 * (x - y); } case DIAMOND: case OCTAHEDRON: return 2 * (x + y); default: return 2 * Math.max(x, y); } } public List<Coord> pointsInside(int centerX, int centerY, int radiusLength, boolean surpassEdges, int width, int height) { return pointsInside(centerX, centerY, radiusLength, surpassEdges, width, height, null); } public List<Coord> pointsInside(Coord center, int radiusLength, boolean surpassEdges, int width, int height) { if(center == null) return null; return pointsInside(center.x, center.y, radiusLength, surpassEdges, width, height, null); } public List<Coord> pointsInside(int centerX, int centerY, int radiusLength, boolean surpassEdges, int width, int height, List<Coord> buf) { List<Coord> contents = buf == null ? new ArrayList<Coord>((int)Math.ceil(volume2D(radiusLength))) : buf; if(!surpassEdges && (centerX < 0 || centerX >= width || centerY < 0 || centerY >= height)) return contents; if(radiusLength < 1) { contents.add(Coord.get(centerX, centerY)); return contents; } switch (this) { case SQUARE: case CUBE: { for (int i = centerX - radiusLength; i <= centerX + radiusLength; i++) { for (int j = centerY - radiusLength; j <= centerY + radiusLength; j++) { if(!surpassEdges && (i < 0 || j < 0 || i >= width || j >= height)) continue; contents.add(Coord.get(i, j)); } } } break; case DIAMOND: case OCTAHEDRON: { for (int i = centerX - radiusLength; i <= centerX + radiusLength; i++) { for (int j = centerY - radiusLength; j <= centerY + radiusLength; j++) { if ((Math.abs(centerX - i) + Math.abs(centerY- j) > radiusLength) || (!surpassEdges && (i < 0 || j < 0 || i >= width || j >= height))) continue; contents.add(Coord.get(i, j)); } } } break; default: { int high; for (int dx = -radiusLength; dx <= radiusLength; ++dx) { high = (int) Math.sqrt(radiusLength * radiusLength - dx * dx); for (int dy = -high; dy <= high; ++dy) { if (!surpassEdges && (centerX + dx < 0 || centerY + dy < 0 || centerX + dx >= width || centerY + dy >= height)) continue; contents.add(Coord.get(centerX + dx, centerY + dy)); } } } } return contents; } /** * Gets a List of all Coord points within {@code radiusLength} of {@code center} using Chebyshev measurement (making * a square). Appends Coords to {@code buf} if it is non-null, and returns either buf or a freshly-allocated List of * Coord. If {@code surpassEdges} is false, which is the normal usage, this will not produce Coords with x or y less * than 0 or greater than {@code width} or {@code height}; if surpassEdges is true, then it can produce any Coords * in the actual radius. * @param centerX the center Coord x * @param centerY the center Coord x * @param radiusLength the inclusive distance from (centerX,centerY) for Coords to use in the List * @param surpassEdges usually should be false; if true, can produce Coords with negative x/y or past width/height * @param width the width of the area this can place Coords (exclusive, not relative to center, usually map width) * @param height the height of the area this can place Coords (exclusive, not relative to center, usually map height) * @return a new List containing the points within radiusLength of the center */ public static List<Coord> inSquare(int centerX, int centerY, int radiusLength, boolean surpassEdges, int width, int height) { return SQUARE.pointsInside(centerX, centerY, radiusLength, surpassEdges, width, height, null); } /** * Gets a List of all Coord points within {@code radiusLength} of {@code center} using Manhattan measurement (making * a diamond). Appends Coords to {@code buf} if it is non-null, and returns either buf or a freshly-allocated List * of Coord. If {@code surpassEdges} is false, which is the normal usage, this will not produce Coords with x or y * less than 0 or greater than {@code width} or {@code height}; if surpassEdges is true, then it can produce any * Coords in the actual radius. * @param centerX the center Coord x * @param centerY the center Coord x * @param radiusLength the inclusive distance from (centerX,centerY) for Coords to use in the List * @param surpassEdges usually should be false; if true, can produce Coords with negative x/y or past width/height * @param width the width of the area this can place Coords (exclusive, not relative to center, usually map width) * @param height the height of the area this can place Coords (exclusive, not relative to center, usually map height) * @return a new List containing the points within radiusLength of the center */ public static List<Coord> inDiamond(int centerX, int centerY, int radiusLength, boolean surpassEdges, int width, int height) { return DIAMOND.pointsInside(centerX, centerY, radiusLength, surpassEdges, width, height, null); } /** * Gets a List of all Coord points within {@code radiusLength} of {@code center} using Euclidean measurement (making * a circle). Appends Coords to {@code buf} if it is non-null, and returns either buf or a freshly-allocated List of * Coord. If {@code surpassEdges} is false, which is the normal usage, this will not produce Coords with x or y less * than 0 or greater than {@code width} or {@code height}; if surpassEdges is true, then it can produce any Coords * in the actual radius. * @param centerX the center Coord x * @param centerY the center Coord x * @param radiusLength the inclusive distance from (centerX,centerY) for Coords to use in the List * @param surpassEdges usually should be false; if true, can produce Coords with negative x/y or past width/height * @param width the width of the area this can place Coords (exclusive, not relative to center, usually map width) * @param height the height of the area this can place Coords (exclusive, not relative to center, usually map height) * @return a new List containing the points within radiusLength of the center */ public static List<Coord> inCircle(int centerX, int centerY, int radiusLength, boolean surpassEdges, int width, int height) { return CIRCLE.pointsInside(centerX, centerY, radiusLength, surpassEdges, width, height, null); } /** * Gets a List of all Coord points within {@code radiusLength} of {@code center} using Chebyshev measurement (making * a square). Appends Coords to {@code buf} if it is non-null, and returns either buf or a freshly-allocated List of * Coord. If {@code surpassEdges} is false, which is the normal usage, this will not produce Coords with x or y less * than 0 or greater than {@code width} or {@code height}; if surpassEdges is true, then it can produce any Coords * in the actual radius. * @param centerX the center Coord x * @param centerY the center Coord x * @param radiusLength the inclusive distance from (centerX,centerY) for Coords to use in the List * @param surpassEdges usually should be false; if true, can produce Coords with negative x/y or past width/height * @param width the width of the area this can place Coords (exclusive, not relative to center, usually map width) * @param height the height of the area this can place Coords (exclusive, not relative to center, usually map height) * @param buf the List of Coord to append points to; may be null to create a new List * @return buf, after appending Coords to it, or a new List if buf was null */ public static List<Coord> inSquare(int centerX, int centerY, int radiusLength, boolean surpassEdges, int width, int height, List<Coord> buf) { return SQUARE.pointsInside(centerX, centerY, radiusLength, surpassEdges, width, height, buf); } /** * Gets a List of all Coord points within {@code radiusLength} of {@code center} using Manhattan measurement (making * a diamond). Appends Coords to {@code buf} if it is non-null, and returns either buf or a freshly-allocated List * of Coord. If {@code surpassEdges} is false, which is the normal usage, this will not produce Coords with x or y * less than 0 or greater than {@code width} or {@code height}; if surpassEdges is true, then it can produce any * Coords in the actual radius. * @param centerX the center Coord x * @param centerY the center Coord x * @param radiusLength the inclusive distance from (centerX,centerY) for Coords to use in the List * @param surpassEdges usually should be false; if true, can produce Coords with negative x/y or past width/height * @param width the width of the area this can place Coords (exclusive, not relative to center, usually map width) * @param height the height of the area this can place Coords (exclusive, not relative to center, usually map height) * @param buf the List of Coord to append points to; may be null to create a new List * @return buf, after appending Coords to it, or a new List if buf was null */ public static List<Coord> inDiamond(int centerX, int centerY, int radiusLength, boolean surpassEdges, int width, int height, List<Coord> buf) { return DIAMOND.pointsInside(centerX, centerY, radiusLength, surpassEdges, width, height, buf); } /** * Gets a List of all Coord points within {@code radiusLength} of {@code center} using Euclidean measurement (making * a circle). Appends Coords to {@code buf} if it is non-null, and returns either buf or a freshly-allocated List of * Coord. If {@code surpassEdges} is false, which is the normal usage, this will not produce Coords with x or y less * than 0 or greater than {@code width} or {@code height}; if surpassEdges is true, then it can produce any Coords * in the actual radius. * @param centerX the center Coord x * @param centerY the center Coord x * @param radiusLength the inclusive distance from (centerX,centerY) for Coords to use in the List * @param surpassEdges usually should be false; if true, can produce Coords with negative x/y or past width/height * @param width the width of the area this can place Coords (exclusive, not relative to center, usually map width) * @param height the height of the area this can place Coords (exclusive, not relative to center, usually map height) * @param buf the List of Coord to append points to; may be null to create a new List * @return buf, after appending Coords to it, or a new List if buf was null */ public static List<Coord> inCircle(int centerX, int centerY, int radiusLength, boolean surpassEdges, int width, int height, List<Coord> buf) { return CIRCLE.pointsInside(centerX, centerY, radiusLength, surpassEdges, width, height, buf); } /** * Given an Iterable of Coord (such as a List or Set), a distance to expand outward by (using this Radius), and the * bounding height and width of the map, gets a "thickened" group of Coord as a Set where each Coord in points has * been expanded out by an amount no greater than distance. As an example, you could call this on a line generated * by Bresenham, OrthoLine, or an LOS object's getLastPath() method, and expand the line into a thick "brush stroke" * where this Radius affects the shape of the ends. This will never produce a Coord with negative x or y, a Coord * with x greater than or equal to width, or a Coord with y greater than or equal to height. * @param distance the distance, as measured by this Radius, to expand each Coord on points up to * @param width the bounding width of the map (exclusive) * @param height the bounding height of the map (exclusive) * @param points an Iterable (such as a List or Set) of Coord that this will make a "thickened" version of * @return a Set of Coord that covers a wider area than what points covers; each Coord will be unique (it's a Set) */ public Set<Coord> expand(int distance, int width, int height, Iterable<Coord> points) { List<Coord> around = pointsInside(Coord.get(distance, distance), distance, false, width, height); OrderedSet<Coord> expanded = new OrderedSet<>(around.size() * 16); int tx, ty; for(Coord pt : points) { for(Coord ar : around) { tx = pt.x + ar.x - distance; ty = pt.y + ar.y - distance; if(tx >= 0 && tx < width && ty >= 0 && ty < height) expanded.add(Coord.get(tx, ty)); } } return expanded; } }