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 : };
|