LCOV - code coverage report
Current view: top level - maps/random/rmgen-common - player.js (source / functions) Hit Total Coverage
Test: lcov.info Lines: 0 303 0.0 %
Date: 2023-04-02 12:52:40 Functions: 0 53 0.0 %

          Line data    Source code
       1             : /**
       2             :  * @file These functions locate and place the starting entities of players.
       3             :  */
       4             : 
       5           0 : var g_NomadTreasureTemplates = {
       6             :         "food": "gaia/treasure/food_jars",
       7             :         "wood": "gaia/treasure/wood",
       8             :         "stone": "gaia/treasure/stone",
       9             :         "metal": "gaia/treasure/metal"
      10             : };
      11             : 
      12             : /**
      13             :  * These are identifiers of functions that can generate parts of a player base.
      14             :  * There must be a function starting with placePlayerBase and ending with this name.
      15             :  * This is a global so mods can extend this from external files.
      16             :  */
      17           0 : var g_PlayerBaseFunctions = [
      18             :         // Possibly mark player class first here and use it afterwards
      19             :         "CityPatch",
      20             :         // Create the largest and most important entities first
      21             :         "Trees",
      22             :         "Mines",
      23             :         "Treasures",
      24             :         "Berries",
      25             :         "StartingAnimal",
      26             :         "Decoratives"
      27             : ];
      28             : 
      29             : function isNomad()
      30             : {
      31           0 :         return !!g_MapSettings.Nomad;
      32             : }
      33             : 
      34             : function getNumPlayers()
      35             : {
      36           0 :         return g_MapSettings.PlayerData.length - 1;
      37             : }
      38             : 
      39             : function getCivCode(playerID)
      40             : {
      41           0 :         return g_MapSettings.PlayerData[playerID].Civ;
      42             : }
      43             : 
      44             : function areAllies(playerID1, playerID2)
      45             : {
      46           0 :         return g_MapSettings.PlayerData[playerID1].Team !== undefined &&
      47             :                g_MapSettings.PlayerData[playerID2].Team !== undefined &&
      48             :                g_MapSettings.PlayerData[playerID1].Team != -1 &&
      49             :                g_MapSettings.PlayerData[playerID2].Team != -1 &&
      50             :                g_MapSettings.PlayerData[playerID1].Team === g_MapSettings.PlayerData[playerID2].Team;
      51             : }
      52             : 
      53             : function getPlayerTeam(playerID)
      54             : {
      55           0 :         if (g_MapSettings.PlayerData[playerID].Team === undefined)
      56           0 :                 return -1;
      57             : 
      58           0 :         return g_MapSettings.PlayerData[playerID].Team;
      59             : }
      60             : 
      61             : /**
      62             :  * Gets the default starting entities for the civ of the given player, as defined by the civ file.
      63             :  */
      64             : function getStartingEntities(playerID)
      65             : {
      66           0 :         return g_CivData[getCivCode(playerID)].StartEntities;
      67             : }
      68             : 
      69             : /**
      70             :  * Places the given entities at the given location (typically a civic center and starting units).
      71             :  * @param location - A Vector2D specifying tile coordinates.
      72             :  * @param civEntities - An array of objects with the Template property and optionally a Count property.
      73             :  * The first entity is placed in the center, the other ones surround it.
      74             :  */
      75             : function placeStartingEntities(location, playerID, civEntities, dist = 6, orientation = BUILDING_ORIENTATION)
      76             : {
      77             :         // Place the central structure
      78           0 :         let i = 0;
      79           0 :         let firstTemplate = civEntities[i].Template;
      80           0 :         if (firstTemplate.startsWith("structures/"))
      81             :         {
      82           0 :                 g_Map.placeEntityPassable(firstTemplate, playerID, location, orientation);
      83           0 :                 ++i;
      84             :         }
      85             : 
      86             :         // Place entities surrounding it
      87           0 :         let space = 2;
      88           0 :         for (let j = i; j < civEntities.length; ++j)
      89             :         {
      90           0 :                 let angle = orientation - Math.PI * (1 - j / 2);
      91           0 :                 let count = civEntities[j].Count || 1;
      92             : 
      93           0 :                 for (let num = 0; num < count; ++num)
      94             :                 {
      95           0 :                         let position = Vector2D.sum([
      96             :                                 location,
      97             :                                 new Vector2D(dist, 0).rotate(-angle),
      98             :                                 new Vector2D(space * (-num + (count - 1) / 2), 0).rotate(angle)
      99             :                         ]);
     100             : 
     101           0 :                         g_Map.placeEntityPassable(civEntities[j].Template, playerID, position, angle);
     102             :                 }
     103             :         }
     104             : }
     105             : 
     106             : /**
     107             :  * Places the default starting entities as defined by the civilization definition, optionally including city walls.
     108             :  */
     109             : function placeCivDefaultStartingEntities(position, playerID, wallType, dist = 6, orientation = BUILDING_ORIENTATION)
     110             : {
     111           0 :         placeStartingEntities(position, playerID, getStartingEntities(playerID), dist, orientation);
     112           0 :         placeStartingWalls(position, playerID, wallType, orientation);
     113             : }
     114             : 
     115             : /**
     116             :  * If the map is large enough and the civilization defines them, places the initial city walls or towers.
     117             :  * @param {string|boolean} wallType - Either "towers" to only place the wall turrets or a boolean indicating enclosing city walls.
     118             :  */
     119             : function placeStartingWalls(position, playerID, wallType, orientation = BUILDING_ORIENTATION)
     120             : {
     121           0 :         let civ = getCivCode(playerID);
     122           0 :         if (civ != "iber" || g_Map.getSize() <= 128)
     123           0 :                 return;
     124             : 
     125             :         // TODO: should prevent trees inside walls
     126             :         // When fixing, remove the DeleteUponConstruction flag from template_gaia_flora.xml
     127             : 
     128           0 :         if (wallType == "towers")
     129           0 :                 placePolygonalWall(position, 15, ["entry"], "tower", civ, playerID, orientation, 7);
     130           0 :         else if (wallType)
     131           0 :                 placeGenericFortress(position, 20, playerID);
     132             : }
     133             : 
     134             : /**
     135             :  * Places the civic center and starting resources for all given players.
     136             :  */
     137             : function placePlayerBases(playerBaseArgs)
     138             : {
     139           0 :         g_Map.log("Creating playerbases");
     140             : 
     141           0 :         let [playerIDs, playerPosition] = playerBaseArgs.PlayerPlacement;
     142             : 
     143           0 :         for (let i = 0; i < getNumPlayers(); ++i)
     144             :         {
     145           0 :                 playerBaseArgs.playerID = playerIDs[i];
     146           0 :                 playerBaseArgs.playerPosition = playerPosition[i];
     147           0 :                 placePlayerBase(playerBaseArgs);
     148             :         }
     149             : }
     150             : 
     151             : /**
     152             :  * Places the civic center and starting resources.
     153             :  */
     154             : function placePlayerBase(playerBaseArgs)
     155             : {
     156           0 :         if (isNomad())
     157           0 :                 return;
     158             : 
     159           0 :         placeCivDefaultStartingEntities(playerBaseArgs.playerPosition, playerBaseArgs.playerID, playerBaseArgs.Walls !== undefined ? playerBaseArgs.Walls : true);
     160             : 
     161           0 :         if (playerBaseArgs.PlayerTileClass)
     162           0 :                 addCivicCenterAreaToClass(playerBaseArgs.playerPosition, playerBaseArgs.PlayerTileClass);
     163             : 
     164           0 :         for (let functionID of g_PlayerBaseFunctions)
     165             :         {
     166           0 :                 let funcName = "placePlayerBase" + functionID;
     167           0 :                 let func = global[funcName];
     168           0 :                 if (!func)
     169           0 :                         throw new Error("Could not find " + funcName);
     170             : 
     171           0 :                 if (!playerBaseArgs[functionID])
     172           0 :                         continue;
     173             : 
     174           0 :                 let args = playerBaseArgs[functionID];
     175             : 
     176             :                 // Copy some global arguments to the arguments for each function
     177           0 :                 for (let prop of ["playerID", "playerPosition", "BaseResourceClass", "baseResourceConstraint"])
     178           0 :                         args[prop] = playerBaseArgs[prop];
     179             : 
     180           0 :                 func(args);
     181             :         }
     182             : }
     183             : 
     184             : function defaultPlayerBaseRadius()
     185             : {
     186           0 :         return scaleByMapSize(15, 25);
     187             : }
     188             : 
     189             : /**
     190             :  * Marks the corner and center tiles of an area that is about the size of a Civic Center with the given TileClass.
     191             :  * Used to prevent resource collisions with the Civic Center.
     192             :  */
     193             : function addCivicCenterAreaToClass(position, tileClass)
     194             : {
     195           0 :         createArea(
     196             :                 new DiskPlacer(5, position),
     197             :                 new TileClassPainter(tileClass));
     198             : }
     199             : 
     200             : /**
     201             :  * Helper function.
     202             :  */
     203             : function getPlayerBaseArgs(playerBaseArgs)
     204             : {
     205           0 :         let baseResourceConstraint = playerBaseArgs.BaseResourceClass && avoidClasses(playerBaseArgs.BaseResourceClass, 4);
     206             : 
     207           0 :         if (playerBaseArgs.baseResourceConstraint)
     208           0 :                 baseResourceConstraint = new AndConstraint([baseResourceConstraint, playerBaseArgs.baseResourceConstraint]);
     209             : 
     210           0 :         return [
     211           0 :                 (property, defaultVal) => playerBaseArgs[property] === undefined ? defaultVal : playerBaseArgs[property],
     212             :                 playerBaseArgs.playerPosition,
     213             :                 baseResourceConstraint
     214             :         ];
     215             : }
     216             : 
     217             : function placePlayerBaseCityPatch(args)
     218             : {
     219           0 :         let [get, basePosition, baseResourceConstraint] = getPlayerBaseArgs(args);
     220             : 
     221           0 :         let painters = [];
     222             : 
     223           0 :         if (args.outerTerrain && args.innerTerrain)
     224           0 :                 painters.push(new LayeredPainter([args.outerTerrain, args.innerTerrain], [get("width", 1)]));
     225             : 
     226           0 :         if (args.painters)
     227           0 :                 painters = painters.concat(args.painters);
     228             : 
     229           0 :         createArea(
     230             :                 new ClumpPlacer(
     231             :                         Math.floor(diskArea(get("radius", defaultPlayerBaseRadius() / 3))),
     232             :                         get("coherence", 0.6),
     233             :                         get("smoothness", 0.3),
     234             :                         get("failFraction", Infinity),
     235             :                         basePosition),
     236             :                 painters);
     237             : }
     238             : 
     239             : function placePlayerBaseStartingAnimal(args)
     240             : {
     241           0 :         let [get, basePosition, baseResourceConstraint] = getPlayerBaseArgs(args);
     242             : 
     243           0 :         const template = get("template", "gaia/fauna_chicken");
     244           0 :         const count = template === "gaia/fauna_chicken" ? 5 :
     245             :                 Math.round(5 * (Engine.GetTemplate("gaia/fauna_chicken").ResourceSupply.Max / Engine.GetTemplate(get("template")).ResourceSupply.Max))
     246             : 
     247           0 :         for (let i = 0; i < get("groupCount", 2); ++i)
     248             :         {
     249           0 :                 let success = false;
     250           0 :                 for (let tries = 0; tries < get("maxTries", 30); ++tries)
     251             :                 {
     252           0 :                         let position = new Vector2D(0, get("distance", 9)).rotate(randomAngle()).add(basePosition);
     253           0 :                         if (createObjectGroup(
     254             :                                 new SimpleGroup(
     255             :                                         [
     256             :                                                 new SimpleObject(
     257             :                                                         template,
     258             :                                                         get("minGroupCount", count),
     259             :                                                         get("maxGroupCount", count),
     260             :                                                         get("minGroupDistance", 0),
     261             :                                                         get("maxGroupDistance", 2))
     262             :                                         ],
     263             :                                         true,
     264             :                                         args.BaseResourceClass,
     265             :                                         position),
     266             :                                 0,
     267             :                                 baseResourceConstraint))
     268             :                         {
     269           0 :                                 success = true;
     270           0 :                                 break;
     271             :                         }
     272             :                 }
     273             : 
     274           0 :                 if (!success)
     275             :                 {
     276           0 :                         error("Could not place startingAnimal for player " + args.playerID);
     277           0 :                         return;
     278             :                 }
     279             :         }
     280             : }
     281             : 
     282             : function placePlayerBaseBerries(args)
     283             : {
     284           0 :         let [get, basePosition, baseResourceConstraint] = getPlayerBaseArgs(args);
     285           0 :         for (let tries = 0; tries < get("maxTries", 30); ++tries)
     286             :         {
     287           0 :                 let position = new Vector2D(0, get("distance", 12)).rotate(randomAngle()).add(basePosition);
     288           0 :                 if (createObjectGroup(
     289             :                         new SimpleGroup(
     290             :                                 [new SimpleObject(args.template, get("minCount", 5), get("maxCount", 5), get("maxDist", 1), get("maxDist", 3))],
     291             :                                 true,
     292             :                                 args.BaseResourceClass,
     293             :                                 position),
     294             :                         0,
     295             :                         baseResourceConstraint))
     296           0 :                         return;
     297             :         }
     298             : 
     299           0 :         error("Could not place berries for player " + args.playerID);
     300             : }
     301             : 
     302             : function placePlayerBaseMines(args)
     303             : {
     304           0 :         let [get, basePosition, baseResourceConstraint] = getPlayerBaseArgs(args);
     305             : 
     306           0 :         let angleBetweenMines = randFloat(get("minAngle", Math.PI / 6), get("maxAngle", Math.PI / 3));
     307           0 :         let mineCount = args.types.length;
     308             : 
     309           0 :         let groupElements = [];
     310           0 :         if (args.groupElements)
     311           0 :                 groupElements = groupElements.concat(args.groupElements);
     312             : 
     313           0 :         for (let tries = 0; tries < get("maxTries", 75); ++tries)
     314             :         {
     315             :                 // First find a place where all mines can be placed
     316           0 :                 let pos = [];
     317           0 :                 let startAngle = randomAngle();
     318           0 :                 for (let i = 0; i < mineCount; ++i)
     319             :                 {
     320           0 :                         let angle = startAngle + angleBetweenMines * (i + (mineCount - 1) / 2);
     321           0 :                         pos[i] = new Vector2D(0, get("distance", 12)).rotate(angle).add(basePosition).round();
     322           0 :                         if (!g_Map.validTilePassable(pos[i]) || !baseResourceConstraint.allows(pos[i]))
     323             :                         {
     324           0 :                                 pos = undefined;
     325           0 :                                 break;
     326             :                         }
     327             :                 }
     328             : 
     329           0 :                 if (!pos)
     330           0 :                         continue;
     331             : 
     332             :                 // Place the mines
     333           0 :                 for (let i = 0; i < mineCount; ++i)
     334             :                 {
     335           0 :                         if (args.types[i].type && args.types[i].type == "stone_formation")
     336             :                         {
     337           0 :                                 createStoneMineFormation(pos[i], args.types[i].template, args.types[i].terrain);
     338           0 :                                 args.BaseResourceClass.add(pos[i]);
     339           0 :                                 continue;
     340             :                         }
     341             : 
     342           0 :                         createObjectGroup(
     343             :                                 new SimpleGroup(
     344             :                                         [new SimpleObject(args.types[i].template, 1, 1, 0, 0)].concat(groupElements),
     345             :                                         true,
     346             :                                         args.BaseResourceClass,
     347             :                                         pos[i]),
     348             :                                 0);
     349             :                 }
     350           0 :                 return;
     351             :         }
     352             : 
     353           0 :         error("Could not place mines for player " + args.playerID);
     354             : }
     355             : 
     356             : function placePlayerBaseTrees(args)
     357             : {
     358           0 :         let [get, basePosition, baseResourceConstraint] = getPlayerBaseArgs(args);
     359             : 
     360           0 :         let num = Math.floor(get("count", scaleByMapSize(7, 20)));
     361             : 
     362           0 :         for (let x = 0; x < get("maxTries", 30); ++x)
     363             :         {
     364           0 :                 let position = new Vector2D(0, randFloat(get("minDist", 11), get("maxDist", 13))).rotate(randomAngle()).add(basePosition).round();
     365             : 
     366           0 :                 if (createObjectGroup(
     367             :                         new SimpleGroup(
     368             :                                 [new SimpleObject(args.template, num, num, get("minDistGroup", 0), get("maxDistGroup", 5))],
     369             :                                 false,
     370             :                                 args.BaseResourceClass,
     371             :                                 position),
     372             :                         0,
     373             :                         baseResourceConstraint))
     374           0 :                         return;
     375             :         }
     376             : 
     377           0 :         error("Could not place starting trees for player " + args.playerID);
     378             : }
     379             : 
     380             : function placePlayerBaseTreasures(args)
     381             : {
     382           0 :         let [get, basePosition, baseResourceConstraint] = getPlayerBaseArgs(args);
     383             : 
     384           0 :         for (let resourceTypeArgs of args.types)
     385             :         {
     386           0 :                 get = (property, defaultVal) => resourceTypeArgs[property] === undefined ? defaultVal : resourceTypeArgs[property];
     387             : 
     388           0 :                 let success = false;
     389             : 
     390           0 :                 for (let tries = 0; tries < get("maxTries", 30); ++tries)
     391             :                 {
     392           0 :                         let position = new Vector2D(0, randFloat(get("minDist", 11), get("maxDist", 13))).rotate(randomAngle()).add(basePosition).round();
     393             : 
     394           0 :                         if (createObjectGroup(
     395             :                                 new SimpleGroup(
     396             :                                         [new SimpleObject(resourceTypeArgs.template, get("count", 14), get("count", 14), get("minDistGroup", 1), get("maxDistGroup", 3))],
     397             :                                         false,
     398             :                                         args.BaseResourceClass,
     399             :                                         position),
     400             :                                 0,
     401             :                                 baseResourceConstraint))
     402             :                         {
     403           0 :                                 success = true;
     404           0 :                                 break;
     405             :                         }
     406             :                 }
     407           0 :                 if (!success)
     408             :                 {
     409           0 :                         error("Could not place treasure " + resourceTypeArgs.template + " for player " + args.playerID);
     410           0 :                         return;
     411             :                 }
     412             :         }
     413             : }
     414             : 
     415             : /**
     416             :  * Typically used for placing grass tufts around the civic centers.
     417             :  */
     418             : function placePlayerBaseDecoratives(args)
     419             : {
     420           0 :         let [get, basePosition, baseResourceConstraint] = getPlayerBaseArgs(args);
     421             : 
     422           0 :         for (let i = 0; i < get("count", scaleByMapSize(2, 5)); ++i)
     423             :         {
     424           0 :                 let success = false;
     425           0 :                 for (let x = 0; x < get("maxTries", 30); ++x)
     426             :                 {
     427           0 :                         let position = new Vector2D(0, randIntInclusive(get("minDist", 8), get("maxDist", 11))).rotate(randomAngle()).add(basePosition).round();
     428             : 
     429           0 :                         if (createObjectGroup(
     430             :                                 new SimpleGroup(
     431             :                                         [new SimpleObject(args.template, get("minCount", 2), get("maxCount", 5), 0, 1)],
     432             :                                         false,
     433             :                                         args.BaseResourceClass,
     434             :                                         position),
     435             :                                 0,
     436             :                                 baseResourceConstraint))
     437             :                         {
     438           0 :                                 success = true;
     439           0 :                                 break;
     440             :                         }
     441             :                 }
     442           0 :                 if (!success)
     443             :                         // Don't warn since the decoratives are not important
     444           0 :                         return;
     445             :         }
     446             : }
     447             : 
     448             : function placePlayersNomad(playerClass, constraints)
     449             : {
     450           0 :         if (!isNomad())
     451           0 :                 return undefined;
     452             : 
     453           0 :         g_Map.log("Placing nomad starting units");
     454             : 
     455           0 :         let distance = scaleByMapSize(60, 240);
     456           0 :         let constraint = new StaticConstraint(constraints);
     457             : 
     458           0 :         let numPlayers = getNumPlayers();
     459           0 :         let playerIDs = shuffleArray(sortAllPlayers());
     460           0 :         let playerPosition = [];
     461             : 
     462           0 :         for (let i = 0; i < numPlayers; ++i)
     463             :         {
     464           0 :                 let objects = getStartingEntities(playerIDs[i]).filter(ents => ents.Template.startsWith("units/")).map(
     465           0 :                         ents => new SimpleObject(ents.Template, ents.Count || 1, ents.Count || 1, 1, 3));
     466             : 
     467             :                 // Add treasure if too few resources for a civic center
     468           0 :                 let ccCost = Engine.GetTemplate("structures/" + getCivCode(playerIDs[i]) + "/civil_centre").Cost.Resources;
     469           0 :                 for (let resourceType in ccCost)
     470             :                 {
     471           0 :                         let treasureTemplate = g_NomadTreasureTemplates[resourceType];
     472             : 
     473           0 :                         let count = Math.max(0, Math.ceil(
     474             :                                 (ccCost[resourceType] - (g_MapSettings.StartingResources || 0)) /
     475             :                                 Engine.GetTemplate(treasureTemplate).Treasure.Resources[resourceType]));
     476             : 
     477           0 :                         objects.push(new SimpleObject(treasureTemplate, count, count, 3, 5));
     478             :                 }
     479             : 
     480             :                 // Try place these entities at a random location
     481           0 :                 let group = new SimpleGroup(objects, true, playerClass);
     482           0 :                 let success = false;
     483           0 :                 for (let distanceFactor of [1, 1/2, 1/4, 0])
     484           0 :                         if (createObjectGroups(group, playerIDs[i], new AndConstraint([constraint, avoidClasses(playerClass, distance * distanceFactor)]), 1, 200, false).length)
     485             :                         {
     486           0 :                                 success = true;
     487           0 :                                 playerPosition[i] = group.centerPosition;
     488           0 :                                 break;
     489             :                         }
     490             : 
     491           0 :                 if (!success)
     492           0 :                         throw new Error("Could not place starting units for player " + playerIDs[i] + "!");
     493             :         }
     494             : 
     495           0 :         return [playerIDs, playerPosition];
     496             : }
     497             : 
     498             : /**
     499             :  * Sorts an array of player IDs by team index. Players without teams come first.
     500             :  * Randomize order for players of the same team.
     501             :  */
     502             : function sortPlayers(playerIDs)
     503             : {
     504           0 :         return shuffleArray(playerIDs).sort((playerID1, playerID2) => getPlayerTeam(playerID1) - getPlayerTeam(playerID2));
     505             : }
     506             : 
     507             : /**
     508             :  * Randomize playerIDs but sort by team.
     509             :  *
     510             :  * @returns {Array} - every item is an array of player indices
     511             :  */
     512             : function sortAllPlayers()
     513             : {
     514           0 :         let playerIDs = [];
     515           0 :         for (let i = 0; i < getNumPlayers(); ++i)
     516           0 :                 playerIDs.push(i+1);
     517             : 
     518           0 :         return sortPlayers(playerIDs);
     519             : }
     520             : 
     521             : /**
     522             :  * Rearrange order so that teams of neighboring players alternate (if the given IDs are sorted by team).
     523             :  */
     524             : function primeSortPlayers(playerIDs)
     525             : {
     526           0 :         let prime = [];
     527           0 :         for (let i = 0; i < Math.floor(playerIDs.length / 2); ++i)
     528             :         {
     529           0 :                 prime.push(playerIDs[i]);
     530           0 :                 prime.push(playerIDs[playerIDs.length - 1 - i]);
     531             :         }
     532             : 
     533           0 :         if (playerIDs.length % 2)
     534           0 :                 prime.push(playerIDs[Math.floor(playerIDs.length / 2)]);
     535             : 
     536           0 :         return prime;
     537             : }
     538             : 
     539             : function primeSortAllPlayers()
     540             : {
     541           0 :         return primeSortPlayers(sortAllPlayers());
     542             : }
     543             : 
     544             : /*
     545             :  * Separates playerIDs into two arrays such that teammates are in the same array,
     546             :  * unless everyone's on the same team in which case they'll be split in half.
     547             :  */
     548             : function partitionPlayers(playerIDs)
     549             : {
     550           0 :         let teamIDs = Array.from(new Set(playerIDs.map(getPlayerTeam)));
     551           0 :         let teams = teamIDs.map(teamID => playerIDs.filter(playerID => getPlayerTeam(playerID) == teamID));
     552           0 :         if (teamIDs.indexOf(-1) != -1)
     553           0 :                 teams = teams.concat(teams.splice(teamIDs.indexOf(-1), 1)[0].map(playerID => [playerID]));
     554             : 
     555           0 :         if (teams.length == 1)
     556             :         {
     557           0 :                 let idx = Math.floor(teams[0].length / 2);
     558           0 :                 teams = [teams[0].slice(idx), teams[0].slice(0, idx)];
     559             :         }
     560             : 
     561           0 :         teams.sort((a, b) => b.length - a.length);
     562             : 
     563             :         // Use the greedy algorithm: add the next team to the side with fewer players
     564           0 :         return teams.reduce(([east, west], team) =>
     565           0 :                 east.length > west.length ?
     566             :                         [east, west.concat(team)] :
     567             :                         [east.concat(team), west],
     568             :                 [[], []]);
     569             : }
     570             : 
     571             : /**
     572             :  * Determine player starting positions on a circular pattern.
     573             :  */
     574             : function playerPlacementCircle(radius, startingAngle = undefined, center = undefined)
     575             : {
     576           0 :         let startAngle = startingAngle !== undefined ? startingAngle : randomAngle();
     577           0 :         let [playerPosition, playerAngle] = distributePointsOnCircle(getNumPlayers(), startAngle, radius, center || g_Map.getCenter());
     578           0 :         return [sortAllPlayers(), playerPosition.map(p => p.round()), playerAngle, startAngle];
     579             : }
     580             : 
     581             : /**
     582             :  * Determine player starting positions on a circular pattern, with a custom angle for each player.
     583             :  * Commonly used for gulf terrains.
     584             :  */
     585             : function playerPlacementCustomAngle(radius, center, playerAngleFunc)
     586             : {
     587           0 :         let playerPosition = [];
     588           0 :         let playerAngle = [];
     589             : 
     590           0 :         let numPlayers = getNumPlayers();
     591             : 
     592           0 :         for (let i = 0; i < numPlayers; ++i)
     593             :         {
     594           0 :                 playerAngle[i] = playerAngleFunc(i);
     595           0 :                 playerPosition[i] = Vector2D.add(center, new Vector2D(radius, 0).rotate(-playerAngle[i])).round();
     596             :         }
     597             : 
     598           0 :         return [playerPosition, playerAngle];
     599             : }
     600             : 
     601             : /**
     602             :  * Returns player starting positions equally spaced along an arc.
     603             :  */
     604             : function playerPlacementArc(playerIDs, center, radius, startAngle, endAngle)
     605             : {
     606           0 :         return distributePointsOnCircularSegment(
     607             :                 playerIDs.length + 2,
     608             :                 endAngle - startAngle,
     609             :                 startAngle,
     610             :                 radius,
     611             :                 center
     612           0 :         )[0].slice(1, -1).map(p => p.round());
     613             : }
     614             : 
     615             : /**
     616             :  * Returns player starting positions located on two symmetrically placed arcs, with teammates placed on the same arc.
     617             :  */
     618             : function playerPlacementArcs(playerIDs, center, radius, mapAngle, startAngle, endAngle)
     619             : {
     620           0 :         let [east, west] = partitionPlayers(playerIDs);
     621           0 :         let eastPosition = playerPlacementArc(east, center, radius, mapAngle + startAngle, mapAngle + endAngle);
     622           0 :         let westPosition = playerPlacementArc(west, center, radius, mapAngle - startAngle, mapAngle - endAngle);
     623           0 :         return playerIDs.map(playerID => east.indexOf(playerID) != -1 ?
     624             :                 eastPosition[east.indexOf(playerID)] :
     625             :                 westPosition[west.indexOf(playerID)]);
     626             : }
     627             : 
     628             : /**
     629             :  * Returns player starting positions located on two parallel lines, typically used by central river maps.
     630             :  * If there are two teams with an equal number of players, each team will occupy exactly one line.
     631             :  * Angle 0 means the players are placed in north to south direction, i.e. along the Z axis.
     632             :  */
     633             : function playerPlacementRiver(angle, width, center = undefined)
     634             : {
     635           0 :         let numPlayers = getNumPlayers();
     636           0 :         let numPlayersEven = numPlayers % 2 == 0;
     637           0 :         let mapSize = g_Map.getSize();
     638           0 :         let centerPosition = center || g_Map.getCenter();
     639           0 :         let playerPosition = [];
     640             : 
     641           0 :         for (let i = 0; i < numPlayers; ++i)
     642             :         {
     643           0 :                 let currentPlayerEven = i % 2 == 0;
     644             : 
     645           0 :                 let offsetDivident = numPlayersEven || currentPlayerEven ? (i + 1) % 2 : 0;
     646           0 :                 let offsetDivisor = numPlayersEven ? 0 : currentPlayerEven ? +1 : -1;
     647             : 
     648           0 :                 playerPosition[i] = new Vector2D(
     649             :                         width * (i % 2) + (mapSize - width) / 2,
     650             :                         fractionToTiles(((i - 1 + offsetDivident) / 2 + 1) / ((numPlayers + offsetDivisor) / 2 + 1))
     651             :                 ).rotateAround(angle, centerPosition).round();
     652             :         }
     653             : 
     654           0 :         return groupPlayersByArea(new Array(numPlayers).fill(0).map((_p, i) => i + 1), playerPosition);
     655             : }
     656             : 
     657             : /**
     658             :  * Returns starting positions located on two parallel lines.
     659             :  * The locations on the first line are shifted in comparison to the other line.
     660             :  */
     661             : function playerPlacementLine(angle, center, width)
     662             : {
     663           0 :         let playerPosition = [];
     664           0 :         let numPlayers = getNumPlayers();
     665             : 
     666           0 :         for (let i = 0; i < numPlayers; ++i)
     667           0 :                 playerPosition[i] = Vector2D.add(
     668             :                         center,
     669             :                         new Vector2D(
     670             :                                 fractionToTiles((i + 1) / (numPlayers + 1) - 0.5),
     671             :                                 width * (i % 2 - 1/2)
     672             :                         ).rotate(angle)
     673             :                 ).round();
     674             : 
     675           0 :         return playerPosition;
     676             : }
     677             : 
     678             : /**
     679             :  * Returns a random location for each player that meets the given constraints and
     680             :  * orders the playerIDs so that players become grouped by team.
     681             :  */
     682             : function playerPlacementRandom(playerIDs, constraints = undefined)
     683             : {
     684           0 :         let locations = [];
     685           0 :         let attempts = 0;
     686           0 :         let resets = 0;
     687             : 
     688           0 :         let mapCenter = g_Map.getCenter();
     689           0 :         let playerMinDistSquared = Math.square(fractionToTiles(0.25));
     690           0 :         let borderDistance = fractionToTiles(0.08);
     691             : 
     692           0 :         let area = createArea(new MapBoundsPlacer(), undefined, new AndConstraint(constraints));
     693             : 
     694           0 :         for (let i = 0; i < getNumPlayers(); ++i)
     695             :         {
     696           0 :                 const position = pickRandom(area.getPoints());
     697           0 :                 if (!position)
     698           0 :                         return undefined;
     699             : 
     700             :                 // Minimum distance between initial bases must be a quarter of the map diameter
     701           0 :                 if (locations.some(loc => loc.distanceToSquared(position) < playerMinDistSquared) ||
     702             :                     position.distanceToSquared(mapCenter) > Math.square(mapCenter.x - borderDistance))
     703             :                 {
     704           0 :                         --i;
     705           0 :                         ++attempts;
     706             : 
     707             :                         // Reset if we're in what looks like an infinite loop
     708           0 :                         if (attempts > 500)
     709             :                         {
     710           0 :                                 locations = [];
     711           0 :                                 i = -1;
     712           0 :                                 attempts = 0;
     713           0 :                                 ++resets;
     714             : 
     715             :                                 // Reduce minimum player distance progressively
     716           0 :                                 if (resets % 25 == 0)
     717           0 :                                         playerMinDistSquared *= 0.95;
     718             : 
     719             :                                 // If we only pick bad locations, stop trying to place randomly
     720           0 :                                 if (resets == 500)
     721           0 :                                         return undefined;
     722             :                         }
     723           0 :                         continue;
     724             :                 }
     725             : 
     726           0 :                 locations[i] = position;
     727             :         }
     728           0 :         return groupPlayersByArea(playerIDs, locations);
     729             : }
     730             : 
     731             : /**
     732             :  *  Pick locations from the given set so that teams end up grouped.
     733             :  */
     734             : function groupPlayersByArea(playerIDs, locations)
     735             : {
     736           0 :         playerIDs = sortPlayers(playerIDs);
     737             : 
     738           0 :         let minDist = Infinity;
     739             :         let minLocations;
     740             : 
     741             :         // Of all permutations of starting locations, find the one where
     742             :         // the sum of the distances between allies is minimal, weighted by teamsize.
     743           0 :         heapsPermute(shuffleArray(locations).slice(0, playerIDs.length), v => v.clone(), permutation => {
     744           0 :                 let dist = 0;
     745           0 :                 let teamDist = 0;
     746           0 :                 let teamSize = 0;
     747             : 
     748           0 :                 for (let i = 1; i < playerIDs.length; ++i)
     749             :                 {
     750           0 :                         let team1 = getPlayerTeam(playerIDs[i - 1]);
     751           0 :                         let team2 = getPlayerTeam(playerIDs[i]);
     752           0 :                         ++teamSize;
     753           0 :                         if (team1 != -1 && team1 == team2)
     754           0 :                                 teamDist += permutation[i - 1].distanceTo(permutation[i]);
     755             :                         else
     756             :                         {
     757           0 :                                 dist += teamDist / teamSize;
     758           0 :                                 teamDist = 0;
     759           0 :                                 teamSize = 0;
     760             :                         }
     761             :                 }
     762             : 
     763           0 :                 if (teamSize)
     764           0 :                         dist += teamDist / teamSize;
     765             : 
     766           0 :                 if (dist < minDist)
     767             :                 {
     768           0 :                         minDist = dist;
     769           0 :                         minLocations = permutation;
     770             :                 }
     771             :         });
     772             : 
     773           0 :         return [playerIDs, minLocations];
     774             : }
     775             : 
     776             : /**
     777             :  * Sorts the playerIDs so that team members are as close as possible on a ring.
     778             :  */
     779             : function groupPlayersCycle(startLocations)
     780             : {
     781           0 :         let startLocationOrder = sortPointsShortestCycle(startLocations);
     782             : 
     783           0 :         let newStartLocations = [];
     784           0 :         for (let i = 0; i < startLocations.length; ++i)
     785           0 :                 newStartLocations.push(startLocations[startLocationOrder[i]]);
     786             : 
     787           0 :         startLocations = newStartLocations;
     788             : 
     789             :         // Sort players by team
     790           0 :         let playerIDs = [];
     791           0 :         let teams = [];
     792           0 :         for (let i = 0; i < g_MapSettings.PlayerData.length - 1; ++i)
     793             :         {
     794           0 :                 playerIDs.push(i+1);
     795           0 :                 let t = g_MapSettings.PlayerData[i + 1].Team;
     796           0 :                 if (teams.indexOf(t) == -1 && t !== undefined)
     797           0 :                         teams.push(t);
     798             :         }
     799             : 
     800           0 :         playerIDs = sortPlayers(playerIDs);
     801             : 
     802           0 :         if (!teams.length)
     803           0 :                 return [playerIDs, startLocations];
     804             : 
     805             :         // Minimize maximum distance between players within a team
     806           0 :         let minDistance = Infinity;
     807             :         let bestShift;
     808           0 :         for (let s = 0; s < playerIDs.length; ++s)
     809             :         {
     810           0 :                 let maxTeamDist = 0;
     811           0 :                 for (let pi = 0; pi < playerIDs.length - 1; ++pi)
     812             :                 {
     813           0 :                         let t1 = getPlayerTeam(playerIDs[(pi + s) % playerIDs.length]);
     814             : 
     815           0 :                         if (teams.indexOf(t1) === -1)
     816           0 :                                 continue;
     817             : 
     818           0 :                         for (let pj = pi + 1; pj < playerIDs.length; ++pj)
     819             :                         {
     820           0 :                                 if (t1 != getPlayerTeam(playerIDs[(pj + s) % playerIDs.length]))
     821           0 :                                         continue;
     822             : 
     823           0 :                                 maxTeamDist = Math.max(
     824             :                                         maxTeamDist,
     825             :                                         Math.euclidDistance2D(
     826             :                                                 startLocations[pi].x,
     827             :                                                 startLocations[pi].y,
     828             :                                                 startLocations[pj].x,
     829             :                                                 startLocations[pj].y));
     830             :                         }
     831             :                 }
     832             : 
     833           0 :                 if (maxTeamDist < minDistance)
     834             :                 {
     835           0 :                         minDistance = maxTeamDist;
     836           0 :                         bestShift = s;
     837             :                 }
     838             :         }
     839             : 
     840           0 :         if (bestShift)
     841             :         {
     842           0 :                 let newPlayerIDs = [];
     843           0 :                 for (let i = 0; i < playerIDs.length; ++i)
     844           0 :                         newPlayerIDs.push(playerIDs[(i + bestShift) % playerIDs.length]);
     845           0 :                 playerIDs = newPlayerIDs;
     846             :         }
     847             : 
     848           0 :         return [playerIDs, startLocations];
     849             : }

Generated by: LCOV version 1.14