package squidpony.squidai;
import squidpony.annotation.GwtIncompatible;
import squidpony.squidgrid.FOV;
import squidpony.squidgrid.FOVCache;
import squidpony.squidgrid.Radius;
import squidpony.squidgrid.mapping.DungeonUtility;
import squidpony.squidmath.Coord;
import squidpony.squidmath.OrderedMap;
import squidpony.squidmath.OrderedSet;
import java.util.*;
/**
* An AOE type that has an origin, a radius, an angle, and a span; it will blast from the origin to a length equal to
* radius along the angle (in degrees), moving somewhat around corners/obstacles, and also spread a total of span
* degrees around the angle (a span of 90 will affect a full quadrant, centered on angle). You can specify the
* RadiusType to Radius.DIAMOND for Manhattan distance, RADIUS.SQUARE for Chebyshev, or RADIUS.CIRCLE for Euclidean.
*
* RADIUS.CIRCLE (Euclidean measurement) will produce the most real-looking cones. This will produce doubles for its
* findArea() method which are greater than 0.0 and less than or equal to 1.0.
*
* This class uses squidpony.squidgrid.FOV to create its area of effect.
* Created by Tommy Ettinger on 7/13/2015.
*/
public class ConeAOE implements AOE {
private FOV fov;
private Coord origin;
private double radius, angle, span;
private double[][] map;
private char[][] dungeon;
private Radius radiusType;
private Reach reach = new Reach(1, 1, Radius.SQUARE, null);
public ConeAOE(Coord origin, Coord endCenter, double span, Radius radiusType)
{
fov = new FOV(FOV.RIPPLE_LOOSE);
this.origin = origin;
radius = radiusType.radius(origin.x, origin.y, endCenter.x, endCenter.y);
angle = (Math.toDegrees(Math.atan2(endCenter.y - origin.y, endCenter.x - origin.x)) % 360.0 + 360.0) % 360.0;
// this.startAngle = Math.abs((angle - span / 2.0) % 360.0);
// this.endAngle = Math.abs((angle + span / 2.0) % 360.0);
this.span = span;
this.radiusType = radiusType;
}
public ConeAOE(Coord origin, int radius, double angle, double span, Radius radiusType)
{
fov = new FOV(FOV.RIPPLE_LOOSE);
this.origin = origin;
this.radius = radius;
// this.startAngle = Math.abs((angle - span / 2.0) % 360.0);
// this.endAngle = Math.abs((angle + span / 2.0) % 360.0);
this.angle = angle;
this.span = span;
this.radiusType = radiusType;
}
@Override
public Coord getOrigin() {
return origin;
}
@Override
public void setOrigin(Coord origin) {
this.origin = origin;
}
@Override
public AimLimit getLimitType() {
return reach.limit;
}
@Override
public int getMinRange() {
return reach.minDistance;
}
@Override
public int getMaxRange() {
return reach.maxDistance;
}
@Override
public Radius getMetric() {
return reach.metric;
}
/**
* Gets the same values returned by getLimitType(), getMinRange(), getMaxRange(), and getMetric() bundled into one
* Reach object.
*
* @return a non-null Reach object.
*/
@Override
public Reach getReach() {
return reach;
}
@Override
public void setLimitType(AimLimit limitType) {
reach.limit = limitType;
}
@Override
public void setMinRange(int minRange) {
reach.minDistance = minRange;
}
@Override
public void setMaxRange(int maxRange) {
reach.maxDistance = maxRange;
}
@Override
public void setMetric(Radius metric) {
reach.metric = metric;
}
/**
* Sets the same values as setLimitType(), setMinRange(), setMaxRange(), and setMetric() using one Reach object.
*
* @param reach a non-null Reach object.
*/
@Override
public void setReach(Reach reach) {
if(reach != null)
this.reach = reach;
}
public double getRadius() {
return radius;
}
public void setRadius(double radius) {
this.radius = radius;
}
public double getAngle() {
return angle;
}
public void setAngle(double angle) {
if (reach.limit == null || reach.limit == AimLimit.FREE ||
(reach.limit == AimLimit.EIGHT_WAY && (int) angle % 45 == 0) ||
(reach.limit == AimLimit.DIAGONAL && (int) angle % 90 == 45) ||
(reach.limit == AimLimit.ORTHOGONAL && (int) angle % 90 == 0)) {
this.angle = angle;
// this.startAngle = Math.abs((angle - span / 2.0) % 360.0);
// this.endAngle = Math.abs((angle + span / 2.0) % 360.0);
}
}
public void setEndCenter(Coord endCenter) {
// radius = radiusType.radius(origin.x, origin.y, endCenter.x, endCenter.y);
if (AreaUtils.verifyLimit(reach.limit, origin, endCenter)) {
angle = (Math.toDegrees(Math.atan2(endCenter.y - origin.y, endCenter.x - origin.x)) % 360.0 + 360.0) % 360.0;
// startAngle = Math.abs((angle - span / 2.0) % 360.0);
// endAngle = Math.abs((angle + span / 2.0) % 360.0);
}
}
public double getSpan() {
return span;
}
public void setSpan(double span) {
this.span = span;
// this.startAngle = Math.abs((angle - span / 2.0) % 360.0);
// this.endAngle = Math.abs((angle + span / 2.0) % 360.0);
}
public Radius getRadiusType() {
return radiusType;
}
public void setRadiusType(Radius radiusType) {
this.radiusType = radiusType;
}
@Override
public void shift(Coord aim) {
setEndCenter(aim);
}
@Override
public boolean mayContainTarget(Collection<Coord> targets) {
for (Coord p : targets) {
if (radiusType.radius(origin.x, origin.y, p.x, p.y) <= radius) {
double d = (angle - Math.toDegrees(Math.atan2(p.y - origin.y, p.x - origin.x)) % 360.0 + 360.0) % 360.0;
if(d > 180)
d = 360 - d;
if(d < span / 2.0)
return true;
}
}
return false;
}
@Override
public OrderedMap<Coord, ArrayList<Coord>> idealLocations(Collection<Coord> targets, Collection<Coord> requiredExclusions) {
if(targets == null)
return new OrderedMap<>();
if(requiredExclusions == null) requiredExclusions = new OrderedSet<>();
//requiredExclusions.remove(origin);
int totalTargets = targets.size();
OrderedMap<Coord, ArrayList<Coord>> bestPoints = new OrderedMap<>(totalTargets * 8);
if(totalTargets == 0)
return bestPoints;
Coord[] ts = targets.toArray(new Coord[targets.size()]);
Coord[] exs = requiredExclusions.toArray(new Coord[requiredExclusions.size()]);
Coord t = exs[0];
double[][][] compositeMap = new double[ts.length][dungeon.length][dungeon[0].length];
double tAngle; //, tStartAngle, tEndAngle;
char[][] dungeonCopy = new char[dungeon.length][dungeon[0].length];
for (int i = 0; i < dungeon.length; i++) {
System.arraycopy(dungeon[i], 0, dungeonCopy[i], 0, dungeon[i].length);
}
double[][] tmpfov;
Coord tempPt = Coord.get(0, 0);
for (int i = 0; i < exs.length; i++) {
t = exs[i];
// tRadius = radiusType.radius(origin.x, origin.y, t.x, t.y);
tAngle = (Math.toDegrees(Math.atan2(t.y - origin.y, t.x - origin.x)) % 360.0 + 360.0) % 360.0;
// tStartAngle = Math.abs((tAngle - span / 2.0) % 360.0);
// tEndAngle = Math.abs((tAngle + span / 2.0) % 360.0);
tmpfov = fov.calculateFOV(map, origin.x, origin.y, radius, radiusType, tAngle, span);
for (int x = 0; x < dungeon.length; x++) {
for (int y = 0; y < dungeon[x].length; y++) {
tempPt = Coord.get(x, y);
dungeonCopy[x][y] = (tmpfov[x][y] > 0.0 || !AreaUtils.verifyLimit(reach.limit, origin, tempPt)) ? '!' : dungeonCopy[x][y];
}
}
}
t = ts[0];
DijkstraMap.Measurement dmm = DijkstraMap.Measurement.MANHATTAN;
if(radiusType == Radius.SQUARE || radiusType == Radius.CUBE) dmm = DijkstraMap.Measurement.CHEBYSHEV;
else if(radiusType == Radius.CIRCLE || radiusType == Radius.SPHERE) dmm = DijkstraMap.Measurement.EUCLIDEAN;
for (int i = 0; i < ts.length; ++i) {
DijkstraMap dm = new DijkstraMap(dungeon, dmm);
t = ts[i];
// tRadius = radiusType.radius(origin.x, origin.y, t.x, t.y);
tAngle = (Math.toDegrees(Math.atan2(t.y - origin.y, t.x - origin.x)) % 360.0 + 360.0) % 360.0;
// tStartAngle = Math.abs((tAngle - span / 2.0) % 360.0);
// tEndAngle = Math.abs((tAngle + span / 2.0) % 360.0);
tmpfov = fov.calculateFOV(map, origin.x, origin.y, radius, radiusType, tAngle, span);
for (int x = 0; x < dungeon.length; x++) {
for (int y = 0; y < dungeon[x].length; y++) {
if (tmpfov[x][y] > 0.0)
{
compositeMap[i][x][y] = dm.physicalMap[x][y];
}
else compositeMap[i][x][y] = DijkstraMap.WALL;
}
}
if(compositeMap[i][t.x][t.y] > DijkstraMap.FLOOR)
{
for (int x = 0; x < dungeon.length; x++) {
Arrays.fill(compositeMap[i][x], 99999.0);
}
continue;
}
dm.initialize(compositeMap[i]);
dm.setGoal(t);
dm.scan(null);
for (int x = 0; x < dungeon.length; x++) {
for (int y = 0; y < dungeon[x].length; y++) {
compositeMap[i][x][y] = (dm.gradientMap[x][y] < DijkstraMap.FLOOR && dungeonCopy[x][y] != '!') ? dm.gradientMap[x][y] : 99999.0;
}
}
}
double bestQuality = 99999 * ts.length;
double[][] qualityMap = new double[dungeon.length][dungeon[0].length];
for (int x = 0; x < qualityMap.length; x++) {
for (int y = 0; y < qualityMap[x].length; y++) {
qualityMap[x][y] = 0.0;
long bits = 0;
for (int i = 0; i < ts.length; ++i) {
qualityMap[x][y] += compositeMap[i][x][y];
if(compositeMap[i][x][y] < 99999.0 && i < 63)
bits |= 1 << i;
}
if(qualityMap[x][y] < bestQuality)
{
ArrayList<Coord> ap = new ArrayList<>();
for (int i = 0; i < ts.length && i < 63; ++i) {
if((bits & (1 << i)) != 0)
ap.add(ts[i]);
}
if(ap.size() > 0) {
bestQuality = qualityMap[x][y];
bestPoints.clear();
bestPoints.put(Coord.get(x, y), ap);
}
}
else if(qualityMap[x][y] == bestQuality) {
ArrayList<Coord> ap = new ArrayList<>();
for (int i = 0; i < ts.length && i < 63; ++i) {
if ((bits & (1 << i)) != 0)
ap.add(ts[i]);
}
if (ap.size() > 0) {
bestPoints.put(Coord.get(x, y), ap);
}
}
}
}
return bestPoints;
}
@Override
public OrderedMap<Coord, ArrayList<Coord>> idealLocations(Collection<Coord> priorityTargets, Collection<Coord> lesserTargets, Collection<Coord> requiredExclusions) {
if(priorityTargets == null)
return idealLocations(lesserTargets, requiredExclusions);
if(requiredExclusions == null) requiredExclusions = new OrderedSet<>();
//requiredExclusions.remove(origin);
int totalTargets = priorityTargets.size() + lesserTargets.size();
OrderedMap<Coord, ArrayList<Coord>> bestPoints = new OrderedMap<>(totalTargets * 8);
if(totalTargets == 0)
return bestPoints;
Coord[] pts = priorityTargets.toArray(new Coord[priorityTargets.size()]);
Coord[] lts = lesserTargets.toArray(new Coord[lesserTargets.size()]);
Coord[] exs = requiredExclusions.toArray(new Coord[requiredExclusions.size()]);
Coord t = exs[0];
double[][][] compositeMap = new double[totalTargets][dungeon.length][dungeon[0].length];
double tAngle; //, tStartAngle, tEndAngle;
char[][] dungeonCopy = new char[dungeon.length][dungeon[0].length],
dungeonPriorities = new char[dungeon.length][dungeon[0].length];
for (int i = 0; i < dungeon.length; i++) {
System.arraycopy(dungeon[i], 0, dungeonCopy[i], 0, dungeon[i].length);
Arrays.fill(dungeonPriorities[i], '#');
}
double[][] tmpfov;
Coord tempPt = Coord.get(0, 0);
for (int i = 0; i < exs.length; ++i) {
t = exs[i];
tAngle = (Math.toDegrees(Math.atan2(t.y - origin.y, t.x - origin.x)) % 360.0 + 360.0) % 360.0;
// tStartAngle = Math.abs((tAngle - span / 2.0) % 360.0);
// tEndAngle = Math.abs((tAngle + span / 2.0) % 360.0);
tmpfov = fov.calculateFOV(map, origin.x, origin.y, radius, radiusType, tAngle, span);
for (int x = 0; x < dungeon.length; x++) {
for (int y = 0; y < dungeon[x].length; y++) {
tempPt = Coord.get(x, y);
dungeonCopy[x][y] = (tmpfov[x][y] > 0.0 || !AreaUtils.verifyLimit(reach.limit, origin, tempPt)) ? '!' : dungeonCopy[x][y];
}
}
}
t = pts[0];
DijkstraMap.Measurement dmm = DijkstraMap.Measurement.MANHATTAN;
if(radiusType == Radius.SQUARE || radiusType == Radius.CUBE) dmm = DijkstraMap.Measurement.CHEBYSHEV;
else if(radiusType == Radius.CIRCLE || radiusType == Radius.SPHERE) dmm = DijkstraMap.Measurement.EUCLIDEAN;
for (int i = 0; i < pts.length; ++i) {
DijkstraMap dm = new DijkstraMap(dungeon, dmm);
t = pts[i];
tAngle = (Math.toDegrees(Math.atan2(t.y - origin.y, t.x - origin.x)) % 360.0 + 360.0) % 360.0;
// tStartAngle = Math.abs((tAngle - span / 2.0) % 360.0);
// tEndAngle = Math.abs((tAngle + span / 2.0) % 360.0);
tmpfov = fov.calculateFOV(map, origin.x, origin.y, radius, radiusType, tAngle, span);
for (int x = 0; x < dungeon.length; x++) {
for (int y = 0; y < dungeon[x].length; y++) {
if (tmpfov[x][y] > 0.0){
compositeMap[i][x][y] = dm.physicalMap[x][y];
dungeonPriorities[x][y] = dungeon[x][y];
}
else compositeMap[i][x][y] = DijkstraMap.WALL;
}
}
if(compositeMap[i][pts[i].x][pts[i].y] > DijkstraMap.FLOOR)
{
for (int x = 0; x < dungeon.length; x++) {
Arrays.fill(compositeMap[i][x], 399999.0);
}
continue;
}
dm.initialize(compositeMap[i]);
dm.setGoal(t);
dm.scan(null);
for (int x = 0; x < dungeon.length; x++) {
for (int y = 0; y < dungeon[x].length; y++) {
compositeMap[i][x][y] = (dm.gradientMap[x][y] < DijkstraMap.FLOOR && dungeonCopy[x][y] != '!') ? dm.gradientMap[x][y] : 399999.0;
}
}
}
t = lts[0];
for (int i = pts.length; i < totalTargets; ++i) {
DijkstraMap dm = new DijkstraMap(dungeon, dmm);
t = lts[i - pts.length];
tAngle = (Math.toDegrees(Math.atan2(t.y - origin.y, t.x - origin.x)) % 360.0 + 360.0) % 360.0;
// tStartAngle = Math.abs((tAngle - span / 2.0) % 360.0);
// tEndAngle = Math.abs((tAngle + span / 2.0) % 360.0);
tmpfov = fov.calculateFOV(map, origin.x, origin.y, radius, radiusType, tAngle, span);
for (int x = 0; x < dungeon.length; x++) {
for (int y = 0; y < dungeon[x].length; y++) {
if (tmpfov[x][y] > 0.0){
compositeMap[i][x][y] = dm.physicalMap[x][y];
}
else compositeMap[i][x][y] = DijkstraMap.WALL;
}
}
if(compositeMap[i][lts[i - pts.length].x][lts[i - pts.length].y] > DijkstraMap.FLOOR)
{
for (int x = 0; x < dungeon.length; x++)
{
Arrays.fill(compositeMap[i][x], 99999.0);
}
continue;
}
dm.initialize(compositeMap[i]);
dm.setGoal(t);
dm.scan(null);
for (int x = 0; x < dungeon.length; x++) {
for (int y = 0; y < dungeon[x].length; y++) {
compositeMap[i][x][y] = (dm.gradientMap[x][y] < DijkstraMap.FLOOR && dungeonCopy[x][y] != '!' && dungeonPriorities[x][y] != '#') ? dm.gradientMap[x][y] : 99999.0;
}
}
}
double bestQuality = 99999 * lts.length + 399999 * pts.length;
double[][] qualityMap = new double[dungeon.length][dungeon[0].length];
for (int x = 0; x < qualityMap.length; x++) {
for (int y = 0; y < qualityMap[x].length; y++) {
qualityMap[x][y] = 0.0;
long pbits = 0, lbits = 0;
for (int i = 0; i < pts.length; ++i) {
qualityMap[x][y] += compositeMap[i][x][y];
if(compositeMap[i][x][y] < 399999.0 && i < 63)
pbits |= 1 << i;
}
for (int i = pts.length; i < totalTargets; ++i) {
qualityMap[x][y] += compositeMap[i][x][y];
if(compositeMap[i][x][y] < 99999.0 && i < 63)
lbits |= 1 << i;
}
if(qualityMap[x][y] < bestQuality)
{
ArrayList<Coord> ap = new ArrayList<>();
for (int i = 0; i < pts.length && i < 63; ++i) {
if((pbits & (1 << i)) != 0)
ap.add(pts[i]);
}
for (int i = pts.length; i < totalTargets && i < 63; ++i) {
if((lbits & (1 << i)) != 0)
ap.add(lts[i - pts.length]);
}
if(ap.size() > 0) {
bestQuality = qualityMap[x][y];
bestPoints.clear();
bestPoints.put(Coord.get(x, y), ap);
}
}
else if(qualityMap[x][y] == bestQuality) {
ArrayList<Coord> ap = new ArrayList<>();
for (int i = 0; i < pts.length && i < 63; ++i) {
if ((pbits & (1 << i)) != 0) {
ap.add(pts[i]);
ap.add(pts[i]);
ap.add(pts[i]);
ap.add(pts[i]);
}
}
for (int i = pts.length; i < totalTargets && i < 63; ++i) {
if ((lbits & (1 << i)) != 0)
ap.add(lts[i - pts.length]);
}
if (ap.size() > 0) {
bestPoints.put(Coord.get(x, y), ap);
}
}
}
}
return bestPoints;
}
/*
@Override
public ArrayList<ArrayList<Coord>> idealLocations(Set<Coord> targets, Set<Coord> requiredExclusions) {
int totalTargets = targets.size() + 1;
int maxEffect = (int)(radiusType.volume2D(radius) * Math.max(5, span) / 360.0);
double allowed = Math.toRadians(span / 2.0);
ArrayList<ArrayList<Coord>> locs = new ArrayList<ArrayList<Coord>>(totalTargets);
for(int i = 0; i < totalTargets; i++)
{
locs.add(new ArrayList<Coord>(maxEffect));
}
if(totalTargets == 1)
return locs;
int ctr = 0;
if(radius < 1)
{
locs.get(totalTargets - 2).addAll(targets);
return locs;
}
double tmpAngle, ang;
boolean[][] tested = new boolean[dungeon.length][dungeon[0].length];
for (int x = 1; x < dungeon.length - 1; x += radius) {
BY_POINT:
for (int y = 1; y < dungeon[x].length - 1; y += radius) {
ang = Math.atan2(y - origin.y, x - origin.x); // between -pi and pi
for(Coord ex : requiredExclusions) {
if (radiusType.radius(x, y, ex.x, ex.y) <= radius) {
tmpAngle = Math.abs(ang - Math.atan2(ex.y - origin.y, ex.x - origin.x));
if(tmpAngle > Math.PI) tmpAngle = PI2 - tmpAngle;
if(tmpAngle < allowed)
continue BY_POINT;
}
}
ctr = 0;
for(Coord tgt : targets) {
if (radiusType.radius(x, y, tgt.x, tgt.y) <= radius) {
tmpAngle = Math.abs(ang - Math.atan2(tgt.y - origin.y, tgt.x - origin.x));
if(tmpAngle > Math.PI) tmpAngle = PI2 - tmpAngle;
if(tmpAngle < allowed)
ctr++;
}
}
if(ctr > 0)
locs.get(totalTargets - ctr).add(Coord.get(x, y));
}
}
Coord it;
for(int t = 0; t < totalTargets - 1; t++)
{
if(locs.get(t).size() > 0) {
int numPoints = locs.get(t).size();
for (int i = 0; i < numPoints; i++) {
it = locs.get(t).get(i);
for (int x = Math.max(1, it.x - (int)(radius) / 2); x < it.x + (radius + 1) / 2 && x < dungeon.length - 1; x++) {
BY_POINT:
for (int y = Math.max(1, it.y - (int)(radius) / 2); y <= it.y + (radius - 1) / 2 && y < dungeon[0].length - 1; y++)
{
if(tested[x][y])
continue;
tested[x][y] = true;
ang = Math.atan2(y - origin.y, x - origin.x); // between -pi and pi
for(Coord ex : requiredExclusions) {
if (radiusType.radius(x, y, ex.x, ex.y) <= radius) {
tmpAngle = Math.abs(ang - Math.atan2(ex.y - origin.y, ex.x - origin.x));
if(tmpAngle > Math.PI) tmpAngle = PI2 - tmpAngle;
if(tmpAngle < allowed)
continue BY_POINT;
}
}
ctr = 0;
for(Coord tgt : targets) {
if (radiusType.radius(x, y, tgt.x, tgt.y) <= radius) {
tmpAngle = Math.abs(ang - Math.atan2(tgt.y - origin.y, tgt.x - origin.x));
if(tmpAngle > Math.PI) tmpAngle = PI2 - tmpAngle;
if(tmpAngle < allowed)
ctr++;
}
}
if(ctr > 0)
locs.get(totalTargets - ctr).add(Coord.get(x, y));
}
}
}
}
}
return locs;
}
*/
@Override
public void setMap(char[][] map) {
this.map = DungeonUtility.generateResistances(map);
dungeon = map;
}
@Override
public OrderedMap<Coord, Double> findArea() {
OrderedMap<Coord, Double> r = AreaUtils.arrayToHashMap(fov.calculateFOV(map, origin.x, origin.y, radius,
radiusType, angle, span));
r.remove(origin);
return r;
}
/**
* If you use FOVCache to pre-compute FOV maps for a level, you can share the speedup from using the cache with
* some AOE implementations that rely on FOV. Not all implementations need to actually make use of the cache, but
* those that use FOV for calculations should benefit. The cache parameter this receives should have completed its
* calculations, which can be confirmed by calling awaitCache(). Ideally, the FOVCache will have done its initial
* calculations in another thread while the previous level or menu was being displayed, and awaitCache() will only
* be a formality.
*
* @param cache The FOVCache for the current level; can be null to stop using the cache
*/
@GwtIncompatible
@Override
public void setCache(FOVCache cache) {
fov = cache;
}
}