Index: binaries/data/mods/public/gui/gamesettings/attributes/TeamPlacement.js =================================================================== --- binaries/data/mods/public/gui/gamesettings/attributes/TeamPlacement.js +++ binaries/data/mods/public/gui/gamesettings/attributes/TeamPlacement.js @@ -71,5 +71,10 @@ "Id": "stronghold", "Name": translateWithContext("team placement", "Stronghold"), "Description": translate("Allied players are grouped in one random place of the map."), + }, + { + "Id": "besideAllies", + "Name": translateWithContext("team placement", "Beside Allies"), + "Description": translate("Allied players are grouped closely together equidistant to opposing teams.") } ]; Index: binaries/data/mods/public/maps/random/african_plains.js =================================================================== --- binaries/data/mods/public/maps/random/african_plains.js +++ binaries/data/mods/public/maps/random/african_plains.js @@ -1,5 +1,6 @@ Engine.LoadLibrary("rmgen"); Engine.LoadLibrary("rmgen-common"); +Engine.LoadLibrary("rmgen2"); Engine.LoadLibrary("rmbiome"); if (g_MapSettings.Biome) @@ -68,41 +69,60 @@ var clFood = g_Map.createTileClass(); var clBaseResource = g_Map.createTileClass(); -placePlayerBases({ - "PlayerPlacement": playerPlacementCircle(fractionToTiles(0.35)), - "PlayerTileClass": clPlayer, - "BaseResourceClass": clBaseResource, - "CityPatch": { - "outerTerrain": tPrimary, - "innerTerrain": tCitytiles - }, - "Chicken": { - }, - "Berries": { - "template": oBerryBush - }, - "Mines": { - "types": [ - { - "template": oMetalLarge +if (!isNomad()) +{ + const pattern = g_MapSettings.TeamPlacement || pickRandom(Object.keys(g_PlayerbaseTypes)); + createBasesByPattern( + pattern, + g_PlayerbaseTypes[pattern].distance, + g_PlayerbaseTypes[pattern].groupedDistance, + randomAngle(), + createBasesAfricanPlains); +} + +function createBasesAfricanPlains(playerIDs, playerPosition, walls) +{ + for (let i = 0; i < getNumPlayers(); ++i) + { + placePlayerBase({ + "playerID": playerIDs[i], + "playerPosition": playerPosition[i], + "PlayerTileClass": clPlayer, + "BaseResourceClass": clBaseResource, + "Walls": g_Map.getSize() > 192 && walls, + "CityPatch": { + "outerTerrain": tPrimary, + "innerTerrain": tCitytiles }, - { - "type": "stone_formation", - "template": oStoneSmall, - "terrain": tDirt4 + "Chicken": { + }, + "Berries": { + "template": oBerryBush + }, + "Mines": { + "types": [ + { + "template": oMetalLarge + }, + { + "type": "stone_formation", + "template": oStoneSmall, + "terrain": tDirt4 + } + ] + }, + "Trees": { + "template": oBaobab, + "count": scaleByMapSize(3, 12), + "minDistGroup": 2, + "maxDistGroup": 6, + "minDist": 15, + "maxDist": 16 } - ] - }, - "Trees": { - "template": oBaobab, - "count": scaleByMapSize(3, 12), - "minDistGroup": 2, - "maxDistGroup": 6, - "minDist": 15, - "maxDist": 16 + // No decoratives + }); } - // No decoratives -}); +} Engine.SetProgress(20); // The specificity of this map is a bunch of watering holes & hills making it somewhat cluttered. Index: binaries/data/mods/public/maps/random/african_plains.json =================================================================== --- binaries/data/mods/public/maps/random/african_plains.json +++ binaries/data/mods/public/maps/random/african_plains.json @@ -5,6 +5,7 @@ "Description" : "The central region of the vast continent of Africa, birthplace of humanity. Players start in a lush area teeming with vegetation and wildlife.", "SupportedBiomes": ["generic/savanna", "generic/sahara", "generic/nubia"], "Preview" : "african_plains.png", - "CircularMap" : true + "CircularMap" : true, + "TeamPlacements": ["radial", "line", "stronghold", "besideAllies", "randomGroup"] } } Index: binaries/data/mods/public/maps/random/ambush.json =================================================================== --- binaries/data/mods/public/maps/random/ambush.json +++ binaries/data/mods/public/maps/random/ambush.json @@ -6,6 +6,6 @@ "Preview" : "ambush.png", "SupportedBiomes": "generic/", "CircularMap" : true, - "TeamPlacements": ["radial", "line", "randomGroup", "stronghold"] + "TeamPlacements": ["radial", "line", "randomGroup", "stronghold", "besideAllies"] } } Index: binaries/data/mods/public/maps/random/frontier.json =================================================================== --- binaries/data/mods/public/maps/random/frontier.json +++ binaries/data/mods/public/maps/random/frontier.json @@ -6,6 +6,6 @@ "Preview" : "frontier.png", "SupportedBiomes": "generic/", "CircularMap" : true, - "TeamPlacements": ["radial", "line", "randomGroup", "stronghold"] + "TeamPlacements": ["radial", "line", "randomGroup", "stronghold", "besideAllies"] } } Index: binaries/data/mods/public/maps/random/mainland.js =================================================================== --- binaries/data/mods/public/maps/random/mainland.js +++ binaries/data/mods/public/maps/random/mainland.js @@ -1,5 +1,6 @@ Engine.LoadLibrary("rmgen"); Engine.LoadLibrary("rmgen-common"); +Engine.LoadLibrary("rmgen2"); Engine.LoadLibrary("rmbiome"); setSelectedBiome(); @@ -54,33 +55,52 @@ var clFood = g_Map.createTileClass(); var clBaseResource = g_Map.createTileClass(); -placePlayerBases({ - "PlayerPlacement": playerPlacementCircle(fractionToTiles(0.35)), - "PlayerTileClass": clPlayer, - "BaseResourceClass": clBaseResource, - "CityPatch": { - "outerTerrain": tRoadWild, - "innerTerrain": tRoad - }, - "Chicken": { - }, - "Berries": { - "template": oFruitBush - }, - "Mines": { - "types": [ - { "template": oMetalLarge }, - { "template": oStoneLarge } - ] - }, - "Trees": { - "template": oTree1, - "count": 5 - }, - "Decoratives": { - "template": aGrassShort +if (!isNomad()) +{ + const pattern = g_MapSettings.TeamPlacement || pickRandom(Object.keys(g_PlayerbaseTypes)); + createBasesByPattern( + pattern, + g_PlayerbaseTypes[pattern].distance, + g_PlayerbaseTypes[pattern].groupedDistance, + randomAngle(), + createBasesMainland); +} + +function createBasesMainland(playerIDs, playerPosition, walls) +{ + for (let i = 0; i < getNumPlayers(); ++i) + { + placePlayerBase({ + "playerID": playerIDs[i], + "playerPosition": playerPosition[i], + "PlayerTileClass": clPlayer, + "BaseResourceClass": clBaseResource, + "Walls": g_Map.getSize() > 192 && walls, + "CityPatch": { + "outerTerrain": tRoadWild, + "innerTerrain": tRoad + }, + "Chicken": { + }, + "Berries": { + "template": oFruitBush + }, + "Mines": { + "types": [ + { "template": oMetalLarge }, + { "template": oStoneLarge } + ] + }, + "Trees": { + "template": oTree1, + "count": 5 + }, + "Decoratives": { + "template": aGrassShort + } + }); } -}); +} Engine.SetProgress(20); createBumps(avoidClasses(clPlayer, 20)); Index: binaries/data/mods/public/maps/random/mainland.json =================================================================== --- binaries/data/mods/public/maps/random/mainland.json +++ binaries/data/mods/public/maps/random/mainland.json @@ -6,6 +6,7 @@ "Preview" : "mainland.png", "Keywords": ["multiplayer"], "SupportedBiomes": "generic/", - "CircularMap" : true + "CircularMap" : true, + "TeamPlacements": ["radial", "line", "stronghold", "besideAllies", "randomGroup"] } } Index: binaries/data/mods/public/maps/random/rmgen-common/player.js =================================================================== --- binaries/data/mods/public/maps/random/rmgen-common/player.js +++ binaries/data/mods/public/maps/random/rmgen-common/player.js @@ -843,3 +843,81 @@ return [playerIDs, startLocations]; } + +/** + * Place on an arc on a circle, 1 arc per team. Each team member is on the same arc. + * @param {Array[int]} playerIDs + * @param {float} radius + * @param {float} mapAngle + */ +function playerPlacementMultiArcs(playerIDs, radius, mapAngle) +{ + const teamIDs = playerIDs.map(function(playerID, i) { + const teamID = getPlayerTeam(playerID); + // If teamID is -1 (which is None), set to any unique number. + // This can be any number above 3. (The max 0ad teamID is 3.) + return teamID === -1 ? 1000+i : teamID; + }); + const uniqueTeams = new Set(teamIDs); + const nTeams = uniqueTeams.size; + + const teamIntMap = {}; + const teamFreqPlayers = {}; + const teamPlayersIntMap = {}; + + // Shuffle team order. + let i = 0; + const teamShuffle = Array.from(Array(nTeams), () => i++); + for(i = teamShuffle.length - 1; i > 0; i--) + { + const j = Math.round(Math.random() * (teamShuffle.length-1)); + const temp = teamShuffle[i]; + teamShuffle[i] = teamShuffle[j]; + teamShuffle[j] = temp; + } + + // Team to array (random) index map + team player int map. + // This is used when positioning players on a team. + Array.from(uniqueTeams).forEach(function(val, idx) { + teamIntMap[val] = teamShuffle[idx]; + teamPlayersIntMap[val] = 0; + }); + + // teamPlayersIntMap and teamIntMap is used to calculate how much angle to + // add to a player, based on what order in the circle they're placed. + + // Player frequency in teams. + teamIDs.forEach(function(v) { teamFreqPlayers[v] = (teamFreqPlayers[v] || 0) + 1; }); + + // teamPlayersIntMap gives the number of ally gaps between players + // placed at an earlier point around the circle. + for (const key in teamPlayersIntMap) + { + if (teamPlayersIntMap.hasOwnProperty(key)) + { + for (const key2 in teamPlayersIntMap) + { + if (teamPlayersIntMap.hasOwnProperty(key)) + { + if (teamIntMap[key2] > teamIntMap[key]) + { + // -1 because e.g. if there's 3 allies, there's only 2 gaps between them. + teamPlayersIntMap[key2] += teamFreqPlayers[key]-1; + } + } + } + } + } + + // The gap is dependent on the value of radius(g_PlayerbaseTypes[pattern].distance) + const allyGapAngle = 0.6303860648814489; + const teamGapAngle = (2 * Math.PI - allyGapAngle * (playerIDs.length-nTeams)) / nTeams; + + const playerAngleFunc = function(idx) + { + return mapAngle + teamGapAngle*teamIntMap[teamIDs[idx]] + allyGapAngle*((teamPlayersIntMap[teamIDs[idx]]++)); + }; + + const [playerPosition, playerAngle] = playerPlacementCustomAngle(radius, g_Map.getCenter(), playerAngleFunc); + return [playerIDs, playerPosition.map(p => p.round()), playerAngle, mapAngle]; +} Index: binaries/data/mods/public/maps/random/rmgen2/setup.js =================================================================== --- binaries/data/mods/public/maps/random/rmgen2/setup.js +++ binaries/data/mods/public/maps/random/rmgen2/setup.js @@ -78,6 +78,12 @@ "distance": fractionToTiles(randFloat(0.2, 0.35)), "groupedDistance": fractionToTiles(randFloat(0.08, 0.1)), "walls": false + }, + "besideAllies": { + "getPosition": (distance, groupedDistance, startAngle) => playerPlacementMultiArcs(sortAllPlayers(), distance, startAngle), + "distance": fractionToTiles(randFloat(0.3, 0.35)), + "groupedDistance": null, // The gap between allies is computed within the function + "walls": false } }; @@ -144,11 +150,12 @@ * @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 + * @param {function} createBaseFunc - function to create a base for a single player (optional). * @returns {Array|undefined} - If successful, each element is an object that contains id, angle, x, z for each player */ -function createBasesByPattern(type, distance, groupedDistance, startAngle) +function createBasesByPattern(type, distance, groupedDistance, startAngle, createBasesFunc = createBases) { - return createBases(...g_PlayerbaseTypes[type].getPosition(distance, groupedDistance, startAngle), g_PlayerbaseTypes[type].walls); + return createBasesFunc(...g_PlayerbaseTypes[type].getPosition(distance, groupedDistance, startAngle), g_PlayerbaseTypes[type].walls); } function createBases(playerIDs, playerPosition, walls)