Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding Recursive Shadowcasting which allows for 90/180 degree viewsheds #29

Merged
merged 12 commits into from
Mar 31, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ SOURCES = src/rot.js \
src/fov/fov.js \
src/fov/discrete-shadowcasting.js \
src/fov/precise-shadowcasting.js \
src/fov/recursive-shadowcasting.js \
src/color.js \
src/lighting.js \
src/path/path.js \
Expand Down
75 changes: 70 additions & 5 deletions manual/pages/fov.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,16 @@ <h2>Field of View (FOV) computation</h2>
or <code>false</code> (light does not pass). Output callback is called with these arguments: <code>x</code>, <code>y</code>, <code>r</code> and <code>visibility</code>:
the meaning is that "the place at [x,y] is visible with a distance of r". The last argument specifies the amount of visibility (0..1).</p>

<p>FOV computation is initiated by calling the <code>compute</code> method with the following arguments:</p>
<p>Currently there are three FOV algorithms available: </p>
<ol>
<li><a href="http://www.roguebasin.roguelikedevelopment.org/index.php?title=Precise_Shadowcasting_in_JavaScript">Precise Shadowcasting</a></li>
<li>Discrete Shadowcasting</li>
<li><a href="http://www.roguebasin.com/index.php?title=FOV_using_recursive_shadowcasting">Recursive Shadowcasting</a></li>
</ol>

<h3>Precise and Discrete shadowcasting</h3>

<p>Precise and Discrete shadowcasting FOV computations currently support 360-degree viewsheds and are initiated by calling the <code>compute</code> method with the following arguments:</p>

<ol>
<li><code>x</code></li>
Expand All @@ -21,10 +30,6 @@ <h2>Field of View (FOV) computation</h2>
<li><code>callback</code> &ndash; output callback function</li>
</ol>

<h3>Precise shadowcasting</h3>

<p>For now, there is only one FOV algorithm available: the <a href="http://www.roguebasin.roguelikedevelopment.org/index.php?title=Precise_Shadowcasting_in_JavaScript">Precise Shadowcasting</a>.</p>

<div class="example">
ROT.RNG.setSeed(12345);
ROT.DEFAULT_WIDTH = 80;
Expand Down Expand Up @@ -56,3 +61,63 @@ <h3>Precise shadowcasting</h3>
display.draw(x, y, ch, "#fff", color);
});
</div>

<h3>Recursive shadowcasting</h3>

<p>Recursive shadowcasting FOV computations are initiated by calling one of its three <code>compute</code> methods. It currently supports viewing an entire 360-degree circle (<code>compute</code>), as well as half-circles (<code>compute180</code>) and quadrants (<code>compute90</code>). While the <code>compute</code> method behaves the same as with Precise and Discrete shadowcasting, the two partial views require an additional argument noting the direction to look in:</p>

<ol>
<li><code>x</code></li>
<li><code>y</code></li>
<li><code>r</code> &ndash; maximum visibility radius</li>
<li><code>dir</code> &ndash; direction to face in from ROT.DIRS[8]</li>
<li><code>callback</code> &ndash; output callback function</li>
</ol>

<div class="example">
ROT.RNG.setSeed(12345);
ROT.DEFAULT_WIDTH = 80;
ROT.DEFAULT_HEIGHT = 30;
DIR_NORTH = 0;
DIR_WEST = 6;

var display = new ROT.Display({fontSize:12});
SHOW(display.getContainer());

/* create a map */
var data = {};
new ROT.Map.Uniform().create(function(x, y, type) {
data[x+","+y] = type;
display.DEBUG(x, y, type);
});

/* input callback */
var lightPasses = function(x, y) {
var key = x+","+y;
if (key in data) { return (data[key] == 0); }
return false;
}

var fov = new ROT.FOV.RecursiveShadowcasting(lightPasses);

/* output callback for mob with bad vision */
fov.compute90(50, 22, 10, DIR_WEST, function(x, y, r, visibility) {
var ch = (r ? "1" : "@");
var color = (data[x+","+y] ? "#aa0": "#660");
display.draw(x, y, ch, "#fff", color);
});

