Index: ps/trunk/binaries/data/mods/public/maps/random/rmgen2/setup.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/rmgen2/setup.js (revision 19202) +++ ps/trunk/binaries/data/mods/public/maps/random/rmgen2/setup.js (revision 19203) @@ -1,633 +1,635 @@ 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_MapInfo; var g_TileClasses; var g_Forests; /** * 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) ); } /** * Converts "amount" terms to numbers. */ function pickAmount(amounts) { var amount = amounts[randInt(amounts.length)]; if (amount in g_Amounts) return g_Amounts[amount]; return g_Amounts.normal; } /** * Converts "mix" terms to numbers. */ function pickMix(mixes) { var mix = mixes[randInt(mixes.length)]; if (mix in g_Mixes) return g_Mixes[mix]; return g_Mixes.normal; } /** * Converts "size" terms to numbers. */ function pickSize(sizes) { var size = sizes[randInt(sizes.length)]; if (size in g_Sizes) return g_Sizes[size]; return g_Sizes.normal; } /** * Paints the entire map with a single tile type. */ function resetTerrain(terrain, tc, elevation) { g_MapInfo.mapSize = getMapSize(); g_MapInfo.mapArea = g_MapInfo.mapSize * g_MapInfo.mapSize; g_MapInfo.centerOfMap = Math.floor(g_MapInfo.mapSize / 2); g_MapInfo.mapRadius = -PI / 4; var placer = new ClumpPlacer(g_MapInfo.mapArea, 1, 1, 1, g_MapInfo.centerOfMap, g_MapInfo.centerOfMap); var terrainPainter = new LayeredPainter([terrain], []); var elevationPainter = new SmoothElevationPainter(ELEVATION_SET, elevation, 1); createArea(placer, [terrainPainter, elevationPainter, paintClass(tc)], null); g_MapInfo.mapHeight = elevation; } /** * Chose starting locations for the given players. * * @param {string} type - "radial", "stacked", "stronghold", "random" * @param {number} distance - radial distance from the center of the map */ function addBases(type, distance, groupedDistance) { type = type || "radial"; distance = distance || 0.3; groupedDistance = groupedDistance || 0.05; var playerIDs = randomizePlayers(); - var players = {}; switch(type) { case "line": - players = placeLine(playerIDs, distance, groupedDistance); - break; + return placeLine(playerIDs, distance, groupedDistance); case "radial": - players = placeRadial(playerIDs, distance); - break; + return placeRadial(playerIDs, distance); case "random": - players = placeRandom(playerIDs); - break; + return placeRandom(playerIDs) || placeRadial(playerIDs, distance); case "stronghold": - players = placeStronghold(playerIDs, distance, groupedDistance); - break; + return placeStronghold(playerIDs, distance, groupedDistance); } - - return players; } /** * 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) { // Get the x and z in tiles var fx = fractionToTiles(player.x); var fz = fractionToTiles(player.z); var ix = round(fx); var iz = round(fz); // Mark player position and a tile in each direction to avoid placing things inside the CC addToClass(ix, iz, g_TileClasses.player); addToClass(ix + 5, iz, g_TileClasses.player); addToClass(ix, iz + 5, g_TileClasses.player); addToClass(ix - 5, iz, g_TileClasses.player); addToClass(ix, iz - 5, g_TileClasses.player); // Create starting units if (walls && g_MapInfo.mapSize > 192) placeCivDefaultEntities(fx, fz, player.id); else placeCivDefaultEntities(fx, fz, player.id, { 'iberWall': false }); // Create the city patch var cityRadius = scaleByMapSize(15, 25) / 3; var placer = new ClumpPlacer(PI * cityRadius * cityRadius, 0.6, 0.3, 10, ix, iz); var painter = new LayeredPainter([g_Terrains.roadWild, g_Terrains.road], [1]); createArea(placer, painter, null); // TODO: retry loops are needed as resources might conflict with neighboring ones // Create initial berry bushes at random angle var bbAngle = randFloat(0, TWO_PI); var bbDist = 10; var bbX = round(fx + bbDist * cos(bbAngle)); var bbZ = round(fz + bbDist * sin(bbAngle)); var group = new SimpleGroup( [new SimpleObject(g_Gaia.fruitBush, 5, 5, 0, 3)], true, g_TileClasses.baseResource, bbX, bbZ ); createObjectGroup(group, 0, avoidClasses(g_TileClasses.baseResource, 2)); // Create metal mine at a different angle var mAngle = bbAngle; while(abs(mAngle - bbAngle) < PI / 3) mAngle = randFloat(0, TWO_PI); var mDist = 12; var mX = round(fx + mDist * cos(mAngle)); var mZ = round(fz + mDist * sin(mAngle)); group = new SimpleGroup( [new SimpleObject(g_Gaia.metalLarge, 1, 1, 0, 0)], true, g_TileClasses.baseResource, mX, mZ ); createObjectGroup(group, 0, avoidClasses(g_TileClasses.baseResource, 2)); // Create stone mine beside metal mAngle += randFloat(PI / 8, PI / 4); mX = round(fx + mDist * cos(mAngle)); mZ = round(fz + mDist * sin(mAngle)); group = new SimpleGroup( [new SimpleObject(g_Gaia.stoneLarge, 1, 1, 0, 2)], true, g_TileClasses.baseResource, mX, mZ ); createObjectGroup(group, 0, avoidClasses(g_TileClasses.baseResource, 2)); placeDefaultChicken( fx, fz, g_TileClasses.baseResource, avoidClasses(g_TileClasses.baseResource, 4), g_Gaia.chicken ); var hillSize = PI * g_MapInfo.mapRadius * g_MapInfo.mapRadius; // Create starting trees var num = g_MapInfo.biome == g_BiomeSavanna ? 5 : 15; for (var tries = 0; tries < 10; ++tries) { var tAngle = randFloat(0, TWO_PI); var tDist = randFloat(12, 13); var tX = round(fx + tDist * cos(tAngle)); var tZ = round(fz + tDist * sin(tAngle)); group = new SimpleGroup( [new SimpleObject(g_Gaia.tree1, num, num, 1, 3)], false, g_TileClasses.baseResource, tX, tZ ); if (createObjectGroup(group, 0, avoidClasses(g_TileClasses.baseResource, 4))) break; } // Create grass tufts var num = hillSize / 250; for (var j = 0; j < num; ++j) { var gAngle = randFloat(0, TWO_PI); var gDist = g_MapInfo.mapRadius - (5 + randInt(7)); var gX = round(fx + gDist * cos(gAngle)); var gZ = round(fz + gDist * sin(gAngle)); group = new SimpleGroup( [new SimpleObject(g_Decoratives.grassShort, 2, 5, 0, 1, -PI / 8, PI / 8)], false, g_TileClasses.baseResource, gX, gZ ); createObjectGroup(group, 0, avoidClasses(g_TileClasses.baseResource, 4)); } } /** * Return an array where each element is an array of playerIndices of a team. */ function getTeams(numPlayers) { // Group players by team var teams = []; for (var i = 0; i < numPlayers; ++i) { let team = getPlayerTeam(i); if (team == -1) continue; if (!teams[team]) teams[team] = []; teams[team].push(i+1); } // Players without a team get a custom index for (var i = 0; i < numPlayers; ++i) if (getPlayerTeam(i) == -1) teams.push([i+1]); // Remove unused indices return teams.filter(team => true); } /** * Chose a random pattern for placing the bases of the players. */ function randomStartingPositionPattern() { var formats = ["radial"]; // Enable stronghold if we have a few teams and a big enough map if (g_MapInfo.teams.length >= 2 && g_MapInfo.numPlayers >= 4 && g_MapInfo.mapSize >= 256) formats.push("stronghold"); // Enable random if we have enough teams or enough players on a big enough map if (g_MapInfo.mapSize >= 256 && (g_MapInfo.teams.length >= 3 || g_MapInfo.numPlayers > 4)) formats.push("random"); // Enable line if we have enough teams and players on a big enough map if (g_MapInfo.teams.length >= 2 && g_MapInfo.numPlayers >= 4 && g_MapInfo.mapSize >= 384) formats.push("line"); return { "setup": formats[randInt(formats.length)], "distance": randFloat(0.2, 0.35), "separation": randFloat(0.05, 0.1) }; } /** * Mix player indices but sort by team. * * @returns {Array} - every item is an array of player indices */ function randomizePlayers() { var playerIDs = []; for (var i = 0; i < g_MapInfo.numPlayers; ++i) playerIDs.push(i + 1); return sortPlayers(playerIDs); } /** * 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 * * @returns {Array} - contains id, angle, x, z for every player */ function placeLine(playerIDs, distance, groupedDistance) { var players = []; for (var i = 0; i < g_MapInfo.teams.length; ++i) { var safeDist = distance; if (distance + g_MapInfo.teams[i].length * groupedDistance > 0.45) safeDist = 0.45 - g_MapInfo.teams[i].length * groupedDistance; var teamAngle = g_MapInfo.startAngle + (i + 1) * TWO_PI / g_MapInfo.teams.length; // Create player base for (var p = 0; p < g_MapInfo.teams[i].length; ++p) { players[g_MapInfo.teams[i][p]] = { "id": g_MapInfo.teams[i][p], "angle": g_MapInfo.startAngle + (p + 1) * TWO_PI / g_MapInfo.teams[i].length, "x": 0.5 + (safeDist + p * groupedDistance) * cos(teamAngle), "z": 0.5 + (safeDist + p * groupedDistance) * sin(teamAngle) }; createBase(players[g_MapInfo.teams[i][p]], false); } } return players; } /** * Place players in a circle-pattern. * * @param {number} distance - radial distance from the center of the map */ function placeRadial(playerIDs, distance) { var players = new Array(g_MapInfo.numPlayers); for (var i = 0; i < g_MapInfo.numPlayers; ++i) { var angle = g_MapInfo.startAngle + i * TWO_PI / g_MapInfo.numPlayers; players[i] = { "id": playerIDs[i], "angle": angle, "x": 0.5 + distance * cos(angle), "z": 0.5 + distance * sin(angle) }; createBase(players[i]); } return players; } /** * Chose arbitrary starting locations. */ function placeRandom(playerIDs) { var players = []; var placed = []; + var attempts = 0; + var resets = 0; + for (var i = 0; i < g_MapInfo.numPlayers; ++i) { - var attempts = 0; var playerAngle = randFloat(0, TWO_PI); + + // Distance from the center of the map in percent + // Mapsize being used as a diameter, so 0.5 is the edge of the map var distance = randFloat(0, 0.42); var x = 0.5 + distance * cos(playerAngle); var z = 0.5 + distance * sin(playerAngle); var tooClose = false; for (var j = 0; j < placed.length; ++j) { + // Minimum distance between initial bases must be a quarter of the map diameter var sep = getDistance(x, z, placed[j].x, placed[j].z); if (sep < 0.25) { tooClose = true; break; } } if (tooClose) { --i; ++attempts; // Reset if we're in what looks like an infinite loop if (attempts > 100) { players = []; placed = []; i = -1; attempts = 0; - } + ++resets; + // If we only pick bad locations, stop trying to place randomly + if (resets == 100) + return undefined; + } continue; } players[i] = { "id": playerIDs[i], "angle": playerAngle, "x": x, "z": z }; placed.push(players[i]); } - // Create the bases for (var i = 0; i < g_MapInfo.numPlayers; ++i) createBase(players[i]); return players; } /** * Place given players in a stronghold-pattern. * * @param distance - radial distance from the center of the map * @param groupedDistance - distance between neighboring players */ function placeStronghold(playerIDs, distance, groupedDistance) { var players = []; for (var i = 0; i < g_MapInfo.teams.length; ++i) { var teamAngle = g_MapInfo.startAngle + (i + 1) * TWO_PI / g_MapInfo.teams.length; var fractionX = 0.5 + distance * cos(teamAngle); var fractionZ = 0.5 + distance * sin(teamAngle); var teamGroupDistance = groupedDistance; // If we have a team of above average size, make sure they're spread out if (g_MapInfo.teams[i].length > 4) teamGroupDistance = Math.max(0.08, groupedDistance); // If we have a solo player, place them on the center of the team's location if (g_MapInfo.teams[i].length == 1) teamGroupDistance = 0; // TODO: Ensure players are not placed outside of the map area, similar to placeLine // Create player base for (var p = 0; p < g_MapInfo.teams[i].length; ++p) { var angle = g_MapInfo.startAngle + (p + 1) * TWO_PI / g_MapInfo.teams[i].length; players[g_MapInfo.teams[i][p]] = { "id": g_MapInfo.teams[i][p], "angle": angle, "x": fractionX + teamGroupDistance * cos(angle), "z": fractionZ + teamGroupDistance * sin(angle) }; createBase(players[g_MapInfo.teams[i][p]], false); } } return players; } /** * Places players either randomly or in a stronghold-pattern at a set of given heightmap coordinates. * * @param singleBases - pair of coordinates of the heightmap to place isolated bases. * @param singleBases - pair of coordinates of the heightmap to place team bases. * @param groupedDistance - distance between neighboring players. * @param func - A function called for every player base or stronghold placed. */ function randomPlayerPlacementAt(singleBases, strongholdBases, heightmapScale, groupedDistance, func) { let strongholdBasesRandom = shuffleArray(strongholdBases); let singleBasesRandom = shuffleArray(singleBases); if (randInt(2) == 1 && g_MapInfo.mapSize >= 256 && g_MapInfo.teams.length >= 2 && g_MapInfo.teams.length < g_MapInfo.numPlayers && g_MapInfo.teams.length <= strongholdBasesRandom.length) { for (let t = 0; t < g_MapInfo.teams.length; ++t) { let tileX = Math.floor(strongholdBasesRandom[t][0] / heightmapScale); let tileY = Math.floor(strongholdBasesRandom[t][1] / heightmapScale); let x = tileX / g_MapInfo.mapSize; let z = tileY / g_MapInfo.mapSize; let team = g_MapInfo.teams[t].map(playerID => ({ "id": playerID })); let players = []; if (func) func(tileX, tileY); for (let p = 0; p < team.length; ++p) { let angle = g_MapInfo.startAngle + (p + 1) * TWO_PI / team.length; players[p] = { "id": team[p].id, "angle": angle, "x": x + groupedDistance * cos(angle), "z": z + groupedDistance * sin(angle) }; createBase(players[p], false); } } } else { let players = randomizePlayers(); for (let p = 0; p < players.length; ++p) { let tileX = Math.floor(singleBasesRandom[p][0] / heightmapScale); let tileY = Math.floor(singleBasesRandom[p][1] / heightmapScale); if (func) func(tileX, tileY); createBase({ "id": players[p], "x": tileX / g_MapInfo.mapSize, "z": tileY / g_MapInfo.mapSize }); } } } /** * 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 !== undefined) classNames = classNames.concat(newClasses); g_TileClasses = {}; for (var className of classNames) g_TileClasses[className] = createTileClass(); } /** * Get biome-specific names of entities and terrain after randomization. */ function initBiome() { g_Forests = { "forest1": [ g_Terrains.forestFloor2 + TERRAIN_SEPARATOR + g_Gaia.tree1, g_Terrains.forestFloor2 + TERRAIN_SEPARATOR + g_Gaia.tree2, g_Terrains.forestFloor2 ], "forest2": [ g_Terrains.forestFloor1 + TERRAIN_SEPARATOR + g_Gaia.tree4, g_Terrains.forestFloor1 + TERRAIN_SEPARATOR + g_Gaia.tree5, g_Terrains.forestFloor1 ] }; } /** * Creates an object of commonly used functions. */ function initMapSettings() { initBiome(); let numPlayers = getNumPlayers(); g_MapInfo = { "biome": g_BiomeID, "numPlayers": numPlayers, "teams": getTeams(numPlayers), "startAngle": randFloat(0, TWO_PI) }; }