1
0
forked from mirrors/0ad
Files
0ad/binaries/data/mods/public/maps/random/rmgen/RandomMap.js
T
elexis c6af8b4e76 Fix createObjectGroup (singular) and SimpleGroup.prototype.place not returning an array of entities but an array of undefined following the SimpleGroup change in 78ce731fc5.
The bug was never noticeable because the return result was never read
from yet.

This was SVN commit r21405.
2018-03-01 17:48:13 +00:00

495 lines
14 KiB
JavaScript

/**
* @file The RandomMap stores the elevation grid, terrain textures and entities that are exported to the engine.
*
* @param {Number} baseHeight - Initial elevation of the map
* @param {String|Array} baseTerrain - One or more texture names
*/
function RandomMap(baseHeight, baseTerrain)
{
this.logger = new RandomMapLogger();
// Size must be 0 to 1024, divisible by patches
this.size = g_MapSettings.Size;
// Create name <-> id maps for textures
this.nameToID = {};
this.IDToName = [];
// Texture 2D array
this.texture = [];
for (let x = 0; x < this.size; ++x)
{
this.texture[x] = new Uint16Array(this.size);
for (let z = 0; z < this.size; ++z)
this.texture[x][z] = this.getTextureID(
typeof baseTerrain == "string" ? baseTerrain : pickRandom(baseTerrain));
}
// Create 2D arrays for terrain objects and areas
this.terrainEntities = [];
for (let i = 0; i < this.size; ++i)
{
this.terrainEntities[i] = [];
for (let j = 0; j < this.size; ++j)
this.terrainEntities[i][j] = undefined;
}
// Create 2D array for heightmap
let mapSize = this.size;
if (!TILE_CENTERED_HEIGHT_MAP)
++mapSize;
this.height = [];
for (let i = 0; i < mapSize; ++i)
{
this.height[i] = new Float32Array(mapSize);
for (let j = 0; j < mapSize; ++j)
this.height[i][j] = baseHeight;
}
this.entities = [];
// Starting entity ID, arbitrary number to leave some space for player entities
this.entityCount = 150;
}
/**
* Prints a timed log entry to stdout and the logfile.
*/
RandomMap.prototype.log = function(text)
{
this.logger.print(text);
};
/**
* Loads an imagefile and uses it as the heightmap for the current map.
* Scales the map (including height) proportionally with the mapsize.
*/
RandomMap.prototype.LoadMapTerrain = function(filename)
{
g_Map.log("Loading terrain file " + filename);
let mapTerrain = Engine.LoadMapTerrain("maps/random/" + filename + ".pmp");
let heightmapPainter = new HeightmapPainter(convertHeightmap1Dto2D(mapTerrain.height));
createArea(
new MapBoundsPlacer(),
[
heightmapPainter,
new TerrainTextureArrayPainter(mapTerrain.textureIDs, mapTerrain.textureNames)
]);
return heightmapPainter.getScale();
};
/**
* Loads PMP terrain file that contains elevation grid and terrain textures created in atlas.
* Scales the map (including height) proportionally with the mapsize.
* 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.
*/
RandomMap.prototype.LoadHeightmapImage = function(filename, normalMinHeight, normalMaxHeight)
{
g_Map.log("Loading heightmap " + filename);
let heightmapPainter = new HeightmapPainter(
convertHeightmap1Dto2D(Engine.LoadHeightmapImage("maps/random/" + filename)), normalMinHeight, normalMaxHeight);
createArea(
new MapBoundsPlacer(),
heightmapPainter);
return heightmapPainter.getScale();
};
/**
* Returns the ID of a texture name.
* Creates a new ID if there isn't one assigned yet.
*/
RandomMap.prototype.getTextureID = function(texture)
{
if (texture in this.nameToID)
return this.nameToID[texture];
let id = this.IDToName.length;
this.nameToID[texture] = id;
this.IDToName[id] = texture;
return id;
};
/**
* Returns the next unused entityID.
*/
RandomMap.prototype.getEntityID = function()
{
return this.entityCount++;
};
RandomMap.prototype.isCircularMap = function()
{
return !!g_MapSettings.CircularMap;
};
RandomMap.prototype.getSize = function()
{
return this.size;
};
/**
* Returns the center tile coordinates of the map.
*/
RandomMap.prototype.getCenter = function()
{
return deepfreeze(new Vector2D(this.size / 2, this.size / 2));
};
/**
* Returns a human-readable reference to the smallest and greatest coordinates of the map.
*/
RandomMap.prototype.getBounds = function()
{
return deepfreeze({
"left": 0,
"right": this.size,
"top": this.size,
"bottom": 0
});
};
/**
* Determines whether the given coordinates are within the given distance of the map area.
* Should be used to restrict actor placement.
* Entity placement should be checked against validTilePassable to exclude the map border.
* Terrain texture changes should be tested against inMapBounds.
*/
RandomMap.prototype.validTile = function(position, distance = 0)
{
if (this.isCircularMap())
return Math.round(position.distanceTo(this.getCenter())) < this.size / 2 - distance - 1;
return position.x >= distance && position.y >= distance && position.x < this.size - distance && position.y < this.size - distance;
};
/**
* Determines whether the given coordinates are within the given distance of the passable map area.
* Should be used to restrict entity placement and path creation.
*/
RandomMap.prototype.validTilePassable = function(position, distance = 0)
{
return this.validTile(position, distance + MAP_BORDER_WIDTH);
};
/**
* Determines whether the given coordinates are within the tile grid, passable or not.
* Should be used to restrict texture painting.
*/
RandomMap.prototype.inMapBounds = function(position)
{
return position.x >= 0 && position.y >= 0 && position.x < this.size && position.y < this.size;
};
/**
* Determines whether the given coordinates are within the heightmap grid.
* Should be used to restrict elevation changes.
*/
RandomMap.prototype.validHeight = function(position)
{
if (position.x < 0 || position.y < 0)
return false;
if (TILE_CENTERED_HEIGHT_MAP)
return position.x < this.size && position.y < this.size;
return position.x <= this.size && position.y <= this.size;
};
/**
* Returns a random point on the map.
* @param passableOnly - Should be true for entity placement and false for terrain or elevation operations.
*/
RandomMap.prototype.randomCoordinate = function(passableOnly)
{
let border = passableOnly ? MAP_BORDER_WIDTH : 0;
if (this.isCircularMap())
// Polar coordinates
// Uniformly distributed on the disk
return Vector2D.add(
this.getCenter(),
new Vector2D((this.size / 2 - border) * Math.sqrt(randFloat(0, 1)), 0).rotate(randomAngle()).floor());
// Rectangular coordinates
return new Vector2D(
randIntExclusive(border, this.size - border),
randIntExclusive(border, this.size - border));
};
/**
* Returns the name of the texture of the given tile.
*/
RandomMap.prototype.getTexture = function(position)
{
if (!this.inMapBounds(position))
throw new Error("getTexture: invalid tile position " + uneval(position));
return this.IDToName[this.texture[position.x][position.y]];
};
/**
* Paints the given texture on the given tile.
*/
RandomMap.prototype.setTexture = function(position, texture)
{
if (position.x < 0 ||
position.y < 0 ||
position.x >= this.texture.length ||
position.y >= this.texture[position.x].length)
throw new Error("setTexture: invalid tile position " + uneval(position));
this.texture[position.x][position.y] = this.getTextureID(texture);
};
RandomMap.prototype.getHeight = function(position)
{
if (!this.validHeight(position))
throw new Error("getHeight: invalid vertex position " + uneval(position));
return this.height[position.x][position.y];
};
RandomMap.prototype.setHeight = function(position, height)
{
if (!this.validHeight(position))
throw new Error("setHeight: invalid vertex position " + uneval(position));
this.height[position.x][position.y] = height;
};
/**
* Adds the given Entity to the map at the location it defines, even if at the impassable map border.
*/
RandomMap.prototype.placeEntityAnywhere = function(templateName, playerID, position, orientation)
{
let entity = new Entity(this.getEntityID(), templateName, playerID, position, orientation);
this.entities.push(entity);
return entity;
};
/**
* Adds the given Entity to the map at the location it defines, if that area is not at the impassable map border.
*/
RandomMap.prototype.placeEntityPassable = function(templateName, playerID, position, orientation)
{
if (!this.validTilePassable(position))
return undefined;
return this.placeEntityAnywhere(templateName, playerID, position, orientation);
};
/**
* Returns the Entity that was painted by a Terrain class on the given tile or undefined otherwise.
*/
RandomMap.prototype.getTerrainEntity = function(position)
{
if (!this.validTilePassable(position))
throw new Error("getTerrainEntity: invalid tile position " + uneval(position));
return this.terrainEntities[position.x][position.y];
};
/**
* Places the Entity on the given tile and allows to later replace it if the terrain was painted over.
*/
RandomMap.prototype.setTerrainEntity = function(templateName, playerID, position, orientation)
{
let tilePosition = position.clone().floor();
if (!this.validTilePassable(tilePosition))
throw new Error("setTerrainEntity: invalid tile position " + uneval(position));
this.terrainEntities[tilePosition.x][tilePosition.y] =
new Entity(this.getEntityID(), templateName, playerID, position, orientation);
};
RandomMap.prototype.deleteTerrainEntity = function(position)
{
let tilePosition = position.clone().floor();
if (!this.validTilePassable(tilePosition))
throw new Error("setTerrainEntity: invalid tile position " + uneval(position));
this.terrainEntities[tilePosition.x][tilePosition.y] = undefined;
};
RandomMap.prototype.createTileClass = function()
{
return new TileClass(this.size);
};
/**
* Retrieve interpolated height for arbitrary coordinates within the heightmap grid.
*/
RandomMap.prototype.getExactHeight = function(position)
{
let xi = Math.min(Math.floor(position.x), this.size);
let zi = Math.min(Math.floor(position.y), this.size);
let xf = position.x - xi;
let zf = position.y - zi;
let h00 = this.height[xi][zi];
let h01 = this.height[xi][zi + 1];
let h10 = this.height[xi + 1][zi];
let h11 = this.height[xi + 1][zi + 1];
return (1 - zf) * ((1 - xf) * h00 + xf * h10) + zf * ((1 - xf) * h01 + xf * h11);
};
// Converts from the tile centered height map to the corner based height map, used when TILE_CENTERED_HEIGHT_MAP = true
RandomMap.prototype.cornerHeight = function(position)
{
let count = 0;
let sumHeight = 0;
for (let vertex of g_TileVertices)
{
let pos = Vector2D.sub(position, vertex);
if (this.validHeight(pos))
{
++count;
sumHeight += this.getHeight(pos);
}
}
if (!count)
return 0;
return sumHeight / count;
};
RandomMap.prototype.getAdjacentPoints = function(position)
{
let adjacentPositions = [];
for (let adjacentCoordinate of g_AdjacentCoordinates)
{
let adjacentPos = Vector2D.add(position, adjacentCoordinate).round();
if (this.inMapBounds(adjacentPos))
adjacentPositions.push(adjacentPos);
}
return adjacentPositions;
};
/**
* Returns the average height of adjacent tiles, helpful for smoothing.
*/
RandomMap.prototype.getAverageHeight = function(position)
{
let adjacentPositions = this.getAdjacentPoints(position);
if (!adjacentPositions.length)
return 0;
return adjacentPositions.reduce((totalHeight, pos) => totalHeight + this.getHeight(pos), 0) / adjacentPositions.length;
};
/**
* Returns the steepness of the given location, defined as the average height difference of the adjacent tiles.
*/
RandomMap.prototype.getSlope = function(position)
{
let adjacentPositions = this.getAdjacentPoints(position);
if (!adjacentPositions.length)
return 0;
return adjacentPositions.reduce((totalSlope, adjacentPos) =>
totalSlope + Math.abs(this.getHeight(adjacentPos) - this.getHeight(position)), 0) / adjacentPositions.length;
};
/**
* Retrieve an array of all Entities placed on the map.
*/
RandomMap.prototype.exportEntityList = function()
{
let nonTerrainCount = this.entities.length;
// Change rotation from simple 2d to 3d befor giving to engine
for (let entity of this.entities)
entity.rotation.y = Math.PI / 2 - entity.rotation.y;
// Terrain objects e.g. trees
for (let x = 0; x < this.size; ++x)
for (let z = 0; z < this.size; ++z)
if (this.terrainEntities[x][z])
this.entities.push(this.terrainEntities[x][z]);
this.logger.printDirectly(
"Total entities: " + this.entities.length + ", " +
"Terrain entities: " + (this.entities.length - nonTerrainCount) + ", " +
"Textures: " + this.IDToName.length + ".\n");
return this.entities;
};
/**
* Convert the elevation grid to a one-dimensional array.
*/
RandomMap.prototype.exportHeightData = function()
{
let heightmapSize = this.size + 1;
let heightmap = new Uint16Array(Math.square(heightmapSize));
for (let x = 0; x < heightmapSize; ++x)
for (let z = 0; z < heightmapSize; ++z)
{
let position = new Vector2D(x, z);
let currentHeight = TILE_CENTERED_HEIGHT_MAP ? this.cornerHeight(position) : this.getHeight(position);
// Correct height by SEA_LEVEL and prevent under/overflow in terrain data
heightmap[z * heightmapSize + x] = Math.max(0, Math.min(0xFFFF, Math.floor((currentHeight + SEA_LEVEL) * HEIGHT_UNITS_PER_METRE)));
}
return heightmap;
};
/**
* Assemble terrain textures in a one-dimensional array.
*/
RandomMap.prototype.exportTerrainTextures = function()
{
let tileIndex = new Uint16Array(Math.square(this.size));
let tilePriority = new Uint16Array(Math.square(this.size));
for (let x = 0; x < this.size; ++x)
for (let z = 0; z < this.size; ++z)
{
// TODO: For now just use the texture's index as priority, might want to do this another way
tileIndex[z * this.size + x] = this.texture[x][z];
tilePriority[z * this.size + x] = this.texture[x][z];
}
return {
"index": tileIndex,
"priority": tilePriority
};
};
RandomMap.prototype.ExportMap = function()
{
if (g_Environment.Water.WaterBody.Height === undefined)
g_Environment.Water.WaterBody.Height = SEA_LEVEL - 0.1;
this.logger.close();
Engine.ExportMap({
"entities": this.exportEntityList(),
"height": this.exportHeightData(),
"seaLevel": SEA_LEVEL,
"size": this.size,
"textureNames": this.IDToName,
"tileData": this.exportTerrainTextures(),
"Camera": g_Camera,
"Environment": g_Environment
});
};