/* output callback for second mob with better vision */
fov.compute180(57, 14, 10, DIR_NORTH, function(x, y, r, visibility) {
var ch = (r ? "2" : "@");
var color = (data[x+","+y] ? "#aa0": "#660");
display.draw(x, y, ch, "#fff", color);
});

/* output callback for third mob with supernatural vision */
fov.compute(65, 5, 10, function(x, y, r, visibility) {
var ch = (r ? "3" : "@");
var color = (data[x+","+y] ? "#aa0": "#660");
display.draw(x, y, ch, "#fff", color);
});
</div>
158 changes: 156 additions & 2 deletions rot.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
This is rot.js, the ROguelike Toolkit in JavaScript.
Version 0.5~dev, generated on Fri Mar 28 16:42:28 EDT 2014.
Version 0.5~dev, generated on Sat Mar 29 09:34:56 EDT 2014.
*/
/**
* @namespace Top-level ROT namespace
Expand Down Expand Up @@ -3773,7 +3773,7 @@ ROT.FOV = function(lightPassesCallback, options) {
};

/**
* Compute visibility
* Compute visibility for a 360-degree circle
* @param {int} x
* @param {int} y
* @param {int} R Maximum visibility radius
Expand Down Expand Up @@ -4063,6 +4063,160 @@ ROT.FOV.PreciseShadowcasting.prototype._checkVisibility = function(A1, A2, block

return visibleLength/arcLength;
}
/**
* @class Recursive shadowcasting algorithm
* @augments ROT.FOV
* Currently only supports 4/8 topologies, not hexagonal.
* Based on Peter Harkins' implementation of Björn Bergström's algorithm described here: http://www.roguebasin.com/index.php?title=FOV_using_recursive_shadowcasting
*/
ROT.FOV.RecursiveShadowcasting = function(lightPassesCallback, options) {
ROT.FOV.call(this, lightPassesCallback, options);
}
ROT.FOV.RecursiveShadowcasting.extend(ROT.FOV);

/** Octants used for translating recursive shadowcasting offsets */
ROT.FOV.RecursiveShadowcasting.OCTANTS = [
[-1, 0, 0, 1],
[ 0, -1, 1, 0],
[ 0, -1, -1, 0],
[-1, 0, 0, -1],
[ 1, 0, 0, -1],
[ 0, 1, -1, 0],
[ 0, 1, 1, 0],
[ 1, 0, 0, 1]
];

/**
* Compute visibility for a 360-degree circle
* @param {int} x
* @param {int} y
* @param {int} R Maximum visibility radius
* @param {function} callback
*/
ROT.FOV.RecursiveShadowcasting.prototype.compute = function(x, y, R, callback) {
//You can always see your own tile
callback(x, y, 0, true);
for(var i = 0; i < ROT.FOV.RecursiveShadowcasting.OCTANTS.length; i++) {
this._renderOctant(x, y, ROT.FOV.RecursiveShadowcasting.OCTANTS[i], R, callback);
}
}

/**
* Compute visibility for a 180-degree arc
* @param {int} x
* @param {int} y
* @param {int} R Maximum visibility radius
* @param {int} dir Direction to look in (expressed in a ROT.DIR value);
* @param {function} callback
*/
ROT.FOV.RecursiveShadowcasting.prototype.compute180 = function(x, y, R, dir, callback) {
//You can always see your own tile
callback(x, y, 0, true);
var previousOctant = (dir - 1 + 8) % 8; //Need to retrieve the previous octant to render a full 180 degrees
var nextPreviousOctant = (dir - 2 + 8) % 8; //Need to retrieve the previous two octants to render a full 180 degrees
var nextOctant = (dir+ 1 + 8) % 8; //Need to grab to next octant to render a full 180 degrees
this._renderOctant(x, y, ROT.FOV.RecursiveShadowcasting.OCTANTS[nextPreviousOctant], R, callback);
this._renderOctant(x, y, ROT.FOV.RecursiveShadowcasting.OCTANTS[previousOctant], R, callback);
this._renderOctant(x, y, ROT.FOV.RecursiveShadowcasting.OCTANTS[dir], R, callback);
this._renderOctant(x, y, ROT.FOV.RecursiveShadowcasting.OCTANTS[nextOctant], R, callback);
}

