forked from 0ad/0ad
350 lines
10 KiB
JavaScript
350 lines
10 KiB
JavaScript
/*
|
|
* TerrainAnalysis inherits from Map
|
|
*
|
|
* This creates a suitable passability map for pathfinding units and provides the findClosestPassablePoint() function.
|
|
* This is intended to be a base object for the terrain analysis modules to inherit from.
|
|
*/
|
|
|
|
function TerrainAnalysis(gameState){
|
|
var passabilityMap = gameState.getMap();
|
|
|
|
var obstructionMask = gameState.getPassabilityClassMask("pathfinderObstruction");
|
|
obstructionMask |= gameState.getPassabilityClassMask("default");
|
|
|
|
var obstructionTiles = new Uint16Array(passabilityMap.data.length);
|
|
for (var i = 0; i < passabilityMap.data.length; ++i)
|
|
{
|
|
obstructionTiles[i] = (passabilityMap.data[i] & obstructionMask) ? 0 : 65535;
|
|
}
|
|
|
|
this.Map(gameState, obstructionTiles);
|
|
};
|
|
|
|
copyPrototype(TerrainAnalysis, Map);
|
|
|
|
// Returns the (approximately) closest point which is passable by searching in a spiral pattern
|
|
TerrainAnalysis.prototype.findClosestPassablePoint = function(startPoint, quick, limitDistance){
|
|
var w = this.width;
|
|
var p = startPoint;
|
|
var direction = 1;
|
|
|
|
if (p[0] + w*p[1] > 0 && p[0] + w*p[1] < this.length &&
|
|
this.map[p[0] + w*p[1]] != 0){
|
|
if (this.countConnected(p, 10) >= 10){
|
|
return p;
|
|
}
|
|
}
|
|
|
|
var count = 0;
|
|
// search in a spiral pattern.
|
|
for (var i = 1; i < w; i++){
|
|
for (var j = 0; j < 2; j++){
|
|
for (var k = 0; k < i; k++){
|
|
p[j] += direction;
|
|
if (p[0] + w*p[1] > 0 && p[0] + w*p[1] < this.length &&
|
|
this.map[p[0] + w*p[1]] != 0){
|
|
if (quick || this.countConnected(p, 10) >= 10){
|
|
return p;
|
|
}
|
|
}
|
|
if (limitDistance && count > 40){
|
|
return undefined;
|
|
}
|
|
count += 1;
|
|
}
|
|
}
|
|
direction *= -1;
|
|
}
|
|
|
|
return undefined;
|
|
};
|
|
|
|
// Counts how many accessible tiles there are connected to the start Point. If there are >= maxCount then it stops.
|
|
// This is inefficient for large areas so maxCount should be kept small for efficiency.
|
|
TerrainAnalysis.prototype.countConnected = function(startPoint, maxCount, curCount, checked){
|
|
curCount = curCount || 0;
|
|
checked = checked || [];
|
|
|
|
var w = this.width;
|
|
|
|
var positions = [[0,1], [0,-1], [1,0], [-1,0]];
|
|
|
|
curCount += 1; // add 1 for the current point
|
|
checked.push(startPoint);
|
|
if (curCount >= maxCount){
|
|
return curCount;
|
|
}
|
|
|
|
for (var i in positions){
|
|
var p = [startPoint[0] + positions[i][0], startPoint[1] + positions[i][1]];
|
|
if (p[0] + w*p[1] > 0 && p[0] + w*p[1] < this.length &&
|
|
this.map[p[0] + w*p[1]] != 0 && !(p in checked)){
|
|
curCount += this.countConnected(p, maxCount, curCount, checked);
|
|
}
|
|
}
|
|
return curCount;
|
|
};
|
|
|
|
/*
|
|
* PathFinder inherits from TerrainAnalysis
|
|
*
|
|
* Used to create a list of distinct paths between two points.
|
|
*
|
|
* Currently it works with a basic implementation which should be improved.
|
|
*
|
|
* TODO: Make this use territories.
|
|
*/
|
|
|
|
|
|
function PathFinder(gameState){
|
|
this.TerrainAnalysis(gameState);
|
|
}
|
|
|
|
copyPrototype(PathFinder, TerrainAnalysis);
|
|
|
|
/*
|
|
* Returns a list of distinct paths to the destination. Currently paths are distinct if they are more than
|
|
* blockRadius apart at a distance of blockPlacementRadius from the destination. Where blockRadius and
|
|
* blockPlacementRadius are defined in walkGradient
|
|
*/
|
|
PathFinder.prototype.getPaths = function(start, end, mode){
|
|
var s = this.findClosestPassablePoint(this.gamePosToMapPos(start));
|
|
var e = this.findClosestPassablePoint(this.gamePosToMapPos(end));
|
|
|
|
if (!s || !e){
|
|
return undefined;
|
|
}
|
|
|
|
var paths = [];
|
|
while (true){
|
|
this.makeGradient(s,e);
|
|
var curPath = this.walkGradient(e, mode);
|
|
|
|
if (curPath !== undefined){
|
|
paths.push(curPath);
|
|
}else{
|
|
break;
|
|
}
|
|
|
|
this.wipeGradient();
|
|
}
|
|
|
|
//this.dumpIm("terrainanalysis.png", 511);
|
|
|
|
if (paths.length > 0){
|
|
return paths;
|
|
}else{
|
|
return undefined;
|
|
}
|
|
};
|
|
|
|
// Creates a potential gradient with the start point having the lowest potential
|
|
PathFinder.prototype.makeGradient = function(start, end){
|
|
var w = this.width;
|
|
var map = this.map;
|
|
|
|
// Holds the list of current points to work outwards from
|
|
var stack = [];
|
|
// We store the next level in its own stack
|
|
var newStack = [];
|
|
// Relative positions or new cells from the current one. We alternate between the adjacent 4 and 8 cells
|
|
// so that there is an average 1.5 distance for diagonals which is close to the actual sqrt(2) ~ 1.41
|
|
var positions = [[[0,1], [0,-1], [1,0], [-1,0]],
|
|
[[0,1], [0,-1], [1,0], [-1,0], [1,1], [-1,-1], [1,-1], [-1,1]]];
|
|
|
|
//Set the distance of the start point to be 1 to distinguish it from the impassable areas
|
|
map[start[0] + w*(start[1])] = 1;
|
|
stack.push(start);
|
|
|
|
// while there are new points being added to the stack
|
|
while (stack.length > 0){
|
|
//run through the current stack
|
|
while (stack.length > 0){
|
|
var cur = stack.pop();
|
|
// stop when we reach the end point
|
|
if (cur[0] == end[0] && cur[1] == end[1]){
|
|
return;
|
|
}
|
|
|
|
var dist = map[cur[0] + w*(cur[1])] + 1;
|
|
// Check the positions adjacent to the current cell
|
|
for (var i = 0; i < positions[dist % 2].length; i++){
|
|
var pos = positions[dist % 2][i];
|
|
var cell = cur[0]+pos[0] + w*(cur[1]+pos[1]);
|
|
if (cell >= 0 && cell < this.length && map[cell] > dist){
|
|
map[cell] = dist;
|
|
newStack.push([cur[0]+pos[0], cur[1]+pos[1]]);
|
|
}
|
|
}
|
|
}
|
|
// Replace the old empty stack with the newly filled one.
|
|
stack = newStack;
|
|
newStack = [];
|
|
}
|
|
|
|
};
|
|
|
|
// Clears the map to just have the obstructions marked on it.
|
|
PathFinder.prototype.wipeGradient = function(){
|
|
for (var i = 0; i < this.length; i++){
|
|
if (this.map[i] > 0){
|
|
this.map[i] = 65535;
|
|
}
|
|
}
|
|
};
|
|
|
|
// Returns the path down a gradient from the start to the bottom of the gradient, returns a point for every 20 cells in normal mode
|
|
// in entryPoints mode this returns the point where the path enters the region near the destination, currently defined
|
|
// by blockPlacementRadius. Note doesn't return a path when the destination is within the blockpoint radius.
|
|
PathFinder.prototype.walkGradient = function(start, mode){
|
|
var positions = [[0,1], [0,-1], [1,0], [-1,0], [1,1], [-1,-1], [1,-1], [-1,1]];
|
|
|
|
var path = [[start[0]*this.cellSize, start[1]*this.cellSize]];
|
|
|
|
var blockPoint = undefined;
|
|
var blockPlacementRadius = 45;
|
|
var blockRadius = 23;
|
|
var count = 0;
|
|
|
|
var cur = start;
|
|
var w = this.width;
|
|
var dist = this.map[cur[0] + w*cur[1]];
|
|
var moved = false;
|
|
while (this.map[cur[0] + w*cur[1]] !== 0){
|
|
for (var i = 0; i < positions.length; i++){
|
|
var pos = positions[i];
|
|
var cell = cur[0]+pos[0] + w*(cur[1]+pos[1]);
|
|
if (cell >= 0 && cell < this.length && this.map[cell] > 0 && this.map[cell] < dist){
|
|
dist = this.map[cell];
|
|
cur = [cur[0]+pos[0], cur[1]+pos[1]];
|
|
moved = true;
|
|
count++;
|
|
// Mark the point to put an obstruction at before calculating the next path
|
|
if (count === blockPlacementRadius){
|
|
blockPoint = cur;
|
|
}
|
|
// Add waypoints to the path, fairly well spaced apart.
|
|
if (count % 40 === 0){
|
|
path.unshift([cur[0]*this.cellSize, cur[1]*this.cellSize]);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
if (!moved){
|
|
break;
|
|
}
|
|
moved = false;
|
|
}
|
|
if (blockPoint === undefined){
|
|
return undefined;
|
|
}
|
|
|
|
// Add an obstruction to the map at the blockpoint so the next path will take a different route.
|
|
this.addInfluence(blockPoint[0], blockPoint[1], blockRadius, -1000000, 'constant');
|
|
if (mode === 'entryPoints'){
|
|
// returns the point where the path enters the blockPlacementRadius
|
|
return [blockPoint[0] * this.cellSize, blockPoint[1] * this.cellSize];
|
|
}else{
|
|
// return a path of points 20 squares apart on the route
|
|
return path;
|
|
}
|
|
};
|
|
|
|
// Would be used to calculate the width of a chokepoint
|
|
// NOTE: Doesn't currently work.
|
|
PathFinder.prototype.countAttached = function(pos){
|
|
var positions = [[0,1], [0,-1], [1,0], [-1,0]];
|
|
var w = this.width;
|
|
var val = this.map[pos[0] + w*pos[1]];
|
|
|
|
var stack = [pos];
|
|
var used = {};
|
|
|
|
while (stack.length > 0){
|
|
var cur = stack.pop();
|
|
used[cur[0] + " " + cur[1]] = true;
|
|
for (var i = 0; i < positions.length; i++){
|
|
var p = positions[i];
|
|
var cell = cur[0]+p[0] + w*(cur[1]+p[1]);
|
|
|
|
}
|
|
}
|
|
};
|
|
|
|
/*
|
|
* Accessibility inherits from TerrainAnalysis
|
|
*
|
|
* Determines whether there is a path from one point to another. It is initialised with a single point (p1) and then
|
|
* can efficiently determine if another point is reachable from p1. Initialising the object is costly so it should be
|
|
* cached.
|
|
*/
|
|
|
|
function Accessibility(gameState, location){
|
|
this.TerrainAnalysis(gameState);
|
|
|
|
var start = this.findClosestPassablePoint(this.gamePosToMapPos(location));
|
|
|
|
// Check that the accessible region is a decent size, otherwise obstacles close to the start point can create
|
|
// tiny accessible areas which makes the rest of the map inaceesible.
|
|
var iterations = 0;
|
|
while (this.floodFill(start) < 20 && iterations < 30){
|
|
this.map[start[0] + this.width*(start[1])] = 0;
|
|
start = this.findClosestPassablePoint(this.gamePosToMapPos(location));
|
|
iterations += 1;
|
|
}
|
|
|
|
}
|
|
|
|
copyPrototype(Accessibility, TerrainAnalysis);
|
|
|
|
// Return true if the given point is accessible from the point given when initialising the Accessibility object. #
|
|
// If the given point is impassable the closest passable point is used.
|
|
Accessibility.prototype.isAccessible = function(position){
|
|
var s = this.findClosestPassablePoint(this.gamePosToMapPos(position), true, true);
|
|
if (!s)
|
|
return false;
|
|
|
|
return this.map[s[0] + this.width * s[1]] === 1;
|
|
};
|
|
|
|
// fill all of the accessible areas with value 1
|
|
Accessibility.prototype.floodFill = function(start){
|
|
var w = this.width;
|
|
var map = this.map;
|
|
|
|
// Holds the list of current points to work outwards from
|
|
var stack = [];
|
|
// We store new points to be added to the stack temporarily in here while we run through the current stack
|
|
var newStack = [];
|
|
// Relative positions or new cells from the current one.
|
|
var positions = [[0,1], [0,-1], [1,0], [-1,0]];
|
|
|
|
// Set the start point to be accessible
|
|
map[start[0] + w*(start[1])] = 1;
|
|
stack.push(start);
|
|
|
|
var count = 0;
|
|
|
|
// while there are new points being added to the stack
|
|
while (stack.length > 0){
|
|
//run through the current stack
|
|
while (stack.length > 0){
|
|
var cur = stack.pop();
|
|
|
|
// Check the positions adjacent to the current cell
|
|
for (var i = 0; i < positions.length; i++){
|
|
var pos = positions[i];
|
|
var cell = cur[0]+pos[0] + w*(cur[1]+pos[1]);
|
|
if (cell >= 0 && cell < this.length && map[cell] > 1){
|
|
map[cell] = 1;
|
|
newStack.push([cur[0]+pos[0], cur[1]+pos[1]]);
|
|
count += 1;
|
|
}
|
|
}
|
|
}
|
|
// Replace the old empty stack with the newly filled one.
|
|
stack = newStack;
|
|
newStack = [];
|
|
}
|
|
return count;
|
|
}; |