LCOV - code coverage report
Current view: top level - maps/random/rmgen - RandomMap.js (source / functions) Hit Total Coverage
Test: lcov.info Lines: 77 186 41.4 %
Date: 2023-04-02 12:52:40 Functions: 9 37 24.3 %

          Line data    Source code
       1             : /**
       2             :  * @file The RandomMap stores the elevation grid, terrain textures and entities that are exported to the engine.
       3             :  *
       4             :  * @param {number} baseHeight - Initial elevation of the map
       5             :  * @param {String|Array} baseTerrain - One or more texture names
       6             :  */
       7             : function RandomMap(baseHeight, baseTerrain)
       8             : {
       9           8 :         this.logger = new RandomMapLogger();
      10             : 
      11             :         // Size must be 0 to 1024, divisible by patches
      12           8 :         this.size = g_MapSettings.Size;
      13             : 
      14             :         // Create name <-> id maps for textures
      15           8 :         this.nameToID = {};
      16           8 :         this.IDToName = [];
      17             : 
      18             :         // Texture 2D array
      19           8 :         this.texture = [];
      20           8 :         for (let x = 0; x < this.size; ++x)
      21             :         {
      22        3904 :                 this.texture[x] = new Uint16Array(this.size);
      23             : 
      24        3904 :                 for (let z = 0; z < this.size; ++z)
      25     1937408 :                         this.texture[x][z] = this.getTextureID(
      26             :                                 typeof baseTerrain == "string" ? baseTerrain : pickRandom(baseTerrain));
      27             :         }
      28             : 
      29             :         // Create 2D arrays for terrain objects and areas
      30           8 :         this.terrainEntities = [];
      31             : 
      32           8 :         for (let i = 0; i < this.size; ++i)
      33             :         {
      34        3904 :                 this.terrainEntities[i] = [];
      35        3904 :                 for (let j = 0; j < this.size; ++j)
      36     1937408 :                         this.terrainEntities[i][j] = undefined;
      37             :         }
      38             : 
      39             :         // Create 2D array for heightmap
      40           8 :         let mapSize = this.size;
      41           8 :         if (!TILE_CENTERED_HEIGHT_MAP)
      42           8 :                 ++mapSize;
      43             : 
      44           8 :         this.height = [];
      45           8 :         for (let i = 0; i < mapSize; ++i)
      46             :         {
      47        3912 :                 this.height[i] = new Float32Array(mapSize);
      48             : 
      49        3912 :                 for (let j = 0; j < mapSize; ++j)
      50     1945224 :                         this.height[i][j] = baseHeight;
      51             :         }
      52             : 
      53           8 :         this.entities = [];
      54             : 
      55             :         // Starting entity ID, arbitrary number to leave some space for player entities
      56           8 :         this.entityCount = 150;
      57             : }
      58             : 
      59             : /**
      60             :  * Prints a timed log entry to stdout and the logfile.
      61             :  */
      62           6 : RandomMap.prototype.log = function(text)
      63             : {
      64           0 :         this.logger.print(text);
      65             : };
      66             : 
      67             : /**
      68             :  * Loads an imagefile and uses it as the heightmap for the current map.
      69             :  * Scales the map (including height) proportionally with the mapsize.
      70             :  */
      71           6 : RandomMap.prototype.LoadMapTerrain = function(filename)
      72             : {
      73           0 :         g_Map.log("Loading terrain file " + filename);
      74           0 :         let mapTerrain = Engine.LoadMapTerrain("maps/random/" + filename + ".pmp");
      75             : 
      76           0 :         let heightmapPainter = new HeightmapPainter(convertHeightmap1Dto2D(mapTerrain.height));
      77             : 
      78           0 :         createArea(
      79             :                 new MapBoundsPlacer(),
      80             :                 [
      81             :                         heightmapPainter,
      82             :                         new TerrainTextureArrayPainter(mapTerrain.textureIDs, mapTerrain.textureNames)
      83             :                 ]);
      84             : 
      85           0 :         return heightmapPainter.getScale();
      86             : };
      87             : 
      88             : /**
      89             :  * Loads PMP terrain file that contains elevation grid and terrain textures created in atlas.
      90             :  * Scales the map (including height) proportionally with the mapsize.
      91             :  * Notice that the image heights can only be between 0 and 255, but the resulting sizes can exceed that range due to the cubic interpolation.
      92             :  */
      93           6 : RandomMap.prototype.LoadHeightmapImage = function(filename, normalMinHeight, normalMaxHeight)
      94             : {
      95           0 :         g_Map.log("Loading heightmap " + filename);
      96             : 
      97           0 :         let heightmapPainter = new HeightmapPainter(
      98             :                 convertHeightmap1Dto2D(Engine.LoadHeightmapImage("maps/random/" + filename)), normalMinHeight, normalMaxHeight);
      99             : 
     100           0 :         createArea(
     101             :                 new MapBoundsPlacer(),
     102             :                 heightmapPainter);
     103             : 
     104           0 :         return heightmapPainter.getScale();
     105             : };
     106             : 
     107             : /**
     108             :  * Returns the ID of a texture name.
     109             :  * Creates a new ID if there isn't one assigned yet.
     110             :  */
     111           6 : RandomMap.prototype.getTextureID = function(texture)
     112             : {
     113     1937457 :         if (texture in this.nameToID)
     114     1937447 :                 return this.nameToID[texture];
     115             : 
     116          10 :         let id = this.IDToName.length;
     117          10 :         this.nameToID[texture] = id;
     118          10 :         this.IDToName[id] = texture;
     119             : 
     120          10 :         return id;
     121             : };
     122             : 
     123             : /**
     124             :  * Returns the next unused entityID.
     125             :  */
     126           6 : RandomMap.prototype.getEntityID = function()
     127             : {
     128           0 :         return this.entityCount++;
     129             : };
     130             : 
     131           6 : RandomMap.prototype.isCircularMap = function()
     132             : {
     133           0 :         return !!g_MapSettings.CircularMap;
     134             : };
     135             : 
     136           6 : RandomMap.prototype.getSize = function()
     137             : {
     138          26 :         return this.size;
     139             : };
     140             : 
     141           6 : RandomMap.prototype.getArea = function(size = this.size)
     142             : {
     143           0 :         return this.isCircularMap ? diskArea(size / 2) : size * size;
     144             : };
     145             : 
     146             : /**
     147             :  * Returns the center tile coordinates of the map.
     148             :  */
     149           6 : RandomMap.prototype.getCenter = function()
     150             : {
     151           0 :         return deepfreeze(new Vector2D(this.size / 2, this.size / 2));
     152             : };
     153             : 
     154             : /**
     155             :  * Returns a human-readable reference to the smallest and greatest coordinates of the map.
     156             :  */
     157           6 : RandomMap.prototype.getBounds = function()
     158             : {
     159           0 :         return deepfreeze({
     160             :                 "left": 0,
     161             :                 "right": this.size,
     162             :                 "top": this.size,
     163             :                 "bottom": 0
     164             :         });
     165             : };
     166             : 
     167             : /**
     168             :  * Determines whether the given coordinates are within the given distance of the map area.
     169             :  * Should be used to restrict actor placement.
     170             :  * Entity placement should be checked against validTilePassable to exclude the map border.
     171             :  * Terrain texture changes should be tested against inMapBounds.
     172             :  */
     173           6 : RandomMap.prototype.validTile = function(position, distance = 0)
     174             : {
     175           0 :         if (this.isCircularMap())
     176           0 :                 return Math.round(position.distanceTo(this.getCenter())) < this.size / 2 - distance - 1;
     177             : 
     178           0 :         return position.x >= distance && position.y >= distance && position.x < this.size - distance && position.y < this.size - distance;
     179             : };
     180             : 
     181             : /**
     182             :  * Determines whether the given coordinates are within the given distance of the passable map area.
     183             :  * Should be used to restrict entity placement and path creation.
     184             :  */
     185           6 : RandomMap.prototype.validTilePassable = function(position, distance = 0)
     186             : {
     187           0 :         return this.validTile(position, distance + MAP_BORDER_WIDTH);
     188             : };
     189             : 
     190             : /**
     191             :  * Determines whether the given coordinates are within the tile grid, passable or not.
     192             :  * Should be used to restrict texture painting.
     193             :  */
     194           6 : RandomMap.prototype.inMapBounds = function(position)
     195             : {
     196        1349 :         return position.x >= 0 && position.y >= 0 && position.x < this.size && position.y < this.size;
     197             : };
     198             : 
     199             : /**
     200             :  * Determines whether the given coordinates are within the heightmap grid.
     201             :  * Should be used to restrict elevation changes.
     202             :  */
     203           6 : RandomMap.prototype.validHeight = function(position)
     204             : {
     205         911 :         if (position.x < 0 || position.y < 0)
     206           0 :                 return false;
     207             : 
     208         911 :         if (TILE_CENTERED_HEIGHT_MAP)
     209           0 :                 return position.x < this.size && position.y < this.size;
     210             : 
     211         911 :         return position.x <= this.size && position.y <= this.size;
     212             : };
     213             : 
     214             : /**
     215             :  * Returns a random point on the map.
     216             :  * @param passableOnly - Should be true for entity placement and false for terrain or elevation operations.
     217             :  */
     218           6 : RandomMap.prototype.randomCoordinate = function(passableOnly)
     219             : {
     220           0 :         let border = passableOnly ? MAP_BORDER_WIDTH : 0;
     221             : 
     222           0 :         if (this.isCircularMap())
     223             :                 // Polar coordinates
     224             :                 // Uniformly distributed on the disk
     225           0 :                 return Vector2D.add(
     226             :                         this.getCenter(),
     227             :                         new Vector2D((this.size / 2 - border) * Math.sqrt(randFloat(0, 1)), 0).rotate(randomAngle()).floor());
     228             : 
     229             :         // Rectangular coordinates
     230           0 :         return new Vector2D(
     231             :                 randIntExclusive(border, this.size - border),
     232             :                 randIntExclusive(border, this.size - border));
     233             : };
     234             : 
     235             : /**
     236             :  * Returns the name of the texture of the given tile.
     237             :  */
     238           6 : RandomMap.prototype.getTexture = function(position)
     239             : {
     240           6 :         if (!this.inMapBounds(position))
     241           0 :                 throw new Error("getTexture: invalid tile position " + uneval(position));
     242             : 
     243           6 :         return this.IDToName[this.texture[position.x][position.y]];
     244             : };
     245             : 
     246             : /**
     247             :  * Paints the given texture on the given tile.
     248             :  */
     249           6 : RandomMap.prototype.setTexture = function(position, texture)
     250             : {
     251          49 :         if (position.x < 0 ||
     252             :             position.y < 0 ||
     253             :             position.x >= this.texture.length ||
     254             :             position.y >= this.texture[position.x].length)
     255           0 :                 throw new Error("setTexture: invalid tile position " + uneval(position));
     256             : 
     257          49 :         this.texture[position.x][position.y] = this.getTextureID(texture);
     258             : };
     259             : 
     260           6 : RandomMap.prototype.getHeight = function(position)
     261             : {
     262         403 :         if (!this.validHeight(position))
     263           0 :                 throw new Error("getHeight: invalid vertex position " + uneval(position));
     264             : 
     265         403 :         return this.height[position.x][position.y];
     266             : };
     267             : 
     268           6 : RandomMap.prototype.setHeight = function(position, height)
     269             : {
     270          52 :         if (!this.validHeight(position))
     271           0 :                 throw new Error("setHeight: invalid vertex position " + uneval(position));
     272             : 
     273          52 :         this.height[position.x][position.y] = height;
     274             : };
     275             : 
     276             : /**
     277             :  * Adds the given Entity to the map at the location it defines, even if at the impassable map border.
     278             :  */
     279           6 : RandomMap.prototype.placeEntityAnywhere = function(templateName, playerID, position, orientation)
     280             : {
     281           0 :         let entity = new Entity(this.getEntityID(), templateName, playerID, position, orientation);
     282           0 :         this.entities.push(entity);
     283           0 :         return entity;
     284             : };
     285             : 
     286             : /**
     287             :  * Adds the given Entity to the map at the location it defines, if that area is not at the impassable map border.
     288             :  */
     289           6 : RandomMap.prototype.placeEntityPassable = function(templateName, playerID, position, orientation)
     290             : {
     291           0 :         if (!this.validTilePassable(position))
     292           0 :                 return undefined;
     293             : 
     294           0 :         return this.placeEntityAnywhere(templateName, playerID, position, orientation);
     295             : };
     296             : 
     297             : /**
     298             :  * Returns the Entity that was painted by a Terrain class on the given tile or undefined otherwise.
     299             :  */
     300           6 : RandomMap.prototype.getTerrainEntity = function(position)
     301             : {
     302           0 :         if (!this.validTilePassable(position))
     303           0 :                 throw new Error("getTerrainEntity: invalid tile position " + uneval(position));
     304             : 
     305           0 :         return this.terrainEntities[position.x][position.y];
     306             : };
     307             : 
     308             : /**
     309             :  * Places the Entity on the given tile and allows to later replace it if the terrain was painted over.
     310             :  */
     311           6 : RandomMap.prototype.setTerrainEntity = function(templateName, playerID, position, orientation)
     312             : {
     313           0 :         let tilePosition = position.clone().floor();
     314           0 :         if (!this.validTilePassable(tilePosition))
     315           0 :                 throw new Error("setTerrainEntity: invalid tile position " + uneval(position));
     316             : 
     317           0 :         this.terrainEntities[tilePosition.x][tilePosition.y] =
     318             :                 new Entity(this.getEntityID(), templateName, playerID, position, orientation);
     319             : };
     320             : 
     321           6 : RandomMap.prototype.deleteTerrainEntity = function(position)
     322             : {
     323           0 :         let tilePosition = position.clone().floor();
     324           0 :         if (!this.validTilePassable(tilePosition))
     325           0 :                 throw new Error("setTerrainEntity: invalid tile position " + uneval(position));
     326             : 
     327           0 :         this.terrainEntities[tilePosition.x][tilePosition.y] = undefined;
     328             : };
     329             : 
     330           6 : RandomMap.prototype.createTileClass = function()
     331             : {
     332           0 :         return new TileClass(this.size);
     333             : };
     334             : 
     335             : /**
     336             :  * Retrieve interpolated height for arbitrary coordinates within the heightmap grid.
     337             :  */
     338           6 : RandomMap.prototype.getExactHeight = function(position)
     339             : {
     340           0 :         let xi = Math.min(Math.floor(position.x), this.size);
     341           0 :         let zi = Math.min(Math.floor(position.y), this.size);
     342           0 :         let xf = position.x - xi;
     343           0 :         let zf = position.y - zi;
     344             : 
     345           0 :         let h00 = this.height[xi][zi];
     346           0 :         let h01 = this.height[xi][zi + 1];
     347           0 :         let h10 = this.height[xi + 1][zi];
     348           0 :         let h11 = this.height[xi + 1][zi + 1];
     349             : 
     350           0 :         return (1 - zf) * ((1 - xf) * h00 + xf * h10) + zf * ((1 - xf) * h01 + xf * h11);
     351             : };
     352             : 
     353             : // Converts from the tile centered height map to the corner based height map, used when TILE_CENTERED_HEIGHT_MAP = true
     354           6 : RandomMap.prototype.cornerHeight = function(position)
     355             : {
     356           0 :         let count = 0;
     357           0 :         let sumHeight = 0;
     358             : 
     359           0 :         for (let vertex of g_TileVertices)
     360             :         {
     361           0 :                 let pos = Vector2D.sub(position, vertex);
     362           0 :                 if (this.validHeight(pos))
     363             :                 {
     364           0 :                         ++count;
     365           0 :                         sumHeight += this.getHeight(pos);
     366             :                 }
     367             :         }
     368             : 
     369           0 :         if (!count)
     370           0 :                 return 0;
     371             : 
     372           0 :         return sumHeight / count;
     373             : };
     374             : 
     375           6 : RandomMap.prototype.getAdjacentPoints = function(position)
     376             : {
     377           0 :         let adjacentPositions = [];
     378             : 
     379           0 :         for (let adjacentCoordinate of g_AdjacentCoordinates)
     380             :         {
     381           0 :                 let adjacentPos = Vector2D.add(position, adjacentCoordinate).round();
     382           0 :                 if (this.inMapBounds(adjacentPos))
     383           0 :                         adjacentPositions.push(adjacentPos);
     384             :         }
     385             : 
     386           0 :         return adjacentPositions;
     387             : };
     388             : 
     389             : /**
     390             :  * Returns the average height of adjacent tiles, helpful for smoothing.
     391             :  */
     392           6 : RandomMap.prototype.getAverageHeight = function(position)
     393             : {
     394           0 :         let adjacentPositions = this.getAdjacentPoints(position);
     395           0 :         if (!adjacentPositions.length)
     396           0 :                 return 0;
     397             : 
     398           0 :         return adjacentPositions.reduce((totalHeight, pos) => totalHeight + this.getHeight(pos), 0) / adjacentPositions.length;
     399             : };
     400             : 
     401             : /**
     402             :  * Returns the steepness of the given location, defined as the average height difference of the adjacent tiles.
     403             :  */
     404           6 : RandomMap.prototype.getSlope = function(position)
     405             : {
     406           0 :         let adjacentPositions = this.getAdjacentPoints(position);
     407           0 :         if (!adjacentPositions.length)
     408           0 :                 return 0;
     409             : 
     410           0 :         return adjacentPositions.reduce((totalSlope, adjacentPos) =>
     411           0 :                 totalSlope + Math.abs(this.getHeight(adjacentPos) - this.getHeight(position)), 0) / adjacentPositions.length;
     412             : };
     413             : 
     414             : /**
     415             :  * Retrieve an array of all Entities placed on the map.
     416             :  */
     417           6 : RandomMap.prototype.exportEntityList = function()
     418             : {
     419           0 :         let nonTerrainCount = this.entities.length;
     420             : 
     421             :         // Change rotation from simple 2d to 3d befor giving to engine
     422           0 :         for (let entity of this.entities)
     423           0 :                 entity.rotation.y = Math.PI / 2 - entity.rotation.y;
     424             : 
     425             :         // Terrain objects e.g. trees
     426           0 :         for (let x = 0; x < this.size; ++x)
     427           0 :                 for (let z = 0; z < this.size; ++z)
     428           0 :                         if (this.terrainEntities[x][z])
     429           0 :                                 this.entities.push(this.terrainEntities[x][z]);
     430             : 
     431           0 :         this.logger.printDirectly(
     432             :                 "Total entities: " + this.entities.length + ", " +
     433             :                 "Terrain entities: " + (this.entities.length - nonTerrainCount) + ", " +
     434             :                 "Textures: " + this.IDToName.length + ".\n");
     435             : 
     436           0 :         return this.entities;
     437             : };
     438             : 
     439             : /**
     440             :  * Convert the elevation grid to a one-dimensional array.
     441             :  */
     442           6 : RandomMap.prototype.exportHeightData = function()
     443             : {
     444           0 :         let heightmapSize = this.size + 1;
     445           0 :         let heightmap = new Uint16Array(Math.square(heightmapSize));
     446             : 
     447           0 :         for (let x = 0; x < heightmapSize; ++x)
     448           0 :                 for (let z = 0; z < heightmapSize; ++z)
     449             :                 {
     450           0 :                         let position = new Vector2D(x, z);
     451           0 :                         let currentHeight = TILE_CENTERED_HEIGHT_MAP ? this.cornerHeight(position) : this.getHeight(position);
     452             : 
     453             :                         // Correct height by SEA_LEVEL and prevent under/overflow in terrain data
     454           0 :                         heightmap[z * heightmapSize + x] = Math.max(0, Math.min(0xFFFF, Math.floor((currentHeight + SEA_LEVEL) * HEIGHT_UNITS_PER_METRE)));
     455             :                 }
     456             : 
     457           0 :         return heightmap;
     458             : };
     459             : 
     460             : /**
     461             :  * Assemble terrain textures in a one-dimensional array.
     462             :  */
     463           6 : RandomMap.prototype.exportTerrainTextures = function()
     464             : {
     465           0 :         let tileIndex = new Uint16Array(Math.square(this.size));
     466           0 :         let tilePriority = new Uint16Array(Math.square(this.size));
     467             : 
     468           0 :         for (let x = 0; x < this.size; ++x)
     469           0 :                 for (let z = 0; z < this.size; ++z)
     470             :                 {
     471             :                         // TODO: For now just use the texture's index as priority, might want to do this another way
     472           0 :                         tileIndex[z * this.size + x] = this.texture[x][z];
     473           0 :                         tilePriority[z * this.size + x] = this.texture[x][z];
     474             :                 }
     475             : 
     476           0 :         return {
     477             :                 "index": tileIndex,
     478             :                 "priority": tilePriority
     479             :         };
     480             : };
     481             : 
     482           6 : RandomMap.prototype.ExportMap = function()
     483             : {
     484           0 :         if (g_Environment.Water.WaterBody.Height === undefined)
     485           0 :                 g_Environment.Water.WaterBody.Height = SEA_LEVEL - 0.1;
     486             : 
     487           0 :         this.logger.close();
     488             : 
     489           0 :         Engine.ExportMap({
     490             :                 "entities": this.exportEntityList(),
     491             :                 "height": this.exportHeightData(),
     492             :                 "seaLevel": SEA_LEVEL,
     493             :                 "size": this.size,
     494             :                 "textureNames": this.IDToName,
     495             :                 "tileData": this.exportTerrainTextures(),
     496             :                 "Camera": g_Camera,
     497             :                 "Environment": g_Environment
     498             :         });
     499             : };

Generated by: LCOV version 1.14