Index: binaries/data/mods/public/gamesettings/attributes/TeamPlacement.js =================================================================== --- binaries/data/mods/public/gamesettings/attributes/TeamPlacement.js +++ binaries/data/mods/public/gamesettings/attributes/TeamPlacement.js @@ -51,14 +51,24 @@ GameSettings.prototype.Attributes.TeamPlacement.prototype.StartingPositions = [ { - "Id": "radial", - "Name": translateWithContext("team placement", "Circle"), + "Id": "groupedCircle", + "Name": translateWithContext("team placement", "Grouped circle"), "Description": translate("Allied players are grouped and placed with opposing players on one circle spanning the map.") }, { - "Id": "line", - "Name": translateWithContext("team placement", "Line"), - "Description": translate("Allied players are placed in a linear pattern."), + "Id": "alternatingCircle", + "Name": translateWithContext("team placement", "Alternating circle"), + "Description": translate("Players are placed alternating with opposing players on one circle spanning the map.") + }, + { + "Id": "river", + "Name": translateWithContext("team placement", "River"), + "Description": translate("Players are placed on two parallel lines. Teams are mostly grouped.") + }, + { + "Id": "groupedLines", + "Name": translateWithContext("team placement", "Grouped lines"), + "Description": translate("Allied players are grouped together in a linear pattern."), }, { "Id": "randomGroup", Index: binaries/data/mods/public/maps/random/ambush.js =================================================================== --- binaries/data/mods/public/maps/random/ambush.js +++ binaries/data/mods/public/maps/random/ambush.js @@ -18,18 +18,18 @@ Engine.SetProgress(10); -var playerIDs; -var playerPosition; if (!isNomad()) { - let pattern = g_MapSettings.TeamPlacement || pickRandom(Object.keys(g_PlayerbaseTypes)); - - [playerIDs, playerPosition] = - createBasesByPattern( - pattern, - g_PlayerbaseTypes[pattern].distance, - g_PlayerbaseTypes[pattern].groupedDistance, - randomAngle()); + let pattern = g_MapSettings.TeamPlacement; + var [playerIDs, playerPosition] = + createBases( + ...playerPlacementByPattern( + pattern, + fractionToTiles(randFloat(0.2, 0.35)), + fractionToTiles(randFloat(0.08, 0.1)), + randomAngle(), + undefined), + undefined); markPlayerAvoidanceArea(playerPosition, defaultPlayerBaseRadius()); } 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": ["groupedCircle", "groupedLines", "randomGroup", "stronghold"] } } Index: binaries/data/mods/public/maps/random/empire.js =================================================================== --- binaries/data/mods/public/maps/random/empire.js +++ binaries/data/mods/public/maps/random/empire.js @@ -17,7 +17,14 @@ const teamsArray = getTeamsArray(); const startAngle = randomAngle(); -createBasesByPattern("stronghold", fractionToTiles(0.37), fractionToTiles(0.04), startAngle); +createBases( + ...playerPlacementByPattern( + "stronghold", + fractionToTiles(0.37), + fractionToTiles(0.04), + startAngle, + undefined), + undefined); Engine.SetProgress(20); // Change the starting angle and add the players again @@ -29,7 +36,14 @@ if (teamsArray.length == 4) rotation = 5/4 * Math.PI; -createBasesByPattern("stronghold", fractionToTiles(0.15), fractionToTiles(0.04), startAngle + rotation); +createBases( + ...playerPlacementByPattern( + "stronghold", + fractionToTiles(0.15), + fractionToTiles(0.04), + startAngle + rotation, + undefined), + undefined); Engine.SetProgress(40); addElements(shuffleArray([ Index: binaries/data/mods/public/maps/random/frontier.js =================================================================== --- binaries/data/mods/public/maps/random/frontier.js +++ binaries/data/mods/public/maps/random/frontier.js @@ -21,12 +21,15 @@ if (!isNomad()) { - let pattern = g_MapSettings.TeamPlacement || pickRandom(Object.keys(g_PlayerbaseTypes)); - createBasesByPattern( - pattern, - g_PlayerbaseTypes[pattern].distance, - g_PlayerbaseTypes[pattern].groupedDistance, - randomAngle()); + let pattern = g_MapSettings.TeamPlacement; + createBases( + ...playerPlacementByPattern( + pattern, + fractionToTiles(randFloat(0.2, 0.35)), + fractionToTiles(randFloat(0.08, 0.1)), + randomAngle(), + undefined), + undefined); } Engine.SetProgress(40); 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": ["groupedCircle", "alternatingCircle", "groupedLines", "randomGroup", "stronghold"] } } Index: binaries/data/mods/public/maps/random/harbor.js =================================================================== --- binaries/data/mods/public/maps/random/harbor.js +++ binaries/data/mods/public/maps/random/harbor.js @@ -25,7 +25,15 @@ const mapCenter = g_Map.getCenter(); const startAngle = randomAngle(); -const [playerIDs, playerPosition] = createBasesByPattern("radial", fractionToTiles(0.38), fractionToTiles(0.05), startAngle); +const [playerIDs, playerPosition] = + createBases( + ...playerPlacementByPattern( + "groupedCircle", + fractionToTiles(0.38), + fractionToTiles(0.05), + startAngle, + undefined), + undefined); Engine.SetProgress(20); addCenterLake(); Index: binaries/data/mods/public/maps/random/hells_pass.js =================================================================== --- binaries/data/mods/public/maps/random/hells_pass.js +++ binaries/data/mods/public/maps/random/hells_pass.js @@ -22,7 +22,14 @@ const teamsArray = getTeamsArray(); const startAngle = randomAngle(); -createBasesByPattern("line", fractionToTiles(0.2), fractionToTiles(0.08), startAngle); +createBases( + ...playerPlacementByPattern( + "groupedLines", + fractionToTiles(0.2), + fractionToTiles(0.08), + startAngle, + undefined), + undefined); Engine.SetProgress(20); placeBarriers(); Index: binaries/data/mods/public/maps/random/lions_den.js =================================================================== --- binaries/data/mods/public/maps/random/lions_den.js +++ binaries/data/mods/public/maps/random/lions_den.js @@ -25,7 +25,14 @@ Engine.SetProgress(10); -createBasesByPattern("radial", fractionToTiles(0.4), fractionToTiles(randFloat(0.05, 0.1)), startAngle); +createBases( + ...playerPlacementByPattern( + "groupedCircle", + fractionToTiles(0.4), + fractionToTiles(randFloat(0.05, 0.1)), + startAngle, + undefined), + undefined); Engine.SetProgress(20); createSunkenTerrain(); Index: binaries/data/mods/public/maps/random/mediterranean.js =================================================================== --- binaries/data/mods/public/maps/random/mediterranean.js +++ binaries/data/mods/public/maps/random/mediterranean.js @@ -180,7 +180,40 @@ new ClumpPlacer(diskArea(defaultPlayerBaseRadius() * 0.8), 0.95, 0.6, Infinity, playerPosition[i]), new SmoothElevationPainter(ELEVATION_SET, g_Map.getHeight(playerPosition[i]), 6)); - createBase(playerIDs[i], playerPosition[i], mapSize >= 384); + placePlayerBase({ + "playerID": playerIDs[i], + "playerPosition": playerPosition[i], + "PlayerTileClass": g_TileClasses.player, + "BaseResourceClass": g_TileClasses.baseResource, + "baseResourceConstraint": avoidClasses(g_TileClasses.water, 0, g_TileClasses.mountain, 0), + "Walls": mapSize >= 384, + "CityPatch": { + "outerTerrain": g_Terrains.roadWild, + "innerTerrain": g_Terrains.road, + "painters": [ + new TileClassPainter(g_TileClasses.player) + ] + }, + "StartingAnimal": { + "template": g_Gaia.startingAnimal + }, + "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 + } + }); } } Engine.SetProgress(50); 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 @@ -156,7 +156,12 @@ if (isNomad()) return; - placeCivDefaultStartingEntities(playerBaseArgs.playerPosition, playerBaseArgs.playerID, playerBaseArgs.Walls !== undefined ? playerBaseArgs.Walls : true); + placeCivDefaultStartingEntities( + playerBaseArgs.playerPosition, + playerBaseArgs.playerID, + playerBaseArgs.Walls !== undefined ? playerBaseArgs.Walls : true, + 6, + BUILDING_ORIENTATION); if (playerBaseArgs.PlayerTileClass) addCivicCenterAreaToClass(playerBaseArgs.playerPosition, playerBaseArgs.PlayerTileClass); @@ -568,14 +573,72 @@ [[], []]); } +/** + * 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); +} + /** * Determine player starting positions on a circular pattern. */ -function playerPlacementCircle(radius, startingAngle = undefined, center = undefined) +function playerPlacementByPattern(patternName, distance = undefined, groupedDistance = undefined, angle = undefined, center = undefined) +{ + if (patternName === undefined) + patternName = g_MapSettings.TeamPlacement; + + switch (patternName) + { + case "groupedCircle": + return playerPlacementCircle(distance, angle, center); + case "alternatingCircle": + return playerPlacementCircle(distance, angle, center, true); + case "river": + return playerPlacementRiver(angle, distance, center); + case "groupedLines": + return playerPlacementGroupedLines(getTeamsArray(), distance, groupedDistance, angle); + case "stronghold": + return playerPlacementStronghold(getTeamsArray(), distance, groupedDistance, angle); + case "randomGroup": + return playerPlacementRandom(sortAllPlayers(), undefined); + default: + throw new Error("Unknown placement pattern: " + patternName); + } +} + +/** + * Determine player starting positions on a circular pattern. + */ +function playerPlacementCircle(radius, startingAngle = undefined, center = undefined, grouped = false) { let startAngle = startingAngle !== undefined ? startingAngle : randomAngle(); let [playerPosition, playerAngle] = distributePointsOnCircle(getNumPlayers(), startAngle, radius, center || g_Map.getCenter()); - return [sortAllPlayers(), playerPosition.map(p => p.round()), playerAngle, startAngle]; + return [grouped ? sortAllPlayers() : primeSortAllPlayers(), playerPosition.map(p => p.round()), playerAngle, startAngle]; } /** @@ -675,6 +738,85 @@ return playerPosition; } +/** + * 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 playerPlacementGroupedLines(teamsArray, distance, groupedDistance, startAngle) +{ + let playerIDs = []; + let playerPosition = []; + + 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; + + for (let p = 0; p < teamsArray[i].length; ++p) + { + playerIDs.push(teamsArray[i][p]); + playerPosition.push(Vector2D.add(mapCenter, new Vector2D(safeDist + p * groupedDistance, 0).rotate(-teamAngle)).round()); + } + } + + return [playerIDs, playerPosition]; +} + +/** + * 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 playerPlacementStronghold(teamsArray, distance, groupedDistance, startAngle) +{ + var mapCenter = g_Map.getCenter(); + + let playerIDs = []; + let playerPosition = []; + + 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 playerPlacementGroupedLines + + // Create player base + for (var p = 0; p < teamsArray[i].length; ++p) + { + var angle = startAngle + (p + 1) * 2 * Math.PI / teamsArray[i].length; + playerIDs.push(teamsArray[i][p]); + playerPosition.push(Vector2D.add(teamPosition, new Vector2D(teamGroupDistance, 0).rotate(-angle)).round()); + } + } + + return [playerIDs, playerPosition]; +} + /** * Returns a random location for each player that meets the given constraints and * orders the playerIDs so that players become grouped by team. 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 @@ -55,13 +55,13 @@ var g_TileClasses; var g_PlayerbaseTypes = { - "line": { - "getPosition": (distance, groupedDistance, startAngle) => placeLine(getTeamsArray(), distance, groupedDistance, startAngle), + "groupedLines": { + "getPosition": (distance, groupedDistance, startAngle) => playerPlacementGroupedLines(getTeamsArray(), distance, groupedDistance, startAngle), "distance": fractionToTiles(randFloat(0.2, 0.35)), "groupedDistance": fractionToTiles(randFloat(0.08, 0.1)), "walls": false }, - "radial": { + "groupedCircle": { "getPosition": (distance, groupedDistance, startAngle) => playerPlacementCircle(distance, startAngle), "distance": fractionToTiles(randFloat(0.25, 0.35)), "groupedDistance": fractionToTiles(randFloat(0.08, 0.1)), @@ -74,7 +74,7 @@ "walls": true }, "stronghold": { - "getPosition": (distance, groupedDistance, startAngle) => placeStronghold(getTeamsArray(), distance, groupedDistance, startAngle), + "getPosition": (distance, groupedDistance, startAngle) => playerPlacementStronghold(getTeamsArray(), distance, groupedDistance, startAngle), "distance": fractionToTiles(randFloat(0.2, 0.35)), "groupedDistance": fractionToTiles(randFloat(0.08, 0.1)), "walls": false @@ -140,7 +140,7 @@ /** * Choose starting locations for all players. * - * @param {string} type - "radial", "line", "stronghold", "randomGroup" + * @param {string} type - "groupedCircle", "groupedLines", "stronghold", "randomGroup" * @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 @@ -148,15 +148,55 @@ */ function createBasesByPattern(type, distance, groupedDistance, startAngle) { - return createBases(...g_PlayerbaseTypes[type].getPosition(distance, groupedDistance, startAngle), g_PlayerbaseTypes[type].walls); + error("createBasesByPattern() has been deprecated. Use playerPlacementByPattern() instead."); + return createBases( + ...playerPlacementByPattern( + type, // patternName + distance, // distanceFromCenter + groupedDistance, + startAngle, // angle + undefined), // center + undefined); // walls } function createBases(playerIDs, playerPosition, walls) { + g_Map.log("createBases() will be deprecated in the future. Please switch to placePlayerBases() instead."); g_Map.log("Creating bases"); + placePlayerBases({ + "PlayerPlacement": [playerIDs, playerPosition], + "PlayerTileClass": g_TileClasses.player, + "BaseResourceClass": g_TileClasses.baseResource, + "baseResourceConstraint": avoidClasses(g_TileClasses.water, 0, g_TileClasses.mountain, 0), + "Walls": g_Map.getSize() > 192 && walls, + "CityPatch": { + "outerTerrain": g_Terrains.roadWild, + "innerTerrain": g_Terrains.road, + "painters": [ + new TileClassPainter(g_TileClasses.player) + ] + }, + "StartingAnimal": { + "template": g_Gaia.startingAnimal + }, + "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 + } + }); - for (let i = 0; i < getNumPlayers(); ++i) - createBase(playerIDs[i], playerPosition[i], walls); return [playerIDs, playerPosition]; } @@ -169,6 +209,7 @@ */ function createBase(playerID, playerPosition, walls) { + g_Map.log("createBase() will be deprecated in the future. Please switch to placePlayerBase() instead."); placePlayerBase({ "playerID": playerID, "playerPosition": playerPosition, @@ -205,116 +246,6 @@ }); } -/** - * 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); -} - -/** - * 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 playerIDs = []; - let playerPosition = []; - - 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; - - for (let p = 0; p < teamsArray[i].length; ++p) - { - playerIDs.push(teamsArray[i][p]); - playerPosition.push(Vector2D.add(mapCenter, new Vector2D(safeDist + p * groupedDistance, 0).rotate(-teamAngle)).round()); - } - } - - return [playerIDs, playerPosition]; -} - -/** - * 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 mapCenter = g_Map.getCenter(); - - let playerIDs = []; - let playerPosition = []; - - 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; - playerIDs.push(teamsArray[i][p]); - playerPosition.push(Vector2D.add(teamPosition, new Vector2D(teamGroupDistance, 0).rotate(-angle)).round()); - } - } - - return [playerIDs, playerPosition]; -} - /** * Creates tileClass for the default classes and every class given. * Index: binaries/data/mods/public/maps/random/stronghold.js =================================================================== --- binaries/data/mods/public/maps/random/stronghold.js +++ binaries/data/mods/public/maps/random/stronghold.js @@ -17,7 +17,15 @@ Engine.SetProgress(20); -const [playerIDs, playerPosition] = createBasesByPattern("stronghold", fractionToTiles(randFloat(0.2, 0.35)), fractionToTiles(randFloat(0.05, 0.1)), randomAngle()); +const [playerIDs, playerPosition] = + createBases( + ...playerPlacementByPattern( + "stronghold", + fractionToTiles(randFloat(0.2, 0.35)), + fractionToTiles(randFloat(0.05, 0.1)), + randomAngle(), + undefined), + undefined); markPlayerAvoidanceArea(playerPosition, defaultPlayerBaseRadius()); Engine.SetProgress(30);