rot-js
Version:
A roguelike toolkit in JavaScript
127 lines (126 loc) • 5.04 kB
JavaScript
import FOV from "./fov.js";
/**
* @class Precise shadowcasting algorithm
* @augments ROT.FOV
*/
export default class PreciseShadowcasting extends FOV {
compute(x, y, R, callback) {
/* this place is always visible */
callback(x, y, 0, 1);
/* standing in a dark place. FIXME is this a good idea? */
if (!this._lightPasses(x, y)) {
return;
}
/* list of all shadows */
let SHADOWS = [];
let cx, cy, blocks, A1, A2, visibility;
/* analyze surrounding cells in concentric rings, starting from the center */
for (let r = 1; r <= R; r++) {
let neighbors = this._getCircle(x, y, r);
let neighborCount = neighbors.length;
for (let i = 0; i < neighborCount; i++) {
cx = neighbors[i][0];
cy = neighbors[i][1];
/* shift half-an-angle backwards to maintain consistency of 0-th cells */
A1 = [i ? 2 * i - 1 : 2 * neighborCount - 1, 2 * neighborCount];
A2 = [2 * i + 1, 2 * neighborCount];
blocks = !this._lightPasses(cx, cy);
visibility = this._checkVisibility(A1, A2, blocks, SHADOWS);
if (visibility) {
callback(cx, cy, r, visibility);
}
if (SHADOWS.length == 2 && SHADOWS[0][0] == 0 && SHADOWS[1][0] == SHADOWS[1][1]) {
return;
} /* cutoff? */
} /* for all cells in this ring */
} /* for all rings */
}
/**
* @param {int[2]} A1 arc start
* @param {int[2]} A2 arc end
* @param {bool} blocks Does current arc block visibility?
* @param {int[][]} SHADOWS list of active shadows
*/
_checkVisibility(A1, A2, blocks, SHADOWS) {
if (A1[0] > A2[0]) { /* split into two sub-arcs */
let v1 = this._checkVisibility(A1, [A1[1], A1[1]], blocks, SHADOWS);
let v2 = this._checkVisibility([0, 1], A2, blocks, SHADOWS);
return (v1 + v2) / 2;
}
/* index1: first shadow >= A1 */
let index1 = 0, edge1 = false;
while (index1 < SHADOWS.length) {
let old = SHADOWS[index1];
let diff = old[0] * A1[1] - A1[0] * old[1];
if (diff >= 0) { /* old >= A1 */
if (diff == 0 && !(index1 % 2)) {
edge1 = true;
}
break;
}
index1++;
}
/* index2: last shadow <= A2 */
let index2 = SHADOWS.length, edge2 = false;
while (index2--) {
let old = SHADOWS[index2];
let diff = A2[0] * old[1] - old[0] * A2[1];
if (diff >= 0) { /* old <= A2 */
if (diff == 0 && (index2 % 2)) {
edge2 = true;
}
break;
}
}
let visible = true;
if (index1 == index2 && (edge1 || edge2)) { /* subset of existing shadow, one of the edges match */
visible = false;
}
else if (edge1 && edge2 && index1 + 1 == index2 && (index2 % 2)) { /* completely equivalent with existing shadow */
visible = false;
}
else if (index1 > index2 && (index1 % 2)) { /* subset of existing shadow, not touching */
visible = false;
}
if (!visible) {
return 0;
} /* fast case: not visible */
let visibleLength;
/* compute the length of visible arc, adjust list of shadows (if blocking) */
let remove = index2 - index1 + 1;
if (remove % 2) {
if (index1 % 2) { /* first edge within existing shadow, second outside */
let P = SHADOWS[index1];
visibleLength = (A2[0] * P[1] - P[0] * A2[1]) / (P[1] * A2[1]);
if (blocks) {
SHADOWS.splice(index1, remove, A2);
}
}
else { /* second edge within existing shadow, first outside */
let P = SHADOWS[index2];
visibleLength = (P[0] * A1[1] - A1[0] * P[1]) / (A1[1] * P[1]);
if (blocks) {
SHADOWS.splice(index1, remove, A1);
}
}
}
else {
if (index1 % 2) { /* both edges within existing shadows */
let P1 = SHADOWS[index1];
let P2 = SHADOWS[index2];
visibleLength = (P2[0] * P1[1] - P1[0] * P2[1]) / (P1[1] * P2[1]);
if (blocks) {
SHADOWS.splice(index1, remove);
}
}
else { /* both edges outside existing shadows */
if (blocks) {
SHADOWS.splice(index1, remove, A1, A2);
}
return 1; /* whole arc visible! */
}
}
let arcLength = (A2[0] * A1[1] - A1[0] * A2[1]) / (A1[1] * A2[1]);
return visibleLength / arcLength;
}
}