Separate object-oriented random map generation core library "rmgen" from miscellaneou...
[0ad.git] / binaries / data / mods / public / maps / random / rmgen / random_map.js
blob4b66a8754ba8b5df87b145f8a4eacaa013f469d3
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)
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
15         this.nameToID = {};
16         this.IDToName = [];
18         // Texture 2D array
19         this.texture = [];
20         for (let x = 0; x < this.size; ++x)
21         {
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));
27         }
29         // Create 2D arrays for terrain objects and areas
30         this.terrainEntities = [];
31         this.area = [];
33         for (let i = 0; i < this.size; ++i)
34         {
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;
40         }
42         // Create 2D array for heightmap
43         let mapSize = this.size;
44         if (!TILE_CENTERED_HEIGHT_MAP)
45                 ++mapSize;
47         this.height = [];
48         for (let i = 0; i < mapSize; ++i)
49         {
50                 this.height[i] = new Float32Array(mapSize);
52                 for (let j = 0; j < mapSize; ++j)
53                         this.height[i][j] = baseHeight;
54         }
56         this.entities = [];
58         this.areaID = 0;
60         // Starting entity ID, arbitrary number to leave some space for player entities
61         this.entityCount = 150;
64 /**
65  * Prints a timed log entry to stdout and the logfile.
66  */
67 RandomMap.prototype.log = function(text)
69         this.logger.print(text);
72 /**
73  * Loads an imagefile and uses it as the heightmap for the current map.
74  * Scales the map (including height) proportionally with the mapsize.
75  */
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));
83         createArea(
84                 new MapBoundsPlacer(),
85                 [
86                         heightmapPainter,
87                         new TerrainTextureArrayPainter(mapTerrain.textureIDs, mapTerrain.textureNames)
88                 ]);
90         return heightmapPainter.getScale();
93 /**
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.
97  */
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);
105         createArea(
106                 new MapBoundsPlacer(),
107                 heightmapPainter);
109         return heightmapPainter.getScale();
113  * Returns the ID of a texture name.
114  * Creates a new ID if there isn't one assigned yet.
115  */
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;
125         return id;
129  * Returns the next unused entityID.
130  */
131 RandomMap.prototype.getEntityID = function()
133         return this.entityCount++;
136 RandomMap.prototype.isCircularMap = function()
138         return !!g_MapSettings.CircularMap;
141 RandomMap.prototype.getSize = function()
143         return this.size;
147  * Returns the center tile coordinates of the map.
148  */
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.
156  */
157 RandomMap.prototype.getBounds = function()
159         return deepfreeze({
160                 "left": 0,
161                 "right": this.size,
162                 "top": this.size,
163                 "bottom": 0
164         });
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 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.
184  */
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.
193  */
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.
202  */
203 RandomMap.prototype.validHeight = function(position)
205         if (position.x < 0 || position.y < 0)
206                 return false;
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.
217  */
218 RandomMap.prototype.randomCoordinate = function(passableOnly)
220         let border = passableOnly ? MAP_BORDER_WIDTH : 0;
222         if (this.isCircularMap())
223                 // Polar coordinates
224                 // Uniformly distributed on the disk
225                 return Vector2D.add(
226                         this.getCenter(),
227                         new Vector2D((this.size / 2 - border) * Math.sqrt(randFloat(0, 1)), 0).rotate(randomAngle()).floor());
229         // Rectangular coordinates
230         return new Vector2D(
231                 randIntExclusive(border, this.size - border),
232                 randIntExclusive(border, this.size - border));
236  * Returns the name of the texture of the given tile.
237  */
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.
248  */
249 RandomMap.prototype.setTexture = function(position, texture)
251         if (position.x < 0 ||
252             position.y < 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.
278  */
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.
287  */
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.
296  */
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.
307  */
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.
320  */
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.
336  */
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)
355         let count = 0;
356         let sumHeight = 0;
358         for (let vertex of g_TileVertices)
359         {
360                 let pos = Vector2D.sub(position, vertex);
361                 if (this.validHeight(pos))
362                 {
363                         ++count;
364                         sumHeight += this.getHeight(pos);
365                 }
366         }
368         if (!count)
369                 return 0;
371         return sumHeight / count;
374 RandomMap.prototype.getAdjacentPoints = function(position)
376         let adjacentPositions = [];
378         for (let adjacentCoordinate of g_AdjacentCoordinates)
379         {
380                 let adjacentPos = Vector2D.add(position, adjacentCoordinate).round();
381                 if (this.inMapBounds(adjacentPos))
382                         adjacentPositions.push(adjacentPos);
383         }
385         return adjacentPositions;
389  * Returns the average height of adjacent tiles, helpful for smoothing.
390  */
391 RandomMap.prototype.getAverageHeight = function(position)
393         let adjacentPositions = this.getAdjacentPoints(position);
394         if (!adjacentPositions.length)
395                 return 0;
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.
402  */
403 RandomMap.prototype.getSlope = function(position)
405         let adjacentPositions = this.getAdjacentPoints(position);
406         if (!adjacentPositions.length)
407                 return 0;
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.
415  */
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.
434  */
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)
442                 {
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)));
448                 }
450         return heightmap;
454  * Assemble terrain textures in a one-dimensional array.
455  */
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)
463                 {
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];
467                 }
469         return {
470                 "index": tileIndex,
471                 "priority": tilePriority
472         };
475 RandomMap.prototype.ExportMap = function()
477         if (g_Environment.Water.WaterBody.Height === undefined)
478                 g_Environment.Water.WaterBody.Height = SEA_LEVEL - 0.1;
480         this.logger.close();
482         Engine.ExportMap({
483                 "entities": this.exportEntityList(),
484                 "height": this.exportHeightData(),
485                 "seaLevel": SEA_LEVEL,
486                 "size": this.size,
487                 "textureNames": this.IDToName,
488                 "tileData": this.exportTerrainTextures(),
489                 "Camera": g_Camera,
490                 "Environment": g_Environment
491         });