Anonymous avatar Anonymous committed a915e64

ripped out TCOD FOV stuff, ripped off squidlib's, not adapted yet

Comments (0)

Files changed (28)

src/main/java/net/fishbulb/jcod/demo/Fov.java

 import com.badlogic.gdx.Input;
 import com.badlogic.gdx.graphics.Color;
 import net.fishbulb.jcod.Console;
-import net.fishbulb.jcod.fov.Basic;
-import net.fishbulb.jcod.fov.FovAlgorithm;
-import net.fishbulb.jcod.fov.FovMap;
 import net.fishbulb.jcod.util.BlendMode;
 import net.fishbulb.jcod.util.CharCodes;
 
     int px = 20;
     int py = 10;
 
-    FovMap light;
-    FovAlgorithm algo;
-
     private long lastUpdate;
 
     public Fov(Console parent) {
         super(parent);
-        light = new FovMap(width, height);
-        algo = new Basic(light);
         initFov();
     }
 
     private void initFov() {
-        for (int y = 0; y < height; y++) {
-            for (int x = 0; x < width; x++) {
-                light.setTransparent(x, y, map[y].charAt(x) != '#');
-            }
-        }
     }
 
     @Override public void update() {
         lastUpdate = now;
 
         console.clear();
+        console.setDefaultForeground(Color.WHITE);
+        console.print(1, 0, "WASD: Move around");
+        console.print(1, 1, "L: Light Walls");
+        console.print(1, 2, "T: Torch (not yet)");
+        console.print(1, 3, "X: Algorithm");
+
         console.setDefaultForeground(Color.BLACK);
+
         console.putChar(px, py, '@', BlendMode.None);
-        algo.computeFov(px, py, 0, lightWalls);
+//        algo.computeFov(px, py, 0, lightWalls);
 
         for (int y = 0; y < height; y++) {
             for (int x = 0; x < width; x++) {
                 char c = map[y].charAt(x);
-                boolean lit = light.isFov(x, y);
+//                boolean lit = light.isFov(x, y);
+                boolean lit = false;
                 switch (c) {
                     case '=':
                         console.putChar(x, y, CharCodes.OEM.DHLINE, BlendMode.None);
 
     private void moveTo(int x, int y) {
         if (map[y].charAt(x) == ' ') {
-            console.putChar(px,py,' ',BlendMode.None);
+            console.putChar(px, py, ' ', BlendMode.None);
             px = x;
             py = y;
-            console.putChar(px,py,'@',BlendMode.None);
+            console.putChar(px, py, '@', BlendMode.None);
         }
     }
 
     @Override public boolean keyDown(int keyCode) {
         switch (keyCode) {
             case Input.Keys.W:
-                moveTo(px, py-1);
+                moveTo(px, py - 1);
                 return true;
             case Input.Keys.A:
-                moveTo(px-1, py);
+                moveTo(px - 1, py);
                 return true;
             case Input.Keys.S:
-                moveTo(px, py+1);
+                moveTo(px, py + 1);
                 return true;
             case Input.Keys.D:
-                moveTo(px+1, py);
+                moveTo(px + 1, py);
                 return true;
             case Input.Keys.T:
                 // TODO: torch effect

src/main/java/net/fishbulb/jcod/fov/Basic.java

-package net.fishbulb.jcod.fov;
-
-import static java.lang.Math.max;
-import static java.lang.Math.min;
-
-public class Basic extends FovAlgorithm {
-
-    public Basic(FovMap map) {
-        super(map);
-    }
-
-    @Override
-    public void computeFov(int x, int y, int maxRadius, boolean walls) {
-        int xo, yo;
-        int xmin = 0;
-        int ymin = 0;
-        int xmax = map.getWidth();
-        int ymax = map.getHeight();
-        int r2 = maxRadius * maxRadius;
-        if (maxRadius > 0) {
-            xmin = max(0, x - maxRadius);
-            ymin = max(0, y - maxRadius);
-            xmax = min(map.getWidth(), x + maxRadius + 1);
-            ymax = min(map.getHeight(), y + maxRadius + 1);
-        }
-        map.clearFov();
-
-        xo = xmin; yo = ymin;
-        while (xo < xmax)
-            castRay(x, y, xo++, yo, r2, walls);
-
-        xo = xmax - 1; yo = ymin + 1;
-        while (yo < ymax)
-            castRay(x, y, xo, yo++, r2, walls);
-
-        xo = xmax - 2; yo = ymax - 1;
-        while (xo >= 0)
-            castRay(x, y, xo--, yo, r2, walls);
-
-        xo = xmin; yo = ymax - 2;
-        while (yo > 0)
-            castRay(x, y, xo, yo--, r2, walls);
-
-        if (walls) {
-            // post-processing artefact fix
-            postProcess(xmin, ymin, x, y, -1, -1);
-            postProcess(x, ymin, xmax - 1, y, 1, -1);
-            postProcess(xmin, y, x, ymax - 1, -1, 1);
-            postProcess(x, y, xmax - 1, ymax - 1, 1, 1);
-        }
-    }
-
-    void postProcess(int x0, int y0, int x1, int y1, int dx, int dy) {
-        for (int cx = x0; cx <= x1; cx++) {
-            for (int cy = y0; cy <= y1; cy++) {
-                int x2 = cx + dx;
-                int y2 = cy + dy;
-                if (!(map.contains(cx, cy) && map.isFov(cx, cy) && map.isTransparent(cx, cy)))
-                    continue;
-
-                if (x2 >= x0 && x2 <= x1 && map.contains(x2, cy) && !map.isTransparent(x2, cy))
-                    map.setFov(x2, cy, true);
-
-                if (y2 >= y0 && y2 <= y1 && map.contains(cx, y2) && !map.isTransparent(cx, y2))
-                    map.setFov(cx, y2, true);
-
-                if (x2 >= x0 && x2 <= x1 && y2 >= y0 && y2 <= y1 && map.contains(x2, y2) && !map.isTransparent(x2, y2))
-                    map.setFov(x2, y2, true);
-            }
-        }
-    }
-}

src/main/java/net/fishbulb/jcod/fov/BasicRadiusStrategy.java

+package net.fishbulb.jcod.fov;
+
+/**
+ * Basic radius strategy implementations.
+ *
+ * @author Eben Howard - http://squidpony.com - howard@squidpony.com
+ */
+public enum BasicRadiusStrategy implements RadiusStrategy {
+
+    /**
+     * 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;
+
+    @Override
+    public float radius(int startx, int starty, int endx, int endy) {
+        return radius((float) startx, (float) starty, (float) endx, (float) endy);
+    }
+
+    @Override
+    public float radius(float startx, float starty, float endx, float endy) {
+        float dx = Math.abs(startx - endx);
+        float dy = Math.abs(starty - endy);
+        return radius(dx, dy);
+    }
+
+    @Override
+    public float radius(int dx, int dy) {
+        return radius((float) dx, (float) dy);
+    }
+
+    @Override
+    public float radius(float dx, float dy) {
+        dx = Math.abs(dx);
+        dy = Math.abs(dy);
+        float radius = 0f;
+        switch (this) {
+            case SQUARE:
+                radius = Math.max(dx, dy);//radius is longest axial distance
+                break;
+            case DIAMOND:
+                radius = dx + dy;//radius is the manhattan distance
+                break;
+            case CIRCLE:
+                radius = (float) Math.sqrt(dx * dx + dy * dy);//standard circular radius
+        }
+        return radius;
+    }
+}

src/main/java/net/fishbulb/jcod/fov/BresenhamLOS.java

+package net.fishbulb.jcod.fov;
+
+import net.fishbulb.jcod.util.Bresenham;
+
+import java.awt.Point;
+import java.util.LinkedList;
+import java.util.Queue;
+
+/**
+ * A Bresenham-based line-of-sight algorithm.
+ *
+ * @author Eben Howard - http://squidpony.com - howard@squidpony.com
+ */
+public class BresenhamLOS implements LOSSolver {
+
+    Queue<Point> lastPath = new LinkedList<>();
+
+    @Override
+    public boolean isReachable(float[][] resistanceMap, int startx, int starty, int targetx, int targety, float force, float decay, RadiusStrategy radiusStrategy) {
+        Queue<Point> path = Bresenham.line2D(startx, starty, targetx, targety);
+        lastPath = new LinkedList<>(path);//save path for later retreival
+        float currentForce = force;
+        for (Point p : path) {
+            if (p.x == targetx && p.y == targety) {
+                return true;//reached the end 
+            }
+            if (p.x != startx || p.y != starty) {//don't discount the start location even if on resistant cell
+                currentForce *= (1 - resistanceMap[p.x][p.y]);
+            }
+            double radius = radiusStrategy.radius(startx, starty, p.x, p.y);
+            if (currentForce - (radius * decay) <= 0) {
+                return false;//too much resistance
+            }
+        }
+        return false;//never got to the target point
+    }
+
+    @Override
+    public Queue<Point> getLastPath() {
+        return lastPath;
+    }
+
+    @Override
+    public boolean isReachable(float[][] resistanceMap, int startx, int starty, int targetx, int targety) {
+        return isReachable(resistanceMap, startx, starty, targetx, targety, Float.MAX_VALUE, 0f, BasicRadiusStrategy.CIRCLE);
+    }
+}

src/main/java/net/fishbulb/jcod/fov/Diamond.java

-package net.fishbulb.jcod.fov;
-
-public class Diamond {
-}

src/main/java/net/fishbulb/jcod/fov/Direction.java

+package net.fishbulb.jcod.fov;
+
+/**
+ * Represents the eight grid directions and the deltaX, deltaY values associated
+ * with those directions.
+ *
+ * The grid referenced has x positive to the right and y positive downwards.
+ *
+ * @author Eben Howard - http://squidpony.com - howard@squidpony.com
+ */
+public enum Direction {
+
+    UP(0, -1), DOWN(0, 1), LEFT(-1, 0), RIGHT(1, 0),
+    UP_LEFT(-1, -1), UP_RIGHT(1, -1), DOWN_LEFT(-1, 1), DOWN_RIGHT(1, 1),
+    NONE(0, 0);
+    /**
+     * An array which holds only the four cardinal directions.
+     */
+    public static final Direction[] CARDINALS = {UP, DOWN, LEFT, RIGHT};
+    /**
+     * An array which holds only the four diagonal directions.
+     */
+    public static final Direction[] DIAGONALS = {UP_LEFT, UP_RIGHT, DOWN_LEFT, DOWN_RIGHT};
+    /**
+     * An array which holds all eight OUTWARDS directions.
+     */
+    public static final Direction[] OUTWARDS = {UP, DOWN, LEFT, RIGHT, UP_LEFT, UP_RIGHT, DOWN_LEFT, DOWN_RIGHT};
+    /**
+     * The x coordinate difference for this direction.
+     */
+    public int deltaX;
+    /**
+     * The y coordinate difference for this direction.
+     */
+    public int deltaY;
+
+    /**
+     * Returns the direction that most closely matches the input.
+     *
+     * @param x
+     * @param y
+     * @return
+     */
+    static public Direction getDirection(int x, int y) {
+        if (x < 0) {
+            if (y < 0) {
+                return UP_LEFT;
+            } else if (y == 0) {
+                return LEFT;
+            } else {
+                return DOWN_LEFT;
+            }
+        } else if (x == 0) {
+            if (y < 0) {
+                return UP;
+            } else if (y == 0) {
+                return NONE;
+            } else {
+                return DOWN;
+            }
+        } else {
+            if (y < 0) {
+                return UP_RIGHT;
+            } else if (y == 0) {
+                return RIGHT;
+            } else {
+                return DOWN_RIGHT;
+            }
+        }
+    }
+
+    /**
+     * Returns the Direction one step clockwise including diagonals.
+     *
+     * @param dir
+     * @return
+     */
+    public Direction clockwise() {
+        switch (this) {
+            case UP:
+                return UP_RIGHT;
+            case DOWN:
+                return DOWN_LEFT;
+            case LEFT:
+                return UP_LEFT;
+            case RIGHT:
+                return DOWN_RIGHT;
+            case UP_LEFT:
+                return UP;
+            case UP_RIGHT:
+                return RIGHT;
+            case DOWN_LEFT:
+                return LEFT;
+            case DOWN_RIGHT:
+                return DOWN;
+            case NONE:
+            default:
+                return NONE;
+        }
+    }
+
+    /**
+     * Returns the Direction one step counterclockwise including diagonals.
+     *
+     * @param dir
+     * @return
+     */
+    public Direction counterClockwise() {
+        switch (this) {
+            case UP:
+                return UP_LEFT;
+            case DOWN:
+                return DOWN_RIGHT;
+            case LEFT:
+                return DOWN_LEFT;
+            case RIGHT:
+                return UP_RIGHT;
+            case UP_LEFT:
+                return LEFT;
+            case UP_RIGHT:
+                return UP;
+            case DOWN_LEFT:
+                return DOWN;
+            case DOWN_RIGHT:
+                return RIGHT;
+            case NONE:
+            default:
+                return NONE;
+        }
+    }
+
+    /**
+     * Returns the direction directly opposite of this one.
+     *
+     * @return
+     */
+    public Direction opposite() {
+        switch (this) {
+            case UP:
+                return DOWN;
+            case DOWN:
+                return UP;
+            case LEFT:
+                return RIGHT;
+            case RIGHT:
+                return LEFT;
+            case UP_LEFT:
+                return DOWN_RIGHT;
+            case UP_RIGHT:
+                return DOWN_LEFT;
+            case DOWN_LEFT:
+                return UP_RIGHT;
+            case DOWN_RIGHT:
+                return UP_LEFT;
+            case NONE:
+            default:
+                return NONE;
+        }
+    }
+
+    private Direction(int x, int y) {
+        this.deltaX = x;
+        this.deltaY = y;
+    }
+}

src/main/java/net/fishbulb/jcod/fov/EliasFOV.java

+package net.fishbulb.jcod.fov;
+
+import com.google.common.annotations.Beta;
+import net.fishbulb.jcod.util.Elias;
+
+/**
+ * Uses the Elias line running to raycast.
+ *
+ * Does not currently support translucency.
+ * 
+ * For information on the sideview parameter, see the EliasLOS documentation.
+ *
+ * @author Eben Howard - http://squidpony.com - howard@squidpony.com
+ */
+@Beta
+public class EliasFOV implements FOVSolver {
+
+    private float[][] lightMap, resistanceMap;
+    private float maxRadius, force, decay;
+    private int width, height;
+    private RadiusStrategy rStrat;
+    private float sideview = 0.75f;
+
+    /**
+     * Creates a solver which will use the default sideview on the internal
+     * EliasLOS solver.
+     */
+    public EliasFOV() {
+    }
+
+    /**
+     * Creates a solver which will use the provided sideview value on the
+     * internal EliasLOS solver.
+     *
+     * @param sideview
+     */
+    public EliasFOV(float sideview) {
+        this.sideview = sideview;
+    }
+
+    @Override
+    public float[][] calculateFOV(float[][] resistanceMap, int startx, int starty, float force, float decay, RadiusStrategy radiusStrategy) {
+        this.resistanceMap = resistanceMap;
+        width = resistanceMap.length;
+        height = resistanceMap[0].length;
+        lightMap = new float[width][height];
+        this.force = force;
+        this.decay = decay;
+        rStrat = radiusStrategy;
+
+        maxRadius = force / decay;
+        int left = (int) Math.max(0, startx - maxRadius - 1);
+        int right = (int) Math.min(width - 1, startx + maxRadius + 1);
+        int top = (int) Math.max(0, starty - maxRadius - 1);
+        int bottom = (int) Math.min(height - 1, starty + maxRadius + 1);
+
+
+        //run rays out to edges
+        for (int x = left; x <= right; x++) {
+            runLineGroup(startx, starty, x, top);
+            runLineGroup(startx, starty, x, bottom);
+        }
+        for (int y = top; y <= bottom; y++) {
+            runLineGroup(startx, starty, left, y);
+            runLineGroup(startx, starty, right, y);
+        }
+
+        return lightMap;
+    }
+
+    private void runLineGroup(int startx, int starty, int endx, int endy) {
+        float[][] tempMap = Elias.lightMap(startx, starty, endx, endy);
+        EliasLOS los = new EliasLOS(sideview);
+//        boolean xpositive = endx > startx;
+//        boolean ypositive = endy > starty;
+        for (int x = Math.min(startx, endx); x <= Math.max(startx, endx); x++) {
+            for (int y = Math.min(starty, endy); y <= Math.max(starty, endy); y++) {
+                float radius = rStrat.radius(startx, starty, x, y);
+                if (radius < maxRadius && los.isReachable(resistanceMap, startx, starty, x, y)) {
+                    lightMap[x][y] = Math.max(lightMap[x][y], tempMap[x][y] * (force - radius * decay));
+                }
+            }
+        }
+    }
+
+    @Override
+    public float[][] calculateFOV(float[][] resistanceMap, int startx, int starty, float radius) {
+        return calculateFOV(resistanceMap, startx, starty, 1, 1 / radius, BasicRadiusStrategy.CIRCLE);
+    }
+}

src/main/java/net/fishbulb/jcod/fov/EliasLOS.java

+package net.fishbulb.jcod.fov;
+
+import com.google.common.annotations.Beta;
+import net.fishbulb.jcod.util.Elias;
+
+import java.awt.Point;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Queue;
+
+/**
+ * Uses Wu's Algorithm as modified by Elias to draw the line.
+ *
+ * The side view parameter is how far along the antialiased path to consider
+ * when determining if the far end can be seen, from 0.0 to 1.0 with 1.0
+ * considering the entire line. The closer to 1.0 this value is, the more
+ * complete and symmetrical end-to-end the line is, but the computation cost
+ * also increases.
+ *
+ * If undesired artifacts are seen when running the lines (missing squares
+ * perhaps), try increasing the sideview value.
+ *
+ * @author Eben Howard - http://squidpony.com - howard@squidpony.com
+ */
+@Beta
+public class EliasLOS implements LOSSolver {
+
+    private Queue<Point> lastPath = new LinkedList<>();
+    private float sideview = 0.75f;
+
+    /**
+     * Creates this solver with the default side view parameter.
+     */
+    public EliasLOS() {
+    }
+
+    /**
+     * Creates this solver with the provided value for how far along the
+     * antialiased line the antialiased portion will be considered.
+     *
+     * @param sideview
+     */
+    public EliasLOS(float sideview) {
+        this.sideview = sideview;
+    }
+
+    public float getSideview() {
+        return sideview;
+    }
+
+    public void setSideview(float sideview) {
+        this.sideview = sideview;
+    }
+
+    @Override
+    public boolean isReachable(float[][] resistanceMap, int startx, int starty, int targetx, int targety, float force, float decay, RadiusStrategy radiusStrategy) {
+        List<Point> path = Elias.line(startx, starty, targetx, targety);
+        lastPath = new LinkedList<>(path);//save path for later retreival
+
+        BresenhamLOS los1 = new BresenhamLOS(),
+                los2 = new BresenhamLOS();
+
+        float checkRadius = radiusStrategy.radius(startx, starty) * 0.75f;
+        while (!path.isEmpty()) {
+            Point p = path.remove(0);
+
+            //if a non-solid midpoint on the path can see both the start and end, consider the two ends to be able to see each other
+            if (resistanceMap[p.x][p.y] < 1
+                    && radiusStrategy.radius(startx, starty, p.x, p.y) < checkRadius
+                    && los1.isReachable(resistanceMap, p.x, p.y, targetx, targety, force - (radiusStrategy.radius(startx, starty, p.x, p.y) * decay), decay, radiusStrategy)
+                    && los2.isReachable(resistanceMap, startx, starty, p.x, p.y, force, decay, radiusStrategy)) {
+
+                //record actual sight path used
+                lastPath = new LinkedList<>(los1.lastPath);
+                lastPath.addAll(los2.lastPath);
+                return true;
+            }
+        }
+        return false;//never got to the target point
+    }
+
+    @Override
+    public boolean isReachable(float[][] resistanceMap, int startx, int starty, int targetx, int targety) {
+        return isReachable(resistanceMap, startx, starty, targetx, targety, Float.MAX_VALUE, 0f, BasicRadiusStrategy.CIRCLE);
+    }
+
+    @Override
+    public Queue<Point> getLastPath() {
+        return lastPath;
+    }
+}

src/main/java/net/fishbulb/jcod/fov/FOVSolver.java

+package net.fishbulb.jcod.fov;
+
+/**
+ * An interface for Field of View algorithms.
+ *
+ * FIeld of View (FOV) algorithms determine how much area surrounding a point
+ * can be seen. They return a two dimensional array of floats, representing the
+ * amount of view (typically sight, but perhaps sound, smell, etc.) which the
+ * origin has of each cell.
+ *
+ * The force parameter allows tiles to have a varying resistance to the kind of
+ * rays emanating from the source. This parameter is a percentage of light
+ * passed so 1f or higher will block all light. 0f or lower will block no light.
+ *
+ * If a simple radius is desired, set the resistance of your cells to 0f, the force
+ * to 1f, and the decay to 1f / (the radius).
+ *
+ * The coordinates of the returned structure match those of the input grid.
+ *
+ * Some solvers expect the edges of the map to have opaque cells. Since there
+ * are no bounds checking in the these algorithms, they will fail if the edges
+ * are not opaque. Check the documentation for the individual solvers for more
+ * details.
+ *
+ * @author Eben Howard - http://squidpony.com - howard@squidpony.com
+ */
+public interface FOVSolver {
+
+    /**
+     * Calculates the Field Of View for the provided map from the given x, y
+     * coordinates. Returns a lightmap for a result where the values represent a
+     * percentage of fully lit.
+     *
+     * In general a value equal to or below 0 means that cell is not in the
+     * field of view, whereas a value equal to or above 1 means that cell is
+     * fully in the field of view.
+     *
+     * Note that values below 0 and above 1 may be returned in certain
+     * circumstances. In these cases it is up to the calling class to determine
+     * how to treat such values.
+     *
+     * The starting point for the calculations is considered to be at the center
+     * of the origin cell. Radius determinations are determined by the provided
+     * BasicRadiusStrategy.
+     *
+     * @param resistanceMap the grid of cells to calculate on
+     * @param startx the horizontal component of the starting location
+     * @param starty the vertical component of the starting location
+     * @param force the power of the ray
+     * @param decay how much the light is reduced for each whole integer step in
+     * distance
+     * @param radiusStrategy provides a means to calculate the radius as desired
+     * @return the computed light grid
+     */
+    public float[][] calculateFOV(float[][] resistanceMap, int startx, int starty, float force, float decay, RadiusStrategy radiusStrategy);
+
+    /**
+     * Calculates the Field of View in the same manner as the version with more
+     * parameters.
+     *
+     * Light will extend to the radius value. Uses the implementation's default
+     * radius strategy.
+     *
+     * @param resistanceMap the grid of cells to calculate on
+     * @param startx
+     * @param starty
+     * @param radius
+     * @return
+     */
+    public float[][] calculateFOV(float[][] resistanceMap, int startx, int starty, float radius);
+}

src/main/java/net/fishbulb/jcod/fov/FOVTranslator.java

+package net.fishbulb.jcod.fov;
+
+/**
+ * An Adapter which wraps a FOVSolver and allows the input and output of
+ * multiple types.
+ *
+ * The FOV methods that don't require an explicit resistance map will only
+ * return a valid result if one of the constructors passing one in or one of the
+ * explicit resistance map calculation methods is used.
+ *
+ * Once any of the calculateFOV methods have been called, the results can be
+ * accessed as a variety of types through getter methods.
+ *
+ * When using the integer methods a scaling factor is required. A typical usage
+ * scenario for this would be to simply make it 100, indicating that an opaque
+ * cell will have a resistance of 100. Force and decay are also adjusted by the
+ * scaling factor so it does not need to be pre-multiplied for them. Note that
+ * any light value that when scaled down would be less than 1/scale will be
+ * treated as unlit by the integer returning methods but may still be considered
+ * lit under the boolean and float systems.
+ *
+ * When using the boolean methods, true is equivalent to fully resistant on the
+ * input and fully lit on the output.
+ *
+ * Regarding scale: floating point operations have an implicit scale of 1f and
+ * boolean operations have an implicit infinitely small scale making any value
+ * which indicates some light present be treated as fully lit. Using an integer
+ * scale of 1 is the inverse of using the boolean scale because rounding will
+ * make any value less than fully lit be considered unlit.
+ *
+ * @author Eben Howard - http://squidpony.com - howard@squidpony.com
+ */
+public class FOVTranslator implements FOVSolver {
+
+    private FOVSolver solver;
+    private float[][] lightMap;//backing map for results
+
+    /**
+     * Creates an empty instance. At least one FOVSolver must be added through
+     * an add method before FOV calculations can be performed with this object.
+     */
+    public FOVTranslator(FOVSolver solver) {
+        this.solver = solver;
+    }
+
+    @Override
+    public float[][] calculateFOV(float[][] map, int startx, int starty, float force, float decay, RadiusStrategy radiusStrategy) {
+        lightMap = solver.calculateFOV(map, startx, starty, force, decay, radiusStrategy);
+        return lightMap;
+    }
+
+    @Override
+    public float[][] calculateFOV(float[][] map, int startx, int starty, float radius) {
+        lightMap = solver.calculateFOV(map, startx, starty, 1, 1f / radius, BasicRadiusStrategy.CIRCLE);
+        return lightMap;
+    }
+
+    /**
+     * Calculates the FOV using an integer array.
+     *
+     * @param map
+     * @param startx
+     * @param starty
+     * @param force
+     * @param decay
+     * @param radiusStrategy
+     * @param scale
+     * @return
+     */
+    public int[][] calculateFOV(int[][] map, int startx, int starty, float force, float decay, RadiusStrategy radiusStrategy, float scale) {
+        int width = map.length;
+        int height = map[0].length;
+        float[][] tempMap = new float[width][height];
+        for (int x = 0; x < width; x++) {
+            for (int y = 0; y < height; y++) {
+                tempMap[x][y] = map[x][y] / scale;
+            }
+        }
+        lightMap = solver.calculateFOV(tempMap, startx, starty, force, decay, radiusStrategy);
+        int[][] resultMap = new int[width][height];
+        for (int x = 0; x < width; x++) {
+            for (int y = 0; y < height; y++) {
+                resultMap[x][y] = (int) (lightMap[x][y] * scale);
+            }
+        }
+        return resultMap;
+    }
+
+    /**
+     * Calculates the FOV using an integer array.
+     *
+     * @param map
+     * @param startx
+     * @param starty
+     * @param radius
+     * @param scale
+     * @return
+     */
+    public int[][] calculateFOV(int[][] map, int startx, int starty, float radius, float scale) {
+        int width = map.length;
+        int height = map[0].length;
+        float[][] tempMap = new float[width][height];
+        for (int x = 0; x < width; x++) {
+            for (int y = 0; y < height; y++) {
+                tempMap[x][y] = map[x][y] / scale;
+            }
+        }
+        lightMap = solver.calculateFOV(tempMap, startx, starty, radius);
+        int[][] resultMap = new int[width][height];
+        for (int x = 0; x < width; x++) {
+            for (int y = 0; y < height; y++) {
+                resultMap[x][y] = (int) (lightMap[x][y] / scale);
+            }
+        }
+        return resultMap;
+    }
+
+    /**
+     * Calculates the FOV using a boolean array where true indicates that the
+     * location blocks all light.
+     *
+     * In the returned array, true indicates that the cell is lit.
+     *
+     * @param map
+     * @param startx
+     * @param starty
+     * @param force
+     * @param decay
+     * @param radiusStrategy
+     * @return
+     */
+    public boolean[][] calculateFOV(boolean[][] map, int startx, int starty, float force, float decay, RadiusStrategy radiusStrategy) {
+        int width = map.length;
+        int height = map[0].length;
+        float[][] tempMap = new float[width][height];
+        for (int x = 0; x < width; x++) {
+            for (int y = 0; y < height; y++) {
+                tempMap[x][y] = map[x][y] ? 1f : 0f;
+            }
+        }
+        lightMap = solver.calculateFOV(tempMap, startx, starty, force, decay, radiusStrategy);
+        boolean[][] resultMap = new boolean[width][height];
+        for (int x = 0; x < width; x++) {
+            for (int y = 0; y < height; y++) {
+                resultMap[x][y] = lightMap[x][y] > 0;
+            }
+        }
+        return resultMap;
+    }
+
+    /**
+     * Calculates the FOV using a boolean array where true indicates that the
+     * location blocks all light.
+     *
+     * In the returned array, true indicates that the cell is lit.
+     *
+     * @param map
+     * @param startx
+     * @param starty
+     * @param radius
+     * @return
+     */
+    public boolean[][] calculateFOV(boolean[][] map, int startx, int starty, float radius) {
+        int width = map.length;
+        int height = map[0].length;
+        float[][] tempMap = new float[width][height];
+        for (int x = 0; x < width; x++) {
+            for (int y = 0; y < height; y++) {
+                tempMap[x][y] = map[x][y] ? 1f : 0f;
+            }
+        }
+        lightMap = solver.calculateFOV(tempMap, startx, starty, radius);
+        boolean[][] resultMap = new boolean[width][height];
+        for (int x = 0; x < width; x++) {
+            for (int y = 0; y < height; y++) {
+                resultMap[x][y] = lightMap[x][y] > 0;
+            }
+        }
+        return resultMap;
+    }
+
+    /**
+     * Returns the last calculated light map as a boolean 2d array.
+     *
+     * Lit cells are marked true and unlit marked false;
+     *
+     * @return
+     */
+    public boolean[][] getBooleanArray() {
+        if (lightMap == null) {//make sure there's something to work with
+            return null;
+        }
+
+        int width = lightMap.length;
+        int height = lightMap[0].length;
+        boolean[][] retMap = new boolean[width][height];
+        for (int x = 0; x < width; x++) {
+            for (int y = 0; y < height; y++) {
+                retMap[x][y] = lightMap[x][y] > 0f;
+            }
+        }
+
+        return retMap;
+    }
+
+    /**
+     * Returns an integer array representation of the last calculated FOV light
+     * map.
+     *
+     * @param scale
+     * @return
+     */
+    public int[][] getIntArray(float scale) {
+        if (lightMap == null) {//make sure there's something to work with
+            return null;
+        }
+
+        int width = lightMap.length;
+        int height = lightMap[0].length;
+        int[][] retMap = new int[width][height];
+        for (int x = 0; x < width; x++) {
+            for (int y = 0; y < height; y++) {
+                retMap[x][y] = (int) (lightMap[x][y] * scale);
+            }
+        }
+
+        return retMap;
+    }
+
+    /**
+     * Returns true if the cell at (x, y) is considered lit.
+     *
+     * @param x
+     * @param y
+     * @return
+     */
+    public boolean isLit(int x, int y) {
+        return lightMap[x][y] > 0f;
+    }
+
+    /**
+     * Returns the scaled value at (x, y).
+     *
+     * @param x
+     * @param y
+     * @param scale
+     * @return
+     */
+    public int getInt(int x, int y, int scale) {
+        return (int) (lightMap[x][y] * scale);
+    }
+}

src/main/java/net/fishbulb/jcod/fov/FovAlgorithm.java

-package net.fishbulb.jcod.fov;
-
-import lombok.Getter;
-import lombok.Setter;
-import net.fishbulb.jcod.util.PlotAlgorithms;
-
-import static net.fishbulb.jcod.util.PlotAlgorithms.bresenham;
-
-public abstract class FovAlgorithm {
-
-    @Getter @Setter
-    protected FovMap map;
-
-    protected FovAlgorithm(FovMap map) {
-        this.map = map;
-    }
-
-    class RayCast implements PlotAlgorithms.PlotFunction {
-
-        private final int xo;
-        private final int yo;
-        private final int xd;
-        private final int yd;
-        private final int r2;
-        private final boolean includeWalls;
-
-        private boolean inside = false;
-
-        RayCast(int xo, int yo, int xd, int yd, int r, boolean includeWalls) {
-            this.xo = xo;
-            this.yo = yo;
-            this.xd = xd;
-            this.yd = yd;
-            this.r2 = r * r;
-            this.includeWalls = includeWalls;
-        }
-
-        public void cast() {
-            bresenham(xo, yo, xd, yd, this);
-        }
-
-        @Override public boolean apply(int x, int y) {
-            if (r2 > 0 && ((x - xo) *(x - xo) + (y - yo) * (y - yo)) > r2) return false;
-            if (!map.contains(x,y)) return !inside;
-            inside = true;
-            if (map.isTransparent(x,y)) {
-                map.setFov(x, y, true);
-            } else {
-                map.setFov(x, y, includeWalls);
-                return false;
-            }
-            return true;
-        }
-    }
-
-    public void castRay(int x0, int y0, int x1, int y1, int r, boolean w) {
-        new RayCast(x0, y0, x1, y1, r, w).cast();
-    }
-
-    public abstract void computeFov(int x, int y, int maxRadius, boolean walls);
-
-}

src/main/java/net/fishbulb/jcod/fov/FovMap.java

-package net.fishbulb.jcod.fov;
-
-import lombok.Getter;
-
-public class FovMap {
-    @Getter
-    private int width;
-
-    @Getter
-    private int height;
-
-    class Cell {
-        boolean transparent = true;
-        boolean fov = false;
-
-        Cell(boolean transparent, boolean fov) {
-            this.transparent = transparent;
-            this.fov = fov;
-        }
-
-        Cell(Cell that) {
-            this.transparent = that.transparent;
-            this.fov = that.fov;
-        }
-    }
-
-    private Cell[] cells;
-
-    public FovMap(int width, int height) {
-        this.width = width;
-        this.height = height;
-        cells = new Cell[width * height];
-        for (int i = 0; i < cells.length; i++) {
-            cells[i] = new Cell(false, false);
-        }
-    }
-
-    public FovMap(FovMap that) {
-        width = that.width;
-        height = that.height;
-        cells = new Cell[width * height];
-        for (int i = 0; i < cells.length; i++) {
-            cells[i] = new Cell(that.cells[i]);
-        }
-    }
-
-    public void clear(boolean transparent) {
-        for (Cell c: cells) {
-            c.transparent = transparent;
-            c.fov = false;
-        }
-    }
-
-    public void clearFov() {
-        for (Cell c: cells) c.fov = false;
-    }
-
-    public boolean contains(int x, int y) {
-        return (x >= 0 && y >= 0 && x < width && y < height);
-    }
-
-    Cell cellAt(int x, int y) {
-        return cells[x + y * width];
-    }
-
-    public boolean isTransparent(int x, int y) {
-        return cellAt(x, y).transparent;
-    }
-
-    public void setTransparent(int x, int y, boolean transparent) {
-        cellAt(x,y).transparent = transparent;
-    }
-
-    public boolean isFov(int x, int y) {
-        return cellAt(x, y).fov;
-    }
-
-    public void setFov(int x, int y, boolean fov) {
-        cellAt(x, y).fov = fov;
-    }
-
-
-
-}

src/main/java/net/fishbulb/jcod/fov/LOSSolver.java

+package net.fishbulb.jcod.fov;
+
+import java.awt.Point;
+import java.util.Queue;
+
+/**
+ * An interface for Line of Sight algorithms.
+ *
+ * Line of Site (LOS) algorithms find if there is or is not a path between two
+ * given points.
+ *
+ * If the decay is set to 0 then the line will be run until an obstruction or
+ * target is reached. Otherwise the target is considered not reached if the
+ * calculation of force, decay, and resistances prevent the line from reaching
+ * the target.
+ *
+ * @author Eben Howard - http://squidpony.com - howard@squidpony.com
+ */
+public interface LOSSolver {
+
+    /**
+     * Returns true if a line can be drawn from the start point to the target
+     * point without intervening obstructions.
+     *
+     * @param resistanceMap marks the level of resistance the the line per cell
+     * @param startx
+     * @param starty
+     * @param targetx
+     * @param targety
+     * @param force the amount of impetus to start with
+     * @param decay the amount the force is reduced per unit distance
+     * @param radiusStrategy the strategy to use in computing unit distance
+     * @return
+     */
+    public boolean isReachable(float[][] resistanceMap, int startx, int starty, int targetx, int targety, float force, float decay, RadiusStrategy radiusStrategy);
+
+    /**
+     * Returns true if a line can be drawn from the start point to the target
+     * point without intervening obstructions.
+     *
+     * Does not take into account resistance less than opaque or distance cost.
+     *
+     * Uses the implementation's default RadiusStrategy.
+     *
+     * @param resistanceMap
+     * @param startx
+     * @param starty
+     * @param targetx
+     * @param targety
+     * @return
+     */
+    public boolean isReachable(float[][] resistanceMap, int startx, int starty, int targetx, int targety);
+
+    /**
+     * Returns the path of the last LOS calculation, with the starting point as
+     * the head of the queue.
+     *
+     * @return
+     */
+    public Queue<Point> getLastPath();
+}

src/main/java/net/fishbulb/jcod/fov/MergedFOV.java

+package net.fishbulb.jcod.fov;
+
+
+import com.google.common.annotations.Beta;
+
+/**
+ * This class merges the results from two or more FOVSolvers.
+ * 
+ * Currently a work in progress.
+ *
+ * @author Eben Howard - http://squidpony.com - howard@squidpony.com
+ */
+@Beta
+public class MergedFOV implements FOVSolver {
+    
+    private float totalWeight;
+
+    @Override
+    public float[][] calculateFOV(float[][] resistanceMap, int startx, int starty, float force, float decay, RadiusStrategy radiusStrategy) {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+
+    @Override
+    public float[][] calculateFOV(float[][] resistanceMap, int startx, int starty, float radius) {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+
+    /**
+     * Used when constructing a MergedFOV to indicate what kind of merge should be performed.
+     */
+    public enum MergeType {
+
+        /**
+         * The weight will be used to average out all light maps.
+         */
+        WEIGHTED_AVERAGE,
+        /**
+         * The weight will be used to average out all light maps, but if any
+         * light map shows a cell to have no light then it will be marked as
+         * unlit.
+         */
+        AND,
+        /**
+         * The maximum light from the light maps will be used. The weight is
+         * ignored.
+         */
+        MAXIMUM,
+        /**
+         * The minimum light from the light maps will be used. The weight is
+         * ignored.
+         */
+        MINIMUM;
+        
+        float[][] getLight(float[][][] maps){
+            int width = maps[0].length;
+            int height = maps[0][0].length;
+            float[][] result = new float[width][height];
+            
+            for(int z = 0;z<maps.length;z++){
+                for(int x = 0;x<width;x++){
+                    for(int y = 0;y<height;y++){
+                        switch (this){
+                            case WEIGHTED_AVERAGE:
+                               
+                                break;
+                            case AND:
+                                
+                                break;
+                            case MAXIMUM:
+                                
+                                break;
+                            case MINIMUM: 
+                               
+                                break;
+                        }
+                    }
+                }
+            }
+            
+            return result;
+        }
+    }
+}

src/main/java/net/fishbulb/jcod/fov/Permissive.java

-package net.fishbulb.jcod.fov;
-
-public class Permissive {
-}

src/main/java/net/fishbulb/jcod/fov/RadiusStrategy.java

+package net.fishbulb.jcod.fov;
+
+/**
+ * Indicates which method of dealing with the radius during FOV and LOS solving
+ * is preferred.
+ *
+ * @author Eben Howard - http://squidpony.com - howard@squidpony.com
+ */
+public interface RadiusStrategy {
+
+    /**
+     * Returns the radius between the two points provided.
+     *
+     * @param startx
+     * @param starty
+     * @param endx
+     * @param endy
+     * @return
+     */
+    public float radius(int startx, int starty, int endx, int endy);
+    
+    /**
+     * Returns the radius calculated using the two distances provided.
+     * 
+     * @param dx
+     * @param dy
+     * @return 
+     */
+    public float radius(int dx, int dy);
+
+    /**
+     * Returns the radius between the two points provided.
+     *
+     * @param startx
+     * @param starty
+     * @param endx
+     * @param endy
+     * @return
+     */
+    public float radius(float startx, float starty, float endx, float endy);
+    
+    /**
+     * Returns the radius calculated based on the two distances provided.
+     * 
+     * @param dx
+     * @param dy
+     * @return 
+     */
+    public float radius(float dx, float dy);
+}

src/main/java/net/fishbulb/jcod/fov/RayCastingFOV.java

+package net.fishbulb.jcod.fov;
+
+
+import com.google.common.annotations.Beta;
+
+/**
+ * Simple raytracing algorithm for Field of View. In large areas will be
+ * relatively inefficient due to repeated visiting of some cells.
+ *
+ * Tracing is done from four points near the corners of the cells. The line
+ * between points is then traversed to find what cells are intersected. Once the
+ * traversal hits an opaque cell, runs out of light (based on decay), or reaches
+ * the set radius, that walk is terminated.
+ *
+ *
+ * Light will decay, with solid objects being lit if there is a lit cell next to
+ * them in the direction of the source point. Such objects will be lit according
+ * to the decay so a solid object at the edge of vision will not be lit if a
+ * transparent object in the same cell would not be lit.
+ * 
+ * Currently a work in progress.
+ *
+ * @author Eben Howard - http://squidpony.com - howard@squidpony.com
+ */
+@Beta
+public class RayCastingFOV implements FOVSolver {
+
+    private float gap = 0.4f;//how much gap to leave from the edges when tracing rays
+    private float step = 0.1f;//the size of step to take when walking out rays
+    private float[][] lightMap;
+    private float[][] map;
+    private float decay, force, fx, fy;
+    private int width, height;
+    private RadiusStrategy rStrat;
+
+    /**
+     * Builds a new ray tracing fov solver.
+     *
+     * @param step the length along the ray to traverse in each step
+     * @param gap the offset from the center the lines will be traced
+     */
+    public RayCastingFOV(float step, float gap) {
+        this.step = step;
+        this.gap = gap;
+    }
+
+    /**
+     * Builds a new ray tracing fov solver with the default step size.
+     */
+    public RayCastingFOV() {
+    }
+
+    @Override
+    public float[][] calculateFOV(float[][] resistanceMap, int startx, int starty, float force, float decay, RadiusStrategy radiusStrategy) {
+        this.map = resistanceMap;
+        this.force = force;
+        this.decay = decay;
+        this.fx = startx + 0.5f;
+        this.fy = starty + 0.5f;
+        this.rStrat = radiusStrategy;
+        width = resistanceMap.length;
+        height = resistanceMap[0].length;
+        lightMap = new float[width][height];
+
+        float maxRadius = force / decay + 1;
+
+        int left = (int) Math.max(0, startx - maxRadius);
+        int right = (int) Math.min(width - 1, startx + maxRadius);
+        int top = (int) Math.max(0, starty - maxRadius);
+        int bottom = (int) Math.min(height - 1, starty + maxRadius);
+
+        lightMap[startx][starty] = force;
+
+        //run rays out to edges
+        for (int x = left; x <= right; x++) {
+            runLineGroup(fx, fy, x, top);
+            runLineGroup(fx, fy, x, bottom);
+        }
+        for (int y = top; y <= bottom; y++) {
+            runLineGroup(fx, fy, left, y);
+            runLineGroup(fx, fy, right, y);
+        }
+
+        return lightMap;
+    }
+
+    /**
+     * Runs rays from approximately the corners of the cells. This reduces
+     * artifacts in the results.
+     *
+     * @param startx
+     * @param starty
+     * @param endx
+     * @param endy
+     */
+    private void runLineGroup(float startx, float starty, int endx, int endy) {
+
+        //build up arrays of points to run
+        float[] x1 = {startx, startx - gap, startx + gap},
+                x2 = {endx + 0.5f},
+                y1 = {starty, starty - gap, starty + gap},
+                y2 = {endy + 0.5f};
+
+        for (int i = 0; i < x1.length; i++) {
+            for (int j = 0; j < y1.length; j++) {
+                for (int k = 0; k < x2.length; k++) {
+                    for (int f = 0; f < y2.length; f++) {
+                        double angle = Math.atan2(y2[f] - y1[j], x2[k] - x1[i]);
+                        runLine(x1[i], y1[j], angle, force);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Follows the line and lights each transparent point along the path.
+     *
+     * @param x
+     * @param y
+     * @param angle
+     * @return true if end point reached
+     */
+    private void runLine(float x, float y, double angle, float currentLight) {
+        if (currentLight <= 0) {
+            return;//reached edge of vision
+        }
+
+        int x2 = (int) x;
+        int y2 = (int) y;
+        lightMap[x2][y2] = Math.max(lightMap[x2][y2], currentLight);
+        if (map[x2][y2] < 1f) {
+            float nextX = x + step * (float) Math.cos(angle);
+            float nextY = y + step * (float) Math.sin(angle);
+            float bright = currentLight;//start with current amount of light
+            if (x2 != (int) nextX || y2 != (int) nextY) {//only change light level if actually moving out of the square
+                if (x2 != (int) fx || y2 != (int) fy) {//make sure not on starting point
+                    float distance = rStrat.radius(x2, y2, (int) nextX, (int) nextY);
+                    bright -= decay * distance;
+                    bright = bright * (1 - map[x2][y2]);//decrease it by the resistance of the cell
+                }
+            }
+            runLine(nextX, nextY, angle, bright);
+        }
+    }
+
+    @Override
+    public float[][] calculateFOV(float[][] resistanceMap, int startx, int starty, float radius) {
+        return calculateFOV(resistanceMap, startx, starty, 1, 1 / radius, BasicRadiusStrategy.CIRCLE);
+    }
+}

src/main/java/net/fishbulb/jcod/fov/RayCastingLOS.java

+package net.fishbulb.jcod.fov;
+
+import com.google.common.annotations.Beta;
+
+import java.awt.Point;
+import java.util.LinkedList;
+import java.util.Queue;
+
+/**
+ * Uses a series of rays internal to the start and end point to determine
+ * visibility.
+ *
+ * @author Eben Howard - http://squidpony.com - howard@squidpony.com
+ */
+@Beta
+public class RayCastingLOS implements LOSSolver {
+
+    Queue<Point> path;
+    private float gap = 0.4f;//how much gap to leave from the edges when tracing rays
+    private float step = 0.1f;//the size of step to take when walking out rays
+
+    /**
+     * Creates a new instance of this solver which uses the provided step
+     * distance and gap distance when performing calculations.
+     *
+     * @param step
+     * @param gap
+     */
+    public RayCastingLOS(float step, float gap) {
+        this.step = step;
+        this.gap = gap;
+    }
+
+    /**
+     * Creates an instance of this solver which uses the default gap and step.
+     */
+    public RayCastingLOS() {
+    }
+
+    @Override
+    public boolean isReachable(float[][] resistanceMap, int startx, int starty, int targetx, int targety, float force, float decay, RadiusStrategy radiusStrategy) {
+        path = new LinkedList<>();
+        float maxRadius = force / decay;
+        float x1 = startx + 0.5f;
+        float y1 = starty + 0.5f;
+        float x2 = targetx + 0.5f;
+        float y2 = targety + 0.5f;
+
+
+        double angle = Math.atan2(y2 - y1, x2 - x1);
+        return false;
+    }
+
+    @Override
+    public boolean isReachable(float[][] resistanceMap, int startx, int starty, int targetx, int targety) {
+        return isReachable(resistanceMap, startx, starty, targetx, targety, 1, 0, BasicRadiusStrategy.CIRCLE);
+    }
+
+    @Override
+    public Queue<Point> getLastPath() {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+}

src/main/java/net/fishbulb/jcod/fov/Restrictive.java

-package net.fishbulb.jcod.fov;
-
-public class Restrictive {
-}

src/main/java/net/fishbulb/jcod/fov/RippleFOV.java

+package net.fishbulb.jcod.fov;
+
+
+import com.google.common.annotations.Beta;
+
+/**
+ * Performs FOV by pushing values outwards from the source location. It will
+ * only go around corners slightly.
+ *
+ * This algorithm does perform bounds checking.
+ *
+ * @author Eben Howard - http://squidpony.com - howard@squidpony.com
+ */
+@Beta
+public class RippleFOV implements FOVSolver {
+
+    private float[][] lightMap;
+    private float[][] map;
+    private boolean[][] indirect;//marks when a tile is only indirectly lit
+    private float radius, decay;
+    private int startx, starty, width = 1, height = 1;
+    private RadiusStrategy rStrat;
+
+    public RippleFOV() {
+    }
+
+    /**
+     * Find the light let through by the nearest square.
+     *
+     * @param x
+     * @param y
+     * @return
+     */
+    private float getNearLight(int x, int y) {
+        if (Math.abs(startx - x) <= 1 && Math.abs(starty - y) <= 1) {//if next to start cell, get full light
+            return lightMap[startx][starty];
+        }
+
+        if (indirect[x][y]) {
+            return 0f;//no light if this one was only indirectly lit
+        }
+
+        int x2 = x - (int) Math.signum(x - startx);
+        int y2 = y - (int) Math.signum(y - starty);
+
+        int xDominance = Math.abs(x - startx) - Math.abs(y - starty);
+        float distance = rStrat.radius(x, y, x2, y2);
+        boolean corner = false;
+
+        //clamp x2 and y2 to bound within map
+        x2 = Math.max(0, x2);
+        x2 = Math.min(width - 1, x2);
+        y2 = Math.max(0, y2);
+        y2 = Math.min(height - 1, y2);
+
+        if (map[x2][y2] < 1f && map[x][y2] >= 1f && map[x2][y] >= 1f) {
+            corner = true;
+        }
+
+        boolean mainLit = false, sideALit = false, sideBLit = false;
+
+        //find largest emmitted light in direction of source
+        float light = 0f;
+        if (!indirect[x2][y2] && map[x2][y2] < 1f && lightMap[x2][y2] > 0) {
+            light = Math.max(light, lightMap[x2][y2] * (1 - map[x2][y2]));
+            mainLit = true;
+        }
+
+        //check neighbors
+        if (x2 == x) {//add one left and right
+            int dx1 = Math.max(0, x - 1);
+            int dx2 = Math.min(width - 1, x + 1);
+            int dy = y + (int) Math.signum(y - starty);//move one step further away from the source
+            dy = Math.max(dy, 0);
+            dy = Math.min(dy, height - 1);
+            if (!indirect[dx2][dy] && map[dx2][dy] < 1f && lightMap[dx2][dy] > 0) {
+                light = Math.max(light, lightMap[dx2][dy] * (1 - map[dx2][dy]));
+                sideALit = true;
+            }
+            if (!indirect[dx1][dy] && map[dx1][dy] < 1f && lightMap[dx1][dy] > 0) {
+                light = Math.max(light, lightMap[dx1][dy] * (1 - map[dx1][dy]));
+                sideBLit = true;
+            }
+        } else if (y2 == y) {//add one up and one down
+            int dy1 = Math.max(0, y - 1);
+            int dy2 = Math.min(height - 1, y + 1);
+            int dx = x + (int) Math.signum(x - startx);//move one step further away from the source
+            dx = Math.max(dx, 0);
+            dx = Math.min(dx, width - 1);
+            if (!indirect[dx][dy1] && map[dx][dy1] < 1f && lightMap[dx][dy1] > 0) {
+                light = Math.max(light, lightMap[dx][dy1] * (1 - map[dx][dy1]));
+                sideALit = true;
+            }
+            if (!indirect[dx][dy2] && map[dx][dy2] < 1f && lightMap[dx][dy2] > 0) {
+                light = Math.max(light, lightMap[dx][dy2] * (1 - map[dx][dy2]));
+                sideBLit = true;
+            }
+        } else {
+            if (!indirect[x2][y] && xDominance > 0 && map[x2][y] < 1f && lightMap[x2][y] > 0) {
+                float tempLight = lightMap[x2][y];
+                if (tempLight > 0) {
+                    light = Math.max(light, tempLight * (1 - map[x2][y]));
+                    sideALit = true;
+                }
+            } else if (!indirect[x][y2] && xDominance < 0 && map[x][y2] < 1f && lightMap[x][y2] > 0) {
+                float tempLight = lightMap[x][y2];
+                if (tempLight > 0) {
+                    light = Math.max(light, tempLight * (1 - map[x][y2]));
+                    sideBLit = true;
+                }
+            } else if (!indirect[x2][y2] && xDominance == 0 && (map[x2][y2] < 1f || (map[x][y2] < 1f && map[x2][y] < 1f))) {//on a diagonal 
+                float tempLight = Math.max(lightMap[x2][y2] * (1 - map[x2][y2]),
+                        Math.max(lightMap[x2][y] * (1 - map[x2][y]), lightMap[x][y2] * (1 - map[x][y2])));
+                if (tempLight > 0) {
+                    light = Math.max(light, tempLight);
+                    mainLit = true;//really it might be that both sides are lit, but that counts the same
+                }
+            }
+        }
+
+        //make sure there's at either the most direct tile or both other tiles lit, but allow an exception if closest cell is clear and both neighbors are walls
+        boolean killLight = true;//broken out into steps for debugging
+        if (mainLit || (sideALit && sideBLit) || corner) {
+            killLight = false;
+            if (!mainLit) {
+                indirect[x][y] = true;
+            }
+        }
+        if (killLight) {//not lit at all counts as indirectly lit
+            light = 0;
+        }
+
+        light -= decay * distance;
+        return light;
+    }
+
+    @Override
+    public float[][] calculateFOV(float[][] map, int startx, int starty, float force, float decay, RadiusStrategy rStrat) {
+        this.map = map;
+        this.decay = decay;
+        this.startx = startx;
+        this.starty = starty;
+        this.rStrat = rStrat;
+        radius = force / decay;//assume worst case of no resistance in tiles
+
+        if (map.length != width || map[0].length != height) {
+            width = map.length;
+            height = map[0].length;
+            lightMap = new float[width][height];
+            indirect = new boolean[width][height];
+        } else {
+            for (int x = 0; x < width; x++) {
+                for (int y = 0; y < height; y++) {
+                    lightMap[x][y] = 0f;//mark as unlit
+                    indirect[x][y] = false;
+                }
+            }
+        }
+
+        lightMap[startx][starty] = force;//make the starting space full power
+
+        lightSurroundings(startx, starty);
+
+        return lightMap;
+    }
+
+    private void lightSurroundings(int x, int y) {
+        if (lightMap[x][y] <= 0 || indirect[x][y]) {
+            return;//no light to spread
+        }
+
+        for (int dx = x - 1; dx <= x + 1; dx++) {
+            for (int dy = y - 1; dy <= y + 1; dy++) {
+                //ensure in bounds
+                if (dx < 0 || dx >= width || dy < 0 || dy >= height) {
+                    continue;
+                }
+
+                double r = rStrat.radius(startx, starty, dx, dy);
+                if (r <= radius) {
+                    float surroundingLight = getNearLight(dx, dy);
+                    if (lightMap[dx][dy] < surroundingLight) {
+                        lightMap[dx][dy] = surroundingLight;
+                        lightSurroundings(dx, dy);//redo neighbors since this one's light changed
+                    }
+                }
+            }
+        }
+    }
+
+    @Override
+    public float[][] calculateFOV(float[][] map, int startx, int starty, float radius) {
+        return calculateFOV(map, startx, starty, 1, 1 / radius, BasicRadiusStrategy.CIRCLE);
+    }
+}

src/main/java/net/fishbulb/jcod/fov/Shadow.java

-package net.fishbulb.jcod.fov;
-
-public class Shadow {
-}

src/main/java/net/fishbulb/jcod/fov/ShadowFOV.java

+package net.fishbulb.jcod.fov;
+
+
+/**
+ * Recursive shadowcasting FOV. Uses force * decay for the radius calculation
+ * and treats all translucent cells as fully transparent.
+ *
+ * Performs bounds checking so edges are not required to be opaque.
+ *
+ * @author Eben Howard - http://squidpony.com - howard@squidpony.com
+ */
+public class ShadowFOV implements FOVSolver {
+
+    private int width, height, startx, starty;
+    private float[][] lightMap;
+    private float[][] resistanceMap;
+    private float force, decay, radius;
+    private RadiusStrategy rStrat;
+
+    @Override
+    public float[][] calculateFOV(float[][] resistanceMap, int startx, int starty, float force, float decay, RadiusStrategy rStrat) {
+        this.startx = startx;
+        this.starty = starty;
+        this.force = force;
+        this.decay = decay;
+        this.rStrat = rStrat;
+        this.resistanceMap = resistanceMap;
+
+        width = resistanceMap.length;
+        height = resistanceMap[0].length;
+        lightMap = new float[width][height];
+        radius = (force / decay);
+
+        lightMap[startx][starty] = force;//light the starting cell
+        for (Direction d : Direction.DIAGONALS) {
+            castLight(1, 1.0f, 0.0f, 0, d.deltaX, d.deltaY, 0);
+            castLight(1, 1.0f, 0.0f, d.deltaX, 0, 0, d.deltaY);
+        }
+
+        return lightMap;
+    }
+
+    private void castLight(int row, float start, float end, int xx, int xy, int yx, int yy) {
+
+        float newStart = 0.0f;
+        if (start < end) {
+            return;
+        }
+        boolean blocked = false;
+        for (int distance = row; distance <= radius && !blocked; distance++) {
+            int deltaY = -distance;
+            for (int deltaX = -distance; deltaX <= 0; deltaX++) {
+                int currentX = startx + deltaX * xx + deltaY * xy;
+                int currentY = starty + deltaX * yx + deltaY * yy;
+                float leftSlope = (deltaX - 0.5f) / (deltaY + 0.5f);
+                float rightSlope = (deltaX + 0.5f) / (deltaY - 0.5f);
+
+                if (!(currentX >= 0 && currentY >= 0 && currentX < this.width && currentY < this.height) || start < rightSlope) {
+                    continue;
+                } else if (end > leftSlope) {
+                    break;
+                }
+
+                //check if it's within the lightable area and light if needed
+                if (rStrat.radius(deltaX, deltaY) <= radius) {
+                    float bright = (float) (1 - (decay * rStrat.radius(deltaX, deltaY) / force));
+                    lightMap[currentX][currentY] = bright;
+                }
+
+                if (blocked) { //previous cell was a blocking one
+                    if (resistanceMap[currentX][currentY] >= 1) {//hit a wall
+                        newStart = rightSlope;
+                        continue;
+                    } else {
+                        blocked = false;
+                        start = newStart;
+                    }
+                } else {
+                    if (resistanceMap[currentX][currentY] >= 1 && distance < radius) {//hit a wall within sight line
+                        blocked = true;
+                        castLight(distance + 1, start, leftSlope, xx, xy, yx, yy);
+                        newStart = rightSlope;
+                    }
+                }
+            }
+        }
+    }
+
+    @Override
+    public float[][] calculateFOV(float[][] resistanceMap, int startx, int starty, float radius) {
+        return calculateFOV(resistanceMap, startx, starty, 1, 1 / radius, BasicRadiusStrategy.CIRCLE);
+    }
+}

src/main/java/net/fishbulb/jcod/fov/SpreadFOV.java

+package net.fishbulb.jcod.fov;
+
+
+import com.google.common.annotations.Beta;
+
+/**
+ * Performs FOV by pushing values outwards from the source location. It will
+ * spread around edges like smoke or water. This may not be the desired behavior
+ * for a strict sight area, but may be appropriate for a sound map.
+ *
+ * This algorithm does perform bounds checking.
+ *
+ * @author Eben Howard - http://squidpony.com - howard@squidpony.com
+ */
+@Beta
+public class SpreadFOV implements FOVSolver {
+
+    private float[][] lightMap;
+    private float[][] map;
+    private float radius, decay;
+    private int startx, starty, width, height;
+    private RadiusStrategy rStrat;
+
+    /**
+     * Find the light let through by the nearest square.
+     *
+     * @param x
+     * @param y
+     * @return
+     */
+    private float getNearLight(int x, int y) {
+        int x2 = x - (int) Math.signum(x - startx);
+        int y2 = y - (int) Math.signum(y - starty);
+
+        //clamp x2 and y2 to bound within map
+        x2 = Math.max(0, x2);
+        x2 = Math.min(width - 1, x2);
+        y2 = Math.max(0, y2);
+        y2 = Math.min(height - 1, y2);
+
+        //find largest emmitted light in direction of source
+        float light = Math.max(Math.max(lightMap[x2][y] * (1 - map[x2][y]),
+                lightMap[x][y2] * (1 - map[x][y2])),
+                lightMap[x2][y2] * (1 - map[x2][y2]));
+
+        float distance = rStrat.radius(x, y, x2, y2);
+        light = light - decay * distance;
+        return light;
+    }
+
+    @Override
+    public float[][] calculateFOV(float[][] map, int startx, int starty, float force, float decay, RadiusStrategy rStrat) {
+        this.map = map;
+        this.decay = decay;
+        this.startx = startx;
+        this.starty = starty;
+        this.rStrat = rStrat;
+        radius = force / decay;//assume worst case of no resistance in tiles
+        width = map.length;
+        height = map[0].length;
+        lightMap = new float[width][height];
+
+        lightMap[startx][starty] = force;//make the starting space full power
+
+        lightSurroundings(startx, starty);
+
+        return lightMap;
+    }
+
+    private void lightSurroundings(int x, int y) {
+        if (lightMap[x][y] <= 0) {
+            return;//no light to spread
+        }
+
+        for (int dx = x - 1; dx <= x + 1; dx++) {
+            for (int dy = y - 1; dy <= y + 1; dy++) {
+                //ensure in bounds
+                if (dx < 0 || dx >= width || dy < 0 || dy >= height) {
+                    continue;
+                }
+
+                double r2 = rStrat.radius(startx, starty, dx, dy);
+                if (r2 <= radius) {
+                    float surroundingLight = getNearLight(dx, dy);
+                    if (lightMap[dx][dy] < surroundingLight) {
+                        lightMap[dx][dy] = surroundingLight;
+                        lightSurroundings(dx, dy);//redo neighbors since this one's light changed
+                    }
+                }
+            }
+        }
+    }
+
+    @Override
+    public float[][] calculateFOV(float[][] map, int startx, int starty, float radius) {
+        return calculateFOV(map, startx, starty, 1, 1 / radius, BasicRadiusStrategy.CIRCLE);
+    }
+}

src/main/java/net/fishbulb/jcod/fov/ThreadedFOVSolver.java

+package net.fishbulb.jcod.fov;
+
+
+import com.google.common.annotations.Beta;
+
+/**
+ * A threaded wrapper for an FOVSolver.
+ *
+ * Will calculate in its own thread when standard calculation methods are