Index: ps/trunk/binaries/data/mods/public/maps/random/caledonian_meadows.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/caledonian_meadows.js (revision 21181) +++ ps/trunk/binaries/data/mods/public/maps/random/caledonian_meadows.js (revision 21182) @@ -1,438 +1,429 @@ Engine.LoadLibrary("rmgen"); Engine.LoadLibrary("rmbiome"); Engine.LoadLibrary("heightmap"); var tGrove = "temp_grass_plants"; var tPath = "road_rome_a"; var oGroveEntities = ["structures/gaul_outpost", "gaia/flora_tree_oak_new"]; var g_Map = new RandomMap(0, "whiteness"); /** - * Drags a path to a target height smoothing it at the edges and return some points along the path. - */ -function placeRandomPathToHeight(start, target, targetHeight, tileClass, texture, width, distance, strength, heightmap) -{ - let painters = [new TerrainPainter(texture)]; - - if (tileClass) - painters.push(new TileClassPainter(tileClass)); - - let position = start.clone(); - while (position.distanceTo(target) >= distance / 2) - { - rectangularSmoothToHeight(position, width * 3, width * 3, targetHeight, strength, heightmap); - - createArea( - new ClumpPlacer(diskArea(width), 1, 1, Infinity, position), - painters); - - position.add(new Vector2D(distance, 0).rotate( - -getAngle(position.x, position.y, target.x, target.y) - randFloat(-1, 1) * Math.PI / 2)); - } -} - -/** * Design resource spots */ // Mines let decorations = [ "actor|geology/gray1.xml", "actor|geology/gray_rock1.xml", "actor|geology/highland1.xml", "actor|geology/highland2.xml", "actor|geology/highland3.xml", "actor|geology/highland_c.xml", "actor|geology/highland_d.xml", "actor|geology/highland_e.xml", "actor|props/flora/bush.xml", "actor|props/flora/bush_dry_a.xml", "actor|props/flora/bush_highlands.xml", "actor|props/flora/bush_tempe_a.xml", "actor|props/flora/bush_tempe_b.xml", "actor|props/flora/ferns.xml" ]; function placeMine(point, centerEntity) { g_Map.placeEntityPassable(centerEntity, 0, point, randomAngle()); let quantity = randIntInclusive(11, 23); let dAngle = 2 * Math.PI / quantity; for (let i = 0; i < quantity; ++i) g_Map.placeEntityPassable( pickRandom(decorations), 0, Vector2D.add(point, new Vector2D(randFloat(2, 5), 0).rotate(-dAngle * randFloat(i, i + 1))), randomAngle()); } // Food, fences with domestic animals g_WallStyles.other = { "overlap": 0, "fence": readyWallElement("other/fence_long", "gaia"), "fence_short": readyWallElement("other/fence_short", "gaia"), "bench": { "angle": Math.PI / 2, "length": 1.5, "indent": 0, "bend": 0, "templateName": "other/bench" }, "sheep": { "angle": 0, "length": 0, "indent": 0.75, "bend": 0, "templateName": "gaia/fauna_sheep" }, "foodBin": { "angle": Math.PI / 2, "length": 1.5, "indent": 0, "bend": 0, "templateName": "gaia/treasure/food_bin" }, "farmstead": { "angle": Math.PI, "length": 0, "indent": -3, "bend": 0, "templateName": "structures/brit_farmstead" } }; let fences = [ new Fortress("fence", [ "foodBin", "farmstead", "bench", "turn_0.25", "sheep", "turn_0.25", "fence", "turn_0.25", "sheep", "turn_0.25", "fence", "turn_0.25", "sheep", "turn_0.25", "fence" ]), new Fortress("fence", [ "foodBin", "farmstead", "fence", "turn_0.25", "sheep", "turn_0.25", "fence", "turn_0.25", "sheep", "turn_0.25", "bench", "sheep", "fence", "turn_0.25", "sheep", "turn_0.25", "fence" ]), new Fortress("fence", [ "foodBin", "farmstead", "turn_0.5", "bench", "turn_-0.5", "fence_short", "turn_0.25", "sheep", "turn_0.25", "fence", "turn_0.25", "sheep", "turn_0.25", "fence", "turn_0.25", "sheep", "turn_0.25", "fence_short", "sheep", "fence" ]), new Fortress("fence", [ "foodBin", "farmstead", "turn_0.5", "fence_short", "turn_-0.5", "bench", "turn_0.25", "sheep", "turn_0.25", "fence", "turn_0.25", "sheep", "turn_0.25", "fence", "turn_0.25", "sheep", "turn_0.25", "fence_short", "sheep", "fence" ]), new Fortress("fence", [ "foodBin", "farmstead", "fence", "turn_0.25", "sheep", "turn_0.25", "bench", "sheep", "fence", "turn_0.25", "sheep", "turn_0.25", "fence_short", "sheep", "fence", "turn_0.25", "sheep", "turn_0.25", "fence_short", "sheep", "fence" ]) ]; let num = fences.length; for (let i = 0; i < num; ++i) fences.push(new Fortress("fence", clone(fences[i].wall).reverse())); // Groves, only Wood let groveEntities = ["gaia/flora_bush_temperate", "gaia/flora_tree_euro_beech"]; let groveActors = [ "actor|geology/highland1_moss.xml", "actor|geology/highland2_moss.xml", "actor|props/flora/bush.xml", "actor|props/flora/bush_dry_a.xml", "actor|props/flora/bush_highlands.xml", "actor|props/flora/bush_tempe_a.xml", "actor|props/flora/bush_tempe_b.xml", "actor|props/flora/ferns.xml" ]; let clGrove = g_Map.createTileClass(); function placeGrove(point) { g_Map.placeEntityPassable(pickRandom(oGroveEntities), 0, point, randomAngle()); let quantity = randIntInclusive(20, 30); let dAngle = 2 * Math.PI / quantity; for (let i = 0; i < quantity; ++i) { let angle = dAngle * randFloat(i, i + 1); let dist = randFloat(2, 5); let objectList = groveEntities; if (i % 3 == 0) objectList = groveActors; let position = Vector2D.add(point, new Vector2D(dist, 0).rotate(-angle)); g_Map.placeEntityPassable(pickRandom(objectList), 0, position, randomAngle()); createArea( new ClumpPlacer(5, 1, 1, Infinity, position), [ new TerrainPainter(tGrove), new TileClassPainter(clGrove) ]); } } // Camps with fire and gold treasure function placeCamp(point, centerEntity = "actor|props/special/eyecandy/campfire.xml", otherEntities = ["gaia/treasure/metal", "gaia/treasure/standing_stone", "units/brit_infantry_slinger_b", "units/brit_infantry_javelinist_b", "units/gaul_infantry_slinger_b", "units/gaul_infantry_javelinist_b", "units/gaul_champion_fanatic", "actor|props/special/common/waypoint_flag.xml", "actor|props/special/eyecandy/barrel_a.xml", "actor|props/special/eyecandy/basket_celt_a.xml", "actor|props/special/eyecandy/crate_a.xml", "actor|props/special/eyecandy/dummy_a.xml", "actor|props/special/eyecandy/handcart_1.xml", "actor|props/special/eyecandy/handcart_1_broken.xml", "actor|props/special/eyecandy/sack_1.xml", "actor|props/special/eyecandy/sack_1_rough.xml" ] ) { g_Map.placeEntityPassable(centerEntity, 0, point, randomAngle()); let quantity = randIntInclusive(5, 11); let dAngle = 2 * Math.PI / quantity; for (let i = 0; i < quantity; ++i) { let angle = dAngle * randFloat(i, i + 1); let dist = randFloat(1, 3); g_Map.placeEntityPassable(pickRandom(otherEntities), 0, Vector2D.add(point, new Vector2D(dist, 0).rotate(-angle)), randomAngle()); } } function placeStartLocationResources(point, foodEntities = ["gaia/flora_bush_berry", "gaia/fauna_chicken", "gaia/fauna_chicken"]) { let currentAngle = randomAngle(); // Stone and chicken let dAngle = 4/9 * Math.PI; let angle = currentAngle + randFloat(1, 3) * dAngle / 4; let stonePosition = Vector2D.add(point, new Vector2D(12, 0).rotate(-angle)); placeMine(stonePosition, "gaia/geology_stonemine_temperate_quarry"); currentAngle += dAngle; // Wood let quantity = 80; dAngle = 2 * Math.PI / quantity / 3; for (let i = 0; i < quantity; ++i) { angle = currentAngle + randFloat(0, dAngle); let objectList = groveEntities; if (i % 2 == 0) objectList = groveActors; let woodPosition = Vector2D.add(point, new Vector2D(randFloat(10, 15), 0).rotate(-angle)); g_Map.placeEntityPassable(pickRandom(objectList), 0, woodPosition, randomAngle()); createArea( new ClumpPlacer(5, 1, 1, Infinity, woodPosition), [ new TerrainPainter("temp_grass_plants"), new TileClassPainter(clGrove) ]); currentAngle += dAngle; } // Metal and chicken dAngle = 2 * Math.PI * 2 / 9; angle = currentAngle + dAngle * randFloat(1, 3) / 4; let metalPosition = Vector2D.add(point, new Vector2D(13, 0).rotate(-angle)); placeMine(metalPosition, "gaia/geology_metal_temperate_slabs"); currentAngle += dAngle; // Berries quantity = 15; dAngle = 2 * Math.PI / quantity * 2 / 9; for (let i = 0; i < quantity; ++i) { angle = currentAngle + randFloat(0, dAngle); let berriesPosition = Vector2D.add(point, new Vector2D(randFloat(10, 15), 0).rotate(-angle)); g_Map.placeEntityPassable(pickRandom(foodEntities), 0, berriesPosition, randomAngle()); currentAngle += dAngle; } } /** * Environment settings */ setBiome("generic/alpine"); g_Environment.Fog.FogColor = { "r": 0.8, "g": 0.8, "b": 0.8, "a": 0.01 }; g_Environment.Water.WaterBody.Colour = { "r" : 0.3, "g" : 0.05, "b" : 0.1, "a" : 0.1 }; g_Environment.Water.WaterBody.Murkiness = 0.4; /** * Base terrain shape generation and settings */ let heightScale = (g_Map.size + 256) / 768 / 4; let heightRange = { "min": MIN_HEIGHT * heightScale, "max": MAX_HEIGHT * heightScale }; // Water coverage let averageWaterCoverage = 1/5; // NOTE: Since terrain generation is quite unpredictable actual water coverage might vary much with the same value let heightSeaGround = -MIN_HEIGHT + heightRange.min + averageWaterCoverage * (heightRange.max - heightRange.min); // Water height in environment and the engine let heightSeaGroundAdjusted = heightSeaGround + MIN_HEIGHT; // Water height in RMGEN setWaterHeight(heightSeaGround); g_Map.log("Generating terrain using diamon-square"); let medH = (heightRange.min + heightRange.max) / 2; let initialHeightmap = [[medH, medH], [medH, medH]]; setBaseTerrainDiamondSquare(heightRange.min, heightRange.max, initialHeightmap, 0.8); g_Map.log("Apply erosion"); for (let i = 0; i < 5; ++i) splashErodeMap(0.1); rescaleHeightmap(heightRange.min, heightRange.max); Engine.SetProgress(25); let 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 let playerHeight = (heighLimits[4] + heighLimits[5]) / 2; // Average player height g_Map.log("Determining height-dependent biome"); // Texture and actor presets let myBiome = []; myBiome.push({ // 0 Deep water "texture": ["shoreline_stoney_a"], "actor": [["gaia/fauna_fish", "actor|geology/stone_granite_boulder.xml"], 0.02], "textureHS": ["alpine_mountainside"], "actorHS": [["gaia/fauna_fish"], 0.1] }); myBiome.push({ // 1 Medium Water "texture": ["shoreline_stoney_a", "alpine_shore_rocks"], "actor": [["actor|geology/stone_granite_boulder.xml", "actor|geology/stone_granite_med.xml"], 0.03], "textureHS": ["alpine_mountainside"], "actorHS": [["actor|geology/stone_granite_boulder.xml", "actor|geology/stone_granite_med.xml"], 0.0] }); myBiome.push({ // 2 Shallow water "texture": ["alpine_shore_rocks"], "actor": [["actor|props/flora/reeds_pond_dry.xml", "actor|geology/stone_granite_large.xml", "actor|geology/stone_granite_med.xml", "actor|props/flora/reeds_pond_lush_b.xml"], 0.2], "textureHS": ["alpine_mountainside"], "actorHS": [["actor|props/flora/reeds_pond_dry.xml", "actor|geology/stone_granite_med.xml"], 0.1] }); myBiome.push({ // 3 Shore "texture": ["alpine_shore_rocks_grass_50", "alpine_grass_rocky"], "actor": [["gaia/flora_tree_pine", "gaia/flora_bush_badlands", "actor|geology/highland1_moss.xml", "actor|props/flora/grass_soft_tuft_a.xml", "actor|props/flora/bush.xml"], 0.3], "textureHS": ["alpine_mountainside"], "actorHS": [["actor|props/flora/grass_soft_tuft_a.xml"], 0.1] }); myBiome.push({ // 4 Low ground "texture": ["alpine_dirt_grass_50", "alpine_grass_rocky"], "actor": [["actor|geology/stone_granite_med.xml", "actor|props/flora/grass_soft_tuft_a.xml", "actor|props/flora/bush.xml", "actor|props/flora/grass_medit_flowering_tall.xml"], 0.2], "textureHS": ["alpine_grass_rocky"], "actorHS": [["actor|geology/stone_granite_med.xml", "actor|props/flora/grass_soft_tuft_a.xml"], 0.1] }); myBiome.push({ // 5 Player and path height "texture": ["new_alpine_grass_c", "new_alpine_grass_b", "new_alpine_grass_d"], "actor": [["actor|geology/stone_granite_small.xml", "actor|props/flora/grass_soft_small.xml", "actor|props/flora/grass_medit_flowering_tall.xml"], 0.2], "textureHS": ["alpine_grass_rocky"], "actorHS": [["actor|geology/stone_granite_small.xml", "actor|props/flora/grass_soft_small.xml"], 0.1] }); myBiome.push({ // 6 High ground "texture": ["new_alpine_grass_a", "alpine_grass_rocky"], "actor": [["actor|geology/stone_granite_med.xml", "actor|props/flora/grass_tufts_a.xml", "actor|props/flora/bush_highlands.xml", "actor|props/flora/grass_medit_flowering_tall.xml"], 0.2], "textureHS": ["alpine_grass_rocky"], "actorHS": [["actor|geology/stone_granite_med.xml", "actor|props/flora/grass_tufts_a.xml"], 0.1] }); myBiome.push({ // 7 Lower forest border "texture": ["new_alpine_grass_mossy", "alpine_grass_rocky"], "actor": [["gaia/flora_tree_pine", "gaia/flora_tree_oak", "actor|props/flora/grass_tufts_a.xml", "gaia/flora_bush_berry", "actor|geology/highland2_moss.xml", "gaia/fauna_goat", "actor|props/flora/bush_tempe_underbrush.xml"], 0.3], "textureHS": ["alpine_cliff_c"], "actorHS": [["actor|props/flora/grass_tufts_a.xml", "actor|geology/highland2_moss.xml"], 0.1] }); myBiome.push({ // 8 Forest "texture": ["alpine_forrestfloor"], "actor": [["gaia/flora_tree_pine", "gaia/flora_tree_pine", "gaia/flora_tree_pine", "gaia/flora_tree_pine", "actor|geology/highland2_moss.xml", "actor|props/flora/bush_highlands.xml"], 0.5], "textureHS": ["alpine_cliff_c"], "actorHS": [["actor|geology/highland2_moss.xml", "actor|geology/stone_granite_med.xml"], 0.1] }); myBiome.push({ // 9 Upper forest border "texture": ["alpine_forrestfloor_snow", "new_alpine_grass_dirt_a"], "actor": [["gaia/flora_tree_pine", "actor|geology/snow1.xml"], 0.3], "textureHS": ["alpine_cliff_b"], "actorHS": [["actor|geology/stone_granite_med.xml", "actor|geology/snow1.xml"], 0.1] }); myBiome.push({ // 10 Hilltop "texture": ["alpine_cliff_a", "alpine_cliff_snow"], "actor": [["actor|geology/highland1.xml"], 0.05], "textureHS": ["alpine_cliff_c"], "actorHS": [["actor|geology/highland1.xml"], 0.0] }); let [playerIDs, playerPosition] = sortPlayersByLocation(getStartLocationsByHeightmap({ "min": heighLimits[4], "max": heighLimits[5] }, 1000, 30)); Engine.SetProgress(30); -g_Map.log("Smooth player locations"); -for (let p = 0; p < playerIDs.length; ++p) - rectangularSmoothToHeight(playerPosition[p], 35, 35, playerHeight, 0.7); +g_Map.log("Smoothing player locations"); +for (let position of playerPosition) + createArea( + new ClumpPlacer(diskArea(35), 1, 1, Infinity, position), + new SmoothElevationPainter(ELEVATION_SET, g_Map.getHeight(position), 35)); -g_Map.log("Creating paths"); -let tchm = getTileCenteredHeightmap(); +g_Map.log("Creating paths between players"); let clPath = g_Map.createTileClass(); for (let i = 0; i < playerPosition.length; ++i) - placeRandomPathToHeight(playerPosition[i], playerPosition[(i + 1) % playerPosition.length], playerHeight, clPath, tPath, 4, 4, 0.08, g_Map.height); + createArea( + new RandomPathPlacer(playerPosition[i], playerPosition[(i + 1) % playerPosition.length], 4, 2, false), + [ + new TerrainPainter(tPath), + new ElevationBlendingPainter(playerHeight, 0.4), + new TileClassPainter(clPath) + ]); + +g_Map.log("Smoothing paths"); +createArea( + new MapBoundsPlacer(), + new SmoothingPainter(5, 1, 1), + new NearTileClassConstraint(clPath, 5)); + Engine.SetProgress(45); g_Map.log("Determining resource locations"); let avoidPoints = playerPosition.map(pos => pos.clone()); for (let i = 0; i < avoidPoints.length; ++i) avoidPoints[i].dist = 30; let resourceSpots = getPointsByHeight({ "min": (heighLimits[3] + heighLimits[4]) / 2, "max": (heighLimits[5] + heighLimits[6]) / 2 }, avoidPoints, clPath); Engine.SetProgress(55); /** * Divide tiles in areas by height and avoid paths */ +let tchm = getTileCenteredHeightmap(); let areas = heighLimits.map(heightLimit => []); for (let x = 0; x < tchm.length; ++x) for (let y = 0; y < tchm[0].length; ++y) { let position = new Vector2D(x, y); if (!avoidClasses(clPath, 0).allows(position)) continue; let minHeight = heightRange.min; for (let h = 0; h < heighLimits.length; ++h) { if (tchm[x][y] >= minHeight && tchm[x][y] <= heighLimits[h]) { areas[h].push(position); break; } minHeight = heighLimits[h]; } } /** * Get max slope of each area */ let slopeMap = getSlopeMap(); let minSlope = []; let maxSlope = []; for (let h = 0; h < heighLimits.length; ++h) { minSlope[h] = Infinity; maxSlope[h] = 0; for (let point of areas[h]) { let slope = slopeMap[point.x][point.y]; if (slope > maxSlope[h]) maxSlope[h] = slope; if (slope < minSlope[h]) minSlope[h] = slope; } } g_Map.log("Painting areas by height and slope"); for (let h = 0; h < heighLimits.length; ++h) for (let point of areas[h]) { let actor; let texture = pickRandom(myBiome[h].texture); if (slopeMap[point.x][point.y] < 0.4 * (minSlope[h] + maxSlope[h])) { if (randBool(myBiome[h].actor[1])) actor = pickRandom(myBiome[h].actor[0]); } else { texture = pickRandom(myBiome[h].textureHS); if (randBool(myBiome[h].actorHS[1])) actor = pickRandom(myBiome[h].actorHS[0]); } g_Map.setTexture(point, texture); if (actor) g_Map.placeEntityAnywhere(actor, 0, randomPositionOnTile(point), randomAngle()); } Engine.SetProgress(80); g_Map.log("Placing players"); if (isNomad()) placePlayersNomad(g_Map.createTileClass(), new HeightConstraint(heighLimits[4], heighLimits[5])); else for (let p = 0; p < playerIDs.length; ++p) { placeCivDefaultStartingEntities(playerPosition[p], playerIDs[p], true); placeStartLocationResources(playerPosition[p]); } g_Map.log("Placing resources, farmsteads, groves and camps"); for (let i = 0; i < resourceSpots.length; ++i) { let pos = new Vector2D(resourceSpots[i].x, resourceSpots[i].y); let choice = i % 5; if (choice == 0) placeMine(pos, "gaia/geology_stonemine_temperate_formation"); if (choice == 1) placeMine(pos, "gaia/geology_metal_temperate_slabs"); if (choice == 2) placeCustomFortress(pos, pickRandom(fences), "other", 0, randomAngle()); if (choice == 3) placeGrove(pos); if (choice == 4) placeCamp(pos); } g_Map.ExportMap(); Index: ps/trunk/binaries/data/mods/public/maps/random/heightmap/heightmap.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/heightmap/heightmap.js (revision 21181) +++ ps/trunk/binaries/data/mods/public/maps/random/heightmap/heightmap.js (revision 21182) @@ -1,438 +1,390 @@ /** * Heightmap manipulation functionality * * A heightmapt is an array of width arrays of height floats * Width and height is normally mapSize+1 (Number of vertices is one bigger than number of tiles in each direction) * The default heightmap is g_Map.height (See the Map object) * * @warning - Ambiguous naming and potential confusion: * To use this library use TILE_CENTERED_HEIGHT_MAP = false (default) * Otherwise TILE_CENTERED_HEIGHT_MAP has nothing to do with any tile centered map in this library * @todo - TILE_CENTERED_HEIGHT_MAP should be removed and g_Map.height should never be tile centered */ /** * Get the height range of a heightmap * @param {array} [heightmap=g_Map.height] - The reliefmap the minimum and maximum height should be determined for * @return {object} [height] - Height range with 2 floats in properties "min" and "max" */ function getMinAndMaxHeight(heightmap = g_Map.height) { let height = {}; height.min = Infinity; height.max = - Infinity; for (let x = 0; x < heightmap.length; ++x) { for (let y = 0; y < heightmap[x].length; ++y) { if (heightmap[x][y] < height.min) height.min = heightmap[x][y]; else if (heightmap[x][y] > height.max) height.max = heightmap[x][y]; } } return height; } /** * Rescales a heightmap so its minimum and maximum height is as the arguments told preserving it's global shape * @param {float} [minHeight=MIN_HEIGHT] - Minimum height that should be used for the resulting heightmap * @param {float} [maxHeight=MAX_HEIGHT] - Maximum height that should be used for the resulting heightmap * @param {array} [heightmap=g_Map.height] - A reliefmap * @todo Add preserveCostline to leave a certain height untoucht and scale below and above that seperately */ function rescaleHeightmap(minHeight = MIN_HEIGHT, maxHeight = MAX_HEIGHT, heightmap = g_Map.height) { let oldHeightRange = getMinAndMaxHeight(heightmap); let max_x = heightmap.length; let max_y = heightmap[0].length; for (let x = 0; x < max_x; ++x) for (let y = 0; y < max_y; ++y) heightmap[x][y] = minHeight + (heightmap[x][y] - oldHeightRange.min) / (oldHeightRange.max - oldHeightRange.min) * (maxHeight - minHeight); } /** * Get start location with the largest minimum distance between players * @param {object} [heightRange] - The height range start locations are allowed * @param {integer} [maxTries=1000] - How often random player distributions are rolled to be compared * @param {float} [minDistToBorder=20] - How far start locations have to be away from the map border * @param {integer} [numberOfPlayers=g_MapSettings.PlayerData.length] - How many start locations should be placed * @param {array} [heightmap=g_Map.height] - The reliefmap for the start locations to be placed on * @param {boolean} [isCircular=g_MapSettings.CircularMap] - If the map is circular or rectangular * @return {Vector2D[]} */ function getStartLocationsByHeightmap(heightRange, maxTries = 1000, minDistToBorder = 20, numberOfPlayers = g_MapSettings.PlayerData.length - 1, heightmap = g_Map.height, isCircular = g_MapSettings.CircularMap) { let validStartLoc = []; let mapCenter = g_Map.getCenter(); let mapSize = g_Map.getSize(); let heightConstraint = new HeightConstraint(heightRange.min, heightRange.max); for (let x = minDistToBorder; x < mapSize - minDistToBorder; ++x) for (let y = minDistToBorder; y < mapSize - minDistToBorder; ++y) { let position = new Vector2D(x, y); if (heightConstraint.allows(position) && (!isCircular || position.distanceTo(mapCenter)) < mapSize / 2 - minDistToBorder) validStartLoc.push(position); } let maxMinDist = 0; let finalStartLoc; for (let tries = 0; tries < maxTries; ++tries) { let startLoc = []; let minDist = Infinity; for (let p = 0; p < numberOfPlayers; ++p) startLoc.push(pickRandom(validStartLoc)); for (let p1 = 0; p1 < numberOfPlayers - 1; ++p1) for (let p2 = p1 + 1; p2 < numberOfPlayers; ++p2) { let dist = startLoc[p1].distanceTo(startLoc[p2]); if (dist < minDist) minDist = dist; } if (minDist > maxMinDist) { maxMinDist = minDist; finalStartLoc = startLoc; } } return finalStartLoc; } /** * Sets the heightmap to a relatively realistic shape * The function doubles the size of the initial heightmap (if given, else a random 2x2 one) until it's big enough, then the extend is cut off * @note min/maxHeight will not necessarily be present in the heightmap * @note On circular maps the edges (given by initialHeightmap) may not be in the playable map area * @note The impact of the initial heightmap depends on its size and target map size * @param {float} [minHeight=MIN_HEIGHT] - Lower limit of the random height to be rolled * @param {float} [maxHeight=MAX_HEIGHT] - Upper limit of the random height to be rolled * @param {array} [initialHeightmap] - Optional, Small (e.g. 3x3) heightmap describing the global shape of the map e.g. an island [[MIN_HEIGHT, MIN_HEIGHT, MIN_HEIGHT], [MIN_HEIGHT, MAX_HEIGHT, MIN_HEIGHT], [MIN_HEIGHT, MIN_HEIGHT, MIN_HEIGHT]] * @param {float} [smoothness=0.5] - Float between 0 (rough, more local structures) to 1 (smoother, only larger scale structures) * @param {array} [heightmap=g_Map.height] - The reliefmap that will be set by this function */ function setBaseTerrainDiamondSquare(minHeight = MIN_HEIGHT, maxHeight = MAX_HEIGHT, initialHeightmap = undefined, smoothness = 0.5, heightmap = g_Map.height) { + g_Map.log("Generating map using the diamond-square algorithm"); + initialHeightmap = (initialHeightmap || [[randFloat(minHeight / 2, maxHeight / 2), randFloat(minHeight / 2, maxHeight / 2)], [randFloat(minHeight / 2, maxHeight / 2), randFloat(minHeight / 2, maxHeight / 2)]]); let heightRange = maxHeight - minHeight; if (heightRange <= 0) warn("setBaseTerrainDiamondSquare: heightRange <= 0"); let offset = heightRange / 2; // Double initialHeightmap width until target width is reached (diamond square method) let newHeightmap = []; while (initialHeightmap.length < heightmap.length) { newHeightmap = []; let oldWidth = initialHeightmap.length; // Square for (let x = 0; x < 2 * oldWidth - 1; ++x) { newHeightmap.push([]); for (let y = 0; y < 2 * oldWidth - 1; ++y) { if (x % 2 == 0 && y % 2 == 0) // Old tile newHeightmap[x].push(initialHeightmap[x/2][y/2]); else if (x % 2 == 1 && y % 2 == 1) // New tile with diagonal old tile neighbors { newHeightmap[x].push((initialHeightmap[(x-1)/2][(y-1)/2] + initialHeightmap[(x+1)/2][(y-1)/2] + initialHeightmap[(x-1)/2][(y+1)/2] + initialHeightmap[(x+1)/2][(y+1)/2]) / 4); newHeightmap[x][y] += (newHeightmap[x][y] - minHeight) / heightRange * randFloat(-offset, offset); } else // New tile with straight old tile neighbors newHeightmap[x].push(undefined); // Define later } } // Diamond for (let x = 0; x < 2 * oldWidth - 1; ++x) { for (let y = 0; y < 2 * oldWidth - 1; ++y) { if (newHeightmap[x][y] !== undefined) continue; if (x > 0 && x + 1 < newHeightmap.length - 1 && y > 0 && y + 1 < newHeightmap.length - 1) // Not a border tile { newHeightmap[x][y] = (newHeightmap[x+1][y] + newHeightmap[x][y+1] + newHeightmap[x-1][y] + newHeightmap[x][y-1]) / 4; newHeightmap[x][y] += (newHeightmap[x][y] - minHeight) / heightRange * randFloat(-offset, offset); } else if (x < newHeightmap.length - 1 && y > 0 && y < newHeightmap.length - 1) // Left border { newHeightmap[x][y] = (newHeightmap[x+1][y] + newHeightmap[x][y+1] + newHeightmap[x][y-1]) / 3; newHeightmap[x][y] += (newHeightmap[x][y] - minHeight) / heightRange * randFloat(-offset, offset); } else if (x > 0 && y > 0 && y < newHeightmap.length - 1) // Right border { newHeightmap[x][y] = (newHeightmap[x][y+1] + newHeightmap[x-1][y] + newHeightmap[x][y-1]) / 3; newHeightmap[x][y] += (newHeightmap[x][y] - minHeight) / heightRange * randFloat(-offset, offset); } else if (x > 0 && x < newHeightmap.length - 1 && y < newHeightmap.length - 1) // Bottom border { newHeightmap[x][y] = (newHeightmap[x+1][y] + newHeightmap[x][y+1] + newHeightmap[x-1][y]) / 3; newHeightmap[x][y] += (newHeightmap[x][y] - minHeight) / heightRange * randFloat(-offset, offset); } else if (x > 0 && x < newHeightmap.length - 1 && y > 0) // Top border { newHeightmap[x][y] = (newHeightmap[x+1][y] + newHeightmap[x-1][y] + newHeightmap[x][y-1]) / 3; newHeightmap[x][y] += (newHeightmap[x][y] - minHeight) / heightRange * randFloat(-offset, offset); } } } initialHeightmap = clone(newHeightmap); offset /= Math.pow(2, smoothness); } // Cut initialHeightmap to fit target width let shift = [Math.floor((newHeightmap.length - heightmap.length) / 2), Math.floor((newHeightmap[0].length - heightmap[0].length) / 2)]; for (let x = 0; x < heightmap.length; ++x) for (let y = 0; y < heightmap[0].length; ++y) heightmap[x][y] = newHeightmap[x + shift[0]][y + shift[1]]; } /** - * Pushes a rectangular area towards a given height smoothing it into the original terrain - * @note The window function to determine the smooth is not exactly a gaussian to ensure smooth edges - * @param {Vector2D} center - The x and y coordinates of the center point (rounded in this function) - * @param {float} [dx] - Distance from the center in x direction the rectangle ends (half width, rounded in this function) - * @param {float} [dy] - Distance from the center in y direction the rectangle ends (half depth, rounded in this function) - * @param {float} [targetHeight] - Height the center of the rectangle will be pushed to - * @param {float} [strength=1] - How strong the height is pushed: 0 means not at all, 1 means the center will be pushed to the target height - * @param {array} [heightmap=g_Map.height] - The heightmap to be manipulated - * @todo Make the window function an argument and maybe add some - */ -function rectangularSmoothToHeight(center, dx, dy, targetHeight, strength = 0.8, heightmap = g_Map.height) -{ - let x = Math.round(center.x); - let y = Math.round(center.y); - dx = Math.round(dx); - dy = Math.round(dy); - - let heightmapWin = []; - for (let wx = 0; wx < 2 * dx + 1; ++wx) - { - heightmapWin.push([]); - for (let wy = 0; wy < 2 * dy + 1; ++wy) - { - let actualX = x - dx + wx; - let actualY = y - dy + wy; - if (actualX >= 0 && actualX < heightmap.length - 1 && actualY >= 0 && actualY < heightmap[0].length - 1) // Is in map - heightmapWin[wx].push(heightmap[actualX][actualY]); - else - heightmapWin[wx].push(targetHeight); - } - } - for (let wx = 0; wx < 2 * dx + 1; ++wx) - { - for (let wy = 0; wy < 2 * dy + 1; ++wy) - { - let actualX = x - dx + wx; - let actualY = y - dy + wy; - if (actualX >= 0 && actualX < heightmap.length - 1 && actualY >= 0 && actualY < heightmap[0].length - 1) // Is in map - { - // Window function polynomial 2nd degree - let scaleX = 1 - (wx / dx - 1) * (wx / dx - 1); - let scaleY = 1 - (wy / dy - 1) * (wy / dy - 1); - - heightmap[actualX][actualY] = heightmapWin[wx][wy] + strength * scaleX * scaleY * (targetHeight - heightmapWin[wx][wy]); - } - } - } -} - -/** * Meant to place e.g. resource spots within a height range * @param {array} [heightRange] - The height range in which to place the entities (An associative array with keys "min" and "max" each containing a float) * @param {array} [avoidPoints=[]] - An array of objects of the form { "x": int, "y": int, "dist": int }, points that will be avoided in the given dist e.g. start locations * @param {object} [avoidClass=undefined] - TileClass to be avoided * @param {integer} [minDistance=30] - How many tile widths the entities to place have to be away from each other, start locations and the map border * @param {array} [heightmap=g_Map.height] - The reliefmap the entities should be distributed on * @param {integer} [maxTries=2 * g_Map.size] - How often random player distributions are rolled to be compared (256 to 1024) * @param {boolean} [isCircular=g_MapSettings.CircularMap] - If the map is circular or rectangular */ function getPointsByHeight(heightRange, avoidPoints = [], avoidClass = undefined, minDistance = 20, maxTries = 2 * g_Map.size, heightmap = g_Map.height, isCircular = g_MapSettings.CircularMap) { let points = []; let placements = clone(avoidPoints); let validVertices = []; let r = 0.5 * (heightmap.length - 1); // Map center x/y as well as radius let avoidMap; if (avoidClass) avoidMap = avoidClass.inclusionCount; for (let x = minDistance; x < heightmap.length - minDistance; ++x) { for (let y = minDistance; y < heightmap[0].length - minDistance; ++y) { if (avoidClass && (avoidMap[Math.max(x - 1, 0)][y] > 0 || avoidMap[x][Math.max(y - 1, 0)] > 0 || avoidMap[Math.min(x + 1, avoidMap.length - 1)][y] > 0 || avoidMap[x][Math.min(y + 1, avoidMap[0].length - 1)] > 0)) continue; if (heightmap[x][y] > heightRange.min && heightmap[x][y] < heightRange.max && // Has correct height (!isCircular || r - Math.euclidDistance2D(x, y, r, r) >= minDistance)) // Enough distance to the map border validVertices.push({ "x": x, "y": y , "dist": minDistance}); } } for (let tries = 0; tries < maxTries; ++tries) { let point = pickRandom(validVertices); if (placements.every(p => Math.euclidDistance2D(p.x, p.y, point.x, point.y) > Math.max(minDistance, p.dist))) { points.push(point); placements.push(point); } } return points; } /** * Returns an approximation of the heights of the tiles between the vertices, a tile centered heightmap * A tile centered heightmap is one smaller in width and height than an ordinary heightmap * It is meant to e.g. texture a map by height (x/y coordinates correspond to those of the terrain texture map) * Don't use this to override g_Map height (Potentially breaks the map)! * @param {array} [heightmap=g_Map.height] - A reliefmap the tile centered version should be build from */ function getTileCenteredHeightmap(heightmap = g_Map.height) { let max_x = heightmap.length - 1; let max_y = heightmap[0].length - 1; let tchm = []; for (let x = 0; x < max_x; ++x) { tchm[x] = new Float32Array(max_y); for (let y = 0; y < max_y; ++y) tchm[x][y] = 0.25 * (heightmap[x][y] + heightmap[x + 1][y] + heightmap[x][y + 1] + heightmap[x + 1][y + 1]); } return tchm; } /** * Returns a slope map (same form as the a heightmap with one less width and height) * Not normalized. Only returns the steepness (float), not the direction of incline. * The x and y coordinates of a tile in the terrain texture map correspond to those of the slope map * @param {array} [inclineMap=getInclineMap(g_Map.height)] - A map with the absolute inclination for each tile */ function getSlopeMap(inclineMap = getInclineMap(g_Map.height)) { let max_x = inclineMap.length; let slopeMap = []; for (let x = 0; x < max_x; ++x) { let max_y = inclineMap[x].length; slopeMap[x] = new Float32Array(max_y); for (let y = 0; y < max_y; ++y) slopeMap[x][y] = Math.euclidDistance2D(0, 0, inclineMap[x][y].x, inclineMap[x][y].y); } return slopeMap; } /** * Returns an inclination map corresponding to the tiles between the heightmaps vertices: * array of heightmap width-1 arrays of height-1 vectors (associative arrays) of the form: * { "x": x_slope, "y": y_slope ] so a 2D Vector pointing to the hightest incline (with the length the incline in the vectors direction) * The x and y coordinates of a tile in the terrain texture map correspond to those of the inclination map * @param {array} [heightmap=g_Map.height] - The reliefmap the inclination map is to be generated from */ function getInclineMap(heightmap) { heightmap = (heightmap || g_Map.height); let max_x = heightmap.length - 1; let max_y = heightmap[0].length - 1; let inclineMap = []; for (let x = 0; x < max_x; ++x) { inclineMap[x] = []; for (let y = 0; y < max_y; ++y) { let dx = heightmap[x + 1][y] - heightmap[x][y]; let dy = heightmap[x][y + 1] - heightmap[x][y]; let next_dx = heightmap[x + 1][y + 1] - heightmap[x][y + 1]; let next_dy = heightmap[x + 1][y + 1] - heightmap[x + 1][y]; inclineMap[x][y] = { "x" : 0.5 * (dx + next_dx), "y" : 0.5 * (dy + next_dy) }; } } return inclineMap; } function getGrad(wrapped = true, scalarField = g_Map.height) { let vectorField = []; let max_x = scalarField.length; let max_y = scalarField[0].length; if (!wrapped) { max_x -= 1; max_y -= 1; } for (let x = 0; x < max_x; ++x) { vectorField.push([]); for (let y = 0; y < max_y; ++y) { vectorField[x].push({ "x" : scalarField[(x + 1) % max_x][y] - scalarField[x][y], "y" : scalarField[x][(y + 1) % max_y] - scalarField[x][y] }); } } return vectorField; } function splashErodeMap(strength = 1, heightmap = g_Map.height) { let max_x = heightmap.length; let max_y = heightmap[0].length; let dHeight = getGrad(heightmap); for (let x = 0; x < max_x; ++x) { let next_x = (x + 1) % max_x; let prev_x = (x + max_x - 1) % max_x; for (let y = 0; y < max_y; ++y) { let next_y = (y + 1) % max_y; let prev_y = (y + max_y - 1) % max_y; let slopes = [- dHeight[x][y].x, - dHeight[x][y].y, dHeight[prev_x][y].x, dHeight[x][prev_y].y]; let sumSlopes = 0; for (let i = 0; i < slopes.length; ++i) if (slopes[i] > 0) sumSlopes += slopes[i]; let drain = []; for (let i = 0; i < slopes.length; ++i) { drain.push(0); if (slopes[i] > 0) drain[i] += Math.min(strength * slopes[i] / sumSlopes, slopes[i]); } let sumDrain = 0; for (let i = 0; i < drain.length; ++i) sumDrain += drain[i]; // Apply changes to maps heightmap[x][y] -= sumDrain; heightmap[next_x][y] += drain[0]; heightmap[x][next_y] += drain[1]; heightmap[prev_x][y] += drain[2]; heightmap[x][prev_y] += drain[3]; } } } Index: ps/trunk/binaries/data/mods/public/maps/random/rmgen/painter.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/rmgen/painter.js (revision 21181) +++ ps/trunk/binaries/data/mods/public/maps/random/rmgen/painter.js (revision 21182) @@ -1,509 +1,524 @@ /** * @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 < 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 = 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).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)); } }; /** + * The ElevationBlendingPainter sets the elevation of each point of the given area to the weighted targetHeight. + */ +function ElevationBlendingPainter(targetHeight, strength) +{ + this.targetHeight = targetHeight; + this.strength = strength; +} + +ElevationBlendingPainter.prototype.paint = function(area) +{ + for (let point of area.points) + g_Map.setHeight(point, this.strength * this.targetHeight + (1 - this.strength) * g_Map.getHeight(point)); +}; + +/** * 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/schwarzwald.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/schwarzwald.js (revision 21181) +++ ps/trunk/binaries/data/mods/public/maps/random/schwarzwald.js (revision 21182) @@ -1,320 +1,321 @@ 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; - +g_Map.log("Locating and smoothing playerbases"); 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); + createArea( + new ClumpPlacer(diskArea(20), 0.8, 0.8, Infinity, playerPosition[i]), + new SmoothElevationPainter(ELEVATION_SET, g_Map.getHeight(playerPosition[i]), 20)); } 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 }, "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(); Index: ps/trunk/binaries/data/mods/public/maps/random/wild_lake.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/wild_lake.js (revision 21181) +++ ps/trunk/binaries/data/mods/public/maps/random/wild_lake.js (revision 21182) @@ -1,638 +1,642 @@ Engine.LoadLibrary("rmgen"); Engine.LoadLibrary("rmbiome"); Engine.LoadLibrary("heightmap"); var g_Map = new RandomMap(0, "whiteness"); /** * getArray - To ensure a terrain texture is contained within an array */ function getArray(stringOrArrayOfStrings) { if (typeof stringOrArrayOfStrings == "string") return [stringOrArrayOfStrings]; return stringOrArrayOfStrings; } setSelectedBiome(); // Terrain, entities and actors let wildLakeBiome = [ // 0 Deep water { "texture": getArray(g_Terrains.water), "actor": [[g_Gaia.fish], 0.01], "textureHS": getArray(g_Terrains.water), "actorHS": [[g_Gaia.fish], 0.03] }, // 1 Shallow water { "texture": getArray(g_Terrains.water), "actor": [[g_Decoratives.lillies, g_Decoratives.reeds], 0.3], "textureHS": getArray(g_Terrains.water), "actorHS": [[g_Decoratives.lillies], 0.1] }, // 2 Shore { "texture": getArray(g_Terrains.shore), "actor": [ [ g_Gaia.tree1, g_Gaia.tree1, g_Gaia.tree2, g_Gaia.tree2, g_Gaia.mainHuntableAnimal, g_Decoratives.grass, g_Decoratives.grass, g_Decoratives.rockMedium, g_Decoratives.rockMedium, g_Decoratives.bushMedium, g_Decoratives.bushMedium ], 0.3 ], "textureHS": getArray(g_Terrains.cliff), "actorHS": [[g_Decoratives.grassShort, g_Decoratives.rockMedium, g_Decoratives.bushSmall], 0.1] }, // 3 Low ground { "texture": getArray(g_Terrains.tier1Terrain), "actor": [ [ g_Decoratives.grass, g_Decoratives.grassShort, g_Decoratives.rockLarge, g_Decoratives.rockMedium, g_Decoratives.bushMedium, g_Decoratives.bushSmall ], 0.2 ], "textureHS": getArray(g_Terrains.cliff), "actorHS": [[g_Decoratives.grassShort, g_Decoratives.rockMedium, g_Decoratives.bushSmall], 0.1] }, // 4 Mid ground. Player and path height { "texture": getArray(g_Terrains.mainTerrain), "actor": [ [ g_Decoratives.grass, g_Decoratives.grassShort, g_Decoratives.rockLarge, g_Decoratives.rockMedium, g_Decoratives.bushMedium, g_Decoratives.bushSmall ], 0.2 ], "textureHS": getArray(g_Terrains.cliff), "actorHS": [[g_Decoratives.grassShort, g_Decoratives.rockMedium, g_Decoratives.bushSmall], 0.1] }, // 5 High ground { "texture": getArray(g_Terrains.tier2Terrain), "actor": [ [ g_Decoratives.grass, g_Decoratives.grassShort, g_Decoratives.rockLarge, g_Decoratives.rockMedium, g_Decoratives.bushMedium, g_Decoratives.bushSmall ], 0.2 ], "textureHS": getArray(g_Terrains.cliff), "actorHS": [[g_Decoratives.grassShort, g_Decoratives.rockMedium, g_Decoratives.bushSmall], 0.1] }, // 6 Lower hilltop forest border { "texture": getArray(g_Terrains.dirt), "actor": [ [ g_Gaia.tree1, g_Gaia.tree3, g_Gaia.fruitBush, g_Gaia.secondaryHuntableAnimal, g_Decoratives.grass, g_Decoratives.rockMedium, g_Decoratives.bushMedium ], 0.3 ], "textureHS": getArray(g_Terrains.cliff), "actorHS": [[g_Decoratives.grassShort, g_Decoratives.rockMedium, g_Decoratives.bushSmall], 0.1] }, // 7 Hilltop forest { "texture": getArray(g_Terrains.forestFloor1), "actor": [ [ g_Gaia.tree1, g_Gaia.tree2, g_Gaia.tree3, g_Gaia.tree4, g_Gaia.tree5, g_Decoratives.tree, g_Decoratives.grass, g_Decoratives.rockMedium, g_Decoratives.bushMedium ], 0.5 ], "textureHS": getArray(g_Terrains.cliff), "actorHS": [[g_Decoratives.grassShort, g_Decoratives.rockMedium, g_Decoratives.bushSmall], 0.1] } ]; var mercenaryCampGuards = { "generic/temperate": [ { "Template" : "structures/merc_camp_egyptian" }, { "Template" : "units/mace_infantry_javelinist_b", "Count" : 4 }, { "Template" : "units/mace_cavalry_spearman_e", "Count" : 3 }, { "Template" : "units/mace_infantry_archer_a", "Count" : 4 }, { "Template" : "units/mace_champion_infantry_a", "Count" : 3 } ], "generic/snowy": [ { "Template" : "structures/ptol_mercenary_camp" }, { "Template" : "units/brit_infantry_javelinist_b", "Count" : 4 }, { "Template" : "units/brit_cavalry_swordsman_e", "Count" : 3 }, { "Template" : "units/brit_infantry_slinger_a", "Count" : 4 }, { "Template" : "units/brit_champion_infantry", "Count" : 3 } ], "generic/desert": [ { "Template" : "structures/ptol_mercenary_camp" }, { "Template" : "units/pers_infantry_javelinist_b", "Count" : 4 }, { "Template" : "units/pers_cavalry_swordsman_e", "Count" : 3 }, { "Template" : "units/pers_infantry_archer_a", "Count" : 4 }, { "Template" : "units/pers_champion_infantry", "Count" : 3 } ], "generic/alpine": [ { "Template" : "structures/ptol_mercenary_camp" }, { "Template" : "units/rome_infantry_swordsman_b", "Count" : 4 }, { "Template" : "units/rome_cavalry_spearman_e", "Count" : 3 }, { "Template" : "units/rome_infantry_javelinist_a", "Count" : 4 }, { "Template" : "units/rome_champion_infantry", "Count" : 3 } ], "generic/mediterranean": [ { "Template" : "structures/merc_camp_egyptian" }, { "Template" : "units/iber_infantry_javelinist_b", "Count" : 4 }, { "Template" : "units/iber_cavalry_spearman_e", "Count" : 3 }, { "Template" : "units/iber_infantry_slinger_a", "Count" : 4 }, { "Template" : "units/iber_champion_infantry", "Count" : 3 } ], "generic/savanna": [ { "Template" : "structures/merc_camp_egyptian" }, { "Template" : "units/sele_infantry_javelinist_b", "Count" : 4 }, { "Template" : "units/sele_cavalry_spearman_merc_e", "Count" : 3 }, { "Template" : "units/sele_infantry_spearman_a", "Count" : 4 }, { "Template" : "units/sele_champion_infantry_swordsman", "Count" : 3 } ], "generic/tropic": [ { "Template" : "structures/merc_camp_egyptian" }, { "Template" : "units/ptol_infantry_javelinist_b", "Count" : 4 }, { "Template" : "units/ptol_cavalry_archer_e", "Count" : 3 }, { "Template" : "units/ptol_infantry_slinger_a", "Count" : 4 }, { "Template" : "units/ptol_champion_infantry_pikeman", "Count" : 3 } ], "generic/autumn": [ { "Template" : "structures/ptol_mercenary_camp" }, { "Template" : "units/gaul_infantry_javelinist_b", "Count" : 4 }, { "Template" : "units/gaul_cavalry_swordsman_e", "Count" : 3 }, { "Template" : "units/gaul_infantry_slinger_a", "Count" : 4 }, { "Template" : "units/gaul_champion_infantry", "Count" : 3 } ] }; /** * Resource spots and other points of interest */ function placeMine(position, centerEntity, decorativeActors = [ g_Decoratives.grass, g_Decoratives.grassShort, g_Decoratives.rockLarge, g_Decoratives.rockMedium, g_Decoratives.bushMedium, g_Decoratives.bushSmall ] ) { g_Map.placeEntityPassable(centerEntity, 0, position, randomAngle()); let quantity = randIntInclusive(11, 23); let dAngle = 2 * Math.PI / quantity; for (let i = 0; i < quantity; ++i) g_Map.placeEntityPassable( pickRandom(decorativeActors), 0, Vector2D.add(position, new Vector2D(randFloat(2, 5), 0).rotate(-dAngle * randFloat(i, i + 1))), randomAngle()); } // Groves, only Wood let groveActors = [g_Decoratives.grass, g_Decoratives.rockMedium, g_Decoratives.bushMedium]; let clGrove = g_Map.createTileClass(); let clGaiaCamp = g_Map.createTileClass(); function placeGrove(point, groveEntities = [ g_Gaia.tree1, g_Gaia.tree1, g_Gaia.tree1, g_Gaia.tree1, g_Gaia.tree1, g_Gaia.tree2, g_Gaia.tree2, g_Gaia.tree2, g_Gaia.tree2, g_Gaia.tree3, g_Gaia.tree3, g_Gaia.tree3, g_Gaia.tree4, g_Gaia.tree4, g_Gaia.tree5 ], groveActors = [g_Decoratives.grass, g_Decoratives.rockMedium, g_Decoratives.bushMedium], groveTileClass = undefined, groveTerrainTexture = getArray(g_Terrains.forestFloor1) ) { let position = new Vector2D(point.x, point.y); g_Map.placeEntityPassable(pickRandom(["structures/gaul_outpost", "gaia/flora_tree_oak_new"]), 0, position, randomAngle()); let quantity = randIntInclusive(20, 30); let dAngle = 2 * Math.PI / quantity; for (let i = 0; i < quantity; ++i) { let angle = dAngle * randFloat(i, i + 1); let dist = randFloat(2, 5); let objectList = groveEntities; if (i % 3 == 0) objectList = groveActors; let pos = Vector2D.add(position, new Vector2D(dist, 0).rotate(-angle)); g_Map.placeEntityPassable(pickRandom(objectList), 0, pos, randomAngle()); let painters = [new TerrainPainter(groveTerrainTexture)]; if (groveTileClass) painters.push(new TileClassPainter(groveTileClass)); createArea( new ClumpPlacer(5, 1, 1, Infinity, pos), painters); } } var farmEntities = { "generic/temperate": { "building": "structures/mace_farmstead", "animal": "gaia/fauna_pig" }, "generic/snowy": { "building": "structures/brit_farmstead", "animal": "gaia/fauna_sheep" }, "generic/desert": { "building": "structures/pers_farmstead", "animal": "gaia/fauna_camel" }, "generic/alpine": { "building": "structures/rome_farmstead", "animal": "gaia/fauna_sheep" }, "generic/mediterranean": { "building": "structures/iber_farmstead", "animal": "gaia/fauna_pig" }, "generic/savanna": { "building": "structures/sele_farmstead", "animal": "gaia/fauna_horse" }, "generic/tropic": { "building": "structures/ptol_farmstead", "animal": "gaia/fauna_camel" }, "generic/autumn": { "building": "structures/gaul_farmstead", "animal": "gaia/fauna_horse" } }; g_WallStyles.other = { "overlap": 0, "fence": readyWallElement("other/fence_long", "gaia"), "fence_short": readyWallElement("other/fence_short", "gaia"), "bench": { "angle": Math.PI / 2, "length": 1.5, "indent": 0, "bend": 0, "templateName": "other/bench" }, "foodBin": { "angle": Math.PI / 2, "length": 1.5, "indent": 0, "bend": 0, "templateName": "gaia/treasure/food_bin" }, "animal": { "angle": 0, "length": 0, "indent": 0.75, "bend": 0, "templateName": farmEntities[currentBiome()].animal }, "farmstead": { "angle": Math.PI, "length": 0, "indent": -3, "bend": 0, "templateName": farmEntities[currentBiome()].building } }; let fences = [ new Fortress("fence", [ "foodBin", "farmstead", "bench", "turn_0.25", "animal", "turn_0.25", "fence", "turn_0.25", "animal", "turn_0.25", "fence", "turn_0.25", "animal", "turn_0.25", "fence" ]), new Fortress("fence", [ "foodBin", "farmstead", "fence", "turn_0.25", "animal", "turn_0.25", "fence", "turn_0.25", "animal", "turn_0.25", "bench", "animal", "fence", "turn_0.25", "animal", "turn_0.25", "fence" ]), new Fortress("fence", [ "foodBin", "farmstead", "turn_0.5", "bench", "turn_-0.5", "fence_short", "turn_0.25", "animal", "turn_0.25", "fence", "turn_0.25", "animal", "turn_0.25", "fence", "turn_0.25", "animal", "turn_0.25", "fence_short", "animal", "fence" ]), new Fortress("fence", [ "foodBin", "farmstead", "turn_0.5", "fence_short", "turn_-0.5", "bench", "turn_0.25", "animal", "turn_0.25", "fence", "turn_0.25", "animal", "turn_0.25", "fence", "turn_0.25", "animal", "turn_0.25", "fence_short", "animal", "fence" ]), new Fortress("fence", [ "foodBin", "farmstead", "fence", "turn_0.25", "animal", "turn_0.25", "bench", "animal", "fence", "turn_0.25", "animal", "turn_0.25", "fence_short", "animal", "fence", "turn_0.25", "animal", "turn_0.25", "fence_short", "animal", "fence" ]) ]; let num = fences.length; for (let i = 0; i < num; ++i) fences.push(new Fortress("fence", clone(fences[i].wall).reverse())); // Camps with fire and gold treasure function placeCamp(position, centerEntity = "actor|props/special/eyecandy/campfire.xml", otherEntities = ["gaia/treasure/metal", "gaia/treasure/standing_stone", "units/brit_infantry_slinger_b", "units/brit_infantry_javelinist_b", "units/gaul_infantry_slinger_b", "units/gaul_infantry_javelinist_b", "units/gaul_champion_fanatic", "actor|props/special/common/waypoint_flag.xml", "actor|props/special/eyecandy/barrel_a.xml", "actor|props/special/eyecandy/basket_celt_a.xml", "actor|props/special/eyecandy/crate_a.xml", "actor|props/special/eyecandy/dummy_a.xml", "actor|props/special/eyecandy/handcart_1.xml", "actor|props/special/eyecandy/handcart_1_broken.xml", "actor|props/special/eyecandy/sack_1.xml", "actor|props/special/eyecandy/sack_1_rough.xml" ] ) { g_Map.placeEntityPassable(centerEntity, 0, position, randomAngle()); let quantity = randIntInclusive(5, 11); let dAngle = 2 * Math.PI / quantity; for (let i = 0; i < quantity; ++i) { let angle = dAngle * randFloat(i, i + 1); let dist = randFloat(1, 3); g_Map.placeEntityPassable(pickRandom(otherEntities), 0, Vector2D.add(position, new Vector2D(dist, 0).rotate(-angle)), randomAngle()); } addCivicCenterAreaToClass(position, clGaiaCamp); } function placeStartLocationResources( point, foodEntities = [g_Gaia.fruitBush, g_Gaia.chicken], groveEntities = [ g_Gaia.tree1, g_Gaia.tree1, g_Gaia.tree1, g_Gaia.tree1, g_Gaia.tree1, g_Gaia.tree2, g_Gaia.tree2, g_Gaia.tree2, g_Gaia.tree2, g_Gaia.tree3, g_Gaia.tree3, g_Gaia.tree3, g_Gaia.tree4, g_Gaia.tree4, g_Gaia.tree5 ], groveTerrainTexture = getArray(g_Terrains.forestFloor1), averageDistToCC = 10, dAverageDistToCC = 2 ) { function getRandDist() { return averageDistToCC + randFloat(-dAverageDistToCC, dAverageDistToCC); } let currentAngle = randomAngle(); // Stone let dAngle = 4/9 * Math.PI; let angle = currentAngle + randFloat(dAngle / 4, 3 * dAngle / 4); placeMine(Vector2D.add(point, new Vector2D(averageDistToCC, 0).rotate(-angle)), g_Gaia.stoneLarge); currentAngle += dAngle; // Wood let quantity = 80; dAngle = 2/3 * Math.PI / quantity; for (let i = 0; i < quantity; ++i) { angle = currentAngle + randFloat(0, dAngle); let dist = getRandDist(); let objectList = groveEntities; if (i % 2 == 0) objectList = groveActors; let position = Vector2D.add(point, new Vector2D(dist, 0).rotate(-angle)); g_Map.placeEntityPassable(pickRandom(objectList), 0, position, randomAngle()); createArea( new ClumpPlacer(5, 1, 1, Infinity, position), [ new TerrainPainter(groveTerrainTexture), new TileClassPainter(clGrove) ]); currentAngle += dAngle; } // Metal dAngle = 4/9 * Math.PI; angle = currentAngle + randFloat(dAngle / 4, 3 * dAngle / 4); placeMine(Vector2D.add(point, new Vector2D(averageDistToCC, 0).rotate(-angle)), g_Gaia.metalLarge); currentAngle += dAngle; // Berries and domestic animals quantity = 15; dAngle = 4/9 * Math.PI / quantity; for (let i = 0; i < quantity; ++i) { angle = currentAngle + randFloat(0, dAngle); let dist = getRandDist(); g_Map.placeEntityPassable(pickRandom(foodEntities), 0, Vector2D.add(point, new Vector2D(dist, 0).rotate(-angle)), randomAngle()); currentAngle += dAngle; } } /** * Base terrain shape generation and settings */ // Height range by map size let heightScale = (g_Map.size + 256) / 768 / 4; let heightRange = { "min": MIN_HEIGHT * heightScale, "max": MAX_HEIGHT * heightScale }; // Water coverage let averageWaterCoverage = 1/5; // NOTE: Since terrain generation is quite unpredictable actual water coverage might vary much with the same value let heightSeaGround = -MIN_HEIGHT + heightRange.min + averageWaterCoverage * (heightRange.max - heightRange.min); // Water height in environment and the engine let heightSeaGroundAdjusted = heightSeaGround + MIN_HEIGHT; // Water height as terrain height setWaterHeight(heightSeaGround); // Generate base terrain shape let lowH = heightRange.min; let medH = (heightRange.min + heightRange.max) / 2; // Lake let initialHeightmap = [ [medH, medH, medH, medH, medH, medH], [medH, medH, medH, medH, medH, medH], [medH, medH, lowH, lowH, medH, medH], [medH, medH, lowH, lowH, medH, medH], [medH, medH, medH, medH, medH, medH], [medH, medH, medH, medH, medH, medH], ]; if (g_Map.size < 256) { initialHeightmap = [ [medH, medH, medH, medH, medH], [medH, medH, medH, medH, medH], [medH, medH, lowH, medH, medH], [medH, medH, medH, medH, medH], [medH, medH, medH, medH, medH] ]; } if (g_Map.size >= 384) { initialHeightmap = [ [medH, medH, medH, medH, medH, medH, medH, medH], [medH, medH, medH, medH, medH, medH, medH, medH], [medH, medH, medH, medH, medH, medH, medH, medH], [medH, medH, medH, lowH, lowH, medH, medH, medH], [medH, medH, medH, lowH, lowH, medH, medH, medH], [medH, medH, medH, medH, medH, medH, medH, medH], [medH, medH, medH, medH, medH, medH, medH, medH], [medH, medH, medH, medH, medH, medH, medH, medH], ]; } setBaseTerrainDiamondSquare(heightRange.min, heightRange.max, initialHeightmap, 0.8); -// Apply simple erosion +g_Map.log("Eroding map"); for (let i = 0; i < 5; ++i) splashErodeMap(0.1); +g_Map.log("Smoothing map"); createArea( new MapBoundsPlacer(), new SmoothingPainter(1, 0.8, 1)); -// Final rescale +g_Map.log("Rescaling map"); rescaleHeightmap(heightRange.min, heightRange.max); Engine.SetProgress(25); /** * Prepare terrain texture placement */ let heighLimits = [ heightRange.min + 3/4 * (heightSeaGroundAdjusted - heightRange.min), // 0 Deep water heightSeaGroundAdjusted, // 1 Shallow water heightSeaGroundAdjusted + 2/8 * (heightRange.max - heightSeaGroundAdjusted), // 2 Shore heightSeaGroundAdjusted + 3/8 * (heightRange.max - heightSeaGroundAdjusted), // 3 Low ground heightSeaGroundAdjusted + 4/8 * (heightRange.max - heightSeaGroundAdjusted), // 4 Player and path height heightSeaGroundAdjusted + 6/8 * (heightRange.max - heightSeaGroundAdjusted), // 5 High ground heightSeaGroundAdjusted + 7/8 * (heightRange.max - heightSeaGroundAdjusted), // 6 Lower forest border heightRange.max // 7 Forest ]; let playerHeightRange = { "min" : heighLimits[3], "max" : heighLimits[4] }; let resourceSpotHeightRange = { "min" : (heighLimits[2] + heighLimits[3]) / 2, "max" : (heighLimits[4] + heighLimits[5]) / 2 }; let playerHeight = (playerHeightRange.min + playerHeightRange.max) / 2; // Average player height g_Map.log("Chosing starting locations"); let [playerIDs, playerPosition] = sortPlayersByLocation(getStartLocationsByHeightmap(playerHeightRange, 1000, 30)); -Engine.SetProgress(30); +g_Map.log("Smoothing starting locations before height calculation"); +for (let position of playerPosition) + createArea( + new ClumpPlacer(diskArea(20), 0.8, 0.8, Infinity, position), + new SmoothElevationPainter(ELEVATION_SET, g_Map.getHeight(position), 20)); -/** - * Smooth Start Locations before height region calculation - */ -let playerBaseRadius = 35; -if (g_Map.size < 256) - playerBaseRadius = 25; -for (let p = 0; p < playerIDs.length; ++p) - rectangularSmoothToHeight(playerPosition[p], playerBaseRadius, playerBaseRadius, playerHeight, 0.7); +Engine.SetProgress(30); /** * Calculate tile centered height map after start position smoothing but before placing paths * This has nothing to to with TILE_CENTERED_HEIGHT_MAP which should be false! */ let tchm = getTileCenteredHeightmap(); g_Map.log("Get points per height"); let areas = heighLimits.map(heightLimit => []); for (let x = 0; x < tchm.length; ++x) for (let y = 0; y < tchm[0].length; ++y) { let minHeight = heightRange.min; for (let h = 0; h < heighLimits.length; ++h) { if (tchm[x][y] >= minHeight && tchm[x][y] <= heighLimits[h]) { areas[h].push(new Vector2D(x, y)); break; } minHeight = heighLimits[h]; } } g_Map.log("Get slope limits per heightrange"); let slopeMap = getSlopeMap(); let minSlope = []; let maxSlope = []; for (let h = 0; h < heighLimits.length; ++h) { minSlope[h] = Infinity; maxSlope[h] = 0; for (let point of areas[h]) { let slope = slopeMap[point.x][point.y]; if (slope > maxSlope[h]) maxSlope[h] = slope; if (slope < minSlope[h]) minSlope[h] = slope; } } g_Map.log("Paint areas by height and slope"); for (let h = 0; h < heighLimits.length; ++h) for (let point of areas[h]) { let actor; let texture = pickRandom(wildLakeBiome[h].texture); if (slopeMap[point.x][point.y] < (minSlope[h] + maxSlope[h]) / 2) { if (randBool(wildLakeBiome[h].actor[1])) actor = pickRandom(wildLakeBiome[h].actor[0]); } else { texture = pickRandom(wildLakeBiome[h].textureHS); if (randBool(wildLakeBiome[h].actorHS[1])) actor = pickRandom(wildLakeBiome[h].actorHS[0]); } g_Map.setTexture(point, texture); if (actor) g_Map.placeEntityAnywhere(actor, 0, randomPositionOnTile(point), randomAngle()); } Engine.SetProgress(80); g_Map.log("Placing resources"); let avoidPoints = playerPosition.map(pos => pos.clone()); for (let i = 0; i < avoidPoints.length; ++i) avoidPoints[i].dist = 30; let resourceSpots = getPointsByHeight(resourceSpotHeightRange, avoidPoints).map(point => new Vector2D(point.x, point.y)); Engine.SetProgress(55); g_Map.log("Placing players"); if (isNomad()) placePlayersNomad( g_Map.createTileClass(), [ new HeightConstraint(playerHeightRange.min, playerHeightRange.max), avoidClasses(clGaiaCamp, 8) ]); else for (let p = 0; p < playerIDs.length; ++p) { placeCivDefaultStartingEntities(playerPosition[p], playerIDs[p], g_Map.size > 192); placeStartLocationResources(playerPosition[p]); } let mercenaryCamps = isNomad() ? 0 : Math.ceil(g_Map.size / 256); g_Map.log("Placing at most " + mercenaryCamps + " mercenary camps"); for (let i = 0; i < resourceSpots.length; ++i) { + let radius; let choice = i % 5; if (choice == 0) placeMine(resourceSpots[i], g_Gaia.stoneLarge); if (choice == 1) placeMine(resourceSpots[i], g_Gaia.metalLarge); if (choice == 2) placeGrove(resourceSpots[i]); if (choice == 3) { placeCamp(resourceSpots[i]); - rectangularSmoothToHeight(resourceSpots[i], 5, 5, g_Map.getHeight(resourceSpots[i]) - 10, 0.5); + radius = 5; } if (choice == 4) { if (mercenaryCamps) { placeStartingEntities(resourceSpots[i], 0, mercenaryCampGuards[currentBiome()]); - rectangularSmoothToHeight(resourceSpots[i], 15, 15, g_Map.getHeight(resourceSpots[i]), 0.5); + radius = 15; --mercenaryCamps; } else { placeCustomFortress(resourceSpots[i], pickRandom(fences), "other", 0, randomAngle()); - rectangularSmoothToHeight(resourceSpots[i], 10, 10, g_Map.getHeight(resourceSpots[i]), 0.5); + radius = 10; } } + + if (radius) + createArea( + new ClumpPlacer(diskArea(radius), 1, 1, Infinity, resourceSpots[i]), + new SmoothElevationPainter(ELEVATION_SET, g_Map.getHeight(resourceSpots[i]), radius)); } g_Map.ExportMap();