Remove min/max mapsize constants from the rmgen library, refs #4034.
[0ad.git] / binaries / data / mods / public / maps / random / rmgen / library.js
blobd4d0855e2ae701a4b0fb50c37c28cd3eacbfbb7d
1 const PI = Math.PI;
2 const TWO_PI = 2 * Math.PI;
3 const TERRAIN_SEPARATOR = "|";
4 const SEA_LEVEL = 20.0;
5 const CELL_SIZE = 4;
6 const HEIGHT_UNITS_PER_METRE = 92;
7 const MAP_BORDER_WIDTH = 3;
8 const FALLBACK_CIV = "athen";
9 /**
10  * Constants needed for heightmap_manipulation.js
11  */
12 const MAX_HEIGHT_RANGE = 0xFFFF / HEIGHT_UNITS_PER_METRE; // Engine limit, Roughly 700 meters
13 const MIN_HEIGHT = - SEA_LEVEL;
14 const MAX_HEIGHT = MAX_HEIGHT_RANGE - SEA_LEVEL;
15 // Default angle for buildings
16 const BUILDING_ORIENTATION = - PI / 4;
18 function fractionToTiles(f)
20         return g_Map.size * f;
23 function tilesToFraction(t)
25         return t / g_Map.size;
28 function fractionToSize(f)
30         return getMapArea() * f;
33 function sizeToFraction(s)
35         return s / getMapArea();
38 function scaleByMapSize(min, max, minMapSize = 128, maxMapSize = 512)
40         return min + (max - min) * (g_Map.size - minMapSize) / (maxMapSize - minMapSize);
43 function cos(x)
45         return Math.cos(x);
48 function sin(x)
50         return Math.sin(x);
53 function abs(x) {
54         return Math.abs(x);
57 function round(x)
59         return Math.round(x);
62 function lerp(a, b, t)
64         return a + (b-a) * t;
67 function sqrt(x)
69         return Math.sqrt(x);
72 function ceil(x)
74         return Math.ceil(x);
77 function floor(x)
79         return Math.floor(x);
82 function max(a, b)
84         return a > b ? a : b;
87 function min(a, b)
89         return a < b ? a : b;
92 /**
93  * Retries the given function with those arguments as often as specified.
94  */
95 function retryPlacing(placeFunc, placeArgs, retryFactor, amount, getResult, behaveDeprecated = false)
97         if (behaveDeprecated && !(placeArgs.placer instanceof SimpleGroup || placeArgs.placer instanceof RandomGroup))
98                 warn("Deprecated version of createFoo should only be used for SimpleGroup and RandomGroup placers!");
100         let maxFail = amount * retryFactor;
102         let results = [];
103         let good = 0;
104         let bad = 0;
106         while (good < amount && bad <= maxFail)
107         {
108                 let result = placeFunc(placeArgs);
110                 if (result !== undefined || behaveDeprecated)
111                 {
112                         ++good;
113                         if (getResult)
114                                 results.push(result);
115                 }
116                 else
117                         ++bad;
118         }
119         return getResult ? results : good;
123  * Helper function for randomly placing areas and groups on the map.
124  */
125 function randomizePlacerCoordinates(placer, halfMapSize)
127         if (!!g_MapSettings.CircularMap)
128         {
129                 // Polar coordinates
130                 // Uniformly distributed on the disk
131                 let r = halfMapSize * Math.sqrt(randFloat(0, 1));
132                 let theta = randFloat(0, 2 * PI);
133                 placer.x = Math.floor(r * Math.cos(theta)) + halfMapSize;
134                 placer.z = Math.floor(r * Math.sin(theta)) + halfMapSize;
135         }
136         else
137         {
138                 // Rectangular coordinates
139                 placer.x = randIntExclusive(0, g_Map.size);
140                 placer.z = randIntExclusive(0, g_Map.size);
141         }
145  * Helper function for randomly placing areas and groups in the given areas.
146  */
147 function randomizePlacerCoordinatesFromAreas(placer, areas)
149         let pt = pickRandom(pickRandom(areas).points);
150         placer.x = pt.x;
151         placer.z = pt.z;
154 // TODO this is a hack to simulate the old behaviour of those functions
155 // until all old maps are changed to use the correct version of these functions
156 function createObjectGroupsDeprecated(placer, player, constraint, amount, retryFactor = 10)
158         return createObjectGroups(placer, player, constraint, amount, retryFactor, true);
161 function createObjectGroupsByAreasDeprecated(placer, player, constraint, amount, retryFactor, areas)
163         return createObjectGroupsByAreas(placer, player, constraint, amount, retryFactor, areas, true);
167  * Attempts to place the given number of areas in random places of the map.
168  * Returns actually placed areas.
169  */
170 function createAreas(centeredPlacer, painter, constraint, amount, retryFactor = 10, behaveDeprecated = false)
172         let placeFunc = function (args) {
173                 randomizePlacerCoordinates(args.placer, args.halfMapSize);
174                 return g_Map.createArea(args.placer, args.painter, args.constraint);
175         };
177         let args = {
178                 "placer": centeredPlacer,
179                 "painter": painter,
180                 "constraint": constraint,
181                 "halfMapSize": g_Map.size / 2
182         };
184         return retryPlacing(placeFunc, args, retryFactor, amount, true, behaveDeprecated);
188  * Attempts to place the given number of areas in random places of the given areas.
189  * Returns actually placed areas.
190  */
191 function createAreasInAreas(centeredPlacer, painter, constraint, amount, retryFactor, areas, behaveDeprecated = false)
193         if (!areas.length)
194                 return [];
196         let placeFunc = function (args) {
197                 randomizePlacerCoordinatesFromAreas(args.placer, args.areas);
198                 return g_Map.createArea(args.placer, args.painter, args.constraint);
199         };
201         let args = {
202                 "placer": centeredPlacer,
203                 "painter": painter,
204                 "constraint": constraint,
205                 "areas": areas,
206                 "halfMapSize": g_Map.size / 2
207         };
209         return retryPlacing(placeFunc, args, retryFactor, amount, true, behaveDeprecated);
213  * Attempts to place the given number of groups in random places of the map.
214  * Returns the number of actually placed groups.
215  */
216 function createObjectGroups(placer, player, constraint, amount, retryFactor = 10, behaveDeprecated = false)
218         let placeFunc = function (args) {
219                 randomizePlacerCoordinates(args.placer, args.halfMapSize);
220                 return createObjectGroup(args.placer, args.player, args.constraint);
221         };
223         let args = {
224                 "placer": placer,
225                 "player": player,
226                 "constraint": constraint,
227                 "halfMapSize": getMapSize() / 2 - MAP_BORDER_WIDTH
228         };
230         return retryPlacing(placeFunc, args, retryFactor, amount, false, behaveDeprecated);
234  * Attempts to place the given number of groups in random places of the given areas.
235  * Returns the number of actually placed groups.
236  */
237 function createObjectGroupsByAreas(placer, player, constraint, amount, retryFactor, areas, behaveDeprecated = false)
239         if (!areas.length)
240                 return 0;
242         let placeFunc = function (args) {
243                 randomizePlacerCoordinatesFromAreas(args.placer, args.areas);
244                 return createObjectGroup(args.placer, args.player, args.constraint);
245         };
247         let args = {
248                 "placer": placer,
249                 "player": player,
250                 "constraint": constraint,
251                 "areas": areas
252         };
254         return retryPlacing(placeFunc, args, retryFactor, amount, false, behaveDeprecated);
257 function createTerrain(terrain)
259         if (!(terrain instanceof Array))
260                 return createSimpleTerrain(terrain);
262         return new RandomTerrain(terrain.map(t => createTerrain(t)));
265 function createSimpleTerrain(terrain)
267         if (typeof(terrain) != "string")
268                 throw new Error("createSimpleTerrain expects string as input, received " + uneval(terrain));
270         // Split string by pipe | character, this allows specifying terrain + tree type in single string
271         let params = terrain.split(TERRAIN_SEPARATOR, 2);
273         if (params.length != 2)
274                 return new SimpleTerrain(terrain);
276         return new SimpleTerrain(params[0], params[1]);
279 function placeObject(x, z, type, player, angle)
281         if (g_Map.validT(x, z))
282                 g_Map.addObject(new Entity(type, player, x, z, angle));
285 function placeTerrain(x, z, terrain)
287         // convert terrain param into terrain object
288         g_Map.placeTerrain(x, z, createTerrain(terrain));
291 function initTerrain(terrain)
293         g_Map.initTerrain(createTerrain(terrain));
296 function isCircularMap()
298         return !!g_MapSettings.CircularMap;
301 function getMapBaseHeight()
303         return g_MapSettings.BaseHeight;
306 function createTileClass()
308         return g_Map.createTileClass();
311 function getTileClass(id)
313         if (!g_Map.validClass(id))
314                 return undefined;
316         return g_Map.tileClasses[id];
319 function createArea(placer, painter, constraint)
321         return g_Map.createArea(placer, painter, constraint);
324 function createObjectGroup(placer, player, constraint)
326         return g_Map.createObjectGroup(placer, player, constraint);
329 function getMapSize()
331         return g_Map.size;
334 function getMapArea()
336         return g_Map.size * g_Map.size;
339 function getNumPlayers()
341         return g_MapSettings.PlayerData.length - 1;
344 function getCivCode(player)
346         if (g_MapSettings.PlayerData[player+1].Civ)
347                 return g_MapSettings.PlayerData[player+1].Civ;
349         warn("undefined civ specified for player " + (player + 1) + ", falling back to '" + FALLBACK_CIV + "'");
350         return FALLBACK_CIV;
353 function areAllies(player1, player2)
355         if (g_MapSettings.PlayerData[player1+1].Team === undefined ||
356                 g_MapSettings.PlayerData[player2+1].Team === undefined ||
357                 g_MapSettings.PlayerData[player2+1].Team == -1 ||
358                 g_MapSettings.PlayerData[player1+1].Team == -1)
359                 return false;
361         return g_MapSettings.PlayerData[player1+1].Team === g_MapSettings.PlayerData[player2+1].Team;
364 function getPlayerTeam(player)
366         if (g_MapSettings.PlayerData[player+1].Team === undefined)
367                 return -1;
369         return g_MapSettings.PlayerData[player+1].Team;
373  * Sorts an array of player IDs by team index. Players without teams come first.
374  * Randomize order for players of the same team.
375  */
376 function sortPlayers(playerIndices)
378         return shuffleArray(playerIndices).sort((p1, p2) => getPlayerTeam(p1 - 1) - getPlayerTeam(p2 - 1));
382  * Mix player indices but sort by team.
384  * @returns {Array} - every item is an array of player indices
385  */
386 function sortAllPlayers()
388         let playerIDs = [];
389         for (let i = 0; i < getNumPlayers(); ++i)
390                 playerIDs.push(i+1);
392         return sortPlayers(playerIDs);
395 function primeSortPlayers(playerIndices)
397         if (!playerIndices.length)
398                 return [];
400         let prime = [];
401         for (let i = 0; i < Math.ceil(playerIndices.length / 2); ++i)
402         {
403                 prime.push(playerIndices[i]);
404                 prime.push(playerIndices[playerIndices.length - 1 - i]);
405         }
407         return prime;
410 function primeSortAllPlayers()
412         return primeSortPlayers(sortAllPlayers());
415 function radialPlayerPlacement(percentRadius = 0.35)
417         let playerIDs = sortAllPlayers();
419         let playerX = [];
420         let playerZ = [];
421         let playerAngle = [];
423         let startAngle = randFloat(0, TWO_PI);
425         for (let i = 0; i < getNumPlayers(); ++i)
426         {
427                 playerAngle[i] = startAngle + i * TWO_PI / getNumPlayers();
428                 playerX[i] = 0.5 + percentRadius * Math.cos(playerAngle[i]);
429                 playerZ[i] = 0.5 + percentRadius * Math.sin(playerAngle[i]);
430         }
432         return [playerIDs, playerX, playerZ, playerAngle, startAngle];
436  * Returns an array of percent numbers indicating the player location on river maps.
437  * For example [0.2, 0.2, 0.4, 0.4, 0.6, 0.6, 0.8, 0.8] for a 4v4 or
438  * [0.25, 0.33, 0.5, 0.67, 0.75] for a 2v3.
439  */
440 function placePlayersRiver()
442         let playerPos = [];
443         let numPlayers = getNumPlayers();
444         let numPlayersEven = numPlayers % 2 == 0;
446         for (let i = 0; i < numPlayers; ++i)
447         {
448                 let currentPlayerEven = i % 2 == 0;
450                 let offsetDivident = numPlayersEven || currentPlayerEven ? (i + 1) % 2 : 0;
451                 let offsetDivisor = numPlayersEven ? 0 : currentPlayerEven ? +1 : -1;
453                 playerPos[i] = ((i - 1 + offsetDivident) / 2 + 1) / ((numPlayers + offsetDivisor) / 2 + 1);
454         }
456         return playerPos;
459 function getStartingEntities(player)
461         let civ = getCivCode(player);
463         if (!g_CivData[civ] || !g_CivData[civ].StartEntities || !g_CivData[civ].StartEntities.length)
464         {
465                 warn("Invalid or unimplemented civ '"+civ+"' specified, falling back to '" + FALLBACK_CIV + "'");
466                 civ = FALLBACK_CIV;
467         }
469         return g_CivData[civ].StartEntities;
472 function getHeight(x, z)
474         return g_Map.getHeight(x, z);
477 function setHeight(x, z, height)
479         g_Map.setHeight(x, z, height);
483  *      Utility functions for classes
484  */
487  * Add point to given class by id
488  */
489 function addToClass(x, z, id)
491         let tileClass = getTileClass(id);
493         if (tileClass !== null)
494                 tileClass.add(x, z);
498  * Remove point from the given class by id
499  */
500 function removeFromClass(x, z, id)
502         let tileClass = getTileClass(id);
504         if (tileClass !== null)
505                 tileClass.remove(x, z);
509  * Create a painter for the given class
510  */
511 function paintClass(id)
513         return new TileClassPainter(getTileClass(id));
517  * Create a painter for the given class
518  */
519 function unPaintClass(id)
521         return new TileClassUnPainter(getTileClass(id));
525  * Create an avoid constraint for the given classes by the given distances
526  */
527 function avoidClasses(/*class1, dist1, class2, dist2, etc*/)
529         let ar = [];
530         for (let i = 0; i < arguments.length/2; ++i)
531                 ar.push(new AvoidTileClassConstraint(arguments[2*i], arguments[2*i+1]));
533         // Return single constraint
534         if (ar.length == 1)
535                 return ar[0];
537         return new AndConstraint(ar);
541  * Create a stay constraint for the given classes by the given distances
542  */
543 function stayClasses(/*class1, dist1, class2, dist2, etc*/)
545         let ar = [];
546         for (let i = 0; i < arguments.length/2; ++i)
547                 ar.push(new StayInTileClassConstraint(arguments[2*i], arguments[2*i+1]));
549         // Return single constraint
550         if (ar.length == 1)
551                 return ar[0];
553         return new AndConstraint(ar);
557  * Create a border constraint for the given classes by the given distances
558  */
559 function borderClasses(/*class1, idist1, odist1, class2, idist2, odist2, etc*/)
561         let ar = [];
562         for (let i = 0; i < arguments.length/3; ++i)
563                 ar.push(new BorderTileClassConstraint(arguments[3*i], arguments[3*i+1], arguments[3*i+2]));
565         // Return single constraint
566         if (ar.length == 1)
567                 return ar[0];
569         return new AndConstraint(ar);
573  * Checks if the given tile is in class "id"
574  */
575 function checkIfInClass(x, z, id)
577         let tileClass = getTileClass(id);
578         if (tileClass === null)
579                 return 0;
581         let members = tileClass.countMembersInRadius(x, z, 1);
582         if (members === null)
583                 return 0;
585         return members;
589  * Returns the angle of the vector between point 1 and point 2.
590  * The angle is counterclockwise from the positive x axis.
591  */
592 function getAngle(x1, z1, x2, z2)
594         return Math.atan2(z2 - z1, x2 - x1);
598  * Returns the gradient of the line between point 1 and 2 in the form dz/dx
599  */
600 function getGradient(x1, z1, x2, z2)
602         if (x1 == x2 && z1 == z2)
603                 return 0;
605         return (z1-z2)/(x1-x2);
608 function getTerrainTexture(x, y)
610         return g_Map.getTexture(x, y);
613 function addCivicCenterAreaToClass(ix, iz, tileClass)
615         addToClass(ix, iz, tileClass);
617         addToClass(ix, iz + 5, tileClass);
618         addToClass(ix, iz - 5, tileClass);
620         addToClass(ix + 5, iz, tileClass);
621         addToClass(ix - 5, iz, tileClass);
625  * Returns the order to go through the points for the shortest closed path (array of indices)
626  * @param {array} [points] - Points to be sorted of the form { "x": x_value, "y": y_value }
627  */
628 function getOrderOfPointsForShortestClosePath(points)
630         let order = [];
631         let distances = [];
632         if (points.length <= 3)
633         {
634                 for (let i = 0; i < points.length; ++i)
635                         order.push(i);
637                 return order;
638         }
640         // Just add the first 3 points
641         let pointsToAdd = clone(points);
642         for (let i = 0; i < 3; ++i)
643         {
644                 order.push(i);
645                 pointsToAdd.shift(i);
646                 if (i)
647                         distances.push(Math.euclidDistance2D(points[order[i]].x, points[order[i]].y, points[order[i - 1]].x, points[order[i - 1]].y));
648         }
650         distances.push(Math.euclidDistance2D(
651                 points[order[0]].x,
652                 points[order[0]].y,
653                 points[order[order.length - 1]].x,
654                 points[order[order.length - 1]].y));
656         // Add remaining points so the path lengthens the least
657         let numPointsToAdd = pointsToAdd.length;
658         for (let i = 0; i < numPointsToAdd; ++i)
659         {
660                 let indexToAddTo;
661                 let minEnlengthen = Infinity;
662                 let minDist1 = 0;
663                 let minDist2 = 0;
664                 for (let k = 0; k < order.length; ++k)
665                 {
666                         let dist1 = Math.euclidDistance2D(pointsToAdd[0].x, pointsToAdd[0].y, points[order[k]].x, points[order[k]].y);
667                         let dist2 = Math.euclidDistance2D(pointsToAdd[0].x, pointsToAdd[0].y, points[order[(k + 1) % order.length]].x, points[order[(k + 1) % order.length]].y);
668                         let enlengthen = dist1 + dist2 - distances[k];
669                         if (enlengthen < minEnlengthen)
670                         {
671                                 indexToAddTo = k;
672                                 minEnlengthen = enlengthen;
673                                 minDist1 = dist1;
674                                 minDist2 = dist2;
675                         }
676                 }
677                 order.splice(indexToAddTo + 1, 0, i + 3);
678                 distances.splice(indexToAddTo, 1, minDist1, minDist2);
679                 pointsToAdd.shift();
680         }
682         return order;