Index: ps/trunk/binaries/data/mods/public/maps/random/caledonian_meadows.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/caledonian_meadows.js (revision 20416) +++ ps/trunk/binaries/data/mods/public/maps/random/caledonian_meadows.js (revision 20417) @@ -1,532 +1,458 @@ RMS.LoadLibrary("rmgen"); RMS.LoadLibrary("rmbiome"); RMS.LoadLibrary("heightmap"); InitMap(); let genStartTime = Date.now(); /** * Drags a path to a target height smoothing it at the edges and return some points along the path. */ function placeRandomPathToHeight( start, target, targetHeight, tileClass = undefined, texture = "road_rome_a", width = 10, distance = 4, strength = 0.08, heightmap = g_Map.height) { let pathPoints = []; let position = clone(start); while (true) { rectangularSmoothToHeight(position, width, width, targetHeight, strength, heightmap); if (texture) { let painters = [new TerrainPainter(texture)]; if (tileClass !== undefined) painters.push(paintClass(tileClass)); createArea( new ClumpPlacer(0.3 * Math.square(width), 1, 1, 1, Math.floor(position.x), Math.floor(position.y)), painters); } pathPoints.push({ "x": position.x, "y": position.y, "dist": distance }); // Check for distance to target and setup for next loop if needed if (Math.euclidDistance2D(position.x, position.y, target.x, target.y) < distance / 2) break; let angleToTarget = getAngle(position.x, position.y, target.x, target.y); let angleOff = randFloat(-PI/2, PI/2); position.x += distance * cos(angleToTarget + angleOff); position.y += distance * sin(angleToTarget + angleOff); } return pathPoints; } /** * Design resource spots */ // Mines let decorations = [ "actor|geology/gray1.xml", "actor|geology/gray_rock1.xml", "actor|geology/highland1.xml", "actor|geology/highland2.xml", "actor|geology/highland3.xml", "actor|geology/highland_c.xml", "actor|geology/highland_d.xml", "actor|geology/highland_e.xml", "actor|props/flora/bush.xml", "actor|props/flora/bush_dry_a.xml", "actor|props/flora/bush_highlands.xml", "actor|props/flora/bush_tempe_a.xml", "actor|props/flora/bush_tempe_b.xml", "actor|props/flora/ferns.xml" ]; function placeMine(point, centerEntity) { placeObject(point.x, point.y, centerEntity, 0, randFloat(0, TWO_PI)); let quantity = randIntInclusive(11, 23); let dAngle = TWO_PI / quantity; for (let i = 0; i < quantity; ++i) { let angle = dAngle * randFloat(i, i + 1); let dist = randFloat(2, 5); placeObject(point.x + dist * Math.cos(angle), point.y + dist * Math.sin(angle), pickRandom(decorations), 0, randFloat(0, 2 * PI)); } } // Food, fences with domestic animals wallStyles.other.sheepIn = new WallElement("sheepIn", "gaia/fauna_sheep", PI / 4, -1.5, 0.75, PI/2); wallStyles.other.foodBin = new WallElement("foodBin", "gaia/special_treasure_food_bin", PI/2, 1.5); wallStyles.other.sheep = new WallElement("sheep", "gaia/fauna_sheep", 0, 0, 0.75); wallStyles.other.farm = new WallElement("farm", "structures/brit_farmstead", PI, 0, -3); let fences = [ new Fortress("fence", ["foodBin", "farm", "bench", "sheepIn", "fence", "sheepIn", "fence", "sheepIn", "fence"]), new Fortress("fence", ["foodBin", "farm", "fence", "sheepIn", "fence", "sheepIn", "bench", "sheep", "fence", "sheepIn", "fence"]), new Fortress("fence", [ "foodBin", "farm", "cornerIn", "bench", "cornerOut", "fence_short", "sheepIn", "fence", "sheepIn", "fence", "sheepIn", "fence_short", "sheep", "fence" ]), new Fortress("fence", [ "foodBin", "farm", "cornerIn", "fence_short", "cornerOut", "bench", "sheepIn", "fence", "sheepIn", "fence", "sheepIn", "fence_short", "sheep", "fence" ]), new Fortress("fence", [ "foodBin", "farm", "fence", "sheepIn", "bench", "sheep", "fence", "sheepIn", "fence_short", "sheep", "fence", "sheepIn", "fence_short", "sheep", "fence" ]) ]; let num = fences.length; for (let i = 0; i < num; ++i) fences.push(new Fortress("fence", clone(fences[i].wall).reverse())); // Groves, only Wood let groveEntities = ["gaia/flora_bush_temperate", "gaia/flora_tree_euro_beech"]; let groveActors = [ "actor|geology/highland1_moss.xml", "actor|geology/highland2_moss.xml", "actor|props/flora/bush.xml", "actor|props/flora/bush_dry_a.xml", "actor|props/flora/bush_highlands.xml", "actor|props/flora/bush_tempe_a.xml", "actor|props/flora/bush_tempe_b.xml", "actor|props/flora/ferns.xml" ]; let clGrove = createTileClass(); function placeGrove(point) { placeObject(point.x, point.y, pickRandom(["structures/gaul_outpost", "gaia/flora_tree_oak_new"]), 0, randFloat(0, 2 * PI)); let quantity = randIntInclusive(20, 30); let dAngle = TWO_PI / quantity; for (let i = 0; i < quantity; ++i) { let angle = dAngle * randFloat(i, i + 1); let dist = randFloat(2, 5); let objectList = groveEntities; if (i % 3 == 0) objectList = groveActors; let x = point.x + dist * Math.cos(angle); let y = point.y + dist * Math.sin(angle); placeObject(x, y, pickRandom(objectList), 0, randFloat(0, 2 * PI)); createArea(new ClumpPlacer(5, 1, 1, 1, floor(x), floor(y)), [new TerrainPainter("temp_grass_plants"), paintClass(clGrove)]); } } // Camps with fire and gold treasure function placeCamp(point, centerEntity = "actor|props/special/eyecandy/campfire.xml", otherEntities = ["gaia/special_treasure_metal", "gaia/special_treasure_standing_stone", "units/brit_infantry_slinger_b", "units/brit_infantry_javelinist_b", "units/gaul_infantry_slinger_b", "units/gaul_infantry_javelinist_b", "units/gaul_champion_fanatic", "actor|props/special/common/waypoint_flag.xml", "actor|props/special/eyecandy/barrel_a.xml", "actor|props/special/eyecandy/basket_celt_a.xml", "actor|props/special/eyecandy/crate_a.xml", "actor|props/special/eyecandy/dummy_a.xml", "actor|props/special/eyecandy/handcart_1.xml", "actor|props/special/eyecandy/handcart_1_broken.xml", "actor|props/special/eyecandy/sack_1.xml", "actor|props/special/eyecandy/sack_1_rough.xml" ] ) { placeObject(point.x, point.y, centerEntity, 0, randFloat(0, TWO_PI)); let quantity = randIntInclusive(5, 11); let dAngle = TWO_PI / quantity; for (let i = 0; i < quantity; ++i) { let angle = dAngle * randFloat(i, i + 1); let dist = randFloat(1, 3); placeObject(point.x + dist * Math.cos(angle), point.y + dist * Math.sin(angle), pickRandom(otherEntities), 0, randFloat(0, 2 * PI)); } } function placeStartLocationResources(point, foodEntities = ["gaia/flora_bush_berry", "gaia/fauna_chicken", "gaia/fauna_chicken"]) { let currentAngle = randFloat(0, TWO_PI); // Stone and chicken let dAngle = TWO_PI * 2 / 9; let angle = currentAngle + randFloat(dAngle / 4, 3 * dAngle / 4); let dist = 12; let x = point.x + dist * Math.cos(angle); let y = point.y + dist * Math.sin(angle); placeMine({ "x": x, "y": y }, "gaia/geology_stonemine_temperate_quarry"); currentAngle += dAngle; // Wood let quantity = 80; dAngle = TWO_PI / quantity / 3; for (let i = 0; i < quantity; ++i) { angle = currentAngle + randFloat(0, dAngle); dist = randFloat(10, 15); let objectList = groveEntities; if (i % 2 == 0) objectList = groveActors; x = point.x + dist * Math.cos(angle); y = point.y + dist * Math.sin(angle); placeObject(x, y, pickRandom(objectList), 0, randFloat(0, 2 * PI)); createArea(new ClumpPlacer(5, 1, 1, 1, floor(x), floor(y)), [new TerrainPainter("temp_grass_plants"), paintClass(clGrove)]); currentAngle += dAngle; } // Metal and chicken dAngle = TWO_PI * 2 / 9; angle = currentAngle + randFloat(dAngle / 4, 3 * dAngle / 4); dist = 13; x = point.x + dist * Math.cos(angle); y = point.y + dist * Math.sin(angle); placeMine({ "x": x, "y": y }, "gaia/geology_metal_temperate_slabs"); currentAngle += dAngle; // Berries quantity = 15; dAngle = TWO_PI / quantity * 2 / 9; for (let i = 0; i < quantity; ++i) { angle = currentAngle + randFloat(0, dAngle); dist = randFloat(10, 15); x = point.x + dist * Math.cos(angle); y = point.y + dist * Math.sin(angle); placeObject(x, y, pickRandom(foodEntities), 0, randFloat(0, 2 * PI)); currentAngle += dAngle; } } log("Functions loaded after " + ((Date.now() - genStartTime) / 1000) + "s"); /** * Environment settings */ setBiome("alpine"); g_Environment.Fog.FogColor = { "r": 0.8, "g": 0.8, "b": 0.8, "a": 0.01 }; g_Environment.Water.WaterBody.Colour = { "r" : 0.3, "g" : 0.05, "b" : 0.1, "a" : 0.1 }; g_Environment.Water.WaterBody.Murkiness = 0.4; /** * Base terrain shape generation and settings */ // Height range by map size let heightScale = (g_Map.size + 256) / 768 / 4; let heightRange = { "min": MIN_HEIGHT * heightScale, "max": MAX_HEIGHT * heightScale }; // Water coverage let averageWaterCoverage = 1/5; // NOTE: Since terrain generation is quite unpredictable actual water coverage might vary much with the same value let waterHeight = -MIN_HEIGHT + heightRange.min + averageWaterCoverage * (heightRange.max - heightRange.min); // Water height in environment and the engine let waterHeightAdjusted = waterHeight + MIN_HEIGHT; // Water height in RMGEN setWaterHeight(waterHeight); // Generate base terrain shape let medH = (heightRange.min + heightRange.max) / 2; let initialHeightmap = [[medH, medH], [medH, medH]]; setBaseTerrainDiamondSquare(heightRange.min, heightRange.max, initialHeightmap, 0.8); // Apply simple erosion for (let i = 0; i < 5; ++i) splashErodeMap(0.1); // Final rescale rescaleHeightmap(heightRange.min, heightRange.max); RMS.SetProgress(25); /** * Prepare terrain texture placement */ let 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 let playerHeight = (heighLimits[4] + heighLimits[5]) / 2; // Average player height // Texture and actor presets let myBiome = []; myBiome.push({ // 0 Deep water "texture": ["shoreline_stoney_a"], "actor": [["gaia/fauna_fish", "actor|geology/stone_granite_boulder.xml"], 0.02], "textureHS": ["alpine_mountainside"], "actorHS": [["gaia/fauna_fish"], 0.1] }); myBiome.push({ // 1 Medium Water "texture": ["shoreline_stoney_a", "alpine_shore_rocks"], "actor": [["actor|geology/stone_granite_boulder.xml", "actor|geology/stone_granite_med.xml"], 0.03], "textureHS": ["alpine_mountainside"], "actorHS": [["actor|geology/stone_granite_boulder.xml", "actor|geology/stone_granite_med.xml"], 0.0] }); myBiome.push({ // 2 Shallow water "texture": ["alpine_shore_rocks"], "actor": [["actor|props/flora/reeds_pond_dry.xml", "actor|geology/stone_granite_large.xml", "actor|geology/stone_granite_med.xml", "actor|props/flora/reeds_pond_lush_b.xml"], 0.2], "textureHS": ["alpine_mountainside"], "actorHS": [["actor|props/flora/reeds_pond_dry.xml", "actor|geology/stone_granite_med.xml"], 0.1] }); myBiome.push({ // 3 Shore "texture": ["alpine_shore_rocks_grass_50", "alpine_grass_rocky"], "actor": [["gaia/flora_tree_pine", "gaia/flora_bush_badlands", "actor|geology/highland1_moss.xml", "actor|props/flora/grass_soft_tuft_a.xml", "actor|props/flora/bush.xml"], 0.3], "textureHS": ["alpine_mountainside"], "actorHS": [["actor|props/flora/grass_soft_tuft_a.xml"], 0.1] }); myBiome.push({ // 4 Low ground "texture": ["alpine_dirt_grass_50", "alpine_grass_rocky"], "actor": [["actor|geology/stone_granite_med.xml", "actor|props/flora/grass_soft_tuft_a.xml", "actor|props/flora/bush.xml", "actor|props/flora/grass_medit_flowering_tall.xml"], 0.2], "textureHS": ["alpine_grass_rocky"], "actorHS": [["actor|geology/stone_granite_med.xml", "actor|props/flora/grass_soft_tuft_a.xml"], 0.1] }); myBiome.push({ // 5 Player and path height "texture": ["new_alpine_grass_c", "new_alpine_grass_b", "new_alpine_grass_d"], "actor": [["actor|geology/stone_granite_small.xml", "actor|props/flora/grass_soft_small.xml", "actor|props/flora/grass_medit_flowering_tall.xml"], 0.2], "textureHS": ["alpine_grass_rocky"], "actorHS": [["actor|geology/stone_granite_small.xml", "actor|props/flora/grass_soft_small.xml"], 0.1] }); myBiome.push({ // 6 High ground "texture": ["new_alpine_grass_a", "alpine_grass_rocky"], "actor": [["actor|geology/stone_granite_med.xml", "actor|props/flora/grass_tufts_a.xml", "actor|props/flora/bush_highlands.xml", "actor|props/flora/grass_medit_flowering_tall.xml"], 0.2], "textureHS": ["alpine_grass_rocky"], "actorHS": [["actor|geology/stone_granite_med.xml", "actor|props/flora/grass_tufts_a.xml"], 0.1] }); myBiome.push({ // 7 Lower forest border "texture": ["new_alpine_grass_mossy", "alpine_grass_rocky"], "actor": [["gaia/flora_tree_pine", "gaia/flora_tree_oak", "actor|props/flora/grass_tufts_a.xml", "gaia/flora_bush_berry", "actor|geology/highland2_moss.xml", "gaia/fauna_goat", "actor|props/flora/bush_tempe_underbrush.xml"], 0.3], "textureHS": ["alpine_cliff_c"], "actorHS": [["actor|props/flora/grass_tufts_a.xml", "actor|geology/highland2_moss.xml"], 0.1] }); myBiome.push({ // 8 Forest "texture": ["alpine_forrestfloor"], "actor": [["gaia/flora_tree_pine", "gaia/flora_tree_pine", "gaia/flora_tree_pine", "gaia/flora_tree_pine", "actor|geology/highland2_moss.xml", "actor|props/flora/bush_highlands.xml"], 0.5], "textureHS": ["alpine_cliff_c"], "actorHS": [["actor|geology/highland2_moss.xml", "actor|geology/stone_granite_med.xml"], 0.1] }); myBiome.push({ // 9 Upper forest border "texture": ["alpine_forrestfloor_snow", "new_alpine_grass_dirt_a"], "actor": [["gaia/flora_tree_pine", "actor|geology/snow1.xml"], 0.3], "textureHS": ["alpine_cliff_b"], "actorHS": [["actor|geology/stone_granite_med.xml", "actor|geology/snow1.xml"], 0.1] }); myBiome.push({ // 10 Hilltop "texture": ["alpine_cliff_a", "alpine_cliff_snow"], "actor": [["actor|geology/highland1.xml"], 0.05], "textureHS": ["alpine_cliff_c"], "actorHS": [["actor|geology/highland1.xml"], 0.0] }); log("Terrain shape generation and texture presets after " + ((Date.now() - genStartTime) / 1000) + "s"); -/** - * Get start locations - */ -let startLocations = getStartLocationsByHeightmap({ "min": heighLimits[4], "max": heighLimits[5] }, 1000, 30); - -// Sort start locations to form a "ring" -let startLocationOrder = getOrderOfPointsForShortestClosePath(startLocations); -let newStartLocations = []; -for (let i = 0; i < startLocations.length; ++i) - newStartLocations.push(startLocations[startLocationOrder[i]]); -startLocations = newStartLocations; - -// Sort players by team -let playerIDs = []; -let teams = []; -for (let i = 0; i < g_MapSettings.PlayerData.length - 1; ++i) -{ - playerIDs.push(i+1); - let t = g_MapSettings.PlayerData[i + 1].Team; - if (teams.indexOf(t) == -1 && t !== undefined) - teams.push(t); -} -playerIDs = sortPlayers(playerIDs); - -// Minimize maximum distance between players within a team -if (teams.length) -{ - let minDistance = Infinity; - let bestShift; - for (let s = 0; s < playerIDs.length; ++s) - { - let maxTeamDist = 0; - for (let pi = 0; pi < playerIDs.length - 1; ++pi) - { - let p1 = playerIDs[(pi + s) % playerIDs.length] - 1; - let t1 = getPlayerTeam(p1); - - if (teams.indexOf(t1) === -1) - continue; - - for (let pj = pi + 1; pj < playerIDs.length; ++pj) - { - let p2 = playerIDs[(pj + s) % playerIDs.length] - 1; - let t2 = getPlayerTeam(p2); - if (t2 != t1) - continue; - - maxTeamDist = Math.max( - maxTeamDist, - Math.euclidDistance2D( - startLocations[pi].x, startLocations[pi].y, - startLocations[pj].x, startLocations[pj].y)); - } - } - - if (maxTeamDist < minDistance) - { - minDistance = maxTeamDist; - bestShift = s; - } - } - if (bestShift) - { - let newPlayerIDs = []; - for (let i = 0; i < playerIDs.length; ++i) - newPlayerIDs.push(playerIDs[(i + bestShift) % playerIDs.length]); - playerIDs = newPlayerIDs; - } -} +let [playerIDs, startLocations] = sortPlayersByLocation(getStartLocationsByHeightmap({ "min": heighLimits[4], "max": heighLimits[5] }, 1000, 30)); log("Start location chosen after " + ((Date.now() - genStartTime) / 1000) + "s"); RMS.SetProgress(30); /** * Smooth Start Locations before height region calculation */ for (let p = 0; p < playerIDs.length; ++p) rectangularSmoothToHeight(startLocations[p], 35, 35, playerHeight, 0.7); /** * Add paths */ let tchm = getTileCenteredHeightmap(); // Calculate tileCenteredHeightMap (This has nothing to to with TILE_CENTERED_HEIGHT_MAP which should be false) let pathPoints = []; let clPath = createTileClass(); for (let i = 0; i < startLocations.length; ++i) { let start = startLocations[i]; let target = startLocations[(i + 1) % startLocations.length]; pathPoints = pathPoints.concat(placeRandomPathToHeight(start, target, playerHeight, clPath)); } log("Paths placed after " + ((Date.now() - genStartTime) / 1000) + "s"); RMS.SetProgress(45); /** * Get resource spots after players start locations calculation */ let avoidPoints = clone(startLocations); for (let i = 0; i < avoidPoints.length; ++i) avoidPoints[i].dist = 30; let resourceSpots = getPointsByHeight({ "min": (heighLimits[3] + heighLimits[4]) / 2, "max": (heighLimits[5] + heighLimits[6]) / 2 }, avoidPoints, clPath); log("Resource spots chosen after " + ((Date.now() - genStartTime) / 1000) + "s"); RMS.SetProgress(55); /** * Divide tiles in areas by height and avoid paths */ let areas = []; for (let h = 0; h < heighLimits.length; ++h) areas.push([]); for (let x = 0; x < tchm.length; ++x) { for (let y = 0; y < tchm[0].length; ++y) { if (g_Map.tileClasses[clPath].inclusionCount[x][y] > 0) // Avoid paths continue; let minHeight = heightRange.min; for (let h = 0; h < heighLimits.length; ++h) { if (tchm[x][y] >= minHeight && tchm[x][y] <= heighLimits[h]) { areas[h].push({ "x": x, "y": y }); break; } else minHeight = heighLimits[h]; } } } /** * Get max slope of each area */ -let slopeMap = getSlopeMap(); // Calculate slope map +let slopeMap = getSlopeMap(); let minSlope = []; let maxSlope = []; for (let h = 0; h < heighLimits.length; ++h) { minSlope[h] = Infinity; maxSlope[h] = 0; for (let t = 0; t < areas[h].length; ++t) { let x = areas[h][t].x; let y = areas[h][t].y; let slope = slopeMap[x][y]; if (slope > maxSlope[h]) maxSlope[h] = slope; if (slope < minSlope[h]) minSlope[h] = slope; } } /** * Paint areas by height and slope */ for (let h = 0; h < heighLimits.length; ++h) { for (let t = 0; t < areas[h].length; ++t) { let x = areas[h][t].x; let y = areas[h][t].y; let actor = undefined; let texture = pickRandom(myBiome[h].texture); if (slopeMap[x][y] < 0.4 * (minSlope[h] + maxSlope[h])) { if (randBool(myBiome[h].actor[1])) actor = pickRandom(myBiome[h].actor[0]); } else { texture = pickRandom(myBiome[h].textureHS); if (randBool(myBiome[h].actorHS[1])) actor = pickRandom(myBiome[h].actorHS[0]); } g_Map.texture[x][y] = g_Map.getTextureID(texture); if (actor) placeObject(randFloat(x, x + 1), randFloat(y, y + 1), actor, 0, randFloat(0, 2 * PI)); } } log("Terrain texture placement finished after " + ((Date.now() - genStartTime) / 1000) + "s"); RMS.SetProgress(80); /** * Add start locations and resource spots after terrain texture and path painting */ for (let p = 0; p < playerIDs.length; ++p) { let point = startLocations[p]; placeCivDefaultEntities(point.x, point.y, playerIDs[p], { "iberWall": true }); placeStartLocationResources(startLocations[p]); } for (let i = 0; i < resourceSpots.length; ++i) { let choice = i % 5; if (choice == 0) placeMine(resourceSpots[i], "gaia/geology_stonemine_temperate_formation"); if (choice == 1) placeMine(resourceSpots[i], "gaia/geology_metal_temperate_slabs"); if (choice == 2) placeCustomFortress(resourceSpots[i].x, resourceSpots[i].y, pickRandom(fences), "other", 0, randFloat(0, 2 * PI)); if (choice == 3) placeGrove(resourceSpots[i]); if (choice == 4) placeCamp(resourceSpots[i]); } -/** - * Stop Timer - */ log("Map generation finished after " + ((Date.now() - genStartTime) / 1000) + "s"); -/** - * Export map data - */ 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 20416) +++ ps/trunk/binaries/data/mods/public/maps/random/rmgen/library.js (revision 20417) @@ -1,583 +1,522 @@ const PI = Math.PI; const TWO_PI = 2 * Math.PI; const TERRAIN_SEPARATOR = "|"; const SEA_LEVEL = 20.0; const HEIGHT_UNITS_PER_METRE = 92; const MAP_BORDER_WIDTH = 3; 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; // Default angle for buildings const BUILDING_ORIENTATION = - PI / 4; 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, minMapSize = 128, maxMapSize = 512) { return min + (max - min) * (g_Map.size - minMapSize) / (maxMapSize - minMapSize); } 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; } /** * Retries the given function with those arguments as often as specified. */ function retryPlacing(placeFunc, placeArgs, retryFactor, amount, getResult, behaveDeprecated = false) { if (behaveDeprecated && !(placeArgs.placer instanceof SimpleGroup || placeArgs.placer instanceof RandomGroup)) warn("Deprecated version of createFoo should only be used for SimpleGroup and RandomGroup placers!"); let maxFail = amount * retryFactor; let results = []; let good = 0; let bad = 0; while (good < amount && bad <= maxFail) { let result = placeFunc(placeArgs); if (result !== undefined || behaveDeprecated) { ++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 // Uniformly distributed on the disk let r = halfMapSize * Math.sqrt(randFloat(0, 1)); 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 = randIntExclusive(0, g_Map.size); placer.z = randIntExclusive(0, g_Map.size); } } /** * Helper function for randomly placing areas and groups in the given areas. */ function randomizePlacerCoordinatesFromAreas(placer, areas) { let pt = pickRandom(pickRandom(areas).points); placer.x = pt.x; placer.z = pt.z; } // TODO this is a hack to simulate the old behaviour of those functions // until all old maps are changed to use the correct version of these functions function createObjectGroupsDeprecated(placer, player, constraint, amount, retryFactor = 10) { return createObjectGroups(placer, player, constraint, amount, retryFactor, true); } function createObjectGroupsByAreasDeprecated(placer, player, constraint, amount, retryFactor, areas) { return createObjectGroupsByAreas(placer, player, constraint, amount, retryFactor, areas, true); } /** * 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, behaveDeprecated = false) { let placeFunc = function (args) { randomizePlacerCoordinates(args.placer, args.halfMapSize); return 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, behaveDeprecated); } /** * 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, behaveDeprecated = false) { if (!areas.length) return []; let placeFunc = function (args) { randomizePlacerCoordinatesFromAreas(args.placer, args.areas); return 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, behaveDeprecated); } /** * 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, behaveDeprecated = false) { 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": getMapSize() / 2 - MAP_BORDER_WIDTH }; return retryPlacing(placeFunc, args, retryFactor, amount, false, behaveDeprecated); } /** * 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, behaveDeprecated = false) { 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, behaveDeprecated); } 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 new Error("createSimpleTerrain expects string as input, received " + uneval(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)) g_Map.addObject(new Entity(type, player, x, z, angle)); } function placeTerrain(x, z, terrainNames) { createTerrain(terrainNames).place(x, z); } function initTerrain(terrainNames) { let terrain = createTerrain(terrainNames); for (let x = 0; x < getMapSize(); ++x) for (let z = 0; z < getMapSize(); ++z) terrain.place(x, z); } function isCircularMap() { return !!g_MapSettings.CircularMap; } function getMapBaseHeight() { return g_MapSettings.BaseHeight; } function createTileClass() { return g_Map.createTileClass(); } function getTileClass(id) { if (!g_Map.validClass(id)) return undefined; return g_Map.tileClasses[id]; } /** * Constructs a new Area shaped by the Placer meeting the Constraint and calls the Painters there. * Supports both Centered and Non-Centered Placers. */ function createArea(placer, painter, constraint) { if (!constraint) constraint = new NullConstraint(); else if (constraint instanceof Array) constraint = new AndConstraint(constraint); let points = placer.place(constraint); if (!points) return undefined; let area = g_Map.createArea(points); if (painter instanceof Array) painter = new MultiPainter(painter); painter.paint(area); return area; } /** * Places the Entities of the given Group if they meet the Constraint * and sets the given player as the owner. */ function createObjectGroup(group, player, constraint) { if (!constraint) constraint = new NullConstraint(); else if (constraint instanceof Array) constraint = new AndConstraint(constraint); return group.place(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; } 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; } function getTerrainTexture(x, y) { return g_Map.getTexture(x, y); } - -/** - * Returns the order to go through the points for the shortest closed path (array of indices) - * @param {array} [points] - Points to be sorted of the form { "x": x_value, "y": y_value } - */ -function getOrderOfPointsForShortestClosePath(points) -{ - let order = []; - let distances = []; - if (points.length <= 3) - { - for (let i = 0; i < points.length; ++i) - order.push(i); - - return order; - } - - // Just add the first 3 points - let pointsToAdd = clone(points); - for (let i = 0; i < 3; ++i) - { - order.push(i); - pointsToAdd.shift(i); - if (i) - distances.push(Math.euclidDistance2D(points[order[i]].x, points[order[i]].y, points[order[i - 1]].x, points[order[i - 1]].y)); - } - - distances.push(Math.euclidDistance2D( - points[order[0]].x, - points[order[0]].y, - points[order[order.length - 1]].x, - points[order[order.length - 1]].y)); - - // Add remaining points so the path lengthens the least - let numPointsToAdd = pointsToAdd.length; - for (let i = 0; i < numPointsToAdd; ++i) - { - let indexToAddTo; - let minEnlengthen = Infinity; - let minDist1 = 0; - let minDist2 = 0; - for (let k = 0; k < order.length; ++k) - { - let dist1 = Math.euclidDistance2D(pointsToAdd[0].x, pointsToAdd[0].y, points[order[k]].x, points[order[k]].y); - let dist2 = Math.euclidDistance2D(pointsToAdd[0].x, pointsToAdd[0].y, points[order[(k + 1) % order.length]].x, points[order[(k + 1) % order.length]].y); - let 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; -} Index: ps/trunk/binaries/data/mods/public/maps/random/rmgen/math.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/rmgen/math.js (revision 20416) +++ ps/trunk/binaries/data/mods/public/maps/random/rmgen/math.js (revision 20417) @@ -1,114 +1,174 @@ function diskArea(radius) { return Math.PI * Math.square(radius); } /** * 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); } /** * Revolve the given point around the given rotation center. */ function rotateCoordinates(x, z, angle, centerX = 0.5, centerZ = 0.5) { let sin = Math.sin(angle); let cos = Math.cos(angle); return [ cos * (x - centerX) - sin * (z - centerZ) + centerX, sin * (x - centerX) + cos * (z - centerZ) + centerZ ]; } /** * Get pointCount points equidistantly located on a circle. */ function distributePointsOnCircle(pointCount, startAngle, radius, centerX, centerZ) { let x = []; let z = []; let angle = []; for (let i = 0; i < pointCount; ++i) { angle[i] = startAngle + 2 * Math.PI * i / pointCount; x[i] = centerX + radius * Math.cos(angle[i]); z[i] = centerZ + radius * Math.sin(angle[i]); } return [x, z, angle]; } /** * Returns the distance of a point from a line. */ function distanceOfPointFromLine(line_x1, line_y1, line_x2, line_y2, point_x, point_y) { let width_x = line_x1 - line_x2; if (!width_x) return Math.abs(point_x - line_x1); let width_y = line_y1 - line_y2; if (!width_y) return Math.abs(point_y - line_y1); let inclination = width_y / width_x; let intercept = line_y1 - inclination * line_x1; return Math.abs((point_y - point_x * inclination - intercept) / Math.sqrt(1 + Math.square(inclination))); } /** * Determines whether two lines with the given width intersect. */ function checkIfIntersect(line1_x1, line1_y1, line1_x2, line1_y2, line2_x1, line2_y1, line2_x2, line2_y2, width) { if (line1_x1 == line1_x2) { if (line2_x1 - line1_x1 < width || line2_x2 - line1_x2 < width) return true; } else { let m = (line1_y1 - line1_y2) / (line1_x1 - line1_x2); let b = line1_y1 - m * line1_x1; let m2 = Math.sqrt(1 + Math.square(m)); if (Math.abs((line2_y1 - line2_x1 * m - b) / m2) < width || Math.abs((line2_y2 - line2_x2 * m - b) / m2) < width) return true; if (line2_x1 == line2_x2) { if (line1_x1 - line2_x1 < width || line1_x2 - line2_x2 < width) return true; } else { let m = (line2_y1 - line2_y2) / (line2_x1 - line2_x2); let b = line2_y1 - m * line2_x1; let m2 = Math.sqrt(1 + Math.square(m)); if (Math.abs((line1_y1 - line1_x1 * m - b) / m2) < width || Math.abs((line1_y2 - line1_x2 * m - b) / m2) < width) return true; } } let s = (line1_x1 - line1_x2) * (line2_y1 - line1_y1) - (line1_y1 - line1_y2) * (line2_x1 - line1_x1); let p = (line1_x1 - line1_x2) * (line2_y2 - line1_y1) - (line1_y1 - line1_y2) * (line2_x2 - line1_x1); if (s * p <= 0) { s = (line2_x1 - line2_x2) * (line1_y1 - line2_y1) - (line2_y1 - line2_y2) * (line1_x1 - line2_x1); p = (line2_x1 - line2_x2) * (line1_y2 - line2_y1) - (line2_y1 - line2_y2) * (line1_x2 - line2_x1); if (s * p <= 0) return true; } return false; } + +/** + * Sorts the given (x, y) points so that the distance between neighboring points becomes minimal (similar to the traveling salesman problem). + */ +function sortPointsShortestCycle(points) +{ + let order = []; + let distances = []; + if (points.length <= 3) + { + for (let i = 0; i < points.length; ++i) + order.push(i); + + return order; + } + + // Just add the first 3 points + let pointsToAdd = clone(points); + for (let i = 0; i < 3; ++i) + { + order.push(i); + pointsToAdd.shift(i); + if (i) + distances.push(Math.euclidDistance2D(points[order[i]].x, points[order[i]].y, points[order[i - 1]].x, points[order[i - 1]].y)); + } + + distances.push(Math.euclidDistance2D( + points[order[0]].x, + points[order[0]].y, + points[order[order.length - 1]].x, + points[order[order.length - 1]].y)); + + // Add remaining points so the path lengthens the least + let numPointsToAdd = pointsToAdd.length; + for (let i = 0; i < numPointsToAdd; ++i) + { + let indexToAddTo; + let minEnlengthen = Infinity; + let minDist1 = 0; + let minDist2 = 0; + for (let k = 0; k < order.length; ++k) + { + let dist1 = Math.euclidDistance2D(pointsToAdd[0].x, pointsToAdd[0].y, points[order[k]].x, points[order[k]].y); + let dist2 = Math.euclidDistance2D(pointsToAdd[0].x, pointsToAdd[0].y, points[order[(k + 1) % order.length]].x, points[order[(k + 1) % order.length]].y); + let 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; +} Index: ps/trunk/binaries/data/mods/public/maps/random/rmgen/player.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/rmgen/player.js (revision 20416) +++ ps/trunk/binaries/data/mods/public/maps/random/rmgen/player.js (revision 20417) @@ -1,163 +1,241 @@ /** * @file These functions locate and place the starting entities of players. */ /** * Gets the default starting entities for the civ of the given player, as defined by the civ file. */ function getStartingEntities(playerID) { let civ = getCivCode(playerID); 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; } /** * Places the given entities at the given location (typically a civic center and starting units). * @param civEntities - An array of objects with the Template property and optionally a Count property. * The first entity is placed in the center, the other ones surround it. */ function placeStartingEntities(fx, fz, playerID, civEntities, dist = 6, orientation = BUILDING_ORIENTATION) { // Place the central structure let i = 0; let firstTemplate = civEntities[i].Template; if (firstTemplate.startsWith("structures/")) { placeObject(fx, fz, firstTemplate, playerID, orientation); ++i; } // Place entities surrounding it let space = 2; for (let j = i; j < civEntities.length; ++j) { let angle = orientation - Math.PI * (1 - j / 2); let count = civEntities[j].Count || 1; for (let num = 0; num < count; ++num) placeObject( fx + dist * Math.cos(angle) + space * (-num + 0.75 * Math.floor(count / 2)) * Math.sin(angle), fz + dist * Math.sin(angle) + space * (num - 0.75 * Math.floor(count / 2)) * Math.cos(angle), civEntities[j].Template, playerID, angle); } } /** * Places the default starting entities as defined by the civilization definition and walls for Iberians. */ function placeCivDefaultEntities(fx, fz, playerID, kwargs, dist = 6, orientation = BUILDING_ORIENTATION) { placeStartingEntities(fx, fz, playerID, getStartingEntities(playerID - 1), dist, orientation); let civ = getCivCode(playerID - 1); if (civ == 'iber' && getMapSize() > 128) { if (kwargs && kwargs.iberWall == 'towers') placePolygonalWall(fx, fz, 15, ['entry'], 'tower', civ, playerID, orientation, 7); else if (!kwargs || kwargs.iberWall) placeGenericFortress(fx, fz, 20, playerID); } } /** * Marks the corner and center tiles of an area that is about the size of a Civic Center with the given TileClass. * Used to prevent resource collisions with the Civic Center. */ function addCivicCenterAreaToClass(ix, iz, tileClass) { addToClass(ix, iz, tileClass); addToClass(ix, iz + 5, tileClass); addToClass(ix, iz - 5, tileClass); addToClass(ix + 5, iz, tileClass); addToClass(ix - 5, iz, tileClass); } /** * Sorts an array of player IDs by team index. Players without teams come first. * Randomize order for players of the same team. */ function sortPlayers(playerIDs) { return shuffleArray(playerIDs).sort((p1, p2) => getPlayerTeam(p1 - 1) - getPlayerTeam(p2 - 1)); } /** * Randomize playerIDs but sort by team. * * @returns {Array} - every item is an array of player indices */ function sortAllPlayers() { let playerIDs = []; for (let i = 0; i < getNumPlayers(); ++i) playerIDs.push(i+1); return sortPlayers(playerIDs); } /** * Rearrange order so that teams of neighboring players alternate (if the given IDs are sorted by team). */ function primeSortPlayers(playerIDs) { if (!playerIDs.length) return []; let prime = []; for (let i = 0; i < Math.ceil(playerIDs.length / 2); ++i) { prime.push(playerIDs[i]); prime.push(playerIDs[playerIDs.length - 1 - i]); } return prime; } function primeSortAllPlayers() { return primeSortPlayers(sortAllPlayers()); } /** * Determine player starting positions on a circular pattern. */ function radialPlayerPlacement(radius = 0.35, startingAngle = undefined, centerX = 0.5, centerZ = 0.5) { let startAngle = startingAngle !== undefined ? startingAngle : randFloat(0, 2 * Math.PI); return [sortAllPlayers(), ...distributePointsOnCircle(getNumPlayers(), startAngle, radius, centerX, centerZ), startAngle]; } /** * Returns an array of percent numbers indicating the player location on river maps. * For example [0.2, 0.2, 0.4, 0.4, 0.6, 0.6, 0.8, 0.8] for a 4v4 or * [0.25, 0.33, 0.5, 0.67, 0.75] for a 2v3. */ function placePlayersRiver() { let playerPos = []; let numPlayers = getNumPlayers(); let numPlayersEven = numPlayers % 2 == 0; for (let i = 0; i < numPlayers; ++i) { let currentPlayerEven = i % 2 == 0; let offsetDivident = numPlayersEven || currentPlayerEven ? (i + 1) % 2 : 0; let offsetDivisor = numPlayersEven ? 0 : currentPlayerEven ? +1 : -1; playerPos[i] = ((i - 1 + offsetDivident) / 2 + 1) / ((numPlayers + offsetDivisor) / 2 + 1); } return playerPos; } + +/** + * Sorts the playerIDs so that team members are as close as possible. + */ +function sortPlayersByLocation(startLocations) +{ + // Sort start locations to form a "ring" + let startLocationOrder = sortPointsShortestCycle(startLocations); + + let newStartLocations = []; + for (let i = 0; i < startLocations.length; ++i) + newStartLocations.push(startLocations[startLocationOrder[i]]); + + startLocations = newStartLocations; + + // Sort players by team + let playerIDs = []; + let teams = []; + for (let i = 0; i < g_MapSettings.PlayerData.length - 1; ++i) + { + playerIDs.push(i+1); + let t = g_MapSettings.PlayerData[i + 1].Team; + if (teams.indexOf(t) == -1 && t !== undefined) + teams.push(t); + } + + playerIDs = sortPlayers(playerIDs); + + if (!teams.length) + return [playerIDs, startLocations]; + + // Minimize maximum distance between players within a team + let minDistance = Infinity; + let bestShift; + for (let s = 0; s < playerIDs.length; ++s) + { + let maxTeamDist = 0; + for (let pi = 0; pi < playerIDs.length - 1; ++pi) + { + let p1 = playerIDs[(pi + s) % playerIDs.length] - 1; + let t1 = getPlayerTeam(p1); + + if (teams.indexOf(t1) === -1) + continue; + + for (let pj = pi + 1; pj < playerIDs.length; ++pj) + { + let p2 = playerIDs[(pj + s) % playerIDs.length] - 1; + if (t1 != getPlayerTeam(p2)) + continue; + + maxTeamDist = Math.max( + maxTeamDist, + Math.euclidDistance2D( + startLocations[pi].x, + startLocations[pi].y, + startLocations[pj].x, + startLocations[pj].y)); + } + } + + if (maxTeamDist < minDistance) + { + minDistance = maxTeamDist; + bestShift = s; + } + } + + if (bestShift) + { + let newPlayerIDs = []; + for (let i = 0; i < playerIDs.length; ++i) + newPlayerIDs.push(playerIDs[(i + bestShift) % playerIDs.length]); + playerIDs = newPlayerIDs; + } + + return [playerIDs, startLocations]; +} Index: ps/trunk/binaries/data/mods/public/maps/random/wild_lake.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/wild_lake.js (revision 20416) +++ ps/trunk/binaries/data/mods/public/maps/random/wild_lake.js (revision 20417) @@ -1,697 +1,629 @@ RMS.LoadLibrary("rmgen"); RMS.LoadLibrary("rmbiome"); RMS.LoadLibrary("heightmap"); InitMap(); let genStartTime = Date.now(); /** * getArray - To ensure a terrain texture is contained within an array */ function getArray(stringOrArrayOfStrings) { if (typeof stringOrArrayOfStrings == "string") return [stringOrArrayOfStrings]; return stringOrArrayOfStrings; } setSelectedBiome(); // Terrain, entities and actors let wildLakeBiome = [ // 0 Deep water { "texture": getArray(g_Terrains.water), "actor": [[g_Gaia.fish], 0.01], "textureHS": getArray(g_Terrains.water), "actorHS": [[g_Gaia.fish], 0.03] }, // 1 Shallow water { "texture": getArray(g_Terrains.water), "actor": [[g_Decoratives.lillies, g_Decoratives.reeds], 0.3], "textureHS": getArray(g_Terrains.water), "actorHS": [[g_Decoratives.lillies], 0.1] }, // 2 Shore { "texture": getArray(g_Terrains.shore), "actor": [ [ g_Gaia.tree1, g_Gaia.tree1, g_Gaia.tree2, g_Gaia.tree2, g_Gaia.mainHuntableAnimal, g_Decoratives.grass, g_Decoratives.grass, g_Decoratives.rockMedium, g_Decoratives.rockMedium, g_Decoratives.bushMedium, g_Decoratives.bushMedium ], 0.3 ], "textureHS": getArray(g_Terrains.cliff), "actorHS": [[g_Decoratives.grassShort, g_Decoratives.rockMedium, g_Decoratives.bushSmall], 0.1] }, // 3 Low ground { "texture": getArray(g_Terrains.tier1Terrain), "actor": [ [ g_Decoratives.grass, g_Decoratives.grassShort, g_Decoratives.rockLarge, g_Decoratives.rockMedium, g_Decoratives.bushMedium, g_Decoratives.bushSmall ], 0.2 ], "textureHS": getArray(g_Terrains.cliff), "actorHS": [[g_Decoratives.grassShort, g_Decoratives.rockMedium, g_Decoratives.bushSmall], 0.1] }, // 4 Mid ground. Player and path height { "texture": getArray(g_Terrains.mainTerrain), "actor": [ [ g_Decoratives.grass, g_Decoratives.grassShort, g_Decoratives.rockLarge, g_Decoratives.rockMedium, g_Decoratives.bushMedium, g_Decoratives.bushSmall ], 0.2 ], "textureHS": getArray(g_Terrains.cliff), "actorHS": [[g_Decoratives.grassShort, g_Decoratives.rockMedium, g_Decoratives.bushSmall], 0.1] }, // 5 High ground { "texture": getArray(g_Terrains.tier2Terrain), "actor": [ [ g_Decoratives.grass, g_Decoratives.grassShort, g_Decoratives.rockLarge, g_Decoratives.rockMedium, g_Decoratives.bushMedium, g_Decoratives.bushSmall ], 0.2 ], "textureHS": getArray(g_Terrains.cliff), "actorHS": [[g_Decoratives.grassShort, g_Decoratives.rockMedium, g_Decoratives.bushSmall], 0.1] }, // 6 Lower hilltop forest border { "texture": getArray(g_Terrains.dirt), "actor": [ [ g_Gaia.tree1, g_Gaia.tree3, g_Gaia.fruitBush, g_Gaia.secondaryHuntableAnimal, g_Decoratives.grass, g_Decoratives.rockMedium, g_Decoratives.bushMedium ], 0.3 ], "textureHS": getArray(g_Terrains.cliff), "actorHS": [[g_Decoratives.grassShort, g_Decoratives.rockMedium, g_Decoratives.bushSmall], 0.1] }, // 7 Hilltop forest { "texture": getArray(g_Terrains.forestFloor1), "actor": [ [ g_Gaia.tree1, g_Gaia.tree2, g_Gaia.tree3, g_Gaia.tree4, g_Gaia.tree5, g_Decoratives.tree, g_Decoratives.grass, g_Decoratives.rockMedium, g_Decoratives.bushMedium ], 0.5 ], "textureHS": getArray(g_Terrains.cliff), "actorHS": [[g_Decoratives.grassShort, g_Decoratives.rockMedium, g_Decoratives.bushSmall], 0.1] } ]; var mercenaryCampGuards = { "temperate": [ { "Template" : "structures/merc_camp_egyptian" }, { "Template" : "units/mace_infantry_javelinist_b", "Count" : 4 }, { "Template" : "units/mace_cavalry_spearman_e", "Count" : 3 }, { "Template" : "units/mace_infantry_archer_a", "Count" : 4 }, { "Template" : "units/mace_champion_infantry_a", "Count" : 3 } ], "snowy": [ { "Template" : "structures/ptol_mercenary_camp" }, { "Template" : "units/brit_infantry_javelinist_b", "Count" : 4 }, { "Template" : "units/brit_cavalry_swordsman_e", "Count" : 3 }, { "Template" : "units/brit_infantry_slinger_a", "Count" : 4 }, { "Template" : "units/brit_champion_infantry", "Count" : 3 } ], "desert": [ { "Template" : "structures/ptol_mercenary_camp" }, { "Template" : "units/pers_infantry_javelinist_b", "Count" : 4 }, { "Template" : "units/pers_cavalry_swordsman_e", "Count" : 3 }, { "Template" : "units/pers_infantry_archer_a", "Count" : 4 }, { "Template" : "units/pers_champion_infantry", "Count" : 3 } ], "alpine": [ { "Template" : "structures/ptol_mercenary_camp" }, { "Template" : "units/rome_infantry_swordsman_b", "Count" : 4 }, { "Template" : "units/rome_cavalry_spearman_e", "Count" : 3 }, { "Template" : "units/rome_infantry_javelinist_a", "Count" : 4 }, { "Template" : "units/rome_champion_infantry", "Count" : 3 } ], "mediterranean": [ { "Template" : "structures/merc_camp_egyptian" }, { "Template" : "units/iber_infantry_javelinist_b", "Count" : 4 }, { "Template" : "units/iber_cavalry_spearman_e", "Count" : 3 }, { "Template" : "units/iber_infantry_slinger_a", "Count" : 4 }, { "Template" : "units/iber_champion_infantry", "Count" : 3 } ], "savanna": [ { "Template" : "structures/merc_camp_egyptian" }, { "Template" : "units/sele_infantry_javelinist_b", "Count" : 4 }, { "Template" : "units/sele_cavalry_spearman_merc_e", "Count" : 3 }, { "Template" : "units/sele_infantry_spearman_a", "Count" : 4 }, { "Template" : "units/sele_champion_infantry_swordsman", "Count" : 3 } ], "tropic": [ { "Template" : "structures/merc_camp_egyptian" }, { "Template" : "units/ptol_infantry_javelinist_b", "Count" : 4 }, { "Template" : "units/ptol_cavalry_archer_e", "Count" : 3 }, { "Template" : "units/ptol_infantry_slinger_a", "Count" : 4 }, { "Template" : "units/ptol_champion_infantry_pikeman", "Count" : 3 } ], "autumn": [ { "Template" : "structures/ptol_mercenary_camp" }, { "Template" : "units/gaul_infantry_javelinist_b", "Count" : 4 }, { "Template" : "units/gaul_cavalry_swordsman_e", "Count" : 3 }, { "Template" : "units/gaul_infantry_slinger_a", "Count" : 4 }, { "Template" : "units/gaul_champion_infantry", "Count" : 3 } ] }; /** * Resource spots and other points of interest */ // Mines function placeMine(point, centerEntity, decorativeActors = [ g_Decoratives.grass, g_Decoratives.grassShort, g_Decoratives.rockLarge, g_Decoratives.rockMedium, g_Decoratives.bushMedium, g_Decoratives.bushSmall ] ) { placeObject(point.x, point.y, centerEntity, 0, randFloat(0, TWO_PI)); let quantity = randIntInclusive(11, 23); let dAngle = TWO_PI / quantity; for (let i = 0; i < quantity; ++i) { let angle = dAngle * randFloat(i, i + 1); let dist = randFloat(2, 5); placeObject(point.x + dist * Math.cos(angle), point.y + dist * Math.sin(angle), pickRandom(decorativeActors), 0, randFloat(0, 2 * PI)); } } // Groves, only Wood let groveActors = [g_Decoratives.grass, g_Decoratives.rockMedium, g_Decoratives.bushMedium]; let clGrove = createTileClass(); function placeGrove(point, groveEntities = [ g_Gaia.tree1, g_Gaia.tree1, g_Gaia.tree1, g_Gaia.tree1, g_Gaia.tree1, g_Gaia.tree2, g_Gaia.tree2, g_Gaia.tree2, g_Gaia.tree2, g_Gaia.tree3, g_Gaia.tree3, g_Gaia.tree3, g_Gaia.tree4, g_Gaia.tree4, g_Gaia.tree5 ], groveActors = [g_Decoratives.grass, g_Decoratives.rockMedium, g_Decoratives.bushMedium], groveTileClass = undefined, groveTerrainTexture = getArray(g_Terrains.forestFloor1) ) { placeObject(point.x, point.y, pickRandom(["structures/gaul_outpost", "gaia/flora_tree_oak_new"]), 0, randFloat(0, 2 * PI)); let quantity = randIntInclusive(20, 30); let dAngle = TWO_PI / quantity; for (let i = 0; i < quantity; ++i) { let angle = dAngle * randFloat(i, i + 1); let dist = randFloat(2, 5); let objectList = groveEntities; if (i % 3 == 0) objectList = groveActors; let x = point.x + dist * Math.cos(angle); let y = point.y + dist * Math.sin(angle); placeObject(x, y, pickRandom(objectList), 0, randFloat(0, 2 * PI)); if (groveTileClass) createArea(new ClumpPlacer(5, 1, 1, 1, floor(x), floor(y)), [new TerrainPainter(groveTerrainTexture), paintClass(groveTileClass)]); else createArea(new ClumpPlacer(5, 1, 1, 1, floor(x), floor(y)), [new TerrainPainter(groveTerrainTexture)]); } } var farmEntities = { "temperate": { "building": "structures/mace_farmstead", "animal": "gaia/fauna_pig" }, "snowy": { "building": "structures/brit_farmstead", "animal": "gaia/fauna_sheep" }, "desert": { "building": "structures/pers_farmstead", "animal": "gaia/fauna_camel" }, "alpine": { "building": "structures/rome_farmstead", "animal": "gaia/fauna_sheep" }, "mediterranean": { "building": "structures/iber_farmstead", "animal": "gaia/fauna_pig" }, "savanna": { "building": "structures/sele_farmstead", "animal": "gaia/fauna_horse" }, "tropic": { "building": "structures/ptol_farmstead", "animal": "gaia/fauna_camel" }, "autumn": { "building": "structures/gaul_farmstead", "animal": "gaia/fauna_horse" } }; wallStyles.other.sheepIn = new WallElement("sheepIn", farmEntities[currentBiome()].animal, PI / 4, -1.5, 0.75, PI/2); wallStyles.other.foodBin = new WallElement("foodBin", "gaia/special_treasure_food_bin", PI/2, 1.5); wallStyles.other.sheep = new WallElement("sheep", farmEntities[currentBiome()].animal, 0, 0, 0.75); wallStyles.other.farm = new WallElement("farm", farmEntities[currentBiome()].building, PI, 0, -3); let fences = [ new Fortress("fence", ["foodBin", "farm", "bench", "sheepIn", "fence", "sheepIn", "fence", "sheepIn", "fence"]), new Fortress("fence", ["foodBin", "farm", "fence", "sheepIn", "fence", "sheepIn", "bench", "sheep", "fence", "sheepIn", "fence"]), new Fortress("fence", [ "foodBin", "farm", "cornerIn", "bench", "cornerOut", "fence_short", "sheepIn", "fence", "sheepIn", "fence", "sheepIn", "fence_short", "sheep", "fence" ]), new Fortress("fence", [ "foodBin", "farm", "cornerIn", "fence_short", "cornerOut", "bench", "sheepIn", "fence", "sheepIn", "fence", "sheepIn", "fence_short", "sheep", "fence" ]), new Fortress("fence", [ "foodBin", "farm", "fence", "sheepIn", "bench", "sheep", "fence", "sheepIn", "fence_short", "sheep", "fence", "sheepIn", "fence_short", "sheep", "fence" ]) ]; let num = fences.length; for (let i = 0; i < num; ++i) fences.push(new Fortress("fence", clone(fences[i].wall).reverse())); // Camps with fire and gold treasure function placeCamp(point, centerEntity = "actor|props/special/eyecandy/campfire.xml", otherEntities = ["gaia/special_treasure_metal", "gaia/special_treasure_standing_stone", "units/brit_infantry_slinger_b", "units/brit_infantry_javelinist_b", "units/gaul_infantry_slinger_b", "units/gaul_infantry_javelinist_b", "units/gaul_champion_fanatic", "actor|props/special/common/waypoint_flag.xml", "actor|props/special/eyecandy/barrel_a.xml", "actor|props/special/eyecandy/basket_celt_a.xml", "actor|props/special/eyecandy/crate_a.xml", "actor|props/special/eyecandy/dummy_a.xml", "actor|props/special/eyecandy/handcart_1.xml", "actor|props/special/eyecandy/handcart_1_broken.xml", "actor|props/special/eyecandy/sack_1.xml", "actor|props/special/eyecandy/sack_1_rough.xml" ] ) { placeObject(point.x, point.y, centerEntity, 0, randFloat(0, TWO_PI)); let quantity = randIntInclusive(5, 11); let dAngle = TWO_PI / quantity; for (let i = 0; i < quantity; ++i) { let angle = dAngle * randFloat(i, i + 1); let dist = randFloat(1, 3); placeObject(point.x + dist * Math.cos(angle), point.y + dist * Math.sin(angle), pickRandom(otherEntities), 0, randFloat(0, 2 * PI)); } } function placeStartLocationResources( point, foodEntities = [g_Gaia.fruitBush, g_Gaia.chicken], groveEntities = [ g_Gaia.tree1, g_Gaia.tree1, g_Gaia.tree1, g_Gaia.tree1, g_Gaia.tree1, g_Gaia.tree2, g_Gaia.tree2, g_Gaia.tree2, g_Gaia.tree2, g_Gaia.tree3, g_Gaia.tree3, g_Gaia.tree3, g_Gaia.tree4, g_Gaia.tree4, g_Gaia.tree5 ], groveTerrainTexture = getArray(g_Terrains.forestFloor1), averageDistToCC = 10, dAverageDistToCC = 2 ) { function getRandDist() { return averageDistToCC + randFloat(-dAverageDistToCC, dAverageDistToCC); } let currentAngle = randFloat(0, TWO_PI); // Stone let dAngle = TWO_PI * 2 / 9; let angle = currentAngle + randFloat(dAngle / 4, 3 * dAngle / 4); placeMine({ "x": point.x + averageDistToCC * Math.cos(angle), "y": point.y + averageDistToCC * Math.sin(angle) }, g_Gaia.stoneLarge); currentAngle += dAngle; // Wood let quantity = 80; dAngle = TWO_PI / quantity / 3; for (let i = 0; i < quantity; ++i) { angle = currentAngle + randFloat(0, dAngle); let dist = getRandDist(); let objectList = groveEntities; if (i % 2 == 0) objectList = groveActors; let x = point.x + dist * Math.cos(angle); let y = point.y + dist * Math.sin(angle); placeObject(x, y, pickRandom(objectList), 0, randFloat(0, 2 * PI)); createArea(new ClumpPlacer(5, 1, 1, 1, floor(x), floor(y)), [new TerrainPainter(groveTerrainTexture), paintClass(clGrove)]); currentAngle += dAngle; } // Metal dAngle = TWO_PI * 2 / 9; angle = currentAngle + randFloat(dAngle / 4, 3 * dAngle / 4); placeMine({ "x": point.x + averageDistToCC * Math.cos(angle), "y": point.y + averageDistToCC * Math.sin(angle) }, g_Gaia.metalLarge); currentAngle += dAngle; // Berries and domestic animals quantity = 15; dAngle = TWO_PI / quantity * 2 / 9; for (let i = 0; i < quantity; ++i) { angle = currentAngle + randFloat(0, dAngle); let dist = getRandDist(); placeObject(point.x + dist * Math.cos(angle), point.y + dist * Math.sin(angle), pickRandom(foodEntities), 0, randFloat(0, 2 * PI)); currentAngle += dAngle; } } log("Functions loaded after " + ((Date.now() - genStartTime) / 1000) + "s"); /** * Base terrain shape generation and settings */ // Height range by map size let heightScale = (g_Map.size + 256) / 768 / 4; let heightRange = { "min": MIN_HEIGHT * heightScale, "max": MAX_HEIGHT * heightScale }; // Water coverage let averageWaterCoverage = 1/5; // NOTE: Since terrain generation is quite unpredictable actual water coverage might vary much with the same value let waterHeight = -MIN_HEIGHT + heightRange.min + averageWaterCoverage * (heightRange.max - heightRange.min); // Water height in environment and the engine let waterHeightAdjusted = waterHeight + MIN_HEIGHT; // Water height as terrain height setWaterHeight(waterHeight); // Generate base terrain shape let lowH = heightRange.min; let medH = (heightRange.min + heightRange.max) / 2; // Lake let initialHeightmap = [ [medH, medH, medH, medH, medH, medH], [medH, medH, medH, medH, medH, medH], [medH, medH, lowH, lowH, medH, medH], [medH, medH, lowH, lowH, medH, medH], [medH, medH, medH, medH, medH, medH], [medH, medH, medH, medH, medH, medH], ]; if (g_Map.size < 256) { initialHeightmap = [ [medH, medH, medH, medH, medH], [medH, medH, medH, medH, medH], [medH, medH, lowH, medH, medH], [medH, medH, medH, medH, medH], [medH, medH, medH, medH, medH] ]; } if (g_Map.size >= 384) { initialHeightmap = [ [medH, medH, medH, medH, medH, medH, medH, medH], [medH, medH, medH, medH, medH, medH, medH, medH], [medH, medH, medH, medH, medH, medH, medH, medH], [medH, medH, medH, lowH, lowH, medH, medH, medH], [medH, medH, medH, lowH, lowH, medH, medH, medH], [medH, medH, medH, medH, medH, medH, medH, medH], [medH, medH, medH, medH, medH, medH, medH, medH], [medH, medH, medH, medH, medH, medH, medH, medH], ]; } setBaseTerrainDiamondSquare(heightRange.min, heightRange.max, initialHeightmap, 0.8); // Apply simple erosion for (let i = 0; i < 5; ++i) splashErodeMap(0.1); globalSmoothHeightmap(); // Final rescale rescaleHeightmap(heightRange.min, heightRange.max); RMS.SetProgress(25); /** * Prepare terrain texture placement */ let heighLimits = [ heightRange.min + 3/4 * (waterHeightAdjusted - heightRange.min), // 0 Deep water waterHeightAdjusted, // 1 Shallow water waterHeightAdjusted + 2/8 * (heightRange.max - waterHeightAdjusted), // 2 Shore waterHeightAdjusted + 3/8 * (heightRange.max - waterHeightAdjusted), // 3 Low ground waterHeightAdjusted + 4/8 * (heightRange.max - waterHeightAdjusted), // 4 Player and path height waterHeightAdjusted + 6/8 * (heightRange.max - waterHeightAdjusted), // 5 High ground waterHeightAdjusted + 7/8 * (heightRange.max - waterHeightAdjusted), // 6 Lower forest border heightRange.max // 7 Forest ]; let playerHeightRange = { "min" : heighLimits[3], "max" : heighLimits[4] }; let resourceSpotHeightRange = { "min" : (heighLimits[2] + heighLimits[3]) / 2, "max" : (heighLimits[4] + heighLimits[5]) / 2 }; let playerHeight = (playerHeightRange.min + playerHeightRange.max) / 2; // Average player height log("Terrain shape generation and biome presets after " + ((Date.now() - genStartTime) / 1000) + "s"); -/** - * Get start locations - */ -let startLocations = getStartLocationsByHeightmap(playerHeightRange, 1000, 30); - -// Sort start locations to form a "ring" -let startLocationOrder = getOrderOfPointsForShortestClosePath(startLocations); -let newStartLocations = []; -for (let i = 0; i < startLocations.length; ++i) - newStartLocations.push(startLocations[startLocationOrder[i]]); -startLocations = newStartLocations; - -// Sort players by team -let playerIDs = []; -let teams = []; -for (let i = 0; i < g_MapSettings.PlayerData.length - 1; ++i) -{ - playerIDs.push(i+1); - let t = g_MapSettings.PlayerData[i + 1].Team; - if (teams.indexOf(t) == -1 && t !== undefined) - teams.push(t); -} -playerIDs = sortPlayers(playerIDs); - -// Minimize maximum distance between players within a team -if (teams.length) -{ - let minDistance = Infinity; - let bestShift; - for (let s = 0; s < playerIDs.length; ++s) - { - let maxTeamDist = 0; - for (let pi = 0; pi < playerIDs.length - 1; ++pi) - { - let p1 = playerIDs[(pi + s) % playerIDs.length] - 1; - let t1 = getPlayerTeam(p1); - - if (teams.indexOf(t1) === -1) - continue; - - for (let pj = pi + 1; pj < playerIDs.length; ++pj) - { - let p2 = playerIDs[(pj + s) % playerIDs.length] - 1; - let t2 = getPlayerTeam(p2); - if (t2 != t1) - continue; - - maxTeamDist = Math.max( - maxTeamDist, - Math.euclidDistance2D( - startLocations[pi].x, startLocations[pi].y, - startLocations[pj].x, startLocations[pj].y)); - } - } - - if (maxTeamDist < minDistance) - { - minDistance = maxTeamDist; - bestShift = s; - } - } - if (bestShift) - { - let newPlayerIDs = []; - for (let i = 0; i < playerIDs.length; ++i) - newPlayerIDs.push(playerIDs[(i + bestShift) % playerIDs.length]); - playerIDs = newPlayerIDs; - } -} +let [playerIDs, startLocations] = sortPlayersByLocation(getStartLocationsByHeightmap(playerHeightRange, 1000, 30)); log("Start location chosen after " + ((Date.now() - genStartTime) / 1000) + "s"); RMS.SetProgress(30); /** * Smooth Start Locations before height region calculation */ let playerBaseRadius = 35; if (g_Map.size < 256) playerBaseRadius = 25; for (let p = 0; p < playerIDs.length; ++p) rectangularSmoothToHeight(startLocations[p], playerBaseRadius, playerBaseRadius, playerHeight, 0.7); /** * Calculate tile centered height map after start position smoothing but before placing paths * This has nothing to to with TILE_CENTERED_HEIGHT_MAP which should be false! */ let tchm = getTileCenteredHeightmap(); /** * Add paths (If any) */ let clPath = createTileClass(); /** * Divide tiles in areas by height and avoid paths */ let areas = []; for (let h = 0; h < heighLimits.length; ++h) areas.push([]); for (let x = 0; x < tchm.length; ++x) { for (let y = 0; y < tchm[0].length; ++y) { if (g_Map.tileClasses[clPath].inclusionCount[x][y] > 0) // Avoid paths continue; let minHeight = heightRange.min; for (let h = 0; h < heighLimits.length; ++h) { if (tchm[x][y] >= minHeight && tchm[x][y] <= heighLimits[h]) { areas[h].push({ "x": x, "y": y }); break; } else minHeight = heighLimits[h]; } } } /** * Get max slope of each area */ let slopeMap = getSlopeMap(); let minSlope = []; let maxSlope = []; for (let h = 0; h < heighLimits.length; ++h) { minSlope[h] = Infinity; maxSlope[h] = 0; for (let t = 0; t < areas[h].length; ++t) { let x = areas[h][t].x; let y = areas[h][t].y; let slope = slopeMap[x][y]; if (slope > maxSlope[h]) maxSlope[h] = slope; if (slope < minSlope[h]) minSlope[h] = slope; } } /** * Paint areas by height and slope */ for (let h = 0; h < heighLimits.length; ++h) { for (let t = 0; t < areas[h].length; ++t) { let x = areas[h][t].x; let y = areas[h][t].y; let actor; let texture = pickRandom(wildLakeBiome[h].texture); if (slopeMap[x][y] < 0.5 * (minSlope[h] + maxSlope[h])) { if (randBool(wildLakeBiome[h].actor[1])) actor = pickRandom(wildLakeBiome[h].actor[0]); } else { texture = pickRandom(wildLakeBiome[h].textureHS); if (randBool(wildLakeBiome[h].actorHS[1])) actor = pickRandom(wildLakeBiome[h].actorHS[0]); } g_Map.texture[x][y] = g_Map.getTextureID(texture); if (actor) placeObject(randFloat(x, x + 1), randFloat(y, y + 1), actor, 0, randFloat(0, 2 * PI)); } } log("Terrain texture placement finished after " + ((Date.now() - genStartTime) / 1000) + "s"); RMS.SetProgress(80); /** * Get resource spots after players start locations calculation and paths */ let avoidPoints = clone(startLocations); for (let i = 0; i < avoidPoints.length; ++i) avoidPoints[i].dist = 30; let resourceSpots = getPointsByHeight(resourceSpotHeightRange, avoidPoints, clPath); log("Resource spots chosen after " + ((Date.now() - genStartTime) / 1000) + "s"); RMS.SetProgress(55); /** * Add start locations and resource spots after terrain texture and path painting */ for (let p = 0; p < playerIDs.length; ++p) { let point = startLocations[p]; placeCivDefaultEntities(point.x, point.y, playerIDs[p], { "iberWall": g_Map.size > 192 }); placeStartLocationResources(point); } let mercenaryCamps = ceil(g_Map.size / 256); log("Maximum number of mercenary camps: " + uneval(mercenaryCamps)); for (let i = 0; i < resourceSpots.length; ++i) { let choice = i % 5; if (choice == 0) placeMine(resourceSpots[i], g_Gaia.stoneLarge); if (choice == 1) placeMine(resourceSpots[i], g_Gaia.metalLarge); if (choice == 2) placeGrove(resourceSpots[i]); if (choice == 3) { placeCamp(resourceSpots[i]); rectangularSmoothToHeight(resourceSpots[i], 5, 5, g_Map.height[resourceSpots[i].x][resourceSpots[i].y] - 10, 0.5); } if (choice == 4) { if (mercenaryCamps) { placeStartingEntities(resourceSpots[i].x, resourceSpots[i].y, 0, mercenaryCampGuards[currentBiome()]); rectangularSmoothToHeight(resourceSpots[i], 15, 15, g_Map.height[resourceSpots[i].x][resourceSpots[i].y], 0.5); --mercenaryCamps; } else { placeCustomFortress(resourceSpots[i].x, resourceSpots[i].y, pickRandom(fences), "other", 0, randFloat(0, 2 * PI)); rectangularSmoothToHeight(resourceSpots[i], 10, 10, g_Map.height[resourceSpots[i].x][resourceSpots[i].y], 0.5); } } } log("Map generation finished after " + ((Date.now() - genStartTime) / 1000) + "s"); ExportMap();