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 g_Map.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 g_Map.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),
37 new TileClassPainter(tileClass)
40 count || scaleByMapSize(1, 4) * getNumPlayers());
44 * Mountains are impassable smoothened cones.
46 function createMountains(terrain, constraint, tileClass, count, maxHeight, minRadius, maxRadius, numCircles)
48 g_Map.log("Creating mountains");
49 let mapSize = g_Map.getSize();
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, constraints, x, z, terrain, tileClass, fcc = 0, q = [])
70 let position = new Vector2D(x, z);
71 let constraint = new AndConstraint(constraints);
73 if (!g_Map.inMapBounds(position) || !constraint.allows(position))
76 let mapSize = g_Map.getSize();
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 let pos = new Vector2D(ix, iz);
121 if (Math.euclidDistance2D(ix, iz, cx, cz) > radius2 || !g_Map.inMapBounds(pos))
124 if (!constraint.allows(pos))
130 let state = gotRet[ix][iz];
137 edges.splice(state, 1);
140 for (let k = state; k < edges.length; ++k)
141 --gotRet[edges[k][0]][edges[k][1]];
152 circles.push([cx, cz, radius]);
154 for (let ix = sx; ix <= lx; ++ix)
155 for (let iz = sz; iz <= lz; ++iz)
157 if (gotRet[ix][iz] != -2 ||
158 fcc && (x - ix > fcc || ix - x > fcc || z - iz > fcc || iz - z > fcc) ||
159 ix > 0 && gotRet[ix-1][iz] == -1 ||
160 iz > 0 && gotRet[ix][iz-1] == -1 ||
161 ix < mapSize && gotRet[ix+1][iz] == -1 ||
162 iz < mapSize && gotRet[ix][iz+1] == -1)
165 edges.push([ix, iz]);
166 gotRet[ix][iz] = edges.length - 1;
170 for (let [cx, cz, radius] of circles)
172 let circlePosition = new Vector2D(cx, cz);
173 let sx = Math.max(0, cx - radius);
174 let sz = Math.max(0, cz - radius);
175 let lx = Math.min(cx + radius, mapSize);
176 let lz = Math.min(cz + radius, mapSize);
178 let clumpHeight = radius / maxRadius * maxHeight * randFloat(0.8, 1.2);
180 for (let ix = sx; ix <= lx; ++ix)
181 for (let iz = sz; iz <= lz; ++iz)
183 let position = new Vector2D(ix, iz);
184 let distance = position.distanceTo(circlePosition);
187 randIntInclusive(0, 2) +
188 Math.round(2/3 * clumpHeight * (Math.sin(Math.PI * 2/3 * (3/4 - distance / radius)) + 0.5));
190 if (distance > radius)
193 if (g_Map.getHeight(position) < newHeight)
194 g_Map.setHeight(position, newHeight);
195 else if (g_Map.getHeight(position) >= newHeight && g_Map.getHeight(position) < newHeight + 4)
196 g_Map.setHeight(position, newHeight + 4);
199 createTerrain(terrain).place(position);
202 tileClass.add(position);
208 * Generates a volcano mountain. Smoke and lava are optional.
210 * @param {number} center - Vector2D location on the tilemap.
211 * @param {number} tileClass - Painted onto every tile that is occupied by the volcano.
212 * @param {string} terrainTexture - The texture painted onto the volcano hill.
213 * @param {array} lavaTextures - Three different textures for the interior, from the outside to the inside.
214 * @param {boolean} smoke - Whether to place smoke particles.
215 * @param {number} elevationType - Elevation painter type, ELEVATION_SET = absolute or ELEVATION_MODIFY = relative.
217 function createVolcano(position, tileClass, terrainTexture, lavaTextures, smoke, elevationType)
219 g_Map.log("Creating volcano");
221 let clLava = g_Map.createTileClass();
224 "clumps": diskArea(scaleByMapSize(18, 25)),
226 "tileClass": tileClass,
230 "clumps": diskArea(scaleByMapSize(16, 23)),
232 "tileClass": g_Map.createTileClass(),
236 "clumps": diskArea(scaleByMapSize(10, 15)),
238 "tileClass": g_Map.createTileClass(),
242 "clumps": diskArea(scaleByMapSize(8, 11)),
244 "tileClass": g_Map.createTileClass(),
248 "clumps": diskArea(scaleByMapSize(4, 6)),
251 "painter": lavaTextures && new LayeredPainter([terrainTexture, ...lavaTextures], [1, 1, 1]),
256 for (let i = 0; i < layers.length; ++i)
258 new ClumpPlacer(layers[i].clumps, 0.7, 0.05, Infinity, position),
260 layers[i].painter || new LayeredPainter([terrainTexture, terrainTexture], [3]),
261 new SmoothElevationPainter(elevationType, layers[i].elevation, layers[i].steepness),
262 new TileClassPainter(layers[i].tileClass)
264 i == 0 ? null : stayClasses(layers[i - 1].tileClass, 1));
268 let num = Math.floor(diskArea(scaleByMapSize(3, 5)));
271 [new SimpleObject("actor|particle/smoke.xml", num, num, 0, 7)],
276 stayClasses(tileClass, 1));
281 * Paint the given terrain texture in the given sizes at random places of the map to diversify monotone land texturing.
283 function createPatches(sizes, terrain, constraint, count, tileClass, failFraction = 0.5)
285 for (let size of sizes)
287 new ChainPlacer(1, Math.floor(scaleByMapSize(3, 5)), size, failFraction),
289 new TerrainPainter(terrain),
290 new TileClassPainter(tileClass)
297 * Same as createPatches, but each patch consists of a set of textures drawn depending to the distance of the patch border.
299 function createLayeredPatches(sizes, terrains, terrainWidths, constraint, count, tileClass, failFraction = 0.5)
301 for (let size of sizes)
303 new ChainPlacer(1, Math.floor(scaleByMapSize(3, 5)), size, failFraction),
305 new LayeredPainter(terrains, terrainWidths),
306 new TileClassPainter(tileClass)
313 * Creates a meandering river at the given location and width.
314 * Optionally calls a function on the affected tiles.
316 * @property start - A Vector2D in tile coordinates stating where the river starts.
317 * @property end - A Vector2D in tile coordinates stating where the river ends.
318 * @property parallel - Whether the shorelines should be parallel or meander separately.
319 * @property width - Size between the two shorelines.
320 * @property fadeDist - Size of the shoreline.
321 * @property deviation - Fuzz effect on the shoreline if greater than 0.
322 * @property heightRiverbed - Ground height of the riverbed.
323 * @proeprty heightLand - Ground height of the end of the shoreline.
324 * @property meanderShort - Strength of frequent meanders.
325 * @property meanderLong - Strength of less frequent meanders.
326 * @property [constraint] - If given, ignores any tiles that don't satisfy the given Constraint.
327 * @property [waterFunc] - Optional function called on tiles within the river.
328 * Provides location on the tilegrid, new elevation and
329 * the location on the axis parallel to the river as a fraction of the river length.
330 * @property [landFunc] - Optional function called on land tiles, providing ix, iz, shoreDist1, shoreDist2.
331 * @property [minHeight] - If given, only changes the elevation below this height while still calling the given functions.
333 function paintRiver(args)
335 g_Map.log("Creating river");
337 // Model the river meandering as the sum of two sine curves.
338 let meanderShort = fractionToTiles(args.meanderShort / scaleByMapSize(35, 160));
339 let meanderLong = fractionToTiles(args.meanderLong / scaleByMapSize(35, 100));
341 // Unless the river is parallel, each riverside will receive an own random seed and starting angle.
342 let seed1 = randFloat(2, 3);
343 let seed2 = randFloat(2, 3);
345 let startingAngle1 = randFloat(0, 1);
346 let startingAngle2 = randFloat(0, 1);
348 // Computes the deflection of the river at a given point.
349 let riverCurve = (riverFraction, startAngle, seed) =>
350 meanderShort * rndRiver(startAngle + fractionToTiles(riverFraction) / 128, seed) +
351 meanderLong * rndRiver(startAngle + fractionToTiles(riverFraction) / 256, seed);
353 // Describe river location in vectors.
354 let riverLength = args.start.distanceTo(args.end);
355 let unitVecRiver = Vector2D.sub(args.start, args.end).normalize();
357 // Describe river boundaries.
358 let riverMinX = Math.min(args.start.x, args.end.x);
359 let riverMinZ = Math.min(args.start.y, args.end.y);
360 let riverMaxX = Math.max(args.start.x, args.end.x);
361 let riverMaxZ = Math.max(args.start.y, args.end.y);
363 let mapSize = g_Map.getSize();
364 for (let ix = 0; ix < mapSize; ++ix)
365 for (let iz = 0; iz < mapSize; ++iz)
367 let vecPoint = new Vector2D(ix, iz);
369 if (args.constraint && !args.constraint.allows(vecPoint))
372 // Compute the shortest distance to the river.
373 let distanceToRiver = distanceOfPointFromLine(args.start, args.end, vecPoint);
375 // Closest point on the river (i.e the foot of the perpendicular).
376 let river = Vector2D.sub(vecPoint, unitVecRiver.perpendicular().mult(distanceToRiver));
378 // Only process points that actually are perpendicular with the river.
379 if (river.x < riverMinX || river.x > riverMaxX ||
380 river.y < riverMinZ || river.y > riverMaxZ)
383 // Coordinate between 0 and 1 on the axis parallel to the river.
384 let riverFraction = river.distanceTo(args.start) / riverLength;
386 // Amplitude of the river at this location.
387 let riverCurve1 = riverCurve(riverFraction, startingAngle1, seed1);
388 let riverCurve2 = args.parallel ? riverCurve1 : riverCurve(riverFraction, startingAngle2, seed2);
391 let deviation = args.deviation * randFloat(-1, 1);
393 // Compute the distance to the shoreline.
394 let shoreDist1 = riverCurve1 + distanceToRiver - deviation - args.width / 2;
395 let shoreDist2 = riverCurve2 + distanceToRiver - deviation + args.width / 2;
397 // Create the elevation for the water and the slopy shoreline and call the user functions.
398 if (shoreDist1 < 0 && shoreDist2 > 0)
400 let height = args.heightRiverbed;
402 if (shoreDist1 > -args.fadeDist)
403 height += (args.heightLand - args.heightRiverbed) * (1 + shoreDist1 / args.fadeDist);
404 else if (shoreDist2 < args.fadeDist)
405 height += (args.heightLand - args.heightRiverbed) * (1 - shoreDist2 / args.fadeDist);
407 if (args.minHeight === undefined || height < args.minHeight)
408 g_Map.setHeight(vecPoint, height);
411 args.waterFunc(vecPoint, height, riverFraction);
413 else if (args.landFunc)
414 args.landFunc(vecPoint, shoreDist1, shoreDist2);
419 * Helper function to create a meandering river.
420 * It works the same as sin or cos function with the difference that it's period is 1 instead of 2 pi.
422 function rndRiver(f, seed)
426 for (let i = 0; i <= f; ++i)
427 rndRw = 10 * (rndRw % 1);
430 let retVal = (Math.floor(f) % 2 ? -1 : 1) * rndRr * (rndRr - 1);
432 let rndRe = Math.floor(rndRw) % 5;
434 retVal *= 2.3 * (rndRr - 0.5) * (rndRr - 0.5);
436 retVal *= 2.6 * (rndRr - 0.3) * (rndRr - 0.7);
438 retVal *= 22 * (rndRr - 0.2) * (rndRr - 0.3) * (rndRr - 0.3) * (rndRr - 0.8);
440 retVal *= 180 * (rndRr - 0.2) * (rndRr - 0.2) * (rndRr - 0.4) * (rndRr - 0.6) * (rndRr - 0.6) * (rndRr - 0.8);
442 retVal *= 2.6 * (rndRr - 0.5) * (rndRr - 0.7);
448 * Add small rivers with shallows starting at a central river ending at the map border, if the given Constraint is met.
450 function createTributaryRivers(riverAngle, riverCount, riverWidth, heightRiverbed, heightRange, maxAngle, tributaryRiverTileClass, shallowTileClass, constraint)
452 g_Map.log("Creating tributary rivers");
454 let smoothness = scaleByMapSize(3, 12);
457 let heightShallow = -2;
459 let mapSize = g_Map.getSize();
460 let mapCenter = g_Map.getCenter();
461 let mapBounds = g_Map.getBounds();
463 let riverConstraint = avoidClasses(tributaryRiverTileClass, 3);
464 if (shallowTileClass)
465 riverConstraint = new AndConstraint([riverConstraint, avoidClasses(shallowTileClass, 2)]);
467 for (let i = 0; i < riverCount; ++i)
469 // Determining tributary river location
470 let searchCenter = new Vector2D(fractionToTiles(randFloat(tapering, 1 - tapering)), mapCenter.y);
471 let sign = randBool() ? 1 : -1;
472 let distanceVec = new Vector2D(0, sign * tapering);
474 let searchStart = Vector2D.add(searchCenter, distanceVec).rotateAround(riverAngle, mapCenter);
475 let searchEnd = Vector2D.sub(searchCenter, distanceVec).rotateAround(riverAngle, mapCenter);
477 let start = findLocationInDirectionBasedOnHeight(searchStart, searchEnd, heightRange[0], heightRange[1], 4);
482 let end = Vector2D.add(mapCenter, new Vector2D(mapSize, 0).rotate(riverAngle - sign * randFloat(maxAngle, 2 * Math.PI - maxAngle))).round();
486 new PathPlacer(start, end, riverWidth, waviness, smoothness, offset, tapering),
488 new SmoothElevationPainter(ELEVATION_SET, heightRiverbed, 4),
489 new TileClassPainter(tributaryRiverTileClass)
491 new AndConstraint([constraint, riverConstraint])))
494 // Create small puddles at the map border to ensure players being separated
496 new ClumpPlacer(diskArea(riverWidth / 2), 0.95, 0.6, Infinity, end),
497 new SmoothElevationPainter(ELEVATION_SET, heightRiverbed, 3),
502 if (shallowTileClass)
504 g_Map.log("Creating shallows in the tributary rivers...");
505 for (let z of [0.25, 0.75])
507 "start": new Vector2D(mapBounds.left, fractionToTiles(z)).rotateAround(riverAngle, mapCenter),
508 "end": new Vector2D(mapBounds.right, fractionToTiles(z)).rotateAround(riverAngle, mapCenter),
509 "startWidth": scaleByMapSize(8, 12),
510 "endWidth": scaleByMapSize(8, 12),
512 "constraint": new HeightConstraint(-Infinity, heightShallow),
513 "startHeight": heightShallow,
514 "endHeight": heightShallow,
515 "tileClass": shallowTileClass
521 * Creates a smooth, passable path between between start and end with the given startWidth and endWidth.
522 * Paints the given tileclass and terrain.
524 * @property {Vector2D} start - Location of the passage.
525 * @property {Vector2D} end
526 * @property {Constraint} [constraint] - Only tiles that meet this constraint are changed.
527 * @property {number} startWidth - Size of the passage (perpendicular to the direction of the passage).
528 * @property {number} endWidth
529 * @property {number} [startHeight] - Fixed height to be used if the height at the location shouldn't be used.
530 * @property {number} [endHeight]
531 * @property {number} [maxHeight] - If given, do not touch any terrain above this height.
532 * @property {number} smoothWidth - Number of tiles at the passage border to apply height interpolation.
533 * @property {number} [tileClass] - Marks the passage with this tile class.
534 * @property {string} [terrain] - Texture to be painted on the passage area.
535 * @property {string} [edgeTerrain] - Texture to be painted on the borders of the passage.
538 function createPassage(args)
540 let bound = x => Math.max(0, Math.min(Math.round(x), g_Map.height.length - 1));
542 let startHeight = args.startHeight !== undefined ? args.startHeight : g_Map.getHeight(new Vector2D(bound(args.start.x), bound(args.start.y)));
543 let endHeight = args.endHeight !== undefined ? args.endHeight : g_Map.getHeight(new Vector2D(bound(args.end.x), bound(args.end.y)));
545 let passageVec = Vector2D.sub(args.end, args.start);
546 let widthDirection = passageVec.perpendicular().normalize();
547 let lengthStep = 1 / (2 * passageVec.length());
550 for (let lengthFraction = 0; lengthFraction <= 1; lengthFraction += lengthStep)
552 let locationLength = Vector2D.add(args.start, Vector2D.mult(passageVec, lengthFraction));
553 let halfPassageWidth = (args.startWidth + (args.endWidth - args.startWidth) * lengthFraction) / 2;
554 let passageHeight = startHeight + (endHeight - startHeight) * lengthFraction;
556 for (let stepWidth = -halfPassageWidth; stepWidth <= halfPassageWidth; stepWidth += 0.5)
558 let location = Vector2D.add(locationLength, Vector2D.mult(widthDirection, stepWidth)).round();
560 if (!g_Map.inMapBounds(location) ||
561 args.constraint && !args.constraint.allows(location))
564 points.push(location);
566 let smoothDistance = args.smoothWidth + Math.abs(stepWidth) - halfPassageWidth;
571 (g_Map.getHeight(location) * smoothDistance + passageHeight / smoothDistance) / (smoothDistance + 1 / smoothDistance) :
575 args.tileClass.add(location);
577 if (args.edgeTerrain && smoothDistance > 0)
578 createTerrain(args.edgeTerrain).place(location);
579 else if (args.terrain)
580 createTerrain(args.terrain).place(location);
584 return new Area(points);
588 * Returns the first location between startPoint and endPoint that lies within the given heightrange.
590 function findLocationInDirectionBasedOnHeight(startPoint, endPoint, minHeight, maxHeight, offset = 0)
592 let stepVec = Vector2D.sub(endPoint, startPoint);
593 let distance = Math.ceil(stepVec.length());
596 for (let i = 0; i < distance; ++i)
598 let pos = Vector2D.add(startPoint, Vector2D.mult(stepVec, i));
599 let ipos = pos.clone().round();
601 if (g_Map.validHeight(ipos) &&
602 g_Map.getHeight(ipos) >= minHeight &&
603 g_Map.getHeight(ipos) <= maxHeight)
604 return pos.add(stepVec.mult(offset));