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 = [
58 * Adds an array of elements to the map.
60 function addElements(elements)
62 for (let element of elements)
65 avoidClasses.apply(null, element.avoid),
66 stayClasses.apply(null, element.stay || null)
68 pickSize(element.sizes),
69 pickMix(element.mixes),
70 pickAmount(element.amounts),
71 element.baseHeight || 0);
75 * Converts "amount" terms to numbers.
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;
88 * Converts "mix" terms to numbers.
90 function pickMix(mixes)
92 let mix = pickRandom(mixes);
97 return g_Mixes.normal;
101 * Converts "size" terms to numbers.
103 function pickSize(sizes)
105 let size = pickRandom(sizes);
108 return g_Sizes[size];
110 return g_Sizes.normal;
114 * Paints the entire map with the given terrain texture, tileclass and elevation.
116 function resetTerrain(terrain, tileClass, elevation)
118 let center = Math.round(fractionToTiles(0.5));
120 new ClumpPlacer(getMapArea(), 1, 1, 1, center, center),
122 new LayeredPainter([terrain], []),
123 new SmoothElevationPainter(ELEVATION_SET, elevation, 1),
124 paintClass(tileClass)
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
138 function addBases(type, distance, groupedDistance, startAngle)
140 let playerIDs = sortAllPlayers();
141 let teamsArray = getTeamsArray();
146 return placeLine(teamsArray, distance, groupedDistance, startAngle);
148 return placeRadial(playerIDs, distance, startAngle);
150 return placeRandom(playerIDs) || placeRadial(playerIDs, distance, startAngle);
152 return placeStronghold(teamsArray, distance, groupedDistance, startAngle);
154 warn("Unknown base placement type:" + type);
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
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);
175 addCivicCenterAreaToClass(ix, iz, g_TileClasses.player);
177 if (walls && mapSize > 192)
178 placeCivDefaultEntities(fx, fz, player.id);
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);
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
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);
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
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
224 createObjectGroup(group, 0, avoidClasses(g_TileClasses.baseResource, 2));
229 g_TileClasses.baseResource,
230 avoidClasses(g_TileClasses.baseResource, 4),
234 // Create starting trees
235 var num = currentBiome() == "savanna" ? 5 : 15;
236 for (var tries = 0; tries < 10; ++tries)
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
248 if (createObjectGroup(group, 0, avoidClasses(g_TileClasses.baseResource, 4)))
252 placeDefaultDecoratives(
255 g_Decoratives.grassShort,
256 g_TileClasses.baseResource,
258 avoidClasses(g_TileClasses.baseResource, 4));
262 * Return an array where each element is an array of playerIndices of a team.
264 function getTeamsArray()
266 var numPlayers = getNumPlayers();
268 // Group players by team
270 for (let i = 0; i < numPlayers; ++i)
272 let team = getPlayerTeam(i);
279 teams[team].push(i+1);
282 // Players without a team get a custom index
283 for (let i = 0; i < numPlayers; ++i)
284 if (getPlayerTeam(i) == -1)
287 // Remove unused indices
288 return teams.filter(team => true);
292 * Choose a random pattern for placing the bases of the players.
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");
313 "setup": pickRandom(formats),
314 "distance": randFloat(0.2, 0.35),
315 "separation": randFloat(0.05, 0.1)
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
329 function placeLine(teamsArray, distance, groupedDistance, startAngle)
333 for (let i = 0; i < teamsArray.length; ++i)
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)
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)
349 createBase(players[teamsArray[i][p]], false);
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
363 function placeRadial(playerIDs, distance, startAngle)
366 let numPlayers = getNumPlayers();
368 for (let i = 0; i < numPlayers; ++i)
370 let angle = startAngle + i * 2 * Math.PI / numPlayers;
373 "x": 0.5 + distance * cos(angle),
374 "z": 0.5 + distance * sin(angle)
376 createBase(players[i]);
383 * Choose arbitrary starting locations.
385 function placeRandom(playerIDs)
391 for (let i = 0; i < getNumPlayers(); ++i)
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))
407 // Reset if we're in what looks like an infinite loop
415 // If we only pick bad locations, stop trying to place randomly
428 let players = groupPlayersByLocations(playerIDs, locations);
429 for (let player of 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.
441 function groupPlayersByLocations(playerIDs, locations)
443 playerIDs = sortPlayers(playerIDs);
445 let minDist = Infinity;
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)
456 for (let i = 1; i < playerIDs.length; ++i)
458 let team1 = g_MapSettings.PlayerData[playerIDs[i - 1]].Team;
459 let team2 = g_MapSettings.PlayerData[playerIDs[i]].Team;
462 if (team1 != -1 && team1 == team2)
463 teamDist += Math.euclidDistance2D(permutation[i - 1].x, permutation[i - 1].z, permutation[i].x, permutation[i].z);
466 dist += teamDist / teamSize;
473 dist += teamDist / teamSize;
478 minLocations = permutation;
483 for (let i = 0; i < playerIDs.length; ++i)
485 let player = minLocations[i];
486 player.id = playerIDs[i];
487 players.push(player);
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
500 function placeStronghold(teamsArray, distance, groupedDistance, startAngle)
504 for (let i = 0; i < teamsArray.length; ++i)
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)
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)
530 createBase(players[teamsArray[i][p]], false);
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.
546 function randomPlayerPlacementAt(teamsArray, singleBases, strongholdBases, heightmapScale, groupedDistance, func)
548 let strongholdBasesRandom = shuffleArray(strongholdBases);
549 let mapSize = getMapSize();
553 teamsArray.length >= 2 &&
554 teamsArray.length < getNumPlayers() &&
555 teamsArray.length <= strongholdBasesRandom.length)
557 let startAngle = randFloat(0, 2 * Math.PI);
559 for (let t = 0; t < teamsArray.length; ++t)
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 }));
573 for (let p = 0; p < team.length; ++p)
575 let angle = startAngle + (p + 1) * TWO_PI / team.length;
579 "x": x + groupedDistance * cos(angle),
580 "z": z + groupedDistance * sin(angle)
583 createBase(players[p], false);
589 let players = groupPlayersByLocations(sortAllPlayers(), singleBases.map(l => ({
590 "x": l[0] / heightmapScale / mapSize,
591 "z": l[1] / heightmapScale / mapSize
594 for (let player of players)
597 func(Math.floor(player.x * mapSize), Math.floor(player.z * mapSize));
605 * Creates tileClass for the default classes and every class given.
607 * @param {Array} newClasses
608 * @returns {Object} - maps from classname to ID
610 function initTileClasses(newClasses)
612 var classNames = g_DefaultTileClasses;
614 if (newClasses !== undefined)
615 classNames = classNames.concat(newClasses);
618 for (var className of classNames)
619 g_TileClasses[className] = createTileClass();