diff --git a/binaries/data/mods/public/maps/random/belgian_uplands.js b/binaries/data/mods/public/maps/random/belgian_uplands.js index f048c781f7..4474b0e9ec 100644 --- a/binaries/data/mods/public/maps/random/belgian_uplands.js +++ b/binaries/data/mods/public/maps/random/belgian_uplands.js @@ -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 diff --git a/binaries/data/mods/public/maps/random/heightmap/heightmap.js b/binaries/data/mods/public/maps/random/heightmap/heightmap.js new file mode 100644 index 0000000000..95daf7e8e6 --- /dev/null +++ b/binaries/data/mods/public/maps/random/heightmap/heightmap.js @@ -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]); + } + } + } +} diff --git a/binaries/data/mods/public/maps/random/island_stronghold.js b/binaries/data/mods/public/maps/random/island_stronghold.js index 323a02b457..630f5154cb 100644 --- a/binaries/data/mods/public/maps/random/island_stronghold.js +++ b/binaries/data/mods/public/maps/random/island_stronghold.js @@ -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], diff --git a/binaries/data/mods/public/maps/random/rmgen/library.js b/binaries/data/mods/public/maps/random/rmgen/library.js index c8a64385a6..5792dbe00d 100644 --- a/binaries/data/mods/public/maps/random/rmgen/library.js +++ b/binaries/data/mods/public/maps/random/rmgen/library.js @@ -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) { diff --git a/binaries/data/mods/public/maps/random/schwarzwald.js b/binaries/data/mods/public/maps/random/schwarzwald.js index c1410cafd6..ad79accb97 100644 --- a/binaries/data/mods/public/maps/random/schwarzwald.js +++ b/binaries/data/mods/public/maps/random/schwarzwald.js @@ -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);