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 : }
|