/*******************************************************************************
* Copyright (c) 2013 Philip Collin.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*
* Contributors:
* Philip Collin - initial API and implementation
******************************************************************************/
package Roguelike.Pathfinding;
import java.util.ArrayDeque;
import java.util.HashSet;
import Roguelike.Global;
import Roguelike.Global.Passability;
import Roguelike.Tiles.Point;
import Roguelike.Util.EnumBitflag;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.IntSet;
import com.badlogic.gdx.utils.ObjectSet;
public class ShadowCaster
{
private final EnumBitflag<Passability> ShadowPassability = new EnumBitflag<Passability>( Passability.LIGHT, Passability.ENTITY );
private final IntSet tileLookup = new IntSet();
private final int range;
private final PathfindingTile[][] grid;
private final EnumBitflag<Passability> travelType;
private final Object self;
public boolean allowOutOfBounds = false;
private int startX;
private int startY;
public ShadowCaster( PathfindingTile[][] grid, int range )
{
this.grid = grid;
this.range = range;
this.travelType = ShadowPassability;
this.self = null;
}
public ShadowCaster( PathfindingTile[][] grid, int range, EnumBitflag<Passability> travelType, Object self )
{
this.grid = grid;
this.range = range;
this.travelType = travelType;
this.self = self;
}
// Takes a circle in the form of a center point and radius, and a function
// that can tell whether a given cell is opaque. Calls the setFoV action on
// every cell that is both within the radius and visible from the center.
public void ComputeFOV( int x, int y, Array<Point> output )
{
this.startX = MathUtils.clamp( x, 0, grid.length - 1 );
this.startY = MathUtils.clamp( y, 0, grid[0].length - 1 );
for ( int octant = 0; octant < 8; octant++ )
{
ComputeFieldOfViewInOctantZero( octant, output );
}
}
private void ComputeFieldOfViewInOctantZero( int octant, Array<Point> output )
{
ArrayDeque<Column> queue = new ArrayDeque<Column>();
queue.addFirst( new Column( 0, new int[] { 1, 0 }, new int[] { 1, 1 }, octant ) );
while ( !queue.isEmpty() )
{
Column current = queue.pollLast();
if ( current.getX() > range )
{
continue;
}
ComputeFoVForColumnPortion( current.getX(), current.getTopVector(), current.getBottomVector(), queue, current.getOctant(), output );
}
}
// This method has two main purposes: (1) it marks points inside the
// portion that are within the radius as in the field of view, and
// (2) it computes which portions of the following column are in the
// field of view, and puts them on a work queue for later processing.
private void ComputeFoVForColumnPortion( int x, int[] topVector, int[] bottomVector, ArrayDeque<Column> queue, int octant, Array<Point> output )
{
// Search for transitions from opaque to transparent or
// transparent to opaque and use those to determine what
// portions of the *next* column are visible from the origin.
// Start at the top of the column portion and work down.
int topY;
if ( x == 0 )
{
topY = 0;
}
else
{
int quotient = ( 2 * x + 1 ) * topVector[1] / ( 2 * topVector[0] );
int remainder = ( 2 * x + 1 ) * topVector[1] % ( 2 * topVector[0] );
if ( remainder > topVector[0] )
{
topY = quotient + 1;
}
else
{
topY = quotient;
}
}
// Note that this can find a top cell that is actually entirely blocked
// by the cell below it; consider detecting and eliminating that.
int bottomY;
if ( x == 0 )
{
bottomY = 0;
}
else
{
int quotient = ( 2 * x - 1 ) * bottomVector[1] / ( 2 * bottomVector[0] );
int remainder = ( 2 * x - 1 ) * bottomVector[1] % ( 2 * bottomVector[0] );
if ( remainder >= bottomVector[0] )
{
bottomY = quotient + 1;
}
else
{
bottomY = quotient;
}
}
// A more sophisticated algorithm would say that a cell is visible if
// there is *any* straight line segment that passes through *any*
// portion
// of the origin cell and any portion of the target cell, passing
// through
// only transparent cells along the way. This is the "Permissive Field
// Of
// View" algorithm, and it is much harder to implement.
Boolean wasLastCellOpaque = null;
for ( int y = topY; y >= bottomY; y-- )
{
Point temp = Global.PointPool.obtain();
Point translated = TranslateOctant( temp.set( x, y ), octant );
Global.PointPool.free( temp );
boolean inRadius = IsInRadius( translated.x, translated.y );
if ( inRadius )
{
// The current cell is in the field of view.
if ( !allowOutOfBounds && ( translated.x < 0 || translated.y < 0 || translated.x >= grid.length || translated.y >= grid[0].length ) )
{
Global.PointPool.free( translated );
continue;
}
int tileVal = translated.y * grid.length + translated.x;
if ( !tileLookup.contains( tileVal ) )
{
output.add( translated );
tileLookup.add( tileVal );
}
else
{
Global.PointPool.free( translated );
}
}
else
{
Global.PointPool.free( translated );
}
// A cell that was too far away to be seen is effectively
// an opaque cell; nothing "above" it is going to be visible
// in the next column, so we might as well treat it as
// an opaque cell and not scan the cells that are also too
// far away in the next column.
boolean currentIsOpaque = !inRadius || isOpaque( x, y, octant );
if ( wasLastCellOpaque != null )
{
if ( currentIsOpaque )
{
// We've found a boundary from transparent to opaque. Make a
// note of it and revisit it later.
if ( !wasLastCellOpaque.booleanValue() )
{
// The new bottom vector touches the upper left corner
// of
// opaque cell that is below the transparent cell.
queue.addFirst( new Column( x + 1, new int[] { x * 2 - 1, y * 2 + 1 }, topVector, octant ) );
}
}
else if ( wasLastCellOpaque.booleanValue() )
{
// We've found a boundary from opaque to transparent. Adjust
// the top vector so that when we find the next boundary or
// do
// the bottom cell, we have the right top vector.
//
// The new top vector touches the lower right corner of the
// opaque cell that is above the transparent cell, which is
// the upper right corner of the current transparent cell.
topVector = new int[] { x * 2 + 1, y * 2 + 1 };
}
}
wasLastCellOpaque = currentIsOpaque;
}
// Make a note of the lowest opaque-->transparent transition, if there
// is one.
if ( wasLastCellOpaque != null && !wasLastCellOpaque.booleanValue() )
{
queue.addFirst( new Column( x + 1, bottomVector, topVector, octant ) );
}
}
// Is the lower-left corner of cell (x,y) within the radius?
private boolean IsInRadius( int x, int y )
{
return Math.abs( x - startX ) <= range && Math.abs( y - startY ) <= range;
}
// Octant helpers
//
//
// \2|1/
// 3\|/0
// ----+----
// 4/|\7
// /5|6\
//
//
private Point TranslateOctant( Point thepos, int octant )
{
Point pos = thepos.copy();
if ( octant == 1 )
{
int temp = pos.x;
pos.x = pos.y;
pos.y = temp;
}
else if ( octant == 2 )
{
int temp = pos.x;
pos.x = pos.y * -1;
pos.y = temp;
}
else if ( octant == 3 )
{
pos.x = pos.x * -1;
}
else if ( octant == 4 )
{
int temp = pos.x * -1;
pos.x = pos.y * -1;
pos.y = temp;
}
else if ( octant == 5 )
{
pos.y = pos.y * -1;
pos.x = pos.x * -1;
}
else if ( octant == 6 )
{
int temp = pos.y;
pos.y = pos.x * -1;
pos.x = temp;
}
else if ( octant == 7 )
{
pos.y = pos.y * -1;
}
pos.x = ( pos.x ) + startX;
pos.y = ( pos.y ) + startY;
return pos;
}
private boolean isOpaque( int x, int y, int octant )
{
Point temp = Global.PointPool.obtain();
Point pos = TranslateOctant( temp.set( x, y ), octant );
Global.PointPool.free( temp );
// hack to prevent start tile from blocking sight
if ( pos.x == startX && pos.y == startY )
{
Global.PointPool.free( pos );
return false;
}
boolean opaque = false;
if ( allowOutOfBounds && ( pos.x < 0 || pos.y < 0 || pos.x >= grid.length || pos.y >= grid[0].length ) )
{
opaque = false;
}
else
{
opaque = !grid[pos.x][pos.y].getPassable( travelType, self );
}
Global.PointPool.free( pos );
return opaque;
}
}
class Column
{
private int X;
private int[] BottomVector;
private int[] TopVector;
private int octant;
public Column( int x, int[] bottom, int[] top, int octant )
{
this.setOctant( octant );
this.X = x;
this.BottomVector = bottom;
this.TopVector = top;
}
public int getX()
{
return X;
}
public void setX( int X )
{
this.X = X;
}
public int[] getBottomVector()
{
return BottomVector;
}
public void setBottomVector( int[] v )
{
BottomVector = v;
}
public int[] getTopVector()
{
return TopVector;
}
public void setTopVector( int[] v )
{
TopVector = v;
}
public int getOctant()
{
return octant;
}
public void setOctant( int octant )
{
this.octant = octant;
}
}