diff --git a/binaries/data/mods/public/simulation/ai/common-api/map-module.js b/binaries/data/mods/public/simulation/ai/common-api/map-module.js index 2880f618cb..5dd610f2f1 100644 --- a/binaries/data/mods/public/simulation/ai/common-api/map-module.js +++ b/binaries/data/mods/public/simulation/ai/common-api/map-module.js @@ -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); + } +} diff --git a/binaries/data/mods/public/simulation/ai/common-api/shared.js b/binaries/data/mods/public/simulation/ai/common-api/shared.js index 4f4e29c666..e68a2084be 100644 --- a/binaries/data/mods/public/simulation/ai/common-api/shared.js +++ b/binaries/data/mods/public/simulation/ai/common-api/shared.js @@ -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() { diff --git a/binaries/data/mods/public/simulation/ai/common-api/terrain-analysis.js b/binaries/data/mods/public/simulation/ai/common-api/terrain-analysis.js index ba5d6f0566..79fd1a0c8e 100644 --- a/binaries/data/mods/public/simulation/ai/common-api/terrain-analysis.js +++ b/binaries/data/mods/public/simulation/ai/common-api/terrain-analysis.js @@ -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; -}; +}