Separate global heightmap manipulation functions to a library. Fixes #3764

This was SVN commit r18141.
This commit is contained in:
FeXoR 2016-05-08 14:58:57 +00:00
parent 1a3fb29ff3
commit 7471a0db63
5 changed files with 339 additions and 499 deletions

View file

@ -4,6 +4,7 @@ timeArray.push(new Date().getTime());
// Importing rmgen libraries
RMS.LoadLibrary("rmgen");
RMS.LoadLibrary("heightmap");
const BUILDING_ANGlE = -PI/4;
@ -17,37 +18,7 @@ var numPlayers = getNumPlayers();
var mapSize = getMapSize();
//////////
// Heightmap functionality
//////////
// Some general heightmap settings
const MIN_HEIGHT = - SEA_LEVEL; // 20, should be set in the libs!
const MAX_HEIGHT = 0xFFFF/HEIGHT_UNITS_PER_METRE - SEA_LEVEL; // A bit smaler than 90, should be set in the libs!
// Add random heightmap generation functionality
function getRandomReliefmap(minHeight, maxHeight)
{
minHeight = (minHeight || MIN_HEIGHT);
maxHeight = (maxHeight || MAX_HEIGHT);
if (minHeight < MIN_HEIGHT)
warn("getRandomReliefmap: Argument minHeight is smaler then the supported minimum height of " + MIN_HEIGHT + " (const MIN_HEIGHT): " + minHeight)
if (maxHeight > MAX_HEIGHT)
warn("getRandomReliefmap: Argument maxHeight is smaler then the supported maximum height of " + MAX_HEIGHT + " (const MAX_HEIGHT): " + maxHeight)
var reliefmap = [];
for (var x = 0; x <= mapSize; x++)
{
reliefmap.push([]);
for (var y = 0; y <= mapSize; y++)
reliefmap[x].push(randFloat(minHeight, maxHeight));
}
return reliefmap;
}
// Apply a heightmap
// Function to apply a heightmap
function setReliefmap(reliefmap)
{
// g_Map.height = reliefmap;
@ -56,70 +27,6 @@ function setReliefmap(reliefmap)
setHeight(x, y, reliefmap[x][y]);
}
// Get minimum and maxumum height used in a heightmap
function getMinAndMaxHeight(reliefmap)
{
var height = {};
height.min = Infinity;
height.max = -Infinity;
for (var x = 0; x <= mapSize; x++)
for (var y = 0; y <= mapSize; y++)
{
if (reliefmap[x][y] < height.min)
height.min = reliefmap[x][y];
else if (reliefmap[x][y] > height.max)
height.max = reliefmap[x][y];
}
return height;
}
// Rescale a heightmap (Waterlevel is not taken into consideration!)
function getRescaledReliefmap(reliefmap, minHeight, maxHeight)
{
var newReliefmap = deepcopy(reliefmap);
minHeight = (minHeight || MIN_HEIGHT);
maxHeight = (maxHeight || MAX_HEIGHT);
if (minHeight < MIN_HEIGHT)
warn("getRescaledReliefmap: Argument minHeight is smaler then the supported minimum height of " + MIN_HEIGHT + " (const MIN_HEIGHT): " + minHeight)
if (maxHeight > MAX_HEIGHT)
warn("getRescaledReliefmap: Argument maxHeight is smaler then the supported maximum height of " + MAX_HEIGHT + " (const MAX_HEIGHT): " + maxHeight)
var oldHeightRange = getMinAndMaxHeight(reliefmap);
for (var x = 0; x <= mapSize; x++)
for (var y = 0; y <= mapSize; y++)
newReliefmap[x][y] = minHeight + (reliefmap[x][y] - oldHeightRange.min) / (oldHeightRange.max - oldHeightRange.min) * (maxHeight - minHeight);
return newReliefmap
}
// Applying decay errosion (terrain independent)
function getHeightErrosionedReliefmap(reliefmap, strength)
{
var newReliefmap = deepcopy(reliefmap);
strength = (strength || 1.0); // Values much higher then 1 (1.32+ for an 8 tile map, 1.45+ for a 12 tile map, 1.62+ @ 20 tile map, 0.99 @ 4 tiles) will result in a resonance disaster/self interference
var map = [[1, 0], [1, 1], [0, 1], [-1, 1], [-1, 0], [-1, -1], [0, -1], [1, -1]]; // Default
for (var x = 0; x <= mapSize; x++)
for (var y = 0; y <= mapSize; y++)
{
var div = 0;
for (var i = 0; i < map.length; i++)
newReliefmap[x][y] += strength / map.length * (reliefmap[(x + map[i][0] + mapSize + 1) % (mapSize + 1)][(y + map[i][1] + mapSize + 1) % (mapSize + 1)] - reliefmap[x][y]); // Not entirely sure if scaling with map.length is perfect but tested values seam to indicate it is
}
return newReliefmap;
}
//////////
// Prepare for hightmap munipulation
//////////
// Set target min and max height depending on map size to make average stepness the same on all map sizes
var heightRange = {"min": MIN_HEIGHT * mapSize / 8192, "max": MAX_HEIGHT * mapSize / 8192};
@ -215,11 +122,12 @@ while (!goodStartPositionsFound)
log("Starting giant while loop try " + tries);
// Generate reliefmap
var myReliefmap = getRandomReliefmap(heightRange.min, heightRange.max);
var myReliefmap = deepcopy(g_Map.height);
setRandomHeightmap(heightRange.min, heightRange.max, myReliefmap);
for (var i = 0; i < 50 + mapSize/4; i++) // Cycles depend on mapsize (more cycles -> bigger structures)
myReliefmap = getHeightErrosionedReliefmap(myReliefmap, 1);
globalSmoothHeightmap(0.8, myReliefmap);
myReliefmap = getRescaledReliefmap(myReliefmap, heightRange.min, heightRange.max);
rescaleHeightmap(heightRange.min, heightRange.max, myReliefmap);
setReliefmap(myReliefmap);
// Find good start position tiles

