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 : }
|