Index: ps/trunk/binaries/data/mods/public/maps/random/ardennes_forest.json =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/ardennes_forest.json (revision 25036) +++ ps/trunk/binaries/data/mods/public/maps/random/ardennes_forest.json (revision 25037) @@ -1,9 +1,9 @@ { "settings" : { "Name" : "Ardennes Forest", "Script" : "ardennes_forest.js", - "Description" : "Each player starts deep in the forest.\n\nThe Ardennes is a region of extensive forests, rolling hills and ridges formed within the Givetian Ardennes mountain range, primarily in modern day Belgium and Luxembourg. The region took its name from the ancient Silva, a vast forest in Roman times called Arduenna Silva.", + "Description" : "Each player starts deep in the forest.\n\nThe Ardennes is a region of extensive forests, rolling hills and ridges formed within the Givetian Ardennes mountain range, primarily in modern day Belgium and Luxembourg. The region took its name from the ancient Silva, a vast forest in Roman times called Arduenna Silva.", "CircularMap" : true, "Preview" : "ardennes_forest.png" } } Index: ps/trunk/binaries/data/mods/public/maps/random/caledonian_meadows.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/caledonian_meadows.js (revision 25036) +++ ps/trunk/binaries/data/mods/public/maps/random/caledonian_meadows.js (revision 25037) @@ -1,430 +1,430 @@ Engine.LoadLibrary("rmgen"); Engine.LoadLibrary("rmgen-common"); Engine.LoadLibrary("rmbiome"); Engine.LoadLibrary("heightmap"); var tGrove = "temp_grass_plants"; var tPath = "road_rome_a"; var oGroveEntities = ["structures/gaul/outpost", "gaia/tree/oak_new"]; var g_Map = new RandomMap(0, "whiteness"); /** * 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("structures/fence_long", "gaia"), "fence_short": readyWallElement("structures/fence_short", "gaia"), - "bench": { "angle": Math.PI / 2, "length": 1.5, "indent": 0, "bend": 0, "templateName": "structures/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" } + "bench": { "angle": Math.PI / 2, "length": 1.5, "indent": 0, "bend": 0, "templateName": "structures/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 +// Groves, only wood let groveEntities = ["gaia/tree/bush_temperate", "gaia/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_javelineer_b", "units/gaul/infantry_slinger_b", "units/gaul/infantry_javelineer_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/fruit/berry_01", "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/rock/temperate_large"); 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/ore/temperate_large"); 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.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/fish/generic", "actor|geology/stone_granite_boulder.xml"], 0.02], "textureHS": ["alpine_mountainside"], "actorHS": [["gaia/fish/generic"], 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/tree/pine", "gaia/tree/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/tree/pine", "gaia/tree/oak", "actor|props/flora/grass_tufts_a.xml", "gaia/fruit/berry_01", "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/tree/pine", "gaia/tree/pine", "gaia/tree/pine", "gaia/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/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] = groupPlayersCycle(getStartLocationsByHeightmap({ "min": heighLimits[4], "max": heighLimits[5] }, 1000, 30)); Engine.SetProgress(30); g_Map.log("Smoothing player locations"); for (let position of playerPosition) createArea( new DiskPlacer(35, position), new SmoothElevationPainter(ELEVATION_SET, g_Map.getHeight(position), 35)); g_Map.log("Creating paths between players"); let clPath = g_Map.createTileClass(); for (let i = 0; i < playerPosition.length; ++i) 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 % (isNomad() ? 4 : 5); if (choice == 0) placeMine(pos, "gaia/rock/temperate_large_02"); if (choice == 1) placeMine(pos, "gaia/ore/temperate_large"); 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/cantabrian_highlands.json =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/cantabrian_highlands.json (revision 25036) +++ ps/trunk/binaries/data/mods/public/maps/random/cantabrian_highlands.json (revision 25037) @@ -1,9 +1,9 @@ { "settings" : { "Name" : "Cantabrian Highlands", "Script" : "cantabrian_highlands.js", - "Description" : "Each player starts on a hill surrounded by steep cliffs. Represents Cantabria, a mountainous region in the North of the Iberian peninsula.", + "Description" : "Each player starts on a hill surrounded by steep cliffs. Represents Cantabria, a mountainous region in the north of the Iberian peninsula.", "Preview" : "cantabrian_highlands.png", "CircularMap" : true } } Index: ps/trunk/binaries/data/mods/public/maps/random/deep_forest.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/deep_forest.js (revision 25036) +++ ps/trunk/binaries/data/mods/public/maps/random/deep_forest.js (revision 25037) @@ -1,201 +1,201 @@ Engine.LoadLibrary("rmgen"); Engine.LoadLibrary("rmgen-common"); var templateStone = "gaia/rock/temperate_small"; var templateStoneMine = "gaia/rock/temperate_large"; var templateMetalMine = "gaia/ore/temperate_large"; var templateTemple = "gaia/ruins/unfinished_greek_temple"; var terrainPrimary = ["temp_grass", "temp_grass_b", "temp_grass_c", "temp_grass_d", "temp_grass_long_b", "temp_grass_clovers_2", "temp_grass_mossy", "temp_grass_plants"]; var terrainWood = ['temp_grass_mossy|gaia/tree/oak', 'temp_forestfloor_pine|gaia/tree/pine', 'temp_mud_plants|gaia/tree/dead', - 'temp_plants_bog|gaia/tree/oak_large', "temp_dirt_gravel_plants|gaia/tree/aleppo_pine", 'temp_forestfloor_autumn|gaia/tree/carob']; //'temp_forestfloor_autumn|gaia/fruit/fig' + 'temp_plants_bog|gaia/tree/oak_large', "temp_dirt_gravel_plants|gaia/tree/aleppo_pine", 'temp_forestfloor_autumn|gaia/tree/carob']; var terrainWoodBorder = ['temp_grass_plants|gaia/tree/euro_beech', 'temp_grass_mossy|gaia/tree/poplar', 'temp_grass_mossy|gaia/tree/poplar_lombardy', 'temp_grass_long|gaia/tree/bush_temperate', 'temp_mud_plants|gaia/tree/bush_temperate', 'temp_mud_plants|gaia/tree/bush_badlands', 'temp_grass_long|gaia/fruit/apple', 'temp_grass_clovers|gaia/fruit/berry_01', 'temp_grass_clovers_2|gaia/fruit/grapes', 'temp_grass_plants|gaia/fauna_deer', "temp_grass_long_b|gaia/fauna_rabbit", "temp_grass_plants"]; var terrainBase = ["temp_dirt_gravel", "temp_grass_b"]; var terrainBaseBorder = ["temp_grass_b", "temp_grass_b", "temp_grass", "temp_grass_c", "temp_grass_mossy"]; var terrainBaseCenter = ['temp_dirt_gravel', 'temp_dirt_gravel', 'temp_grass_b']; var terrainPath = ['temp_road', "temp_road_overgrown", 'temp_grass_b']; var terrainHill = ["temp_highlands", "temp_highlands", "temp_highlands", "temp_dirt_gravel_b", "temp_cliff_a"]; var terrainHillBorder = ["temp_highlands", "temp_highlands", "temp_highlands", "temp_dirt_gravel_b", "temp_dirt_gravel_plants", "temp_highlands", "temp_highlands", "temp_highlands", "temp_dirt_gravel_b", "temp_dirt_gravel_plants", "temp_highlands", "temp_highlands", "temp_highlands", "temp_cliff_b", "temp_dirt_gravel_plants", "temp_highlands", "temp_highlands", "temp_highlands", "temp_cliff_b", "temp_dirt_gravel_plants", "temp_highlands|gaia/fauna_goat"]; var heightPath = -2; var heightLand = 0; var heightOffsetRandomPath = 1; var g_Map = new RandomMap(heightLand, terrainPrimary); var mapSize = g_Map.getSize(); var mapRadius = mapSize/2; var mapCenter = g_Map.getCenter(); var clPlayer = g_Map.createTileClass(); var clPath = g_Map.createTileClass(); var clHill = g_Map.createTileClass(); var clForest = g_Map.createTileClass(); var clBaseResource = g_Map.createTileClass(); var numPlayers = getNumPlayers(); var baseRadius = 20; var minPlayerRadius = Math.min(mapRadius - 1.5 * baseRadius, 5/8 * mapRadius); var maxPlayerRadius = Math.min(mapRadius - baseRadius, 3/4 * mapRadius); var playerPosition = []; var playerAngle = []; var playerAngleStart = randomAngle(); var playerAngleAddAvrg = 2 * Math.PI / numPlayers; var playerAngleMaxOff = playerAngleAddAvrg/4; var radiusEC = Math.max(mapRadius/8, baseRadius/2); var resourceRadius = fractionToTiles(1/3); var resourcePerPlayer = [templateStone, templateMetalMine]; -// For large maps there are memory errors with too many trees. A density of 256*192/mapArea works with 0 players. +// For large maps there are memory errors with too many trees. A density of 256x192/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 var playerIDs = sortAllPlayers(); for (var i=0; i < numPlayers; i++) { playerAngle[i] = (playerAngleStart + i * playerAngleAddAvrg + randFloat(0, playerAngleMaxOff)) % (2 * Math.PI); playerPosition[i] = Vector2D.add(mapCenter, new Vector2D(randFloat(minPlayerRadius, maxPlayerRadius), 0).rotate(-playerAngle[i]).round()); } Engine.SetProgress(10); placePlayerBases({ "PlayerPlacement": [playerIDs, playerPosition], "BaseResourceClass": clBaseResource, // player class painted below "CityPatch": { "radius": 0.8 * baseRadius, "smoothness": 1/8, "painters": [ new LayeredPainter([terrainBaseBorder, terrainBase, terrainBaseCenter], [baseRadius/4, baseRadius/4]), new TileClassPainter(clPlayer) ] }, "Chicken": { }, "Berries": { "template": "gaia/fruit/grapes", "minCount": 2, "maxCount": 2, "distance": 12, "minDist": 5, "maxDist": 8 }, "Mines": { "types": [ { "template": templateMetalMine }, { "template": templateStoneMine } ], "minAngle": Math.PI / 2, "maxAngle": Math.PI }, "Trees": { "template": "gaia/tree/oak_large", "count": 2 } }); Engine.SetProgress(30); 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.25, baseRadius / 2, pathBlending), [ new TerrainPainter(terrainPath), new SmoothElevationPainter(ELEVATION_SET, heightPath, 2, heightOffsetRandomPath), new TileClassPainter(clPath) ], avoidClasses(clBaseResource, 4)); } Engine.SetProgress(50); g_Map.log("Placing expansion resources"); for (let i = 0; i < numPlayers; ++i) for (let rIndex = 0; rIndex < resourcePerPlayer.length; ++rIndex) { let angleDist = numPlayers > 1 ? (playerAngle[(i + 1) % numPlayers] - playerAngle[i] + 2 * Math.PI) % (2 * Math.PI) : 2 * Math.PI; // they are supposed to be in between players on the same radius let angle = playerAngle[i] + angleDist * (rIndex + 1) / (resourcePerPlayer.length + 1); let position = Vector2D.add(mapCenter, new Vector2D(resourceRadius, 0).rotate(-angle)).round(); g_Map.placeEntityPassable(resourcePerPlayer[rIndex], 0, position, randomAngle()); createArea( new ClumpPlacer(40, 1/2, 1/8, Infinity, position), [ new LayeredPainter([terrainHillBorder, terrainHill], [1]), new ElevationPainter(randFloat(1, 2)), new TileClassPainter(clHill) ]); } Engine.SetProgress(60); g_Map.log("Placing temple"); g_Map.placeEntityPassable(templateTemple, 0, mapCenter, randomAngle()); clBaseResource.add(mapCenter); g_Map.log("Creating central mountain"); createArea( new ClumpPlacer(Math.square(radiusEC), 1/2, 1/8, Infinity, mapCenter), [ new LayeredPainter([terrainHillBorder, terrainHill], [radiusEC/4]), new ElevationPainter(randFloat(1, 2)), new TileClassPainter(clHill) ]); -// Woods and general hight map +// Woods and general height map for (var x = 0; x < mapSize; x++) for (var z = 0; z < mapSize; z++) { let position = new Vector2D(x, z); // The 0.5 is a correction for the entities placed on the center of tiles var radius = mapCenter.distanceTo(Vector2D.add(position, new Vector2D(0.5, 0.5))); var minDistToSL = mapSize; for (var 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 tDensFactEC = Math.max(Math.min((radius - radiusEC) / radiusEC, 1), 0); var tDensActual = maxTreeDensity * tDensFactSL * tDensFactRad * tDensFactEC; if (randBool(tDensActual) && g_Map.validTile(position)) { let border = tDensActual < randFloat(0, bushChance * maxTreeDensity); if (avoidClasses(clPath, 1, clHill, border ? 0 : 1).allows(position)) { createTerrain(border ? terrainWoodBorder : terrainWood).place(position); g_Map.setHeight(position, randFloat(0, 1)); clForest.add(position); } } // General height map let hVarMiddleHill = fractionToTiles(1 / 64) * (1 + Math.cos(3/2 * Math.PI * radius / mapRadius)); var hVarHills = 5 * (1 + Math.sin(x / 10) * Math.sin(z / 10)); g_Map.setHeight(position, g_Map.getHeight(position) + hVarMiddleHill + hVarHills + 1); } Engine.SetProgress(95); placePlayersNomad(clPlayer, avoidClasses(clForest, 1, clBaseResource, 4, clHill, 4)); g_Map.ExportMap(); Index: ps/trunk/binaries/data/mods/public/maps/random/dodecanese.json =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/dodecanese.json (revision 25036) +++ ps/trunk/binaries/data/mods/public/maps/random/dodecanese.json (revision 25037) @@ -1,10 +1,10 @@ { "settings" : { "Name" : "Dodecanese", "Script" : "dodecanese.js", - "Description" : "Controlling access to the Aegean Sea from the East, the Dodecanese have been subject to numerous yet short-lived invasions. Ultimately consolidating power with Rhodes at their lead, these islands developed into great maritime, commercial and cultural centers. Will you acheive the same?", + "Description" : "Controlling access to the Aegean Sea from the east, the Dodecanese have been subject to numerous yet short-lived invasions. Ultimately consolidating power with Rhodes at their lead, these islands developed into great maritime, commercial and cultural centers. Will you achieve the same?", "Keywords": ["naval"], "Preview" : "dodecanese.png", "CircularMap" : true } } Index: ps/trunk/binaries/data/mods/public/maps/random/fields_of_meroe.json =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/fields_of_meroe.json (revision 25036) +++ ps/trunk/binaries/data/mods/public/maps/random/fields_of_meroe.json (revision 25037) @@ -1,11 +1,11 @@ { "settings" : { "Name" : "Fields of Meroë", "Script" : "fields_of_meroe.js", - "Description" : "The \"Island of Meroë\", a vast peninsula flanked by the Nile and Atbarah rivers, formed the heartland of ancient Kush. Where the harsh deserts start making way for the semi-arid savannahs and small acacia forests dot the landscape. The area is rich in resources and the ever-present Nile brings life, but grave threats loom on the opposite riverbank.", + "Description" : "The “Island of Meroë”, a vast peninsula flanked by the Nile and Atbarah rivers, formed the heartland of ancient Kush. Where the harsh deserts start making way for the semi-arid savannahs and small acacia forests dot the landscape. The area is rich in resources and the ever-present Nile brings life, but grave threats loom on the opposite riverbank.", "Preview" : "fields_of_meroe_dry.png", "Keywords": [], "CircularMap" : true, "SupportedBiomes": "fields_of_meroe/" } } Index: ps/trunk/binaries/data/mods/public/maps/random/heightmap/heightmap.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/heightmap/heightmap.js (revision 25036) +++ ps/trunk/binaries/data/mods/public/maps/random/heightmap/heightmap.js (revision 25037) @@ -1,414 +1,414 @@ /** * 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 range with 2 floats in properties "min" and "max" */ function getMinAndMaxHeight(heightmap = g_Map.height) { let height = { "min": Infinity, "max": -Infinity }; for (let x = 0; x < heightmap.length; ++x) for (let y = 0; y < heightmap[x].length; ++y) { height.min = Math.min(height.min, heightmap[x][y]); height.max = Math.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 {number} [minHeight=MIN_HEIGHT] - Minimum height that should be used for the resulting heightmap * @param {number} [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); for (let x = 0; x < heightmap.length; ++x) for (let y = 0; y < heightmap[x].length; ++y) heightmap[x][y] = minHeight + (heightmap[x][y] - oldHeightRange.min) / (oldHeightRange.max - oldHeightRange.min) * (maxHeight - minHeight); return heightmap; } /** * Translates the heightmap by the given vector, i.e. moves the heights in that direction. * * @param {Vector2D} offset - A vector indicating direction and distance. * @param {number} [defaultHeight] - The elevation to be set for vertices that don't have a corresponding location on the source heightmap. * @param {Array} [heightmap=g_Map.height] - A reliefmap */ function translateHeightmap(offset, defaultHeight = undefined, heightmap = g_Map.height) { if (defaultHeight === undefined) defaultHeight = getMinAndMaxHeight(heightmap).min; offset.round(); let sourceHeightmap = clone(heightmap); for (let x = 0; x < heightmap.length; ++x) for (let y = 0; y < heightmap[x].length; ++y) heightmap[x][y] = sourceHeightmap[x + offset.x] !== undefined && sourceHeightmap[x + offset.x][y + offset.y] !== undefined ? sourceHeightmap[x + offset.x][y + offset.y] : defaultHeight; return heightmap; } /** * 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 {number} [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 {number} [minHeight=MIN_HEIGHT] - Lower limit of the random height to be rolled * @param {number} [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 {number} [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]]; return heightmap; } /** * 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[x].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}); + 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 + * { "x": x_slope, "y": y_slope } - A 2D vector pointing to the highest incline (with the length the inclination in the vector's 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) }; + 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] + "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 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]; } } return heightmap; } Index: ps/trunk/binaries/data/mods/public/maps/random/jebel_barkal.json =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/jebel_barkal.json (revision 25036) +++ ps/trunk/binaries/data/mods/public/maps/random/jebel_barkal.json (revision 25037) @@ -1,23 +1,23 @@ { "settings" : { "Name" : "Jebel Barkal", "Script" : "jebel_barkal.js", - "Description" : "Starting near the fertile banks of the Nile, the players besiege the heavily defended city Napata which lies at the foot of the hill Jebel Barkal, the \"Pure Mountain\". It is the Southern home of Amun, and according to Kushites and Egyptians alike, the birthplace of man. Known as the Throne of the Two Lands, the ancient religious capital of Napata lay in its shadow. This is where Kings were made... and unmade! Abutting a rich floodplain downstream from the 4th cataract, this area became the breadbasket of ancient Kush.", + "Description" : "Starting near the fertile banks of the Nile, the players besiege the heavily defended city Napata, which lies at the foot of the hill Jebel Barkal, the “Pure Mountain”. It is the southern home of Amun and, according to Kushites and Egyptians alike, the birthplace of man. Known as the “Throne of the Two Lands”, the ancient religious capital of Napata lay in its shadow. This is where kings were made – and unmade! Abutting a rich floodplain downstream from the Fourth Cataract, this area became the breadbasket of ancient Kush.", "Preview" : "jebel_barkal.png", "Keywords": ["trigger"], "CircularMap": true, "TriggerScripts" : [ "scripts/TriggerHelper.js", "random/jebel_barkal_triggers.js" ], "SupportedTriggerDifficulties": { "Values": [ "Very Easy", "Easy", "Medium", "Hard", "Very Hard" ] } } } Index: ps/trunk/binaries/data/mods/public/maps/random/rmbiome/generic/snowy.json =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/rmbiome/generic/snowy.json (revision 25036) +++ ps/trunk/binaries/data/mods/public/maps/random/rmbiome/generic/snowy.json (revision 25037) @@ -1,80 +1,80 @@ { "Description": { "Title": "Snowy", - "Description": "Settle in the cold regions of the North, the native habitat of the wooly muskox. Here you can pine away to your content and also hunt the occasional walrus or two." + "Description": "Settle in the cold regions of the Arctic, the native habitat of the muskox. Here you can pine away to your content and also hunt the occasional walrus or two." }, "Environment": { "SunColor": { "r": 0.550, "g": 0.601, "b": 0.644 }, "Water": { "WaterBody": { "Color": { "r": 0.067, "g": 0.212, "b": 0.361 }, "Tint": { "r": 0.4, "g": 0.486, "b": 0.765 }, "Murkiness": 0.83, "Waviness": 5.5 } }, "Fog": { "FogThickness": 0.21, "FogFactor": 0.006 }, "Postproc": { "PostprocEffect": "hdr", "Saturation": 0.74 } }, "Terrains": { "mainTerrain": [ "polar_snow_b", "snow grass 75", "snow rocks", "snow forest" ], "forestFloor1": "polar_tundra_snow", "forestFloor2": "polar_tundra_snow", "cliff": [ "alpine_cliff_a", "alpine_cliff_b" ], "tier1Terrain": "polar_snow_a", "tier2Terrain": "polar_ice_snow", "tier3Terrain": "polar_ice", "tier4Terrain": "snow grass 2", "hill": [ "polar_snow_rocks", "polar_cliff_snow" ], "dirt": "snow grass 2", "road": "new_alpine_citytile", "roadWild": "polar_ice_cracked", "shoreBlend": "polar_ice", "shore": "alpine_shore_rocks_icy", "water": "alpine_shore_rocks" }, "Gaia": { "tree1": "gaia/tree/pine_w", "tree2": "gaia/tree/pine_w", "tree3": "gaia/tree/pine_w", "tree4": "gaia/tree/pine_w", "tree5": "gaia/tree/pine", "fruitBush": "gaia/fruit/berry_01", "chicken": "gaia/fauna_chicken", "mainHuntableAnimal": "gaia/fauna_muskox", "fish": "gaia/fish/tuna", "secondaryHuntableAnimal": "gaia/fauna_walrus", "stoneLarge": "gaia/rock/alpine_large", "stoneSmall": "gaia/rock/alpine_small", "metalLarge": "gaia/ore/alpine_large", "metalSmall": "gaia/ore/alpine_small" }, "Decoratives": { "grass": "actor|props/flora/grass_soft_dry_small_tall.xml", "grassShort": "actor|props/flora/grass_soft_dry_large.xml", "reeds": "actor|props/flora/reeds_pond_dry.xml", "lillies": "actor|geology/stone_granite_large.xml", "rockLarge": "actor|geology/stone_granite_large.xml", "rockMedium": "actor|geology/stone_granite_med.xml", "bushMedium": "actor|props/flora/bush_desert_dry_a.xml", "bushSmall": "actor|props/flora/bush_desert_dry_a.xml", "tree": "actor|flora/trees/pine_w.xml" } } Index: ps/trunk/binaries/data/mods/public/maps/random/schwarzwald.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/schwarzwald.js (revision 25036) +++ ps/trunk/binaries/data/mods/public/maps/random/schwarzwald.js (revision 25037) @@ -1,322 +1,322 @@ Engine.LoadLibrary("rmgen"); Engine.LoadLibrary("rmgen-common"); 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/rock/alpine_large'; var oMetalLarge = 'gaia/ore/alpine_large'; var oFish = "gaia/fish/generic"; 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/tree/oak', 'alpine_forrestfloor|gaia/tree/pine']; var terrainWoodBorder = ['new_alpine_grass_mossy|gaia/tree/oak', 'alpine_forrestfloor|gaia/tree/pine', 'temp_grass_long|gaia/tree/bush_temperate', 'temp_grass_clovers|gaia/fruit/berry_01', 'temp_grass_clovers_2|gaia/fruit/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. +// Set up woods. +// For large maps there are memory errors with too many trees. A density of 256x192/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 +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 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 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 +// 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 +// 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 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(); 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/fruit/berry_01", "minCount": 2, "maxCount": 2 }, "Mines": { "types": [ { "template": oMetalLarge }, { "template": oStoneLarge } ], "distance": 15, "minAngle": Math.PI / 2, "maxAngle": Math.PI }, "Trees": { "template": "gaia/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); +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)); + 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 25036) +++ ps/trunk/binaries/data/mods/public/maps/random/wild_lake.js (revision 25037) @@ -1,643 +1,644 @@ Engine.LoadLibrary("rmgen"); Engine.LoadLibrary("rmgen-common"); 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_javelineer_b", "Count" : 4 }, - { "Template" : "units/mace/cavalry_spearman_e", "Count" : 3 }, - { "Template" : "units/mace/infantry_archer_a", "Count" : 4 }, - { "Template" : "units/mace/champion_infantry_spearman", "Count" : 3 } + { "Template": "structures/merc_camp_egyptian" }, + { "Template": "units/mace/infantry_javelineer_b", "Count": 4 }, + { "Template": "units/mace/cavalry_spearman_e", "Count": 3 }, + { "Template": "units/mace/infantry_archer_a", "Count": 4 }, + { "Template": "units/mace/champion_infantry_spearman", "Count": 3 } ], "generic/snowy": [ - { "Template" : "structures/ptol/mercenary_camp" }, - { "Template" : "units/brit/infantry_javelineer_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 } + { "Template": "structures/ptol/mercenary_camp" }, + { "Template": "units/brit/infantry_javelineer_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_javelineer_b", "Count" : 4 }, - { "Template" : "units/pers/cavalry_axeman_e", "Count" : 3 }, - { "Template" : "units/pers/infantry_archer_a", "Count" : 4 }, - { "Template" : "units/pers/champion_infantry", "Count" : 3 } + { "Template": "structures/ptol/mercenary_camp" }, + { "Template": "units/pers/infantry_javelineer_b", "Count": 4 }, + { "Template": "units/pers/cavalry_axeman_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_javelineer_a", "Count" : 4 }, - { "Template" : "units/rome/champion_infantry", "Count" : 3 } + { "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_javelineer_a", "Count": 4 }, + { "Template": "units/rome/champion_infantry", "Count": 3 } ], "generic/mediterranean": [ - { "Template" : "structures/merc_camp_egyptian" }, - { "Template" : "units/iber/infantry_javelineer_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 } + { "Template": "structures/merc_camp_egyptian" }, + { "Template": "units/iber/infantry_javelineer_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_javelineer_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 } + { "Template": "structures/merc_camp_egyptian" }, + { "Template": "units/sele/infantry_javelineer_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_javelineer_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 } + { "Template": "structures/merc_camp_egyptian" }, + { "Template": "units/ptol/infantry_javelineer_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_javelineer_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 } + { "Template": "structures/ptol/mercenary_camp" }, + { "Template": "units/gaul/infantry_javelineer_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 +// 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/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("structures/fence_long", "gaia"), "fence_short": readyWallElement("structures/fence_short", "gaia"), - "bench": { "angle": Math.PI / 2, "length": 1.5, "indent": 0, "bend": 0, "templateName": "structures/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 } + "bench": { "angle": Math.PI / 2, "length": 1.5, "indent": 0, "bend": 0, "templateName": "structures/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_javelineer_b", "units/gaul/infantry_slinger_b", "units/gaul/infantry_javelineer_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 + +// Height range by map size let heightScale = (g_Map.size + 512) / 1024 / 5; 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 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); 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.5, Math.ceil(g_Map.size/128) + 1)); 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 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] = groupPlayersCycle(getStartLocationsByHeightmap(playerHeightRange, 1000, 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)); 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(40); 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 % (isNomad() ? 4 : 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]); radius = 5; } if (choice == 4) { if (mercenaryCamps) { placeStartingEntities(resourceSpots[i], 0, mercenaryCampGuards[currentBiome()]); radius = 15; --mercenaryCamps; } else { placeCustomFortress(resourceSpots[i], pickRandom(fences), "other", 0, randomAngle()); radius = 10; } } if (radius) createArea( new DiskPlacer(radius, resourceSpots[i]), new SmoothElevationPainter(ELEVATION_SET, g_Map.getHeight(resourceSpots[i]), radius / 3)); } g_Map.ExportMap(); Index: ps/trunk/binaries/data/mods/public/maps/tutorials/introductory_tutorial.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/tutorials/introductory_tutorial.js (revision 25036) +++ ps/trunk/binaries/data/mods/public/maps/tutorials/introductory_tutorial.js (revision 25037) @@ -1,432 +1,432 @@ Trigger.prototype.tutorialGoals = [ { "instructions": markForTranslation("Welcome to the 0 A.D. tutorial."), }, { - "instructions": markForTranslation("Left-click on a female citizen and then right-click on a berry bush to make that female citizen gather food. Female citizens gather vegetables faster than other units."), + "instructions": markForTranslation("Left-click on a Female Citizen and then right-click on a berry bush to make that Female Citizen gather food. Female Citizens gather vegetables faster than other units."), "OnPlayerCommand": function(msg) { if (msg.cmd.type == "gather" && msg.cmd.target && TriggerHelper.GetResourceType(msg.cmd.target).specific == "fruit") this.NextGoal(); } }, { - "instructions": markForTranslation("Select the citizen-soldier, right-click on a tree near the Civic Center to begin gathering Wood. Citizen Soldiers gather Wood faster than female citizens."), + "instructions": markForTranslation("Select the Citizen Soldier, right-click on a tree near the Civic Center to begin gathering wood. Citizen Soldiers gather wood faster than Female Citizens."), "OnPlayerCommand": function(msg) { if (msg.cmd.type == "gather" && msg.cmd.target && TriggerHelper.GetResourceType(msg.cmd.target).specific == "tree") this.NextGoal(); } }, { "instructions": [ { "text": markForTranslation("Select the Civic Center building and hold %(hotkey)s while clicking on the Hoplite icon once to begin training a batch of Hoplites."), "hotkey": "session.batchtrain" } ], "OnTrainingQueued": function(msg) { if (msg.unitTemplate != "units/spart/infantry_spearman_b" || +msg.count == 1) { let cmpProductionQueue = Engine.QueryInterface(msg.trainerEntity, IID_ProductionQueue); cmpProductionQueue.ResetQueue(); let txt = +msg.count == 1 ? markForTranslation("Do not forget to press the batch training hotkey while clicking to produce multiple units.") : - markForTranslation("Click on the HOPLITE icon."); + markForTranslation("Click on the Hoplite icon."); this.WarningMessage(txt); return; } this.NextGoal(); } }, { - "instructions": markForTranslation("Select the two idle female citizens and build a house nearby by selecting the house icon. Place the house by left-clicking on a piece of land."), + "instructions": markForTranslation("Select the two idle Female Citizens and build a House nearby by selecting the House icon. Place the House by left-clicking on a piece of land."), "OnPlayerCommand": function(msg) { if (msg.cmd.type == "repair" && TriggerHelper.EntityMatchesClassList(msg.cmd.target, "House")) this.NextGoal(); } }, { - "instructions": markForTranslation("When they are ready, select the newly trained Hoplites and assign them to build a storehouse beside some nearby trees. They will begin to gather Wood when it's constructed."), + "instructions": markForTranslation("When they are ready, select the newly trained Hoplites and assign them to build a Storehouse beside some nearby trees. They will begin to gather wood when it's constructed."), "OnPlayerCommand": function(msg) { if (msg.cmd.type == "repair" && TriggerHelper.EntityMatchesClassList(msg.cmd.target, "Storehouse")) this.NextGoal(); } }, { "instructions": [ { "text": markForTranslation("Train a batch of Skirmishers by holding %(hotkey)s and clicking on the Skirmisher icon in the Civic Center."), "hotkey": "session.batchtrain" } ], "Init": function() { this.trainingDone = false; }, "OnTrainingQueued": function(msg) { if (msg.unitTemplate != "units/spart/infantry_javelineer_b" || +msg.count == 1) { let cmpProductionQueue = Engine.QueryInterface(msg.trainerEntity, IID_ProductionQueue); cmpProductionQueue.ResetQueue(); let txt = +msg.count == 1 ? markForTranslation("Do not forget to press the batch training hotkey while clicking to produce multiple units.") : markForTranslation("Click on the Skirmisher icon."); this.WarningMessage(txt); return; } this.NextGoal(); } }, { - "instructions": markForTranslation("Build a farmstead in an open space beside the Civic Center using any idle builders."), + "instructions": markForTranslation("Build a Farmstead in an open space beside the Civic Center using any idle builders."), "OnPlayerCommand": function(msg) { if (msg.cmd.type == "repair" && TriggerHelper.EntityMatchesClassList(msg.cmd.target, "Farmstead")) this.NextGoal(); }, "OnTrainingFinished": function(msg) { this.trainingDone = true; } }, { - "instructions": markForTranslation("Let's wait for the farmstead to be built."), + "instructions": markForTranslation("Let's wait for the Farmstead to be built."), "OnTrainingFinished": function(msg) { this.trainingDone = true; }, "OnStructureBuilt": function(msg) { if (TriggerHelper.EntityMatchesClassList(msg.building, "Farmstead")) this.NextGoal(); } }, { - "instructions": markForTranslation("Once the farmstead is constructed, its builders will automatically begin gathering food if there is any nearby. Select the builders and instead make them construct a field beside the farmstead."), + "instructions": markForTranslation("Once the Farmstead is constructed, its builders will automatically begin gathering food if there is any nearby. Select the builders and instead make them construct a Field beside the Farmstead."), "Init": function() { this.farmStarted = false; }, "IsDone": function() { return this.farmStarted && this.trainingDone; }, "OnPlayerCommand": function(msg) { if (msg.cmd.type == "repair" && TriggerHelper.EntityMatchesClassList(msg.cmd.target, "Field")) this.farmStarted = true; if (this.IsDone()) this.NextGoal(); }, "OnTrainingFinished": function(msg) { this.trainingDone = true; if (this.IsDone()) this.NextGoal(); } }, { - "instructions": markForTranslation("The field's builders will now automatically begin gathering food from the field. Using the newly created group of skirmishers, get them to build another house nearby."), + "instructions": markForTranslation("The Field's builders will now automatically begin gathering food from the Field. Using the newly created group of skirmishers, get them to build another House nearby."), "OnPlayerCommand": function(msg) { if (msg.cmd.type == "repair" && TriggerHelper.EntityMatchesClassList(msg.cmd.target, "House")) this.NextGoal(); } }, { - "instructions": markForTranslation("Train a batch of Hoplites at the Civic Center. Select the Civic Center and with it selected right-click on a tree nearby. Units from the Civic Center will now automatically gather Wood."), + "instructions": markForTranslation("Train a batch of Hoplites at the Civic Center. Select the Civic Center and with it selected right-click on a tree nearby. Units from the Civic Center will now automatically gather wood."), "Init": function() { this.rallyPointSet = false; this.trainingStarted = false; }, "IsDone": function() { return this.rallyPointSet && this.trainingStarted; }, "OnTrainingQueued": function(msg) { if (msg.unitTemplate != "units/spart/infantry_spearman_b" || +msg.count == 1) { let cmpProductionQueue = Engine.QueryInterface(msg.trainerEntity, IID_ProductionQueue); cmpProductionQueue.ResetQueue(); let txt = +msg.count == 1 ? markForTranslation("Do not forget to press the batch training hotkey while clicking to produce multiple units.") : markForTranslation("Click on the Hoplite icon."); this.WarningMessage(txt); return; } this.trainingStarted = true; if (this.IsDone()) this.NextGoal(); }, "OnPlayerCommand": function(msg) { if (msg.cmd.type != "set-rallypoint" || !msg.cmd.data || !msg.cmd.data.command || msg.cmd.data.command != "gather" || !msg.cmd.data.resourceType || msg.cmd.data.resourceType.specific != "tree") { - this.WarningMessage(markForTranslation("Select the Civic Center, then hover the cursor over the tree and right-click when you see your cursor change into a Wood icon.")); + this.WarningMessage(markForTranslation("Select the Civic Center, then hover the cursor over the tree and right-click when you see your cursor change into a wood icon.")); return; } this.rallyPointSet = true; if (this.IsDone()) this.NextGoal(); } }, { - "instructions": markForTranslation("Order the idle Skirmishers to build an outpost to the north east at the edge of your territory. This will be the fifth Village Phase structure that you have built, allowing you to advance to the Town Phase."), + "instructions": markForTranslation("Order the idle Skirmishers to build an outpost to the north east at the edge of your territory. This will be the fifth Village Phase structure that you have built, allowing you to advance to the Town Phase."), "OnPlayerCommand": function(msg) { if (msg.cmd.type == "repair" && TriggerHelper.EntityMatchesClassList(msg.cmd.target, "Outpost")) this.NextGoal(); } }, { "instructions": markForTranslation("Select the Civic Center again and advance to Town Phase by clicking on the II icon (you have to wait for the outpost to be built first). This will allow Town Phase buildings to be constructed."), "IsDone": function() { return TriggerHelper.HasDealtWithTech(this.playerID, "phase_town_generic"); }, "OnResearchQueued": function(msg) { if (msg.technologyTemplate && TriggerHelper.EntityMatchesClassList(msg.researcherEntity, "CivilCentre")) this.NextGoal(); } }, { "instructions": markForTranslation("While waiting for the phasing up, you may reassign your idle workers to gathering the resources you are short of."), "IsDone": function() { let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); let playerEnt = cmpPlayerManager.GetPlayerByID(this.playerID); let cmpTechnologyManager = Engine.QueryInterface(playerEnt, IID_TechnologyManager); return cmpTechnologyManager && cmpTechnologyManager.IsTechnologyResearched("phase_town_generic"); }, "OnResearchFinished": function(msg) { if (msg.tech == "phase_town_generic") this.NextGoal(); } }, { - "instructions": markForTranslation("Start training a batch of female citizens in the Civic Center and set its rally point to the farm (right click on it)."), + "instructions": markForTranslation("Start training a batch of Female Citizens in the Civic Center and set its rally point to the farm (right click on it)."), "Init": function() { this.rallyPointSet = false; this.trainingStarted = false; }, "IsDone": function() { return this.rallyPointSet && this.trainingStarted; }, "OnTrainingQueued": function(msg) { if (msg.unitTemplate != "units/spart/support_female_citizen" || +msg.count == 1) { let cmpProductionQueue = Engine.QueryInterface(msg.trainerEntity, IID_ProductionQueue); cmpProductionQueue.ResetQueue(); let txt = +msg.count == 1 ? markForTranslation("Do not forget to press the batch training hotkey while clicking to produce multiple units.") : - markForTranslation("Click on the female citizen icon."); + markForTranslation("Click on the Female Citizen icon."); this.WarningMessage(txt); return; } this.trainingStarted = true; if (this.IsDone()) this.NextGoal(); }, "OnPlayerCommand": function(msg) { if (msg.cmd.type != "set-rallypoint" || !msg.cmd.data || !msg.cmd.data.command || msg.cmd.data.command != "gather" || !msg.cmd.data.resourceType || msg.cmd.data.resourceType.specific != "grain") return; this.rallyPointSet = true; if (this.IsDone()) this.NextGoal(); } }, { - "instructions": markForTranslation("Build a Barracks nearby. Whenever your population limit is reached, build an extra house using any available builder units."), + "instructions": markForTranslation("Build a Barracks nearby. Whenever your population limit is reached, build an extra House using any available builder units."), "OnPlayerCommand": function(msg) { if (msg.cmd.type == "repair" && TriggerHelper.EntityMatchesClassList(msg.cmd.target, "Barracks")) this.NextGoal(); } }, { "instructions": markForTranslation("Prepare for an attack by an enemy player. Train more soldiers using the Barracks, and get idle soldiers to build a Tower near your Outpost."), "OnPlayerCommand": function(msg) { if (msg.cmd.type == "repair" && TriggerHelper.EntityMatchesClassList(msg.cmd.target, "Tower")) this.NextGoal(); } }, { "instructions": markForTranslation("Build a Forge and research the Infantry Training technology (sword icon) to improve infantry hack attack."), "OnResearchQueued": function(msg) { if (msg.technologyTemplate && TriggerHelper.EntityMatchesClassList(msg.researcherEntity, "Forge")) this.NextGoal(); } }, { "instructions": markForTranslation("The enemy is coming. Train more soldiers to fight off the enemies."), "OnResearchFinished": function(msg) { this.LaunchAttack(); this.NextGoal(); } }, { "instructions": markForTranslation("Try to repel the attack."), "OnOwnershipChanged": function(msg) { if (msg.to != INVALID_PLAYER) return; if (this.IsAttackRepelled()) this.NextGoal(); } }, { "instructions": markForTranslation("The enemy attack has been thwarted. Now build a market and a temple while you assign new units to gather required resources."), "Init": function() { this.marketStarted = false; this.templeStarted = false; }, "IsDone": function() { return this.marketStarted && this.templeStarted; }, "OnPlayerCommand": function(msg) { if (msg.cmd.type != "repair") return; this.marketStarted = this.marketStarted || TriggerHelper.EntityMatchesClassList(msg.cmd.target, "Market"); this.templeStarted = this.templeStarted || TriggerHelper.EntityMatchesClassList(msg.cmd.target, "Temple"); if (this.IsDone()) this.NextGoal(); } }, { "instructions": markForTranslation("Once you meet the City Phase requirements, select your Civic Center and advance to City Phase."), "OnResearchQueued": function(msg) { if (msg.technologyTemplate && TriggerHelper.EntityMatchesClassList(msg.researcherEntity, "CivilCentre")) this.NextGoal(); } }, { "instructions": markForTranslation("While waiting for the phase change, you may train more soldiers at the Barracks."), "OnResearchFinished": function(msg) { if (msg.tech == "phase_city_generic") this.NextGoal(); } }, { "instructions": markForTranslation("Now that you are in City Phase, build a fortress nearby (gather some stone first if needed) and then use it to construct 2 Battering Rams."), "Init": function() { this.ramCount = 0; }, "IsDone": function() { return this.ramCount > 1; }, "OnTrainingQueued": function(msg) { if (msg.unitTemplate == "units/spart/siege_ram") ++this.ramCount; if (this.IsDone()) { this.RemoveChampions(); this.NextGoal(); } } }, { "instructions": [ - markForTranslation("Stop all your soldiers gathering resources and instead task small groups to find the enemy Civic Center on the map. Once The enemy's base has been spotted, send your siege weapons and all remaining soldiers to destroy it.\n"), - markForTranslation("Female citizens should continue to gather resources.") + markForTranslation("Stop all your soldiers gathering resources and instead task small groups to find the enemy Civic Center on the map. Once the enemy's base has been spotted, send your Siege Engines and all remaining soldiers to destroy it.\n"), + markForTranslation("Female Citizens should continue to gather resources.") ], "OnOwnershipChanged": function(msg) { if (msg.from != this.enemyID) return; if (TriggerHelper.EntityMatchesClassList(msg.entity, "CivilCentre")) this.NextGoal(); } }, { "instructions": markForTranslation("The enemy has been defeated. These tutorial tasks are now completed."), } ]; Trigger.prototype.LaunchAttack = function() { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let entities = cmpRangeManager.GetEntitiesByPlayer(this.playerID); let target = entities.find(e => { let cmpIdentity = Engine.QueryInterface(e, IID_Identity); return cmpIdentity && cmpIdentity.HasClass("Tower") && Engine.QueryInterface(e, IID_Position); }) || entities.find(e => { let cmpIdentity = Engine.QueryInterface(e, IID_Identity); return cmpIdentity && cmpIdentity.HasClass("CivilCentre") && Engine.QueryInterface(e, IID_Position); }); let position = Engine.QueryInterface(target, IID_Position).GetPosition2D(); this.attackers = cmpRangeManager.GetEntitiesByPlayer(this.enemyID).filter(e => { let cmpIdentity = Engine.QueryInterface(e, IID_Identity); return Engine.QueryInterface(e, IID_UnitAI) && cmpIdentity && cmpIdentity.HasClass("CitizenSoldier"); }); ProcessCommand(this.enemyID, { "type": "attack-walk", "entities": this.attackers, "x": position.x, "z": position.y, "targetClasses": { "attack": ["Unit"] }, "allowCapture": false, "queued": false }); }; Trigger.prototype.IsAttackRepelled = function() { return !this.attackers.some(e => Engine.QueryInterface(e, IID_Health) && Engine.QueryInterface(e, IID_Health).GetHitpoints() > 0); }; Trigger.prototype.RemoveChampions = function() { let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); let champions = cmpRangeManager.GetEntitiesByPlayer(this.enemyID).filter(e => Engine.QueryInterface(e, IID_Identity).HasClass("Champion")); let keep = 6; for (let ent of champions) { let cmpHealth = Engine.QueryInterface(ent, IID_Health); if (!cmpHealth) Engine.DestroyEntity(ent); else if (--keep < 0) cmpHealth.Kill(); } }; { let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); cmpTrigger.playerID = 1; cmpTrigger.enemyID = 2; cmpTrigger.RegisterTrigger("OnInitGame", "InitTutorial", { "enabled": true }); } Index: ps/trunk/binaries/data/mods/public/maps/tutorials/starting_economy_walkthrough.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/tutorials/starting_economy_walkthrough.js (revision 25036) +++ ps/trunk/binaries/data/mods/public/maps/tutorials/starting_economy_walkthrough.js (revision 25037) @@ -1,459 +1,459 @@ Trigger.prototype.tutorialGoals = [ { "instructions": [ markForTranslation("This tutorial will teach the basics of developing your economy. Typically, you will start with a Civic Center and a couple units in Village Phase and ultimately, your goal will be to develop and expand your empire, often by evolving to Town Phase and City Phase afterward.\n"), { "text": markForTranslation("\nBefore starting, you can toggle between fullscreen and windowed mode using %(hotkey)s."), "hotkey": ["togglefullscreen"] }, markForTranslation("You can change the level of zoom using the mouse wheel and the camera view using any of your keyboard's arrow keys.\n"), markForTranslation("Adjust the game window to your preferences.\n"), { "text": markForTranslation("\nYou may also toggle between showing and hiding this tutorial panel at any moment using %(hotkey)s.\n"), "hotkey": ["session.gui.tutorial.toggle"] } ] }, { "instructions": [ markForTranslation("To start off, select your building, the Civic Center, by clicking on it. A selection ring in the color of your civilization will be displayed after clicking.") ] }, { "instructions": [ markForTranslation("Now that the Civic Center is selected, you will notice that a production panel will appear on the lower right of your screen detailing the actions that the buildings supports. For the production panel, available actions are not masked in any color, while an icon masked in either grey or red indicates that the action has not been unlocked or you do not have sufficient resources to perform that action, respectively. Additionally, you can hover the cursor over any icon to show a tooltip with more details.\n"), - markForTranslation("The top row of buttons contains portraits of units that may be trained at the building while the bottom one or two rows will have researchable technologies. Hover the cursor over the II icon. The tooltip will tell us that advancing to Town Phase requires both more constructed structures as well as more Food and Wood resources.") + markForTranslation("The top row of buttons contains portraits of units that may be trained at the building while the bottom one or two rows will have researchable technologies. Hover the cursor over the II icon. The tooltip will tell us that advancing to Town Phase requires both more constructed structures as well as more food and wood resources.") ] }, { "instructions": [ - markForTranslation("You have two main types of starting units: female citizens and citizen soldiers. Female citizens are purely economic units; they have low HP, no armor, and little to no attack. Citizen soldiers are workers by default, but in times of need, can utilize a weapon to fight. You have two categories of citizen soldiers: infantry and cavalry. Female citizens and infantry citizen soldiers can gather any land resources while cavalry citizen soldiers can only gather meat from hunted animals.\n") + markForTranslation("You have two main types of starting units: Female Citizens and Citizen Soldiers. Female Citizens are purely economic units; they have low health and little to no attack. Citizen Soldiers are workers by default, but in times of need, can utilize a weapon to fight. You have two categories of Citizen Soldiers: Infantry and Cavalry. Female Citizens and Infantry Citizen Soldiers can gather any land resources while Cavalry Citizen Soldiers can only gather meat from animals.\n") ] }, { "instructions": [ markForTranslation("As a general rule of thumb, left-clicking represents selection while right-clicking with an entity selected represents an order (gather, build, fight, etc.).\n") ] }, { "instructions": [ - markForTranslation("At this point, food and wood are the most important resources for developing your economy, so let's start with gathering food. Female citizens gather vegetables faster than other units.\n"), + markForTranslation("At this point, food and wood are the most important resources for developing your economy, so let's start with gathering food. Female Citizens gather vegetables faster than other units.\n"), markForTranslation("There are primarily three ways to select units:\n"), markForTranslation("1) Hold the left mouse button and drag a selection rectangle that encloses the units you want to select.\n"), markForTranslation("2) Click on one of them and then add additional units to your selection by holding Shift and clicking each additional unit (or also via the above selection rectangle).\n"), markForTranslation("3) Double-click on a unit. This will select every unit of the same type as the specified unit in your visible window. Triple-click will select all units of the same type on the entire map.\n"), - markForTranslation("You can click on an empty space on the map to reset the selection. Try each of these methods before tasking all of your female citizens to gather the grapes to the southeast of your Civic Center by right-clicking on the grapes when you have all the female citizens selected.") + markForTranslation("You can click on an empty space on the map to reset the selection. Try each of these methods before tasking all of your Female Citizens to gather the grapes to the southeast of your Civic Center by right-clicking on the grapes when you have all the Female Citizens selected.") ], "OnPlayerCommand": function(msg) { if (msg.cmd.type == "gather" && msg.cmd.target && TriggerHelper.GetResourceType(msg.cmd.target).specific == "fruit") this.NextGoal(); } }, { "instructions": [ - markForTranslation("Now, let's gather some Wood with your Infantry Citizen Soldiers. Select your Infantry Citizen Soldiers and order them to gather Wood by right-clicking on the nearest tree.") + markForTranslation("Now, let's gather some wood with your Infantry Citizen Soldiers. Select your Infantry Citizen Soldiers and order them to gather wood by right-clicking on the nearest tree.") ], "OnPlayerCommand": function(msg) { if (msg.cmd.type == "gather" && msg.cmd.target && TriggerHelper.GetResourceType(msg.cmd.target).specific == "tree") this.NextGoal(); } }, { "instructions": [ - markForTranslation("Cavalry Citizen Soldiers are good for hunting. Select your cavalry and order him to hunt the chickens around your Civic Center in similar fashion.") + markForTranslation("Cavalry Citizen Soldiers are good for hunting. Select your Cavalry and order him to hunt the chickens around your Civic Center in similar fashion.") ], "OnPlayerCommand": function(msg) { if (msg.cmd.type == "gather" && msg.cmd.target && TriggerHelper.GetResourceType(msg.cmd.target).specific == "meat") this.NextGoal(); } }, { "instructions": [ markForTranslation("All your units are now gathering resources. We should train more units!\n"), - markForTranslation("First, let's set a rally point. Setting a rally point on a building that can train units will automatically designate a task to the new unit upon completion of training. We want to send the newly trained units to gather Wood on the group of trees to the south of the Civic Center. To do so, select the Civic Center by clicking on it and then right-click on one of the trees.\n"), + markForTranslation("First, let's set a rally point. Setting a rally point on a building that can train units will automatically designate a task to the new unit upon completion of training. We want to send the newly trained units to gather wood on the group of trees to the south of the Civic Center. To do so, select the Civic Center by clicking on it and then right-click on one of the trees.\n"), markForTranslation("Rally points are indicated by a small flag at the end of the blue line.") ], "OnPlayerCommand": function(msg) { if (msg.cmd.type != "set-rallypoint" || !msg.cmd.data || !msg.cmd.data.command || msg.cmd.data.command != "gather" || !msg.cmd.data.resourceType || msg.cmd.data.resourceType.specific != "tree") { - this.WarningMessage(markForTranslation("Select the Civic Center, then hover the cursor over a tree and right-click when you see the cursor change into a Wood icon.")); + this.WarningMessage(markForTranslation("Select the Civic Center, then hover the cursor over a tree and right-click when you see the cursor change into a wood icon.")); return; } this.NextGoal(); } }, { "instructions": [ markForTranslation("Now that the rally point is set, we can produce additional units and they will do their assigned task automatically.\n"), - markForTranslation("Citizen soldiers gather wood faster than female citizens. Select the Civic Center and, while holding Shift, click on the second unit icon, the hoplites (holding Shift trains a batch of five units). You can also train units individually by simply clicking, but training 5 units together takes less time than training 5 units individually.") + markForTranslation("Citizen Soldiers gather wood faster than Female Citizens. Select the Civic Center and, while holding Shift, click on the second unit icon, the Hoplites (holding Shift trains a batch of five units). You can also train units individually by simply clicking, but training 5 units together takes less time than training 5 units individually.") ], "OnTrainingQueued": function(msg) { if (msg.unitTemplate != "units/athen/infantry_spearman_b" || +msg.count == 1) { let entity = msg.trainerEntity; let cmpProductionQueue = Engine.QueryInterface(entity, IID_ProductionQueue); cmpProductionQueue.ResetQueue(); let txt = +msg.count == 1 ? markForTranslation("Do not forget to hold Shift while clicking to train several units.") : markForTranslation("Hold Shift and click on the Hoplite icon."); this.WarningMessage(txt); return; } this.NextGoal(); } }, { "instructions": [ markForTranslation("Let's wait for the units to be trained.\n"), - markForTranslation("While waiting, direct your attention to the panel at the top of your screen. On the upper left, you will see your current resource supply (Food, Wood, Stone, and Metal). As each worker brings resources back to the Civic Center (or another dropsite), you will see the amount of the corresponding resource increase.\n"), + markForTranslation("While waiting, direct your attention to the panel at the top of your screen. On the upper left, you will see your current resource supply (food, wood, stone, and metal). As each worker brings resources back to the Civic Center (or another dropsite), you will see the amount of the corresponding resource increase.\n"), markForTranslation("This is a very important concept to keep in mind: gathered resources have to be brought back to a dropsite to be accounted, and you should always try to minimize the distance between resource and nearest dropsite to improve your gathering efficiency.") ], "OnTrainingFinished": function(msg) { this.NextGoal(); } }, { "instructions": [ - markForTranslation("The newly trained units automatically go to the trees and start gathering Wood.\n"), - markForTranslation("But as they have to bring it back to the Civic Center to deposit it, their gathering efficiency suffers from the distance. To fix that, we can build a storehouse, a dropsite for Wood, Stone, and Metal, close to the trees. To do so, select your five newly trained Citizen Soldiers and look for the construction panel on the bottom right, click on the storehouse icon, move the mouse as close as possible to the trees you want to gather and click on a valid place to build the dropsite.\n"), + markForTranslation("The newly trained units automatically go to the trees and start gathering wood.\n"), + markForTranslation("But as they have to bring it back to the Civic Center to deposit it, their gathering efficiency suffers from the distance. To fix that, we can build a Storehouse, a dropsite for wood, stone, and metal, close to the trees. To do so, select your five newly trained Citizen Soldiers and look for the construction panel on the bottom right, click on the Storehouse icon, move the mouse as close as possible to the trees you want to gather and click on a valid place to build the dropsite.\n"), markForTranslation("Invalid (obstructed) positions will show the building preview overlay in red.") ], "OnPlayerCommand": function(msg) { if (msg.cmd.type == "construct" && msg.cmd.template == "structures/athen/storehouse") this.NextGoal(); } }, { "instructions": [ - markForTranslation("The selected citizens will automatically start constructing the building once you place the foundation.") + markForTranslation("The selected Citizens will automatically start constructing the building once you place the foundation.") ], "OnStructureBuilt": function(msg) { let cmpResourceDropsite = Engine.QueryInterface(msg.building, IID_ResourceDropsite); if (cmpResourceDropsite && cmpResourceDropsite.AcceptsType("wood")) this.NextGoal(); }, }, { "instructions": [ - markForTranslation("When construction finishes, the builders default to gathering Wood automatically.\n"), - markForTranslation("Let's train some female citizens to gather more food. Select the Civic Center, hold Shift and click on the female citizen icon to train 5 female citizens.") + markForTranslation("When construction finishes, the builders default to gathering wood automatically.\n"), + markForTranslation("Let's train some Female Citizens to gather more food. Select the Civic Center, hold Shift and click on the Female Citizen icon to train five Female Citizens.") ], "Init": function() { this.trainingDone = false; }, "OnTrainingQueued": function(msg) { if (msg.unitTemplate != "units/athen/support_female_citizen" || +msg.count == 1) { let entity = msg.trainerEntity; let cmpProductionQueue = Engine.QueryInterface(entity, IID_ProductionQueue); cmpProductionQueue.ResetQueue(); let txt = +msg.count == 1 ? markForTranslation("Do not forget to hold Shift and click to train several units.") : markForTranslation("Hold shift and click on the Female Citizen icon."); this.WarningMessage(txt); return; } this.NextGoal(); } }, { "instructions": [ markForTranslation("Let's wait for the units to be trained.\n"), - markForTranslation("In the meantime, we seem to have enough workers gathering Wood. We should remove the current rally point of the Civic Center away from gathering Wood. For that purpose, right-click on the Civic Center when it is selected (and the flag icon indicating the rally point is crossed out).") + markForTranslation("In the meantime, we seem to have enough workers gathering wood. We should remove the current rally point of the Civic Center away from gathering wood. For that purpose, right-click on the Civic Center when it is selected (and the flag icon indicating the rally point is crossed out).") ], "OnPlayerCommand": function(msg) { if (msg.cmd.type == "unset-rallypoint") this.NextGoal(); }, "OnTrainingFinished": function(msg) { this.trainingDone = true; } }, { "instructions": [ markForTranslation("The units should be ready soon.\n"), markForTranslation("In the meantime, direct your attention to your population count on the top panel. It is the fifth item from the left, after the resources. It would be prudent to keep an eye on it. It indicates your current population (including those being trained) and the current population limit, which is determined by your built structures.") ], "IsDone": function(msg) { return this.trainingDone; }, "OnTrainingFinished": function(msg) { this.NextGoal(); } }, { "instructions": [ - markForTranslation("As you have nearly reached the population limit, you must increase it by building some new structures if you want to train more units. The most cost effective structure to increase your population limit is the house.\n"), - markForTranslation("Now that the units are ready, let's see how to build several houses in a row.") + markForTranslation("As you have nearly reached the population limit, you must increase it by building some new structures if you want to train more units. The most cost effective structure to increase your population limit is the House.\n"), + markForTranslation("Now that the units are ready, let's see how to build several Houses in a row.") ] }, { "instructions": [ - markForTranslation("Select two of your newly-trained female citizens and ask them to build these houses in the empty space to the east of the Civic Center. To do so, after selecting the female citizens, click on the house icon in the bottom right panel and, while holding Shift, click first on the position in the map where you want to build the first house, and then click on the position where you want to build the second house (when you give a command while holding Shift, you put the command in a queue; units automatically switch to the next command in their queue when they finish their current command). Press Escape to get rid of the house cursor so you don't spam houses all over the map.\n"), - markForTranslation("Reminder: to select only two female citizens, click on the first one and then hold Shift and click on the second one.") + markForTranslation("Select two of your newly-trained Female Citizens and ask them to build these Houses in the empty space to the east of the Civic Center. To do so, after selecting the Female Citizens, click on the House icon in the bottom right panel and, while holding Shift, click first on the position in the map where you want to build the first House, and then click on the position where you want to build the second House (when you give a command while holding Shift, you put the command in a queue; units automatically switch to the next command in their queue when they finish their current command). Press Escape to get rid of the House cursor so you don't spam Houses all over the map.\n"), + markForTranslation("Reminder: to select only two Female Citizens, click on the first one and then hold Shift and click on the second one.") ], "Init": function() { this.houseGoal = new Set(); this.houseCount = 0; }, "IsDone": function() { return this.houseCount > 1; }, "OnOwnershipChanged": function(msg) { if (msg.from != INVALID_PLAYER && this.houseGoal.has(+msg.entity)) { this.houseGoal.delete(+msg.entity); let cmpFoundation = Engine.QueryInterface(+msg.entity, IID_Foundation); if (cmpFoundation && cmpFoundation.GetBuildProgress() < 1) // Destroyed before built --this.houseCount; } else if (msg.from == INVALID_PLAYER && msg.to == this.playerID && Engine.QueryInterface(+msg.entity, IID_Foundation) && TriggerHelper.EntityMatchesClassList(+msg.entity, "House")) { this.houseGoal.add(+msg.entity); ++this.houseCount; if (this.IsDone()) this.NextGoal(); } } }, { "instructions": [ markForTranslation("You may notice that berries are a finite supply of food. We will need a more lasting food source. Fields produce an unlimited food resource, but are slower to gather than forageable fruits.\n"), - markForTranslation("But to minimize the distance between a farm and its corresponding food dropsite, we will first build a farmstead.") + markForTranslation("But to minimize the distance between a farm and its corresponding food dropsite, we will first build a Farmstead.") ], "delay": -1, "OnOwnershipChanged": function(msg) { if (this.houseGoal.has(+msg.entity)) this.houseGoal.delete(+msg.entity); } }, { "instructions": [ - markForTranslation("Select the three remaining (idle) female citizens and order them to build a farmstead in the center of the large open area to the west of the Civic Center.\n"), - markForTranslation("We will need a decent chunk of space around the farmstead to build fields. In addition, we can see goats on the west side to further improve our food gathering efficiency should we ever decide to hunt them.\n"), - markForTranslation("If you try to select the three idle female citizens by clicking and dragging a selection rectangle over them, you might accidentally select additional units. To avoid that, hold the I key while selecting so that only idle units are selected. If you accidentally select a cavalry unit, hold Ctrl and click on the cavalry unit icon of the selection panel at the bottom of the screen to remove the cavalry unit from the current selection.") + markForTranslation("Select the three remaining (idle) Female Citizens and order them to build a Farmstead in the center of the large open area to the west of the Civic Center.\n"), + markForTranslation("We will need a decent chunk of space around the Farmstead to build Fields. In addition, we can see goats on the west side to further improve our food gathering efficiency should we ever decide to hunt them.\n"), + markForTranslation("If you try to select the three idle Female Citizens by clicking and dragging a selection rectangle over them, you might accidentally select additional units. To avoid that, hold the I key while selecting so that only idle units are selected. If you accidentally select a cavalry unit, hold Ctrl and click on the cavalry unit icon of the selection panel at the bottom of the screen to remove the cavalry unit from the current selection.") ], "OnPlayerCommand": function(msg) { if (msg.cmd.type == "construct" && msg.cmd.template == "structures/athen/farmstead") this.NextGoal(); }, "OnOwnershipChanged": function(msg) { if (this.houseGoal.has(+msg.entity)) this.houseGoal.delete(+msg.entity); } }, { "instructions": [ - markForTranslation("When the farmstead construction is finished, its builders will automatically look for food, and in this case, they will go after the nearby goats.\n"), - markForTranslation("But your house builders will only look for something else to build and, if nothing found, become idle. Let's wait for them to build the houses.") + markForTranslation("When the Farmstead construction is finished, its builders will automatically look for food, and in this case, they will go after the nearby goats.\n"), + markForTranslation("But your House builders will only look for something else to build and, if nothing found, become idle. Let's wait for them to build the Houses.") ], "IsDone": function() { return !this.houseGoal.size; }, "OnOwnershipChanged": function(msg) { if (this.houseGoal.has(+msg.entity)) this.houseGoal.delete(+msg.entity); if (this.IsDone()) this.NextGoal(); } }, { "instructions": [ - markForTranslation("When both houses are built, select your two female citizens and order them to build a field as close as possible to the farmstead, which is a dropsite for all types of food.") + markForTranslation("When both Houses are built, select your two Female Citizens and order them to build a Field as close as possible to the Farmstead, which is a dropsite for all types of food.") ], "OnPlayerCommand": function(msg) { if (msg.cmd.type == "construct" && msg.cmd.template == "structures/athen/field") this.NextGoal(); } }, { "instructions": [ - markForTranslation("When the field is ready, the builders will automatically start gathering it.\n"), + markForTranslation("When the Field is ready, the builders will automatically start gathering it.\n"), markForTranslation("The cavalry unit should have slaughtered all chickens by now. Select it and explore the south-west area: there is a lake with some camels around. Move your cavalry by right-clicking on the point you want to go, and when you see a herd of camels, right-click on one of them to start hunting for food.") ], "OnPlayerCommand": function(msg) { if (msg.cmd.type == "gather" && msg.cmd.target && TriggerHelper.GetResourceType(msg.cmd.target).specific == "meat") this.NextGoal(); } }, { "instructions": [ markForTranslation("Up to five Workers can gather from a Field. To add additional Workers, select the Civic Center and set a rally point on a Field by right-clicking on it. If the Field is not yet finished, new Workers sent by a rally point will help building it, and when built, they will gather food.") ], "OnPlayerCommand": function(msg) { if (msg.cmd.type != "set-rallypoint" || !msg.cmd.data || !msg.cmd.data.command || (msg.cmd.data.command != "build" || !msg.cmd.data.target || !TriggerHelper.EntityMatchesClassList(msg.cmd.data.target, "Field")) && (msg.cmd.data.command != "gather" || !msg.cmd.data.resourceType || msg.cmd.data.resourceType.specific != "grain")) { - this.WarningMessage(markForTranslation("Select the Civic Center and right-click on the field.")); + this.WarningMessage(markForTranslation("Select the Civic Center and right-click on the Field.")); return; } this.NextGoal(); } }, { "instructions": [ - markForTranslation("Now click three times on the female citizen icon in the bottom right panel to train three additional farmers.") + markForTranslation("Now click three times on the Female Citizen icon in the bottom right panel to train three additional farmers.") ], "Init": function(msg) { this.femaleCount = 0; }, "OnTrainingQueued": function(msg) { if (msg.unitTemplate != "units/athen/support_female_citizen" || +msg.count != 1) { let entity = msg.trainerEntity; let cmpProductionQueue = Engine.QueryInterface(entity, IID_ProductionQueue); cmpProductionQueue.ResetQueue(); let txt = +msg.count != 1 ? markForTranslation("Click without holding Shift to train a single unit.") : markForTranslation("Click on the Female Citizen icon."); this.WarningMessage(txt); return; } if (++this.femaleCount == 3) this.NextGoal(); } }, { "instructions": [ markForTranslation("You can increase the gather rates of your workers by researching new technologies available in some buildings.\n"), - markForTranslation("The farming rate, for example, can be improved with a researchable technology in the farmstead. Select the farmstead and look at its production panel on the bottom right. You will see several researchable technologies. Hover the cursor over them to see their costs and effects and click on the one you want to research.") + markForTranslation("The farming rate, for example, can be improved with a researchable technology in the Farmstead. Select the Farmstead and look at its production panel on the bottom right. You will see several researchable technologies. Hover the cursor over them to see their costs and effects and click on the one you want to research.") ], "IsDone": function() { return TriggerHelper.HasDealtWithTech(this.playerID, "gather_wicker_baskets") || TriggerHelper.HasDealtWithTech(this.playerID, "gather_farming_plows"); }, "OnResearchQueued": function(msg) { if (msg.technologyTemplate && TriggerHelper.EntityMatchesClassList(msg.researcherEntity, "Farmstead")) this.NextGoal(); } }, { "instructions": [ markForTranslation("We should start preparing to phase up into Town Phase, which will unlock many more units and buildings. Select the Civic Center and hover the cursor over the Town Phase icon to see what is still needed.\n"), markForTranslation("We now have enough resources, but one structure is missing. Although this is an economic tutorial, it is nonetheless useful to be prepared for defense in case of attack, so let's build Barracks.\n"), markForTranslation("Select four of your soldiers and ask them to build a Barracks: as before, start selecting the soldiers, click on the Barracks icon in the production panel and then lay down a foundation not far from your Civic Center where you want to build.") ], "OnPlayerCommand": function(msg) { if (msg.cmd.type == "construct" && msg.cmd.template == "structures/athen/barracks") this.NextGoal(); } }, { "instructions": [ - markForTranslation("Let's wait for the Barracks to be built. As this construction is lengthy, you can add two soldiers to build it faster. To do so, select your Civic Center and set up a rally point on the Barracks foundation by right-clicking on it (you should see a hammer icon). Then produce two more builders by clicking on the hoplite icon twice.") + markForTranslation("Let's wait for the Barracks to be built. As this construction is lengthy, you can add two soldiers to build it faster. To do so, select your Civic Center and set up a rally point on the Barracks foundation by right-clicking on it (you should see a hammer icon). Then produce two more builders by clicking on the Hoplite icon twice.") ], "OnStructureBuilt": function(msg) { if (TriggerHelper.EntityMatchesClassList(msg.building, "Barracks")) this.NextGoal(); }, }, { "instructions": [ markForTranslation("You should now be able to research Town Phase. Select the Civic Center and click on the technology icon.\n"), markForTranslation("If you still miss some resources (icon with red overlay), wait for them to be gathered by your workers.") ], "IsDone": function() { return TriggerHelper.HasDealtWithTech(this.playerID, "phase_town_athen"); }, "OnResearchQueued": function(msg) { if (msg.technologyTemplate && TriggerHelper.EntityMatchesClassList(msg.researcherEntity, "CivilCentre")) this.NextGoal(); } }, { "instructions": [ - markForTranslation("In later phases, you need usually Stone and Metal to build bigger structures and train better soldiers. Hence, while waiting for the research to be done, you will send half of your idle Citizen Soldiers (who have finished building the Barracks) to gather Stone and the other half to gather Metal.\n"), - markForTranslation("To do so, we could select three Citizen Soldiers and right-click on the Stone mine on the west of the Civic Center (the cursor changes when hovering the Stone mine while your soldiers are selected). However, these soldiers were gathering Wood, so they may still carry some Wood which would be lost when starting to gather another resource.") + markForTranslation("In later phases, you need usually stone and metal to build bigger structures and train better soldiers. Hence, while waiting for the research to be done, you will send half of your idle Citizen Soldiers (who have finished building the Barracks) to gather stone and the other half to gather metal.\n"), + markForTranslation("To do so, we could select three Citizen Soldiers and right-click on the stone quarry on the west of the Civic Center (the cursor changes when hovering the stone quarry while your soldiers are selected). However, these soldiers were gathering wood, so they may still carry some wood which would be lost when starting to gather another resource.") ], }, { "instructions": [ - markForTranslation("Thus, we should order them to deposit their Wood in the Civic Center along the way. To do so, we will hold Shift while clicking to queue orders: select your soldiers, hold Shift and right-click on the Civic Center to deposit their Wood and then hold Shift and right-click on the Stone mine to gather it.\n"), - markForTranslation("Perform a similar order queue with the remaining soldiers and the Metal mine in the west.") + markForTranslation("Thus, we should order them to deposit their wood in the Civic Center along the way. To do so, we will hold Shift while clicking to queue orders: select your soldiers, hold Shift and right-click on the Civic Center to deposit their wood and then hold Shift and right-click on the stone quarry to gather it.\n"), + markForTranslation("Perform a similar order queue with the remaining soldiers and the metal mine in the west.") ], "Init": function() { this.stone = false; this.metal = false; }, "IsDone": function() { if (!this.stone || !this.metal) return false; return TriggerHelper.HasDealtWithTech(this.playerID, "phase_town_athen"); }, "OnPlayerCommand": function(msg) { if (msg.cmd.type == "gather" && msg.cmd.target) { if (TriggerHelper.GetResourceType(msg.cmd.target).generic == "stone") this.stone = true; else if (TriggerHelper.GetResourceType(msg.cmd.target).generic == "metal") this.metal = true; } if (this.IsDone()) this.NextGoal(); }, "OnResearchFinished": function(msg) { if (this.IsDone()) this.NextGoal(); } }, { "instructions": [ markForTranslation("This is the end of the walkthrough. This should give you a good idea of the basics of setting up your economy.") ] } ]; { let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); cmpTrigger.playerID = 1; cmpTrigger.RegisterTrigger("OnInitGame", "InitTutorial", { "enabled": true }); } Index: ps/trunk/binaries/data/mods/public/maps/tutorials/starting_economy_walkthrough.xml =================================================================== --- ps/trunk/binaries/data/mods/public/maps/tutorials/starting_economy_walkthrough.xml (revision 25036) +++ ps/trunk/binaries/data/mods/public/maps/tutorials/starting_economy_walkthrough.xml (revision 25037) @@ -1,8226 +1,8226 @@ default 0 0.5 lake 26.0485 4 0.45 0 0 1 0.99 0.1999 default 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0