Use JS classes for Terain-Analysis and InfoMap

`TerrainAnalysis` and `Accessibility` used a costum form of inheritance.

Refs: #6285
This commit is contained in:
phosit
2026-07-14 15:13:32 +02:00
parent ea1026a1cc
commit f9de339f85
3 changed files with 517 additions and 537 deletions
@@ -2,211 +2,213 @@
* The map module.
* Copied with changes from QuantumState's original for qBot, it's a component for storing 8 bit values.
*/
export function InfoMap(sharedScript, type, originalMap, actualCopy)
export class InfoMap
{
// get the correct dimensions according to the map type
const map = type == "territory" || type == "resource" ? sharedScript.territoryMap : sharedScript.passabilityMap;
this.width = map.width;
this.height = map.height;
this.cellSize = map.cellSize;
this.length = this.width * this.height;
this.maxVal = 255;
// sanity check
if (originalMap && originalMap.length != this.length)
warn("AI map size incompatibility with type " + type + ": original " + originalMap.length + " new " + this.length);
if (originalMap && actualCopy)
constructor(sharedScript, type, originalMap, actualCopy)
{
// get the correct dimensions according to the map type
const map = type == "territory" || type == "resource" ? sharedScript.territoryMap : sharedScript.passabilityMap;
this.width = map.width;
this.height = map.height;
this.cellSize = map.cellSize;
this.length = this.width * this.height;
this.maxVal = 255;
// sanity check
if (originalMap && originalMap.length != this.length)
warn("AI map size incompatibility with type " + type + ": original " + originalMap.length + " new " + this.length);
if (originalMap && actualCopy)
{
this.map = new Uint8Array(this.length);
for (let i = 0; i < this.length; ++i)
this.map[i] = originalMap[i];
}
else if (originalMap)
this.map = originalMap;
else
this.map = new Uint8Array(this.length);
}
setMaxVal(val)
{
this.maxVal = val;
}
gamePosToMapPos(p)
{
return [Math.floor(p[0]/this.cellSize), Math.floor(p[1]/this.cellSize)];
}
point(p)
{
const q = this.gamePosToMapPos(p);
q[0] = q[0] >= this.width ? this.width-1 : q[0] < 0 ? 0 : q[0];
q[1] = q[1] >= this.width ? this.width-1 : q[1] < 0 ? 0 : q[1];
return this.map[q[0] + this.width * q[1]];
}
runLoop(x0, x1, y0, y1, cx, cy, maxDist2, func)
{
for (let y = y0; y < y1; ++y)
{
const dy2 = (y - cy) * (y - cy);
const yw = y * this.width;
for (let x = x0; x < x1; ++x)
{
const dx = x - cx;
const r2 = dx * dx + dy2;
if (r2 >= maxDist2)
continue;
const w = x + yw;
this.set(w, func(w, r2));
}
}
}
addInfluence(cx, cy, maxDist, strength, type = "linear")
{
strength = strength ? strength : maxDist;
const x0 = Math.floor(Math.max(0, cx - maxDist));
const y0 = Math.floor(Math.max(0, cy - maxDist));
const x1 = Math.floor(Math.min(this.width-1, cx + maxDist));
const y1 = Math.floor(Math.min(this.height-1, cy + maxDist));
const maxDist2 = maxDist * maxDist;
if (type == "linear")
{
const str = strength / maxDist;
this.runLoop(x0, x1, y0, y1, cx, cy, maxDist2, (w, r2) => this.map[w] + str * (maxDist - Math.sqrt(r2)));
}
else if (type == "quadratic")
{
const str = strength / maxDist2;
this.runLoop(x0, x1, y0, y1, cx, cy, maxDist2, (w, r2) => this.map[w] + str * (maxDist2 - r2));
}
else
this.runLoop(x0, x1, y0, y1, cx, cy, maxDist2, (w, r2) => this.map[w] + strength);
}
multiplyInfluence(cx, cy, maxDist, strength, type = "constant")
{
strength = strength ? +strength : +maxDist;
const x0 = Math.max(0, cx - maxDist);
const y0 = Math.max(0, cy - maxDist);
const x1 = Math.min(this.width, cx + maxDist);
const y1 = Math.min(this.height, cy + maxDist);
const maxDist2 = maxDist * maxDist;
if (type == "linear")
{
const str = strength / maxDist;
this.runLoop(x0, x1, y0, y1, cx, cy, maxDist2, (w, r2) => str * (maxDist - Math.sqrt(r2)) * this.map[w]);
}
else if (type == "quadratic")
{
const str = strength / maxDist2;
this.runLoop(x0, x1, y0, y1, cx, cy, maxDist2, (w, r2) => str * (maxDist2 - r2) * this.map[w]);
}
else
this.runLoop(x0, x1, y0, y1, cx, cy, maxDist2, (w, r2) => this.map[w] * strength);
}
/** add to current map by the parameter map pixelwise */
add(map)
{
this.map = new Uint8Array(this.length);
for (let i = 0; i < this.length; ++i)
this.map[i] = originalMap[i];
this.set(i, this.map[i] + map.map[i]);
}
else if (originalMap)
this.map = originalMap;
else
this.map = new Uint8Array(this.length);
}
InfoMap.prototype.setMaxVal = function(val)
{
this.maxVal = val;
};
InfoMap.prototype.gamePosToMapPos = function(p)
{
return [Math.floor(p[0]/this.cellSize), Math.floor(p[1]/this.cellSize)];
};
InfoMap.prototype.point = function(p)
{
const q = this.gamePosToMapPos(p);
q[0] = q[0] >= this.width ? this.width-1 : q[0] < 0 ? 0 : q[0];
q[1] = q[1] >= this.width ? this.width-1 : q[1] < 0 ? 0 : q[1];
return this.map[q[0] + this.width * q[1]];
};
InfoMap.prototype.runLoop = function(x0, x1, y0, y1, cx, cy, maxDist2, func)
{
for (let y = y0; y < y1; ++y)
/** Set the value taking overflow into account */
set(i, value)
{
const dy2 = (y - cy) * (y - cy);
const yw = y * this.width;
for (let x = x0; x < x1; ++x)
this.map[i] = value < 0 ? 0 : value > this.maxVal ? this.maxVal : value;
}
/** Find the best non-obstructed tile */
findBestTile(radius, obstruction)
{
let bestIdx;
let bestVal = 0;
for (let j = 0; j < this.length; ++j)
{
const dx = x - cx;
const r2 = dx * dx + dy2;
if (r2 >= maxDist2)
if (this.map[j] <= bestVal)
continue;
const w = x + yw;
this.set(w, func(w, r2));
const i = this.getNonObstructedTile(j, radius, obstruction);
if (i < 0)
continue;
bestVal = this.map[j];
bestIdx = i;
}
}
};
InfoMap.prototype.addInfluence = function(cx, cy, maxDist, strength, type = "linear")
{
strength = strength ? strength : maxDist;
const x0 = Math.floor(Math.max(0, cx - maxDist));
const y0 = Math.floor(Math.max(0, cy - maxDist));
const x1 = Math.floor(Math.min(this.width-1, cx + maxDist));
const y1 = Math.floor(Math.min(this.height-1, cy + maxDist));
const maxDist2 = maxDist * maxDist;
if (type == "linear")
{
const str = strength / maxDist;
this.runLoop(x0, x1, y0, y1, cx, cy, maxDist2, (w, r2) => this.map[w] + str * (maxDist - Math.sqrt(r2)));
}
else if (type == "quadratic")
{
const str = strength / maxDist2;
this.runLoop(x0, x1, y0, y1, cx, cy, maxDist2, (w, r2) => this.map[w] + str * (maxDist2 - r2));
}
else
this.runLoop(x0, x1, y0, y1, cx, cy, maxDist2, (w, r2) => this.map[w] + strength);
};
InfoMap.prototype.multiplyInfluence = function(cx, cy, maxDist, strength, type = "constant")
{
strength = strength ? +strength : +maxDist;
const x0 = Math.max(0, cx - maxDist);
const y0 = Math.max(0, cy - maxDist);
const x1 = Math.min(this.width, cx + maxDist);
const y1 = Math.min(this.height, cy + maxDist);
const maxDist2 = maxDist * maxDist;
if (type == "linear")
{
const str = strength / maxDist;
this.runLoop(x0, x1, y0, y1, cx, cy, maxDist2, (w, r2) => str * (maxDist - Math.sqrt(r2)) * this.map[w]);
}
else if (type == "quadratic")
{
const str = strength / maxDist2;
this.runLoop(x0, x1, y0, y1, cx, cy, maxDist2, (w, r2) => str * (maxDist2 - r2) * this.map[w]);
}
else
this.runLoop(x0, x1, y0, y1, cx, cy, maxDist2, (w, r2) => this.map[w] * strength);
};
/** add to current map by the parameter map pixelwise */
InfoMap.prototype.add = function(map)
{
for (let i = 0; i < this.length; ++i)
this.set(i, this.map[i] + map.map[i]);
};
/** Set the value taking overflow into account */
InfoMap.prototype.set = function(i, value)
{
this.map[i] = value < 0 ? 0 : value > this.maxVal ? this.maxVal : value;
};
/** Find the best non-obstructed tile */
InfoMap.prototype.findBestTile = function(radius, obstruction)
{
let bestIdx;
let bestVal = 0;
for (let j = 0; j < this.length; ++j)
{
if (this.map[j] <= bestVal)
continue;
const i = this.getNonObstructedTile(j, radius, obstruction);
if (i < 0)
continue;
bestVal = this.map[j];
bestIdx = i;
return { "idx": bestIdx, "val": bestVal };
}
return { "idx": bestIdx, "val": bestVal };
};
/** return any non obstructed (small) tile inside the (big) tile i from obstruction map */
InfoMap.prototype.getNonObstructedTile = function(i, radius, obstruction)
{
const ratio = this.cellSize / obstruction.cellSize;
const ix = (i % this.width) * ratio;
const iy = Math.floor(i / this.width) * ratio;
const w = obstruction.width;
const r2 = radius * radius;
let lastPoint;
for (let kx = ix; kx < ix + ratio; ++kx)
/** return any non obstructed (small) tile inside the (big) tile i from obstruction map */
getNonObstructedTile(i, radius, obstruction)
{
if (kx < radius || kx >= w - radius)
continue;
for (let ky = iy; ky < iy + ratio; ++ky)
{
if (ky < radius || ky >= w - radius)
continue;
if (lastPoint && (kx - lastPoint.x)*(kx - lastPoint.x) + (ky - lastPoint.y)*(ky - lastPoint.y) < r2)
continue;
lastPoint = obstruction.isObstructedTile(kx, ky, radius);
if (!lastPoint)
return kx + ky*w;
}
}
return -1;
};
/** return true if the area centered on tile kx-ky and with radius is obstructed */
InfoMap.prototype.isObstructedTile = function(kx, ky, radius)
{
const w = this.width;
if (kx < radius || kx >= w - radius || ky < radius || ky >= w - radius || this.map[kx+ky*w] == 0)
return { "x": kx, "y": ky };
if (!this.pattern || this.pattern[0] != radius)
{
this.pattern = [radius];
const ratio = this.cellSize / obstruction.cellSize;
const ix = (i % this.width) * ratio;
const iy = Math.floor(i / this.width) * ratio;
const w = obstruction.width;
const r2 = radius * radius;
for (let i = 1; i <= radius; ++i)
this.pattern.push(Math.floor(Math.sqrt(r2 - (i-0.5)*(i-0.5)) + 0.5));
}
for (let dy = 0; dy <= radius; ++dy)
{
const dxmax = this.pattern[dy];
const xp = kx + (ky + dy)*w;
const xm = kx + (ky - dy)*w;
for (let dx = 0; dx <= dxmax; ++dx)
let lastPoint;
for (let kx = ix; kx < ix + ratio; ++kx)
{
if (this.map[xp + dx] == 0)
return { "x": kx + dx, "y": ky + dy };
if (this.map[xm + dx] == 0)
return { "x": kx + dx, "y": ky - dy };
if (this.map[xp - dx] == 0)
return { "x": kx - dx, "y": ky + dy };
if (this.map[xm - dx] == 0)
return { "x": kx - dx, "y": ky - dy };
if (kx < radius || kx >= w - radius)
continue;
for (let ky = iy; ky < iy + ratio; ++ky)
{
if (ky < radius || ky >= w - radius)
continue;
if (lastPoint && (kx - lastPoint.x)*(kx - lastPoint.x) + (ky - lastPoint.y)*(ky - lastPoint.y) < r2)
continue;
lastPoint = obstruction.isObstructedTile(kx, ky, radius);
if (!lastPoint)
return kx + ky*w;
}
}
return -1;
}
return null;
};
InfoMap.prototype.dumpIm = function(name = "default.png", threshold = this.maxVal)
{
Engine.DumpImage(name, this.map, this.width, this.height, threshold);
};
/** return true if the area centered on tile kx-ky and with radius is obstructed */
isObstructedTile(kx, ky, radius)
{
const w = this.width;
if (kx < radius || kx >= w - radius || ky < radius || ky >= w - radius || this.map[kx+ky*w] == 0)
return { "x": kx, "y": ky };
if (!this.pattern || this.pattern[0] != radius)
{
this.pattern = [radius];
const r2 = radius * radius;
for (let i = 1; i <= radius; ++i)
this.pattern.push(Math.floor(Math.sqrt(r2 - (i-0.5)*(i-0.5)) + 0.5));
}
for (let dy = 0; dy <= radius; ++dy)
{
const dxmax = this.pattern[dy];
const xp = kx + (ky + dy)*w;
const xm = kx + (ky - dy)*w;
for (let dx = 0; dx <= dxmax; ++dx)
{
if (this.map[xp + dx] == 0)
return { "x": kx + dx, "y": ky + dy };
if (this.map[xm + dx] == 0)
return { "x": kx + dx, "y": ky - dy };
if (this.map[xp - dx] == 0)
return { "x": kx - dx, "y": ky + dy };
if (this.map[xm - dx] == 0)
return { "x": kx - dx, "y": ky - dy };
}
}
return null;
}
dumpIm(name = "default.png", threshold = this.maxVal)
{
Engine.DumpImage(name, this.map, this.width, this.height, threshold);
}
}
@@ -113,10 +113,8 @@ SharedScript.prototype.init = function(state, deserialization)
this.entities = new EntityCollection(this, this._entities);
// create the terrain analyzer
this.terrainAnalyzer = new TerrainAnalysis();
this.terrainAnalyzer.init(this, state);
this.accessibility = new Accessibility();
this.accessibility.init(state, this.terrainAnalyzer);
this.terrainAnalyzer = new TerrainAnalysis(this, state);
this.accessibility = new Accessibility(state, this.terrainAnalyzer);
// Resource types: ignore = not used for resource maps
// abundant = abundant resource with small amount each
@@ -379,18 +377,6 @@ SharedScript.prototype.deleteMetadata = function(player, ent, key)
return true;
};
export function copyPrototype(descendant, parent)
{
const sConstructor = parent.toString();
const aMatch = sConstructor.match(/\s*function (.*)\(/);
if (aMatch != null)
descendant.prototype[aMatch[1]] = parent;
for (const p in parent.prototype)
descendant.prototype[p] = parent.prototype[p];
}
/** creates a map of resource density */
SharedScript.prototype.createResourceMaps = function()
{
@@ -1,5 +1,4 @@
import { InfoMap } from "simulation/ai/common-api/map-module.js";
import { copyPrototype } from "simulation/ai/common-api/shared.js";
import * as terrainStates from "simulation/ai/common-api/terrain-states.js";
/**
@@ -12,44 +11,41 @@ import * as terrainStates from "simulation/ai/common-api/terrain-states.js";
* This is intended for use with 8 bit maps for reduced memory usage.
* Upgraded from QuantumState's original TerrainAnalysis for qBot.
*/
export function TerrainAnalysis()
export class TerrainAnalysis extends InfoMap
{
}
copyPrototype(TerrainAnalysis, InfoMap);
TerrainAnalysis.prototype.init = function(sharedScript, rawState)
{
const passabilityMap = rawState.passabilityMap;
this.width = passabilityMap.width;
this.height = passabilityMap.height;
this.cellSize = passabilityMap.cellSize;
const obstructionMaskLand = rawState.passabilityClasses["default-terrain-only"];
const obstructionMaskWater = rawState.passabilityClasses["ship-terrain-only"];
const obstructionTiles = new Uint8Array(passabilityMap.data.length);
for (let i = 0; i < passabilityMap.data.length; ++i)
constructor(sharedScript, rawState)
{
obstructionTiles[i] = (passabilityMap.data[i] & obstructionMaskLand) ?
terrainStates.IMPASSABLE : terrainStates.LAND;
const passabilityMap = rawState.passabilityMap;
if (!(passabilityMap.data[i] & obstructionMaskWater) &&
obstructionTiles[i] === terrainStates.IMPASSABLE)
const obstructionMaskLand = rawState.passabilityClasses["default-terrain-only"];
const obstructionMaskWater = rawState.passabilityClasses["ship-terrain-only"];
const obstructionTiles = new Uint8Array(passabilityMap.data.length);
for (let i = 0; i < passabilityMap.data.length; ++i)
{
obstructionTiles[i] = terrainStates.DEEP_WATER;
}
else if (!(passabilityMap.data[i] & obstructionMaskWater) &&
obstructionTiles[i] === terrainStates.LAND)
{
obstructionTiles[i] = terrainStates.SHALLOW_WATER;
obstructionTiles[i] = (passabilityMap.data[i] & obstructionMaskLand) ?
terrainStates.IMPASSABLE : terrainStates.LAND;
if (!(passabilityMap.data[i] & obstructionMaskWater) &&
obstructionTiles[i] === terrainStates.IMPASSABLE)
{
obstructionTiles[i] = terrainStates.DEEP_WATER;
}
else if (!(passabilityMap.data[i] & obstructionMaskWater) &&
obstructionTiles[i] === terrainStates.LAND)
{
obstructionTiles[i] = terrainStates.SHALLOW_WATER;
}
}
super(rawState, "passability", obstructionTiles);
this.width = passabilityMap.width;
this.height = passabilityMap.height;
this.cellSize = passabilityMap.cellSize;
}
this.InfoMap(rawState, "passability", obstructionTiles);
};
}
/**
* Accessibility inherits from TerrainAnalysis
@@ -59,342 +55,338 @@ TerrainAnalysis.prototype.init = function(sharedScript, rawState)
* for optimizations it's called after the TerrainAnalyser has finished initializing his map
* so this can use the land regions already.
*/
export function Accessibility()
export class Accessibility extends InfoMap
{
}
copyPrototype(Accessibility, TerrainAnalysis);
Accessibility.prototype.init = function(rawState, terrainAnalyser)
{
this.InfoMap(rawState, "passability", terrainAnalyser.map);
this.landPassMap = new Uint16Array(terrainAnalyser.length);
this.navalPassMap = new Uint16Array(terrainAnalyser.length);
this.maxRegions = 65535;
this.regionSize = [];
this.regionType = []; // "inaccessible", "land" or "water";
// ID of the region associated with an array of region IDs.
this.regionLinks = [];
// initialized to 0, it's more optimized to start at 1 (I'm checking that if it's not 0, then it's already aprt of a region, don't touch);
// However I actually store all unpassable as region 1 (because if I don't, on some maps the toal nb of region is over 256, and it crashes as the mapis 8bit.)
// So start at 2.
this.regionID = 2;
for (let i = 0; i < this.landPassMap.length; ++i)
constructor(rawState, terrainAnalyser)
{
if (this.map[i] !== terrainStates.IMPASSABLE)
{ // any non-painted, non-inacessible area.
if (this.landPassMap[i] == 0 && this.floodFill(i, this.regionID, false))
this.regionType[this.regionID++] = "land";
if (this.navalPassMap[i] == 0 && this.floodFill(i, this.regionID, true))
this.regionType[this.regionID++] = "water";
}
else if (this.landPassMap[i] == 0)
{ // any non-painted, inacessible area.
this.floodFill(i, 1, false);
this.floodFill(i, 1, true);
}
}
super(rawState, "passability", terrainAnalyser.map);
this.landPassMap = new Uint16Array(terrainAnalyser.length);
this.navalPassMap = new Uint16Array(terrainAnalyser.length);
this.maxRegions = 65535;
this.regionSize = [];
this.regionType = []; // "inaccessible", "land" or "water";
// ID of the region associated with an array of region IDs.
this.regionLinks = [];
// calculating region links. Regions only touching diagonaly are not linked.
// since we're checking all of them, we'll check from the top left to the bottom right
const w = this.width;
for (let x = 0; x < this.width-1; ++x)
{
for (let y = 0; y < this.height-1; ++y)
// initialized to 0, it's more optimized to start at 1 (I'm checking that if it's not 0, then it's already aprt of a region, don't touch);
// However I actually store all unpassable as region 1 (because if I don't, on some maps the toal nb of region is over 256, and it crashes as the mapis 8bit.)
// So start at 2.
this.regionID = 2;
for (let i = 0; i < this.landPassMap.length; ++i)
{
// checking right.
const thisLID = this.landPassMap[x+y*w];
const thisNID = this.navalPassMap[x+y*w];
const rightLID = this.landPassMap[x+1+y*w];
const rightNID = this.navalPassMap[x+1+y*w];
const bottomLID = this.landPassMap[x+y*w+w];
const bottomNID = this.navalPassMap[x+y*w+w];
if (thisLID > 1)
{
if (rightNID > 1)
if (this.regionLinks[thisLID].indexOf(rightNID) === -1)
this.regionLinks[thisLID].push(rightNID);
if (bottomNID > 1)
if (this.regionLinks[thisLID].indexOf(bottomNID) === -1)
this.regionLinks[thisLID].push(bottomNID);
if (this.map[i] !== terrainStates.IMPASSABLE)
{ // any non-painted, non-inacessible area.
if (this.landPassMap[i] == 0 && this.floodFill(i, this.regionID, false))
this.regionType[this.regionID++] = "land";
if (this.navalPassMap[i] == 0 && this.floodFill(i, this.regionID, true))
this.regionType[this.regionID++] = "water";
}
if (thisNID > 1)
else if (this.landPassMap[i] == 0)
{ // any non-painted, inacessible area.
this.floodFill(i, 1, false);
this.floodFill(i, 1, true);
}
}
// calculating region links. Regions only touching diagonaly are not linked.
// since we're checking all of them, we'll check from the top left to the bottom right
const w = this.width;
for (let x = 0; x < this.width-1; ++x)
{
for (let y = 0; y < this.height-1; ++y)
{
if (rightLID > 1)
if (this.regionLinks[thisNID].indexOf(rightLID) === -1)
this.regionLinks[thisNID].push(rightLID);
if (bottomLID > 1)
if (this.regionLinks[thisNID].indexOf(bottomLID) === -1)
this.regionLinks[thisNID].push(bottomLID);
// checking right.
const thisLID = this.landPassMap[x+y*w];
const thisNID = this.navalPassMap[x+y*w];
const rightLID = this.landPassMap[x+1+y*w];
const rightNID = this.navalPassMap[x+1+y*w];
const bottomLID = this.landPassMap[x+y*w+w];
const bottomNID = this.navalPassMap[x+y*w+w];
if (thisLID > 1)
if (this.regionLinks[thisNID].indexOf(thisLID) === -1)
this.regionLinks[thisNID].push(thisLID);
{
if (rightNID > 1)
if (this.regionLinks[thisLID].indexOf(rightNID) === -1)
this.regionLinks[thisLID].push(rightNID);
if (bottomNID > 1)
if (this.regionLinks[thisLID].indexOf(bottomNID) === -1)
this.regionLinks[thisLID].push(bottomNID);
}
if (thisNID > 1)
{
if (rightLID > 1)
if (this.regionLinks[thisNID].indexOf(rightLID) === -1)
this.regionLinks[thisNID].push(rightLID);
if (bottomLID > 1)
if (this.regionLinks[thisNID].indexOf(bottomLID) === -1)
this.regionLinks[thisNID].push(bottomLID);
if (thisLID > 1)
if (this.regionLinks[thisNID].indexOf(thisLID) === -1)
this.regionLinks[thisNID].push(thisLID);
}
}
}
// Engine.DumpImage("LandPassMap.png", this.landPassMap, this.width, this.height, 255);
// Engine.DumpImage("NavalPassMap.png", this.navalPassMap, this.width, this.height, 255);
}
// Engine.DumpImage("LandPassMap.png", this.landPassMap, this.width, this.height, 255);
// Engine.DumpImage("NavalPassMap.png", this.navalPassMap, this.width, this.height, 255);
};
Accessibility.prototype.getAccessValue = function(position, onWater)
{
const gamePos = this.gamePosToMapPos(position);
if (onWater)
return this.navalPassMap[gamePos[0] + this.width*gamePos[1]];
let ret = this.landPassMap[gamePos[0] + this.width*gamePos[1]];
if (ret === 1)
getAccessValue(position, onWater)
{
// quick spiral search.
const indx = [ [-1, -1], [-1, 0], [-1, 1], [0, 1], [1, 1], [1, 0], [1, -1], [0, -1]];
for (const i of indx)
const gamePos = this.gamePosToMapPos(position);
if (onWater)
return this.navalPassMap[gamePos[0] + this.width*gamePos[1]];
let ret = this.landPassMap[gamePos[0] + this.width*gamePos[1]];
if (ret === 1)
{
const id0 = gamePos[0] + i[0];
const id1 = gamePos[1] + i[1];
if (id0 < 0 || id0 >= this.width || id1 < 0 || id1 >= this.width)
continue;
ret = this.landPassMap[id0 + this.width*id1];
if (ret !== 1)
return ret;
}
}
return ret;
};
Accessibility.prototype.getTrajectTo = function(start, end)
{
const pstart = this.gamePosToMapPos(start);
const istart = pstart[0] + pstart[1]*this.width;
const pend = this.gamePosToMapPos(end);
const iend = pend[0] + pend[1]*this.width;
let onLand = true;
if (this.landPassMap[istart] <= 1 && this.navalPassMap[istart] > 1)
onLand = false;
if (this.landPassMap[istart] <= 1 && this.navalPassMap[istart] <= 1)
return false;
let endRegion = this.landPassMap[iend];
if (endRegion <= 1 && this.navalPassMap[iend] > 1)
endRegion = this.navalPassMap[iend];
else if (endRegion <= 1)
return false;
const startRegion = onLand ? this.landPassMap[istart] : this.navalPassMap[istart];
return this.getTrajectToIndex(startRegion, endRegion);
};
/**
* Return a "path" of accessibility indexes from one point to another, including the start and the end indexes
* this can tell you what sea zone you need to have a dock on, for example.
* assumes a land unit unless start point is over deep water.
*/
Accessibility.prototype.getTrajectToIndex = function(istart, iend)
{
if (istart === iend)
return [istart];
const trajects = new Set();
const explored = new Set();
trajects.add([istart]);
explored.add(istart);
while (trajects.size)
{
for (const traj of trajects)
{
const ilast = traj[traj.length-1];
for (const inew of this.regionLinks[ilast])
// quick spiral search.
const indx = [ [-1, -1], [-1, 0], [-1, 1], [0, 1], [1, 1], [1, 0], [1, -1], [0, -1]];
for (const i of indx)
{
if (inew === iend)
return traj.concat(iend);
if (explored.has(inew))
const id0 = gamePos[0] + i[0];
const id1 = gamePos[1] + i[1];
if (id0 < 0 || id0 >= this.width || id1 < 0 || id1 >= this.width)
continue;
trajects.add(traj.concat(inew));
explored.add(inew);
ret = this.landPassMap[id0 + this.width*id1];
if (ret !== 1)
return ret;
}
trajects.delete(traj);
}
}
return undefined;
};
Accessibility.prototype.getRegionSize = function(position, onWater)
{
const pos = this.gamePosToMapPos(position);
const index = pos[0] + pos[1]*this.width;
const ID = onWater === true ? this.navalPassMap[index] : this.landPassMap[index];
if (this.regionSize[ID] === undefined)
return 0;
return this.regionSize[ID];
};
Accessibility.prototype.getRegionSizei = function(index, onWater)
{
if (this.regionSize[this.landPassMap[index]] === undefined && (!onWater || this.regionSize[this.navalPassMap[index]] === undefined))
return 0;
if (onWater && this.regionSize[this.navalPassMap[index]] > this.regionSize[this.landPassMap[index]])
return this.regionSize[this.navalPassMap[index]];
return this.regionSize[this.landPassMap[index]];
};
/** Implementation of a fast flood fill. Reasonably good performances for JS. */
Accessibility.prototype.floodFill = function(startIndex, value, onWater)
{
if (value > this.maxRegions)
{
error("AI accessibility map: too many regions.");
this.landPassMap[startIndex] = 1;
this.navalPassMap[startIndex] = 1;
return false;
return ret;
}
if (!onWater && this.landPassMap[startIndex] != 0 || onWater && this.navalPassMap[startIndex] != 0)
return false; // already painted.
let floodFor = "land";
if (this.map[startIndex] === terrainStates.IMPASSABLE)
getTrajectTo(start, end)
{
this.landPassMap[startIndex] = 1;
this.navalPassMap[startIndex] = 1;
return false;
const pstart = this.gamePosToMapPos(start);
const istart = pstart[0] + pstart[1]*this.width;
const pend = this.gamePosToMapPos(end);
const iend = pend[0] + pend[1]*this.width;
let onLand = true;
if (this.landPassMap[istart] <= 1 && this.navalPassMap[istart] > 1)
onLand = false;
if (this.landPassMap[istart] <= 1 && this.navalPassMap[istart] <= 1)
return false;
let endRegion = this.landPassMap[iend];
if (endRegion <= 1 && this.navalPassMap[iend] > 1)
endRegion = this.navalPassMap[iend];
else if (endRegion <= 1)
return false;
const startRegion = onLand ? this.landPassMap[istart] : this.navalPassMap[istart];
return this.getTrajectToIndex(startRegion, endRegion);
}
if (onWater === true)
/**
* Return a "path" of accessibility indexes from one point to another, including the start and the end indexes
* this can tell you what sea zone you need to have a dock on, for example.
* assumes a land unit unless start point is over deep water.
*/
getTrajectToIndex(istart, iend)
{
if (this.map[startIndex] !== terrainStates.DEEP_WATER &&
this.map[startIndex] !== terrainStates.SHALLOW_WATER)
if (istart === iend)
return [istart];
const trajects = new Set();
const explored = new Set();
trajects.add([istart]);
explored.add(istart);
while (trajects.size)
{
this.navalPassMap[startIndex] = 1; // impassable for naval
return false; // do nothing
for (const traj of trajects)
{
const ilast = traj[traj.length-1];
for (const inew of this.regionLinks[ilast])
{
if (inew === iend)
return traj.concat(iend);
if (explored.has(inew))
continue;
trajects.add(traj.concat(inew));
explored.add(inew);
}
trajects.delete(traj);
}
}
floodFor = "water";
}
else if (this.map[startIndex] === terrainStates.DEEP_WATER)
{
this.landPassMap[startIndex] = 1; // impassable for land
return false;
return undefined;
}
// here we'll be able to start.
for (let i = this.regionSize.length; i <= value; ++i)
getRegionSize(position, onWater)
{
this.regionLinks.push([]);
this.regionSize.push(0);
this.regionType.push("inaccessible");
const pos = this.gamePosToMapPos(position);
const index = pos[0] + pos[1]*this.width;
const ID = onWater === true ? this.navalPassMap[index] : this.landPassMap[index];
if (this.regionSize[ID] === undefined)
return 0;
return this.regionSize[ID];
}
const w = this.width;
const h = this.height;
// Get x and y from index
const IndexArray = [startIndex];
let newIndex;
while (IndexArray.length)
getRegionSizei(index, onWater)
{
newIndex = IndexArray.pop();
if (this.regionSize[this.landPassMap[index]] === undefined && (!onWater || this.regionSize[this.navalPassMap[index]] === undefined))
return 0;
if (onWater && this.regionSize[this.navalPassMap[index]] > this.regionSize[this.landPassMap[index]])
return this.regionSize[this.navalPassMap[index]];
return this.regionSize[this.landPassMap[index]];
}
let y = 0;
// vertical iteration
while (true)
/** Implementation of a fast flood fill. Reasonably good performances for JS. */
floodFill(startIndex, value, onWater)
{
if (value > this.maxRegions)
{
--y;
const index = newIndex + w*y;
if (index < 0)
break;
if (floodFor === "land" && this.landPassMap[index] === 0 &&
this.map[index] !== terrainStates.IMPASSABLE &&
this.map[index] !== terrainStates.DEEP_WATER)
{
continue;
}
if (floodFor === "water" && this.navalPassMap[index] === 0 &&
(this.map[index] === terrainStates.DEEP_WATER ||
this.map[index] === terrainStates.SHALLOW_WATER))
{
continue;
}
break;
} // should actually break
++y;
let reachLeft = false;
let reachRight = false;
let index;
do
error("AI accessibility map: too many regions.");
this.landPassMap[startIndex] = 1;
this.navalPassMap[startIndex] = 1;
return false;
}
if (!onWater && this.landPassMap[startIndex] != 0 || onWater && this.navalPassMap[startIndex] != 0)
return false; // already painted.
let floodFor = "land";
if (this.map[startIndex] === terrainStates.IMPASSABLE)
{
index = newIndex + w*y;
this.landPassMap[startIndex] = 1;
this.navalPassMap[startIndex] = 1;
return false;
}
if (floodFor === "land" && this.landPassMap[index] === 0 &&
this.map[index] !== terrainStates.IMPASSABLE &&
this.map[index] !== terrainStates.DEEP_WATER)
if (onWater === true)
{
if (this.map[startIndex] !== terrainStates.DEEP_WATER &&
this.map[startIndex] !== terrainStates.SHALLOW_WATER)
{
this.landPassMap[index] = value;
this.regionSize[value]++;
this.navalPassMap[startIndex] = 1; // impassable for naval
return false; // do nothing
}
else if (floodFor === "water" && this.navalPassMap[index] === 0 &&
(this.map[index] === terrainStates.DEEP_WATER ||
this.map[index] === terrainStates.SHALLOW_WATER))
floodFor = "water";
}
else if (this.map[startIndex] === terrainStates.DEEP_WATER)
{
this.landPassMap[startIndex] = 1; // impassable for land
return false;
}
// here we'll be able to start.
for (let i = this.regionSize.length; i <= value; ++i)
{
this.regionLinks.push([]);
this.regionSize.push(0);
this.regionType.push("inaccessible");
}
const w = this.width;
const h = this.height;
// Get x and y from index
const IndexArray = [startIndex];
let newIndex;
while (IndexArray.length)
{
newIndex = IndexArray.pop();
let y = 0;
// vertical iteration
while (true)
{
this.navalPassMap[index] = value;
this.regionSize[value]++;
}
else
--y;
const index = newIndex + w*y;
if (index < 0)
break;
if (floodFor === "land" && this.landPassMap[index] === 0 &&
this.map[index] !== terrainStates.IMPASSABLE &&
this.map[index] !== terrainStates.DEEP_WATER)
{
continue;
}
if (floodFor === "water" && this.navalPassMap[index] === 0 &&
(this.map[index] === terrainStates.DEEP_WATER ||
this.map[index] === terrainStates.SHALLOW_WATER))
{
continue;
}
break;
if (index%w > 0)
{
if (floodFor === "land" && this.landPassMap[index -1] === 0 &&
this.map[index -1] !== terrainStates.IMPASSABLE &&
this.map[index -1] !== terrainStates.DEEP_WATER)
{
if (!reachLeft)
{
IndexArray.push(index -1);
reachLeft = true;
}
}
else if (floodFor === "water" && this.navalPassMap[index -1] === 0 &&
(this.map[index -1] === terrainStates.DEEP_WATER ||
this.map[index -1] === terrainStates.SHALLOW_WATER))
{
if (!reachLeft)
{
IndexArray.push(index -1);
reachLeft = true;
}
}
else if (reachLeft)
reachLeft = false;
}
if (index%w < w - 1)
{
if (floodFor === "land" && this.landPassMap[index +1] === 0 &&
this.map[index +1] !== terrainStates.IMPASSABLE &&
this.map[index +1] !== terrainStates.DEEP_WATER)
{
if (!reachRight)
{
IndexArray.push(index +1);
reachRight = true;
}
}
else if (floodFor === "water" && this.navalPassMap[index +1] === 0 &&
(this.map[index +1] === terrainStates.DEEP_WATER ||
this.map[index +1] === terrainStates.SHALLOW_WATER))
{
if (!reachRight)
{
IndexArray.push(index +1);
reachRight = true;
}
}
else if (reachRight)
reachRight = false;
}
} // should actually break
++y;
} while (index/w < h-1); // should actually break
let reachLeft = false;
let reachRight = false;
let index;
do
{
index = newIndex + w*y;
if (floodFor === "land" && this.landPassMap[index] === 0 &&
this.map[index] !== terrainStates.IMPASSABLE &&
this.map[index] !== terrainStates.DEEP_WATER)
{
this.landPassMap[index] = value;
this.regionSize[value]++;
}
else if (floodFor === "water" && this.navalPassMap[index] === 0 &&
(this.map[index] === terrainStates.DEEP_WATER ||
this.map[index] === terrainStates.SHALLOW_WATER))
{
this.navalPassMap[index] = value;
this.regionSize[value]++;
}
else
break;
if (index%w > 0)
{
if (floodFor === "land" && this.landPassMap[index -1] === 0 &&
this.map[index -1] !== terrainStates.IMPASSABLE &&
this.map[index -1] !== terrainStates.DEEP_WATER)
{
if (!reachLeft)
{
IndexArray.push(index -1);
reachLeft = true;
}
}
else if (floodFor === "water" && this.navalPassMap[index -1] === 0 &&
(this.map[index -1] === terrainStates.DEEP_WATER ||
this.map[index -1] === terrainStates.SHALLOW_WATER))
{
if (!reachLeft)
{
IndexArray.push(index -1);
reachLeft = true;
}
}
else if (reachLeft)
reachLeft = false;
}
if (index%w < w - 1)
{
if (floodFor === "land" && this.landPassMap[index +1] === 0 &&
this.map[index +1] !== terrainStates.IMPASSABLE &&
this.map[index +1] !== terrainStates.DEEP_WATER)
{
if (!reachRight)
{
IndexArray.push(index +1);
reachRight = true;
}
}
else if (floodFor === "water" && this.navalPassMap[index +1] === 0 &&
(this.map[index +1] === terrainStates.DEEP_WATER ||
this.map[index +1] === terrainStates.SHALLOW_WATER))
{
if (!reachRight)
{
IndexArray.push(index +1);
reachRight = true;
}
}
else if (reachRight)
reachRight = false;
}
++y;
} while (index/w < h-1); // should actually break
}
return true;
}
return true;
};
}