2025-08-03 04:30:17 -07:00
import { InfoMap } from "simulation/ai/common-api/map-module.js" ;
2026-07-14 04:43:38 -07:00
import * as terrainStates from "simulation/ai/common-api/terrain-states.js" ;
2013-12-30 02:04:59 -08:00
2016-06-09 12:25:40 -07:00
/ * *
2013-03-05 14:52:48 -08:00
* TerrainAnalysis , inheriting from the Map Component .
2016-11-23 05:02:58 -08:00
*
2015-06-22 10:19:24 -07:00
* This creates a suitable passability map .
2013-03-05 14:52:48 -08:00
* This is part of the Shared Script , and thus should only be used for things that are non - player specific .
* This . map is a map of the world , where particular stuffs are pointed with a value
* For example , impassable land is 0 , water is 200 , areas near tree ( ie forest grounds ) are 41 …
* This is intended for use with 8 bit maps for reduced memory usage .
* Upgraded from QuantumState ' s original TerrainAnalysis for qBot .
* /
2026-07-14 06:13:32 -07:00
export class TerrainAnalysis extends InfoMap
2014-12-09 11:55:37 -08:00
{
2026-07-14 06:13:32 -07:00
constructor ( sharedScript , rawState )
{
const passabilityMap = rawState . passabilityMap ;
2013-03-05 14:52:48 -08:00
2026-07-14 06:13:32 -07:00
const obstructionMaskLand = rawState . passabilityClasses [ "default-terrain-only" ] ;
const obstructionMaskWater = rawState . passabilityClasses [ "ship-terrain-only" ] ;
2015-06-17 14:03:49 -07:00
2026-07-14 06:13:32 -07:00
const obstructionTiles = new Uint8Array ( passabilityMap . data . length ) ;
2016-11-23 05:02:58 -08:00
2026-07-14 06:13:32 -07:00
for ( let i = 0 ; i < passabilityMap . data . length ; ++ i )
2026-07-14 04:43:38 -07:00
{
2026-07-14 06:13:32 -07:00
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 ;
}
2026-07-14 04:43:38 -07:00
}
2013-03-05 14:52:48 -08:00
2026-07-14 06:13:32 -07:00
super ( rawState , "passability" , obstructionTiles ) ;
this . width = passabilityMap . width ;
this . height = passabilityMap . height ;
this . cellSize = passabilityMap . cellSize ;
}
}
2013-03-05 14:52:48 -08:00
2016-06-09 12:25:40 -07:00
/ * *
2013-03-05 14:52:48 -08:00
* Accessibility inherits from TerrainAnalysis
2016-11-23 05:02:58 -08:00
*
2013-03-05 14:52:48 -08:00
* This can easily and efficiently determine if any two points are connected .
* it can also determine if any point is "probably" reachable , assuming the unit can get close enough
* for optimizations it ' s called after the TerrainAnalyser has finished initializing his map
* so this can use the land regions already .
* /
2026-07-14 06:13:32 -07:00
export class Accessibility extends InfoMap
2014-12-09 11:55:37 -08:00
{
2026-07-14 06:13:32 -07:00
constructor ( rawState , terrainAnalyser )
2014-12-27 07:23:20 -08:00
{
2026-07-14 06:13:32 -07:00
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 = [ ] ;
// 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 )
{
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 ) ;
}
2013-09-29 06:32:52 -07:00
}
2016-11-23 05:02:58 -08:00
2026-07-14 06:13:32 -07:00
// 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 )
2013-09-29 06:32:52 -07:00
{
2026-07-14 06:13:32 -07:00
for ( let y = 0 ; y < this . height - 1 ; ++ y )
2013-09-29 06:32:52 -07:00
{
2026-07-14 06:13:32 -07:00
// 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 ] ;
2013-09-29 06:32:52 -07:00
if ( thisLID > 1 )
2026-07-14 06:13:32 -07:00
{
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 ) ;
}
2013-09-29 06:32:52 -07:00
}
2013-03-05 14:52:48 -08:00
}
2016-11-23 05:02:58 -08:00
2026-07-14 06:13:32 -07:00
// Engine.DumpImage("LandPassMap.png", this.landPassMap, this.width, this.height, 255);
// Engine.DumpImage("NavalPassMap.png", this.navalPassMap, this.width, this.height, 255);
}
2013-03-05 14:52:48 -08:00
2026-07-14 06:13:32 -07:00
getAccessValue ( position , onWater )
2013-09-29 06:32:52 -07:00
{
2026-07-14 06:13:32 -07:00
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 )
2013-09-29 06:32:52 -07:00
{
2026-07-14 06:13:32 -07:00
// 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 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 ;
}
2013-09-29 06:32:52 -07:00
}
2026-07-14 06:13:32 -07:00
return ret ;
2013-09-29 06:32:52 -07:00
}
2026-07-14 06:13:32 -07:00
getTrajectTo ( 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 ) ;
}
2013-09-29 06:32:52 -07:00
2026-07-14 06:13:32 -07:00
/ * *
* 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 )
2015-07-07 10:32:35 -07:00
{
2026-07-14 06:13:32 -07:00
if ( istart === iend )
return [ istart ] ;
const trajects = new Set ( ) ;
const explored = new Set ( ) ;
trajects . add ( [ istart ] ) ;
explored . add ( istart ) ;
while ( trajects . size )
2013-09-29 06:32:52 -07:00
{
2026-07-14 06:13:32 -07:00
for ( const traj of trajects )
2014-12-12 09:41:10 -08:00
{
2026-07-14 06:13:32 -07:00
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 ) ;
2013-09-29 06:32:52 -07:00
}
}
2026-07-14 06:13:32 -07:00
return undefined ;
2013-09-29 06:32:52 -07:00
}
2026-07-14 06:13:32 -07:00
getRegionSize ( position , onWater )
2015-03-15 03:00:16 -07:00
{
2026-07-14 06:13:32 -07:00
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 ] ;
2015-03-15 03:00:16 -07:00
}
2026-07-14 06:13:32 -07:00
getRegionSizei ( index , onWater )
2013-09-29 06:32:52 -07:00
{
2026-07-14 06:13:32 -07:00
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 ] ] ;
2013-09-29 06:32:52 -07:00
}
2014-05-18 03:50:44 -07:00
2026-07-14 06:13:32 -07:00
/** Implementation of a fast flood fill. Reasonably good performances for JS. */
floodFill ( startIndex , value , onWater )
2013-09-29 06:32:52 -07:00
{
2026-07-14 06:13:32 -07:00
if ( value > this . maxRegions )
2013-09-29 06:32:52 -07:00
{
2026-07-14 06:13:32 -07:00
error ( "AI accessibility map: too many regions." ) ;
this . landPassMap [ startIndex ] = 1 ;
this . navalPassMap [ startIndex ] = 1 ;
return false ;
2013-09-29 06:32:52 -07:00
}
2016-11-23 05:02:58 -08:00
2026-07-14 06:13:32 -07:00
if ( ! onWater && this . landPassMap [ startIndex ] != 0 || onWater && this . navalPassMap [ startIndex ] != 0 )
return false ; // already painted.
2015-03-15 03:00:16 -07:00
2026-07-14 06:13:32 -07:00
let floodFor = "land" ;
if ( this . map [ startIndex ] === terrainStates . IMPASSABLE )
{
this . landPassMap [ startIndex ] = 1 ;
this . navalPassMap [ startIndex ] = 1 ;
return false ;
}
2013-03-05 14:52:48 -08:00
2026-07-14 06:13:32 -07:00
if ( onWater === true )
Fix eslint rule 'brace-on-same-line'
eslint --no-config-lookup --fix --plugin eslint-plugin-brace-rules \
--rule '"brace-rules/brace-on-same-line": [
"warn",
{
"FunctionDeclaration": "never",
"FunctionExpression": "ignore",
"ArrowFunctionExpression": "always",
"IfStatement": "never",
"TryStatement": "ignore",
"CatchClause": "ignore",
"DoWhileStatement": "never",
"WhileStatement": "never",
"ForStatement": "never",
"ForInStatement": "never",
"ForOfStatement": "never",
"SwitchStatement": "never",
},
{
"allowSingleLine": true,
}
]'
Ref: #7812
Signed-off-by: Ralph Sennhauser <ralph.sennhauser@gmail.com>
2025-05-03 07:13:38 -07:00
{
2026-07-14 06:13:32 -07:00
if ( this . map [ startIndex ] !== terrainStates . DEEP _WATER &&
this . map [ startIndex ] !== terrainStates . SHALLOW _WATER )
2026-07-14 04:43:38 -07:00
{
2026-07-14 06:13:32 -07:00
this . navalPassMap [ startIndex ] = 1 ; // impassable for naval
return false ; // do nothing
2026-07-14 04:43:38 -07:00
}
2026-07-14 06:13:32 -07:00
floodFor = "water" ;
}
else if ( this . map [ startIndex ] === terrainStates . DEEP _WATER )
Fix eslint rule 'brace-on-same-line'
eslint --no-config-lookup --fix --plugin eslint-plugin-brace-rules \
--rule '"brace-rules/brace-on-same-line": [
"warn",
{
"FunctionDeclaration": "never",
"FunctionExpression": "ignore",
"ArrowFunctionExpression": "always",
"IfStatement": "never",
"TryStatement": "ignore",
"CatchClause": "ignore",
"DoWhileStatement": "never",
"WhileStatement": "never",
"ForStatement": "never",
"ForInStatement": "never",
"ForOfStatement": "never",
"SwitchStatement": "never",
},
{
"allowSingleLine": true,
}
]'
Ref: #7812
Signed-off-by: Ralph Sennhauser <ralph.sennhauser@gmail.com>
2025-05-03 07:13:38 -07:00
{
2026-07-14 06:13:32 -07:00
this . landPassMap [ startIndex ] = 1 ; // impassable for land
return false ;
}
2016-11-23 05:02:58 -08:00
2026-07-14 06:13:32 -07:00
// 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 )
2014-05-18 03:50:44 -07:00
{
2026-07-14 06:13:32 -07:00
-- 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 ;
}
2014-05-18 03:50:44 -07:00
break ;
2026-07-14 06:13:32 -07:00
} // should actually break
++ y ;
let reachLeft = false ;
let reachRight = false ;
let index ;
do
2013-03-05 14:52:48 -08:00
{
2026-07-14 06:13:32 -07:00
index = newIndex + w * y ;
if ( floodFor === "land" && this . landPassMap [ index ] === 0 &&
this . map [ index ] !== terrainStates . IMPASSABLE &&
this . map [ index ] !== terrainStates . DEEP _WATER )
2014-05-18 03:50:44 -07:00
{
2026-07-14 06:13:32 -07:00
this . landPassMap [ index ] = value ;
this . regionSize [ value ] ++ ;
2014-05-18 03:50:44 -07:00
}
2026-07-14 06:13:32 -07:00
else if ( floodFor === "water" && this . navalPassMap [ index ] === 0 &&
( this . map [ index ] === terrainStates . DEEP _WATER ||
this . map [ index ] === terrainStates . SHALLOW _WATER ) )
2014-05-18 03:50:44 -07:00
{
2026-07-14 06:13:32 -07:00
this . navalPassMap [ index ] = value ;
this . regionSize [ value ] ++ ;
2013-03-05 14:52:48 -08:00
}
2026-07-14 06:13:32 -07:00
else
break ;
2014-05-18 03:50:44 -07:00
2026-07-14 06:13:32 -07:00
if ( index % w > 0 )
2014-05-18 03:50:44 -07:00
{
2026-07-14 06:13:32 -07:00
if ( floodFor === "land" && this . landPassMap [ index - 1 ] === 0 &&
this . map [ index - 1 ] !== terrainStates . IMPASSABLE &&
this . map [ index - 1 ] !== terrainStates . DEEP _WATER )
2014-05-18 03:50:44 -07:00
{
2026-07-14 06:13:32 -07:00
if ( ! reachLeft )
{
IndexArray . push ( index - 1 ) ;
reachLeft = true ;
}
2013-03-05 14:52:48 -08:00
}
2026-07-14 06:13:32 -07:00
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 ;
2014-05-18 03:50:44 -07:00
}
2026-07-14 06:13:32 -07:00
if ( index % w < w - 1 )
2014-05-18 03:50:44 -07:00
{
2026-07-14 06:13:32 -07:00
if ( floodFor === "land" && this . landPassMap [ index + 1 ] === 0 &&
this . map [ index + 1 ] !== terrainStates . IMPASSABLE &&
this . map [ index + 1 ] !== terrainStates . DEEP _WATER )
2014-05-18 03:50:44 -07:00
{
2026-07-14 06:13:32 -07:00
if ( ! reachRight )
{
IndexArray . push ( index + 1 ) ;
reachRight = true ;
}
2013-03-05 14:52:48 -08:00
}
2026-07-14 06:13:32 -07:00
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 ;
2013-03-05 14:52:48 -08:00
}
2026-07-14 06:13:32 -07:00
++ y ;
} while ( index / w < h - 1 ) ; // should actually break
}
return true ;
2013-03-05 14:52:48 -08:00
}
2026-07-14 06:13:32 -07:00
}