2 const TWO_PI = 2 * Math.PI;
3 const TERRAIN_SEPARATOR = "|";
4 const SEA_LEVEL = 20.0;
6 const HEIGHT_UNITS_PER_METRE = 92;
7 const MAP_BORDER_WIDTH = 3;
8 const FALLBACK_CIV = "athen";
10 * Constants needed for heightmap_manipulation.js
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);
62 function lerp(a, b, t)
93 * Retries the given function with those arguments as often as specified.
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;
106 while (good < amount && bad <= maxFail)
108 let result = placeFunc(placeArgs);
110 if (result !== undefined || behaveDeprecated)
114 results.push(result);
119 return getResult ? results : good;
123 * Helper function for randomly placing areas and groups on the map.
125 function randomizePlacerCoordinates(placer, halfMapSize)
127 if (!!g_MapSettings.CircularMap)
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;
138 // Rectangular coordinates
139 placer.x = randIntExclusive(0, g_Map.size);
140 placer.z = randIntExclusive(0, g_Map.size);
145 * Helper function for randomly placing areas and groups in the given areas.
147 function randomizePlacerCoordinatesFromAreas(placer, areas)
149 let pt = pickRandom(pickRandom(areas).points);
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.
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);
178 "placer": centeredPlacer,
180 "constraint": constraint,
181 "halfMapSize": g_Map.size / 2
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.
191 function createAreasInAreas(centeredPlacer, painter, constraint, amount, retryFactor, areas, behaveDeprecated = false)
196 let placeFunc = function (args) {
197 randomizePlacerCoordinatesFromAreas(args.placer, args.areas);
198 return g_Map.createArea(args.placer, args.painter, args.constraint);
202 "placer": centeredPlacer,
204 "constraint": constraint,
206 "halfMapSize": g_Map.size / 2
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.
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);
226 "constraint": constraint,
227 "halfMapSize": getMapSize() / 2 - MAP_BORDER_WIDTH
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.
237 function createObjectGroupsByAreas(placer, player, constraint, amount, retryFactor, areas, behaveDeprecated = false)
242 let placeFunc = function (args) {
243 randomizePlacerCoordinatesFromAreas(args.placer, args.areas);
244 return createObjectGroup(args.placer, args.player, args.constraint);
250 "constraint": constraint,
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))
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()
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 + "'");
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)
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)
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.
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
386 function sortAllPlayers()
389 for (let i = 0; i < getNumPlayers(); ++i)
392 return sortPlayers(playerIDs);
395 function primeSortPlayers(playerIndices)
397 if (!playerIndices.length)
401 for (let i = 0; i < Math.ceil(playerIndices.length / 2); ++i)
403 prime.push(playerIndices[i]);
404 prime.push(playerIndices[playerIndices.length - 1 - i]);
410 function primeSortAllPlayers()
412 return primeSortPlayers(sortAllPlayers());
415 function radialPlayerPlacement(percentRadius = 0.35)
417 let playerIDs = sortAllPlayers();
421 let playerAngle = [];
423 let startAngle = randFloat(0, TWO_PI);
425 for (let i = 0; i < getNumPlayers(); ++i)
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]);
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.
440 function placePlayersRiver()
443 let numPlayers = getNumPlayers();
444 let numPlayersEven = numPlayers % 2 == 0;
446 for (let i = 0; i < numPlayers; ++i)
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);
459 function getStartingEntities(player)
461 let civ = getCivCode(player);
463 if (!g_CivData[civ] || !g_CivData[civ].StartEntities || !g_CivData[civ].StartEntities.length)
465 warn("Invalid or unimplemented civ '"+civ+"' specified, falling back to '" + FALLBACK_CIV + "'");
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
487 * Add point to given class by id
489 function addToClass(x, z, id)
491 let tileClass = getTileClass(id);
493 if (tileClass !== null)
498 * Remove point from the given class by id
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
511 function paintClass(id)
513 return new TileClassPainter(getTileClass(id));
517 * Create a painter for the given class
519 function unPaintClass(id)
521 return new TileClassUnPainter(getTileClass(id));
525 * Create an avoid constraint for the given classes by the given distances
527 function avoidClasses(/*class1, dist1, class2, dist2, etc*/)
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
537 return new AndConstraint(ar);
541 * Create a stay constraint for the given classes by the given distances
543 function stayClasses(/*class1, dist1, class2, dist2, etc*/)
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
553 return new AndConstraint(ar);
557 * Create a border constraint for the given classes by the given distances
559 function borderClasses(/*class1, idist1, odist1, class2, idist2, odist2, etc*/)
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
569 return new AndConstraint(ar);
573 * Checks if the given tile is in class "id"
575 function checkIfInClass(x, z, id)
577 let tileClass = getTileClass(id);
578 if (tileClass === null)
581 let members = tileClass.countMembersInRadius(x, z, 1);
582 if (members === null)
589 * Returns the angle of the vector between point 1 and point 2.
590 * The angle is counterclockwise from the positive x axis.
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
600 function getGradient(x1, z1, x2, z2)
602 if (x1 == x2 && z1 == z2)
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 }
628 function getOrderOfPointsForShortestClosePath(points)
632 if (points.length <= 3)
634 for (let i = 0; i < points.length; ++i)
640 // Just add the first 3 points
641 let pointsToAdd = clone(points);
642 for (let i = 0; i < 3; ++i)
645 pointsToAdd.shift(i);
647 distances.push(Math.euclidDistance2D(points[order[i]].x, points[order[i]].y, points[order[i - 1]].x, points[order[i - 1]].y));
650 distances.push(Math.euclidDistance2D(
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)
661 let minEnlengthen = Infinity;
664 for (let k = 0; k < order.length; ++k)
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)
672 minEnlengthen = enlengthen;
677 order.splice(indexToAddTo + 1, 0, i + 3);
678 distances.splice(indexToAddTo, 1, minDist1, minDist2);