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