View file

@ -0,0 +1,319 @@
/**
* Heightmap manipulation functionality
*
* A heightmapt is an array of width arrays of height floats
* Width and height is normally mapSize+1 (Number of vertices is one bigger than number of tiles in each direction)
* The default heightmap is g_Map.height (See the Map object)
*
* @warning - Ambiguous naming and potential confusion:
* To use this library use TILE_CENTERED_HEIGHT_MAP = false (default)
* Otherwise TILE_CENTERED_HEIGHT_MAP has nothing to do with any tile centered map in this library
* @todo - TILE_CENTERED_HEIGHT_MAP should be removed and g_Map.height should never be tile centered
*/
/**
* Get the height range of a heightmap
* @param {array} [heightmap=g_Map.height] - The reliefmap the minimum and maximum height should be determined for
* @return {object} [height] - Height range with 2 floats in properties "min" and "max"
*/
function getMinAndMaxHeight(heightmap = g_Map.height)
{
let height = {};
height.min = Infinity;
height.max = - Infinity;
for (let x = 0; x < heightmap.length; ++x)
{
for (let y = 0; y < heightmap[x].length; ++y)
{
if (heightmap[x][y] < height.min)
height.min = heightmap[x][y];
else if (heightmap[x][y] > height.max)
height.max = heightmap[x][y];
}
}
return height;
}
/**
* Rescales a heightmap so its minimum and maximum height is as the arguments told preserving it's global shape
* @param {float} [minHeight=MIN_HEIGHT] - Minimum height that should be used for the resulting heightmap
* @param {float} [maxHeight=MAX_HEIGHT] - Maximum height that should be used for the resulting heightmap
* @param {array} [heightmap=g_Map.height] - A reliefmap
* @todo Add preserveCostline to leave a certain height untoucht and scale below and above that seperately
*/
function rescaleHeightmap(minHeight = MIN_HEIGHT, maxHeight = MAX_HEIGHT, heightmap = g_Map.height)
{
let oldHeightRange = getMinAndMaxHeight(heightmap);
let max_x = heightmap.length;
let max_y = heightmap[0].length;
for (let x = 0; x < max_x; ++x)
for (let y = 0; y < max_y; ++y)
heightmap[x][y] = minHeight + (heightmap[x][y] - oldHeightRange.min) / (oldHeightRange.max - oldHeightRange.min) * (maxHeight - minHeight);
}
/**
* Get start location with the largest minimum distance between players
* @param {array} [heightRange] - The height range start locations are allowed
* @param {integer} [maxTries=1000] - How often random player distributions are rolled to be compared
* @param {float} [minDistToBorder=20] - How far start locations have to be away from the map border
* @param {integer} [numberOfPlayers=g_MapSettings.PlayerData.length] - How many start locations should be placed
* @param {array} [heightmap=g_Map.height] - The reliefmap for the start locations to be placed on
* @param {boolean} [isCircular=g_MapSettings.CircularMap] - If the map is circular or rectangular
* @return {array} [finalStartLoc] - Array of 2D points in the format { "x": float, "y": float}
*/
function getStartLocationsByHeightmap(heightRange, maxTries = 1000, minDistToBorder = 20, numberOfPlayers = g_MapSettings.PlayerData.length - 1, heightmap = g_Map.height, isCircular = g_MapSettings.CircularMap)
{
let validStartLoc = [];
let r = 0.5 * (heightmap.length - 1); // Map center x/y as well as radius
for (let x = minDistToBorder; x < heightmap.length - minDistToBorder; ++x)
for (let y = minDistToBorder; y < heightmap[0].length - minDistToBorder; ++y)
if (heightmap[x][y] > heightRange.min && heightmap[x][y] < heightRange.max) // Is in height range
if (!isCircular || r - getDistance(x, y, r, r) >= minDistToBorder) // Is far enough away from map border
validStartLoc.push({ "x": x, "y": y });
let maxMinDist = 0;
let finalStartLoc;
for (let tries = 0; tries < maxTries; ++tries)
{
let startLoc = [];
let minDist = Infinity;
for (let p = 0; p < numberOfPlayers; ++p)
startLoc.push(validStartLoc[randInt(validStartLoc.length)]);
for (let p1 = 0; p1 < numberOfPlayers - 1; ++p1)
{
for (let p2 = p1 + 1; p2 < numberOfPlayers; ++p2)
{
let dist = getDistance(startLoc[p1].x, startLoc[p1].y, startLoc[p2].x, startLoc[p2].y);
if (dist < minDist)
minDist = dist;
}
}
if (minDist > maxMinDist)
{
maxMinDist = minDist;
finalStartLoc = startLoc;
}
}
return finalStartLoc;
}
/**
* Meant to place e.g. resource spots within a height range
* @param {array} [heightRange] - The height range in which to place the entities (An associative array with keys "min" and "max" each containing a float)
* @param {array} [avoidPoints] - An array of 2D points (arrays of length 2), points that will be avoided in the given minDistance e.g. start locations
* @param {integer} [minDistance=30] - How many tile widths the entities to place have to be away from each other, start locations and the map border
* @param {array} [heightmap=g_Map.height] - The reliefmap the entities should be distributed on
* @param {array} [entityList=[g_Gaia.stoneLarge, g_Gaia.metalLarge]] - Entity/actor strings to be placed with placeObject()
* @param {integer} [maxTries=1000] - How often random player distributions are rolled to be compared
* @param {boolean} [isCircular=g_MapSettings.CircularMap] - If the map is circular or rectangular
*/
function distributeEntitiesByHeight(heightRange, avoidPoints, minDistance = 30, entityList = [g_Gaia.stoneLarge, g_Gaia.metalLarge], maxTries = 1000, heightmap = g_Map.height, isCircular = g_MapSettings.CircularMap)
{
let placements = deepcopy(avoidPoints);
let validTiles = [];
let r = 0.5 * (heightmap.length - 1); // Map center x/y as well as radius
for (let x = minDistance; x < heightmap.length - minDistance; ++x)
for (let y = minDistance; y < heightmap[0].length - minDistance; ++y)
if (heightmap[x][y] > heightRange.min && heightmap[x][y] < heightRange.max) // Has the right height
if (!isCircular || r - getDistance(x, y, r, r) >= minDistance) // Is far enough away from map border
validTiles.push({ "x": x, "y": y });
for (let tries = 0; tries < maxTries; ++tries)
{
let tile = validTiles[randInt(validTiles.length)];
let isValid = true;
for (let p = 0; p < placements.length; ++p)
{
if (getDistance(placements[p].x, placements[p].y, tile.x, tile.y) < minDistance)
{
isValid = false;
break;
}
}
if (isValid)
{
placeObject(tile.x, tile.y, entityList[randInt(entityList.length)], 0, randFloat(0, 2*PI));
placements.push(tile);
}
}
}
/**
* Sets a given heightmap to entirely random values within a given range
* @param {float} [minHeight=MIN_HEIGHT] - Lower limit of the random height to be rolled
* @param {float} [maxHeight=MAX_HEIGHT] - Upper limit of the random height to be rolled
* @param {array} [heightmap=g_Map.height] - The reliefmap that should be randomized
*/
function setRandomHeightmap(minHeight = MIN_HEIGHT, maxHeight = MAX_HEIGHT, heightmap = g_Map.height)
{
for (let x = 0; x < heightmap.length; ++x)
for (let y = 0; y < heightmap[0].length; ++y)
heightmap[x][y] = randFloat(minHeight, maxHeight);
}
/**
* Sets the heightmap to a relatively realistic shape
* The function doubles the size of the initial heightmap (if given, else a random 2x2 one) until it's big enough, then the extend is cut off
* @note min/maxHeight will not necessarily be present in the heightmap
* @note On circular maps the edges (given by initialHeightmap) may not be in the playable map area
* @note The impact of the initial heightmap depends on its size and target map size
* @param {float} [minHeight=MIN_HEIGHT] - Lower limit of the random height to be rolled
* @param {float} [maxHeight=MAX_HEIGHT] - Upper limit of the random height to be rolled
* @param {array} [initialHeightmap] - Optional, Small (e.g. 3x3) heightmap describing the global shape of the map e.g. an island [[MIN_HEIGHT, MIN_HEIGHT, MIN_HEIGHT], [MIN_HEIGHT, MAX_HEIGHT, MIN_HEIGHT], [MIN_HEIGHT, MIN_HEIGHT, MIN_HEIGHT]]
* @param {float} [smoothness=0.5] - Float between 0 (rough, more local structures) to 1 (smoother, only larger scale structures)
* @param {array} [heightmap=g_Map.height] - The reliefmap that will be set by this function
*/
function setBaseTerrainDiamondSquare(minHeight = MIN_HEIGHT, maxHeight = MAX_HEIGHT, initialHeightmap = undefined, smoothness = 0.5, heightmap = g_Map.height)
{
initialHeightmap = (initialHeightmap || [[randFloat(minHeight / 2, maxHeight / 2), randFloat(minHeight / 2, maxHeight / 2)], [randFloat(minHeight / 2, maxHeight / 2), randFloat(minHeight / 2, maxHeight / 2)]]);
let heightRange = maxHeight - minHeight;
if (heightRange <= 0)
warn("setBaseTerrainDiamondSquare: heightRange <= 0");
let offset = heightRange / 2;
// Double initialHeightmap width until target width is reached (diamond square method)
let newHeightmap = [];
while (initialHeightmap.length < heightmap.length)
{
newHeightmap = [];
let oldWidth = initialHeightmap.length;
// Square
for (let x = 0; x < 2 * oldWidth - 1; ++x)
{
newHeightmap.push([]);
for (let y = 0; y < 2 * oldWidth - 1; ++y)
{
if (x % 2 == 0 && y % 2 == 0) // Old tile
newHeightmap[x].push(initialHeightmap[x/2][y/2]);
else if (x % 2 == 1 && y % 2 == 1) // New tile with diagonal old tile neighbors
{
newHeightmap[x].push((initialHeightmap[(x-1)/2][(y-1)/2] + initialHeightmap[(x+1)/2][(y-1)/2] + initialHeightmap[(x-1)/2][(y+1)/2] + initialHeightmap[(x+1)/2][(y+1)/2]) / 4);
newHeightmap[x][y] += (newHeightmap[x][y] - minHeight) / heightRange * randFloat(-offset, offset);
}
else // New tile with straight old tile neighbors
newHeightmap[x].push(undefined); // Define later
}
}
// Diamond
for (let x = 0; x < 2 * oldWidth - 1; ++x)
{
for (let y = 0; y < 2 * oldWidth - 1; ++y)
{
if (newHeightmap[x][y] !== undefined)
continue;
if (x > 0 && x + 1 < newHeightmap.length - 1 && y > 0 && y + 1 < newHeightmap.length - 1) // Not a border tile
{
newHeightmap[x][y] = (newHeightmap[x+1][y] + newHeightmap[x][y+1] + newHeightmap[x-1][y] + newHeightmap[x][y-1]) / 4;
newHeightmap[x][y] += (newHeightmap[x][y] - minHeight) / heightRange * randFloat(-offset, offset);
}
else if (x < newHeightmap.length - 1 && y > 0 && y < newHeightmap.length - 1) // Left border
{
newHeightmap[x][y] = (newHeightmap[x+1][y] + newHeightmap[x][y+1] + newHeightmap[x][y-1]) / 3;
newHeightmap[x][y] += (newHeightmap[x][y] - minHeight) / heightRange * randFloat(-offset, offset);
}
else if (x > 0 && y > 0 && y < newHeightmap.length - 1) // Right border
{
newHeightmap[x][y] = (newHeightmap[x][y+1] + newHeightmap[x-1][y] + newHeightmap[x][y-1]) / 3;
newHeightmap[x][y] += (newHeightmap[x][y] - minHeight) / heightRange * randFloat(-offset, offset);
}
else if (x > 0 && x < newHeightmap.length - 1 && y < newHeightmap.length - 1) // Bottom border
{
newHeightmap[x][y] = (newHeightmap[x+1][y] + newHeightmap[x][y+1] + newHeightmap[x-1][y]) / 3;
newHeightmap[x][y] += (newHeightmap[x][y] - minHeight) / heightRange * randFloat(-offset, offset);
}
else if (x > 0 && x < newHeightmap.length - 1 && y > 0) // Top border
{
newHeightmap[x][y] = (newHeightmap[x+1][y] + newHeightmap[x-1][y] + newHeightmap[x][y-1]) / 3;
newHeightmap[x][y] += (newHeightmap[x][y] - minHeight) / heightRange * randFloat(-offset, offset);
}
}
}
initialHeightmap = deepcopy(newHeightmap);
offset /= Math.pow(2, smoothness);
}
// Cut initialHeightmap to fit target width
let shift = [floor((newHeightmap.length - heightmap.length) / 2), floor((newHeightmap[0].length - heightmap[0].length) / 2)];
for (let x = 0; x < heightmap.length; ++x)
for (let y = 0; y < heightmap[0].length; ++y)
heightmap[x][y] = newHeightmap[x + shift[0]][y + shift[1]];
}
/**
* Smoothens the entire map
* @param {float} [strength=0.8] - How strong the smooth effect should be: 0 means no effect at all, 1 means quite strong, higher values might cause interferences, better apply it multiple times
* @param {array} [heightmap=g_Map.height] - The heightmap to be smoothed
* @param {array} [smoothMap=[[1, 0], [1, 1], [0, 1], [-1, 1], [-1, 0], [-1, -1], [0, -1], [1, -1]]] - Array of offsets discribing the neighborhood tiles to smooth the height of a tile to
*/
function globalSmoothHeightmap(strength = 0.8, heightmap = g_Map.height, smoothMap = [[1, 0], [1, 1], [0, 1], [-1, 1], [-1, 0], [-1, -1], [0, -1], [1, -1]])
{
let referenceHeightmap = deepcopy(heightmap);
let max_x = heightmap.length;
let max_y = heightmap[0].length;
for (let x = 0; x < max_x; ++x)
{
for (let y = 0; y < max_y; ++y)
{
for (let i = 0; i < smoothMap.length; ++i)
{
let mapX = x + smoothMap[i][0];
let mapY = y + smoothMap[i][1];
if (mapX >= 0 && mapX < max_x && mapY >= 0 && mapY < max_y)
heightmap[x][y] += strength / smoothMap.length * (referenceHeightmap[mapX][mapY] - referenceHeightmap[x][y]);
}
}
}
}
/**
* Pushes a rectangular area towards a given height smoothing it into the original terrain
* @note The window function to determine the smooth is not exactly a gaussian to ensure smooth edges
* @param {object} [center] - The x and y coordinates of the center point (rounded in this function)
* @param {float} [dx] - Distance from the center in x direction the rectangle ends (half width, rounded in this function)
* @param {float} [dy] - Distance from the center in y direction the rectangle ends (half depth, rounded in this function)
* @param {float} [targetHeight] - Height the center of the rectangle will be pushed to
* @param {float} [strength=1] - How strong the height is pushed: 0 means not at all, 1 means the center will be pushed to the target height
* @param {array} [heightmap=g_Map.height] - The heightmap to be manipulated
* @todo Make the window function an argument and maybe add some
*/
function rectangularSmoothToHeight(center, dx, dy, targetHeight, strength = 0.8, heightmap = g_Map.height)
{
let x = round(center.x);
let y = round(center.y);
dx = round(dx);
dy = round(dy);
let heightmapWin = [];
for (let wx = 0; wx < 2 * dx + 1; ++wx)
{
heightmapWin.push([]);
for (let wy = 0; wy < 2 * dy + 1; ++wy)
{
let actualX = x - dx + wx;
let actualY = y - dy + wy;
if (actualX >= 0 && actualX < heightmap.length - 1 && actualY >= 0 && actualY < heightmap[0].length - 1) // Is in map
heightmapWin[wx].push(heightmap[actualX][actualY]);
else
heightmapWin[wx].push(targetHeight);
}
}
for (let wx = 0; wx < 2 * dx + 1; ++wx)
{
for (let wy = 0; wy < 2 * dy + 1; ++wy)
{
let actualX = x - dx + wx;
let actualY = y - dy + wy;
if (actualX >= 0 && actualX < heightmap.length - 1 && actualY >= 0 && actualY < heightmap[0].length - 1) // Is in map
{
// Window function polynomial 2nd degree
let scaleX = 1 - (wx / dx - 1) * (wx / dx - 1);
let scaleY = 1 - (wy / dy - 1) * (wy / dy - 1);
heightmap[actualX][actualY] = heightmapWin[wx][wy] + strength * scaleX * scaleY * (targetHeight - heightmapWin[wx][wy]);
}
}
}
}