/**
* Compute visibility for a 90-degree arc
* @param {int} x
* @param {int} y
* @param {int} R Maximum visibility radius
* @param {int} dir Direction to look in (expressed in a ROT.DIR value);
* @param {function} callback
*/
ROT.FOV.RecursiveShadowcasting.prototype.compute90 = function(x, y, R, dir, callback) {
//You can always see your own tile
callback(x, y, 0, true);
var previousOctant = (dir - 1 + 8) % 8; //Need to retrieve the previous octant to render a full 90 degrees
this._renderOctant(x, y, ROT.FOV.RecursiveShadowcasting.OCTANTS[dir], R, callback);
this._renderOctant(x, y, ROT.FOV.RecursiveShadowcasting.OCTANTS[previousOctant], R, callback);
}

/**
* Render one octant (45-degree arc) of the viewshed
* @param {int} x
* @param {int} y
* @param {int} octant Octant to be rendered
* @param {int} R Maximum visibility radius
* @param {function} callback
*/
ROT.FOV.RecursiveShadowcasting.prototype._renderOctant = function(x, y, octant, R, callback) {
//Radius incremented by 1 to provide same coverage area as other shadowcasting radiuses
this._castVisibility(x, y, 1, 1.0, 0.0, R + 1, octant[0], octant[1], octant[2], octant[3], callback);
}

/**
* Actually calculates the visibility
* @param {int} startX The starting X coordinate
* @param {int} startY The starting Y coordinate
* @param {int} row The row to render
* @param {float} visSlopeStart The slope to start at
* @param {float} visSlopeEnd The slope to end at
* @param {int} radius The radius to reach out to
* @param {int} xx
* @param {int} xy
* @param {int} yx
* @param {int} yy
* @param {function} callback The callback to use when we hit a block that is visible
*/
ROT.FOV.RecursiveShadowcasting.prototype._castVisibility = function(startX, startY, row, visSlopeStart, visSlopeEnd, radius, xx, xy, yx, yy, callback) {
if(visSlopeStart < visSlopeEnd) { return; }
for(var i = row; i <= radius; i++) {
var dx = -i - 1;
var dy = -i;
var blocked = false;
var newStart = 0;

//'Row' could be column, names here assume octant 0 and would be flipped for half the octants
while(dx <= 0) {
dx += 1;

//Translate from relative coordinates to map coordinates
var mapX = startX + dx * xx + dy * xy;
var mapY = startY + dx * yx + dy * yy;

//Range of the row
var slopeStart = (dx - 0.5) / (dy + 0.5);
var slopeEnd = (dx + 0.5) / (dy - 0.5);

//Ignore if not yet at left edge of Octant
if(slopeEnd > visSlopeStart) { continue; }

//Done if past right edge
if(slopeStart < visSlopeEnd) { break; }

//If it's in range, it's visible
if((dx * dx + dy * dy) < (radius * radius)) {
callback(mapX, mapY, i, true);
}

if(!blocked) {
//If tile is a blocking tile, cast around it
if(!this._lightPasses(mapX, mapY) && i < radius) {
blocked = true;
this._castVisibility(startX, startY, i + 1, visSlopeStart, slopeStart, radius, xx, xy, yx, yy, callback);
newStart = slopeEnd;
}
} else {
//Keep narrowing if scanning across a block
if(!this._lightPasses(mapX, mapY)) {
newStart = slopeEnd;
continue;
}

//Block has ended
blocked = false;
visSlopeStart = newStart;
}
}
if(blocked) { break; }
}
}
/**
* @namespace Color operations
*/
Expand Down
6 changes: 5 additions & 1 deletion rot.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/fov/fov.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ ROT.FOV = function(lightPassesCallback, options) {
};

/**
* Compute visibility
* Compute visibility for a 360-degree circle
* @param {int} x
* @param {int} y
* @param {int} R Maximum visibility radius
Expand Down
Loading