Index: binaries/data/mods/public/maps/random/rmgen-common/gaia_entities.js =================================================================== --- binaries/data/mods/public/maps/random/rmgen-common/gaia_entities.js +++ binaries/data/mods/public/maps/random/rmgen-common/gaia_entities.js @@ -15,68 +15,83 @@ * Generates two variants of forests from the given terrain textures and tree templates. * The forest border has less trees than the inside. */ -function createForests(terrainSet, constraint, tileClass, treeCount, retryFactor) +function createForests(mainTerrain, forestFloor, trees, constraint, tileClass, treeCount) { if (!treeCount) return; - // Construct different forest types from the terrain textures and template names. - let [mainTerrain, terrainForestFloor1, terrainForestFloor2, terrainForestTree1, terrainForestTree2] = terrainSet; + let borderTerrain = []; + let interiorTerrain = []; - // The painter will pick a random Terrain for each part of the forest. - let forestVariants = [ - { - "borderTerrains": [terrainForestFloor2, mainTerrain, terrainForestTree1], - "interiorTerrains": [terrainForestFloor2, terrainForestTree1] - }, - { - "borderTerrains": [terrainForestFloor1, mainTerrain, terrainForestTree2], - "interiorTerrains": [terrainForestFloor1, terrainForestTree2] - } - ]; + // Construct the terrain/tree brushes + for (let tree of trees) + { + borderTerrain.push(mainTerrain); + borderTerrain.push(mainTerrain); + borderTerrain.push(mainTerrain); + borderTerrain.push(forestFloor); + borderTerrain.push(forestFloor + TERRAIN_SEPARATOR + tree); - g_Map.log("Creating forests"); - let numberOfForests = Math.floor(treeCount / (scaleByMapSize(3, 6) * getNumPlayers() * forestVariants.length)); - for (let forestVariant of forestVariants) - createAreas( - new ChainPlacer(1, Math.floor(scaleByMapSize(3, 5)), treeCount / numberOfForests, 0.5), - [ - new LayeredPainter([forestVariant.borderTerrains, forestVariant.interiorTerrains], [2]), - new TileClassPainter(tileClass) - ], - constraint, - numberOfForests, - retryFactor); + interiorTerrain.push(forestFloor); + interiorTerrain.push(forestFloor); + interiorTerrain.push(mainTerrain); + interiorTerrain.push(forestFloor + TERRAIN_SEPARATOR + tree); + } + + let numberOfForests = Math.floor(treeCount / (3 * getNumPlayers())); + createAreas( + new ChainPlacer(2, 4, treeCount / numberOfForests, 0.5), + [ + new LayeredPainter([borderTerrain, interiorTerrain], [1]), + new TileClassPainter(tileClass) + ], + constraint, + numberOfForests, + 10 + ); } /** * Places the given amount of Entities at random places meeting the given Constraint, chosing a different template for each. */ -function createStragglerTrees(templateNames, constraint, tileClass, treeCount, retryFactor) +function createStragglerTrees(templateNames, constraint, tileClass, treeCount) { g_Map.log("Creating straggler trees"); for (let templateName of templateNames) - createObjectGroupsDeprecated( + createObjectGroups( new SimpleGroup([new SimpleObject(templateName, 1, 1, 0, 3)], true, tileClass), 0, constraint, Math.floor(treeCount / templateNames.length), - retryFactor); + 10 + ); } /** * Places a SimpleGroup consisting of the given number of the given Objects * at random locations that meet the given Constraint. + * The optional areas parameter restricts placement to specific areas. */ -function createMines(objects, constraint, tileClass, count) +function createMines(objects, constraint, tileClass, count, areas) { for (let object of objects) - createObjectGroupsDeprecated( - new SimpleGroup(object, true, tileClass), - 0, - constraint, - count || scaleByMapSize(4, 16), - 70); + if (areas) + createObjectGroupsByAreas( + new SimpleGroup(object, true, tileClass), + 0, + constraint, + count || scaleByMapSize(4, 16), + 1000, + areas + ); + else + createObjectGroups( + new SimpleGroup(object, true, tileClass), + 0, + constraint, + count || scaleByMapSize(4, 16), + 1000 + ); } /** @@ -100,32 +115,53 @@ /** * Places the given amounts of the given Objects at random locations meeting the given Constraint. + * The optional areas parameter restricts placement to specific areas, useful for placing fish in lakes for example. */ -function createFood(objects, counts, constraint, tileClass) +function createFood(objects, counts, constraint, tileClass, areas) { - g_Map.log("Creating food"); for (let i = 0; i < objects.length; ++i) - createObjectGroupsDeprecated( - new SimpleGroup(objects[i], true, tileClass), - 0, - constraint, - counts[i], - 50); + if (areas) + createObjectGroupsByAreas( + new SimpleGroup(objects[i], true, tileClass), + 0, + constraint, + counts[i] || scaleByMapSize(4, 16), + 500, + areas + ); + else + createObjectGroups( + new SimpleGroup(objects[i], true, tileClass), + 0, + constraint, + counts[i] || scaleByMapSize(4, 16), + 500 + ); } /** * Same as createFood, but doesn't mark the terrain with a TileClass. */ -function createDecoration(objects, counts, constraint) +function createDecoration(objects, counts, constraint, areas) { - g_Map.log("Creating decoration"); for (let i = 0; i < objects.length; ++i) - createObjectGroupsDeprecated( - new SimpleGroup(objects[i], true), - 0, - constraint, - counts[i], - 5); + if (areas) + createObjectGroupsByAreas( + new SimpleGroup(objects[i], true), + 0, + constraint, + counts[i] || scaleByMapSize(16, 200), + 250, + areas + ); + else + createObjectGroups( + new SimpleGroup(objects[i], true), + 0, + constraint, + counts[i] || scaleByMapSize(16, 200), + 250 + ); } /** Index: binaries/data/mods/public/maps/random/rmgen-common/gaia_terrain.js =================================================================== --- binaries/data/mods/public/maps/random/rmgen-common/gaia_terrain.js +++ binaries/data/mods/public/maps/random/rmgen-common/gaia_terrain.js @@ -129,9 +129,7 @@ let state = gotRet[ix][iz]; if (state == -1) - { gotRet[ix][iz] = -2; - } else if (state >= 0) { edges.splice(state, 1); @@ -180,7 +178,7 @@ for (let ix = sx; ix <= lx; ++ix) for (let iz = sz; iz <= lz; ++iz) { - let position = new Vector2D(ix, iz); + position = new Vector2D(ix, iz); let distance = position.distanceTo(circlePosition); let newHeight = @@ -261,7 +259,8 @@ new SmoothElevationPainter(elevationType, layers[i].elevation, layers[i].steepness), new TileClassPainter(layers[i].tileClass) ], - i == 0 ? null : stayClasses(layers[i - 1].tileClass, 1)); + i == 0 ? null : getConstraints({ stay: [layers[i - 1].tileClass, 1] }) + ); if (smoke) { @@ -273,14 +272,95 @@ clLava, position), 0, - stayClasses(tileClass, 1)); + getConstraints({ stay: [tileClass, 1] }) + ); } } /** + * This function creates random lines of mountains ensuring that there are enough breaks to pass between them. + * + * To determine their location, random points are connected in shortest-cycle order and then trimmed to max length. + * + * Arguments: + * @param {number} "height": how tall the mountains should be, in absolute height (ie set not modify). + * @param {number} "width": How wide the mountains will be. + * @param {number} "count": How many mountain range segments to create. + * @param {number} "maxLength": The longest that any connected mountain strip is allowed to be. Use to ensure that passable gaps are created. + * @param {number} "bumpiness": (0..1) How much the height of each peak should vary. + * @param {number} "waviness": (0..1) How much the mountain ranges should deviate from a straight line. + * @param {number} "mode": What elevation mode to use (ELEVATION_SET/ELEVATION_MODIFY). + * @param {array strings} "terrain": String or array of two or more strings specifying terrain textures to paint. + * @param {number} "tileclass": The tileclass that the mountains should be assigned. + * @param {array constraints} "constraint": List of tileclasses to avoid painting over. + */ +function createMountainRanges(args) +{ + // These parameters paint the mountainranges after their location was determined. + let height = args.height || scaleByMapSize(60, 120); + let mountainWidth = args.width || height/4; + + let pathplacer = new PathPlacer(undefined, undefined, undefined, args.bumpiness || 0.4, scaleByMapSize(10, 25), args.waviness || 0.75, 0.2, 0.1); + let painters = [ + new TerrainPainter(args.terrain), + new SmoothElevationPainter(args.mode || ELEVATION_SET, height, mountainWidth/2, 3), + new TileClassPainter(args.tileclass) + ]; + let constraint = args.constraint; + + // Array of Vector2D locations where a mountainrange can start or end. + let vertices = new Array(2 * (args.count || 16)).fill(0).map(() => g_Map.randomCoordinate(false)); + + // Maximum length that a mountain path can have. + let maxLength = args.maxLength || fractionToTiles(0.6); + + // Number of mountainranges starting or ending at the given point. + let vertexDegree = vertices.map(p => 0); + + // Init possible edges. + let possibleEdges = []; + let order = sortPointsShortestCycle(vertices); + let length = 0; + for (let i = 0; i < order.length - 1; ++i) + { + length += vertices[order[i]].distanceTo(vertices[order[i+1]]); + if (length <= maxLength) + possibleEdges.push([order[i], order[i+1]]); + else + length = 0; + } + + g_Map.log("Creating mountain ranges with " + possibleEdges.length + " possible edges"); + + // Create the mountain ranges. + for (let i = 0; i < possibleEdges.length; ++i) + { + let currentEdge = possibleEdges[i]; + + pathplacer.start = vertices[currentEdge[0]]; + pathplacer.end = vertices[currentEdge[1]]; + pathplacer.width = mountainWidth; + + if (createArea(pathplacer, painters, constraint)) + { + ++vertexDegree[currentEdge[0]]; + ++vertexDegree[currentEdge[1]]; + } + } + + // Create circular mountains to connect ranges that share vertices. + for (let i = 0; i < vertexDegree.length; ++i) + if (vertexDegree[i] > 1) + createArea( + new ClumpPlacer(diskArea(mountainWidth / 2), 0.95, 0.6, Infinity, vertices[i]), + painters, + constraint); +} + +/** * Paint the given terrain texture in the given sizes at random places of the map to diversify monotone land texturing. */ -function createPatches(sizes, terrain, constraints, count, tileClass, failFraction = 0.5) +function createPatches(sizes, terrain, constraints, count, tileClass, failFraction = 0.5) { for (let size of sizes) createAreas( @@ -460,9 +540,9 @@ let mapCenter = g_Map.getCenter(); let mapBounds = g_Map.getBounds(); - let riverConstraint = avoidClasses(tributaryRiverTileClass, 3); + let riverConstraint = getConstraints({ avoid: [tributaryRiverTileClass, 3] }); if (shallowTileClass) - riverConstraint = new AndConstraint([riverConstraint, avoidClasses(shallowTileClass, 2)]); + riverConstraint = new AndConstraint([riverConstraint, getConstraints({ avoid: [shallowTileClass, 2] })]); for (let i = 0; i < riverCount; ++i) { Index: binaries/data/mods/public/maps/random/rmgen-common/player.js =================================================================== --- binaries/data/mods/public/maps/random/rmgen-common/player.js +++ binaries/data/mods/public/maps/random/rmgen-common/player.js @@ -202,7 +202,7 @@ */ function getPlayerBaseArgs(playerBaseArgs) { - let baseResourceConstraint = playerBaseArgs.BaseResourceClass && avoidClasses(playerBaseArgs.BaseResourceClass, 4); + let baseResourceConstraint = playerBaseArgs.BaseResourceClass && getConstraints({ avoid: [playerBaseArgs.BaseResourceClass, 4] }); if (playerBaseArgs.baseResourceConstraint) baseResourceConstraint = new AndConstraint([baseResourceConstraint, playerBaseArgs.baseResourceConstraint]); @@ -477,7 +477,7 @@ let group = new SimpleGroup(objects, true, playerClass); let success = false; for (let distanceFactor of [1, 1/2, 1/4, 0]) - if (createObjectGroups(group, playerIDs[i], new AndConstraint([constraint, avoidClasses(playerClass, distance * distanceFactor)]), 1, 200, false).length) + if (createObjectGroups(group, playerIDs[i], new AndConstraint([constraint, getConstraints({ avoid: [playerClass, distance * distanceFactor] })]), 1, 200, false).length) { success = true; playerPosition[i] = group.centerPosition; @@ -561,7 +561,8 @@ east.length > west.length ? [east, west.concat(team)] : [east.concat(team), west], - [[], []]); + [[], []] + ); } /** Index: binaries/data/mods/public/maps/random/rmgen/Constraint.js =================================================================== --- binaries/data/mods/public/maps/random/rmgen/Constraint.js +++ binaries/data/mods/public/maps/random/rmgen/Constraint.js @@ -2,7 +2,71 @@ * @file A Constraint decides if a tile satisfies a condition defined by the class. */ +const avoid = "avoid"; +const stay = "stay"; +const near = "near"; +const border = "border"; +const height = "height"; +const slope = "slope"; + /** + * This function returns constraints based on the specifications given by the arguments. + * + * Arguments: + * @param "avoid": {[tileclass1, mindist1, tileclass2, mindist2..etc]} avoid these tileclasses, by no less than the minimum defined distance in all directions. + * @param "stay": {[tileclass1, mindist1, tileclass2, mindist2..etc]} stay within the borders of these tileclasses, by at least the minimum defined distance in all directions. + * @param "near": {[tileclass1, maxdist1, tileclass2, maxdist2..etc]} stay near these tileclasses, by no farther than the maximum defined distance. + * @param "border": {[tileclass1, innerdist1, outerdist1, tileclass2, innerdist2, outerdist2..etc]} stay near the perimeter of these tileclasses, within the innerdist inside and the outerdist outside. + */ +function getConstraints(args) +{ + let constraints = []; + let current; + + current = args.avoid; + if (current) + for (let i = 0; i < current.length/2; ++i) + constraints.push(new AvoidTileClassConstraint(current[2*i], current[2*i+1])); + + current = args.stay; + if (current) + for (let i = 0; i < current.length/2; ++i) + constraints.push(new StayInTileClassConstraint(current[2*i], current[2*i+1])); + + current = args.near; + if (current) + for (let i = 0; i < current.length/2; ++i) + constraints.push(new NearTileClassConstraint(current[2*i], current[2*i+1])); + + current = args.border; + if (current) + for (let i = 0; i < current.length/3; ++i) + constraints.push(new BorderTileClassConstraint(current[3*i], current[3*i+1], current[3*i+2])); + + current = args.height; + if (current) + constraints.push(new HeightConstraint(current[0], current[1])); + current = args.slope; + if (current) + constraints.push(new SlopeConstraint(current[0], current[1])); + + if (constraints.length == 1) + return constraints[0]; + else if (constraints.length == 0) + return new NullConstraint(); + + return new AndConstraint(constraints); +} + +/** + * Creates an area from a set of constraints. + */ +function getAreaFromConstraints(args) +{ + return new Area(new MapBoundsPlacer().place(getConstraints(args))); +} + +/** * The NullConstraint is always satisfied. */ function NullConstraint() {} @@ -18,7 +82,7 @@ function AndConstraint(constraints) { if (constraints instanceof Array) - this.constraints = constraints + this.constraints = constraints; else if (!constraints) this.constraints = []; else @@ -68,7 +132,7 @@ AvoidAreasConstraint.prototype.allows = function(position) { - return this.areas.every(area => !area.contains(position)) + return this.areas.every(area => !area.contains(position)); }; /** @@ -108,7 +172,7 @@ AvoidTileClassConstraint.prototype.allows = function(position) { - return this.tileClass.countMembersInRadius(position, this.distance) == 0; + return !this.tileClass.isNear(position, this.distance); }; /** @@ -122,7 +186,7 @@ StayInTileClassConstraint.prototype.allows = function(position) { - return this.tileClass.countNonMembersInRadius(position, this.distance) == 0; + return this.tileClass.isWithin(position, this.distance); }; /** @@ -136,7 +200,7 @@ NearTileClassConstraint.prototype.allows = function(position) { - return this.tileClass.countMembersInRadius(position, this.distance) > 0; + return this.tileClass.isNear(position, this.distance); }; /** @@ -153,8 +217,8 @@ BorderTileClassConstraint.prototype.allows = function(position) { - return this.tileClass.countMembersInRadius(position, this.distanceOutside) > 0 && - this.tileClass.countNonMembersInRadius(position, this.distanceInside) > 0; + return this.tileClass.isNear(position, this.distanceOutside) && + !this.tileClass.isWithin(position, this.distanceInside); }; /** Index: binaries/data/mods/public/maps/random/rmgen/TileClass.js =================================================================== --- binaries/data/mods/public/maps/random/rmgen/TileClass.js +++ binaries/data/mods/public/maps/random/rmgen/TileClass.js @@ -97,10 +97,8 @@ this.rangeCount[position.y].add(position.x, -1); }; -TileClass.prototype.countInRadius = function(position, radius, returnMembers) +TileClass.prototype.intersect = function(position, radius, near) { - let members = 0; - let nonMembers = 0; let radius2 = Math.square(radius); for (let y = position.y - radius; y <= position.y + radius; ++y) @@ -117,8 +115,10 @@ let newMembers = this.rangeCount[iy].get(minX, maxX); - members += newMembers; - nonMembers += maxX - minX - newMembers; + if (near && newMembers > 0) + return true; + else if (!near && maxX - minX - newMembers > 0) + return false; } } else // Simply check the tiles one by one to find the number @@ -134,26 +134,29 @@ if (Math.square(dx) + Math.square(dy) <= radius2) { if (this.inclusionCount[ix] && this.inclusionCount[ix][iy] && this.inclusionCount[ix][iy] > 0) - ++members; - else - ++nonMembers; + { + if (near) + return true; + } + else if (!near) + return false; } } } } - if (returnMembers) - return members; + if (near) + return false; else - return nonMembers; + return true; }; -TileClass.prototype.countMembersInRadius = function(position, radius) +TileClass.prototype.isNear = function(position, radius) { - return this.countInRadius(position, radius, true); + return this.intersect(position, radius, true); }; -TileClass.prototype.countNonMembersInRadius = function(position, radius) +TileClass.prototype.isWithin = function(position, radius) { - return this.countInRadius(position, radius, false); + return this.intersect(position, radius, false); }; Index: binaries/data/mods/public/maps/random/rmgen/library.js =================================================================== --- binaries/data/mods/public/maps/random/rmgen/library.js +++ binaries/data/mods/public/maps/random/rmgen/library.js @@ -32,7 +32,7 @@ * Constants needed for heightmap_manipulation.js */ const MAX_HEIGHT_RANGE = 0xFFFF / HEIGHT_UNITS_PER_METRE; // Engine limit, Roughly 700 meters -const MIN_HEIGHT = - SEA_LEVEL; +const MIN_HEIGHT = -SEA_LEVEL; /** * Length of one tile of the terrain grid in metres. @@ -68,12 +68,12 @@ let obstructionSize = obstruction.Static ? new Vector2D(obstruction.Static["@depth"], obstruction.Static["@width"]) : - // Used for gates, should consider the position too - obstruction.Obstructions ? - new Vector2D( - Object.keys(obstruction.Obstructions).reduce((depth, key) => Math.max(depth, +obstruction.Obstructions[key]["@depth"]), 0), - Object.keys(obstruction.Obstructions).reduce((width, key) => width + +obstruction.Obstructions[key]["@width"], 0)) : - new Vector2D(0, 0); + // Used for gates, should consider the position too + obstruction.Obstructions ? + new Vector2D( + Object.keys(obstruction.Obstructions).reduce((depth, key) => Math.max(depth, +obstruction.Obstructions[key]["@depth"]), 0), + Object.keys(obstruction.Obstructions).reduce((width, key) => width + (+obstruction.Obstructions[key]["@width"]), 0)) : + new Vector2D(0, 0); return obstructionSize.div(TERRAIN_TILE_SIZE).add(new Vector2D(2, 2).mult(margin)); } @@ -98,6 +98,12 @@ return Vector2D.add(tilePosition, new Vector2D(randFloat(0, 1), randFloat(0, 1))); } +// Smooths the entire heightmap. +function globalSmoothMap(sampleRadius, strength, iterations) +{ + new SmoothingPainter(sampleRadius, strength, iterations).paint(getAreaFromConstraints({})); +} + /** * Retries the given function with those arguments as often as specified. */ @@ -247,54 +253,6 @@ } /** - * Create an avoid constraint for the given classes by the given distances - */ -function avoidClasses(/*class1, dist1, class2, dist2, etc*/) -{ - let ar = []; - for (let i = 0; i < arguments.length/2; ++i) - ar.push(new AvoidTileClassConstraint(arguments[2*i], arguments[2*i+1])); - - // Return single constraint - if (ar.length == 1) - return ar[0]; - - return new AndConstraint(ar); -} - -/** - * Create a stay constraint for the given classes by the given distances - */ -function stayClasses(/*class1, dist1, class2, dist2, etc*/) -{ - let ar = []; - for (let i = 0; i < arguments.length/2; ++i) - ar.push(new StayInTileClassConstraint(arguments[2*i], arguments[2*i+1])); - - // Return single constraint - if (ar.length == 1) - return ar[0]; - - return new AndConstraint(ar); -} - -/** - * Create a border constraint for the given classes by the given distances - */ -function borderClasses(/*class1, idist1, odist1, class2, idist2, odist2, etc*/) -{ - let ar = []; - for (let i = 0; i < arguments.length/3; ++i) - ar.push(new BorderTileClassConstraint(arguments[3*i], arguments[3*i+1], arguments[3*i+2])); - - // Return single constraint - if (ar.length == 1) - return ar[0]; - - return new AndConstraint(ar); -} - -/** * Returns a subset of the given heightmap. */ function extractHeightmap(heightmap, topLeft, size)