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 MIN_MAP_SIZE = 128;
8 const MAX_MAP_SIZE = 512;
9 const MAP_BORDER_WIDTH = 3;
10 const FALLBACK_CIV = "athen";
12 * Constants needed for heightmap_manipulation.js
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);
64 function lerp(a, b, t)
95 * Retries the given function with those arguments as often as specified.
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;
108 while (good < amount && bad <= maxFail)
110 let result = placeFunc(placeArgs);
112 if (result !== undefined || behaveDeprecated)
116 results.push(result);
121 return getResult ? results : good;
125 * Helper function for randomly placing areas and groups on the map.
127 function randomizePlacerCoordinates(placer, halfMapSize)
129 if (!!g_MapSettings.CircularMap)
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;
140 // Rectangular coordinates
141 placer.x = randIntExclusive(0, g_Map.size);
142 placer.z = randIntExclusive(0, g_Map.size);
147 * Helper function for randomly placing areas and groups in the given areas.
149 function randomizePlacerCoordinatesFromAreas(placer, areas)
151 let pt = pickRandom(pickRandom(areas).points);
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.
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);
180 "placer": centeredPlacer,
182 "constraint": constraint,
183 "halfMapSize": g_Map.size / 2
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.
193 function createAreasInAreas(centeredPlacer, painter, constraint, amount, retryFactor, areas, behaveDeprecated = false)
198 let placeFunc = function (args) {
199 randomizePlacerCoordinatesFromAreas(args.placer, args.areas);
200 return g_Map.createArea(args.placer, args.painter, args.constraint);
204 "placer": centeredPlacer,
206 "constraint": constraint,
208 "halfMapSize": g_Map.size / 2
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.
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);
228 "constraint": constraint,
229 "halfMapSize": getMapSize() / 2 - MAP_BORDER_WIDTH
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.
239 function createObjectGroupsByAreas(placer, player, constraint, amount, retryFactor, areas, behaveDeprecated = false)
244 let placeFunc = function (args) {
245 randomizePlacerCoordinatesFromAreas(args.placer, args.areas);
246 return createObjectGroup(args.placer, args.player, args.constraint);
252 "constraint": constraint,
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(terrain)
295 g_Map.initTerrain(createTerrain(terrain));
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))
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()
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 + "'");
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)
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)
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.
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
388 function sortAllPlayers()
391 for (let i = 0; i < getNumPlayers(); ++i)
394 return sortPlayers(playerIDs);
397 function primeSortPlayers(playerIndices)
399 if (!playerIndices.length)
403 for (let i = 0; i < Math.ceil(playerIndices.length / 2); ++i)
405 prime.push(playerIndices[i]);
406 prime.push(playerIndices[playerIndices.length - 1 - i]);
412 function primeSortAllPlayers()
414 return primeSortPlayers(sortAllPlayers());
417 function radialPlayerPlacement(percentRadius = 0.35)
419 let playerIDs = sortAllPlayers();
423 let playerAngle = [];
425 let startAngle = randFloat(0, TWO_PI);
427 for (let i = 0; i < getNumPlayers(); ++i)
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]);
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.
442 function placePlayersRiver()
445 let numPlayers = getNumPlayers();
446 let numPlayersEven = numPlayers % 2 == 0;
448 for (let i = 0; i < numPlayers; ++i)
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);
461 function getStartingEntities(player)
463 let civ = getCivCode(player);
465 if (!g_CivData[civ] || !g_CivData[civ].StartEntities || !g_CivData[civ].StartEntities.length)
467 warn("Invalid or unimplemented civ '"+civ+"' specified, falling back to '" + FALLBACK_CIV + "'");
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
489 * Add point to given class by id
491 function addToClass(x, z, id)
493 let tileClass = getTileClass(id);
495 if (tileClass !== null)
500 * Remove point from the given class by id
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
513 function paintClass(id)
515 return new TileClassPainter(getTileClass(id));
519 * Create a painter for the given class
521 function unPaintClass(id)
523 return new TileClassUnPainter(getTileClass(id));
527 * Create an avoid constraint for the given classes by the given distances
529 function avoidClasses(/*class1, dist1, class2, dist2, etc*/)
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
539 return new AndConstraint(ar);
543 * Create a stay constraint for the given classes by the given distances
545 function stayClasses(/*class1, dist1, class2, dist2, etc*/)
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
555 return new AndConstraint(ar);
559 * Create a border constraint for the given classes by the given distances
561 function borderClasses(/*class1, idist1, odist1, class2, idist2, odist2, etc*/)
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
571 return new AndConstraint(ar);
575 * Checks if the given tile is in class "id"
577 function checkIfInClass(x, z, id)
579 let tileClass = getTileClass(id);
580 if (tileClass === null)
583 let members = tileClass.countMembersInRadius(x, z, 1);
584 if (members === null)
591 * Returns the angle of the vector between point 1 and point 2.
592 * The angle is counterclockwise from the positive x axis.
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
602 function getGradient(x1, z1, x2, z2)
604 if (x1 == x2 && z1 == z2)
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 }
630 function getOrderOfPointsForShortestClosePath(points)
634 if (points.length <= 3)
636 for (let i = 0; i < points.length; ++i)
642 // Just add the first 3 points
643 let pointsToAdd = clone(points);
644 for (let i = 0; i < 3; ++i)
647 pointsToAdd.shift(i);
649 distances.push(Math.euclidDistance2D(points[order[i]].x, points[order[i]].y, points[order[i - 1]].x, points[order[i - 1]].y));
652 distances.push(Math.euclidDistance2D(
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)
663 let minEnlengthen = Infinity;
666 for (let k = 0; k < order.length; ++k)
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)
674 minEnlengthen = enlengthen;
679 order.splice(indexToAddTo + 1, 0, i + 3);
680 distances.splice(indexToAddTo, 1, minDist1, minDist2);