Index: ps/trunk/binaries/data/mods/public/maps/random/island_stronghold.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/island_stronghold.js (revision 21176) +++ ps/trunk/binaries/data/mods/public/maps/random/island_stronghold.js (revision 21177) @@ -1,412 +1,412 @@ Engine.LoadLibrary("rmgen"); Engine.LoadLibrary("rmgen2"); Engine.LoadLibrary("rmbiome"); const g_InitialMineDistance = 14; const g_InitialTrees = 50; setSelectedBiome(); const tMainTerrain = g_Terrains.mainTerrain; const tForestFloor1 = g_Terrains.forestFloor1; const tForestFloor2 = g_Terrains.forestFloor2; const tCliff = g_Terrains.cliff; const tTier1Terrain = g_Terrains.tier1Terrain; const tTier2Terrain = g_Terrains.tier2Terrain; const tTier3Terrain = g_Terrains.tier3Terrain; const tHill = g_Terrains.hill; const tTier4Terrain = g_Terrains.tier4Terrain; const tShore = g_Terrains.shore; const tWater = g_Terrains.water; const oTree1 = g_Gaia.tree1; const oTree2 = g_Gaia.tree2; const oTree3 = g_Gaia.tree3; const oTree4 = g_Gaia.tree4; const oTree5 = g_Gaia.tree5; const oFruitBush = g_Gaia.fruitBush; const oMainHuntableAnimal = g_Gaia.mainHuntableAnimal; const oFish = g_Gaia.fish; const oSecondaryHuntableAnimal = g_Gaia.secondaryHuntableAnimal; const oStoneLarge = g_Gaia.stoneLarge; const oStoneSmall = g_Gaia.stoneSmall; const oMetalLarge = g_Gaia.metalLarge; const oWhale = "gaia/fauna_whale_humpback"; const oShipwreck = "gaia/treasure/shipwreck"; const oShipDebris = "gaia/treasure/shipwreck_debris"; const oObelisk = "other/obelisk"; const aGrass = g_Decoratives.grass; const aGrassShort = g_Decoratives.grassShort; const aRockLarge = g_Decoratives.rockLarge; const aRockMedium = g_Decoratives.rockMedium; const pForest1 = [tForestFloor2 + TERRAIN_SEPARATOR + oTree1, tForestFloor2 + TERRAIN_SEPARATOR + oTree2, tForestFloor2]; const pForest2 = [tForestFloor1 + TERRAIN_SEPARATOR + oTree4, tForestFloor1 + TERRAIN_SEPARATOR + oTree5, tForestFloor1]; const heightSeaGround = -10; const heightLand = 3; const heightHill = 18; var g_Map = new RandomMap(heightSeaGround, tWater); const numPlayers = getNumPlayers(); const mapSize = g_Map.getSize(); const mapCenter = g_Map.getCenter(); const clPlayer = g_Map.createTileClass(); const clHill = g_Map.createTileClass(); const clForest = g_Map.createTileClass(); const clDirt = g_Map.createTileClass(); const clRock = g_Map.createTileClass(); const clMetal = g_Map.createTileClass(); const clFood = g_Map.createTileClass(); const clBaseResource = g_Map.createTileClass(); const clLand = g_Map.createTileClass(); var startAngle = randomAngle(); var teams = getTeamsArray(); var numTeams = teams.filter(team => team).length; var teamPosition = distributePointsOnCircle(numTeams, startAngle, fractionToTiles(0.3), mapCenter)[0]; var teamRadius = fractionToTiles(0.05); var teamNo = 0; g_Map.log("Creating player islands and bases"); for (let i = 0; i < teams.length; ++i) { if (!teams[i] || isNomad()) continue; ++teamNo; let [playerPosition, playerAngle] = distributePointsOnCircle(teams[i].length, startAngle + 2 * Math.PI / teams[i].length, teamRadius, teamPosition[i]); playerPosition.forEach(position => position.round()); for (let p = 0; p < teams[i].length; ++p) { addCivicCenterAreaToClass(playerPosition[p], clPlayer); createArea( - new ChainPlacer(2, Math.floor(scaleByMapSize(5, 11)), Math.floor(scaleByMapSize(60, 250)), Infinity, playerPosition[p], 0, [Math.floor(fractionToTiles(0.01))]), + new ChainPlacer(2, Math.floor(scaleByMapSize(5, 11)), Math.floor(scaleByMapSize(60, 250)), Infinity, playerPosition[p], Infinity, [defaultPlayerBaseRadius() * 3/4]), [ new LayeredPainter([tMainTerrain, tMainTerrain, tMainTerrain], [1, 6]), - new SmoothElevationPainter(ELEVATION_SET, heightLand, 6), + new SmoothElevationPainter(ELEVATION_SET, heightLand, 2), new TileClassPainter(clLand) ]); placeCivDefaultStartingEntities(playerPosition[p], teams[i][p], false); } let mineAngle = randFloat(-1, 1) * Math.PI / teams[i].length; let mines = [ { "template": oMetalLarge, "angle": mineAngle }, { "template": oStoneLarge, "angle": mineAngle + Math.PI / 4 } ]; // Mines for (let p = 0; p < teams[i].length; ++p) for (let mine of mines) { let position = Vector2D.add(playerPosition[p], new Vector2D(g_InitialMineDistance, 0).rotate(-playerAngle[p] - mine.angle)); createObjectGroup( new SimpleGroup([new SimpleObject(mine.template, 1, 1, 0, 4)], true, clBaseResource, position), 0, [avoidClasses(clBaseResource, 4, clPlayer, 4), stayClasses(clLand, 5)]); } // Trees for (let p = 0; p < teams[i].length; ++p) { let tries = 10; for (let x = 0; x < tries; ++x) { let tAngle = playerAngle[p] + randFloat(-1, 1) * 2 * Math.PI / teams[i].length; let treePosition = Vector2D.add(playerPosition[p], new Vector2D(16, 0).rotate(-tAngle)).round(); if (createObjectGroup( new SimpleGroup([new SimpleObject(oTree2, g_InitialTrees, g_InitialTrees, 0, 7)], true, clBaseResource, treePosition), 0, [avoidClasses(clBaseResource, 4, clPlayer, 4), stayClasses(clLand, 4)])) break; } } for (let p = 0; p < teams[i].length; ++p) placePlayerBaseBerries({ "template": oFruitBush, "playerID": teams[i][p], "playerPosition": playerPosition[p], "BaseResourceClass": clBaseResource, "baseResourceConstraint": new AndConstraint([avoidClasses(clPlayer, 4), stayClasses(clLand, 5)]) }); for (let p = 0; p < teams[i].length; ++p) placePlayerBaseChicken({ "playerID": teams[i][p], "playerPosition": playerPosition[p], "BaseResourceClass": clBaseResource, "baseResourceConstraint": new AndConstraint([avoidClasses(clPlayer, 4), stayClasses(clLand, 5)]) }); // Huntable animals for (let p = 0; p < teams[i].length; ++p) { createObjectGroup( new SimpleGroup([new SimpleObject(oMainHuntableAnimal, 2 * numPlayers / numTeams, 2 * numPlayers / numTeams, 0, Math.floor(fractionToTiles(0.2)))], true, clBaseResource, teamPosition[i]), 0, [avoidClasses(clBaseResource, 2, clPlayer, 10), stayClasses(clLand, 5)]); createObjectGroup( new SimpleGroup( [new SimpleObject(oSecondaryHuntableAnimal, 4 * numPlayers / numTeams, 4 * numPlayers / numTeams, 0, Math.floor(fractionToTiles(0.2)))], true, clBaseResource, teamPosition[i]), 0, [avoidClasses(clBaseResource, 2, clPlayer, 10), stayClasses(clLand, 5)]); } } Engine.SetProgress(40); g_Map.log("Creating big islands"); createAreas( new ChainPlacer( Math.floor(scaleByMapSize(4, 8) * (isNomad() ? 2 : 1)), Math.floor(scaleByMapSize(8, 16) * (isNomad() ? 2 : 1)), Math.floor(scaleByMapSize(25, 60)), 0.07, undefined, scaleByMapSize(30, 70)), [ new LayeredPainter([tMainTerrain, tMainTerrain], [2]), new SmoothElevationPainter(ELEVATION_SET, heightLand, 6), new TileClassPainter(clLand) ], avoidClasses(clLand, 3, clPlayer, 3), scaleByMapSize(4, 14) * (isNomad() ? 2 : 1), 1); g_Map.log("Creating small islands"); createAreas( new ChainPlacer(Math.floor(scaleByMapSize(4, 7)), Math.floor(scaleByMapSize(7, 10)), Math.floor(scaleByMapSize(16, 40)), 0.07, undefined, scaleByMapSize(22, 40)), [ new LayeredPainter([tMainTerrain, tMainTerrain], [2]), new SmoothElevationPainter(ELEVATION_SET, heightLand, 6), new TileClassPainter(clLand) ], avoidClasses(clLand, 3, clPlayer, 3), scaleByMapSize(6, 55), 1); Engine.SetProgress(70); g_Map.log("Smoothing heightmap"); createArea( new MapBoundsPlacer(), new SmoothingPainter(1, 0.8, 5)); // repaint clLand to compensate for smoothing unPaintTileClassBasedOnHeight(-10, 10, 3, clLand); paintTileClassBasedOnHeight(0, 5, 3, clLand); Engine.SetProgress(85); createBumps(avoidClasses(clPlayer, 20)); createMines( [ [new SimpleObject(oMetalLarge, 1, 1, 3, (numPlayers * 2) + 1)] ], [avoidClasses(clForest, 1, clPlayer, 40, clRock, 20), stayClasses(clLand, 4)], clMetal ); createMines( [ [new SimpleObject(oStoneLarge, 1, 1, 3, (numPlayers * 2) + 1)], [new SimpleObject(oStoneSmall, 2, 2, 2, (numPlayers * 2) + 1)] ], [avoidClasses(clForest, 1, clPlayer, 40, clMetal, 20), stayClasses(clLand, 4)], clRock ); var [forestTrees, stragglerTrees] = getTreeCounts(...rBiomeTreeCount(1)); createForests( [tMainTerrain, tForestFloor1, tForestFloor2, pForest1, pForest2], [avoidClasses(clPlayer, 10, clForest, 20, clBaseResource, 5, clRock, 6, clMetal, 6), stayClasses(clLand, 3)], clForest, forestTrees); g_Map.log("Creating hills"); createAreas( new ChainPlacer(1, Math.floor(scaleByMapSize(4, 6)), Math.floor(scaleByMapSize(16, 40)), 0.5), [ new LayeredPainter([tCliff, tHill], [2]), new SmoothElevationPainter(ELEVATION_SET, heightHill, 2), new TileClassPainter(clHill) ], [avoidClasses(clBaseResource, 20, clHill, 15, clRock, 6, clMetal, 6), stayClasses(clLand, 0)], scaleByMapSize(4, 13) ); g_Map.log("Smoothing heightmap"); createArea( new MapBoundsPlacer(), - new SmoothingPainter(0.8, 1, 3)); + new SmoothingPainter(1, 0.8, 3)); createStragglerTrees( [oTree1, oTree2, oTree4, oTree3], [ avoidClasses(clForest, 10, clPlayer, 20, clMetal, 6, clRock, 6, clHill, 1), stayClasses(clLand, 4) ], clForest, stragglerTrees); createFood( [ [new SimpleObject(oMainHuntableAnimal, 5, 7, 0, 4)], [new SimpleObject(oSecondaryHuntableAnimal, 2, 3, 0, 2)] ], [3 * numPlayers, 3 * numPlayers], [avoidClasses(clForest, 0, clPlayer, 20, clHill, 1, clRock, 6, clMetal, 6), stayClasses(clLand, 2)], clFood); createFood( [ [new SimpleObject(oFruitBush, 5, 7, 0, 4)] ], [3 * numPlayers], [avoidClasses(clForest, 0, clPlayer, 15, clHill, 1, clFood, 4, clRock, 6, clMetal, 6), stayClasses(clLand, 2)], clFood); if (currentBiome() == "generic/desert") { g_Map.log("Creating obelisks"); let group = new SimpleGroup( [new SimpleObject(oObelisk, 1, 1, 0, 1)], true ); createObjectGroupsDeprecated( group, 0, [avoidClasses(clBaseResource, 0, clHill, 0, clRock, 0, clMetal, 0, clFood, 0), stayClasses(clLand, 1)], scaleByMapSize(3, 8), 1000 ); } g_Map.log("Creating dirt patches"); let numb = currentBiome() == "generic/savanna" ? 3 : 1; for (let size of [scaleByMapSize(3, 6), scaleByMapSize(5, 10), scaleByMapSize(8, 21)]) createAreas( new ChainPlacer(1, Math.floor(scaleByMapSize(3, 5)), size, 0.5), [ new LayeredPainter([[tMainTerrain, tTier1Terrain], [tTier1Terrain, tTier2Terrain], [tTier2Terrain, tTier3Terrain]], [1, 1]), new TileClassPainter(clDirt) ], [avoidClasses(clForest, 0, clHill, 0, clDirt, 5, clPlayer, 0), stayClasses(clLand, 4)], numb*scaleByMapSize(15, 45)); g_Map.log("Creating grass patches"); for (let size of [scaleByMapSize(2, 4), scaleByMapSize(3, 7), scaleByMapSize(5, 15)]) createAreas( new ChainPlacer(1, Math.floor(scaleByMapSize(3, 5)), size, 0.5), new TerrainPainter(tTier4Terrain), [avoidClasses(clForest, 0, clHill, 0, clDirt, 5, clPlayer, 0), stayClasses(clLand, 4)], numb * scaleByMapSize(15, 45)); g_Map.log("Creating small decorative rocks"); let group = new SimpleGroup( [new SimpleObject(aRockMedium, 1, 3, 0, 1)], true ); createObjectGroupsDeprecated( group, 0, [avoidClasses(clForest, 0, clHill, 0), stayClasses(clLand, 2)], scaleByMapSize(16, 262), 50 ); g_Map.log("Creating large decorative rocks"); group = new SimpleGroup( [new SimpleObject(aRockLarge, 1, 2, 0, 1), new SimpleObject(aRockMedium, 1, 3, 0, 2)], true ); createObjectGroupsDeprecated( group, 0, [avoidClasses(clForest, 0, clHill, 0), stayClasses(clLand, 2)], scaleByMapSize(8, 131), 50 ); g_Map.log("Creating fish"); group = new SimpleGroup( [new SimpleObject(oFish, 2, 3, 0, 2)], true, clFood ); createObjectGroupsDeprecated(group, 0, avoidClasses(clLand, 4, clFood, 20), 25 * numPlayers, 60 ); g_Map.log("Creating Whales"); group = new SimpleGroup( [new SimpleObject(oWhale, 1, 1, 0, 3)], true, clFood ); createObjectGroupsDeprecated(group, 0, [avoidClasses(clLand, 4),avoidClasses(clFood, 8)], scaleByMapSize(5, 20), 100 ); g_Map.log("Creating shipwrecks"); group = new SimpleGroup( [new SimpleObject(oShipwreck, 1, 1, 0, 1)], true, clFood ); createObjectGroupsDeprecated(group, 0, [avoidClasses(clLand, 4),avoidClasses(clFood, 8)], scaleByMapSize(12, 16), 100 ); g_Map.log("Creating shipwreck debris"); group = new SimpleGroup( [new SimpleObject(oShipDebris, 1, 1, 0, 1)], true, clFood ); createObjectGroupsDeprecated(group, 0, [avoidClasses(clLand, 4),avoidClasses(clFood, 8)], scaleByMapSize(10, 20), 100 ); g_Map.log("Creating small grass tufts"); let planetm = currentBiome() == "generic/tropic" ? 8 : 1; group = new SimpleGroup( [new SimpleObject(aGrassShort, 1, 2, 0, 1, -Math.PI / 8, Math.PI / 8)] ); createObjectGroupsDeprecated(group, 0, [avoidClasses(clHill, 2, clPlayer, 2, clDirt, 0), stayClasses(clLand, 3)], planetm * scaleByMapSize(13, 200) ); Engine.SetProgress(95); g_Map.log("Creating large grass tufts"); group = new SimpleGroup( [new SimpleObject(aGrass, 2, 4, 0, 1.8, -Math.PI / 8, Math.PI / 8), new SimpleObject(aGrassShort, 3, 6, 1.2,2.5, -Math.PI / 8, Math.PI / 8)] ); createObjectGroupsDeprecated(group, 0, [avoidClasses(clHill, 2, clPlayer, 2, clDirt, 1, clForest, 0), stayClasses(clLand, 5)], planetm * scaleByMapSize(13, 200) ); paintTerrainBasedOnHeight(1, 2, 0, tShore); paintTerrainBasedOnHeight(heightSeaGround, 1, 3, tWater); placePlayersNomad(clPlayer, [stayClasses(clLand, 4), avoidClasses(clHill, 2, clForest, 1, clMetal, 4, clRock, 4, clFood, 2)]); setSkySet(pickRandom(["cloudless", "cumulus", "overcast"])); setSunRotation(randomAngle()); setSunElevation(randFloat(1/5, 1/3) * Math.PI); setWaterWaviness(2); Engine.SetProgress(100); g_Map.ExportMap(); Index: ps/trunk/binaries/data/mods/public/maps/random/rmgen/painter.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/rmgen/painter.js (revision 21176) +++ ps/trunk/binaries/data/mods/public/maps/random/rmgen/painter.js (revision 21177) @@ -1,509 +1,509 @@ /** * @file A Painter modifies an arbitrary feature in a given Area, for instance terrain textures, elevation or calling other painters on that Area. * Typically the area is determined by a Placer called from createArea or createAreas. */ /** * Marks the affected area with the given tileclass. */ function TileClassPainter(tileClass) { this.tileClass = tileClass; } TileClassPainter.prototype.paint = function(area) { for (let point of area.points) this.tileClass.add(point); }; /** * Removes the given tileclass from a given area. */ function TileClassUnPainter(tileClass) { this.tileClass = tileClass; } TileClassUnPainter.prototype.paint = function(area) { for (let point of area.points) this.tileClass.remove(point); }; /** * The MultiPainter applies several painters to the given area. */ function MultiPainter(painters) { if (painters instanceof Array) this.painters = painters; else if (!painters) this.painters = []; else this.painters = [painters]; } MultiPainter.prototype.paint = function(area) { for (let painter of this.painters) painter.paint(area); }; /** * The TerrainPainter draws a given terrain texture over the given area. * When used with TERRAIN_SEPARATOR, an entity is placed on each tile. */ function TerrainPainter(terrain) { this.terrain = createTerrain(terrain); } TerrainPainter.prototype.paint = function(area) { for (let point of area.points) this.terrain.place(point); }; /** * The LayeredPainter sets different Terrains within the Area. * It choses the Terrain depending on the distance to the border of the Area. * * The Terrains given in the first array are painted from the border of the area towards the center (outermost first). * The widths array has one item less than the Terrains array. * Each width specifies how many tiles the corresponding Terrain should be wide (distance to the prior Terrain border). * The remaining area is filled with the last terrain. */ function LayeredPainter(terrainArray, widths) { if (!(terrainArray instanceof Array)) throw new Error("LayeredPainter: terrains must be an array!"); this.terrains = terrainArray.map(terrain => createTerrain(terrain)); this.widths = widths; } LayeredPainter.prototype.paint = function(area) { breadthFirstSearchPaint({ "area": area, "brushSize": 1, "gridSize": g_Map.getSize(), "withinArea": (areaID, position) => g_Map.area[position.x][position.y] == areaID, "paintTile": (point, distance) => { let width = 0; let i = 0; for (; i < this.widths.length; ++i) { width += this.widths[i]; if (width >= distance) break; } this.terrains[i].place(point); } }); }; /** * Applies smoothing to the given area using Inverse-Distance-Weighting / Shepard's method. * * @param {Number} size - Determines the number of neighboring heights to interpolate. The area is a square with the length twice this size. * @param {Number} strength - Between 0 (no effect) and 1 (only neighbor heights count). This parameter has the lowest performance impact. * @param {Number} iterations - How often the process should be repeated. Typically 1. Can be used to gain even more smoothing. */ function SmoothingPainter(size, strength, iterations) { - if (size <= 0) + if (size < 1) throw new Error("Invalid size: " + size); if (strength <= 0 || strength > 1) throw new Error("Invalid strength: " + strength); if (iterations <= 0) throw new Error("Invalid iterations: " + iterations); - this.size = size; + this.size = Math.floor(size); this.strength = strength; this.iterations = iterations; } SmoothingPainter.prototype.paint = function(area) { let brushPoints = getPointsInBoundingBox(getBoundingBox( - new Array(2).fill(0).map((zero, i) => new Vector2D(1, 1).mult(this.size).floor().mult(i ? 1 : -1)))); + new Array(2).fill(0).map((zero, i) => new Vector2D(1, 1).mult(this.size).mult(i ? 1 : -1)))); for (let i = 0; i < this.iterations; ++i) { let heightmap = clone(g_Map.height); // Additional complexity to process all 4 vertices of each tile, i.e the last row too let seen = new Array(heightmap.length).fill(0).map(zero => new Uint8Array(heightmap.length).fill(0)); for (let point of area.points) for (let tileVertex of g_TileVertices) { let vertex = Vector2D.add(point, tileVertex); if (!g_Map.validHeight(vertex) || seen[vertex.x][vertex.y]) continue; seen[vertex.x][vertex.y] = 1; let sumWeightedHeights = 0; let sumWeights = 0; for (let brushPoint of brushPoints) { let position = Vector2D.add(vertex, brushPoint); let distance = Math.abs(brushPoint.x) + Math.abs(brushPoint.y); if (!distance || !g_Map.validHeight(position)) continue; sumWeightedHeights += g_Map.getHeight(position) / distance; sumWeights += 1 / distance; } g_Map.setHeight( vertex, this.strength * sumWeightedHeights / sumWeights + (1 - this.strength) * g_Map.getHeight(vertex)); } } }; /** * Sets the given height in the given Area. */ function ElevationPainter(elevation) { this.elevation = elevation; } ElevationPainter.prototype.paint = function(area) { for (let point of area.points) for (let vertex of g_TileVertices) { let position = Vector2D.add(point, vertex); if (g_Map.validHeight(position)) g_Map.setHeight(position, this.elevation); } }; /** * Sets a random elevation of the given heightrange in the given Area. */ function RandomElevationPainter(minHeight, maxHeight) { this.minHeight = minHeight; this.maxHeight = maxHeight; } RandomElevationPainter.prototype.paint = function(area) { for (let point of area.points) for (let vertex of g_TileVertices) { let position = Vector2D.add(point, vertex); if (g_Map.validHeight(position)) g_Map.setHeight(position, randFloat(this.minHeight, this.maxHeight)); } }; /** * Absolute height change. */ const ELEVATION_SET = 0; /** * Relative height change. */ const ELEVATION_MODIFY = 1; /** * Sets the elevation of the Area in dependence to the given blendRadius and * interpolates it with the existing elevation. * * @param type - ELEVATION_MODIFY or ELEVATION_SET. * @param elevation - target height. * @param blendRadius - How steep the elevation change is. * @param randomElevation - maximum random elevation difference added to each vertex. */ function SmoothElevationPainter(type, elevation, blendRadius, randomElevation = 0) { this.type = type; this.elevation = elevation; this.blendRadius = blendRadius; this.randomElevation = randomElevation; if (type != ELEVATION_SET && type != ELEVATION_MODIFY) throw new Error("SmoothElevationPainter: invalid type '" + type + "'"); } SmoothElevationPainter.prototype.paint = function(area) { // The heightmap grid has one more vertex per side than the tile grid let heightmapSize = g_Map.height.length; // Remember height inside the area before changing it let gotHeightPt = []; let newHeight = []; for (let i = 0; i < heightmapSize; ++i) { gotHeightPt[i] = new Uint8Array(heightmapSize); newHeight[i] = new Float32Array(heightmapSize); } // Get heightmap grid vertices within or adjacent to the area let brushSize = 2; let heightPoints = []; for (let point of area.points) for (let dx = -1; dx < 1 + brushSize; ++dx) { let nx = point.x + dx; for (let dz = -1; dz < 1 + brushSize; ++dz) { let nz = point.y + dz; let position = new Vector2D(nx, nz); if (g_Map.validHeight(position) && !gotHeightPt[nx][nz]) { newHeight[nx][nz] = g_Map.getHeight(position); gotHeightPt[nx][nz] = 1; heightPoints.push(position); } } } // Every vertex of a tile is considered within the area let withinArea = (areaID, position) => { for (let vertex of g_TileVertices) { let vertexPos = Vector2D.sub(position, vertex); if (g_Map.inMapBounds(vertexPos) && g_Map.area[vertexPos.x][vertexPos.y] == areaID) return true; } return false; }; // Change height inside the area depending on the distance to the border breadthFirstSearchPaint({ "area": area, "brushSize": brushSize, "gridSize": heightmapSize, "withinArea": withinArea, "paintTile": (point, distance) => { let a = 1; if (distance <= this.blendRadius) a = (distance - 1) / this.blendRadius; if (this.type == ELEVATION_SET) newHeight[point.x][point.y] = (1 - a) * g_Map.getHeight(point); newHeight[point.x][point.y] += a * this.elevation + randFloat(-0.5, 0.5) * this.randomElevation; } }); // Smooth everything out let areaID = area.getID(); for (let point of heightPoints) { if (!withinArea(areaID, point)) continue; let count = 0; let sum = 0; for (let dx = -1; dx <= 1; ++dx) { let nx = point.x + dx; for (let dz = -1; dz <= 1; ++dz) { let nz = point.y + dz; if (g_Map.validHeight(new Vector2D(nx, nz))) { sum += newHeight[nx][nz]; ++count; } } } g_Map.setHeight(point, (newHeight[point.x][point.y] + sum / count) / 2); } }; /** * Calls the given paintTile function on all points within the given Area, * providing the distance to the border of the area (1 for points on the border). * This function can traverse any grid, for instance the tile grid or the larger heightmap grid. * * @property area - An Area storing the set of points on the tile grid. * @property gridSize - The size of the grid to be traversed. * @property brushSize - Number of points per axis on the grid that are considered a point on the tilemap. * @property withinArea - Whether a point of the grid is considered part of the Area. * @property paintTile - Called for each point of the Area of the tile grid. */ function breadthFirstSearchPaint(args) { // These variables save which points were visited already and the shortest distance to the area let saw = []; let dist = []; for (let i = 0; i < args.gridSize; ++i) { saw[i] = new Uint8Array(args.gridSize); dist[i] = new Uint16Array(args.gridSize); } let withinGrid = (x, z) => Math.min(x, z) >= 0 && Math.max(x, z) < args.gridSize; // Find all points outside of the area, mark them as seen and set zero distance let pointQueue = []; let areaID = args.area.getID(); for (let point of args.area.points) // The brushSize is added because the entire brushSize is by definition part of the area for (let dx = -1; dx < 1 + args.brushSize; ++dx) { let nx = point.x + dx; for (let dz = -1; dz < 1 + args.brushSize; ++dz) { let nz = point.y + dz; let position = new Vector2D(nx, nz); if (!withinGrid(nx, nz) || args.withinArea(areaID, position) || saw[nx][nz]) continue; saw[nx][nz] = 1; dist[nx][nz] = 0; pointQueue.push(position); } } // Visit these points, then direct neighbors of them, then their neighbors recursively. // Call the paintTile method for each point within the area, with distance == 1 for the border. while (pointQueue.length) { let point = pointQueue.shift(); let distance = dist[point.x][point.y]; if (args.withinArea(areaID, point)) args.paintTile(point, distance); // Enqueue neighboring points for (let dx = -1; dx <= 1; ++dx) { let nx = point.x + dx; for (let dz = -1; dz <= 1; ++dz) { let nz = point.y + dz; let position = new Vector2D(nx, nz); if (!withinGrid(nx, nz) || !args.withinArea(areaID, position) || saw[nx][nz]) continue; saw[nx][nz] = 1; dist[nx][nz] = distance + 1; pointQueue.push(position); } } } } /** * Paints the given texture-mapping to the given tiles. * * @param {String[]} textureIDs - Names of the terrain textures * @param {Number[]} textureNames - One-dimensional array of indices of texturenames, one for each tile of the entire map. * @returns */ function TerrainTextureArrayPainter(textureIDs, textureNames) { this.textureIDs = textureIDs; this.textureNames = textureNames; } TerrainTextureArrayPainter.prototype.paint = function(area) { let sourceSize = Math.sqrt(this.textureIDs.length); let scale = sourceSize / g_Map.getSize(); for (let point of area.points) { let sourcePos = Vector2D.mult(point, scale).floor(); g_Map.setTexture(point, this.textureNames[this.textureIDs[sourcePos.x * sourceSize + sourcePos.y]]); } }; /** * Copies the given heightmap to the given area. * Scales the horizontal plane proportionally and applies bicubic interpolation. * The heightrange is either scaled proportionally or mapped to the given heightrange. * * @param {Uint16Array} heightmap - One dimensional array of vertex heights. * @param {Number} [normalMinHeight] - The minimum height the elevation grid of 320 tiles would have. * @param {Number} [normalMaxHeight] - The maximum height the elevation grid of 320 tiles would have. */ function HeightmapPainter(heightmap, normalMinHeight = undefined, normalMaxHeight = undefined) { this.heightmap = heightmap; this.bicubicInterpolation = bicubicInterpolation; this.verticesPerSide = Math.sqrt(heightmap.length); this.normalMinHeight = normalMinHeight; this.normalMaxHeight = normalMaxHeight; } HeightmapPainter.prototype.getScale = function() { return this.verticesPerSide / (g_Map.getSize() + 1); }; HeightmapPainter.prototype.scaleHeight = function(height) { if (this.normalMinHeight === undefined || this.normalMaxHeight === undefined) return height / this.getScale() / HEIGHT_UNITS_PER_METRE; let minHeight = this.normalMinHeight * (g_Map.getSize() + 1) / 321; let maxHeight = this.normalMaxHeight * (g_Map.getSize() + 1) / 321; return minHeight + (maxHeight - minHeight) * height / 0xFFFF; }; HeightmapPainter.prototype.paint = function(area) { let scale = this.getScale(); let leftBottom = new Vector2D(0, 0); let rightTop = new Vector2D(this.verticesPerSide, this.verticesPerSide); let brushSize = new Vector2D(3, 3); let brushCenter = new Vector2D(1, 1); // Additional complexity to process all 4 vertices of each tile, i.e the last row too let seen = new Array(g_Map.height.length).fill(0).map(zero => new Uint8Array(g_Map.height.length).fill(0)); for (let point of area.points) for (let vertex of g_TileVertices) { let vertexPos = Vector2D.add(point, vertex); if (!g_Map.validHeight(vertexPos) || seen[vertexPos.x][vertexPos.y]) continue; seen[vertexPos.x][vertexPos.y] = 1; let sourcePos = Vector2D.mult(vertexPos, scale); let sourceTilePos = sourcePos.clone().floor(); let brushPosition = Vector2D.max( leftBottom, Vector2D.min( Vector2D.sub(sourceTilePos, brushCenter), Vector2D.sub(rightTop, brushSize).sub(brushCenter))); g_Map.setHeight(vertexPos, bicubicInterpolation( Vector2D.sub(sourcePos, brushPosition).sub(brushCenter), ...getPointsInBoundingBox(getBoundingBox([brushPosition, Vector2D.add(brushPosition, brushSize)])).map(pos => this.scaleHeight(this.heightmap[pos.y * this.verticesPerSide + pos.x])))); } }; Index: ps/trunk/binaries/data/mods/public/maps/random/rmgen2/setup.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/rmgen2/setup.js (revision 21176) +++ ps/trunk/binaries/data/mods/public/maps/random/rmgen2/setup.js (revision 21177) @@ -1,496 +1,496 @@ var g_Amounts = { "scarce": 0.2, "few": 0.5, "normal": 1, "many": 1.75, "tons": 3 }; var g_Mixes = { "same": 0, "similar": 0.1, "normal": 0.25, "varied": 0.5, "unique": 0.75 }; var g_Sizes = { "tiny": 0.5, "small": 0.75, "normal": 1, "big": 1.25, "huge": 1.5, }; var g_AllAmounts = Object.keys(g_Amounts); var g_AllMixes = Object.keys(g_Mixes); var g_AllSizes = Object.keys(g_Sizes); var g_DefaultTileClasses = [ "animals", "baseResource", "berries", "bluff", "bluffSlope", "dirt", "fish", "food", "forest", "hill", "land", "map", "metal", "mountain", "plateau", "player", "prop", "ramp", "rock", "settlement", "spine", "valley", "water" ]; var g_TileClasses; /** * Adds an array of elements to the map. */ function addElements(elements) { for (let element of elements) element.func( [ avoidClasses.apply(null, element.avoid), stayClasses.apply(null, element.stay || null) ], pickSize(element.sizes), pickMix(element.mixes), pickAmount(element.amounts), element.baseHeight || 0); } /** * Converts "amount" terms to numbers. */ function pickAmount(amounts) { let amount = pickRandom(amounts); if (amount in g_Amounts) return g_Amounts[amount]; return g_Amounts.normal; } /** * Converts "mix" terms to numbers. */ function pickMix(mixes) { let mix = pickRandom(mixes); if (mix in g_Mixes) return g_Mixes[mix]; return g_Mixes.normal; } /** * Converts "size" terms to numbers. */ function pickSize(sizes) { let size = pickRandom(sizes); if (size in g_Sizes) return g_Sizes[size]; return g_Sizes.normal; } /** * Choose starting locations for all players. * * @param {string} type - "radial", "line", "stronghold", "random" * @param {number} distance - radial distance from the center of the map * @param {number} groupedDistance - space between players within a team * @param {number} startAngle - determined by the map that might want to place something between players * @returns {Array|undefined} - If successful, each element is an object that contains id, angle, x, z for each player */ function addBases(type, distance, groupedDistance, startAngle) { g_Map.log("Creating bases"); let playerIDs = sortAllPlayers(); let teamsArray = getTeamsArray(); switch(type) { case "line": return placeLine(teamsArray, distance, groupedDistance, startAngle); case "radial": return placeRadial(playerIDs, distance, startAngle); case "random": return placeRandom(playerIDs) || placeRadial(playerIDs, distance, startAngle); case "stronghold": return placeStronghold(teamsArray, distance, groupedDistance, startAngle); default: warn("Unknown base placement type:" + type); return undefined; } } /** * Create the base for a single player. * * @param {Object} player - contains id, angle, x, z * @param {boolean} walls - Whether or not iberian gets starting walls */ function createBase(player, walls = true) { placePlayerBase({ "playerID": player.id, "playerPosition": player.position, "PlayerTileClass": g_TileClasses.player, "BaseResourceClass": g_TileClasses.baseResource, - "baseResourceConstraint": avoidClasses(g_TileClasses.water, 0), + "baseResourceConstraint": avoidClasses(g_TileClasses.water, 0, g_TileClasses.mountain, 0), "Walls": g_Map.getSize() > 192 && walls, "CityPatch": { "outerTerrain": g_Terrains.roadWild, "innerTerrain": g_Terrains.road, "painters": [ new TileClassPainter(g_TileClasses.player) ] }, "Chicken": { "template": g_Gaia.chicken }, "Berries": { "template": g_Gaia.fruitBush }, "Mines": { "types": [ { "template": g_Gaia.metalLarge }, { "template": g_Gaia.stoneLarge } ] }, "Trees": { "template": g_Gaia.tree1, "count": currentBiome() == "generic/savanna" ? 5 : 15 }, "Decoratives": { "template": g_Decoratives.grassShort } }); } /** * Return an array where each element is an array of playerIndices of a team. */ function getTeamsArray() { var playerIDs = sortAllPlayers(); var numPlayers = getNumPlayers(); // Group players by team var teams = []; for (let i = 0; i < numPlayers; ++i) { let team = getPlayerTeam(playerIDs[i]); if (team == -1) continue; if (!teams[team]) teams[team] = []; teams[team].push(playerIDs[i]); } // Players without a team get a custom index for (let i = 0; i < numPlayers; ++i) if (getPlayerTeam(playerIDs[i]) == -1) teams.push([playerIDs[i]]); // Remove unused indices return teams.filter(team => true); } /** * Choose a random pattern for placing the bases of the players. */ function randomStartingPositionPattern(teamsArray) { var formats = ["radial"]; var mapSize = g_Map.getSize(); var numPlayers = getNumPlayers(); // Enable stronghold if we have a few teams and a big enough map if (teamsArray.length >= 2 && numPlayers >= 4 && mapSize >= 256) formats.push("stronghold"); // Enable random if we have enough teams or enough players on a big enough map if (mapSize >= 256 && (teamsArray.length >= 3 || numPlayers > 4)) formats.push("random"); // Enable line if we have enough teams and players on a big enough map if (teamsArray.length >= 2 && numPlayers >= 4 && mapSize >= 384) formats.push("line"); return { "setup": pickRandom(formats), "distance": fractionToTiles(randFloat(0.2, 0.35)), "separation": fractionToTiles(randFloat(0.05, 0.1)) }; } /** * Place teams in a line-pattern. * * @param {Array} playerIDs - typically randomized indices of players of a single team * @param {number} distance - radial distance from the center of the map * @param {number} groupedDistance - distance between players * @param {number} startAngle - determined by the map that might want to place something between players. * * @returns {Array} - contains id, angle, x, z for every player */ function placeLine(teamsArray, distance, groupedDistance, startAngle) { let players = []; let mapCenter = g_Map.getCenter(); let dist = fractionToTiles(0.45); for (let i = 0; i < teamsArray.length; ++i) { var safeDist = distance; if (distance + teamsArray[i].length * groupedDistance > dist) safeDist = dist - teamsArray[i].length * groupedDistance; var teamAngle = startAngle + (i + 1) * 2 * Math.PI / teamsArray.length; // Create player base for (let p = 0; p < teamsArray[i].length; ++p) { players[teamsArray[i][p]] = { "id": teamsArray[i][p], "position": Vector2D.add(mapCenter, new Vector2D(safeDist + p * groupedDistance, 0).rotate(-teamAngle)).round() }; createBase(players[teamsArray[i][p]], false); } } return players; } /** * Place players in a circle-pattern. * * @param {Array} playerIDs - order of playerIDs to be placed * @param {number} distance - radial distance from the center of the map * @param {number} startAngle - determined by the map that might want to place something between players */ function placeRadial(playerIDs, distance, startAngle) { let mapCenter = g_Map.getCenter(); let players = []; let numPlayers = getNumPlayers(); for (let i = 0; i < numPlayers; ++i) { let angle = startAngle + i * 2 * Math.PI / numPlayers; players[i] = { "id": playerIDs[i], "position": Vector2D.add(mapCenter, new Vector2D(distance, 0).rotate(-angle)).round() }; createBase(players[i]); } return players; } /** * Place playerbases on random locations on the map meeting the given constraints. */ function placeRandom(playerIDs, constraints = undefined) { let players = randomPlayerLocations(playerIDs, constraints); if (!players) return undefined; for (let player of players) createBase(player); return players; } /** * Choose arbitrary starting locations. */ function randomPlayerLocations(playerIDs, constraints = undefined) { let locations = []; let attempts = 0; let resets = 0; let mapCenter = g_Map.getCenter(); let playerMinDist = fractionToTiles(0.25); let borderDistance = fractionToTiles(0.08); let area = createArea(new MapBoundsPlacer(), undefined, new AndConstraint(constraints)); for (let i = 0; i < getNumPlayers(); ++i) { let position = pickRandom(area.points); // Minimum distance between initial bases must be a quarter of the map diameter if (locations.some(loc => loc.distanceTo(position) < playerMinDist) || position.distanceTo(mapCenter) > mapCenter.x - borderDistance) { --i; ++attempts; // Reset if we're in what looks like an infinite loop if (attempts > 500) { locations = []; i = -1; attempts = 0; ++resets; // Reduce minimum player distance progressively if (resets % 25 == 0) playerMinDist *= 0.975; // If we only pick bad locations, stop trying to place randomly if (resets == 500) { throw new Error("Could not find suitable playerbase locations!"); return undefined; } } continue; } locations[i] = position; } return groupPlayersByLocations(playerIDs, locations); } /** * Pick locations from the given set so that teams end up grouped. * * @param {Array} playerIDs - sorted by teams. * @param {Array} locations - array of Vector2D of possible starting locations. */ function groupPlayersByLocations(playerIDs, locations) { playerIDs = sortPlayers(playerIDs); let minDist = Infinity; let minLocations; // Of all permutations of starting locations, find the one where // the sum of the distances between allies is minimal, weighted by teamsize. heapsPermute(shuffleArray(locations).slice(0, playerIDs.length), v => v.clone(), permutation => { let dist = 0; let teamDist = 0; let teamSize = 0; for (let i = 1; i < playerIDs.length; ++i) { let team1 = getPlayerTeam(playerIDs[i - 1]); let team2 = getPlayerTeam(playerIDs[i]); ++teamSize; if (team1 != -1 && team1 == team2) teamDist += permutation[i - 1].distanceTo(permutation[i]); else { dist += teamDist / teamSize; teamDist = 0; teamSize = 0; } } if (teamSize) dist += teamDist / teamSize; if (dist < minDist) { minDist = dist; minLocations = permutation; } }); let players = []; for (let i = 0; i < playerIDs.length; ++i) players[i] = { "id": playerIDs[i], "position": minLocations[i] }; return players; } /** * Place given players in a stronghold-pattern. * * @param teamsArray - each item is an array of playerIDs placed per stronghold * @param distance - radial distance from the center of the map * @param groupedDistance - distance between neighboring players * @param {number} startAngle - determined by the map that might want to place something between players */ function placeStronghold(teamsArray, distance, groupedDistance, startAngle) { var players = []; var mapCenter = g_Map.getCenter(); for (let i = 0; i < teamsArray.length; ++i) { var teamAngle = startAngle + (i + 1) * 2 * Math.PI / teamsArray.length; var teamPosition = Vector2D.add(mapCenter, new Vector2D(distance, 0).rotate(-teamAngle)); var teamGroupDistance = groupedDistance; // If we have a team of above average size, make sure they're spread out if (teamsArray[i].length > 4) teamGroupDistance = Math.max(fractionToTiles(0.08), groupedDistance); // If we have a solo player, place them on the center of the team's location if (teamsArray[i].length == 1) teamGroupDistance = fractionToTiles(0); // TODO: Ensure players are not placed outside of the map area, similar to placeLine // Create player base for (var p = 0; p < teamsArray[i].length; ++p) { var angle = startAngle + (p + 1) * 2 * Math.PI / teamsArray[i].length; players[teamsArray[i][p]] = { "id": teamsArray[i][p], "position": Vector2D.add(teamPosition, new Vector2D(teamGroupDistance, 0).rotate(-angle)).round() }; createBase(players[teamsArray[i][p]], false); } } return players; } /** * Creates tileClass for the default classes and every class given. * * @param {Array} newClasses * @returns {Object} - maps from classname to ID */ function initTileClasses(newClasses) { var classNames = g_DefaultTileClasses; if (newClasses) classNames = classNames.concat(newClasses); g_TileClasses = {}; for (var className of classNames) g_TileClasses[className] = g_Map.createTileClass(); } Index: ps/trunk/binaries/data/mods/public/maps/random/schwarzwald.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/schwarzwald.js (revision 21176) +++ ps/trunk/binaries/data/mods/public/maps/random/schwarzwald.js (revision 21177) @@ -1,322 +1,320 @@ Engine.LoadLibrary('rmgen'); Engine.LoadLibrary("heightmap"); setSkySet("fog"); setFogFactor(0.35); setFogThickness(0.19); setWaterColor(0.501961, 0.501961, 0.501961); setWaterTint(0.25098, 0.501961, 0.501961); setWaterWaviness(0.5); setWaterType("clap"); setWaterMurkiness(0.75); setPPSaturation(0.37); setPPContrast(0.4); setPPBrightness(0.4); setPPEffect("hdr"); setPPBloom(0.4); var oStoneLarge = 'gaia/geology_stonemine_alpine_quarry'; var oMetalLarge = 'gaia/geology_metal_alpine_slabs'; var oFish = "gaia/fauna_fish"; var aGrass = 'actor|props/flora/grass_soft_small_tall.xml'; var aGrassShort = 'actor|props/flora/grass_soft_large.xml'; var aRockLarge = 'actor|geology/stone_granite_med.xml'; var aRockMedium = 'actor|geology/stone_granite_med.xml'; var aBushMedium = 'actor|props/flora/bush_medit_me.xml'; var aBushSmall = 'actor|props/flora/bush_medit_sm.xml'; var aReeds = 'actor|props/flora/reeds_pond_lush_b.xml'; var terrainPrimary = ["temp_grass_plants", "temp_plants_bog"]; var terrainWood = ['alpine_forrestfloor|gaia/flora_tree_oak', 'alpine_forrestfloor|gaia/flora_tree_pine']; var terrainWoodBorder = ['new_alpine_grass_mossy|gaia/flora_tree_oak', 'alpine_forrestfloor|gaia/flora_tree_pine', 'temp_grass_long|gaia/flora_bush_temperate', 'temp_grass_clovers|gaia/flora_bush_berry', 'temp_grass_clovers_2|gaia/flora_bush_grapes', 'temp_grass_plants|gaia/fauna_deer', 'temp_grass_plants|gaia/fauna_rabbit', 'new_alpine_grass_dirt_a']; var terrainBase = ['temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants', 'temp_grass_plants|gaia/fauna_sheep']; var terrainBaseBorder = ['temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_d', 'temp_grass_plants', 'temp_plants_bog', 'temp_grass_plants', 'temp_grass_plants']; var baseTex = ['temp_road', 'temp_road_overgrown']; var terrainPath = ['temp_road', 'temp_road_overgrown']; var tWater = ['dirt_brown_d']; var tWaterBorder = ['dirt_brown_d']; const heightLand = 1; const heightOffsetPath = -0.1; var g_Map = new RandomMap(heightLand, terrainPrimary); var clPlayer = g_Map.createTileClass(); var clPath = g_Map.createTileClass(); var clForest = g_Map.createTileClass(); var clWater = g_Map.createTileClass(); var clMetal = g_Map.createTileClass(); var clRock = g_Map.createTileClass(); var clFood = g_Map.createTileClass(); var clBaseResource = g_Map.createTileClass(); var clOpen = g_Map.createTileClass(); var mapSize = g_Map.getSize(); var mapCenter = g_Map.getCenter(); var mapRadius = mapSize/2; var numPlayers = getNumPlayers(); var baseRadius = 15; var minPlayerRadius = Math.min(mapRadius - 1.5 * baseRadius, 5/8 * mapRadius); var maxPlayerRadius = Math.min(mapRadius - baseRadius, 3/4 * mapRadius); var playerPosition = []; var playerAngleStart = randomAngle(); var playerAngleAddAvrg = 2 * Math.PI / numPlayers; var playerAngleMaxOff = playerAngleAddAvrg/4; var resourceRadius = fractionToTiles(1/3); // Setup woods // For large maps there are memory errors with too many trees. A density of 256*192/mapArea works with 0 players. // Around each player there is an area without trees so with more players the max density can increase a bit. var maxTreeDensity = Math.min(256 * (192 + 8 * numPlayers) / Math.square(mapSize), 1); // Has to be tweeked but works ok var bushChance = 1/3; // 1 means 50% chance in deepest wood, 0.5 means 25% chance in deepest wood // Set height limits and water level by map size // Set target min and max height depending on map size to make average steepness about the same on all map sizes var heightRange = {'min': MIN_HEIGHT * (g_Map.size + 512) / 8192, 'max': MAX_HEIGHT * (g_Map.size + 512) / 8192, 'avg': (MIN_HEIGHT * (g_Map.size + 512) +MAX_HEIGHT * (g_Map.size + 512))/16384}; // Set average water coverage var averageWaterCoverage = 1/5; // NOTE: Since erosion is not predictable actual water coverage might vary much with the same values var heightSeaGround = -MIN_HEIGHT + heightRange.min + averageWaterCoverage * (heightRange.max - heightRange.min); var heightSeaGroundAdjusted = heightSeaGround + MIN_HEIGHT; setWaterHeight(heightSeaGround); // Setting a 3x3 Grid as initial heightmap var initialReliefmap = [[heightRange.max, heightRange.max, heightRange.max], [heightRange.max, heightRange.min, heightRange.max], [heightRange.max, heightRange.max, heightRange.max]]; setBaseTerrainDiamondSquare(heightRange.min, heightRange.max, initialReliefmap); g_Map.log("Smoothing map"); createArea( new MapBoundsPlacer(), new SmoothingPainter(1, 0.8, 5)); rescaleHeightmap(heightRange.min, heightRange.max); var heighLimits = [ heightRange.min + 1/3 * (heightSeaGroundAdjusted - heightRange.min), // 0 Deep water heightRange.min + 2/3 * (heightSeaGroundAdjusted - heightRange.min), // 1 Medium Water heightRange.min + (heightSeaGroundAdjusted - heightRange.min), // 2 Shallow water heightSeaGroundAdjusted + 1/8 * (heightRange.max - heightSeaGroundAdjusted), // 3 Shore heightSeaGroundAdjusted + 2/8 * (heightRange.max - heightSeaGroundAdjusted), // 4 Low ground heightSeaGroundAdjusted + 3/8 * (heightRange.max - heightSeaGroundAdjusted), // 5 Player and path height heightSeaGroundAdjusted + 4/8 * (heightRange.max - heightSeaGroundAdjusted), // 6 High ground heightSeaGroundAdjusted + 5/8 * (heightRange.max - heightSeaGroundAdjusted), // 7 Lower forest border heightSeaGroundAdjusted + 6/8 * (heightRange.max - heightSeaGroundAdjusted), // 8 Forest heightSeaGroundAdjusted + 7/8 * (heightRange.max - heightSeaGroundAdjusted), // 9 Upper forest border heightSeaGroundAdjusted + (heightRange.max - heightSeaGroundAdjusted)]; // 10 Hilltop var playerHeight = (heighLimits[4] + heighLimits[5]) / 2; for (let i = 0; i < numPlayers; ++i) { playerPosition[i] = Vector2D.add( mapCenter, new Vector2D(randFloat(minPlayerRadius, maxPlayerRadius), 0).rotate( -((playerAngleStart + i * playerAngleAddAvrg + randFloat(0, playerAngleMaxOff)) % (2 * Math.PI)))).round(); rectangularSmoothToHeight(playerPosition[i], 20, 20, playerHeight, 0.8); } placePlayerBases({ "PlayerPlacement": [sortAllPlayers(), playerPosition], "BaseResourceClass": clBaseResource, "Walls": false, // player class painted below "CityPatch": { "radius": 0.8 * baseRadius, "smoothness": 1/8, "painters": [ new TerrainPainter([baseTex], [baseRadius/4, baseRadius/4]), new TileClassPainter(clPlayer) ] }, // No chicken "Berries": { "template": "gaia/flora_bush_berry", "minCount": 2, - "maxCount": 2, - "minDist": 10, - "maxDist": 10 + "maxCount": 2 }, "Mines": { "types": [ { "template": oMetalLarge }, { "template": oStoneLarge } ], "distance": 15, "minAngle": Math.PI / 2, "maxAngle": Math.PI }, "Trees": { "template": "gaia/flora_tree_oak_large", "count": 2 } }); g_Map.log("Creating mines"); for (let [minHeight, maxHeight] of [[heighLimits[3], (heighLimits[4] + heighLimits[3]) / 2], [(heighLimits[5] + heighLimits[6]) / 2, heighLimits[7]]]) for (let [template, tileClass] of [[oStoneLarge, clRock], [oMetalLarge, clMetal]]) createObjectGroups( new SimpleGroup([new SimpleObject(template, 1, 1, 0, 4)], true, tileClass), 0, [ new HeightConstraint(minHeight, maxHeight), avoidClasses(clForest, 4, clPlayer, 20, clMetal, 40, clRock, 40) ], scaleByMapSize(2, 8), 100, false); Engine.SetProgress(50); g_Map.log("Painting textures"); var betweenShallowAndShore = (heighLimits[3] + heighLimits[2]) / 2; createArea( new HeightPlacer(Elevation_IncludeMin_IncludeMax, heighLimits[2], betweenShallowAndShore), new LayeredPainter([terrainBase, terrainBaseBorder], [5])); paintTileClassBasedOnHeight(heighLimits[2], betweenShallowAndShore, 1, clOpen); createArea( new HeightPlacer(Elevation_IncludeMin_IncludeMax, heightRange.min, heighLimits[2]), new LayeredPainter([tWaterBorder, tWater], [2])); paintTileClassBasedOnHeight(heightRange.min, heighLimits[2], 1, clWater); Engine.SetProgress(60); g_Map.log("Painting paths"); var pathBlending = numPlayers <= 4; for (let i = 0; i < numPlayers + (pathBlending ? 1 : 0); ++i) for (let j = pathBlending ? 0 : i + 1; j < numPlayers + 1; ++j) { let pathStart = i < numPlayers ? playerPosition[i] : mapCenter; let pathEnd = j < numPlayers ? playerPosition[j] : mapCenter; createArea( new RandomPathPlacer(pathStart, pathEnd, 1.75, baseRadius / 2, pathBlending), [ new TerrainPainter(terrainPath), new SmoothElevationPainter(ELEVATION_MODIFY, heightOffsetPath, 1), new TileClassPainter(clPath) ], avoidClasses(clPath, 0, clOpen, 0 ,clWater, 4, clBaseResource, 4)); } Engine.SetProgress(75); g_Map.log("Creating decoration"); createDecoration( [ [new SimpleObject(aRockMedium, 1, 3, 0, 1)], [new SimpleObject(aRockLarge, 1, 2, 0, 1), new SimpleObject(aRockMedium, 1, 3, 0, 2)], [new SimpleObject(aGrassShort, 1, 2, 0, 1)], [new SimpleObject(aGrass, 2, 4, 0, 1.8), new SimpleObject(aGrassShort, 3, 6, 1.2, 2.5)], [new SimpleObject(aBushMedium, 1, 2, 0, 2), new SimpleObject(aBushSmall, 2, 4, 0, 2)] ], [ scaleByMapSize(16, 262), scaleByMapSize(8, 131), scaleByMapSize(13, 200), scaleByMapSize(13, 200), scaleByMapSize(13, 200) ], avoidClasses(clForest, 1, clPlayer, 0, clPath, 3, clWater, 3)); Engine.SetProgress(80); g_Map.log("Growing fish"); createFood( [ [new SimpleObject(oFish, 2, 3, 0, 2)] ], [ 100 * numPlayers ], [avoidClasses(clFood, 5), stayClasses(clWater, 4)], clFood); Engine.SetProgress(85); g_Map.log("Planting reeds"); var types = [aReeds]; for (let type of types) createObjectGroupsDeprecated( new SimpleGroup([new SimpleObject(type, 1, 1, 0, 0)], true), 0, borderClasses(clWater, 0, 6), scaleByMapSize(1, 2) * 1000, 1000); Engine.SetProgress(90); g_Map.log("Planting trees"); for (var x = 0; x < mapSize; x++) for (var z = 0;z < mapSize;z++) { let position = new Vector2D(x, z); if (!g_Map.validTile(position)) continue; // The 0.5 is a correction for the entities placed on the center of tiles let radius = Vector2D.add(position, new Vector2D(0.5, 0.5)).distanceTo(mapCenter); var minDistToSL = mapSize; for (let i = 0; i < numPlayers; ++i) minDistToSL = Math.min(minDistToSL, position.distanceTo(playerPosition[i])); // Woods tile based var tDensFactSL = Math.max(Math.min((minDistToSL - baseRadius) / baseRadius, 1), 0); var tDensFactRad = Math.abs((resourceRadius - radius) / resourceRadius); var tDensActual = (maxTreeDensity * tDensFactSL * tDensFactRad)*0.75; if (!randBool(tDensActual)) continue; let border = tDensActual < randFloat(0, bushChance * maxTreeDensity); let constraint = border ? avoidClasses(clPath, 1, clOpen, 2, clWater, 3, clMetal, 4, clRock, 4) : avoidClasses(clPath, 2, clOpen, 3, clWater, 4, clMetal, 4, clRock, 4); if (constraint.allows(position)) { clForest.add(position); createTerrain(border ? terrainWoodBorder : terrainWood).place(position); } } placePlayersNomad(clPlayer, avoidClasses(clWater, 4, clForest, 1, clFood, 2, clMetal, 4, clRock, 4)); Engine.SetProgress(100); g_Map.ExportMap();