This Trac instance is not used for development anymore!

We migrated our development workflow to git and Gitea.
To test the future redirection, replace trac by ariadne in the page URL.

source: ps/trunk/binaries/data/mods/public/maps/random/rmgen/RandomMap.js

Last change on this file was 28122, checked in by phosit, 6 months ago

Fix getArea

The code didn't call this.isCircularMap but checks if it's defined. The function returned the wrong values for square maps.

Broken in rP25894

Comments by: @Stan

Differential Revision: https://code.wildfiregames.com/D5288

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