2 * @file These functions are often used to create a landscape, for instance shaping mountains, hills, rivers or grass and dirt patches.
6 * Bumps add slight, diverse elevation differences to otherwise completely level terrain.
8 function createBumps(constraint, count, minSize, maxSize, spread, failFraction = 0, elevation = 2)
10 log("Creating bumps...");
14 maxSize || Math.floor(scaleByMapSize(4, 6)),
15 spread || Math.floor(scaleByMapSize(2, 5)),
17 new SmoothElevationPainter(ELEVATION_MODIFY, elevation, 2),
19 count || scaleByMapSize(100, 200));
23 * Hills are elevated, planar, impassable terrain areas.
25 function createHills(terrainset, constraint, tileClass, count, minSize, maxSize, spread, failFraction = 0.5, elevation = 18, elevationSmoothing = 2)
27 log("Creating hills...");
31 maxSize || Math.floor(scaleByMapSize(4, 6)),
32 spread || Math.floor(scaleByMapSize(16, 40)),
35 new LayeredPainter(terrainset, [1, elevationSmoothing]),
36 new SmoothElevationPainter(ELEVATION_SET, elevation, elevationSmoothing),
40 count || scaleByMapSize(1, 4) * getNumPlayers());
44 * Mountains are impassable smoothened cones.
46 function createMountains(terrain, constraint, tileClass, count, maxHeight, minRadius, maxRadius, numCircles)
48 log("Creating mountains...");
49 let mapSize = getMapSize();
51 for (let i = 0; i < (count || scaleByMapSize(1, 4) * getNumPlayers()); ++i)
53 maxHeight !== undefined ? maxHeight : Math.floor(scaleByMapSize(30, 50)),
54 minRadius || Math.floor(scaleByMapSize(3, 4)),
55 maxRadius || Math.floor(scaleByMapSize(6, 12)),
56 numCircles || Math.floor(scaleByMapSize(4, 10)),
58 randIntExclusive(0, mapSize),
59 randIntExclusive(0, mapSize),
66 * Create a mountain using a technique very similar to ChainPlacer.
68 function createMountain(maxHeight, minRadius, maxRadius, numCircles, constraint, x, z, terrain, tileClass, fcc = 0, q = [])
70 if (constraint instanceof Array)
71 constraint = new AndConstraint(constraint);
73 if (!g_Map.inMapBounds(x, z) || !constraint.allows(x, z))
76 let mapSize = getMapSize();
77 let queueEmpty = !q.length;
80 for (let i = 0; i < mapSize; ++i)
83 for (let j = 0; j < mapSize; ++j)
89 minRadius = Math.max(1, Math.min(minRadius, maxRadius));
94 for (let i = 0; i < numCircles; ++i)
97 let [cx, cz] = pickRandom(edges);
101 radius = randIntInclusive(minRadius, maxRadius);
105 queueEmpty = !q.length;
108 let sx = Math.max(0, cx - radius);
109 let sz = Math.max(0, cz - radius);
110 let lx = Math.min(cx + radius, mapSize);
111 let lz = Math.min(cz + radius, mapSize);
113 let radius2 = Math.square(radius);
115 for (let ix = sx; ix <= lx; ++ix)
117 for (let iz = sz; iz <= lz; ++iz)
119 if (Math.euclidDistance2D(ix, iz, cx, cz) > radius2 || !g_Map.inMapBounds(ix, iz))
122 if (!constraint.allows(ix, iz))
128 let state = gotRet[ix][iz];
135 edges.splice(state, 1);
138 for (let k = state; k < edges.length; ++k)
139 --gotRet[edges[k][0]][edges[k][1]];
150 circles.push([cx, cz, radius]);
152 for (let ix = sx; ix <= lx; ++ix)
153 for (let iz = sz; iz <= lz; ++iz)
155 if (gotRet[ix][iz] != -2 ||
156 fcc && (x - ix > fcc || ix - x > fcc || z - iz > fcc || iz - z > fcc) ||
157 ix > 0 && gotRet[ix-1][iz] == -1 ||
158 iz > 0 && gotRet[ix][iz-1] == -1 ||
159 ix < mapSize && gotRet[ix+1][iz] == -1 ||
160 iz < mapSize && gotRet[ix][iz+1] == -1)
163 edges.push([ix, iz]);
164 gotRet[ix][iz] = edges.length - 1;
168 for (let [cx, cz, radius] of circles)
170 let sx = Math.max(0, cx - radius);
171 let sz = Math.max(0, cz - radius);
172 let lx = Math.min(cx + radius, mapSize);
173 let lz = Math.min(cz + radius, mapSize);
175 let clumpHeight = radius / maxRadius * maxHeight * randFloat(0.8, 1.2);
177 for (let ix = sx; ix <= lx; ++ix)
178 for (let iz = sz; iz <= lz; ++iz)
180 let distance = Math.euclidDistance2D(ix, iz, cx, cz);
183 randIntInclusive(0, 2) +
184 Math.round(2/3 * clumpHeight * (Math.sin(Math.PI * 2/3 * (3/4 - distance / radius)) + 0.5));
186 if (distance > radius)
189 if (getHeight(ix, iz) < newHeight)
190 setHeight(ix, iz, newHeight);
191 else if (getHeight(ix, iz) >= newHeight && getHeight(ix, iz) < newHeight + 4)
192 setHeight(ix, iz, newHeight + 4);
194 if (terrain !== undefined)
195 placeTerrain(ix, iz, terrain);
197 if (tileClass !== undefined)
198 addToClass(ix, iz, tileClass);
204 * Generates a volcano mountain. Smoke and lava are optional.
206 * @param {number} fx - Horizontal coordinate of the center.
207 * @param {number} fz - Horizontal coordinate of the center.
208 * @param {number} tileClass - Painted onto every tile that is occupied by the volcano.
209 * @param {string} terrainTexture - The texture painted onto the volcano hill.
210 * @param {array} lavaTextures - Three different textures for the interior, from the outside to the inside.
211 * @param {boolean} smoke - Whether to place smoke particles.
212 * @param {number} elevationType - Elevation painter type, ELEVATION_SET = absolute or ELEVATION_MODIFY = relative.
214 function createVolcano(fx, fz, tileClass, terrainTexture, lavaTextures, smoke, elevationType)
216 log("Creating volcano");
218 let ix = Math.round(fractionToTiles(fx));
219 let iz = Math.round(fractionToTiles(fz));
221 let baseSize = getMapArea() / scaleByMapSize(1, 8);
223 let smoothness = 0.05;
224 let failFraction = 100;
227 let clLava = createTileClass();
233 "tileClass": tileClass
238 "tileClass": createTileClass()
243 "tileClass": createTileClass()
248 "tileClass": createTileClass()
254 "painter": lavaTextures && new LayeredPainter([terrainTexture, ...lavaTextures], [1, 1, 1]),
259 for (let i = 0; i < layers.length; ++i)
261 new ClumpPlacer(baseSize * layers[i].clumps, coherence, smoothness, failFraction, ix, iz),
263 layers[i].painter || new LayeredPainter([terrainTexture, terrainTexture], [3]),
264 new SmoothElevationPainter(elevationType, layers[i].elevation, layers[i].steepness || steepness),
265 paintClass(layers[i].tileClass)
267 i == 0 ? null : stayClasses(layers[i - 1].tileClass, 1));
271 let num = Math.floor(baseSize * 0.002);
274 [new SimpleObject("actor|particle/smoke.xml", num, num, 0, 7)],
280 stayClasses(tileClass, 1));
285 * Paint the given terrain texture in the given sizes at random places of the map to diversify monotone land texturing.
287 function createPatches(sizes, terrain, constraint, count, tileClass, failFraction = 0.5)
289 for (let size of sizes)
291 new ChainPlacer(1, Math.floor(scaleByMapSize(3, 5)), size, failFraction),
293 new TerrainPainter(terrain),
294 paintClass(tileClass)
301 * Same as createPatches, but each patch consists of a set of textures drawn depending to the distance of the patch border.
303 function createLayeredPatches(sizes, terrains, terrainWidths, constraint, count, tileClass, failFraction = 0.5)
305 for (let size of sizes)
307 new ChainPlacer(1, Math.floor(scaleByMapSize(3, 5)), size, failFraction),
309 new LayeredPainter(terrains, terrainWidths),
310 paintClass(tileClass)
317 * Creates a meandering river at the given location and width.
318 * Optionally calls a function on the affected tiles.
319 * Horizontal locations and widths (including fadeDist, meandering) are fractions of the mapsize.
321 * @property horizontal - Whether the river is horizontal or vertical
322 * @property parallel - Whether the shorelines should be parallel or meander separately.
323 * @property position - Location of the river.
324 * @property width - Size between the two shorelines.
325 * @property fadeDist - Size of the shoreline.
326 * @property deviation - Fuzz effect on the shoreline if greater than 0.
327 * @property waterHeight - Ground height of the riverbed.
328 * @proeprty landHeight - Ground height of the end of the shoreline.
329 * @property meanderShort - Strength of frequent meanders.
330 * @property meanderLong - Strength of less frequent meanders.
331 * @property [constraint] - If given, ignores any tiles that don't satisfy the given Constraint.
332 * @property [waterFunc] - Optional function called on tiles within the river.
333 * Provides location on the tilegrid, new elevation and
334 * the location on the axis parallel to the river as a fraction of the river length.
335 * @property [landFunc] - Optional function called on land tiles, providing ix, iz, shoreDist1, shoreDist2.
336 * @property [minHeight] - If given, only changes the elevation below this height while still calling the given functions.
338 function paintRiver(args)
340 log("Creating the river");
342 // Model the river meandering as the sum of two sine curves.
343 let meanderShort = fractionToTiles(args.meanderShort / scaleByMapSize(35, 160));
344 let meanderLong = fractionToTiles(args.meanderLong / scaleByMapSize(35, 100));
346 // Unless the river is parallel, each riverside will receive an own random seed and starting angle.
347 let seed1 = randFloat(2, 3);
348 let seed2 = randFloat(2, 3);
350 let startingAngle1 = randFloat(0, 1);
351 let startingAngle2 = randFloat(0, 1);
353 // Computes the deflection of the river at a given point.
354 let riverCurve = (riverFraction, startAngle, seed) =>
355 meanderShort * rndRiver(startAngle + fractionToTiles(riverFraction) / 128, seed) +
356 meanderLong * rndRiver(startAngle + fractionToTiles(riverFraction) / 256, seed);
358 // Describe river width and length of the shoreline.
359 let halfWidth = fractionToTiles(args.width / 2);
360 let fadeDist = fractionToTiles(args.fadeDist);
362 // Describe river location in vectors.
363 let mapSize = getMapSize();
364 let vecStart = new Vector2D(args.startX, args.startZ).mult(mapSize);
365 let vecEnd = new Vector2D(args.endX, args.endZ).mult(mapSize);
366 let riverLength = vecStart.distanceTo(vecEnd);
367 let unitVecRiver = Vector2D.sub(vecStart, vecEnd).normalize();
369 // Describe river boundaries.
370 let riverMinX = Math.min(vecStart.x, vecEnd.x);
371 let riverMinZ = Math.min(vecStart.y, vecEnd.y);
372 let riverMaxX = Math.max(vecStart.x, vecEnd.x);
373 let riverMaxZ = Math.max(vecStart.y, vecEnd.y);
375 for (let ix = 0; ix < mapSize; ++ix)
376 for (let iz = 0; iz < mapSize; ++iz)
378 if (args.constraint && !args.constraint.allows(ix, iz))
381 let vecPoint = new Vector2D(ix, iz);
383 // Compute the shortest distance to the river.
384 let distanceToRiver = unitVecRiver.cross(Vector2D.sub(vecPoint, vecEnd));
386 // Closest point on the river (i.e the foot of the perpendicular).
387 let river = Vector2D.sub(vecPoint, unitVecRiver.perpendicular().mult(distanceToRiver));
389 // Only process points that actually are perpendicular with the river.
390 if (river.x < riverMinX || river.x > riverMaxX ||
391 river.y < riverMinZ || river.y > riverMaxZ)
394 // Coordinate between 0 and 1 on the axis parallel to the river.
395 let riverFraction = river.distanceTo(vecStart) / riverLength;
397 // Amplitude of the river at this location.
398 let riverCurve1 = riverCurve(riverFraction, startingAngle1, seed1);
399 let riverCurve2 = args.parallel ? riverCurve1 : riverCurve(riverFraction, startingAngle2, seed2);
402 let deviation = fractionToTiles(args.deviation) * randFloat(-1, 1);
404 // Compute the distance to the shoreline.
405 let sign = Math.sign(distanceToRiver || 1);
406 let shoreDist1 = sign * riverCurve1 + Math.abs(distanceToRiver) - deviation - halfWidth;
407 let shoreDist2 = sign * riverCurve2 + Math.abs(distanceToRiver) - deviation + halfWidth;
409 // Create the elevation for the water and the slopy shoreline and call the user functions.
410 if (shoreDist1 < 0 && shoreDist2 > 0)
412 let height = args.waterHeight;
414 if (shoreDist1 > -fadeDist)
415 height += (args.landHeight - args.waterHeight) * (1 + shoreDist1 / fadeDist);
416 else if (shoreDist2 < fadeDist)
417 height += (args.landHeight - args.waterHeight) * (1 - shoreDist2 / fadeDist);
419 if (args.minHeight === undefined || height < args.minHeight)
420 setHeight(ix, iz, height);
423 args.waterFunc(ix, iz, height, riverFraction);
425 else if (args.landFunc)
426 args.landFunc(ix, iz, shoreDist1, shoreDist2);
431 * Helper function to create a meandering river.
432 * It works the same as sin or cos function with the difference that it's period is 1 instead of 2 pi.
434 function rndRiver(f, seed)
438 for (let i = 0; i <= f; ++i)
439 rndRw = 10 * (rndRw % 1);
442 let retVal = (Math.floor(f) % 2 ? -1 : 1) * rndRr * (rndRr - 1);
444 let rndRe = Math.floor(rndRw) % 5;
446 retVal *= 2.3 * (rndRr - 0.5) * (rndRr - 0.5);
448 retVal *= 2.6 * (rndRr - 0.3) * (rndRr - 0.7);
450 retVal *= 22 * (rndRr - 0.2) * (rndRr - 0.3) * (rndRr - 0.3) * (rndRr - 0.8);
452 retVal *= 180 * (rndRr - 0.2) * (rndRr - 0.2) * (rndRr - 0.4) * (rndRr - 0.6) * (rndRr - 0.6) * (rndRr - 0.8);
454 retVal *= 2.6 * (rndRr - 0.5) * (rndRr - 0.7);
460 * Add small rivers with shallows starting at a central river ending at the map border, if the given Constraint is met.
462 function createTributaryRivers(horizontal, riverCount, riverWidth, waterHeight, heightRange, maxAngle, tributaryRiverTileClass, shallowTileClass, constraint)
464 log("Creating tributary rivers...");
466 let smoothness = scaleByMapSize(3, 12);
470 let riverConstraint = avoidClasses(tributaryRiverTileClass, 3);
471 if (shallowTileClass)
472 riverConstraint = new AndConstraint([riverConstraint, avoidClasses(shallowTileClass, 2)]);
474 for (let i = 0; i < riverCount; ++i)
476 // Determining tributary start point
477 let location = randFloat(tapering, 1 - tapering);
478 let sign = randBool() ? 1 : -1;
479 let angle = sign * randFloat(maxAngle, 2 * Math.PI - maxAngle);
480 let distance = sign * tapering;
482 let searchStart = [fractionToTiles(location), fractionToTiles(0.5 + distance)];
483 let searchEnd = [fractionToTiles(location), fractionToTiles(0.5 - distance)];
487 searchStart.reverse();
491 let start = getTIPIADBON(searchStart, searchEnd, heightRange, 0.5, 4);
495 let endX = fractionToTiles(0.5 + 0.5 * Math.cos(angle));
496 let endZ = fractionToTiles(0.5 + 0.5 * Math.sin(angle));
501 Math.floor(start[0]),
502 Math.floor(start[1]),
511 new SmoothElevationPainter(ELEVATION_SET, waterHeight, 4),
512 paintClass(tributaryRiverTileClass)
514 new AndConstraint([constraint, riverConstraint])))
517 // Create small puddles at the map border to ensure players being separated
519 new ClumpPlacer(Math.floor(diskArea(riverWidth / 2)), 0.95, 0.6, 10, endX, endZ),
520 new SmoothElevationPainter(ELEVATION_SET, waterHeight, 3),
525 if (shallowTileClass)
526 for (let z of [0.25, 0.75])
528 let m1 = [Math.round(fractionToTiles(0.2)), Math.round(fractionToTiles(z))];
529 let m2 = [Math.round(fractionToTiles(0.8)), Math.round(fractionToTiles(z))];
537 createShallowsPassage(...m1, ...m2, scaleByMapSize(4, 8), -2, -2, 2, shallowTileClass, undefined, waterHeight);
542 * Create shallow water between (x1, z1) and (x2, z2) of tiles below maxHeight.
544 function createShallowsPassage(x1, z1, x2, z2, width, maxHeight, shallowHeight, smooth, tileClass, terrain, riverHeight)
549 let distance = Math.euclidDistance2D(x1, z1, x2, z2);
550 let mapSize = getMapSize();
552 for (let ix = 0; ix < mapSize; ++ix)
553 for (let iz = 0; iz < mapSize; ++iz)
555 let c = a * (ix - x1) + b * (iz - z1);
556 let my = iz - b * c / Math.square(distance);
562 dis = Math.abs(ix - x1);
563 if (iz >= Math.min(z1, z2) && iz <= Math.max(z1, z2))
566 else if (my >= Math.min(z1, z2) && my <= Math.max(z1, z2))
568 dis = Math.abs(c) / distance;
572 if (dis > width || !inline || getHeight(ix, iz) > maxHeight)
575 if (dis > width - smooth)
576 setHeight(ix, iz, ((width - dis) * shallowHeight + riverHeight * (smooth - width + dis)) / smooth);
577 else if (dis <= width - smooth)
578 setHeight(ix, iz, shallowHeight);
580 if (tileClass !== undefined)
581 addToClass(ix, iz, tileClass);
583 if (terrain !== undefined)
584 placeTerrain(ix, iz, terrain);
589 * Creates a ramp from (x1, y1) to (x2, y2).
591 function createRamp(x1, y1, x2, y2, minHeight, maxHeight, width, smoothLevel, mainTerrain, edgeTerrain, tileClass)
593 let halfWidth = width / 2;
606 y3 = (x1 - x2) / (y1 - y2) * (x2 - x3) + y2;
609 let minBoundX = Math.max(Math.min(x1, x2) - halfWidth, 0);
610 let minBoundY = Math.max(Math.min(y1, y2) - halfWidth, 0);
611 let maxBoundX = Math.min(Math.max(x1, x2) + halfWidth, getMapSize());
612 let maxBoundY = Math.min(Math.max(y1, y2) + halfWidth, getMapSize());
614 for (let x = minBoundX; x < maxBoundX; ++x)
615 for (let y = minBoundY; y < maxBoundY; ++y)
617 let lDist = distanceOfPointFromLine(x3, y3, x2, y2, x, y);
618 let sDist = distanceOfPointFromLine(x1, y1, x2, y2, x, y);
619 let rampLength = Math.euclidDistance2D(x1, y1, x2, y2);
621 if (lDist > rampLength || sDist > halfWidth)
624 let height = ((rampLength - lDist) * maxHeight + lDist * minHeight) / rampLength;
626 if (sDist >= halfWidth - smoothLevel)
628 height = (height - minHeight) * (halfWidth - sDist) / smoothLevel + minHeight;
631 placeTerrain(x, y, edgeTerrain);
633 else if (mainTerrain)
634 placeTerrain(x, y, mainTerrain);
636 if (tileClass !== undefined)
637 addToClass(x, y, tileClass);
639 if (getHeight(Math.floor(x), Math.floor(y)) < height && height <= maxHeight)
640 setHeight(x, y, height);
645 * Get The Intended Point In A Direction Based On Height.
646 * Retrieves the N'th point with a specific height in a line and returns it as a [x, y] array.
648 * @param startPoint - [x, y] array defining the start point
649 * @param endPoint - [x, y] array defining the ending point
650 * @param heightRange - [min, max] array defining the range which the height of the intended point can be. includes both "min" and "max"
651 * @param step - how much tile units per turn should the search go. more value means faster but less accurate
652 * @param n - how many points to skip before ending the search. skips """n-1 points""".
654 function getTIPIADBON(startPoint, endPoint, heightRange, step, n)
656 let X = endPoint[0] - startPoint[0];
657 let Y = endPoint[1] - startPoint[1];
661 error("getTIPIADBON startPoint and endPoint are identical! " + new Error().stack);
665 let M = Math.sqrt(Math.square(X) + step * Math.square(Y));
666 let stepX = step * X / M;
667 let stepY = step * Y / M;
669 let y = startPoint[1];
672 let mapSize = getMapSize();
674 for (let x = startPoint[0]; true; x += stepX)
676 let ix = Math.floor(x);
677 let iy = Math.floor(y);
679 if (ix < mapSize || iy < mapSize)
681 if (getHeight(ix, iy) <= heightRange[1] &&
682 getHeight(ix, iy) >= heightRange[0])
691 if (y > endPoint[1] && stepY > 0 ||
692 y < endPoint[1] && stepY < 0 ||
693 x > endPoint[1] && stepX > 0 ||
694 x < endPoint[1] && stepX < 0)