LCOV - code coverage report
Current view: top level - maps/random/rmgen-common - wall_builder.js (source / functions) Hit Total Coverage
Test: lcov.info Lines: 0 377 0.0 %
Date: 2023-04-02 12:52:40 Functions: 0 22 0.0 %

          Line data    Source code
       1             : /**
       2             :  * @file Contains functionality to place walls on random maps.
       3             :  */
       4             : 
       5             : /**
       6             :  * Provide the bare minimum so we can load templates without error.
       7             :  * We don't actually need to know the actual resource codes.
       8             :  */
       9           0 : const g_Resources = {
      10           0 :         "GetCodes": () => [],
      11             : };
      12             : 
      13             : /**
      14             :  * Set some globals for this module.
      15             :  */
      16           0 : var g_WallStyles = loadWallsetsFromCivData();
      17           0 : var g_FortressTypes = createDefaultFortressTypes();
      18             : 
      19             : /**
      20             :  * Fetches wallsets from {civ}.json files, and then uses them to load
      21             :  * basic wall elements.
      22             :  */
      23             : function loadWallsetsFromCivData()
      24             : {
      25           0 :         let wallsets = {};
      26           0 :         for (let civ in g_CivData)
      27             :         {
      28           0 :                 let civInfo = g_CivData[civ];
      29           0 :                 if (!civInfo.WallSets)
      30           0 :                         continue;
      31             : 
      32           0 :                 for (let path of civInfo.WallSets)
      33             :                 {
      34             :                         // File naming conventions:
      35             :                         // - structures/wallset_{style}
      36             :                         // - structures/{civ}/wallset_{style}
      37           0 :                         let style = basename(path).split("_")[1];
      38           0 :                         if (path.split("/").indexOf(civ) != -1)
      39           0 :                                 style = civ + "/" + style;
      40             : 
      41           0 :                         if (!wallsets[style])
      42           0 :                                 wallsets[style] = loadWallset(Engine.GetTemplate(path), civ);
      43             :                 }
      44             :         }
      45           0 :         return wallsets;
      46             : }
      47             : 
      48             : function loadWallset(wallsetPath, civ)
      49             : {
      50           0 :         let newWallset = { "curves": [] };
      51           0 :         const wallsetData = GetTemplateDataHelper(wallsetPath, null, null, g_Resources).wallSet;
      52             : 
      53           0 :         for (let element in wallsetData.templates)
      54           0 :                 if (element == "curves")
      55           0 :                         for (let filename of wallsetData.templates.curves)
      56           0 :                                 newWallset.curves.push(readyWallElement(filename, civ));
      57             :                 else
      58           0 :                         newWallset[element] = readyWallElement(wallsetData.templates[element], civ);
      59             : 
      60           0 :         newWallset.overlap = wallsetData.minTowerOverlap * newWallset.tower.length;
      61             : 
      62           0 :         return newWallset;
      63             : }
      64             : 
      65             : /**
      66             :  * Fortress class definition
      67             :  *
      68             :  * We use "fortress" to describe a closed wall built of multiple wall
      69             :  * elements attached together surrounding a central point. We store the
      70             :  * abstract of the wall (gate, tower, wall, ...) and only apply the style
      71             :  * when we get to build it.
      72             :  *
      73             :  * @param {string} type - Descriptive string, example: "tiny". Not really needed (WallTool.wallTypes["type string"] is used). Mainly for custom wall elements.
      74             :  * @param {array} [wall] - Array of wall element strings. May be defined at a later point.
      75             :  *                         Example: ["medium", "cornerIn", "gate", "cornerIn", "medium", "cornerIn", "gate", "cornerIn"]
      76             :  * @param {Object} [centerToFirstElement] - Vector from the visual center of the fortress to the first wall element.
      77             :  * @param {number} [centerToFirstElement.x]
      78             :  * @param {number} [centerToFirstElement.y]
      79             :  */
      80             : function Fortress(type, wall=[], centerToFirstElement=undefined)
      81             : {
      82           0 :         this.type = type;
      83           0 :         this.wall = wall;
      84           0 :         this.centerToFirstElement = centerToFirstElement;
      85             : }
      86             : 
      87             : function createDefaultFortressTypes()
      88             : {
      89           0 :         let defaultFortresses = {};
      90             : 
      91             :         /**
      92             :          * Define some basic default fortress types.
      93             :          */
      94           0 :         let addFortress = (type, walls) => defaultFortresses[type] = { "wall": walls.concat(walls, walls, walls) };
      95           0 :         addFortress("tiny", ["gate", "tower", "short", "cornerIn", "short", "tower"]);
      96           0 :         addFortress("small", ["gate", "tower", "medium", "cornerIn", "medium", "tower"]);
      97           0 :         addFortress("medium", ["gate", "tower", "long", "cornerIn", "long", "tower"]);
      98           0 :         addFortress("normal", ["gate", "tower", "medium", "cornerIn", "medium", "cornerOut", "medium", "cornerIn", "medium", "tower"]);
      99           0 :         addFortress("large", ["gate", "tower", "long", "cornerIn", "long", "cornerOut", "long", "cornerIn", "long", "tower"]);
     100           0 :         addFortress("veryLarge", ["gate", "tower", "medium", "cornerIn", "medium", "cornerOut", "long", "cornerIn", "long", "cornerOut", "medium", "cornerIn", "medium", "tower"]);
     101           0 :         addFortress("giant", ["gate", "tower", "long", "cornerIn", "long", "cornerOut", "long", "cornerIn", "long", "cornerOut", "long", "cornerIn", "long", "tower"]);
     102             : 
     103             :         /**
     104             :          * Define some fortresses based on those above, but designed for use
     105             :          * with the "palisades" wallset.
     106             :          */
     107           0 :         for (let fortressType in defaultFortresses)
     108             :         {
     109           0 :                 const fillTowersBetween = ["short", "medium", "long", "start", "end", "cornerIn", "cornerOut"];
     110           0 :                 const newKey = fortressType + "Palisades";
     111           0 :                 const oldWall = defaultFortresses[fortressType].wall;
     112             : 
     113           0 :                 defaultFortresses[newKey] = { "wall": [] };
     114           0 :                 for (let j = 0; j < oldWall.length; ++j)
     115             :                 {
     116           0 :                         defaultFortresses[newKey].wall.push(oldWall[j]);
     117             : 
     118           0 :                         if (j + 1 < oldWall.length &&
     119             :                                 fillTowersBetween.indexOf(oldWall[j]) != -1 &&
     120             :                                 fillTowersBetween.indexOf(oldWall[j + 1]) != -1)
     121             :                         {
     122           0 :                                 defaultFortresses[newKey].wall.push("tower");
     123             :                         }
     124             :                 }
     125             :         }
     126             : 
     127           0 :         return defaultFortresses;
     128             : }
     129             : 
     130             : /**
     131             :  * Define some helper functions
     132             :  */
     133             : 
     134             : /**
     135             :  * Get a wall element of a style.
     136             :  *
     137             :  * Valid elements:
     138             :  *   long, medium, short, start, end, cornerIn, cornerOut, tower, fort, gate, entry, entryTower, entryFort
     139             :  *
     140             :  * Dynamic elements:
     141             :  *   `gap_{x}` returns a non-blocking gap of length `x` meters.
     142             :  *   `turn_{x}` returns a zero-length turn of angle `x` radians.
     143             :  *
     144             :  * Any other arbitrary string passed will be attempted to be used as: `structures/{civ}/{arbitrary_string}`.
     145             :  *
     146             :  * @param {string} element - What sort of element to fetch.
     147             :  * @param {string} [style] - The style from which this element should come from.
     148             :  * @returns {Object} The wall element requested. Or a tower element.
     149             :  */
     150             : function getWallElement(element, style)
     151             : {
     152           0 :         style = validateStyle(style);
     153           0 :         if (g_WallStyles[style][element])
     154           0 :                 return g_WallStyles[style][element];
     155             : 
     156             :         // Attempt to derive any unknown elements.
     157             :         // Defaults to a wall tower piece
     158           0 :         const quarterBend = Math.PI / 2;
     159           0 :         let wallset = g_WallStyles[style];
     160           0 :         let civ = style.split("/")[0];
     161           0 :         let ret = wallset.tower ? clone(wallset.tower) : { "angle": 0, "bend": 0, "length": 0, "indent": 0 };
     162             : 
     163           0 :         switch (element)
     164             :         {
     165             :         case "cornerIn":
     166           0 :                 if (wallset.curves)
     167           0 :                         for (let curve of wallset.curves)
     168           0 :                                 if (curve.bend == quarterBend)
     169           0 :                                         ret = curve;
     170             : 
     171           0 :                 if (ret.bend != quarterBend)
     172             :                 {
     173           0 :                         ret.angle += Math.PI / 4;
     174           0 :                         ret.indent = ret.length / 4;
     175           0 :                         ret.length = 0;
     176           0 :                         ret.bend = Math.PI / 2;
     177             :                 }
     178           0 :                 break;
     179             : 
     180             :         case "cornerOut":
     181           0 :                 if (wallset.curves)
     182           0 :                         for (let curve of wallset.curves)
     183           0 :                                 if (curve.bend == quarterBend)
     184             :                                 {
     185           0 :                                         ret = clone(curve);
     186           0 :                                         ret.angle += Math.PI / 2;
     187           0 :                                         ret.indent -= ret.indent * 2;
     188             :                                 }
     189             : 
     190           0 :                 if (ret.bend != quarterBend)
     191             :                 {
     192           0 :                         ret.angle -= Math.PI / 4;
     193           0 :                         ret.indent = -ret.length / 4;
     194           0 :                         ret.length = 0;
     195             :                 }
     196           0 :                 ret.bend = -Math.PI / 2;
     197           0 :                 break;
     198             : 
     199             :         case "entry":
     200           0 :                 ret.templateName = undefined;
     201           0 :                 ret.length = wallset.gate.length;
     202           0 :                 break;
     203             : 
     204             :         case "entryTower":
     205           0 :                 ret.templateName = g_CivData[civ] ? "structures/" + civ + "/defense_tower" : "structures/palisades_watchtower";
     206           0 :                 ret.indent = ret.length * -3;
     207           0 :                 ret.length = wallset.gate.length;
     208           0 :                 break;
     209             : 
     210             :         case "entryFort":
     211           0 :                 ret = clone(wallset.fort);
     212           0 :                 ret.angle -= Math.PI;
     213           0 :                 ret.length *= 1.5;
     214           0 :                 ret.indent = ret.length;
     215           0 :                 break;
     216             : 
     217             :         case "start":
     218           0 :                 if (wallset.end)
     219             :                 {
     220           0 :                         ret = clone(wallset.end);
     221           0 :                         ret.angle += Math.PI;
     222             :                 }
     223           0 :                 break;
     224             : 
     225             :         case "end":
     226           0 :                 if (wallset.end)
     227           0 :                         ret = wallset.end;
     228           0 :                 break;
     229             : 
     230             :         default:
     231           0 :                 if (element.startsWith("gap_"))
     232             :                 {
     233           0 :                         ret.templateName = undefined;
     234           0 :                         ret.angle = 0;
     235           0 :                         ret.length = +element.slice("gap_".length);
     236             :                 }
     237           0 :                 else if (element.startsWith("turn_"))
     238             :                 {
     239           0 :                         ret.templateName = undefined;
     240           0 :                         ret.bend = +element.slice("turn_".length) * Math.PI;
     241           0 :                         ret.length = 0;
     242             :                 }
     243             :                 else
     244             :                 {
     245           0 :                         if (!g_CivData[civ])
     246           0 :                                 civ = Object.keys(g_CivData)[0];
     247             : 
     248           0 :                         let templateName = "structures/" + civ + "/" + element;
     249           0 :                         if (Engine.TemplateExists(templateName))
     250             :                         {
     251           0 :                                 ret.indent = ret.length * (element == "outpost" || element.endsWith("_tower") ? -3 : 3.5);
     252           0 :                                 ret.templateName = templateName;
     253           0 :                                 ret.length = 0;
     254             :                         }
     255             :                         else
     256           0 :                                 warn("Unrecognised wall element: '" + element + "' (" + style + "). Defaulting to " + (wallset.tower ? "'tower'." : "a blank element."));
     257             :                 }
     258             :         }
     259             : 
     260             :         // Cache to save having to calculate this element again.
     261           0 :         g_WallStyles[style][element] = deepfreeze(ret);
     262             : 
     263           0 :         return ret;
     264             : }
     265             : 
     266             : /**
     267             :  * Prepare a wall element for inclusion in a style.
     268             :  *
     269             :  * @param {string} path - The template path to read values from
     270             :  */
     271             : function readyWallElement(path, civCode)
     272             : {
     273           0 :         path = path.replace(/\{civ\}/g, civCode);
     274           0 :         const template = GetTemplateDataHelper(Engine.GetTemplate(path), null, null, g_Resources);
     275           0 :         let length = template.wallPiece ? template.wallPiece.length : template.obstruction.shape.width;
     276             : 
     277           0 :         return deepfreeze({
     278             :                 "templateName": path,
     279             :                 "angle": template.wallPiece ? template.wallPiece.angle : Math.PI,
     280             :                 "length": length / TERRAIN_TILE_SIZE,
     281             :                 "indent": template.wallPiece ? template.wallPiece.indent / TERRAIN_TILE_SIZE : 0,
     282             :                 "bend": template.wallPiece ? template.wallPiece.bend : 0
     283             :         });
     284             : }
     285             : 
     286             : /**
     287             :  * Returns a list of objects containing all information to place all the wall elements entities with placeObject (but the player ID)
     288             :  * Placing the first wall element at startX/startY placed with an angle given by orientation
     289             :  * An alignment can be used to get the "center" of a "wall" (more likely used for fortresses) with getCenterToFirstElement
     290             :  *
     291             :  * @param {Vector2D} position
     292             :  * @param {array} [wall]
     293             :  * @param {string} [style]
     294             :  * @param {number} [orientation]
     295             :  * @returns {array}
     296             :  */
     297             : function getWallAlignment(position, wall = [], style = "athen_stone", orientation = 0)
     298             : {
     299           0 :         style = validateStyle(style);
     300           0 :         let alignment = [];
     301           0 :         let wallPosition = position.clone();
     302             : 
     303           0 :         for (let i = 0; i < wall.length; ++i)
     304             :         {
     305           0 :                 let element = getWallElement(wall[i], style);
     306           0 :                 if (!element && i == 0)
     307             :                 {
     308           0 :                         warn("Not a valid wall element: style = " + style + ", wall[" + i + "] = " + wall[i] + "; " + uneval(element));
     309           0 :                         continue;
     310             :                 }
     311             : 
     312             :                 // Add wall elements entity placement arguments to the alignment
     313           0 :                 alignment.push({
     314             :                         "position": Vector2D.sub(wallPosition, new Vector2D(element.indent, 0).rotate(-orientation)),
     315             :                         "templateName": element.templateName,
     316             :                         "angle": orientation + element.angle
     317             :                 });
     318             : 
     319             :                 // Preset vars for the next wall element
     320           0 :                 if (i + 1 < wall.length)
     321             :                 {
     322           0 :                         orientation += element.bend;
     323           0 :                         let nextElement = getWallElement(wall[i + 1], style);
     324           0 :                         if (!nextElement)
     325             :                         {
     326           0 :                                 warn("Not a valid wall element: style = " + style + ", wall[" + (i + 1) + "] = " + wall[i + 1] + "; " + uneval(nextElement));
     327           0 :                                 continue;
     328             :                         }
     329             : 
     330           0 :                         let distance = (element.length + nextElement.length) / 2 - g_WallStyles[style].overlap;
     331             : 
     332             :                         // Corrections for elements with indent AND bending
     333           0 :                         let indent = element.indent;
     334           0 :                         let bend = element.bend;
     335           0 :                         if (bend != 0 && indent != 0)
     336             :                         {
     337             :                                 // Indent correction to adjust distance
     338           0 :                                 distance += indent * Math.sin(bend);
     339             : 
     340             :                                 // Indent correction to normalize indentation
     341           0 :                                 wallPosition.add(new Vector2D(indent).rotate(-orientation));
     342             :                         }
     343             : 
     344             :                         // Set the next coordinates of the next element in the wall without indentation adjustment
     345           0 :                         wallPosition.add(new Vector2D(distance, 0).rotate(-orientation).perpendicular());
     346             :                 }
     347             :         }
     348           0 :         return alignment;
     349             : }
     350             : 
     351             : /**
     352             :  * Center calculation works like getting the center of mass assuming all wall elements have the same "weight"
     353             :  *
     354             :  * Used to get centerToFirstElement of fortresses by default
     355             :  *
     356             :  * @param {number} alignment
     357             :  * @returns {Object} Vector from the center of the set of aligned wallpieces to the first wall element.
     358             :  */
     359             : function getCenterToFirstElement(alignment)
     360             : {
     361           0 :         return alignment.reduce((result, align) => result.sub(Vector2D.div(align.position, alignment.length)), new Vector2D(0, 0));
     362             : }
     363             : 
     364             : /**
     365             :  * Does not support bending wall elements like corners.
     366             :  *
     367             :  * @param {string} style
     368             :  * @param {array} wall
     369             :  * @returns {number} The sum length (in terrain cells, not meters) of the provided wall.
     370             :  */
     371             : function getWallLength(style, wall)
     372             : {
     373           0 :         style = validateStyle(style);
     374             : 
     375           0 :         let length = 0;
     376           0 :         let overlap = g_WallStyles[style].overlap;
     377           0 :         for (let element of wall)
     378           0 :                 length += getWallElement(element, style).length - overlap;
     379             : 
     380           0 :         return length;
     381             : }
     382             : 
     383             : /**
     384             :  * Makes sure the style exists and, if not, provides a fallback.
     385             :  *
     386             :  * @param {string} style
     387             :  * @param {number} [playerId]
     388             :  * @returns {string} Valid style.
     389             :  */
     390             : function validateStyle(style, playerId = 0)
     391             : {
     392           0 :         if (!style || !g_WallStyles[style])
     393             :         {
     394           0 :                 if (playerId == 0)
     395           0 :                         return Object.keys(g_WallStyles)[0];
     396             : 
     397           0 :                 style = getCivCode(playerId) + "/stone";
     398           0 :                 return !g_WallStyles[style] ? Object.keys(g_WallStyles)[0] : style;
     399             :         }
     400           0 :         return style;
     401             : }
     402             : 
     403             : /**
     404             :  * Define the different wall placer functions
     405             :  */
     406             : 
     407             : /**
     408             :  * Places an abitrary wall beginning at the location comprised of the array of elements provided.
     409             :  *
     410             :  * @param {Vector2D} position
     411             :  * @param {array} [wall] - Array of wall element types. Example: ["start", "long", "tower", "long", "end"]
     412             :  * @param {string} [style] - Wall style string.
     413             :  * @param {number} [playerId] - Identifier of the player for whom the wall will be placed.
     414             :  * @param {number} [orientation] - Angle at which the first wall element is placed.
     415             :  *                                 0 means "outside" or "front" of the wall is right (positive X) like placeObject
     416             :  *                                 It will then be build towards top/positive Y (if no bending wall elements like corners are used)
     417             :  *                                 Raising orientation means the wall is rotated counter-clockwise like placeObject
     418             :  */
     419             : function placeWall(position, wall = [], style, playerId = 0, orientation = 0, constraints = undefined)
     420             : {
     421           0 :         style = validateStyle(style, playerId);
     422             : 
     423           0 :         let entities = [];
     424           0 :         let constraint = new StaticConstraint(constraints);
     425             : 
     426           0 :         for (let align of getWallAlignment(position, wall, style, orientation))
     427           0 :                 if (align.templateName && g_Map.inMapBounds(align.position) && constraint.allows(align.position.clone().floor()))
     428           0 :                         entities.push(g_Map.placeEntityPassable(align.templateName, playerId, align.position, align.angle));
     429             : 
     430           0 :         return entities;
     431             : }
     432             : 
     433             : /**
     434             :  * Places an abitrarily designed "fortress" (closed loop of wall elements)
     435             :  * centered around a given point.
     436             :  *
     437             :  * The fortress wall should always start with the main entrance (like
     438             :  * "entry" or "gate") to get the orientation correct.
     439             :  *
     440             :  * @param {Vector2D} centerPosition
     441             :  * @param {Object} [fortress] - If not provided, defaults to the predefined "medium" fortress type.
     442             :  * @param {string} [style] - Wall style string.
     443             :  * @param {number} [playerId] - Identifier of the player for whom the wall will be placed.
     444             :  * @param {number} [orientation] - Angle the first wall element (should be a gate or entrance) is placed. Default is 0
     445             :  */
     446             : function placeCustomFortress(centerPosition, fortress, style, playerId = 0, orientation = 0, constraints = undefined)
     447             : {
     448           0 :         fortress = fortress || g_FortressTypes.medium;
     449           0 :         style = validateStyle(style, playerId);
     450             : 
     451             :         // Calculate center if fortress.centerToFirstElement is undefined (default)
     452           0 :         let centerToFirstElement = fortress.centerToFirstElement;
     453           0 :         if (centerToFirstElement === undefined)
     454           0 :                 centerToFirstElement = getCenterToFirstElement(getWallAlignment(new Vector2D(0, 0), fortress.wall, style));
     455             : 
     456             :         // Placing the fortress wall
     457           0 :         let position = Vector2D.sum([
     458             :                 centerPosition,
     459             :                 new Vector2D(centerToFirstElement.x, 0).rotate(-orientation),
     460             :                 new Vector2D(centerToFirstElement.y, 0).perpendicular().rotate(-orientation)
     461             :         ]);
     462             : 
     463           0 :         return placeWall(position, fortress.wall, style, playerId, orientation, constraints);
     464             : }
     465             : 
     466             : /**
     467             :  * Places a predefined fortress centered around the provided point.
     468             :  *
     469             :  * @see Fortress
     470             :  *
     471             :  * @param {string} [type] - Predefined fortress type, as used as a key in g_FortressTypes.
     472             :  */
     473             : function placeFortress(centerPosition, type = "medium", style, playerId = 0, orientation = 0, constraints = undefined)
     474             : {
     475           0 :         return placeCustomFortress(centerPosition, g_FortressTypes[type], style, playerId, orientation, constraints);
     476             : }
     477             : 
     478             : /**
     479             :  * Places a straight wall from a given point to another, using the provided
     480             :  * wall parts repeatedly.
     481             :  *
     482             :  * Note: Any "bending" wall pieces passed will be complained about.
     483             :  *
     484             :  * @param {Vector2D} startPosition - Approximate start point of the wall.
     485             :  * @param {Vector2D} targetPosition - Approximate end point of the wall.
     486             :  * @param {array} [wallPart=["tower", "long"]]
     487             :  * @param {number} [playerId]
     488             :  * @param {boolean} [endWithFirst] - If true, the first wall element will also be the last.
     489             :  */
     490             : function placeLinearWall(startPosition, targetPosition, wallPart = undefined, style, playerId = 0, endWithFirst = true, constraints = undefined)
     491             : {
     492           0 :         wallPart = wallPart || ["tower", "long"];
     493           0 :         style = validateStyle(style, playerId);
     494             : 
     495             :         // Check arguments
     496           0 :         for (let element of wallPart)
     497           0 :                 if (getWallElement(element, style).bend != 0)
     498           0 :                         warn("placeLinearWall : Bending is not supported by this function, but the following bending wall element was used: " + element);
     499             : 
     500             :         // Setup number of wall parts
     501           0 :         let totalLength = startPosition.distanceTo(targetPosition);
     502           0 :         let wallPartLength = getWallLength(style, wallPart);
     503           0 :         let numParts = Math.ceil(totalLength / wallPartLength);
     504           0 :         if (endWithFirst)
     505           0 :                 numParts = Math.ceil((totalLength - getWallElement(wallPart[0], style).length) / wallPartLength);
     506             : 
     507             :         // Setup scale factor
     508           0 :         let scaleFactor = totalLength / (numParts * wallPartLength);
     509           0 :         if (endWithFirst)
     510           0 :                 scaleFactor = totalLength / (numParts * wallPartLength + getWallElement(wallPart[0], style).length);
     511             : 
     512             :         // Setup angle
     513           0 :         let wallAngle = getAngle(startPosition.x, startPosition.y, targetPosition.x, targetPosition.y);
     514           0 :         let placeAngle = wallAngle - Math.PI / 2;
     515             : 
     516             :         // Place wall entities
     517           0 :         let entities = [];
     518           0 :         let position = startPosition.clone();
     519           0 :         let overlap = g_WallStyles[style].overlap;
     520           0 :         let constraint = new StaticConstraint(constraints);
     521           0 :         for (let partIndex = 0; partIndex < numParts; ++partIndex)
     522           0 :                 for (let elementIndex = 0; elementIndex < wallPart.length; ++elementIndex)
     523             :                 {
     524           0 :                         let wallEle = getWallElement(wallPart[elementIndex], style);
     525             : 
     526           0 :                         let wallLength = (wallEle.length - overlap) / 2;
     527           0 :                         let dist = new Vector2D(scaleFactor * wallLength, 0).rotate(-wallAngle);
     528             : 
     529             :                         // Length correction
     530           0 :                         position.add(dist);
     531             : 
     532             :                         // Indent correction
     533           0 :                         let place = Vector2D.add(position, new Vector2D(0, wallEle.indent).rotate(-wallAngle));
     534             : 
     535           0 :                         if (wallEle.templateName && g_Map.inMapBounds(place) && constraint.allows(place.clone().floor()))
     536           0 :                                 entities.push(g_Map.placeEntityPassable(wallEle.templateName, playerId, place, placeAngle + wallEle.angle));
     537             : 
     538           0 :                         position.add(dist);
     539             :                 }
     540             : 
     541           0 :         if (endWithFirst)
     542             :         {
     543           0 :                 let wallEle = getWallElement(wallPart[0], style);
     544           0 :                 let wallLength = (wallEle.length - overlap) / 2;
     545           0 :                 position.add(new Vector2D(scaleFactor * wallLength, 0).rotate(-wallAngle));
     546           0 :                 if (wallEle.templateName && g_Map.inMapBounds(position) && constraint.allows(position.clone().floor()))
     547           0 :                         entities.push(g_Map.placeEntityPassable(wallEle.templateName, playerId, position, placeAngle + wallEle.angle));
     548             :         }
     549             : 
     550           0 :         return entities;
     551             : }
     552             : 
     553             : /**
     554             :  * Places a (semi-)circular wall of repeated wall elements around a central
     555             :  * point at a given radius.
     556             :  *
     557             :  * The wall does not have to be closed, and can be left open in the form
     558             :  * of an arc if maxAngle < 2 * Pi. In this case, the orientation determines
     559             :  * where this open part faces, with 0 meaning "right" like an unrotated
     560             :  * building's drop-point.
     561             :  *
     562             :  * Note: Any "bending" wall pieces passed will be complained about.
     563             :  *
     564             :  * @param {Vector2D} center - Center of the circle or arc.
     565             :  * @param (number} radius - Approximate radius of the circle. (Given the maxBendOff argument)
     566             :  * @param {array} [wallPart]
     567             :  * @param {string} [style]
     568             :  * @param {number} [playerId]
     569             :  * @param {number} [orientation] - Angle at which the first wall element is placed.
     570             :  * @param {number} [maxAngle] - How far the wall should circumscribe the center. Default is Pi * 2 (for a full circle).
     571             :  * @param {boolean} [endWithFirst] - If true, the first wall element will also be the last. For full circles, the default is false. For arcs, true.
     572             :  * @param {number} [maxBendOff]    Optional. How irregular the circle should be. 0 means regular circle, PI/2 means very irregular. Default is 0 (regular circle)
     573             :  */
     574             : function placeCircularWall(center, radius, wallPart, style, playerId = 0, orientation = 0, maxAngle = 2 * Math.PI, endWithFirst, maxBendOff = 0, constraints = undefined)
     575             : {
     576           0 :         wallPart = wallPart || ["tower", "long"];
     577           0 :         style = validateStyle(style, playerId);
     578             : 
     579           0 :         if (endWithFirst === undefined)
     580           0 :                 endWithFirst = maxAngle < Math.PI * 2 - 0.001; // Can this be done better?
     581             : 
     582             :         // Check arguments
     583           0 :         if (maxBendOff > Math.PI / 2 || maxBendOff < 0)
     584           0 :                 warn("placeCircularWall : maxBendOff should satisfy 0 < maxBendOff < PI/2 (~1.5rad) but it is: " + maxBendOff);
     585             : 
     586           0 :         for (let element of wallPart)
     587           0 :                 if (getWallElement(element, style).bend != 0)
     588           0 :                         warn("placeCircularWall : Bending is not supported by this function, but the following bending wall element was used: " + element);
     589             : 
     590             :         // Setup number of wall parts
     591           0 :         let totalLength = maxAngle * radius;
     592           0 :         let wallPartLength = getWallLength(style, wallPart);
     593           0 :         let numParts = Math.ceil(totalLength / wallPartLength);
     594           0 :         if (endWithFirst)
     595           0 :                 numParts = Math.ceil((totalLength - getWallElement(wallPart[0], style).length) / wallPartLength);
     596             : 
     597             :         // Setup scale factor
     598           0 :         let scaleFactor = totalLength / (numParts * wallPartLength);
     599           0 :         if (endWithFirst)
     600           0 :                 scaleFactor = totalLength / (numParts * wallPartLength + getWallElement(wallPart[0], style).length);
     601             : 
     602             :         // Place wall entities
     603           0 :         let entities = [];
     604           0 :         let constraint = new StaticConstraint(constraints);
     605           0 :         let actualAngle = orientation;
     606           0 :         let position = Vector2D.add(center, new Vector2D(radius, 0).rotate(-actualAngle));
     607           0 :         let overlap = g_WallStyles[style].overlap;
     608           0 :         for (let partIndex = 0; partIndex < numParts; ++partIndex)
     609           0 :                 for (let wallEle of wallPart)
     610             :                 {
     611           0 :                         wallEle = getWallElement(wallEle, style);
     612             : 
     613             :                         // Width correction
     614           0 :                         let addAngle = scaleFactor * (wallEle.length - overlap) / radius;
     615           0 :                         let target = Vector2D.add(center, new Vector2D(radius, 0).rotate(-actualAngle - addAngle));
     616           0 :                         let place = Vector2D.average([position, target]);
     617           0 :                         let placeAngle = actualAngle + addAngle / 2;
     618             : 
     619             :                         // Indent correction
     620           0 :                         place.sub(new Vector2D(wallEle.indent, 0).rotate(-placeAngle));
     621             : 
     622             :                         // Placement
     623           0 :                         if (wallEle.templateName && g_Map.inMapBounds(place) && constraint.allows(place.clone().floor()))
     624             :                         {
     625           0 :                                 let entity = g_Map.placeEntityPassable(wallEle.templateName, playerId, place, placeAngle + wallEle.angle);
     626           0 :                                 if (entity)
     627           0 :                                         entities.push(entity);
     628             :                         }
     629             : 
     630             :                         // Prepare for the next wall element
     631           0 :                         actualAngle += addAngle;
     632           0 :                         position = Vector2D.add(center, new Vector2D(radius, 0).rotate(-actualAngle));
     633             :                 }
     634             : 
     635           0 :         if (endWithFirst)
     636             :         {
     637           0 :                 let wallEle = getWallElement(wallPart[0], style);
     638           0 :                 let addAngle = scaleFactor * wallEle.length / radius;
     639           0 :                 let target = Vector2D.add(center, new Vector2D(radius, 0).rotate(-actualAngle - addAngle));
     640           0 :                 let place = Vector2D.average([position, target]);
     641           0 :                 let placeAngle = actualAngle + addAngle / 2;
     642           0 :                 if (g_Map.inMapBounds(place) && constraint.allows(place.clone().floor()))
     643           0 :                         entities.push(g_Map.placeEntityPassable(wallEle.templateName, playerId, place, placeAngle + wallEle.angle));
     644             :         }
     645             : 
     646           0 :         return entities;
     647             : }
     648             : 
     649             : /**
     650             :  * Places a polygonal wall of repeated wall elements around a central
     651             :  * point at a given radius.
     652             :  *
     653             :  * Note: Any "bending" wall pieces passed will be ignored.
     654             :  *
     655             :  * @param {Vector2D} centerPosition
     656             :  * @param {number} radius
     657             :  * @param {array} [wallPart]
     658             :  * @param {string} [cornerWallElement] - Wall element to be placed at the polygon's corners.
     659             :  * @param {string} [style]
     660             :  * @param {number} [playerId]
     661             :  * @param {number} [orientation] - Direction the first wall piece or opening in the wall faces.
     662             :  * @param {number} [numCorners] - How many corners the polygon will have.
     663             :  * @param {boolean} [skipFirstWall] - If the first linear wall part will be left opened as entrance.
     664             :  */
     665             : function placePolygonalWall(centerPosition, radius, wallPart, cornerWallElement = "tower", style, playerId = 0, orientation = 0, numCorners = 8, skipFirstWall = true, constraints = undefined)
     666             : {
     667           0 :         wallPart = wallPart || ["long", "tower"];
     668           0 :         style = validateStyle(style, playerId);
     669             : 
     670           0 :         let entities = [];
     671           0 :         let constraint = new StaticConstraint(constraints);
     672           0 :         let angleAdd = Math.PI * 2 / numCorners;
     673           0 :         let angleStart = orientation - angleAdd / 2;
     674           0 :         let corners = new Array(numCorners).fill(0).map((zero, i) =>
     675           0 :                 Vector2D.add(centerPosition, new Vector2D(radius, 0).rotate(-angleStart - i * angleAdd)));
     676             : 
     677           0 :         for (let i = 0; i < numCorners; ++i)
     678             :         {
     679           0 :                 let angleToCorner = getAngle(corners[i].x, corners[i].y, centerPosition.x, centerPosition.y);
     680           0 :                 if (g_Map.inMapBounds(corners[i]) && constraint.allows(corners[i].clone().floor()))
     681             :                 {
     682           0 :                         let entity = g_Map.placeEntityPassable(getWallElement(cornerWallElement, style).templateName, playerId, corners[i], angleToCorner);
     683           0 :                         if (entity)
     684           0 :                                 entities.push(entity);
     685             :                 }
     686             : 
     687           0 :                 if (!skipFirstWall || i != 0)
     688             :                 {
     689           0 :                         let cornerLength = getWallElement(cornerWallElement, style).length / 2;
     690           0 :                         let cornerAngle = angleToCorner + angleAdd / 2;
     691           0 :                         let targetCorner = (i + 1) % numCorners;
     692           0 :                         let cornerPosition = new Vector2D(cornerLength, 0).rotate(-cornerAngle).perpendicular();
     693             : 
     694           0 :                         entities = entities.concat(
     695             :                                 placeLinearWall(
     696             :                                         // Adjustment to the corner element width (approximately)
     697             :                                         Vector2D.sub(corners[i], cornerPosition),
     698             :                                         Vector2D.add(corners[targetCorner], cornerPosition),
     699             :                                         wallPart,
     700             :                                         style,
     701             :                                         playerId,
     702             :                                         undefined,
     703             :                                         constraints));
     704             :                 }
     705             :         }
     706             : 
     707           0 :         return entities;
     708             : }
     709             : 
     710             : /**
     711             :  * Places an irregular polygonal wall consisting of parts semi-randomly
     712             :  * chosen from a provided assortment, built around a central point at a
     713             :  * given radius.
     714             :  *
     715             :  * Note: Any "bending" wall pieces passed will be ... I'm not sure. TODO: test what happens!
     716             :  *
     717             :  * Note: The wallPartsAssortment is last because it's the hardest to set.
     718             :  *
     719             :  * @param {Vector2D} centerPosition
     720             :  * @param {number} radius
     721             :  * @param {string} [cornerWallElement] - Wall element to be placed at the polygon's corners.
     722             :  * @param {string} [style]
     723             :  * @param {number} [playerId]
     724             :  * @param {number} [orientation] - Direction the first wallpiece or opening in the wall faces.
     725             :  * @param {number} [numCorners] - How many corners the polygon will have.
     726             :  * @param {number} [irregularity] - How irregular the polygon will be. 0 = regular, 1 = VERY irregular.
     727             :  * @param {boolean} [skipFirstWall] - If true, the first linear wall part will be left open as an entrance.
     728             :  * @param {array} [wallPartsAssortment] - An array of wall part arrays to choose from for each linear wall connecting the corners.
     729             :  */
     730             : function placeIrregularPolygonalWall(centerPosition, radius, cornerWallElement = "tower", style, playerId = 0, orientation = 0, numCorners, irregularity = 0.5, skipFirstWall = false, wallPartsAssortment = undefined, constraints = undefined)
     731             : {
     732           0 :         style = validateStyle(style, playerId);
     733           0 :         numCorners = numCorners || randIntInclusive(5, 7);
     734             : 
     735             :         // Generating a generic wall part assortment with each wall part including 1 gate lengthened by walls and towers
     736             :         // NOTE: It might be a good idea to write an own function for that...
     737           0 :         let defaultWallPartsAssortment = [["short"], ["medium"], ["long"], ["gate", "tower", "short"]];
     738           0 :         let centeredWallPart = ["gate"];
     739           0 :         let extendingWallPartAssortment = [["tower", "long"], ["tower", "medium"]];
     740           0 :         defaultWallPartsAssortment.push(centeredWallPart);
     741           0 :         for (let assortment of extendingWallPartAssortment)
     742             :         {
     743           0 :                 let wallPart = centeredWallPart;
     744           0 :                 for (let j = 0; j < radius; ++j)
     745             :                 {
     746           0 :                         if (j % 2 == 0)
     747           0 :                                 wallPart = wallPart.concat(assortment);
     748             :                         else
     749             :                         {
     750           0 :                                 assortment.reverse();
     751           0 :                                 wallPart = assortment.concat(wallPart);
     752           0 :                                 assortment.reverse();
     753             :                         }
     754           0 :                         defaultWallPartsAssortment.push(wallPart);
     755             :                 }
     756             :         }
     757             : 
     758             :         // Setup optional arguments to the default
     759           0 :         wallPartsAssortment = wallPartsAssortment || defaultWallPartsAssortment;
     760             : 
     761             :         // Setup angles
     762           0 :         let angleToCover = Math.PI * 2;
     763           0 :         let angleAddList = [];
     764           0 :         for (let i = 0; i < numCorners; ++i)
     765             :         {
     766             :                 // Randomize covered angles. Variety scales down with raising angle though...
     767           0 :                 angleAddList.push(angleToCover / (numCorners - i) * (1 + randFloat(-irregularity, irregularity)));
     768           0 :                 angleToCover -= angleAddList[angleAddList.length - 1];
     769             :         }
     770             : 
     771             :         // Setup corners
     772           0 :         let corners = [];
     773           0 :         let angleActual = orientation - angleAddList[0] / 2;
     774           0 :         for (let i = 0; i < numCorners; ++i)
     775             :         {
     776           0 :                 corners.push(Vector2D.add(centerPosition, new Vector2D(radius, 0).rotate(-angleActual)));
     777             : 
     778           0 :                 if (i < numCorners - 1)
     779           0 :                         angleActual += angleAddList[i + 1];
     780             :         }
     781             : 
     782             :         // Setup best wall parts for the different walls (a bit confusing naming...)
     783           0 :         let wallPartLengths = [];
     784           0 :         let maxWallPartLength = 0;
     785           0 :         for (let wallPart of wallPartsAssortment)
     786             :         {
     787           0 :                 let length = getWallLength(style, wallPart);
     788           0 :                 wallPartLengths.push(length);
     789           0 :                 if (length > maxWallPartLength)
     790           0 :                         maxWallPartLength = length;
     791             :         }
     792             : 
     793           0 :         let wallPartList = []; // This is the list of the wall parts to use for the walls between the corners, not to confuse with wallPartsAssortment!
     794           0 :         for (let i = 0; i < numCorners; ++i)
     795             :         {
     796           0 :                 let bestWallPart = []; // This is a simple wall part not a wallPartsAssortment!
     797           0 :                 let bestWallLength = Infinity;
     798           0 :                 let targetCorner = (i + 1) % numCorners;
     799             :                 // NOTE: This is not quite the length the wall will be in the end. Has to be tweaked...
     800           0 :                 let wallLength = corners[i].distanceTo(corners[targetCorner]);
     801           0 :                 let numWallParts = Math.ceil(wallLength / maxWallPartLength);
     802           0 :                 for (let partIndex = 0; partIndex < wallPartsAssortment.length; ++partIndex)
     803             :                 {
     804           0 :                         let linearWallLength = numWallParts * wallPartLengths[partIndex];
     805           0 :                         if (linearWallLength < bestWallLength && linearWallLength > wallLength)
     806             :                         {
     807           0 :                                 bestWallPart = wallPartsAssortment[partIndex];
     808           0 :                                 bestWallLength = linearWallLength;
     809             :                         }
     810             :                 }
     811           0 :                 wallPartList.push(bestWallPart);
     812             :         }
     813             : 
     814             :         // Place Corners and walls
     815           0 :         let entities = [];
     816           0 :         let constraint = new StaticConstraint(constraints);
     817           0 :         for (let i = 0; i < numCorners; ++i)
     818             :         {
     819           0 :                 let angleToCorner = getAngle(corners[i].x, corners[i].y, centerPosition.x, centerPosition.y);
     820           0 :                 if (g_Map.inMapBounds(corners[i]) && constraint.allows(corners[i].clone().floor()))
     821           0 :                         entities.push(
     822             :                                 g_Map.placeEntityPassable(getWallElement(cornerWallElement, style).templateName, playerId, corners[i], angleToCorner));
     823             : 
     824           0 :                 if (!skipFirstWall || i != 0)
     825             :                 {
     826           0 :                         let cornerLength = getWallElement(cornerWallElement, style).length / 2;
     827           0 :                         let targetCorner = (i + 1) % numCorners;
     828           0 :                         let startAngle = angleToCorner + angleAddList[i] / 2;
     829           0 :                         let targetAngle = angleToCorner + angleAddList[targetCorner] / 2;
     830             : 
     831           0 :                         entities = entities.concat(
     832             :                                 placeLinearWall(
     833             :                                         // Adjustment to the corner element width (approximately)
     834             :                                         Vector2D.sub(corners[i], new Vector2D(cornerLength, 0).perpendicular().rotate(-startAngle)),
     835             :                                         Vector2D.add(corners[targetCorner], new Vector2D(cornerLength, 0).rotate(-targetAngle - Math.PI / 2)),
     836             :                                         wallPartList[i],
     837             :                                         style,
     838             :                                         playerId,
     839             :                                         false,
     840             :                                         constraints));
     841             :                 }
     842             :         }
     843           0 :         return entities;
     844             : }
     845             : 
     846             : /**
     847             :  * Places a generic fortress with towers at the edges connected with long
     848             :  * walls and gates, positioned around a central point at a given radius.
     849             :  *
     850             :  * The difference between this and the other two Fortress placement functions
     851             :  * is that those place a predefined fortress, regardless of terrain type.
     852             :  * This function attempts to intelligently place a wall circuit around
     853             :  * the central point taking into account terrain and other obstacles.
     854             :  *
     855             :  * This is the default Iberian civ bonus starting wall.
     856             :  *
     857             :  * @param {Vector2D} center - The approximate center coordinates of the fortress
     858             :  * @param {number} [radius] - The approximate radius of the wall to be placed.
     859             :  * @param {number} [playerId]
     860             :  * @param {string} [style]
     861             :  * @param {number} [irregularity] - 0 = circle, 1 = very spiky
     862             :  * @param {number} [gateOccurence] - Integer number, every n-th walls will be a gate instead.
     863             :  * @param {number} [maxTries] - How often the function tries to find a better fitting shape.
     864             :  */
     865             : function placeGenericFortress(center, radius = 20, playerId = 0, style, irregularity = 0.5, gateOccurence = 3, maxTries = 100, constraints = undefined)
     866             : {
     867           0 :         style = validateStyle(style, playerId);
     868             : 
     869             :         // Setup some vars
     870           0 :         let startAngle = randomAngle();
     871           0 :         let actualOff = new Vector2D(radius, 0).rotate(-startAngle);
     872           0 :         let actualAngle = startAngle;
     873           0 :         let pointDistance = getWallLength(style, ["long", "tower"]);
     874             : 
     875             :         // Searching for a well fitting point derivation
     876           0 :         let tries = 0;
     877             :         let bestPointDerivation;
     878           0 :         let minOverlap = 1000;
     879             :         let overlap;
     880           0 :         while (tries < maxTries && minOverlap > g_WallStyles[style].overlap)
     881             :         {
     882           0 :                 let pointDerivation = [];
     883           0 :                 let distanceToTarget = 1000;
     884           0 :                 while (true)
     885             :                 {
     886           0 :                         let indent = randFloat(-irregularity * pointDistance, irregularity * pointDistance);
     887           0 :                         let tmp = new Vector2D(radius + indent, 0).rotate(-actualAngle - pointDistance / radius);
     888           0 :                         let tmpAngle = getAngle(actualOff.x, actualOff.y, tmp.x, tmp.y);
     889             : 
     890           0 :                         actualOff.add(new Vector2D(pointDistance, 0).rotate(-tmpAngle));
     891           0 :                         actualAngle = getAngle(0, 0, actualOff.x, actualOff.y);
     892           0 :                         pointDerivation.push(actualOff.clone());
     893           0 :                         distanceToTarget = pointDerivation[0].distanceTo(actualOff);
     894             : 
     895           0 :                         let numPoints = pointDerivation.length;
     896           0 :                         if (numPoints > 3 && distanceToTarget < pointDistance) // Could be done better...
     897             :                         {
     898           0 :                                 overlap = pointDistance - pointDerivation[numPoints - 1].distanceTo(pointDerivation[0]);
     899           0 :                                 if (overlap < minOverlap)
     900             :                                 {
     901           0 :                                         minOverlap = overlap;
     902           0 :                                         bestPointDerivation = pointDerivation;
     903             :                                 }
     904           0 :                                 break;
     905             :                         }
     906             :                 }
     907           0 :                 ++tries;
     908             :         }
     909             : 
     910           0 :         log("placeGenericFortress: Reduced overlap to " + minOverlap + " after " + tries + " tries");
     911             : 
     912             :         // Place wall
     913           0 :         let entities = [];
     914           0 :         let constraint = new StaticConstraint(constraints);
     915           0 :         for (let pointIndex = 0; pointIndex < bestPointDerivation.length; ++pointIndex)
     916             :         {
     917           0 :                 let start = Vector2D.add(center, bestPointDerivation[pointIndex]);
     918           0 :                 let target = Vector2D.add(center, bestPointDerivation[(pointIndex + 1) % bestPointDerivation.length]);
     919           0 :                 let angle = getAngle(start.x, start.y, target.x, target.y);
     920             : 
     921           0 :                 let element = (pointIndex + 1) % gateOccurence == 0 ? "gate" : "long";
     922           0 :                 element = getWallElement(element, style);
     923             : 
     924           0 :                 if (element.templateName)
     925             :                 {
     926           0 :                         let pos = Vector2D.add(start, new Vector2D(start.distanceTo(target) / 2, 0).rotate(-angle));
     927           0 :                         if (g_Map.inMapBounds(pos) && constraint.allows(pos.clone().floor()))
     928           0 :                                 entities.push(g_Map.placeEntityPassable(element.templateName, playerId, pos, angle - Math.PI / 2 + element.angle));
     929             :                 }
     930             : 
     931             :                 // Place tower
     932           0 :                 start = Vector2D.add(center, bestPointDerivation[(pointIndex + bestPointDerivation.length - 1) % bestPointDerivation.length]);
     933           0 :                 angle = getAngle(start.x, start.y, target.x, target.y);
     934             : 
     935           0 :                 let tower = getWallElement("tower", style);
     936           0 :                 let pos = Vector2D.add(center, bestPointDerivation[pointIndex]);
     937           0 :                 if (g_Map.inMapBounds(pos) && constraint.allows(pos.clone().floor()))
     938           0 :                         entities.push(
     939             :                                 g_Map.placeEntityPassable(tower.templateName, playerId, pos, angle - Math.PI / 2 + tower.angle));
     940             :         }
     941             : 
     942           0 :         return entities;
     943             : }

Generated by: LCOV version 1.14