Index: ps/trunk/binaries/data/mods/public/maps/random/belgian_uplands.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/belgian_uplands.js (revision 18140) +++ ps/trunk/binaries/data/mods/public/maps/random/belgian_uplands.js (revision 18141) @@ -1,552 +1,460 @@ // Prepare progress calculation var timeArray = []; timeArray.push(new Date().getTime()); // Importing rmgen libraries RMS.LoadLibrary("rmgen"); +RMS.LoadLibrary("heightmap"); const BUILDING_ANGlE = -PI/4; // initialize map log("Initializing map..."); InitMap(); 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; for (var x = 0; x <= mapSize; x++) for (var y = 0; y <= mapSize; y++) 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}; // Set average water coverage var averageWaterCoverage = 1/3; // NOTE: Since errosion is not predictable actual water coverage might differ much with the same value if (mapSize < 200) // Sink the waterlevel on tiny maps to ensure enough space averageWaterCoverage = 2/3 * averageWaterCoverage; var waterHeight = -MIN_HEIGHT + heightRange.min + averageWaterCoverage * (heightRange.max - heightRange.min); var waterHeightAdjusted = waterHeight + MIN_HEIGHT; setWaterHeight(waterHeight); ////////// // Prepare terrain texture by height placement ////////// var textueByHeight = []; // Deep water textueByHeight.push({"upperHeightLimit": heightRange.min + 1/3 * (waterHeightAdjusted - heightRange.min), "terrain": "temp_sea_rocks"}); // Medium deep water (with fish) var terrains = ["temp_sea_weed"]; terrains = terrains.concat(terrains, terrains, terrains, terrains); terrains = terrains.concat(terrains, terrains, terrains, terrains); terrains.push("temp_sea_weed|gaia/fauna_fish"); textueByHeight.push({"upperHeightLimit": heightRange.min + 2/3 * (waterHeightAdjusted - heightRange.min), "terrain": terrains}); // Flat Water textueByHeight.push({"upperHeightLimit": heightRange.min + 3/3 * (waterHeightAdjusted - heightRange.min), "terrain": "temp_mud_a"}); // Water surroundings/bog (with stone/metal some rabits and bushes) var terrains = ["temp_plants_bog", "temp_plants_bog_aut", "temp_dirt_gravel_plants", "temp_grass_d"]; terrains = terrains.concat(terrains, terrains, terrains, terrains, terrains); terrains = ["temp_plants_bog|gaia/flora_bush_temperate"].concat(terrains, terrains); terrains = ["temp_dirt_gravel_plants|gaia/geology_metal_temperate", "temp_dirt_gravel_plants|gaia/geology_stone_temperate", "temp_plants_bog|gaia/fauna_rabbit"].concat(terrains, terrains); terrains = ["temp_plants_bog_aut|gaia/flora_tree_dead"].concat(terrains, terrains); textueByHeight.push({"upperHeightLimit": waterHeightAdjusted + 1/6 * (heightRange.max - waterHeightAdjusted), "terrain": terrains}); // Juicy grass near bog textueByHeight.push({"upperHeightLimit": waterHeightAdjusted + 2/6 * (heightRange.max - waterHeightAdjusted), "terrain": ["temp_grass", "temp_grass_d", "temp_grass_long_b", "temp_grass_plants"]}); // Medium level grass // var testActor = "actor|geology/decal_stone_medit_a.xml"; textueByHeight.push({"upperHeightLimit": waterHeightAdjusted + 3/6 * (heightRange.max - waterHeightAdjusted), "terrain": ["temp_grass", "temp_grass_b", "temp_grass_c", "temp_grass_mossy"]}); // Long grass near forest border textueByHeight.push({"upperHeightLimit": waterHeightAdjusted + 4/6 * (heightRange.max - waterHeightAdjusted), "terrain": ["temp_grass", "temp_grass_b", "temp_grass_c", "temp_grass_d", "temp_grass_long_b", "temp_grass_clovers_2", "temp_grass_mossy", "temp_grass_plants"]}); // Forest border (With wood/food plants/deer/rabits) var terrains = ["temp_grass_plants|gaia/flora_tree_euro_beech", "temp_grass_mossy|gaia/flora_tree_poplar", "temp_grass_mossy|gaia/flora_tree_poplar_lombardy", "temp_grass_long|gaia/flora_bush_temperate", "temp_mud_plants|gaia/flora_bush_temperate", "temp_mud_plants|gaia/flora_bush_badlands", "temp_grass_long|gaia/flora_tree_apple", "temp_grass_clovers|gaia/flora_bush_berry", "temp_grass_clovers_2|gaia/flora_bush_grapes", "temp_grass_plants|gaia/fauna_deer", "temp_grass_long_b|gaia/fauna_rabbit"]; var numTerrains = terrains.length; for (var i = 0; i < numTerrains; i++) terrains.push("temp_grass_plants"); textueByHeight.push({"upperHeightLimit": waterHeightAdjusted + 5/6 * (heightRange.max - waterHeightAdjusted), "terrain": terrains}); // Unpassable woods textueByHeight.push({"upperHeightLimit": waterHeightAdjusted + 6/6 * (heightRange.max - waterHeightAdjusted), "terrain": ["temp_grass_mossy|gaia/flora_tree_oak", "temp_forestfloor_pine|gaia/flora_tree_pine", "temp_grass_mossy|gaia/flora_tree_oak", "temp_forestfloor_pine|gaia/flora_tree_pine", "temp_mud_plants|gaia/flora_tree_dead", "temp_plants_bog|gaia/flora_tree_oak_large", "temp_dirt_gravel_plants|gaia/flora_tree_aleppo_pine", "temp_forestfloor_autumn|gaia/flora_tree_carob"]}); var minTerrainDistToBorder = 3; // Time check 1 timeArray.push(new Date().getTime()); RMS.SetProgress(5); // START THE GIANT WHILE LOOP: // - Generate Heightmap // - Search valid start position tiles // - Choose a good start position derivation (largest distance between closest players) // - Restart the loop if start positions are invalid or two players are to close to each other var goodStartPositionsFound = false; var minDistBetweenPlayers = 16 + mapSize / 16; // Don't set this higher than 25 for tiny maps! It will take forever with 8 players! var enoughTiles = false; var tries = 0; while (!goodStartPositionsFound) { tries++; 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 var startPositions = []; var possibleStartPositions = []; var neededDistance = 7; var distToBorder = 2 * neededDistance; // Has to be greater than neededDistance! Otherwise the check if low/high ground is near will fail... var lowerHeightLimit = textueByHeight[3].upperHeightLimit; var upperHeightLimit = textueByHeight[6].upperHeightLimit; // Check for valid points by height for (var x = distToBorder + minTerrainDistToBorder; x < mapSize - distToBorder - minTerrainDistToBorder; x++) for (var y = distToBorder + minTerrainDistToBorder; y < mapSize - distToBorder - minTerrainDistToBorder; y++) { var actualHeight = getHeight(x, y); if (actualHeight > lowerHeightLimit && actualHeight < upperHeightLimit) { // Check for points within a valid area by height (rectangular since faster) var isPossible = true; for (var offX = - neededDistance; offX <= neededDistance; offX++) for (var offY = - neededDistance; offY <= neededDistance; offY++) { var testHeight = getHeight(x + offX, y + offY); if (testHeight <= lowerHeightLimit || testHeight >= upperHeightLimit) { isPossible = false; break; } } if (isPossible) possibleStartPositions.push([x, y]); // placeTerrain(x, y, "blue"); // For debug reasons. Plz don't remove. // Only works properly for 1 loop } } // Trying to reduce the number of possible start locations... // Reduce to tiles in a circle of mapSize / 2 distance to the center (to avoid players placed in corners) var possibleStartPositionsTemp = []; var maxDistToCenter = mapSize / 2; for (var i = 0; i < possibleStartPositions.length; i++) { var deltaX = possibleStartPositions[i][0] - mapSize / 2; var deltaY = possibleStartPositions[i][1] - mapSize / 2; var distToCenter = Math.pow(Math.pow(deltaX, 2) + Math.pow(deltaY, 2), 1/2); if (distToCenter < maxDistToCenter) possibleStartPositionsTemp.push(possibleStartPositions[i]); // placeTerrain(possibleStartPositions[i][0], possibleStartPositions[i][1], "purple"); // Only works properly for 1 loop } possibleStartPositions = deepcopy(possibleStartPositionsTemp); // Reduce to tiles near low and high ground (Rectangular check since faster) to make sure each player has access to all resource types. var possibleStartPositionsTemp = []; var maxDistToResources = distToBorder; // Has to be <= distToBorder! var minNumLowTiles = 10; var minNumHighTiles = 10; for (var i = 0; i < possibleStartPositions.length; i++) { var numLowTiles = 0; var numHighTiles = 0; for (var dx = - maxDistToResources; dx < maxDistToResources; dx++) { for (var dy = - maxDistToResources; dy < maxDistToResources; dy++) { var testHeight = getHeight(possibleStartPositions[i][0] + dx, possibleStartPositions[i][1] + dy); if (testHeight < lowerHeightLimit) numLowTiles++; if (testHeight > upperHeightLimit) numHighTiles++; if (numLowTiles > minNumLowTiles && numHighTiles > minNumHighTiles) break; } if (numLowTiles > minNumLowTiles && numHighTiles > minNumHighTiles) break; } if (numLowTiles > minNumLowTiles && numHighTiles > minNumHighTiles) possibleStartPositionsTemp.push(possibleStartPositions[i]); // placeTerrain(possibleStartPositions[i][0], possibleStartPositions[i][1], "red"); // Only works properly for 1 loop } possibleStartPositions = deepcopy(possibleStartPositionsTemp); if(possibleStartPositions.length > numPlayers) enoughTiles = true; else { enoughTiles = false; log("possibleStartPositions.length < numPlayers, possibleStartPositions.length = " + possibleStartPositions.length + ", numPlayers = " + numPlayers); } // Find a good start position derivation if (enoughTiles) { // Get some random start location derivations. NOTE: Itterating over all possible derivations is just to much (valid points ** numPlayers) var maxTries = 100000; // floor(800000 / (Math.pow(numPlayers, 2) / 2)); var possibleDerivations = []; for (var i = 0; i < maxTries; i++) { var vector = []; for (var p = 0; p < numPlayers; p++) vector.push(randInt(possibleStartPositions.length)); possibleDerivations.push(vector); } // Choose the start location derivation with the greatest minimum distance between players var maxMinDist = 0; for (var d = 0; d < possibleDerivations.length; d++) { var minDist = 2 * mapSize; for (var p1 = 0; p1 < numPlayers - 1; p1++) { for (var p2 = p1 + 1; p2 < numPlayers; p2++) { if (p1 != p2) { var StartPositionP1 = possibleStartPositions[possibleDerivations[d][p1]]; var StartPositionP2 = possibleStartPositions[possibleDerivations[d][p2]]; var actualDist = Math.pow(Math.pow(StartPositionP1[0] - StartPositionP2[0], 2) + Math.pow(StartPositionP1[1] - StartPositionP2[1], 2), 1/2); if (actualDist < minDist) minDist = actualDist; if (minDist < maxMinDist) break; } } if (minDist < maxMinDist) break; } if (minDist > maxMinDist) { maxMinDist = minDist; var bestDerivation = possibleDerivations[d]; } } if (maxMinDist > minDistBetweenPlayers) { goodStartPositionsFound = true; log("Exiting giant while loop after " + tries + " tries with a minimum player distance of " + maxMinDist); } else log("maxMinDist <= " + minDistBetweenPlayers + ", maxMinDist = " + maxMinDist); } // End of derivation check } // END THE GIANT WHILE LOOP // Time check 2 timeArray.push(new Date().getTime()); RMS.SetProgress(60); //////// // Paint terrain by height and add props //////// var propDensity = 1; // 1 means as determined in the loop, less for large maps as set below if (mapSize > 500) propDensity = 1/4; else if (mapSize > 400) propDensity = 3/4; for(var x = minTerrainDistToBorder; x < mapSize - minTerrainDistToBorder; x++) { for (var y = minTerrainDistToBorder; y < mapSize - minTerrainDistToBorder; y++) { var textureMinHeight = heightRange.min; for (var i = 0; i < textueByHeight.length; i++) { if (getHeight(x, y) >= textureMinHeight && getHeight(x, y) <= textueByHeight[i].upperHeightLimit) { placeTerrain(x, y, textueByHeight[i].terrain); // Add some props at... if (i == 0) // ...deep water { if (randFloat() < 1/100 * propDensity) placeObject(x, y, "actor|props/flora/pond_lillies_large.xml", 0, randFloat(0, 2*PI)); else if (randFloat() < 1/40 * propDensity) placeObject(x, y, "actor|props/flora/water_lillies.xml", 0, randFloat(0, 2*PI)); } if (i == 1) // ...medium water (with fish) { if (randFloat() < 1/200 * propDensity) placeObject(x, y, "actor|props/flora/pond_lillies_large.xml", 0, randFloat(0, 2*PI)); else if (randFloat() < 1/100 * propDensity) placeObject(x, y, "actor|props/flora/water_lillies.xml", 0, randFloat(0, 2*PI)); } if (i == 2) // ...low water/mud { if (randFloat() < 1/200 * propDensity) placeObject(x, y, "actor|props/flora/water_log.xml", 0, randFloat(0, 2*PI)); else if (randFloat() < 1/100 * propDensity) placeObject(x, y, "actor|props/flora/water_lillies.xml", 0, randFloat(0, 2*PI)); else if (randFloat() < 1/40 * propDensity) placeObject(x, y, "actor|geology/highland_c.xml", 0, randFloat(0, 2*PI)); else if (randFloat() < 1/20 * propDensity) placeObject(x, y, "actor|props/flora/reeds_pond_lush_b.xml", 0, randFloat(0, 2*PI)); else if (randFloat() < 1/10 * propDensity) placeObject(x, y, "actor|props/flora/reeds_pond_lush_a.xml", 0, randFloat(0, 2*PI)); } if (i == 3) // ...water suroundings/bog { if (randFloat() < 1/200 * propDensity) placeObject(x, y, "actor|props/flora/water_log.xml", 0, randFloat(0, 2*PI)); else if (randFloat() < 1/100 * propDensity) placeObject(x, y, "actor|geology/highland_c.xml", 0, randFloat(0, 2*PI)); else if (randFloat() < 1/40 * propDensity) placeObject(x, y, "actor|props/flora/reeds_pond_lush_a.xml", 0, randFloat(0, 2*PI)); } if (i == 4) // ...low height grass { if (randFloat() < 1/800 * propDensity) placeObject(x, y, "actor|props/flora/grass_field_flowering_tall.xml", 0, randFloat(0, 2*PI)); else if (randFloat() < 1/400 * propDensity) placeObject(x, y, "actor|geology/gray_rock1.xml", 0, randFloat(0, 2*PI)); else if (randFloat() < 1/200 * propDensity) placeObject(x, y, "actor|props/flora/bush_tempe_sm_lush.xml", 0, randFloat(0, 2*PI)); else if (randFloat() < 1/100 * propDensity) placeObject(x, y, "actor|props/flora/bush_tempe_b.xml", 0, randFloat(0, 2*PI)); else if (randFloat() < 1/40 * propDensity) placeObject(x, y, "actor|props/flora/grass_soft_small_tall.xml", 0, randFloat(0, 2*PI)); } if (i == 5) // ...medium height grass { if (randFloat() < 1/800 * propDensity) placeObject(x, y, "actor|geology/decal_stone_medit_a.xml", 0, randFloat(0, 2*PI)); else if (randFloat() < 1/400 * propDensity) placeObject(x, y, "actor|props/flora/decals_flowers_daisies.xml", 0, randFloat(0, 2*PI)); else if (randFloat() < 1/200 * propDensity) placeObject(x, y, "actor|props/flora/bush_tempe_underbrush.xml", 0, randFloat(0, 2*PI)); else if (randFloat() < 1/100 * propDensity) placeObject(x, y, "actor|props/flora/grass_soft_small_tall.xml", 0, randFloat(0, 2*PI)); else if (randFloat() < 1/40 * propDensity) placeObject(x, y, "actor|props/flora/grass_temp_field.xml", 0, randFloat(0, 2*PI)); } if (i == 6) // ...high height grass { if (randFloat() < 1/400 * propDensity) placeObject(x, y, "actor|geology/stone_granite_boulder.xml", 0, randFloat(0, 2*PI)); else if (randFloat() < 1/200 * propDensity) placeObject(x, y, "actor|props/flora/foliagebush.xml", 0, randFloat(0, 2*PI)); else if (randFloat() < 1/100 * propDensity) placeObject(x, y, "actor|props/flora/bush_tempe_underbrush.xml", 0, randFloat(0, 2*PI)); else if (randFloat() < 1/40 * propDensity) placeObject(x, y, "actor|props/flora/grass_soft_small_tall.xml", 0, randFloat(0, 2*PI)); else if (randFloat() < 1/20 * propDensity) placeObject(x, y, "actor|props/flora/ferns.xml", 0, randFloat(0, 2*PI)); } if (i == 7) // ...forest border (with wood/food plants/deer/rabits) { if (randFloat() < 1/400 * propDensity) placeObject(x, y, "actor|geology/highland_c.xml", 0, randFloat(0, 2*PI)); else if (randFloat() < 1/200 * propDensity) placeObject(x, y, "actor|props/flora/bush_tempe_a.xml", 0, randFloat(0, 2*PI)); else if (randFloat() < 1/100 * propDensity) placeObject(x, y, "actor|props/flora/ferns.xml", 0, randFloat(0, 2*PI)); else if (randFloat() < 1/40 * propDensity) placeObject(x, y, "actor|props/flora/grass_soft_tuft_a.xml", 0, randFloat(0, 2*PI)); } if (i == 8) // ...woods { if (randFloat() < 1/200 * propDensity) placeObject(x, y, "actor|geology/highland2_moss.xml", 0, randFloat(0, 2*PI)); else if (randFloat() < 1/100 * propDensity) placeObject(x, y, "actor|props/flora/grass_soft_tuft_a.xml", 0, randFloat(0, 2*PI)); else if (randFloat() < 1/40 * propDensity) placeObject(x, y, "actor|props/flora/ferns.xml", 0, randFloat(0, 2*PI)); } break; } else textureMinHeight = textueByHeight[i].upperHeightLimit; } } } // Time check 3 timeArray.push(new Date().getTime()); RMS.SetProgress(90); //////// // Place players and start resources //////// for (var p = 0; p < numPlayers; p++) { var actualX = possibleStartPositions[bestDerivation[p]][0]; var actualY = possibleStartPositions[bestDerivation[p]][1]; placeCivDefaultEntities(actualX, actualY, p + 1, BUILDING_ANGlE, {"iberWall" : false}); // Place some start resources var uDist = 8; var uSpace = 1; for (var j = 1; j <= 4; ++j) { var uAngle = BUILDING_ANGlE - PI * (2-j) / 2; var count = 4; for (var numberofentities = 0; numberofentities < count; numberofentities++) { var ux = actualX + uDist * cos(uAngle) + numberofentities * uSpace * cos(uAngle + PI/2) - (0.75 * uSpace * floor(count / 2) * cos(uAngle + PI/2)); var uz = actualY + uDist * sin(uAngle) + numberofentities * uSpace * sin(uAngle + PI/2) - (0.75 * uSpace * floor(count / 2) * sin(uAngle + PI/2)); if (j % 2 == 0) placeObject(ux, uz, "gaia/flora_bush_berry", 0, randFloat(0, 2*PI)); else placeObject(ux, uz, "gaia/flora_tree_cypress", 0, randFloat(0, 2*PI)); } } } // Export map data ExportMap(); // Time check 7 timeArray.push(new Date().getTime()); // Calculate progress percentage with the time checks var generationTime = timeArray[timeArray.length - 1] - timeArray[0]; log("Total generation time (ms): " + generationTime); for (var i = 0; i < timeArray.length; i++) { var timeSinceStart = timeArray[i] - timeArray[0]; var progressPercentage = 100 * timeSinceStart / generationTime; log("Time check " + i + ": Progress (%): " + progressPercentage); } Index: ps/trunk/binaries/data/mods/public/maps/random/heightmap/heightmap.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/heightmap/heightmap.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/maps/random/heightmap/heightmap.js (revision 18141) @@ -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]); + } + } + } +} Index: ps/trunk/binaries/data/mods/public/maps/random/island_stronghold.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/island_stronghold.js (revision 18140) +++ ps/trunk/binaries/data/mods/public/maps/random/island_stronghold.js (revision 18141) @@ -1,604 +1,589 @@ -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. */ function getPlayerTileCoordinates(playerIdx, teamIdx, fractionX, fractionZ) { let playerAngle = startAngle + (playerIdx+1) * TWO_PI / teams[teamIdx].length; let fx = fractionToTiles(fractionX + 0.05 * cos(playerAngle)); let fz = fractionToTiles(fractionZ + 0.05 * sin(playerAngle)); return [playerAngle, fx, fz, round(fx), round(fz)]; } RMS.LoadLibrary("rmgen"); +RMS.LoadLibrary("heightmap"); const g_InitialMines = 1; const g_InitialMineDistance = 14; const g_InitialTrees = 50; let random_terrain = randomizeBiome([g_BiomeSavanna]); const tMainTerrain = rBiomeT1(); const tForestFloor1 = rBiomeT2(); const tForestFloor2 = rBiomeT3(); const tCliff = rBiomeT4(); const tTier1Terrain = rBiomeT5(); const tTier2Terrain = rBiomeT6(); const tTier3Terrain = rBiomeT7(); const tHill = rBiomeT8(); const tTier4Terrain = rBiomeT12(); const tShore = rBiomeT14(); const tWater = rBiomeT15(); // gaia entities const oTree1 = rBiomeE1(); const oTree2 = rBiomeE2(); const oTree3 = rBiomeE3(); const oTree4 = rBiomeE4(); const oTree5 = rBiomeE5(); const oFruitBush = rBiomeE6(); const oChicken = rBiomeE7(); const oMainHuntableAnimal = rBiomeE8(); const oFish = rBiomeE9(); const oSecondaryHuntableAnimal = rBiomeE10(); const oStoneLarge = rBiomeE11(); const oStoneSmall = rBiomeE12(); const oMetalLarge = rBiomeE13(); const oWhale = "gaia/fauna_whale_humpback"; const oShipwreck = "other/special_treasure_shipwreck"; const oShipDebris = "other/special_treasure_shipwreck_debris"; const oObelisk = "other/obelisk"; // decorative props const aGrass = rBiomeA1(); const aGrassShort = rBiomeA2(); const aRockLarge = rBiomeA5(); const aRockMedium = rBiomeA6(); const pForest1 = [tForestFloor2 + TERRAIN_SEPARATOR + oTree1, tForestFloor2 + TERRAIN_SEPARATOR + oTree2, tForestFloor2]; const pForest2 = [tForestFloor1 + TERRAIN_SEPARATOR + oTree4, tForestFloor1 + TERRAIN_SEPARATOR + oTree5, tForestFloor1]; const BUILDING_ANGlE = -PI/4; log("Initializing map..."); InitMap(); const numPlayers = getNumPlayers(); const mapSize = getMapSize(); // create tile classes let clPlayer = createTileClass(); let clHill = createTileClass(); let clForest = createTileClass(); let clDirt = createTileClass(); let clRock = createTileClass(); let clMetal = createTileClass(); let clFood = createTileClass(); let clBaseResource = createTileClass(); let clLand = createTileClass(); for (let ix = 0; ix < mapSize; ++ix) for (let iz = 0; iz < mapSize; ++iz) placeTerrain(ix, iz, tWater); // some constants let radius = scaleByMapSize(15, 25); let fx = fractionToTiles(0.5); let fz = fractionToTiles(0.5); let startAngle = randFloat(0, TWO_PI); // Group players by team let teams = []; for (let i = 0; i < numPlayers; ++i) { let team = getPlayerTeam(i); if (team == -1) continue; if (!teams[team]) teams[team] = []; teams[team].push(i+1); } // Players without a team get a custom index for (let i = 0; i < numPlayers; ++i) { let team = getPlayerTeam(i); if (team != -1) continue; let unusedIndex = teams.findIndex(team => !team); if (unusedIndex != -1) teams[unusedIndex] = [i+1]; else teams.push([i+1]); } // Get number of used team IDs let numTeams = teams.filter(team => team).length; RMS.SetProgress(10); let shoreRadius = 6; let elevation = 3; let teamNo = 0; for (let i = 0; i < teams.length; ++i) { if (!teams[i]) continue; ++teamNo; let teamAngle = startAngle + teamNo*TWO_PI/numTeams; let fractionX = 0.5 + 0.3 * cos(teamAngle); let fractionZ = 0.5 + 0.3 * sin(teamAngle); let teamX = fractionToTiles(fractionX); let teamZ = fractionToTiles(fractionZ); log("Creating island and starting entities for team " + i); for (let p = 0; p < teams[i].length; ++p) { let [playerAngle, fx, fz, ix, iz] = getPlayerTileCoordinates(p, i, fractionX, fractionZ); // mark a small area around the player's starting coordinates with the clPlayer class addToClass(ix, iz, clPlayer); addToClass(ix+5, iz, clPlayer); addToClass(ix, iz+5, clPlayer); addToClass(ix-5, iz, clPlayer); addToClass(ix, iz-5, clPlayer); // create an island let placer = new ChainPlacer(2, floor(scaleByMapSize(5, 11)), floor(scaleByMapSize(60, 250)), 1, ix, iz, 0, [floor(mapSize * 0.01)]); let terrainPainter = new LayeredPainter( [tMainTerrain, tMainTerrain, tMainTerrain], // terrains [1, shoreRadius] // widths ); let elevationPainter = new SmoothElevationPainter( ELEVATION_SET, // type elevation, // elevation shoreRadius // blend radius ); createArea(placer, [terrainPainter, elevationPainter, paintClass(clLand)], null); // create starting units placeCivDefaultEntities(fx, fz, teams[i][p], BUILDING_ANGlE, { "iberWall": false }); } log("Create initial mines for team " + i); for (let p = 0; p < teams[i].length; ++p) { let [playerAngle, fx, fz, ix, iz] = getPlayerTileCoordinates(p, i, fractionX, fractionZ); let mAngle = randFloat(playerAngle - PI / teams[i].length, playerAngle + PI / teams[i].length); // Metal let mX = round(fx + g_InitialMineDistance * cos(mAngle)); let mZ = round(fz + g_InitialMineDistance * sin(mAngle)); let group = new SimpleGroup( [new SimpleObject(oMetalLarge, g_InitialMines, g_InitialMines, 0, 4)], true, clBaseResource, mX, mZ ); createObjectGroup(group, 0, [avoidClasses(clBaseResource, 2, clPlayer, 4), stayClasses(clLand, 2)]); // Stone let sX = round(fx + g_InitialMineDistance * cos(mAngle + PI/4)); let sZ = round(fz + g_InitialMineDistance * sin(mAngle + PI/4)); group = new SimpleGroup( [new SimpleObject(oStoneLarge, g_InitialMines, g_InitialMines, 0, 4)], true, clBaseResource, sX, sZ ); createObjectGroup(group, 0, [avoidClasses(clBaseResource, 2, clPlayer, 4), stayClasses(clLand, 2)]); } log("Place initial trees and animals for team " + i); for (let p = 0; p < teams[i].length; ++p) { let [playerAngle, fx, fz, ix, iz] = getPlayerTileCoordinates(p, i, fractionX, fractionZ); // create initial chicken for (let j = 0; j < 2; ++j) { let aAngle = randFloat(0, TWO_PI); let aDist = 7; let aX = round(fx + aDist * cos(aAngle)); let aZ = round(fz + aDist * sin(aAngle)); let group = new SimpleGroup( [new SimpleObject(oChicken, 5, 5, 0, 2)], true, clBaseResource, aX, aZ ); createObjectGroup(group, 0, [stayClasses(clLand, 5)]); } // create initial berry bushes let bbAngle = randFloat(PI, PI*1.5); let bbDist = 10; let bbX = round(fx + bbDist * cos(bbAngle)); let bbZ = round(fz + bbDist * sin(bbAngle)); let group = new SimpleGroup( [new SimpleObject(oFruitBush, 5, 5, 0, 3)], true, clBaseResource, bbX, bbZ ); createObjectGroup(group, 0, [avoidClasses(clBaseResource, 4, clPlayer, 4), stayClasses(clLand, 5)]); // create initial trees let tries = 10; let tDist = 16; for (let x = 0; x < tries; ++x) { let tAngle = randFloat(playerAngle - TWO_PI/teams[i].length, playerAngle + TWO_PI/teams[i].length); let tX = round(fx + tDist * cos(tAngle)); let tZ = round(fz + tDist * sin(tAngle)); group = new SimpleGroup( [new SimpleObject(oTree2, g_InitialTrees, g_InitialTrees, 0, 7)], true, clBaseResource, tX, tZ ); if (createObjectGroup(group, 0, [avoidClasses(clBaseResource, 4, clPlayer, 4), stayClasses(clLand, 4)])) break; } // create huntable animals group = new SimpleGroup( [new SimpleObject(oMainHuntableAnimal, 2 * numPlayers / numTeams, 2 * numPlayers / numTeams, 0, floor(mapSize * 0.2))], true, clBaseResource, teamX, teamZ ); createObjectGroup(group, 0, [avoidClasses(clBaseResource, 2, clHill, 1, clPlayer, 10), stayClasses(clLand, 5)]); group = new SimpleGroup( [new SimpleObject(oSecondaryHuntableAnimal, 4 * numPlayers / numTeams, 4 * numPlayers / numTeams, 0, floor(mapSize * 0.2))], true, clBaseResource, teamX, teamZ ); createObjectGroup(group, 0, [avoidClasses(clBaseResource, 2, clHill, 1, clPlayer, 10), stayClasses(clLand, 5)]); } } RMS.SetProgress(40); log("Creating expansion islands..."); let landAreas = []; let playerConstraint = new AvoidTileClassConstraint(clPlayer, floor(scaleByMapSize(12, 16))); let landConstraint = new AvoidTileClassConstraint(clLand, floor(scaleByMapSize(12, 16))); for (let x = 0; x < mapSize; ++x) for (let z = 0; z < mapSize; ++z) if (playerConstraint.allows(x, z) && landConstraint.allows(x, z)) landAreas.push([x, z]); log("Creating big islands..."); let chosenPoint; let landAreaLen; let numIslands = scaleByMapSize(4, 14); for (let i = 0; i < numIslands; ++i) { landAreaLen = landAreas.length; if (!landAreaLen) break; chosenPoint = landAreas[randInt(landAreaLen)]; // create big islands let placer = new ChainPlacer(floor(scaleByMapSize(4, 8)), floor(scaleByMapSize(8, 14)), floor(scaleByMapSize(25, 60)), 0.07, chosenPoint[0], chosenPoint[1], scaleByMapSize(30, 70)); let terrainPainter = new LayeredPainter( [tMainTerrain, tMainTerrain], // terrains [2] // widths ); let elevationPainter = new SmoothElevationPainter(ELEVATION_SET, 3, 6); let newIsland = createAreas( placer, [terrainPainter, elevationPainter, paintClass(clLand)], avoidClasses(clLand, 3, clPlayer, 3), 1, 1 ); if (!newIsland || !newIsland.length) continue; let n = 0; for (let j = 0; j < landAreaLen; ++j) { let x = landAreas[j][0]; let z = landAreas[j][1]; if (playerConstraint.allows(x, z) && landConstraint.allows(x, z)) landAreas[n++] = landAreas[j]; } landAreas.length = n; } playerConstraint = new AvoidTileClassConstraint(clPlayer, floor(scaleByMapSize(9, 12))); landConstraint = new AvoidTileClassConstraint(clLand, floor(scaleByMapSize(9, 12))); log("Creating small islands..."); numIslands = scaleByMapSize(6, 18) * scaleByMapSize(1, 3); for (let i = 0; i < numIslands; ++i) { landAreaLen = landAreas.length; if (!landAreaLen) break; chosenPoint = landAreas[randInt(0, landAreaLen)]; let placer = new ChainPlacer(floor(scaleByMapSize(4, 7)), floor(scaleByMapSize(7, 10)), floor(scaleByMapSize(16, 40)), 0.07, chosenPoint[0], chosenPoint[1], scaleByMapSize(22, 40)); let terrainPainter = new LayeredPainter( [tMainTerrain, tMainTerrain], // terrains [2] // widths ); let elevationPainter = new SmoothElevationPainter(ELEVATION_SET, 3, 6); let newIsland = createAreas( placer, [terrainPainter, elevationPainter, paintClass(clLand)], avoidClasses(clLand, 3, clPlayer, 3), 1, 1 ); if (newIsland === undefined) continue; let temp = []; for (let j = 0; j < landAreaLen; ++j) { let x = landAreas[j][0]; let z = landAreas[j][1]; if (playerConstraint.allows(x, z) && landConstraint.allows(x, z)) temp.push([x, z]); } landAreas = temp; } 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); paintTileClassBasedOnHeight(0, 5, 3, clLand); RMS.SetProgress(85); createBumps(); createMines( [ [new SimpleObject(oMetalLarge, 1, 1, 3, (numPlayers * 2) + 1)] ], [avoidClasses(clForest, 1, clPlayer, 40, clRock, 20, clHill, 5), stayClasses(clLand, 4)], clMetal ); createMines( [ [new SimpleObject(oStoneLarge, 1, 1, 3, (numPlayers * 2) + 1)], [new SimpleObject(oStoneSmall, 2, 2, 2, (numPlayers * 2) + 1)] ], [avoidClasses(clForest, 1, clPlayer, 40, clMetal, 20, clHill, 5), stayClasses(clLand, 4)], clRock ); createForests( [tMainTerrain, tForestFloor1, tForestFloor2, pForest1, pForest2], [avoidClasses(clPlayer, 10, clForest, 20, clHill, 10, clBaseResource, 5, clRock, 4, clMetal, 4), stayClasses(clLand, 3)], clForest, 1.0, random_terrain ); log("Creating hills..."); let placer = new ChainPlacer(1, floor(scaleByMapSize(4, 6)), floor(scaleByMapSize(16, 40)), 0.5); let painter = new LayeredPainter( [tCliff, tHill], // terrains [2] // widths ); let elevationPainter = new SmoothElevationPainter(ELEVATION_SET, 18, 2); createAreas( placer, [painter, elevationPainter, paintClass(clHill)], [avoidClasses(clBaseResource, 20, clHill, 15, clRock, 4, clMetal, 4), stayClasses(clLand, 0)], scaleByMapSize(4, 13) ); for (let i = 0; i < 3; ++i) - decayErrodeHeightmap(0.2); + globalSmoothHeightmap(); createStragglerTrees( [oTree1, oTree2, oTree4, oTree3], [avoidClasses(clForest, 10, clPlayer, 20, clMetal, 1, clRock, 1, clHill, 1), stayClasses(clLand, 4)] ); createFood( [ [new SimpleObject(oMainHuntableAnimal, 5, 7, 0, 4)], [new SimpleObject(oSecondaryHuntableAnimal, 2, 3, 0, 2)] ], [3 * numPlayers, 3 * numPlayers], [avoidClasses(clForest, 0, clPlayer, 20, clHill, 1, clRock, 4, clMetal, 4), stayClasses(clLand, 2)] ); createFood( [ [new SimpleObject(oFruitBush, 5, 7, 0, 4)] ], [3 * numPlayers], [avoidClasses(clForest, 0, clPlayer, 15, clHill, 1, clFood, 4, clRock, 4, clMetal, 4), stayClasses(clLand, 2)] ); if (random_terrain == g_BiomeDesert) { log("Creating obelisks"); let group = new SimpleGroup( [new SimpleObject(oObelisk, 1, 1, 0, 1)], true ); createObjectGroups( group, 0, [avoidClasses(clBaseResource, 0, clHill, 0, clRock, 0, clMetal, 0, clFood, 0), stayClasses(clLand, 1)], scaleByMapSize(3, 8), 1000 ); } log("Creating dirt patches..."); let sizes = [scaleByMapSize(3, 6), scaleByMapSize(5, 10), scaleByMapSize(8, 21)]; let numb = random_terrain == g_BiomeSavanna ? 3 : 1; for (let i = 0; i < sizes.length; ++i) { placer = new ChainPlacer(1, floor(scaleByMapSize(3, 5)), sizes[i], 0.5); painter = new LayeredPainter( [[tMainTerrain,tTier1Terrain], [tTier1Terrain,tTier2Terrain], [tTier2Terrain,tTier3Terrain]], // terrains [1, 1] // widths ); createAreas( placer, [painter, paintClass(clDirt)], [avoidClasses(clForest, 0, clHill, 0, clDirt, 5, clPlayer, 0), stayClasses(clLand, 4)], numb*scaleByMapSize(15, 45) ); } log("Creating grass patches..."); sizes = [scaleByMapSize(2, 4), scaleByMapSize(3, 7), scaleByMapSize(5, 15)]; for (let i = 0; i < sizes.length; ++i) { placer = new ChainPlacer(1, floor(scaleByMapSize(3, 5)), sizes[i], 0.5); painter = new TerrainPainter(tTier4Terrain); createAreas( placer, painter, [avoidClasses(clForest, 0, clHill, 0, clDirt, 5, clPlayer, 0), stayClasses(clLand, 4)], numb * scaleByMapSize(15, 45) ); } log("Creating small decorative rocks..."); let group = new SimpleGroup( [new SimpleObject(aRockMedium, 1, 3, 0, 1)], true ); createObjectGroups( group, 0, [avoidClasses(clForest, 0, clHill, 0), stayClasses(clLand, 2)], scaleByMapSize(16, 262), 50 ); log("Creating large decorative rocks..."); group = new SimpleGroup( [new SimpleObject(aRockLarge, 1, 2, 0, 1), new SimpleObject(aRockMedium, 1, 3, 0, 2)], true ); createObjectGroups( group, 0, [avoidClasses(clForest, 0, clHill, 0), stayClasses(clLand, 2)], scaleByMapSize(8, 131), 50 ); log("Creating fish..."); group = new SimpleGroup( [new SimpleObject(oFish, 2, 3, 0, 2)], true, clFood ); createObjectGroups(group, 0, avoidClasses(clLand, 4, clFood, 20), 25 * numPlayers, 60 ); log("Creating Whales..."); group = new SimpleGroup( [new SimpleObject(oWhale, 1, 1, 0, 3)], true, clFood ); createObjectGroups(group, 0, [avoidClasses(clLand, 4),avoidClasses(clFood, 8)], scaleByMapSize(5, 20), 100 ); log("Creating shipwrecks..."); group = new SimpleGroup( [new SimpleObject(oShipwreck, 1, 1, 0, 1)], true, clFood ); createObjectGroups(group, 0, [avoidClasses(clLand, 4),avoidClasses(clFood, 8)], scaleByMapSize(12, 16), 100 ); log("Creating shipwreck debris..."); group = new SimpleGroup( [new SimpleObject(oShipDebris, 1, 1, 0, 1)], true, clFood ); createObjectGroups(group, 0, [avoidClasses(clLand, 4),avoidClasses(clFood, 8)], scaleByMapSize(10, 20), 100 ); log("Creating grass tufts..."); let num = (PI * radius * radius) / 250; for (let j = 0; j < num; ++j) { let gAngle = randFloat(0, TWO_PI); let gDist = radius - (5 + randInt(7)); let gX = round(fx + gDist * cos(gAngle)); let gZ = round(fz + gDist * sin(gAngle)); group = new SimpleGroup( [new SimpleObject(aGrassShort, 2, 5, 0, 1, -PI / 8, PI / 8)], false, clBaseResource, gX, gZ ); createObjectGroup(group, 0, [stayClasses(clLand, 5)]); } log("Creating small grass tufts..."); let planetm = random_terrain == 7 ? 8 : 1; group = new SimpleGroup( [new SimpleObject(aGrassShort, 1, 2, 0, 1, -PI / 8, PI / 8)] ); createObjectGroups(group, 0, [avoidClasses(clHill, 2, clPlayer, 2, clDirt, 0), stayClasses(clLand, 3)], planetm * scaleByMapSize(13, 200) ); RMS.SetProgress(95); log("Creating large grass tufts..."); group = new SimpleGroup( [new SimpleObject(aGrass, 2, 4, 0, 1.8, -PI / 8, PI / 8), new SimpleObject(aGrassShort, 3, 6, 1.2,2.5, -PI / 8, PI / 8)] ); createObjectGroups(group, 0, [avoidClasses(clHill, 2, clPlayer, 2, clDirt, 1, clForest, 0), stayClasses(clLand, 5)], planetm * scaleByMapSize(13, 200) ); paintTerrainBasedOnHeight(1, 2, 0, tShore); paintTerrainBasedOnHeight(getMapBaseHeight(), 1, 3, tWater); setSkySet(shuffleArray(["cloudless", "cumulus", "overcast"])[0]); setSunRotation(randFloat(0, TWO_PI)); setSunElevation(randFloat(PI/5, PI/3)); setWaterWaviness(2); RMS.SetProgress(100); ExportMap(); Index: ps/trunk/binaries/data/mods/public/maps/random/rmgen/library.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/rmgen/library.js (revision 18140) +++ ps/trunk/binaries/data/mods/public/maps/random/rmgen/library.js (revision 18141) @@ -1,549 +1,555 @@ const PI = Math.PI; const TWO_PI = 2 * Math.PI; const TERRAIN_SEPARATOR = "|"; const SEA_LEVEL = 20.0; const CELL_SIZE = 4; 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) { return g_Map.size * f; } function tilesToFraction(t) { return t / g_Map.size; } function fractionToSize(f) { return getMapArea() * f; } function sizeToFraction(s) { return s / getMapArea(); } function scaleByMapSize(min, max) { return min + (max - min) * (g_Map.size - MIN_MAP_SIZE) / (MAX_MAP_SIZE - MIN_MAP_SIZE); } function cos(x) { return Math.cos(x); } function sin(x) { return Math.sin(x); } function abs(x) { return Math.abs(x); } function round(x) { return Math.round(x); } function lerp(a, b, t) { return a + (b-a) * t; } function sqrt(x) { return Math.sqrt(x); } function ceil(x) { return Math.ceil(x); } function floor(x) { return Math.floor(x); } function max(a, b) { return a > b ? a : b; } function min(a, b) { return a < b ? a : b; } /** * "Inside-out" implementation of Fisher-Yates shuffle */ function shuffleArray(source) { if (!source.length) return []; let result = [source[0]]; for (let i = 1; i < source.length; ++i) { let j = randInt(0, i); result[i] = result[j]; result[j] = source[i]; } return result; } /** * Retries the given function with those arguments as often as specified. */ function retryPlacing(placeFunc, placeArgs, retryFactor, amount, getResult) { let maxFail = amount * retryFactor; let results = []; let good = 0; let bad = 0; while (good < amount && bad <= maxFail) { let result = placeFunc(placeArgs); if (result !== undefined) { ++good; if (getResult) results.push(result); } else ++bad; } return getResult ? results : good; } /** * Helper function for randomly placing areas and groups on the map. */ function randomizePlacerCoordinates(placer, halfMapSize) { if (!!g_MapSettings.CircularMap) { // Polar coordinates let r = halfMapSize * Math.sqrt(randFloat()); // uniform distribution let theta = randFloat(0, 2 * PI); placer.x = Math.floor(r * Math.cos(theta)) + halfMapSize; placer.z = Math.floor(r * Math.sin(theta)) + halfMapSize; } else { // Rectangular coordinates placer.x = randInt(g_Map.size); placer.z = randInt(g_Map.size); } } /** * Helper function for randomly placing areas and groups in the given areas. */ function randomizePlacerCoordinatesFromAreas(placer, areas) { let i = randInt(areas.length); let pt = areas[i].points[randInt(areas[i].points.length)]; placer.x = pt.x; placer.z = pt.z; } /** * Attempts to place the given number of areas in random places of the map. * Returns actually placed areas. */ function createAreas(centeredPlacer, painter, constraint, amount, retryFactor = 10) { let placeFunc = function (args) { randomizePlacerCoordinates(args.placer, args.halfMapSize); return g_Map.createArea(args.placer, args.painter, args.constraint); }; let args = { "placer": centeredPlacer, "painter": painter, "constraint": constraint, "halfMapSize": g_Map.size / 2 }; return retryPlacing(placeFunc, args, retryFactor, amount, true); } /** * Attempts to place the given number of areas in random places of the given areas. * Returns actually placed areas. */ function createAreasInAreas(centeredPlacer, painter, constraint, amount, retryFactor, areas) { if (!areas.length) return []; let placeFunc = function (args) { randomizePlacerCoordinatesFromAreas(args.placer, args.areas); return g_Map.createArea(args.placer, args.painter, args.constraint); }; let args = { "placer": centeredPlacer, "painter": painter, "constraint": constraint, "areas": areas, "halfMapSize": g_Map.size / 2 }; return retryPlacing(placeFunc, args, retryFactor, amount, true); } /** * Attempts to place the given number of groups in random places of the map. * Returns the number of actually placed groups. */ function createObjectGroups(placer, player, constraint, amount, retryFactor = 10) { let placeFunc = function (args) { randomizePlacerCoordinates(args.placer, args.halfMapSize); return createObjectGroup(args.placer, args.player, args.constraint); }; let args = { "placer": placer, "player": player, "constraint": constraint, "halfMapSize": g_Map.size / 2 - 3 }; return retryPlacing(placeFunc, args, retryFactor, amount, false); } /** * Attempts to place the given number of groups in random places of the given areas. * Returns the number of actually placed groups. */ function createObjectGroupsByAreas(placer, player, constraint, amount, retryFactor, areas) { if (!areas.length) return 0; let placeFunc = function (args) { randomizePlacerCoordinatesFromAreas(args.placer, args.areas); return createObjectGroup(args.placer, args.player, args.constraint); }; let args = { "placer": placer, "player": player, "constraint": constraint, "areas": areas }; return retryPlacing(placeFunc, args, retryFactor, amount, false); } function createTerrain(terrain) { if (!(terrain instanceof Array)) return createSimpleTerrain(terrain); return new RandomTerrain(terrain.map(t => createTerrain(t))); } function createSimpleTerrain(terrain) { if (typeof(terrain) != "string") throw("createSimpleTerrain expects string as input, received "+terrain); // Split string by pipe | character, this allows specifying terrain + tree type in single string let params = terrain.split(TERRAIN_SEPARATOR, 2); if (params.length != 2) return new SimpleTerrain(terrain); return new SimpleTerrain(params[0], params[1]); } function placeObject(x, z, type, player, angle) { if (g_Map.validT(x, z, 3)) g_Map.addObject(new Entity(type, player, x, z, angle)); } function placeTerrain(x, z, terrain) { // convert terrain param into terrain object g_Map.placeTerrain(x, z, createTerrain(terrain)); } function isCircularMap() { return !!g_MapSettings.CircularMap; } function getMapBaseHeight() { return g_MapSettings.BaseHeight || 0; } function createTileClass() { return g_Map.createTileClass(); } function getTileClass(id) { if (!g_Map.validClass(id)) return undefined; return g_Map.tileClasses[id]; } function createArea(placer, painter, constraint) { return g_Map.createArea(placer, painter, constraint); } function createObjectGroup(placer, player, constraint) { return g_Map.createObjectGroup(placer, player, constraint); } function getMapSize() { return g_Map.size; } function getMapArea() { return g_Map.size * g_Map.size; } function getNumPlayers() { return g_MapSettings.PlayerData.length - 1; } function getCivCode(player) { if (g_MapSettings.PlayerData[player+1].Civ) return g_MapSettings.PlayerData[player+1].Civ; warn("undefined civ specified for player " + (player + 1) + ", falling back to '" + FALLBACK_CIV + "'"); return FALLBACK_CIV; } function areAllies(player1, player2) { if (g_MapSettings.PlayerData[player1+1].Team === undefined || g_MapSettings.PlayerData[player2+1].Team === undefined || g_MapSettings.PlayerData[player2+1].Team == -1 || g_MapSettings.PlayerData[player1+1].Team == -1) return false; return g_MapSettings.PlayerData[player1+1].Team === g_MapSettings.PlayerData[player2+1].Team; } function getPlayerTeam(player) { if (g_MapSettings.PlayerData[player+1].Team === undefined) return -1; return g_MapSettings.PlayerData[player+1].Team; } /** * Sorts an array of player IDs by team index. Players without teams come first. * Randomize order for players of the same team. */ function sortPlayers(playerIndices) { return shuffleArray(playerIndices).sort((p1, p2) => getPlayerTeam(p1 - 1) - getPlayerTeam(p2 - 1)); } function primeSortPlayers(playerIndices) { if (!playerIndices.length) return []; let prime = []; for (let i = 0; i < Math.ceil(playerIndices.length / 2); ++i) { prime.push(playerIndices[i]); prime.push(playerIndices[playerIndices.length - 1 - i]); } return prime; } function getStartingEntities(player) { let civ = getCivCode(player); if (!g_CivData[civ] || !g_CivData[civ].StartEntities || !g_CivData[civ].StartEntities.length) { warn("Invalid or unimplemented civ '"+civ+"' specified, falling back to '" + FALLBACK_CIV + "'"); civ = FALLBACK_CIV; } return g_CivData[civ].StartEntities; } function getHeight(x, z) { return g_Map.getHeight(x, z); } function setHeight(x, z, height) { g_Map.setHeight(x, z, height); } /** * Utility functions for classes */ /** * Add point to given class by id */ function addToClass(x, z, id) { let tileClass = getTileClass(id); if (tileClass !== null) tileClass.add(x, z); } /** * Remove point from the given class by id */ function removeFromClass(x, z, id) { let tileClass = getTileClass(id); if (tileClass !== null) tileClass.remove(x, z); } /** * Create a painter for the given class */ function paintClass(id) { return new TileClassPainter(getTileClass(id)); } /** * Create a painter for the given class */ function unPaintClass(id) { return new TileClassUnPainter(getTileClass(id)); } /** * Create an avoid constraint for the given classes by the given distances */ function avoidClasses(/*class1, dist1, class2, dist2, etc*/) { let ar = []; for (let i = 0; i < arguments.length/2; ++i) ar.push(new AvoidTileClassConstraint(arguments[2*i], arguments[2*i+1])); // Return single constraint if (ar.length == 1) return ar[0]; return new AndConstraint(ar); } /** * Create a stay constraint for the given classes by the given distances */ function stayClasses(/*class1, dist1, class2, dist2, etc*/) { let ar = []; for (let i = 0; i < arguments.length/2; ++i) ar.push(new StayInTileClassConstraint(arguments[2*i], arguments[2*i+1])); // Return single constraint if (ar.length == 1) return ar[0]; return new AndConstraint(ar); } /** * Create a border constraint for the given classes by the given distances */ function borderClasses(/*class1, idist1, odist1, class2, idist2, odist2, etc*/) { let ar = []; for (let i = 0; i < arguments.length/3; ++i) ar.push(new BorderTileClassConstraint(arguments[3*i], arguments[3*i+1], arguments[3*i+2])); // Return single constraint if (ar.length == 1) return ar[0]; return new AndConstraint(ar); } /** * Checks if the given tile is in class "id" */ function checkIfInClass(x, z, id) { let tileClass = getTileClass(id); if (tileClass === null) return 0; let members = tileClass.countMembersInRadius(x, z, 1); if (members === null) return 0; return members; } /** * Returns the distance between 2 points */ function getDistance(x1, z1, x2, z2) { return Math.pow(Math.pow(x1 - x2, 2) + Math.pow(z1 - z2, 2), 1/2); } /** * Returns the angle of the vector between point 1 and point 2. * The angle is counterclockwise from the positive x axis. */ function getAngle(x1, z1, x2, z2) { return Math.atan2(z2 - z1, x2 - x1); } /** * Returns the gradient of the line between point 1 and 2 in the form dz/dx */ function getGradient(x1, z1, x2, z2) { if (x1 == x2 && z1 == z2) return 0; return (z1-z2)/(x1-x2); } function getTerrainTexture(x, y) { return g_Map.getTexture(x, y); } Index: ps/trunk/binaries/data/mods/public/maps/random/schwarzwald.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/schwarzwald.js (revision 18140) +++ ps/trunk/binaries/data/mods/public/maps/random/schwarzwald.js (revision 18141) @@ -1,834 +1,456 @@ -// 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...'); InitMap(); //////////////// // // Initializing // //////////////// //sky setSkySet("fog"); setFogFactor(0.35); setFogThickness(0.19); // water setWaterColor(0.501961, 0.501961, 0.501961); setWaterTint(0.25098, 0.501961, 0.501961); setWaterWaviness(0.5); setWaterType("clap"); setWaterMurkiness(0.75); // post processing setPPSaturation(0.37); setPPContrast(0.4); setPPBrightness(0.4); setPPEffect("hdr"); setPPBloom(0.4); // Setup tile classes var clPlayer = createTileClass(); var clPath = createTileClass(); var clHill = createTileClass(); var clForest = createTileClass(); var clWater = createTileClass(); var clRock = createTileClass(); var clFood = createTileClass(); var clBaseResource = createTileClass(); var clOpen = createTileClass(); // Setup Templates var templateStone = 'gaia/geology_stone_alpine_a'; var templateStoneMine = 'gaia/geology_stonemine_alpine_quarry'; var templateMetal = 'actor|geology/stone_granite_med.xml'; var templateMetalMine = 'gaia/geology_metal_alpine_slabs'; var startingResources = ['gaia/flora_tree_pine', 'gaia/flora_tree_pine','gaia/flora_tree_pine', templateStoneMine, 'gaia/flora_bush_grapes', 'gaia/flora_tree_aleppo_pine','gaia/flora_tree_aleppo_pine','gaia/flora_tree_aleppo_pine', 'gaia/flora_bush_berry', templateMetalMine]; var aGrass = 'actor|props/flora/grass_soft_small_tall.xml'; var aGrassShort = 'actor|props/flora/grass_soft_large.xml'; var aRockLarge = 'actor|geology/stone_granite_med.xml'; var aRockMedium = 'actor|geology/stone_granite_med.xml'; var aBushMedium = 'actor|props/flora/bush_medit_me.xml'; var aBushSmall = 'actor|props/flora/bush_medit_sm.xml'; var aReeds = 'actor|props/flora/reeds_pond_lush_b.xml'; var oFish = "gaia/fauna_fish"; // Setup terrain var terrainWood = ['alpine_forrestfloor|gaia/flora_tree_oak', 'alpine_forrestfloor|gaia/flora_tree_pine']; var terrainWoodBorder = ['new_alpine_grass_mossy|gaia/flora_tree_oak', 'alpine_forrestfloor|gaia/flora_tree_pine', 'temp_grass_long|gaia/flora_bush_temperate', 'temp_grass_clovers|gaia/flora_bush_berry', 'temp_grass_clovers_2|gaia/flora_bush_grapes', 'temp_grass_plants|gaia/fauna_deer', 'temp_grass_plants|gaia/fauna_rabbit', 'new_alpine_grass_dirt_a']; var terrainBase = ['temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants', 'temp_grass_plants|gaia/fauna_sheep']; var terrainBaseBorder = ['temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants']; var terrainBaseCenter = ['temp_dirt_gravel_b']; var baseTex = ['temp_road', 'temp_road_overgrown']; var terrainPath = ['temp_road', 'temp_road_overgrown']; var terrainHill = ['temp_highlands', 'temp_highlands', 'temp_highlands', 'temp_grass_plants_b', 'temp_cliff_a']; var terrainHillBorder = ['temp_highlands', 'temp_highlands', 'temp_highlands', 'temp_grass_plants_b', 'temp_grass_plants_plants', 'temp_highlands', 'temp_highlands', 'temp_highlands', 'temp_grass_plants_b', 'temp_grass_plants_plants', 'temp_highlands', 'temp_highlands', 'temp_highlands', 'temp_cliff_b', 'temp_grass_plants_plants', 'temp_highlands', 'temp_highlands', 'temp_highlands', 'temp_cliff_b', 'temp_grass_plants_plants', 'temp_highlands|gaia/fauna_goat']; var tWater = ['dirt_brown_d']; var tWaterBorder = ['dirt_brown_d']; const BUILDING_ANGlE = -PI/4; // Setup map var mapSize = getMapSize(); var mapRadius = mapSize/2; var playableMapRadius = mapRadius - 5; var mapCenterX = mapRadius; var mapCenterZ = mapRadius; // Setup players and bases var numPlayers = getNumPlayers(); var baseRadius = 15; var minPlayerRadius = min(mapRadius-1.5*baseRadius, 5*mapRadius/8); var maxPlayerRadius = min(mapRadius-baseRadius, 3*mapRadius/4); var playerStartLocX = new Array(numPlayers); var playerStartLocZ = new Array(numPlayers); var playerAngle = new Array(numPlayers); var playerAngleStart = randFloat(0, 2*PI); var playerAngleAddAvrg = 2*PI / numPlayers; var playerAngleMaxOff = playerAngleAddAvrg/4; // Setup paths var pathSucsessRadius = baseRadius/2; var pathAngleOff = PI/2; var pathWidth = 10; // This is not really the path's thickness in tiles but the number of tiles in the clumbs of the path // Setup additional resources var resourceRadius = 2*mapRadius/3; // 3*mapRadius/8; //var resourcePerPlayer = [templateStone, templateMetalMine]; // Setup woods // For large maps there are memory errors with too many trees. A density of 256*192/mapArea works with 0 players. // Around each player there is an area without trees so with more players the max density can increase a bit. var maxTreeDensity = min(256 * (192 + 8 * numPlayers) / (mapSize * mapSize), 1); // Has to be tweeked but works ok var bushChance = 1/3; // 1 means 50% chance in deepest wood, 0.5 means 25% chance in deepest wood //////////////// // // Some general functions // //////////////// function HeightPlacer(lowerBound, upperBound) { this.lowerBound = lowerBound; this.upperBound = upperBound; } HeightPlacer.prototype.place = function (constraint) { constraint = (constraint || new NullConstraint()); var ret = []; for (var x = 0; x < g_Map.size; x++) { for (var y = 0; y < g_Map.size; y++) { if (g_Map.height[x][y] >= this.lowerBound && g_Map.height[x][y] <= this.upperBound && constraint.allows(x, y)) { ret.push(new PointXZ(x, y)); } } } 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 //////////////// // Set target min and max height depending on map size to make average steepness about the same on all map sizes var heightRange = {'min': MIN_HEIGHT * (g_Map.size + 512) / 8192, 'max': MAX_HEIGHT * (g_Map.size + 512) / 8192, 'avg': (MIN_HEIGHT * (g_Map.size + 512) +MAX_HEIGHT * (g_Map.size + 512))/16384}; // Set average water coverage var averageWaterCoverage = 1/5; // NOTE: Since erosion is not predictable actual water coverage might vary much with the same values var waterHeight = -MIN_HEIGHT + heightRange.min + averageWaterCoverage * (heightRange.max - heightRange.min); var waterHeightAdjusted = waterHeight + MIN_HEIGHT; setWaterHeight(waterHeight); //////////////// // Generate base terrain //////////////// // 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); ////////// // Setup height limit ////////// // Height presets var heighLimits = [ heightRange.min + 1/3 * (waterHeightAdjusted - heightRange.min), // 0 Deep water heightRange.min + 2/3 * (waterHeightAdjusted - heightRange.min), // 1 Medium Water heightRange.min + (waterHeightAdjusted - heightRange.min), // 2 Shallow water waterHeightAdjusted + 1/8 * (heightRange.max - waterHeightAdjusted), // 3 Shore waterHeightAdjusted + 2/8 * (heightRange.max - waterHeightAdjusted), // 4 Low ground waterHeightAdjusted + 3/8 * (heightRange.max - waterHeightAdjusted), // 5 Player and path height waterHeightAdjusted + 4/8 * (heightRange.max - waterHeightAdjusted), // 6 High ground waterHeightAdjusted + 5/8 * (heightRange.max - waterHeightAdjusted), // 7 Lower forest border waterHeightAdjusted + 6/8 * (heightRange.max - waterHeightAdjusted), // 8 Forest waterHeightAdjusted + 7/8 * (heightRange.max - waterHeightAdjusted), // 9 Upper forest border waterHeightAdjusted + (heightRange.max - waterHeightAdjusted)]; // 10 Hilltop ////////// // Place start locations and apply terrain texture and decorative props ////////// // Get start locations var startLocations = getStartLocationsByHeightmap({'min': heighLimits[4], 'max': heighLimits[5]}); var playerHeight = (heighLimits[4] + heighLimits[5]) / 2; for (var i=0; i < numPlayers; i++) { playerAngle[i] = (playerAngleStart + i*playerAngleAddAvrg + randFloat(0, playerAngleMaxOff))%(2*PI); var x = round(mapCenterX + randFloat(minPlayerRadius, maxPlayerRadius)*cos(playerAngle[i])); var z = round(mapCenterZ + randFloat(minPlayerRadius, maxPlayerRadius)*sin(playerAngle[i])); playerStartLocX[i] = x; playerStartLocZ[i] = z; // Place starting entities rectangularSmoothToHeight([x,z] , 20, 20, playerHeight, 0.8); placeCivDefaultEntities(x, z, i+1, BUILDING_ANGlE, {'iberWall': false}); // Place base texture var placer = new ClumpPlacer(2*baseRadius*baseRadius, 2/3, 1/8, 10, x, z); var painter = [new TerrainPainter([baseTex], [baseRadius/4, baseRadius/4]), paintClass(clPlayer)]; createArea(placer, painter); // Place starting resources var distToSL = 15; var resStartAngle = playerAngle[i] + PI; var resAddAngle = 2*PI / startingResources.length; for (var rIndex = 0; rIndex < startingResources.length; rIndex++) { var angleOff = randFloat(-resAddAngle/2, resAddAngle/2); var placeX = x + distToSL*cos(resStartAngle + rIndex*resAddAngle + angleOff); var placeZ = z + distToSL*sin(resStartAngle + rIndex*resAddAngle + angleOff); placeObject(placeX, placeZ, startingResources[rIndex], 0, randFloat(0, 2*PI)); addToClass(round(placeX), round(placeZ), clBaseResource); } } // 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); //place water & open terrain textures and assign TileClasses log("Painting textures..."); var placer = new HeightPlacer(heighLimits[2], (heighLimits[3]+heighLimits[2])/2); var painter = new LayeredPainter([terrainBase, terrainBaseBorder], [5]); createArea(placer, painter); paintTileClassBasedOnHeight(heighLimits[2], (heighLimits[3]+heighLimits[2])/2, 1, clOpen); var placer = new HeightPlacer(heightRange.min, heighLimits[2]); var painter = new LayeredPainter([tWaterBorder, tWater], [2]); createArea(placer, painter); paintTileClassBasedOnHeight(heightRange.min, heighLimits[2], 1, clWater); RMS.SetProgress(60); // Place paths log("Placing paths..."); var doublePaths = true; if (numPlayers > 4) doublePaths = false; var doublePathMayPlayers = 4; if (doublePaths === true) var maxI = numPlayers+1; else var maxI = numPlayers; for (var i = 0; i < maxI; i++) { if (doublePaths === true) var minJ = 0; else var minJ = i+1; for (var j = minJ; j < numPlayers+1; j++) { // Setup start and target coordinates if (i < numPlayers) { var x = playerStartLocX[i]; var z = playerStartLocZ[i]; } else { var x = mapCenterX; var z = mapCenterZ; } if (j < numPlayers) { var targetX = playerStartLocX[j]; var targetZ = playerStartLocZ[j]; } else { var targetX = mapCenterX; var targetZ = mapCenterZ; } // Prepare path placement var angle = getAngle(x, z, targetX, targetZ); x += round(pathSucsessRadius*cos(angle)); z += round(pathSucsessRadius*sin(angle)); var targetReached = false; var tries = 0; // Placing paths while (targetReached === false && tries < 2*mapSize) { var placer = new ClumpPlacer(pathWidth, 1, 1, 1, x, z); var painter = [new TerrainPainter(terrainPath), new SmoothElevationPainter(ELEVATION_MODIFY, -0.1, 1.0), paintClass(clPath)]; createArea(placer, painter, avoidClasses(clPath, 0, clOpen, 0 ,clWater, 4, clBaseResource, 4)); // addToClass(x, z, clPath); // Not needed... // Set vars for next loop angle = getAngle(x, z, targetX, targetZ); if (doublePaths === true) // Bended paths { x += round(cos(angle + randFloat(-pathAngleOff/2, 3*pathAngleOff/2))); z += round(sin(angle + randFloat(-pathAngleOff/2, 3*pathAngleOff/2))); } else // Straight paths { x += round(cos(angle + randFloat(-pathAngleOff, pathAngleOff))); z += round(sin(angle + randFloat(-pathAngleOff, pathAngleOff))); } if (getDistance(x, z, targetX, targetZ) < pathSucsessRadius) targetReached = true; tries++; } } } RMS.SetProgress(75); //create general decoration log("Creating decoration..."); createDecoration ( [[new SimpleObject(aRockMedium, 1,3, 0,1)], [new SimpleObject(aRockLarge, 1,2, 0,1), new SimpleObject(aRockMedium, 1,3, 0,2)], [new SimpleObject(aGrassShort, 1,2, 0,1, -PI/8,PI/8)], [new SimpleObject(aGrass, 2,4, 0,1.8, -PI/8,PI/8), new SimpleObject(aGrassShort, 3,6, 1.2,2.5, -PI/8,PI/8)], [new SimpleObject(aBushMedium, 1,2, 0,2), new SimpleObject(aBushSmall, 2,4, 0,2)] ], [ scaleByMapSize(16, 262), scaleByMapSize(8, 131), scaleByMapSize(13, 200), scaleByMapSize(13, 200), scaleByMapSize(13, 200) ], avoidClasses(clForest, 1, clPlayer, 0, clPath, 3, clWater, 3) ); RMS.SetProgress(80); //create fish log("Growing fish..."); createFood ( [ [new SimpleObject(oFish, 2,3, 0,2)] ], [ 100 * numPlayers ], [avoidClasses(clFood, 5), stayClasses(clWater, 4)] ); RMS.SetProgress(85); // create reeds log("Planting reeds..."); var types = [aReeds]; // some variation for (var i = 0; i < types.length; ++i) { var group = new SimpleGroup([new SimpleObject(types[i], 1,1, 0,0)], true); createObjectGroups(group, 0, borderClasses(clWater, 0, 6), scaleByMapSize(960, 2000), 1000 ); } RMS.SetProgress(90); // place trees log("Planting trees..."); for (var x = 0; x < mapSize; x++) { for (var z = 0;z < mapSize;z++) { var radius = Math.pow(Math.pow(mapCenterX - x - 0.5, 2) + Math.pow(mapCenterZ - z - 0.5, 2), 1/2); // The 0.5 is a correction for the entities placed on the center of tiles var minDistToSL = mapSize; for (var i=0; i < numPlayers; i++) minDistToSL = min(minDistToSL, getDistance(playerStartLocX[i], playerStartLocZ[i], x, z)); // Woods tile based var tDensFactSL = max(min((minDistToSL - baseRadius) / baseRadius, 1), 0); var tDensFactRad = abs((resourceRadius - radius) / resourceRadius); var tDensActual = (maxTreeDensity * tDensFactSL * tDensFactRad)*0.75; if (randFloat() < tDensActual && radius < playableMapRadius) { if (tDensActual < bushChance*randFloat()*maxTreeDensity) { var placer = new ClumpPlacer(1, 1.0, 1.0, 1, x, z); var painter = [new TerrainPainter(terrainWoodBorder), paintClass(clForest)]; createArea(placer, painter, avoidClasses(clPath, 1, clOpen, 2, clWater,3)); } else { var placer = new ClumpPlacer(1, 1.0, 1.0, 1, x, z); var painter = [new TerrainPainter(terrainWood), paintClass(clForest)]; createArea(placer, painter, avoidClasses(clPath, 2, clOpen, 3, clWater, 4));} } } } RMS.SetProgress(100); ExportMap();