Remove rmgen euclidian distance helper function, refs rP20328 / D969.
[0ad.git] / binaries / data / mods / public / maps / random / rmgen / library.js
blob7d39e9fedca071d40f0411d63edfecda41bb329f
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 MIN_MAP_SIZE = 128;
8 const MAX_MAP_SIZE = 512;
9 const MAP_BORDER_WIDTH = 3;
10 const FALLBACK_CIV = "athen";
11 /**
12  * Constants needed for heightmap_manipulation.js
13  */
14 const MAX_HEIGHT_RANGE = 0xFFFF / HEIGHT_UNITS_PER_METRE; // Engine limit, Roughly 700 meters
15 const MIN_HEIGHT = - SEA_LEVEL;
16 const MAX_HEIGHT = MAX_HEIGHT_RANGE - SEA_LEVEL;
17 // Default angle for buildings
18 const BUILDING_ORIENTATION = - PI / 4;
20 function fractionToTiles(f)
22         return g_Map.size * f;
25 function tilesToFraction(t)
27         return t / g_Map.size;
30 function fractionToSize(f)
32         return getMapArea() * f;
35 function sizeToFraction(s)
37         return s / getMapArea();
40 function scaleByMapSize(min, max)
42         return min + (max - min) * (g_Map.size - MIN_MAP_SIZE) / (MAX_MAP_SIZE - MIN_MAP_SIZE);
45 function cos(x)
47         return Math.cos(x);
50 function sin(x)
52         return Math.sin(x);
55 function abs(x) {
56         return Math.abs(x);
59 function round(x)
61         return Math.round(x);
64 function lerp(a, b, t)
66         return a + (b-a) * t;
69 function sqrt(x)
71         return Math.sqrt(x);
74 function ceil(x)
76         return Math.ceil(x);
79 function floor(x)
81         return Math.floor(x);
84 function max(a, b)
86         return a > b ? a : b;
89 function min(a, b)
91         return a < b ? a : b;
94 /**
95  * Retries the given function with those arguments as often as specified.
96  */
97 function retryPlacing(placeFunc, placeArgs, retryFactor, amount, getResult, behaveDeprecated = false)
99         if (behaveDeprecated && !(placeArgs.placer instanceof SimpleGroup || placeArgs.placer instanceof RandomGroup))
100                 warn("Deprecated version of createFoo should only be used for SimpleGroup and RandomGroup placers!");
102         let maxFail = amount * retryFactor;
104         let results = [];
105         let good = 0;
106         let bad = 0;
108         while (good < amount && bad <= maxFail)
109         {
110                 let result = placeFunc(placeArgs);
112                 if (result !== undefined || behaveDeprecated)
113                 {
114                         ++good;
115                         if (getResult)
116                                 results.push(result);
117                 }
118                 else
119                         ++bad;
120         }
121         return getResult ? results : good;
125  * Helper function for randomly placing areas and groups on the map.
126  */
127 function randomizePlacerCoordinates(placer, halfMapSize)
129         if (!!g_MapSettings.CircularMap)
130         {
131                 // Polar coordinates
132                 // Uniformly distributed on the disk
133                 let r = halfMapSize * Math.sqrt(randFloat(0, 1));
134                 let theta = randFloat(0, 2 * PI);
135                 placer.x = Math.floor(r * Math.cos(theta)) + halfMapSize;
136                 placer.z = Math.floor(r * Math.sin(theta)) + halfMapSize;
137         }
138         else
139         {
140                 // Rectangular coordinates
141                 placer.x = randIntExclusive(0, g_Map.size);
142                 placer.z = randIntExclusive(0, g_Map.size);
143         }
147  * Helper function for randomly placing areas and groups in the given areas.
148  */
149 function randomizePlacerCoordinatesFromAreas(placer, areas)
151         let pt = pickRandom(pickRandom(areas).points);
152         placer.x = pt.x;
153         placer.z = pt.z;
156 // TODO this is a hack to simulate the old behaviour of those functions
157 // until all old maps are changed to use the correct version of these functions
158 function createObjectGroupsDeprecated(placer, player, constraint, amount, retryFactor = 10)
160         return createObjectGroups(placer, player, constraint, amount, retryFactor, true);
163 function createObjectGroupsByAreasDeprecated(placer, player, constraint, amount, retryFactor, areas)
165         return createObjectGroupsByAreas(placer, player, constraint, amount, retryFactor, areas, true);
169  * Attempts to place the given number of areas in random places of the map.
170  * Returns actually placed areas.
171  */
172 function createAreas(centeredPlacer, painter, constraint, amount, retryFactor = 10, behaveDeprecated = false)
174         let placeFunc = function (args) {
175                 randomizePlacerCoordinates(args.placer, args.halfMapSize);
176                 return g_Map.createArea(args.placer, args.painter, args.constraint);
177         };
179         let args = {
180                 "placer": centeredPlacer,
181                 "painter": painter,
182                 "constraint": constraint,
183                 "halfMapSize": g_Map.size / 2
184         };
186         return retryPlacing(placeFunc, args, retryFactor, amount, true, behaveDeprecated);
190  * Attempts to place the given number of areas in random places of the given areas.
191  * Returns actually placed areas.
192  */
193 function createAreasInAreas(centeredPlacer, painter, constraint, amount, retryFactor, areas, behaveDeprecated = false)
195         if (!areas.length)
196                 return [];
198         let placeFunc = function (args) {
199                 randomizePlacerCoordinatesFromAreas(args.placer, args.areas);
200                 return g_Map.createArea(args.placer, args.painter, args.constraint);
201         };
203         let args = {
204                 "placer": centeredPlacer,
205                 "painter": painter,
206                 "constraint": constraint,
207                 "areas": areas,
208                 "halfMapSize": g_Map.size / 2
209         };
211         return retryPlacing(placeFunc, args, retryFactor, amount, true, behaveDeprecated);
215  * Attempts to place the given number of groups in random places of the map.
216  * Returns the number of actually placed groups.
217  */
218 function createObjectGroups(placer, player, constraint, amount, retryFactor = 10, behaveDeprecated = false)
220         let placeFunc = function (args) {
221                 randomizePlacerCoordinates(args.placer, args.halfMapSize);
222                 return createObjectGroup(args.placer, args.player, args.constraint);
223         };
225         let args = {
226                 "placer": placer,
227                 "player": player,
228                 "constraint": constraint,
229                 "halfMapSize": getMapSize() / 2 - MAP_BORDER_WIDTH
230         };
232         return retryPlacing(placeFunc, args, retryFactor, amount, false, behaveDeprecated);
236  * Attempts to place the given number of groups in random places of the given areas.
237  * Returns the number of actually placed groups.
238  */
239 function createObjectGroupsByAreas(placer, player, constraint, amount, retryFactor, areas, behaveDeprecated = false)
241         if (!areas.length)
242                 return 0;
244         let placeFunc = function (args) {
245                 randomizePlacerCoordinatesFromAreas(args.placer, args.areas);
246                 return createObjectGroup(args.placer, args.player, args.constraint);
247         };
249         let args = {
250                 "placer": placer,
251                 "player": player,
252                 "constraint": constraint,
253                 "areas": areas
254         };
256         return retryPlacing(placeFunc, args, retryFactor, amount, false, behaveDeprecated);
259 function createTerrain(terrain)
261         if (!(terrain instanceof Array))
262                 return createSimpleTerrain(terrain);
264         return new RandomTerrain(terrain.map(t => createTerrain(t)));
267 function createSimpleTerrain(terrain)
269         if (typeof(terrain) != "string")
270                 throw new Error("createSimpleTerrain expects string as input, received " + uneval(terrain));
272         // Split string by pipe | character, this allows specifying terrain + tree type in single string
273         let params = terrain.split(TERRAIN_SEPARATOR, 2);
275         if (params.length != 2)
276                 return new SimpleTerrain(terrain);
278         return new SimpleTerrain(params[0], params[1]);
281 function placeObject(x, z, type, player, angle)
283         if (g_Map.validT(x, z))
284                 g_Map.addObject(new Entity(type, player, x, z, angle));
287 function placeTerrain(x, z, terrain)
289         // convert terrain param into terrain object
290         g_Map.placeTerrain(x, z, createTerrain(terrain));
293 function initTerrain(tileClass)
295         g_Map.initTerrain(createTerrain(tileClass));
298 function isCircularMap()
300         return !!g_MapSettings.CircularMap;
303 function getMapBaseHeight()
305         return g_MapSettings.BaseHeight;
308 function createTileClass()
310         return g_Map.createTileClass();
313 function getTileClass(id)
315         if (!g_Map.validClass(id))
316                 return undefined;
318         return g_Map.tileClasses[id];
321 function createArea(placer, painter, constraint)
323         return g_Map.createArea(placer, painter, constraint);
326 function createObjectGroup(placer, player, constraint)
328         return g_Map.createObjectGroup(placer, player, constraint);
331 function getMapSize()
333         return g_Map.size;
336 function getMapArea()
338         return g_Map.size * g_Map.size;
341 function getNumPlayers()
343         return g_MapSettings.PlayerData.length - 1;
346 function getCivCode(player)
348         if (g_MapSettings.PlayerData[player+1].Civ)
349                 return g_MapSettings.PlayerData[player+1].Civ;
351         warn("undefined civ specified for player " + (player + 1) + ", falling back to '" + FALLBACK_CIV + "'");
352         return FALLBACK_CIV;
355 function areAllies(player1, player2)
357         if (g_MapSettings.PlayerData[player1+1].Team === undefined ||
358                 g_MapSettings.PlayerData[player2+1].Team === undefined ||
359                 g_MapSettings.PlayerData[player2+1].Team == -1 ||
360                 g_MapSettings.PlayerData[player1+1].Team == -1)
361                 return false;
363         return g_MapSettings.PlayerData[player1+1].Team === g_MapSettings.PlayerData[player2+1].Team;
366 function getPlayerTeam(player)
368         if (g_MapSettings.PlayerData[player+1].Team === undefined)
369                 return -1;
371         return g_MapSettings.PlayerData[player+1].Team;
375  * Sorts an array of player IDs by team index. Players without teams come first.
376  * Randomize order for players of the same team.
377  */
378 function sortPlayers(playerIndices)
380         return shuffleArray(playerIndices).sort((p1, p2) => getPlayerTeam(p1 - 1) - getPlayerTeam(p2 - 1));
384  * Mix player indices but sort by team.
386  * @returns {Array} - every item is an array of player indices
387  */
388 function sortAllPlayers()
390         let playerIDs = [];
391         for (let i = 0; i < getNumPlayers(); ++i)
392                 playerIDs.push(i+1);
394         return sortPlayers(playerIDs);
397 function primeSortPlayers(playerIndices)
399         if (!playerIndices.length)
400                 return [];
402         let prime = [];
403         for (let i = 0; i < Math.ceil(playerIndices.length / 2); ++i)
404         {
405                 prime.push(playerIndices[i]);
406                 prime.push(playerIndices[playerIndices.length - 1 - i]);
407         }
409         return prime;
412 function primeSortAllPlayers()
414         return primeSortPlayers(sortAllPlayers());
417 function radialPlayerPlacement(percentRadius = 0.35)
419         let playerIDs = sortAllPlayers();
421         let playerX = [];
422         let playerZ = [];
423         let playerAngle = [];
425         let startAngle = randFloat(0, TWO_PI);
427         for (let i = 0; i < getNumPlayers(); ++i)
428         {
429                 playerAngle[i] = startAngle + i * TWO_PI / getNumPlayers();
430                 playerX[i] = 0.5 + percentRadius * Math.cos(playerAngle[i]);
431                 playerZ[i] = 0.5 + percentRadius * Math.sin(playerAngle[i]);
432         }
434         return [playerIDs, playerX, playerZ, playerAngle, startAngle];
438  * Returns an array of percent numbers indicating the player location on river maps.
439  * For example [0.2, 0.2, 0.4, 0.4, 0.6, 0.6, 0.8, 0.8] for a 4v4 or
440  * [0.25, 0.33, 0.5, 0.67, 0.75] for a 2v3.
441  */
442 function placePlayersRiver()
444         let playerPos = [];
445         let numPlayers = getNumPlayers();
446         let numPlayersEven = numPlayers % 2 == 0;
448         for (let i = 0; i < numPlayers; ++i)
449         {
450                 let currentPlayerEven = i % 2 == 0;
452                 let offsetDivident = numPlayersEven || currentPlayerEven ? (i + 1) % 2 : 0;
453                 let offsetDivisor = numPlayersEven ? 0 : currentPlayerEven ? +1 : -1;
455                 playerPos[i] = ((i - 1 + offsetDivident) / 2 + 1) / ((numPlayers + offsetDivisor) / 2 + 1);
456         }
458         return playerPos;
461 function getStartingEntities(player)
463         let civ = getCivCode(player);
465         if (!g_CivData[civ] || !g_CivData[civ].StartEntities || !g_CivData[civ].StartEntities.length)
466         {
467                 warn("Invalid or unimplemented civ '"+civ+"' specified, falling back to '" + FALLBACK_CIV + "'");
468                 civ = FALLBACK_CIV;
469         }
471         return g_CivData[civ].StartEntities;
474 function getHeight(x, z)
476         return g_Map.getHeight(x, z);
479 function setHeight(x, z, height)
481         g_Map.setHeight(x, z, height);
485  *      Utility functions for classes
486  */
489  * Add point to given class by id
490  */
491 function addToClass(x, z, id)
493         let tileClass = getTileClass(id);
495         if (tileClass !== null)
496                 tileClass.add(x, z);
500  * Remove point from the given class by id
501  */
502 function removeFromClass(x, z, id)
504         let tileClass = getTileClass(id);
506         if (tileClass !== null)
507                 tileClass.remove(x, z);
511  * Create a painter for the given class
512  */
513 function paintClass(id)
515         return new TileClassPainter(getTileClass(id));
519  * Create a painter for the given class
520  */
521 function unPaintClass(id)
523         return new TileClassUnPainter(getTileClass(id));
527  * Create an avoid constraint for the given classes by the given distances
528  */
529 function avoidClasses(/*class1, dist1, class2, dist2, etc*/)
531         let ar = [];
532         for (let i = 0; i < arguments.length/2; ++i)
533                 ar.push(new AvoidTileClassConstraint(arguments[2*i], arguments[2*i+1]));
535         // Return single constraint
536         if (ar.length == 1)
537                 return ar[0];
539         return new AndConstraint(ar);
543  * Create a stay constraint for the given classes by the given distances
544  */
545 function stayClasses(/*class1, dist1, class2, dist2, etc*/)
547         let ar = [];
548         for (let i = 0; i < arguments.length/2; ++i)
549                 ar.push(new StayInTileClassConstraint(arguments[2*i], arguments[2*i+1]));
551         // Return single constraint
552         if (ar.length == 1)
553                 return ar[0];
555         return new AndConstraint(ar);
559  * Create a border constraint for the given classes by the given distances
560  */
561 function borderClasses(/*class1, idist1, odist1, class2, idist2, odist2, etc*/)
563         let ar = [];
564         for (let i = 0; i < arguments.length/3; ++i)
565                 ar.push(new BorderTileClassConstraint(arguments[3*i], arguments[3*i+1], arguments[3*i+2]));
567         // Return single constraint
568         if (ar.length == 1)
569                 return ar[0];
571         return new AndConstraint(ar);
575  * Checks if the given tile is in class "id"
576  */
577 function checkIfInClass(x, z, id)
579         let tileClass = getTileClass(id);
580         if (tileClass === null)
581                 return 0;
583         let members = tileClass.countMembersInRadius(x, z, 1);
584         if (members === null)
585                 return 0;
587         return members;
591  * Returns the angle of the vector between point 1 and point 2.
592  * The angle is counterclockwise from the positive x axis.
593  */
594 function getAngle(x1, z1, x2, z2)
596         return Math.atan2(z2 - z1, x2 - x1);
600  * Returns the gradient of the line between point 1 and 2 in the form dz/dx
601  */
602 function getGradient(x1, z1, x2, z2)
604         if (x1 == x2 && z1 == z2)
605                 return 0;
607         return (z1-z2)/(x1-x2);
610 function getTerrainTexture(x, y)
612         return g_Map.getTexture(x, y);
615 function addCivicCenterAreaToClass(ix, iz, tileClass)
617         addToClass(ix, iz, tileClass);
619         addToClass(ix, iz + 5, tileClass);
620         addToClass(ix, iz - 5, tileClass);
622         addToClass(ix + 5, iz, tileClass);
623         addToClass(ix - 5, iz, tileClass);
627  * Returns the order to go through the points for the shortest closed path (array of indices)
628  * @param {array} [points] - Points to be sorted of the form { "x": x_value, "y": y_value }
629  */
630 function getOrderOfPointsForShortestClosePath(points)
632         let order = [];
633         let distances = [];
634         if (points.length <= 3)
635         {
636                 for (let i = 0; i < points.length; ++i)
637                         order.push(i);
639                 return order;
640         }
642         // Just add the first 3 points
643         let pointsToAdd = clone(points);
644         for (let i = 0; i < 3; ++i)
645         {
646                 order.push(i);
647                 pointsToAdd.shift(i);
648                 if (i)
649                         distances.push(Math.euclidDistance2D(points[order[i]].x, points[order[i]].y, points[order[i - 1]].x, points[order[i - 1]].y));
650         }
652         distances.push(Math.euclidDistance2D(
653                 points[order[0]].x,
654                 points[order[0]].y,
655                 points[order[order.length - 1]].x,
656                 points[order[order.length - 1]].y));
658         // Add remaining points so the path lengthens the least
659         let numPointsToAdd = pointsToAdd.length;
660         for (let i = 0; i < numPointsToAdd; ++i)
661         {
662                 let indexToAddTo;
663                 let minEnlengthen = Infinity;
664                 let minDist1 = 0;
665                 let minDist2 = 0;
666                 for (let k = 0; k < order.length; ++k)
667                 {
668                         let dist1 = Math.euclidDistance2D(pointsToAdd[0].x, pointsToAdd[0].y, points[order[k]].x, points[order[k]].y);
669                         let dist2 = Math.euclidDistance2D(pointsToAdd[0].x, pointsToAdd[0].y, points[order[(k + 1) % order.length]].x, points[order[(k + 1) % order.length]].y);
670                         let enlengthen = dist1 + dist2 - distances[k];
671                         if (enlengthen < minEnlengthen)
672                         {
673                                 indexToAddTo = k;
674                                 minEnlengthen = enlengthen;
675                                 minDist1 = dist1;
676                                 minDist2 = dist2;
677                         }
678                 }
679                 order.splice(indexToAddTo + 1, 0, i + 3);
680                 distances.splice(indexToAddTo, 1, minDist1, minDist2);
681                 pointsToAdd.shift();
682         }
684         return order;