2 * @file The RandomMap stores the elevation grid, terrain textures and entities that are exported to the engine.
4 * @param {Number} baseHeight - Initial elevation of the map
5 * @param {String|Array} baseTerrain - One or more texture names
7 function RandomMap(baseHeight, baseTerrain)
9 this.logger = new RandomMapLogger();
11 // Size must be 0 to 1024, divisible by patches
12 this.size = g_MapSettings.Size;
14 // Create name <-> id maps for textures
20 for (let x = 0; x < this.size; ++x)
22 this.texture[x] = new Uint16Array(this.size);
24 for (let z = 0; z < this.size; ++z)
25 this.texture[x][z] = this.getTextureID(
26 typeof baseTerrain == "string" ? baseTerrain : pickRandom(baseTerrain));
29 // Create 2D arrays for terrain objects and areas
30 this.terrainEntities = [];
33 for (let i = 0; i < this.size; ++i)
35 this.area[i] = new Uint16Array(this.size);
37 this.terrainEntities[i] = [];
38 for (let j = 0; j < this.size; ++j)
39 this.terrainEntities[i][j] = undefined;
42 // Create 2D array for heightmap
43 let mapSize = this.size;
44 if (!TILE_CENTERED_HEIGHT_MAP)
48 for (let i = 0; i < mapSize; ++i)
50 this.height[i] = new Float32Array(mapSize);
52 for (let j = 0; j < mapSize; ++j)
53 this.height[i][j] = baseHeight;
60 // Starting entity ID, arbitrary number to leave some space for player entities
61 this.entityCount = 150;
65 * Prints a timed log entry to stdout and the logfile.
67 RandomMap.prototype.log = function(text)
69 this.logger.print(text);
73 * Loads an imagefile and uses it as the heightmap for the current map.
74 * Scales the map (including height) proportionally with the mapsize.
76 RandomMap.prototype.LoadMapTerrain = function(filename)
78 g_Map.log("Loading terrain file " + filename);
79 let mapTerrain = Engine.LoadMapTerrain("maps/random/" + filename + ".pmp");
81 let heightmapPainter = new HeightmapPainter(convertHeightmap1Dto2D(mapTerrain.height));
84 new MapBoundsPlacer(),
87 new TerrainTextureArrayPainter(mapTerrain.textureIDs, mapTerrain.textureNames)
90 return heightmapPainter.getScale();
94 * Loads PMP terrain file that contains elevation grid and terrain textures created in atlas.
95 * Scales the map (including height) proportionally with the mapsize.
96 * 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.
98 RandomMap.prototype.LoadHeightmapImage = function(filename, normalMinHeight, normalMaxHeight)
100 g_Map.log("Loading heightmap " + filename);
102 let heightmapPainter = new HeightmapPainter(
103 convertHeightmap1Dto2D(Engine.LoadHeightmapImage("maps/random/" + filename)), normalMinHeight, normalMaxHeight);
106 new MapBoundsPlacer(),
109 return heightmapPainter.getScale();
113 * Returns the ID of a texture name.
114 * Creates a new ID if there isn't one assigned yet.
116 RandomMap.prototype.getTextureID = function(texture)
118 if (texture in this.nameToID)
119 return this.nameToID[texture];
121 let id = this.IDToName.length;
122 this.nameToID[texture] = id;
123 this.IDToName[id] = texture;
129 * Returns the next unused entityID.
131 RandomMap.prototype.getEntityID = function()
133 return this.entityCount++;
136 RandomMap.prototype.isCircularMap = function()
138 return !!g_MapSettings.CircularMap;
141 RandomMap.prototype.getSize = function()
147 * Returns the center tile coordinates of the map.
149 RandomMap.prototype.getCenter = function()
151 return deepfreeze(new Vector2D(this.size / 2, this.size / 2));
155 * Returns a human-readable reference to the smallest and greatest coordinates of the map.
157 RandomMap.prototype.getBounds = function()
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.
173 RandomMap.prototype.validTile = function(position, distance = 0)
175 if (this.isCircularMap())
176 return Math.round(position.distanceTo(this.getCenter())) < this.size / 2 - distance - 1;
178 return position.x >= distance && position.y >= distance && position.x < this.size - distance && position.y < this.size - distance;
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.
185 RandomMap.prototype.validTilePassable = function(position, distance = 0)
187 return this.validTile(position, distance + MAP_BORDER_WIDTH);
191 * Determines whether the given coordinates are within the tile grid, passable or not.
192 * Should be used to restrict texture painting.
194 RandomMap.prototype.inMapBounds = function(position)
196 return position.x >= 0 && position.y >= 0 && position.x < this.size && position.y < this.size;
200 * Determines whether the given coordinates are within the heightmap grid.
201 * Should be used to restrict elevation changes.
203 RandomMap.prototype.validHeight = function(position)
205 if (position.x < 0 || position.y < 0)
208 if (TILE_CENTERED_HEIGHT_MAP)
209 return position.x < this.size && position.y < this.size;
211 return position.x <= this.size && position.y <= this.size;
215 * Returns a random point on the map.
216 * @param passableOnly - Should be true for entity placement and false for terrain or elevation operations.
218 RandomMap.prototype.randomCoordinate = function(passableOnly)
220 let border = passableOnly ? MAP_BORDER_WIDTH : 0;
222 if (this.isCircularMap())
224 // Uniformly distributed on the disk
227 new Vector2D((this.size / 2 - border) * Math.sqrt(randFloat(0, 1)), 0).rotate(randomAngle()).floor());
229 // Rectangular coordinates
231 randIntExclusive(border, this.size - border),
232 randIntExclusive(border, this.size - border));
236 * Returns the name of the texture of the given tile.
238 RandomMap.prototype.getTexture = function(position)
240 if (!this.inMapBounds(position))
241 throw new Error("getTexture: invalid tile position " + uneval(position));
243 return this.IDToName[this.texture[position.x][position.y]];
247 * Paints the given texture on the given tile.
249 RandomMap.prototype.setTexture = function(position, texture)
251 if (position.x < 0 ||
253 position.x >= this.texture.length ||
254 position.y >= this.texture[position.x].length)
255 throw new Error("setTexture: invalid tile position " + uneval(position));
257 this.texture[position.x][position.y] = this.getTextureID(texture);
260 RandomMap.prototype.getHeight = function(position)
262 if (!this.validHeight(position))
263 throw new Error("getHeight: invalid vertex position " + uneval(position));
265 return this.height[position.x][position.y];
268 RandomMap.prototype.setHeight = function(position, height)
270 if (!this.validHeight(position))
271 throw new Error("setHeight: invalid vertex position " + uneval(position));
273 this.height[position.x][position.y] = height;
277 * Adds the given Entity to the map at the location it defines, even if at the impassable map border.
279 RandomMap.prototype.placeEntityAnywhere = function(templateName, playerID, position, orientation)
281 let entity = new Entity(this.getEntityID(), templateName, playerID, position, orientation);
282 this.entities.push(entity);
286 * Adds the given Entity to the map at the location it defines, if that area is not at the impassable map border.
288 RandomMap.prototype.placeEntityPassable = function(templateName, playerID, position, orientation)
290 if (this.validTilePassable(position))
291 this.placeEntityAnywhere(templateName, playerID, position, orientation);
295 * Returns the Entity that was painted by a Terrain class on the given tile or undefined otherwise.
297 RandomMap.prototype.getTerrainEntity = function(position)
299 if (!this.validTilePassable(position))
300 throw new Error("getTerrainEntity: invalid tile position " + uneval(position));
302 return this.terrainEntities[position.x][position.y];
306 * Places the Entity on the given tile and allows to later replace it if the terrain was painted over.
308 RandomMap.prototype.setTerrainEntity = function(templateName, playerID, position, orientation)
310 let tilePosition = position.clone().floor();
311 if (!this.validTilePassable(tilePosition))
312 throw new Error("setTerrainEntity: invalid tile position " + uneval(position));
314 this.terrainEntities[tilePosition.x][tilePosition.y] =
315 new Entity(this.getEntityID(), templateName, playerID, position, orientation);
319 * Constructs a new Area object and informs the Map which points correspond to this area.
321 RandomMap.prototype.createArea = function(points)
323 let areaID = ++this.areaID;
324 for (let p of points)
325 this.area[p.x][p.y] = areaID;
326 return new Area(points, areaID);
329 RandomMap.prototype.createTileClass = function()
331 return new TileClass(this.size);
335 * Retrieve interpolated height for arbitrary coordinates within the heightmap grid.
337 RandomMap.prototype.getExactHeight = function(position)
339 let xi = Math.min(Math.floor(position.x), this.size);
340 let zi = Math.min(Math.floor(position.y), this.size);
341 let xf = position.x - xi;
342 let zf = position.y - zi;
344 let h00 = this.height[xi][zi];
345 let h01 = this.height[xi][zi + 1];
346 let h10 = this.height[xi + 1][zi];
347 let h11 = this.height[xi + 1][zi + 1];
349 return (1 - zf) * ((1 - xf) * h00 + xf * h10) + zf * ((1 - xf) * h01 + xf * h11);
352 // Converts from the tile centered height map to the corner based height map, used when TILE_CENTERED_HEIGHT_MAP = true
353 RandomMap.prototype.cornerHeight = function(position)
358 for (let vertex of g_TileVertices)
360 let pos = Vector2D.sub(position, vertex);
361 if (this.validHeight(pos))
364 sumHeight += this.getHeight(pos);
371 return sumHeight / count;
374 RandomMap.prototype.getAdjacentPoints = function(position)
376 let adjacentPositions = [];
378 for (let adjacentCoordinate of g_AdjacentCoordinates)
380 let adjacentPos = Vector2D.add(position, adjacentCoordinate).round();
381 if (this.inMapBounds(adjacentPos))
382 adjacentPositions.push(adjacentPos);
385 return adjacentPositions;
389 * Returns the average height of adjacent tiles, helpful for smoothing.
391 RandomMap.prototype.getAverageHeight = function(position)
393 let adjacentPositions = this.getAdjacentPoints(position);
394 if (!adjacentPositions.length)
397 return adjacentPositions.reduce((totalHeight, pos) => totalHeight + this.getHeight(pos), 0) / adjacentPositions.length;
401 * Returns the steepness of the given location, defined as the average height difference of the adjacent tiles.
403 RandomMap.prototype.getSlope = function(position)
405 let adjacentPositions = this.getAdjacentPoints(position);
406 if (!adjacentPositions.length)
409 return adjacentPositions.reduce((totalSlope, adjacentPos) =>
410 totalSlope + Math.abs(this.getHeight(adjacentPos) - this.getHeight(position)), 0) / adjacentPositions.length;
414 * Retrieve an array of all Entities placed on the map.
416 RandomMap.prototype.exportEntityList = function()
418 // Change rotation from simple 2d to 3d befor giving to engine
419 for (let entity of this.entities)
420 entity.rotation.y = Math.PI / 2 - entity.rotation.y;
422 // Terrain objects e.g. trees
423 for (let x = 0; x < this.size; ++x)
424 for (let z = 0; z < this.size; ++z)
425 if (this.terrainEntities[x][z])
426 this.entities.push(this.terrainEntities[x][z]);
428 this.logger.printDirectly("Total entities: " + this.entities.length + ".\n")
429 return this.entities;
433 * Convert the elevation grid to a one-dimensional array.
435 RandomMap.prototype.exportHeightData = function()
437 let heightmapSize = this.size + 1;
438 let heightmap = new Uint16Array(Math.square(heightmapSize));
440 for (let x = 0; x < heightmapSize; ++x)
441 for (let z = 0; z < heightmapSize; ++z)
443 let position = new Vector2D(x, z);
444 let currentHeight = TILE_CENTERED_HEIGHT_MAP ? this.cornerHeight(position) : this.getHeight(position);
446 // Correct height by SEA_LEVEL and prevent under/overflow in terrain data
447 heightmap[z * heightmapSize + x] = Math.max(0, Math.min(0xFFFF, Math.floor((currentHeight + SEA_LEVEL) * HEIGHT_UNITS_PER_METRE)));
454 * Assemble terrain textures in a one-dimensional array.
456 RandomMap.prototype.exportTerrainTextures = function()
458 let tileIndex = new Uint16Array(Math.square(this.size));
459 let tilePriority = new Uint16Array(Math.square(this.size));
461 for (let x = 0; x < this.size; ++x)
462 for (let z = 0; z < this.size; ++z)
464 // TODO: For now just use the texture's index as priority, might want to do this another way
465 tileIndex[z * this.size + x] = this.texture[x][z];
466 tilePriority[z * this.size + x] = this.texture[x][z];
471 "priority": tilePriority
475 RandomMap.prototype.ExportMap = function()
477 if (g_Environment.Water.WaterBody.Height === undefined)
478 g_Environment.Water.WaterBody.Height = SEA_LEVEL - 0.1;
483 "entities": this.exportEntityList(),
484 "height": this.exportHeightData(),
485 "seaLevel": SEA_LEVEL,
487 "textureNames": this.IDToName,
488 "tileData": this.exportTerrainTextures(),
490 "Environment": g_Environment