Index: ps/trunk/binaries/data/mods/public/art/textures/ui/session/icons/mappreview/belgian_uplands.png =================================================================== Cannot display: file marked as a binary type. svn:mime-type = application/octet-stream Index: ps/trunk/binaries/data/mods/public/maps/random/belgian_uplands.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/belgian_uplands.js (revision 21170) +++ ps/trunk/binaries/data/mods/public/maps/random/belgian_uplands.js (revision 21171) @@ -1,407 +1,285 @@ Engine.LoadLibrary("rmgen"); +Engine.LoadLibrary("rmgen2"); Engine.LoadLibrary("heightmap"); const tPrimary = ["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"]; const heightLand = 0; var g_Map = new RandomMap(heightLand, tPrimary); var numPlayers = getNumPlayers(); + var mapSize = g_Map.getSize(); var mapCenter = g_Map.getCenter(); -// 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++) - g_Map.setHeight(new Vector2D(x, y), reliefmap[x][y]); -} - // 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 heightSeaGround = -MIN_HEIGHT + heightRange.min + averageWaterCoverage * (heightRange.max - heightRange.min); var heightSeaGroundAdjusted = heightSeaGround + MIN_HEIGHT; setWaterHeight(heightSeaGround); // Prepare terrain texture by height placement var textueByHeight = []; // Deep water textueByHeight.push({"upperHeightLimit": heightRange.min + 1/3 * (heightSeaGroundAdjusted - 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 * (heightSeaGroundAdjusted - heightRange.min), "terrain": terrains}); // Flat Water textueByHeight.push({"upperHeightLimit": heightRange.min + 3/3 * (heightSeaGroundAdjusted - 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": heightSeaGroundAdjusted + 1/6 * (heightRange.max - heightSeaGroundAdjusted), "terrain": terrains}); // Juicy grass near bog textueByHeight.push({"upperHeightLimit": heightSeaGroundAdjusted + 2/6 * (heightRange.max - heightSeaGroundAdjusted), "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": heightSeaGroundAdjusted + 3/6 * (heightRange.max - heightSeaGroundAdjusted), "terrain": ["temp_grass", "temp_grass_b", "temp_grass_c", "temp_grass_mossy"]}); // Long grass near forest border textueByHeight.push({"upperHeightLimit": heightSeaGroundAdjusted + 4/6 * (heightRange.max - heightSeaGroundAdjusted), "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": heightSeaGroundAdjusted + 5/6 * (heightRange.max - heightSeaGroundAdjusted), "terrain": terrains}); // Unpassable woods textueByHeight.push({"upperHeightLimit": heightSeaGroundAdjusted + 6/6 * (heightRange.max - heightSeaGroundAdjusted), "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; - Engine.SetProgress(5); -// - 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 tries = 0; var lowerHeightLimit = textueByHeight[3].upperHeightLimit; var upperHeightLimit = textueByHeight[6].upperHeightLimit; -while (!goodStartPositionsFound) +var playerPositions; +while (true) { - tries++; - g_Map.log("Starting giant while loop try " + tries); + g_Map.log("Randomizing heightmap") + setRandomHeightmap(heightRange.min, heightRange.max, g_Map.height); - // Generate reliefmap - var myReliefmap = clone(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) - globalSmoothHeightmap(0.8, myReliefmap); - - rescaleHeightmap(heightRange.min, heightRange.max, myReliefmap); - setReliefmap(myReliefmap); - - // Find good start position tiles - 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... - - // 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++) - { - let position = new Vector2D(x, y); - let actualHeight = g_Map.getHeight(position); - 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 = g_Map.getHeight(Vector2D.add(position, new Vector2D(offX, offY))); - if (testHeight <= lowerHeightLimit || testHeight >= upperHeightLimit) - { - isPossible = false; - break; - } - } - - if (isPossible) - possibleStartPositions.push([x, y]); - } + // More cycles yield bigger structures + g_Map.log("Smoothing map"); + for (let i = 0; i < 50 + mapSize/4; ++i) + globalSmoothHeightmap(); + + g_Map.log("Rescaling map"); + rescaleHeightmap(heightRange.min, heightRange.max, g_Map.height); + + g_Map.log("Mark valid heightrange for player starting positions"); + let tHeightRange = g_Map.createTileClass(); + let area = createArea( + new ClumpPlacer(diskArea(fractionToTiles(0.5) - MAP_BORDER_WIDTH), 1, 1, Infinity, mapCenter), + new TileClassPainter(tHeightRange), + new HeightConstraint(lowerHeightLimit, upperHeightLimit)); + + if (area) + try { + playerPositions = randomPlayerLocations(sortAllPlayers(), new stayClasses(tHeightRange, 15)); + break; } - - // 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 = []; - for (var i = 0; i < possibleStartPositions.length; i++) - { - if (Math.euclidDistance2D(...possibleStartPositions[i], mapCenter.x, mapCenter.y) < mapSize / 2) - possibleStartPositionsTemp.push(possibleStartPositions[i]); - } - possibleStartPositions = clone(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 = g_Map.getHeight(new Vector2D(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]); - } - - possibleStartPositions = clone(possibleStartPositionsTemp); - - // Find a good start position derivation - if (possibleStartPositions.length > numPlayers) - { - // Get some random start location derivations. NOTE: Iterating over all possible derivations is just too much (valid points * numPlayers) - var maxTries = 100000; - var possibleDerivations = []; - for (var i = 0; i < maxTries; i++) - { - var vector = []; - for (var p = 0; p < numPlayers; p++) - vector.push(randIntExclusive(0, possibleStartPositions.length)); - possibleDerivations.push(vector); + catch (e) { } - // 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) - { - minDist = Math.min(minDist, Math.euclidDistance2D(...possibleStartPositions[possibleDerivations[d][p1]], ...possibleStartPositions[possibleDerivations[d][p2]])); - if (minDist < maxMinDist) - break; - } - } - if (minDist < maxMinDist) - break; - } - if (minDist > maxMinDist) - { - maxMinDist = minDist; - var bestDerivation = possibleDerivations[d]; - } - } - if (maxMinDist > minDistBetweenPlayers) - { - goodStartPositionsFound = true; - g_Map.log("Exiting giant while loop after " + tries + " tries with a minimum player distance of " + maxMinDist); - } - else - g_Map.log("maxMinDist <= " + minDistBetweenPlayers + ", maxMinDist = " + maxMinDist); - } + g_Map.log("Too few starting locations"); } - Engine.SetProgress(60); g_Map.log("Painting 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++) +for (let x = 0; x < mapSize; ++x) + for (let y = 0; y < mapSize; ++y) { let position = new Vector2D(x, y); + if (!g_Map.validHeight(position)) + continue; + var textureMinHeight = heightRange.min; for (var i = 0; i < textueByHeight.length; i++) { if (g_Map.getHeight(position) >= textureMinHeight && g_Map.getHeight(position) <= textueByHeight[i].upperHeightLimit) { createTerrain(textueByHeight[i].terrain).place(position); let template; if (i == 0) // ...deep water { if (randBool(propDensity / 100)) template = "actor|props/flora/pond_lillies_large.xml"; else if (randBool(propDensity / 40)) template = "actor|props/flora/water_lillies.xml"; } if (i == 1) // ...medium water (with fish) { if (randBool(propDensity / 200)) template = "actor|props/flora/pond_lillies_large.xml"; else if (randBool(propDensity / 100)) template = "actor|props/flora/water_lillies.xml"; } if (i == 2) // ...low water/mud { if (randBool(propDensity / 200)) template = "actor|props/flora/water_log.xml"; else if (randBool(propDensity / 100)) template = "actor|props/flora/water_lillies.xml"; else if (randBool(propDensity / 40)) template = "actor|geology/highland_c.xml"; else if (randBool(propDensity / 20)) template = "actor|props/flora/reeds_pond_lush_b.xml"; else if (randBool(propDensity / 10)) template = "actor|props/flora/reeds_pond_lush_a.xml"; } if (i == 3) // ...water suroundings/bog { if (randBool(propDensity / 200)) template = "actor|props/flora/water_log.xml"; else if (randBool(propDensity / 100)) template = "actor|geology/highland_c.xml"; else if (randBool(propDensity / 40)) template = "actor|props/flora/reeds_pond_lush_a.xml"; } if (i == 4) // ...low height grass { if (randBool(propDensity / 800)) template = "actor|props/flora/grass_field_flowering_tall.xml"; else if (randBool(propDensity / 400)) template = "actor|geology/gray_rock1.xml"; else if (randBool(propDensity / 200)) template = "actor|props/flora/bush_tempe_sm_lush.xml"; else if (randBool(propDensity / 100)) template = "actor|props/flora/bush_tempe_b.xml"; else if (randBool(propDensity / 40)) template = "actor|props/flora/grass_soft_small_tall.xml"; } if (i == 5) // ...medium height grass { if (randBool(propDensity / 800)) template = "actor|geology/decal_stone_medit_a.xml"; else if (randBool(propDensity / 400)) template = "actor|props/flora/decals_flowers_daisies.xml"; else if (randBool(propDensity / 200)) template = "actor|props/flora/bush_tempe_underbrush.xml"; else if (randBool(propDensity / 100)) template = "actor|props/flora/grass_soft_small_tall.xml"; else if (randBool(propDensity / 40)) template = "actor|props/flora/grass_temp_field.xml"; } if (i == 6) // ...high height grass { if (randBool(propDensity / 400)) template = "actor|geology/stone_granite_boulder.xml"; else if (randBool(propDensity / 200)) template = "actor|props/flora/foliagebush.xml"; else if (randBool(propDensity / 100)) template = "actor|props/flora/bush_tempe_underbrush.xml"; else if (randBool(propDensity / 40)) template = "actor|props/flora/grass_soft_small_tall.xml"; else if (randBool(propDensity / 20)) template = "actor|props/flora/ferns.xml"; } if (i == 7) // ...forest border (with wood/food plants/deer/rabits) { if (randBool(propDensity / 400)) template = "actor|geology/highland_c.xml"; else if (randBool(propDensity / 200)) template = "actor|props/flora/bush_tempe_a.xml"; else if (randBool(propDensity / 100)) template = "actor|props/flora/ferns.xml"; else if (randBool(propDensity / 40)) template = "actor|props/flora/grass_soft_tuft_a.xml"; } if (i == 8) // ...woods { if (randBool(propDensity / 200)) template = "actor|geology/highland2_moss.xml"; else if (randBool(propDensity / 100)) template = "actor|props/flora/grass_soft_tuft_a.xml"; else if (randBool(propDensity / 40)) template = "actor|props/flora/ferns.xml"; } if (template) g_Map.placeEntityAnywhere(template, 0, position, randomAngle()); break; } else textureMinHeight = textueByHeight[i].upperHeightLimit; } } Engine.SetProgress(90); if (isNomad()) placePlayersNomad(g_Map.createTileClass(), new HeightConstraint(lowerHeightLimit, upperHeightLimit)); else { g_Map.log("Placing players and starting resources"); - let playerIDs = sortAllPlayers(); let resourceDistance = 8; let resourceSpacing = 1; let resourceCount = 4; for (let i = 0; i < numPlayers; ++i) { - let playerPos = new Vector2D(possibleStartPositions[bestDerivation[i]][0], possibleStartPositions[bestDerivation[i]][1]); - placeCivDefaultStartingEntities(playerPos, playerIDs[i], false); + placeCivDefaultStartingEntities(playerPositions[i].position, playerPositions[i].id, false); for (let j = 1; j <= 4; ++j) { let uAngle = BUILDING_ORIENTATION - Math.PI * (2-j) / 2; for (let k = 0; k < resourceCount; ++k) { let pos = Vector2D.sum([ - playerPos, + playerPositions[i].position, new Vector2D(resourceDistance, 0).rotate(-uAngle), new Vector2D(k * resourceSpacing, 0).rotate(-uAngle - Math.PI/2), new Vector2D(-0.75 * resourceSpacing * Math.floor(resourceCount / 2), 0).rotate(-uAngle - Math.PI/2) ]); g_Map.placeEntityPassable(j % 2 ? "gaia/flora_tree_cypress" : "gaia/flora_bush_berry", 0, pos, randomAngle()); } } } } g_Map.ExportMap(); Index: ps/trunk/binaries/data/mods/public/maps/random/belgian_uplands.json =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/belgian_uplands.json (revision 21170) +++ ps/trunk/binaries/data/mods/public/maps/random/belgian_uplands.json (revision 21171) @@ -1,12 +1,12 @@ { "settings" : { "Name" : "Belgian Uplands", "Script" : "belgian_uplands.js", "Description" : "An experimental map with its heightmap generated by erosion to look more natural. Not all seeds will be fair though! Tiny maps with 8 players may take a while to generate.", "DisabledTemplates": [ "structures/ptol_lighthouse" ], - "CircularMap" : false, + "CircularMap" : true, "Preview" : "belgian_uplands.png" } } Index: ps/trunk/binaries/data/mods/public/maps/random/rmgen2/setup.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/rmgen2/setup.js (revision 21170) +++ ps/trunk/binaries/data/mods/public/maps/random/rmgen2/setup.js (revision 21171) @@ -1,496 +1,496 @@ var g_Amounts = { "scarce": 0.2, "few": 0.5, "normal": 1, "many": 1.75, "tons": 3 }; var g_Mixes = { "same": 0, "similar": 0.1, "normal": 0.25, "varied": 0.5, "unique": 0.75 }; var g_Sizes = { "tiny": 0.5, "small": 0.75, "normal": 1, "big": 1.25, "huge": 1.5, }; var g_AllAmounts = Object.keys(g_Amounts); var g_AllMixes = Object.keys(g_Mixes); var g_AllSizes = Object.keys(g_Sizes); var g_DefaultTileClasses = [ "animals", "baseResource", "berries", "bluff", "bluffSlope", "dirt", "fish", "food", "forest", "hill", "land", "map", "metal", "mountain", "plateau", "player", "prop", "ramp", "rock", "settlement", "spine", "valley", "water" ]; var g_TileClasses; /** * Adds an array of elements to the map. */ function addElements(elements) { for (let element of elements) element.func( [ avoidClasses.apply(null, element.avoid), stayClasses.apply(null, element.stay || null) ], pickSize(element.sizes), pickMix(element.mixes), pickAmount(element.amounts), element.baseHeight || 0); } /** * Converts "amount" terms to numbers. */ function pickAmount(amounts) { let amount = pickRandom(amounts); if (amount in g_Amounts) return g_Amounts[amount]; return g_Amounts.normal; } /** * Converts "mix" terms to numbers. */ function pickMix(mixes) { let mix = pickRandom(mixes); if (mix in g_Mixes) return g_Mixes[mix]; return g_Mixes.normal; } /** * Converts "size" terms to numbers. */ function pickSize(sizes) { let size = pickRandom(sizes); if (size in g_Sizes) return g_Sizes[size]; return g_Sizes.normal; } /** * Choose starting locations for all players. * * @param {string} type - "radial", "line", "stronghold", "random" * @param {number} distance - radial distance from the center of the map * @param {number} groupedDistance - space between players within a team * @param {number} startAngle - determined by the map that might want to place something between players * @returns {Array|undefined} - If successful, each element is an object that contains id, angle, x, z for each player */ function addBases(type, distance, groupedDistance, startAngle) { g_Map.log("Creating bases"); let playerIDs = sortAllPlayers(); let teamsArray = getTeamsArray(); switch(type) { case "line": return placeLine(teamsArray, distance, groupedDistance, startAngle); case "radial": return placeRadial(playerIDs, distance, startAngle); case "random": return placeRandom(playerIDs) || placeRadial(playerIDs, distance, startAngle); case "stronghold": return placeStronghold(teamsArray, distance, groupedDistance, startAngle); default: warn("Unknown base placement type:" + type); return undefined; } } /** * Create the base for a single player. * * @param {Object} player - contains id, angle, x, z * @param {boolean} walls - Whether or not iberian gets starting walls */ function createBase(player, walls = true) { placePlayerBase({ "playerID": player.id, "playerPosition": player.position, "PlayerTileClass": g_TileClasses.player, "BaseResourceClass": g_TileClasses.baseResource, "baseResourceConstraint": avoidClasses(g_TileClasses.water, 0), "Walls": g_Map.getSize() > 192 && walls, "CityPatch": { "outerTerrain": g_Terrains.roadWild, "innerTerrain": g_Terrains.road, "painters": [ new TileClassPainter(g_TileClasses.player) ] }, "Chicken": { "template": g_Gaia.chicken }, "Berries": { "template": g_Gaia.fruitBush }, "Mines": { "types": [ { "template": g_Gaia.metalLarge }, { "template": g_Gaia.stoneLarge } ] }, "Trees": { "template": g_Gaia.tree1, "count": currentBiome() == "generic/savanna" ? 5 : 15 }, "Decoratives": { "template": g_Decoratives.grassShort } }); } /** * Return an array where each element is an array of playerIndices of a team. */ function getTeamsArray() { var playerIDs = sortAllPlayers(); var numPlayers = getNumPlayers(); // Group players by team var teams = []; for (let i = 0; i < numPlayers; ++i) { let team = getPlayerTeam(playerIDs[i]); if (team == -1) continue; if (!teams[team]) teams[team] = []; teams[team].push(playerIDs[i]); } // Players without a team get a custom index for (let i = 0; i < numPlayers; ++i) if (getPlayerTeam(playerIDs[i]) == -1) teams.push([playerIDs[i]]); // Remove unused indices return teams.filter(team => true); } /** * Choose a random pattern for placing the bases of the players. */ function randomStartingPositionPattern(teamsArray) { var formats = ["radial"]; var mapSize = g_Map.getSize(); var numPlayers = getNumPlayers(); // Enable stronghold if we have a few teams and a big enough map if (teamsArray.length >= 2 && numPlayers >= 4 && mapSize >= 256) formats.push("stronghold"); // Enable random if we have enough teams or enough players on a big enough map if (mapSize >= 256 && (teamsArray.length >= 3 || numPlayers > 4)) formats.push("random"); // Enable line if we have enough teams and players on a big enough map if (teamsArray.length >= 2 && numPlayers >= 4 && mapSize >= 384) formats.push("line"); return { "setup": pickRandom(formats), "distance": fractionToTiles(randFloat(0.2, 0.35)), "separation": fractionToTiles(randFloat(0.05, 0.1)) }; } /** * Place teams in a line-pattern. * * @param {Array} playerIDs - typically randomized indices of players of a single team * @param {number} distance - radial distance from the center of the map * @param {number} groupedDistance - distance between players * @param {number} startAngle - determined by the map that might want to place something between players. * * @returns {Array} - contains id, angle, x, z for every player */ function placeLine(teamsArray, distance, groupedDistance, startAngle) { let players = []; let mapCenter = g_Map.getCenter(); let dist = fractionToTiles(0.45); for (let i = 0; i < teamsArray.length; ++i) { var safeDist = distance; if (distance + teamsArray[i].length * groupedDistance > dist) safeDist = dist - teamsArray[i].length * groupedDistance; var teamAngle = startAngle + (i + 1) * 2 * Math.PI / teamsArray.length; // Create player base for (let p = 0; p < teamsArray[i].length; ++p) { players[teamsArray[i][p]] = { "id": teamsArray[i][p], "position": Vector2D.add(mapCenter, new Vector2D(safeDist + p * groupedDistance, 0).rotate(-teamAngle)).round() }; createBase(players[teamsArray[i][p]], false); } } return players; } /** * Place players in a circle-pattern. * * @param {Array} playerIDs - order of playerIDs to be placed * @param {number} distance - radial distance from the center of the map * @param {number} startAngle - determined by the map that might want to place something between players */ function placeRadial(playerIDs, distance, startAngle) { let mapCenter = g_Map.getCenter(); let players = []; let numPlayers = getNumPlayers(); for (let i = 0; i < numPlayers; ++i) { let angle = startAngle + i * 2 * Math.PI / numPlayers; players[i] = { "id": playerIDs[i], "position": Vector2D.add(mapCenter, new Vector2D(distance, 0).rotate(-angle)).round() }; createBase(players[i]); } return players; } /** * Place playerbases on random locations on the map meeting the given constraints. */ function placeRandom(playerIDs, constraints = undefined) { let players = randomPlayerLocations(playerIDs, constraints); if (!players) return undefined; for (let player of players) createBase(player); return players; } /** * Choose arbitrary starting locations. */ function randomPlayerLocations(playerIDs, constraints = undefined) { let locations = []; let attempts = 0; let resets = 0; let mapCenter = g_Map.getCenter(); let playerMinDist = fractionToTiles(0.25); let borderDistance = fractionToTiles(0.08); let area = createArea(new MapBoundsPlacer(), undefined, new AndConstraint(constraints)); for (let i = 0; i < getNumPlayers(); ++i) { let position = pickRandom(area.points); // Minimum distance between initial bases must be a quarter of the map diameter if (locations.some(loc => loc.distanceTo(position) < playerMinDist) || position.distanceTo(mapCenter) > mapCenter.x - borderDistance) { --i; ++attempts; // Reset if we're in what looks like an infinite loop if (attempts > 500) { locations = []; i = -1; attempts = 0; ++resets; // Reduce minimum player distance progressively if (resets % 25 == 0) playerMinDist *= 0.975; // If we only pick bad locations, stop trying to place randomly if (resets == 500) { - error("Could not place playerbases!"); + throw new Error("Could not find suitable playerbase locations!"); return undefined; } } continue; } locations[i] = position; } return groupPlayersByLocations(playerIDs, locations); } /** * Pick locations from the given set so that teams end up grouped. * * @param {Array} playerIDs - sorted by teams. * @param {Array} locations - array of Vector2D of possible starting locations. */ function groupPlayersByLocations(playerIDs, locations) { playerIDs = sortPlayers(playerIDs); let minDist = Infinity; let minLocations; // Of all permutations of starting locations, find the one where // the sum of the distances between allies is minimal, weighted by teamsize. heapsPermute(shuffleArray(locations).slice(0, playerIDs.length), v => v.clone(), permutation => { let dist = 0; let teamDist = 0; let teamSize = 0; for (let i = 1; i < playerIDs.length; ++i) { let team1 = getPlayerTeam(playerIDs[i - 1]); let team2 = getPlayerTeam(playerIDs[i]); ++teamSize; if (team1 != -1 && team1 == team2) teamDist += permutation[i - 1].distanceTo(permutation[i]); else { dist += teamDist / teamSize; teamDist = 0; teamSize = 0; } } if (teamSize) dist += teamDist / teamSize; if (dist < minDist) { minDist = dist; minLocations = permutation; } }); let players = []; for (let i = 0; i < playerIDs.length; ++i) players[i] = { "id": playerIDs[i], "position": minLocations[i] }; return players; } /** * Place given players in a stronghold-pattern. * * @param teamsArray - each item is an array of playerIDs placed per stronghold * @param distance - radial distance from the center of the map * @param groupedDistance - distance between neighboring players * @param {number} startAngle - determined by the map that might want to place something between players */ function placeStronghold(teamsArray, distance, groupedDistance, startAngle) { var players = []; var mapCenter = g_Map.getCenter(); for (let i = 0; i < teamsArray.length; ++i) { var teamAngle = startAngle + (i + 1) * 2 * Math.PI / teamsArray.length; var teamPosition = Vector2D.add(mapCenter, new Vector2D(distance, 0).rotate(-teamAngle)); var teamGroupDistance = groupedDistance; // If we have a team of above average size, make sure they're spread out if (teamsArray[i].length > 4) teamGroupDistance = Math.max(fractionToTiles(0.08), groupedDistance); // If we have a solo player, place them on the center of the team's location if (teamsArray[i].length == 1) teamGroupDistance = fractionToTiles(0); // TODO: Ensure players are not placed outside of the map area, similar to placeLine // Create player base for (var p = 0; p < teamsArray[i].length; ++p) { var angle = startAngle + (p + 1) * 2 * Math.PI / teamsArray[i].length; players[teamsArray[i][p]] = { "id": teamsArray[i][p], "position": Vector2D.add(teamPosition, new Vector2D(teamGroupDistance, 0).rotate(-angle)).round() }; createBase(players[teamsArray[i][p]], false); } } return players; } /** * Creates tileClass for the default classes and every class given. * * @param {Array} newClasses * @returns {Object} - maps from classname to ID */ function initTileClasses(newClasses) { var classNames = g_DefaultTileClasses; if (newClasses) classNames = classNames.concat(newClasses); g_TileClasses = {}; for (var className of classNames) g_TileClasses[className] = g_Map.createTileClass(); }