View file

@ -1,19 +1,3 @@
function decayErrodeHeightmap(strength, heightmap)
{
strength = strength || 0.9; // 0 to 1
heightmap = heightmap || g_Map.height;
let referenceHeightmap = deepcopy(heightmap);
// let map = [[1, 0], [0, 1], [-1, 0], [0, -1]]; // faster
let map = [[1, 0], [1, 1], [0, 1], [-1, 1], [-1, 0], [-1, -1], [0, -1], [1, -1]]; // smoother
let max_x = heightmap.length;
let max_y = heightmap[0].length;
for (let x = 0; x < max_x; ++x)
for (let y = 0; y < max_y; ++y)
for (let i = 0; i < map.length; ++i)
heightmap[x][y] += strength / map.length * (referenceHeightmap[(x + map[i][0] + max_x) % max_x][(y + map[i][1] + max_y) % max_y] - referenceHeightmap[x][y]); // Not entirely sure if scaling with map.length is perfect but tested values seam to indicate it is
}
/**
* Returns starting position in tile coordinates for the given player.
*/
@ -28,6 +12,7 @@ function getPlayerTileCoordinates(playerIdx, teamIdx, fractionX, fractionZ)
}
RMS.LoadLibrary("rmgen");
RMS.LoadLibrary("heightmap");
const g_InitialMines = 1;
const g_InitialMineDistance = 14;
@ -372,7 +357,7 @@ RMS.SetProgress(70);
log("Smoothing heightmap...");
for (let i = 0; i < 5; ++i)
decayErrodeHeightmap(0.5);
globalSmoothHeightmap();
// repaint clLand to compensate for smoothing
unPaintTileClassBasedOnHeight(-10, 10, 3, clLand);
@ -420,7 +405,7 @@ createAreas(
scaleByMapSize(4, 13)
);
for (let i = 0; i < 3; ++i)
decayErrodeHeightmap(0.2);
globalSmoothHeightmap();
createStragglerTrees(
[oTree1, oTree2, oTree4, oTree3],

View file

@ -7,6 +7,12 @@ const HEIGHT_UNITS_PER_METRE = 92;
const MIN_MAP_SIZE = 128;
const MAX_MAP_SIZE = 512;
const FALLBACK_CIV = "athen";
/**
* Constants needed for heightmap_manipulation.js
*/
const MAX_HEIGHT_RANGE = 0xFFFF / HEIGHT_UNITS_PER_METRE // Engine limit, Roughly 700 meters
const MIN_HEIGHT = - SEA_LEVEL;
const MAX_HEIGHT = MAX_HEIGHT_RANGE - SEA_LEVEL;
function fractionToTiles(f)
{

View file

@ -1,11 +1,5 @@
// Created by Niek ten Brinke (aka niektb)
// Based on FeXoR's Daimond Square Algorithm for heightmap generation and several official random maps
'use strict';
RMS.LoadLibrary('rmgen');
// initialize map
RMS.LoadLibrary("heightmap");
log('Initializing map...');
@ -178,378 +172,6 @@ HeightPlacer.prototype.place = function (constraint) {
return ret;
};
/*
Takes an array of 2D points (arrays of length 2)
Returns the order to go through the points for the shortest closed path (array of indices)
*/
function getOrderOfPointsForShortestClosePath(points)
{
var order = [];
var distances = [];
if (points.length <= 3)
{
for (var i = 0; i < points.length; i++)
order.push(i);
return order;
}
// Just add the first 3 points
var pointsToAdd = deepcopy(points);
for (var i = 0; i < min(points.length, 3); i++)
{
order.push(i);
pointsToAdd.shift(i);
if (i)
distances.push(getDistance(points[order[i]][0], points[order[i]][1], points[order[i - 1]][0], points[order[i - 1]][1]));
}
distances.push(getDistance(points[order[0]][0], points[order[0]][1], points[order[order.length - 1]][0], points[order[order.length - 1]][1]));
// Add remaining points so the path lengthens the least
var numPointsToAdd = pointsToAdd.length;
for (var i = 0; i < numPointsToAdd; i++)
{
var indexToAddTo = undefined;
var minEnlengthen = Infinity;
var minDist1 = 0;
var minDist2 = 0;
for (var k = 0; k < order.length; k++)
{
var dist1 = getDistance(pointsToAdd[0][0], pointsToAdd[0][1], points[order[k]][0], points[order[k]][1]);
var dist2 = getDistance(pointsToAdd[0][0], pointsToAdd[0][1], points[order[(k + 1) % order.length]][0], points[order[(k + 1) % order.length]][1]);
var enlengthen = dist1 + dist2 - distances[k];
if (enlengthen < minEnlengthen)
{
indexToAddTo = k;
minEnlengthen = enlengthen;
minDist1 = dist1;
minDist2 = dist2;
}
}
order.splice(indexToAddTo + 1, 0, i + 3);
distances.splice(indexToAddTo, 1, minDist1, minDist2);
pointsToAdd.shift();
}
return order;
}
////////////////
//
// Heightmap functionality
//
////////////////
// Some heightmap constants
const MIN_HEIGHT = - SEA_LEVEL; // -20
const MAX_HEIGHT = 0xFFFF/HEIGHT_UNITS_PER_METRE - SEA_LEVEL; // A bit smaller than 90
// Get the diferrence between minimum and maxumum height
function getMinAndMaxHeight(reliefmap)
{
var height = {};
height.min = Infinity;
height.max = - Infinity;
for (var x = 0; x < reliefmap.length; x++)
for (var y = 0; y < reliefmap[x].length; y++)
{
if (reliefmap[x][y] < height.min)
height.min = reliefmap[x][y];
else if (reliefmap[x][y] > height.max)
height.max = reliefmap[x][y];
}
return height;
}
function rescaleHeightmap(minHeight, maxHeight, heightmap)
{
minHeight = (minHeight || - SEA_LEVEL);
maxHeight = (maxHeight || 0xFFFF / HEIGHT_UNITS_PER_METRE - SEA_LEVEL);
heightmap = (heightmap || g_Map.height);
var oldHeightRange = getMinAndMaxHeight(heightmap);
var max_x = heightmap.length;
var max_y = heightmap[0].length;
for (var x = 0; x < max_x; x++)
for (var y = 0; y < max_y; y++)
heightmap[x][y] = minHeight + (heightmap[x][y] - oldHeightRange.min) / (oldHeightRange.max - oldHeightRange.min) * (maxHeight - minHeight);
}
/*
getStartLocationsByHeightmap
Takes
hightRange An associative array with keys 'min' and 'max' each containing a float (the height range start locations are allowed)
heightmap Optional, default is g_Map.height, an array of (map width) arrays of (map depth) floats
maxTries Optional, default is 1000, an integer, how often random player distributions are rolled to be compared
minDistToBorder Optional, default is 20, an integer, how far start locations have to be
numberOfPlayers Optional, default is getNumPlayers, an integer, how many start locations should be placed
Returns
An array of 2D points (arrays of length 2)
*/
function getStartLocationsByHeightmap(hightRange, maxTries, minDistToBorder, numberOfPlayers, heightmap)
{
maxTries = (maxTries || 1000);
minDistToBorder = (minDistToBorder || 20);
numberOfPlayers = (numberOfPlayers || getNumPlayers());
heightmap = (heightmap || g_Map.height);
var validStartLocTiles = [];
for (var x = minDistToBorder; x < heightmap.length - minDistToBorder; x++)
for (var y = minDistToBorder; y < heightmap[0].length - minDistToBorder; y++)
if (heightmap[x][y] > hightRange.min && heightmap[x][y] < hightRange.max) // Has the right hight
validStartLocTiles.push([x, y]);
var maxMinDist = 0;
for (var tries = 0; tries < maxTries; tries++)
{
var startLoc = [];
var minDist = heightmap.length;
for (var p = 0; p < numberOfPlayers; p++)
startLoc.push(validStartLocTiles[randInt(validStartLocTiles.length)]);
for (var p1 = 0; p1 < numberOfPlayers - 1; p1++)
for (var p2 = p1 + 1; p2 < numberOfPlayers; p2++)
{
var dist = getDistance(startLoc[p1][0], startLoc[p1][1], startLoc[p2][0], startLoc[p2][1]);
if (dist < minDist)
minDist = dist;
}
if (minDist > maxMinDist)
{
maxMinDist = minDist;
var finalStartLoc = startLoc;
}
}
return finalStartLoc;
}
/*
derivateEntitiesByHeight
Takes
hightRange An associative array with keys 'min' and 'max' each containing a float (the height range start locations are allowed)
startLoc An array of 2D points (arrays of length 2)
heightmap Optional, default is g_Map.height, an array of (map width) arrays of (map depth) floats
entityList Array of entities/actors (strings to be placed with placeObject())
maxTries Optional, default is 1000, an integer, how often random player distributions are rolled to be compared
minDistance Optional, default is 30, an integer, how far start locations have to be away from start locations and the map border
Returns
An array of 2D points (arrays of length 2)
*/
function derivateEntitiesByHeight(hightRange, startLoc, entityList, maxTries, minDistance, heightmap)
{
entityList = (entityList || [templateMetalMine, templateStoneMine]);
maxTries = (maxTries || 1000);
minDistance = (minDistance || 40);
heightmap = (heightmap || g_Map.height);
var placements = deepcopy(startLoc);
var validTiles = [];
for (var x = minDistance; x < heightmap.length - minDistance; x++)
for (var y = minDistance; y < heightmap[0].length - minDistance; y++)
if (heightmap[x][y] > hightRange.min && heightmap[x][y] < hightRange.max) // Has the right hight
validTiles.push([x, y]);
if (!validTiles.length)
return;
for (var tries = 0; tries < maxTries; tries++)
{
var tile = validTiles[randInt(validTiles.length)];
var isValid = true;
for (var p = 0; p < placements.length; p++)
if (getDistance(placements[p][0], placements[p][1], tile[0], tile[1]) < minDistance)
{
isValid = false;
break;
}
if (isValid)
{
placeObject(tile[0], tile[1], entityList[randInt(entityList.length)], 0, randFloat(0, 2*PI));
// placeObject(tile[0], tile[1], 'actor|geology/decal_stone_medit_b.xml', 0, randFloat(0, 2*PI));
placements.push(tile);
}
}
}
////////////////
//
// Base terrain generation functionality
//
////////////////
function setBaseTerrainDiamondSquare(minHeight, maxHeight, smoothness, initialHeightmap, heightmap)
{
// Make some arguments optional
minHeight = (minHeight || 0);
maxHeight = (maxHeight || 1);
var heightRange = maxHeight - minHeight;
if (heightRange <= 0)
warn('setBaseTerrainDiamondSquare: heightRange < 0');
smoothness = (smoothness || 1);
var offset = heightRange / 2;
initialHeightmap = (initialHeightmap || [[randFloat(minHeight / 2, maxHeight / 2), randFloat(minHeight / 2, maxHeight / 2)], [randFloat(minHeight / 2, maxHeight / 2), randFloat(minHeight / 2, maxHeight / 2)]]);
// Double initialHeightmap width untill target width is reached (diamond square method)
while (initialHeightmap.length < heightmap.length)
{
var newHeightmap = [];
var oldWidth = initialHeightmap.length;
// Square
for (var x = 0; x < 2 * oldWidth - 1; x++)
{
newHeightmap.push([]);
for (var y = 0; y < 2 * oldWidth - 1; y++)
{
if (x % 2 === 0 && y % 2 === 0) // Old tile
newHeightmap[x].push(initialHeightmap[x/2][y/2]);
else if (x % 2 == 1 && y % 2 == 1) // New tile with diagonal old tile neighbors
{
newHeightmap[x].push((initialHeightmap[(x-1)/2][(y-1)/2] + initialHeightmap[(x+1)/2][(y-1)/2] + initialHeightmap[(x-1)/2][(y+1)/2] + initialHeightmap[(x+1)/2][(y+1)/2]) / 4);
newHeightmap[x][y] += (newHeightmap[x][y] - minHeight) / heightRange * randFloat(-offset, offset);
}
else // New tile with straight old tile neighbors
newHeightmap[x].push(undefined); // Define later
}
}
// Diamond
for (var x = 0; x < 2 * oldWidth - 1; x++)
for (var y = 0; y < 2 * oldWidth - 1; y++)
{
if (newHeightmap[x][y] === undefined)
{
if (x > 0 && x + 1 < newHeightmap.length - 1 && y > 0 && y + 1 < newHeightmap.length - 1) // Not a border tile
{
newHeightmap[x][y] = (newHeightmap[x+1][y] + newHeightmap[x][y+1] + newHeightmap[x-1][y] + newHeightmap[x][y-1]) / 4;
newHeightmap[x][y] += (newHeightmap[x][y] - minHeight) / heightRange * randFloat(-offset, offset);
}
else if (x < newHeightmap.length - 1 && y > 0 && y < newHeightmap.length - 1) // Left border
{
newHeightmap[x][y] = (newHeightmap[x+1][y] + newHeightmap[x][y+1] + newHeightmap[x][y-1]) / 3;
newHeightmap[x][y] += (newHeightmap[x][y] - minHeight) / heightRange * randFloat(-offset, offset);
}
else if (x > 0 && y > 0 && y < newHeightmap.length - 1) // Right border
{
newHeightmap[x][y] = (newHeightmap[x][y+1] + newHeightmap[x-1][y] + newHeightmap[x][y-1]) / 3;
newHeightmap[x][y] += (newHeightmap[x][y] - minHeight) / heightRange * randFloat(-offset, offset);
}
else if (x > 0 && x < newHeightmap.length - 1 && y < newHeightmap.length - 1) // Bottom border
{
newHeightmap[x][y] = (newHeightmap[x+1][y] + newHeightmap[x][y+1] + newHeightmap[x-1][y]) / 3;
newHeightmap[x][y] += (newHeightmap[x][y] - minHeight) / heightRange * randFloat(-offset, offset);
}
else if (x > 0 && x < newHeightmap.length - 1 && y > 0) // Top border
{
newHeightmap[x][y] = (newHeightmap[x+1][y] + newHeightmap[x-1][y] + newHeightmap[x][y-1]) / 3;
newHeightmap[x][y] += (newHeightmap[x][y] - minHeight) / heightRange * randFloat(-offset, offset);
}
}
}
initialHeightmap = deepcopy(newHeightmap);
offset /= Math.pow(2, smoothness);
}
// Cut initialHeightmap to fit target width
var shift = [floor((newHeightmap.length - heightmap.length) / 2), floor((newHeightmap[0].length - heightmap[0].length) / 2)];
for (var x = 0; x < heightmap.length; x++)
for (var y = 0; y < heightmap[0].length; y++)
heightmap[x][y] = newHeightmap[x + shift[0]][y + shift[1]];
}
////////////////
//
// Terrain erosion functionality
//
////////////////
function decayErrodeHeightmap(strength, heightmap)
{
strength = (strength || 0.9); // 0 to 1
heightmap = (heightmap || g_Map.height);
var referenceHeightmap = deepcopy(heightmap);
// var map = [[1, 0], [0, 1], [-1, 0], [0, -1]]; // faster
var map = [[1, 0], [1, 1], [0, 1], [-1, 1], [-1, 0], [-1, -1], [0, -1], [1, -1]]; // smoother
var max_x = heightmap.length;
var max_y = heightmap[0].length;
for (var x = 0; x < max_x; x++)
for (var y = 0; y < max_y; y++)
for (var i = 0; i < map.length; i++)
heightmap[x][y] += strength / map.length * (referenceHeightmap[(x + map[i][0] + max_x) % max_x][(y + map[i][1] + max_y) % max_y] - referenceHeightmap[x][y]); // Not entirely sure if scaling with map.length is perfect but tested values seam to indicate it is
}
function rectangularSmoothToHeight(center, dx, dy, targetHeight, strength, heightmap)
{
var x = round(center[0]);
var y = round(center[1]);
dx = round(dx);
dy = round(dy);
strength = (strength || 1);
heightmap = (heightmap || g_Map.height);
var heightmapWin = [];
for (var wx = 0; wx < 2 * dx + 1; wx++)
{
heightmapWin.push([]);
for (var wy = 0; wy < 2 * dy + 1; wy++)
{
var actualX = x - dx + wx;
var actualY = y - dy + wy;
if (actualX >= 0 && actualX < heightmap.length - 1 && actualY >= 0 && actualY < heightmap[0].length - 1) // Is in map
heightmapWin[wx].push(heightmap[actualX][actualY]);
else
heightmapWin[wx].push(targetHeight);
}
}
for (var wx = 0; wx < 2 * dx + 1; wx++)
for (var wy = 0; wy < 2 * dy + 1; wy++)
{
var actualX = x - dx + wx;
var actualY = y - dy + wy;
if (actualX >= 0 && actualX < heightmap.length - 1 && actualY >= 0 && actualY < heightmap[0].length - 1) // Is in map
{
// Window function polynomial 2nd degree
var scaleX = 1 - (wx / dx - 1) * (wx / dx - 1);
var scaleY = 1 - (wy / dy - 1) * (wy / dy - 1);
heightmap[actualX][actualY] = heightmapWin[wx][wy] + strength * scaleX * scaleY * (targetHeight - heightmapWin[wx][wy]);
}
}
}
////////////////
//
// Actually do stuff
//
////////////////
////////////////
// Set height limits and water level by map size
@ -571,10 +193,10 @@ setWaterHeight(waterHeight);
// Setting a 3x3 Grid as initial heightmap
var initialReliefmap = [[heightRange.max, heightRange.max, heightRange.max], [heightRange.max, heightRange.min, heightRange.max], [heightRange.max, heightRange.max, heightRange.max]];
setBaseTerrainDiamondSquare(heightRange.min, heightRange.max, 0.5, initialReliefmap, g_Map.height);
setBaseTerrainDiamondSquare(heightRange.min, heightRange.max, initialReliefmap);
// Apply simple erosion
for (var i = 0; i < 5; i++)
decayErrodeHeightmap(0.5);
globalSmoothHeightmap();
rescaleHeightmap(heightRange.min, heightRange.max);
RMS.SetProgress(50);
@ -642,8 +264,8 @@ for (var i=0; i < numPlayers; i++)
}
// Add further stone and metal mines
derivateEntitiesByHeight({'min': heighLimits[3], 'max': ((heighLimits[4]+heighLimits[3])/2)}, startLocations);
derivateEntitiesByHeight({'min': ((heighLimits[5]+heighLimits[6])/2), 'max': heighLimits[7]}, startLocations);
distributeEntitiesByHeight({ 'min': heighLimits[3], 'max': ((heighLimits[4] + heighLimits[3]) / 2) }, startLocations, 40);
distributeEntitiesByHeight({ 'min': ((heighLimits[5] + heighLimits[6]) / 2), 'max': heighLimits[7] }, startLocations, 40);
RMS.SetProgress(50);