2 * @file Contains functionality to place walls on random maps.
6 * Set some globals for this module.
8 var g_WallStyles = loadWallsetsFromCivData();
9 var g_FortressTypes = createDefaultFortressTypes();
12 * Fetches wallsets from {civ}.json files, and then uses them to load
13 * basic wall elements.
15 function loadWallsetsFromCivData()
18 for (let civ in g_CivData)
20 let civInfo = g_CivData[civ];
21 if (!civInfo.WallSets)
24 for (let path of civInfo.WallSets)
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];
33 wallsets[style] = loadWallset(Engine.GetTemplate(path), civ);
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));
49 newWallset[element] = readyWallElement(wallsetData.templates[element], civ);
51 newWallset.overlap = wallsetData.minTowerOverlap * newWallset.tower.length;
57 * Fortress class definition
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.
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]
71 function Fortress(type, wall=[], centerToFirstElement=undefined)
75 this.centerToFirstElement = centerToFirstElement;
78 function createDefaultFortressTypes()
80 let defaultFortresses = {};
83 * Define some basic default fortress types.
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"]);
95 * Define some fortresses based on those above, but designed for use
96 * with the "palisades" wallset.
98 for (let fortressType in defaultFortresses)
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)
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)
113 defaultFortresses[newKey].wall.push("tower");
118 return defaultFortresses;
122 * Define some helper functions
126 * Get a wall element of a style.
129 * long, medium, short, start, end, cornerIn, cornerOut, tower, fort, gate, entry, entryTower, entryFort
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.
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 };
158 for (let curve of wallset.curves)
159 if (curve.bend == quarterBend)
162 if (ret.bend != quarterBend)
164 ret.angle += Math.PI / 4;
165 ret.indent = ret.length / 4;
167 ret.bend = Math.PI / 2;
173 for (let curve of wallset.curves)
174 if (curve.bend == quarterBend)
177 ret.angle += Math.PI / 2;
178 ret.indent -= ret.indent * 2;
181 if (ret.bend != quarterBend)
183 ret.angle -= Math.PI / 4;
184 ret.indent = -ret.length / 4;
187 ret.bend = -Math.PI / 2;
191 ret.templateName = undefined;
192 ret.length = wallset.gate.length;
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;
202 ret = clone(wallset.fort);
203 ret.angle -= Math.PI;
205 ret.indent = ret.length;
211 ret = clone(wallset.end);
212 ret.angle += Math.PI;
222 if (element.startsWith("gap_"))
224 ret.templateName = undefined;
226 ret.length = +element.slice("gap_".length);
228 else if (element.startsWith("turn_"))
230 ret.templateName = undefined;
231 ret.bend = +element.slice("turn_".length) * Math.PI;
237 civ = Object.keys(g_CivData)[0];
239 let templateName = "structures/" + civ + "_" + element;
240 if (Engine.TemplateExists(templateName))
242 ret.indent = ret.length * (element == "outpost" || element.endsWith("_tower") ? -3 : 3.5);
243 ret.templateName = templateName;
247 warn("Unrecognised wall element: '" + element + "' (" + style + "). Defaulting to " + (wallset.tower ? "'tower'." : "a blank element."));
251 // Cache to save having to calculate this element again.
252 g_WallStyles[style][element] = deepfreeze(ret);
258 * Prepare a wall element for inclusion in a style.
260 * @param {string} path - The template path to read values from
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;
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
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]
288 function getWallAlignment(position, wall = [], style = "athen_stone", orientation = 0)
290 style = validateStyle(style);
292 let wallPosition = position.clone();
294 for (let i = 0; i < wall.length; ++i)
296 let element = getWallElement(wall[i], style);
297 if (!element && i == 0)
299 warn("Not a valid wall element: style = " + style + ", wall[" + i + "] = " + wall[i] + "; " + uneval(element));
303 // Add wall elements entity placement arguments to the alignment
305 "position": Vector2D.sub(wallPosition, new Vector2D(element.indent, 0).rotate(-orientation)),
306 "templateName": element.templateName,
307 "angle": orientation + element.angle
310 // Preset vars for the next wall element
311 if (i + 1 < wall.length)
313 orientation += element.bend;
314 let nextElement = getWallElement(wall[i + 1], style);
317 warn("Not a valid wall element: style = " + style + ", wall[" + (i + 1) + "] = " + wall[i + 1] + "; " + uneval(nextElement));
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)
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));
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());
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.
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.
362 function getWallLength(style, wall)
364 style = validateStyle(style);
367 let overlap = g_WallStyles[style].overlap;
368 for (let element of wall)
369 length += getWallElement(element, style).length - overlap;
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.
381 function validateStyle(style, playerId = 0)
383 if (!style || !g_WallStyles[style])
386 return Object.keys(g_WallStyles)[0];
388 style = getCivCode(playerId) + "_stone";
389 return !g_WallStyles[style] ? Object.keys(g_WallStyles)[0] : style;
395 * Define the different wall placer functions
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
410 function placeWall(position, wall = [], style, playerId = 0, orientation = 0, constraints = undefined)
412 style = validateStyle(style, playerId);
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));
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
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([
450 new Vector2D(centerToFirstElement.x, 0).rotate(-orientation),
451 new Vector2D(centerToFirstElement.y, 0).perpendicular().rotate(-orientation)
454 return placeWall(position, fortress.wall, style, playerId, orientation, constraints);
458 * Places a predefined fortress centered around the provided point.
462 * @param {string} [type] - Predefined fortress type, as used as a key in g_FortressTypes.
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.
481 function placeLinearWall(startPosition, targetPosition, wallPart = undefined, style, playerId = 0, endWithFirst = true, constraints = undefined)
483 wallPart = wallPart || ["tower", "long"];
484 style = validateStyle(style, playerId);
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);
496 numParts = Math.ceil((totalLength - getWallElement(wallPart[0], style).length) / wallPartLength);
498 // Setup scale factor
499 let scaleFactor = totalLength / (numParts * wallPartLength);
501 scaleFactor = totalLength / (numParts * wallPartLength + getWallElement(wallPart[0], style).length);
504 let wallAngle = getAngle(startPosition.x, startPosition.y, targetPosition.x, targetPosition.y);
505 let placeAngle = wallAngle - Math.PI / 2;
507 // Place wall 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)
515 let wallEle = getWallElement(wallPart[elementIndex], style);
517 let wallLength = (wallEle.length - overlap) / 2;
518 let dist = new Vector2D(scaleFactor * wallLength, 0).rotate(-wallAngle);
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));
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));
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)
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?
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);
586 numParts = Math.ceil((totalLength - getWallElement(wallPart[0], style).length) / wallPartLength);
588 // Setup scale factor
589 let scaleFactor = totalLength / (numParts * wallPartLength);
591 scaleFactor = totalLength / (numParts * wallPartLength + getWallElement(wallPart[0], style).length);
593 // Place wall 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)
602 wallEle = getWallElement(wallEle, style);
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;
611 place.sub(new Vector2D(wallEle.indent, 0).rotate(-placeAngle));
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));
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));
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.
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);
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)
666 let angleToCorner = getAngle(corners[i].x, corners[i].y, centerPosition.x, centerPosition.y);
667 if (constraint.allows(corners[i].clone().floor()))
669 g_Map.placeEntityPassable(getWallElement(cornerWallElement, style).templateName, playerId, corners[i], angleToCorner));
671 if (!skipFirstWall || i != 0)
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(
680 // Adjustment to the corner element width (approximately)
681 Vector2D.sub(corners[i], cornerPosition),
682 Vector2D.add(corners[targetCorner], cornerPosition),
695 * Places an irregular polygonal wall consisting of parts semi-randomly
696 * chosen from a provided assortment, built around a central point at a
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.
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)
727 let wallPart = centeredWallPart;
728 for (let j = 0; j < radius; ++j)
731 wallPart = wallPart.concat(assortment);
734 assortment.reverse();
735 wallPart = assortment.concat(wallPart);
736 assortment.reverse();
738 defaultWallPartsAssortment.push(wallPart);
742 // Setup optional arguments to the default
743 wallPartsAssortment = wallPartsAssortment || defaultWallPartsAssortment;
746 let angleToCover = Math.PI * 2;
747 let angleAddList = [];
748 for (let i = 0; i < numCorners; ++i)
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];
757 let angleActual = orientation - angleAddList[0] / 2;
758 for (let i = 0; i < numCorners; ++i)
760 corners.push(Vector2D.add(centerPosition, new Vector2D(radius, 0).rotate(-angleActual)));
762 if (i < numCorners - 1)
763 angleActual += angleAddList[i + 1];
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)
771 let length = getWallLength(style, wallPart);
772 wallPartLengths.push(length);
773 if (length > maxWallPartLength)
774 maxWallPartLength = length;
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)
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)
788 let linearWallLength = numWallParts * wallPartLengths[partIndex];
789 if (linearWallLength < bestWallLength && linearWallLength > wallLength)
791 bestWallPart = wallPartsAssortment[partIndex];
792 bestWallLength = linearWallLength;
795 wallPartList.push(bestWallPart);
798 // Place Corners and walls
800 for (let i = 0; i < numCorners; ++i)
802 let angleToCorner = getAngle(corners[i].x, corners[i].y, centerPosition.x, centerPosition.y);
803 if (constraint.allows(corners[i]))
805 g_Map.placeEntityPassable(getWallElement(cornerWallElement, style).templateName, playerId, corners[i], angleToCorner));
807 if (!skipFirstWall || i != 0)
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(
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)),
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.
848 function placeGenericFortress(center, radius = 20, playerId = 0, style, irregularity = 0.5, gateOccurence = 3, maxTries = 100, constraints = undefined)
850 style = validateStyle(style, playerId);
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
860 let bestPointDerivation;
861 let minOverlap = 1000;
863 while (tries < maxTries && minOverlap > g_WallStyles[style].overlap)
865 let pointDerivation = [];
866 let distanceToTarget = 1000;
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...
881 overlap = pointDistance - pointDerivation[numPoints - 1].distanceTo(pointDerivation[0]);
882 if (overlap < minOverlap)
884 minOverlap = overlap;
885 bestPointDerivation = pointDerivation;
893 log("placeGenericFortress: Reduced overlap to " + minOverlap + " after " + tries + " tries");
897 let constraint = new StaticConstraint(constraints);
898 for (let pointIndex = 0; pointIndex < bestPointDerivation.length; ++pointIndex)
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)
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));
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()))
922 g_Map.placeEntityPassable(tower.templateName, playerId, pos, angle - Math.PI / 2 + tower.angle));