Remove rmgen euclidian distance helper function, refs rP20328 / D969.
[0ad.git] / binaries / data / mods / public / maps / random / rmgen2 / setup.js
blob5e51523f61469f6e654a067616e13569bd013fe3
1 var g_Amounts = {
2         "scarce": 0.2,
3         "few": 0.5,
4         "normal": 1,
5         "many": 1.75,
6         "tons": 3
7 };
9 var g_Mixes = {
10         "same": 0,
11         "similar": 0.1,
12         "normal": 0.25,
13         "varied": 0.5,
14         "unique": 0.75
17 var g_Sizes = {
18         "tiny": 0.5,
19         "small": 0.75,
20         "normal": 1,
21         "big": 1.25,
22         "huge": 1.5,
25 var g_AllAmounts = Object.keys(g_Amounts);
26 var g_AllMixes = Object.keys(g_Mixes);
27 var g_AllSizes = Object.keys(g_Sizes);
29 var g_DefaultTileClasses = [
30         "animals",
31         "baseResource",
32         "berries",
33         "bluff",
34         "bluffSlope",
35         "dirt",
36         "fish",
37         "food",
38         "forest",
39         "hill",
40         "land",
41         "map",
42         "metal",
43         "mountain",
44         "plateau",
45         "player",
46         "prop",
47         "ramp",
48         "rock",
49         "settlement",
50         "spine",
51         "valley",
52         "water"
55 var g_TileClasses;
57 /**
58  * Adds an array of elements to the map.
59  */
60 function addElements(elements)
62         for (let element of elements)
63                 element.func(
64                         [
65                                 avoidClasses.apply(null, element.avoid),
66                                 stayClasses.apply(null, element.stay || null)
67                         ],
68                         pickSize(element.sizes),
69                         pickMix(element.mixes),
70                         pickAmount(element.amounts),
71                         element.baseHeight || 0);
74 /**
75  * Converts "amount" terms to numbers.
76  */
77 function pickAmount(amounts)
79         let amount = pickRandom(amounts);
81         if (amount in g_Amounts)
82                 return g_Amounts[amount];
84         return g_Amounts.normal;
87 /**
88  * Converts "mix" terms to numbers.
89  */
90 function pickMix(mixes)
92         let mix = pickRandom(mixes);
94         if (mix in g_Mixes)
95                 return g_Mixes[mix];
97         return g_Mixes.normal;
101  * Converts "size" terms to numbers.
102  */
103 function pickSize(sizes)
105         let size = pickRandom(sizes);
107         if (size in g_Sizes)
108                 return g_Sizes[size];
110         return g_Sizes.normal;
114  * Paints the entire map with the given terrain texture, tileclass and elevation.
115  */
116 function resetTerrain(terrain, tileClass, elevation)
118         let center = Math.round(fractionToTiles(0.5));
119         createArea(
120                 new ClumpPlacer(getMapArea(), 1, 1, 1, center, center),
121                 [
122                         new LayeredPainter([terrain], []),
123                         new SmoothElevationPainter(ELEVATION_SET, elevation, 1),
124                         paintClass(tileClass)
125                 ],
126                 null);
130  * Choose starting locations for all players.
132  * @param {string} type - "radial", "line", "stronghold", "random"
133  * @param {number} distance - radial distance from the center of the map
134  * @param {number} groupedDistance - space between players within a team
135  * @param {number} startAngle - determined by the map that might want to place something between players
136  * @returns {Array|undefined} - If successful, each element is an object that contains id, angle, x, z for each player
137  */
138 function addBases(type, distance, groupedDistance, startAngle)
140         let playerIDs = sortAllPlayers();
141         let teamsArray = getTeamsArray();
143         switch(type)
144         {
145                 case "line":
146                         return placeLine(teamsArray, distance, groupedDistance, startAngle);
147                 case "radial":
148                         return placeRadial(playerIDs, distance, startAngle);
149                 case "random":
150                         return placeRandom(playerIDs) || placeRadial(playerIDs, distance, startAngle);
151                 case "stronghold":
152                         return placeStronghold(teamsArray, distance, groupedDistance, startAngle);
153                 default:
154                         warn("Unknown base placement type:" + type);
155                         return undefined;
156         }
160  * Create the base for a single player.
162  * @param {Object} player - contains id, angle, x, z
163  * @param {boolean} walls - Whether or not iberian gets starting walls
164  */
165 function createBase(player, walls = true)
167         var mapSize = getMapSize();
169         // Get the x and z in tiles
170         var fx = fractionToTiles(player.x);
171         var fz = fractionToTiles(player.z);
172         var ix = round(fx);
173         var iz = round(fz);
175         addCivicCenterAreaToClass(ix, iz, g_TileClasses.player);
177         if (walls && mapSize > 192)
178                 placeCivDefaultEntities(fx, fz, player.id);
179         else
180                 placeCivDefaultEntities(fx, fz, player.id, { 'iberWall': false });
182         // Create the city patch
183         var radius = scaleByMapSize(15, 25);
184         var cityRadius = radius / 3;
185         var placer = new ClumpPlacer(PI * cityRadius * cityRadius, 0.6, 0.3, 10, ix, iz);
186         var painter = new LayeredPainter([g_Terrains.roadWild, g_Terrains.road], [1]);
187         createArea(placer, painter, null);
189         // TODO: retry loops are needed as resources might conflict with neighboring ones
191         // Create initial berry bushes at random angle
192         var bbAngle = randFloat(0, TWO_PI);
193         var bbDist = 10;
194         var bbX = round(fx + bbDist * cos(bbAngle));
195         var bbZ = round(fz + bbDist * sin(bbAngle));
196         var group = new SimpleGroup(
197                 [new SimpleObject(g_Gaia.fruitBush, 5, 5, 0, 3)],
198                 true, g_TileClasses.baseResource, bbX, bbZ
199         );
200         createObjectGroup(group, 0, avoidClasses(g_TileClasses.baseResource, 2));
202         // Create metal mine at a different angle
203         var mAngle = bbAngle;
204         while(abs(mAngle - bbAngle) < PI / 3)
205                 mAngle = randFloat(0, TWO_PI);
207         var mDist = 12;
208         var mX = round(fx + mDist * cos(mAngle));
209         var mZ = round(fz + mDist * sin(mAngle));
210         group = new SimpleGroup(
211                 [new SimpleObject(g_Gaia.metalLarge, 1, 1, 0, 0)],
212                 true, g_TileClasses.baseResource, mX, mZ
213         );
214         createObjectGroup(group, 0, avoidClasses(g_TileClasses.baseResource, 2));
216         // Create stone mine beside metal
217         mAngle += randFloat(PI / 8, PI / 4);
218         mX = round(fx + mDist * cos(mAngle));
219         mZ = round(fz + mDist * sin(mAngle));
220         group = new SimpleGroup(
221                 [new SimpleObject(g_Gaia.stoneLarge, 1, 1, 0, 2)],
222                 true, g_TileClasses.baseResource, mX, mZ
223         );
224         createObjectGroup(group, 0, avoidClasses(g_TileClasses.baseResource, 2));
226         placeDefaultChicken(
227                 fx,
228                 fz,
229                 g_TileClasses.baseResource,
230                 avoidClasses(g_TileClasses.baseResource, 4),
231                 g_Gaia.chicken
232         );
234         // Create starting trees
235         var num = currentBiome() == "savanna" ? 5 : 15;
236         for (var tries = 0; tries < 10; ++tries)
237         {
238                 var tAngle = randFloat(0, TWO_PI);
239                 var tDist = randFloat(12, 13);
240                 var tX = round(fx + tDist * cos(tAngle));
241                 var tZ = round(fz + tDist * sin(tAngle));
243                 group = new SimpleGroup(
244                         [new SimpleObject(g_Gaia.tree1, num, num, 1, 3)],
245                         false, g_TileClasses.baseResource, tX, tZ
246                 );
248                 if (createObjectGroup(group, 0, avoidClasses(g_TileClasses.baseResource, 4)))
249                         break;
250         }
252         placeDefaultDecoratives(
253                 fx,
254                 fz,
255                 g_Decoratives.grassShort,
256                 g_TileClasses.baseResource,
257                 radius,
258                 avoidClasses(g_TileClasses.baseResource, 4));
262  * Return an array where each element is an array of playerIndices of a team.
263  */
264 function getTeamsArray()
266         var numPlayers = getNumPlayers();
268         // Group players by team
269         var teams = [];
270         for (let i = 0; i < numPlayers; ++i)
271         {
272                 let team = getPlayerTeam(i);
273                 if (team == -1)
274                         continue;
276                 if (!teams[team])
277                         teams[team] = [];
279                 teams[team].push(i+1);
280         }
282         // Players without a team get a custom index
283         for (let i = 0; i < numPlayers; ++i)
284                 if (getPlayerTeam(i) == -1)
285                         teams.push([i+1]);
287         // Remove unused indices
288         return teams.filter(team => true);
292  * Choose a random pattern for placing the bases of the players.
293  */
294 function randomStartingPositionPattern(teamsArray)
296         var formats = ["radial"];
297         var mapSize = getMapSize();
298         var numPlayers = getNumPlayers();
300         // Enable stronghold if we have a few teams and a big enough map
301         if (teamsArray.length >= 2 && numPlayers >= 4 && mapSize >= 256)
302                 formats.push("stronghold");
304         // Enable random if we have enough teams or enough players on a big enough map
305         if (mapSize >= 256 && (teamsArray.length >= 3 || numPlayers > 4))
306                 formats.push("random");
308         // Enable line if we have enough teams and players on a big enough map
309         if (teamsArray.length >= 2 && numPlayers >= 4 && mapSize >= 384)
310                 formats.push("line");
312         return {
313                 "setup": pickRandom(formats),
314                 "distance": randFloat(0.2, 0.35),
315                 "separation": randFloat(0.05, 0.1)
316         };
320  * Place teams in a line-pattern.
322  * @param {Array} playerIDs - typically randomized indices of players of a single team
323  * @param {number} distance - radial distance from the center of the map
324  * @param {number} groupedDistance - distance between players
325  * @param {number} startAngle - determined by the map that might want to place something between players.
327  * @returns {Array} - contains id, angle, x, z for every player
328  */
329 function placeLine(teamsArray, distance, groupedDistance, startAngle)
331         var players = [];
333         for (let i = 0; i < teamsArray.length; ++i)
334         {
335                 var safeDist = distance;
336                 if (distance + teamsArray[i].length * groupedDistance > 0.45)
337                         safeDist = 0.45 - teamsArray[i].length * groupedDistance;
339                 var teamAngle = startAngle + (i + 1) * 2 * Math.PI / teamsArray.length;
341                 // Create player base
342                 for (var p = 0; p < teamsArray[i].length; ++p)
343                 {
344                         players[teamsArray[i][p]] = {
345                                 "id": teamsArray[i][p],
346                                 "x": 0.5 + (safeDist + p * groupedDistance) * cos(teamAngle),
347                                 "z": 0.5 + (safeDist + p * groupedDistance) * sin(teamAngle)
348                         };
349                         createBase(players[teamsArray[i][p]], false);
350                 }
351         }
353         return players;
357  * Place players in a circle-pattern.
359  * @param {Array} playerIDs - order of playerIDs to be placed
360  * @param {number} distance - radial distance from the center of the map
361  * @param {number} startAngle - determined by the map that might want to place something between players
362  */
363 function placeRadial(playerIDs, distance, startAngle)
365         let players = [];
366         let numPlayers = getNumPlayers();
368         for (let i = 0; i < numPlayers; ++i)
369         {
370                 let angle = startAngle + i * 2 * Math.PI / numPlayers;
371                 players[i] = {
372                         "id": playerIDs[i],
373                         "x": 0.5 + distance * cos(angle),
374                         "z": 0.5 + distance * sin(angle)
375                 };
376                 createBase(players[i]);
377         }
379         return players;
383  * Choose arbitrary starting locations.
384  */
385 function placeRandom(playerIDs)
387         var locations = [];
388         var attempts = 0;
389         var resets = 0;
391         for (let i = 0; i < getNumPlayers(); ++i)
392         {
393                 var playerAngle = randFloat(0, TWO_PI);
395                 // Distance from the center of the map in percent
396                 // Mapsize being used as a diameter, so 0.5 is the edge of the map
397                 var distance = randFloat(0, 0.42);
398                 var x = 0.5 + distance * cos(playerAngle);
399                 var z = 0.5 + distance * sin(playerAngle);
401                 // Minimum distance between initial bases must be a quarter of the map diameter
402                 if (locations.some(loc => Math.euclidDistance2D(x, z, loc.x, loc.z) < 0.25))
403                 {
404                         --i;
405                         ++attempts;
407                         // Reset if we're in what looks like an infinite loop
408                         if (attempts > 100)
409                         {
410                                 locations = [];
411                                 i = -1;
412                                 attempts = 0;
413                                 ++resets;
415                                 // If we only pick bad locations, stop trying to place randomly
416                                 if (resets == 100)
417                                         return undefined;
418                         }
419                         continue;
420                 }
422                 locations[i] = {
423                         "x": x,
424                         "z": z
425                 };
426         }
428         let players = groupPlayersByLocations(playerIDs, locations);
429         for (let player of players)
430                 createBase(player);
432         return players;
436  *  Pick locations from the given set so that teams end up grouped.
438  *  @param {Array} playerIDs - sorted by teams.
439  *  @param {Array} locations - array of x/z pairs of possible starting locations.
440  */
441 function groupPlayersByLocations(playerIDs, locations)
443         playerIDs = sortPlayers(playerIDs);
445         let minDist = Infinity;
446         let minLocations;
448         // Of all permutations of starting locations, find the one where
449         // the sum of the distances between allies is minimal, weighted by teamsize.
450         heapsPermute(shuffleArray(locations).slice(0, playerIDs.length), function(permutation)
451         {
452                 let dist = 0;
453                 let teamDist = 0;
454                 let teamSize = 0;
456                 for (let i = 1; i < playerIDs.length; ++i)
457                 {
458                         let team1 = g_MapSettings.PlayerData[playerIDs[i - 1]].Team;
459                         let team2 = g_MapSettings.PlayerData[playerIDs[i]].Team;
460                         ++teamSize;
462                         if (team1 != -1 && team1 == team2)
463                                 teamDist += Math.euclidDistance2D(permutation[i - 1].x, permutation[i - 1].z, permutation[i].x, permutation[i].z);
464                         else
465                         {
466                                 dist += teamDist / teamSize;
467                                 teamDist = 0;
468                                 teamSize = 0;
469                         }
470                 }
472                 if (teamSize)
473                         dist += teamDist / teamSize;
475                 if (dist < minDist)
476                 {
477                         minDist = dist;
478                         minLocations = permutation;
479                 }
480         });
482         let players = [];
483         for (let i = 0; i < playerIDs.length; ++i)
484         {
485                 let player = minLocations[i];
486                 player.id = playerIDs[i];
487                 players.push(player);
488         }
489         return players;
493  * Place given players in a stronghold-pattern.
495  * @param teamsArray - each item is an array of playerIDs placed per stronghold
496  * @param distance - radial distance from the center of the map
497  * @param groupedDistance - distance between neighboring players
498  * @param {number} startAngle - determined by the map that might want to place something between players
499  */
500 function placeStronghold(teamsArray, distance, groupedDistance, startAngle)
502         var players = [];
504         for (let i = 0; i < teamsArray.length; ++i)
505         {
506                 var teamAngle = startAngle + (i + 1) * 2 * Math.PI / teamsArray.length;
507                 var fractionX = 0.5 + distance * cos(teamAngle);
508                 var fractionZ = 0.5 + distance * sin(teamAngle);
509                 var teamGroupDistance = groupedDistance;
511                 // If we have a team of above average size, make sure they're spread out
512                 if (teamsArray[i].length > 4)
513                         teamGroupDistance = Math.max(0.08, groupedDistance);
515                 // If we have a solo player, place them on the center of the team's location
516                 if (teamsArray[i].length == 1)
517                         teamGroupDistance = 0;
519                 // TODO: Ensure players are not placed outside of the map area, similar to placeLine
521                 // Create player base
522                 for (var p = 0; p < teamsArray[i].length; ++p)
523                 {
524                         var angle = startAngle + (p + 1) * 2 * Math.PI / teamsArray[i].length;
525                         players[teamsArray[i][p]] = {
526                                 "id": teamsArray[i][p],
527                                 "x": fractionX + teamGroupDistance * cos(angle),
528                                 "z": fractionZ + teamGroupDistance * sin(angle)
529                         };
530                         createBase(players[teamsArray[i][p]], false);
531                 }
532         }
534         return players;
538  * Places players either randomly or in a stronghold-pattern at a set of given heightmap coordinates.
540  * @param teamsArray - Array where each item is an array of playerIDs, possibly going to be grouped.
541  * @param singleBases - pair of coordinates of the heightmap to place isolated bases.
542  * @param singleBases - pair of coordinates of the heightmap to place team bases.
543  * @param groupedDistance - distance between neighboring players.
544  * @param func - A function called for every player base or stronghold placed.
545  */
546 function randomPlayerPlacementAt(teamsArray, singleBases, strongholdBases, heightmapScale, groupedDistance, func)
548         let strongholdBasesRandom = shuffleArray(strongholdBases);
549         let mapSize = getMapSize();
551         if (randBool(1/3) &&
552             mapSize >= 256 &&
553             teamsArray.length >= 2 &&
554             teamsArray.length < getNumPlayers() &&
555             teamsArray.length <= strongholdBasesRandom.length)
556         {
557                 let startAngle = randFloat(0, 2 * Math.PI);
559                 for (let t = 0; t < teamsArray.length; ++t)
560                 {
561                         let tileX = Math.floor(strongholdBasesRandom[t][0] / heightmapScale);
562                         let tileY = Math.floor(strongholdBasesRandom[t][1] / heightmapScale);
564                         let x = tileX / mapSize;
565                         let z = tileY / mapSize;
567                         let team = teamsArray[t].map(playerID => ({ "id": playerID }));
568                         let players = [];
570                         if (func)
571                                 func(tileX, tileY);
573                         for (let p = 0; p < team.length; ++p)
574                         {
575                                 let angle = startAngle + (p + 1) * TWO_PI / team.length;
577                                 players[p] = {
578                                         "id": team[p].id,
579                                         "x": x + groupedDistance * cos(angle),
580                                         "z": z + groupedDistance * sin(angle)
581                                 };
583                                 createBase(players[p], false);
584                         }
585                 }
586         }
587         else
588         {
589                 let players = groupPlayersByLocations(sortAllPlayers(), singleBases.map(l => ({
590                         "x": l[0] / heightmapScale / mapSize,
591                         "z": l[1] / heightmapScale / mapSize
592                 })));
594                 for (let player of players)
595                 {
596                         if (func)
597                                 func(Math.floor(player.x * mapSize), Math.floor(player.z * mapSize));
599                         createBase(player);
600                 }
601         }
605  * Creates tileClass for the default classes and every class given.
607  * @param {Array} newClasses
608  * @returns {Object} - maps from classname to ID
609  */
610 function initTileClasses(newClasses)
612         var classNames = g_DefaultTileClasses;
614         if (newClasses !== undefined)
615                 classNames = classNames.concat(newClasses);
617         g_TileClasses = {};
618         for (var className of classNames)
619                 g_TileClasses[className] = createTileClass();