Index: ps/trunk/binaries/data/mods/public/maps/random/guadalquivir_river.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/guadalquivir_river.js (revision 21702) +++ ps/trunk/binaries/data/mods/public/maps/random/guadalquivir_river.js (revision 21703) @@ -1,286 +1,290 @@ Engine.LoadLibrary("rmgen"); Engine.LoadLibrary("rmgen-common"); const tGrass = ["medit_grass_field_a", "medit_grass_field_b"]; const tForestFloorC = "medit_plants_dirt"; const tForestFloorP = "medit_grass_shrubs"; const tGrassA = "medit_grass_field_b"; const tGrassB = "medit_grass_field_brown"; const tGrassC = "medit_grass_field_dry"; const tRoad = "medit_city_tile"; const tRoadWild = "medit_city_tile"; const tGrassPatch = "medit_grass_shrubs"; const tShore = "sand_grass_25"; const tWater = "medit_sand_wet"; const oPoplar = "gaia/flora_tree_poplar"; const oApple = "gaia/flora_tree_apple"; const oCarob = "gaia/flora_tree_carob"; const oBerryBush = "gaia/flora_bush_berry"; const oDeer = "gaia/fauna_deer"; const oFish = "gaia/fauna_fish"; const oSheep = "gaia/fauna_sheep"; const oStoneLarge = "gaia/geology_stonemine_medit_quarry"; const oStoneSmall = "gaia/geology_stone_mediterranean"; const oMetalLarge = "gaia/geology_metal_mediterranean_slabs"; const aGrass = "actor|props/flora/grass_soft_large_tall.xml"; const aGrassShort = "actor|props/flora/grass_soft_large.xml"; const aReeds = "actor|props/flora/reeds_pond_lush_a.xml"; const aLillies = "actor|props/flora/water_lillies.xml"; const aRockLarge = "actor|geology/stone_granite_large.xml"; const aRockMedium = "actor|geology/stone_granite_med.xml"; const aBushMedium = "actor|props/flora/bush_medit_me.xml"; const aBushSmall = "actor|props/flora/bush_medit_sm.xml"; const pForestP = [tForestFloorP + TERRAIN_SEPARATOR + oPoplar, tForestFloorP]; const pForestC = [tForestFloorC + TERRAIN_SEPARATOR + oCarob, tForestFloorC]; var heightSeaGround = -3; var heightShallow = -1.5; var heightShore = 2; var heightLand = 3; var g_Map = new RandomMap(heightSeaGround, tWater); const numPlayers = getNumPlayers(); const mapSize = g_Map.getSize(); const mapCenter = g_Map.getCenter(); const mapBounds = g_Map.getBounds(); var clPlayer = g_Map.createTileClass(); var clForest = g_Map.createTileClass(); var clDirt = g_Map.createTileClass(); var clRock = g_Map.createTileClass(); var clMetal = g_Map.createTileClass(); var clFood = g_Map.createTileClass(); var clBaseResource = g_Map.createTileClass(); var clLand = g_Map.createTileClass(); var clRiver = g_Map.createTileClass(); var clShallow = g_Map.createTileClass(); g_Map.log("Create the continent body"); var startAngle = randomAngle(); -var continentCenter = new Vector2D(fractionToTiles(0.5), fractionToTiles(0.7)); -var continentCenterR = continentCenter.clone().rotateAround(startAngle, mapCenter).round() +var continentCenter = new Vector2D(fractionToTiles(0.5), fractionToTiles(0.7)).rotateAround(startAngle, mapCenter).round(); + createArea( new ChainPlacer( 2, Math.floor(scaleByMapSize(5, 12)), Math.floor(scaleByMapSize(60, 700)), Infinity, - continentCenterR, + continentCenter, 0, [Math.floor(fractionToTiles(0.49))]), [ new TerrainPainter(tGrass), new SmoothElevationPainter(ELEVATION_SET, heightLand, 4), new TileClassPainter(clLand) ]); -var playerPosition = playerPlacementCustomAngle( - fractionToTiles(0.35), +var playerIDs = sortAllPlayers(); +var playerPosition = playerPlacementArcs( + playerIDs, continentCenter, - i => Math.PI * (-0.46 / numPlayers * (i + i % 2) - (i % 2) / 2))[0].map(pos => pos.rotateAround(startAngle, mapCenter)); + fractionToTiles(0.35), + -startAngle - 0.5 * Math.PI, + 0, + 0.65 * Math.PI); placePlayerBases({ - "PlayerPlacement": [primeSortAllPlayers(), playerPosition], + "PlayerPlacement": [playerIDs, playerPosition], "PlayerTileClass": clPlayer, "BaseResourceClass": clBaseResource, "Walls": false, "CityPatch": { "outerTerrain": tRoadWild, "innerTerrain": tRoad }, "Chicken": { }, "Berries": { "template": oBerryBush }, "Mines": { "types": [ { "template": oMetalLarge }, { "template": oStoneLarge } ] }, "Trees": { "template": oPoplar, "count": 2 }, "Decoratives": { "template": aGrassShort } }); Engine.SetProgress(20); paintRiver({ "parallel": true, "constraint": stayClasses(clLand, 0), "start": new Vector2D(mapCenter.x, mapBounds.top).rotateAround(startAngle, mapCenter), "end": new Vector2D(mapCenter.x, mapBounds.bottom).rotateAround(startAngle, mapCenter), "width": fractionToTiles(0.07), "fadeDist": scaleByMapSize(3, 12), "deviation": 1, "heightRiverbed": heightSeaGround, "heightLand": heightShore, "meanderShort": 12, "meanderLong": 0, "waterFunc": (position, height, z) => { clRiver.add(position); createTerrain(tWater).place(position); if (height < heightShallow && ( z > 0.3 && z < 0.4 || z > 0.5 && z < 0.6 || z > 0.7 && z < 0.8)) { g_Map.setHeight(position, heightShallow); clShallow.add(position); } } }); paintTerrainBasedOnHeight(1, 3, 0, tShore); paintTerrainBasedOnHeight(-8, 1, 2, tWater); createBumps([avoidClasses(clPlayer, 20, clRiver, 1), stayClasses(clLand, 3)]); var [forestTrees, stragglerTrees] = getTreeCounts(500, 3000, 0.7); createForests( [tGrass, tForestFloorP, tForestFloorC, pForestC, pForestP], [avoidClasses(clPlayer, 20, clForest, 17, clRiver, 1), stayClasses(clLand, 7)], clForest, forestTrees); Engine.SetProgress(50); g_Map.log("Creating dirt patches"); createLayeredPatches( [scaleByMapSize(3, 6), scaleByMapSize(5, 10), scaleByMapSize(8, 21)], [[tGrass,tGrassA],[tGrassA,tGrassB], [tGrassB,tGrassC]], [1,1], [avoidClasses(clForest, 0, clDirt, 3, clPlayer, 8, clRiver, 1), stayClasses(clLand, 7)], scaleByMapSize(15, 45), clDirt); g_Map.log("Creating grass patches"); createPatches( [scaleByMapSize(2, 4), scaleByMapSize(3, 7), scaleByMapSize(5, 15)], tGrassPatch, [avoidClasses(clForest, 0, clDirt, 3, clPlayer, 8, clRiver, 1), stayClasses(clLand, 7)], scaleByMapSize(15, 45), clDirt); Engine.SetProgress(55); g_Map.log("Creating stone mines"); createMines( [ [new SimpleObject(oStoneSmall, 0, 2, 0, 4, 0, 2 * Math.PI, 1), new SimpleObject(oStoneLarge, 1, 1, 0, 4, 0, 2 * Math.PI, 4)], [new SimpleObject(oStoneSmall, 2,5, 1,3)] ], [avoidClasses(clForest, 1, clPlayer, 20, clRock, 10, clRiver, 1), stayClasses(clLand, 5)], clRock); g_Map.log("Creating metal mines"); createMines( [ [new SimpleObject(oMetalLarge, 1,1, 0,4)] ], [avoidClasses(clForest, 1, clPlayer, 20, clMetal, 10, clRock, 5, clRiver, 1), stayClasses(clLand, 5)], clMetal ); Engine.SetProgress(65); 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(clPlayer, 1, clDirt, 1, clRiver, 1), stayClasses(clLand, 6)]); g_Map.log("Create water decoration in the shallow parts"); createDecoration( [ [new SimpleObject(aReeds, 1, 3, 0, 1)], [new SimpleObject(aLillies, 1, 2, 0, 1)] ], [ scaleByMapSize(800, 12800), scaleByMapSize(800, 12800) ], stayClasses(clShallow, 0)); Engine.SetProgress(70); createFood( [ [new SimpleObject(oDeer, 5, 7, 0, 4)], [new SimpleObject(oSheep, 2, 3, 0, 2)] ], [ 3 * numPlayers, 3 * numPlayers ], [avoidClasses(clForest, 0, clPlayer, 20, clFood, 20, clRiver, 1), stayClasses(clLand, 3)], clFood); createFood( [ [new SimpleObject(oBerryBush, 5, 7, 0, 4)] ], [ randIntInclusive(1, 4) * numPlayers + 2 ], [avoidClasses(clForest, 0, clPlayer, 20, clFood, 10, clRiver, 1), stayClasses(clLand, 3)], clFood); createFood( [ [new SimpleObject(oFish, 2, 3, 0, 2)] ], [ 25 * numPlayers ], avoidClasses(clLand, 2, clRiver, 1), clFood); Engine.SetProgress(85); createStragglerTrees( [oPoplar, oCarob, oApple], [avoidClasses(clForest, 1, clPlayer, 9, clMetal, 6, clRock, 6, clRiver, 1), stayClasses(clLand, 7)], clForest, stragglerTrees); placePlayersNomad( clPlayer, new AndConstraint([ stayClasses(clLand, 4), avoidClasses(clForest, 1, clMetal, 4, clRock, 4, clFood, 2)])); setSkySet("cumulus"); setWaterColor(0.2,0.312,0.522); setWaterTint(0.1,0.1,0.8); setWaterWaviness(4.0); setWaterType("lake"); setWaterMurkiness(0.73); setFogFactor(0.3); setFogThickness(0.25); setPPEffect("hdr"); setPPContrast(0.62); setPPSaturation(0.51); setPPBloom(0.12); g_Map.ExportMap(); Index: ps/trunk/binaries/data/mods/public/maps/random/jebel_barkal.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/jebel_barkal.js (revision 21702) +++ ps/trunk/binaries/data/mods/public/maps/random/jebel_barkal.js (revision 21703) @@ -1,1340 +1,1343 @@ /** * For historic reference, see http://www.jebelbarkal.org/images/maps/siteplan.jpg */ Engine.LoadLibrary("rmgen"); Engine.LoadLibrary("rmgen-common"); Engine.LoadLibrary("heightmap"); TILE_CENTERED_HEIGHT_MAP = true; const tSand = "desert_sand_dunes_100"; const tHilltop = ["new_savanna_dirt_c", "new_savanna_dirt_d"]; const tHillGround = ["savanna_dirt_rocks_a", "savanna_dirt_rocks_b", "savanna_dirt_rocks_c"]; const tHillCliff = ["savanna_cliff_a_red", "savanna_cliff_b_red"]; const tRoadDesert = "savanna_tile_a"; const tRoadFertileLand = "savanna_tile_a"; const tWater = "desert_sand_wet"; const tGrass = ["savanna_shrubs_a_wetseason", "alpine_grass_b_wild", "medit_shrubs_a", "steppe_grass_green_a"]; const tForestFloorFertile = pickRandom(tGrass); const tGrassTransition1 = "desert_grass_a"; const tGrassTransition2 = "steppe_grass_dirt_66"; const tPath = "road2"; const tPathWild = "road_med"; const oAcacia = "gaia/flora_tree_acacia"; const oPalmPath = "gaia/flora_tree_cretan_date_palm_tall"; const oPalms = [ "gaia/flora_tree_cretan_date_palm_tall", "gaia/flora_tree_cretan_date_palm_short", "gaia/flora_tree_palm_tropic", "gaia/flora_tree_date_palm", "gaia/flora_tree_senegal_date_palm", "gaia/flora_tree_medit_fan_palm" ]; const oBerryBushGrapes = "gaia/flora_bush_grapes"; const oBerryBushDesert = "gaia/flora_bush_berry_desert"; const oStoneLarge = "gaia/geology_stonemine_desert_quarry"; const oStoneSmall = "gaia/geology_stone_desert_small"; const oMetalLarge = "gaia/geology_metal_desert_slabs"; const oMetalSmall = "gaia/geology_metal_desert_small"; const oFoodTreasureBin = "gaia/treasure/food_bin"; const oFoodTreasureCrate = "gaia/treasure/food_crate"; const oFoodTreasureJars = "gaia/treasure/food_jars"; const oWoodTreasure = "gaia/treasure/wood"; const oStoneTreasure = "gaia/treasure/stone"; const oMetalTreasure = "gaia/treasure/metal"; const oTreasuresHill = [oWoodTreasure, oStoneTreasure, oMetalTreasure]; const oTreasuresCity = [oFoodTreasureBin, oFoodTreasureCrate, oFoodTreasureJars].concat(oTreasuresHill); const oGiraffe = "gaia/fauna_giraffe"; const oGiraffeInfant = "gaia/fauna_giraffe_infant"; const oGazelle = "gaia/fauna_gazelle"; const oRhino = "gaia/fauna_rhino"; const oWarthog = "gaia/fauna_boar"; const oElephant = "gaia/fauna_elephant_african_bush"; const oElephantInfant = "gaia/fauna_elephant_african_infant"; const oLion = "gaia/fauna_lion"; const oLioness = "gaia/fauna_lioness"; const oCrocodile = "gaia/fauna_crocodile"; const oFish = "gaia/fauna_fish_tilapia"; const oHawk = "gaia/fauna_hawk"; const oTempleApedemak = "structures/kush_temple"; const oTempleAmun = "structures/kush_temple_amun"; const oPyramidLarge = "structures/kush_pyramid_large"; const oPyramidSmall = "structures/kush_pyramid_small"; const oWonderPtol = "structures/ptol_wonder"; const oFortress = "structures/kush_fortress"; const oTower = g_MapSettings.Size >= 256 ? "structures/kush_defense_tower" : "structures/kush_sentry_tower"; const oHouse = "structures/kush_house"; const oMarket = "structures/kush_market"; const oBlacksmith = "structures/kush_blacksmith"; const oBlemmyeCamp = "structures/kush_blemmye_camp"; const oNubaVillage = "structures/kush_nuba_village"; const oCivicCenter = "structures/kush_civil_centre"; const oBarracks = "structures/kush_barracks"; const oStable = "structures/kush_stable"; const oElephantStables = "structures/kush_elephant_stables"; const oWallMedium = "structures/kush_wall_medium"; const oWallGate = "structures/kush_wall_gate"; const oWallTower = "structures/kush_wall_tower"; const oKushCitizenArcher = "units/kush_infantry_archer_b"; const oKushHealer = "units/kush_support_healer_b"; const oKushChampionArcher = "units/kush_champion_infantry"; const oKushChampions = [ oKushChampionArcher, "units/kush_champion_infantry_amun", "units/kush_champion_infantry_apedemak" ]; const oPtolSiege = ["units/ptol_mechanical_siege_lithobolos_unpacked", "units/ptol_mechanical_siege_polybolos_unpacked"]; const oTriggerPointCityPath = "trigger/trigger_point_A"; const oTriggerPointAttackerPatrol = "trigger/trigger_point_B"; const aPalmPath = actorTemplate("flora/trees/palm_cretan_date_tall"); const aRock = actorTemplate("geology/stone_savanna_med"); const aHandcart = actorTemplate("props/special/eyecandy/handcart_1"); const aPlotFence = actorTemplate("props/special/common/plot_fence"); const aStatueKush = actorTemplate("props/special/eyecandy/statues_kush"); const aStatues = [ "props/structures/kushites/statue_pedestal_rectangular", "props/structures/kushites/statue_pedestal_rectangular_lion" ].map(actorTemplate); const aBushesFertileLand = [ ...new Array(3).fill("props/flora/shrub_spikes"), ...new Array(3).fill("props/flora/ferns"), "props/flora/shrub_tropic_plant_a", "props/flora/shrub_tropic_plant_b", "props/flora/shrub_tropic_plant_flower", "props/flora/foliagebush", "props/flora/bush", "props/flora/bush_medit_la", "props/flora/bush_medit_la_lush", "props/flora/bush_medit_me_lush", "props/flora/bush_medit_sm", "props/flora/bush_medit_sm_lush", "props/flora/bush_tempe_la_lush" ].map(actorTemplate); const aBushesCity = [ "props/flora/bush_dry_a", "props/flora/bush_medit_la_dry", "props/flora/bush_medit_me_dry", "props/flora/bush_medit_sm", "props/flora/bush_medit_sm_dry", ].map(actorTemplate); const aBushesDesert = [ "props/flora/bush_tempe_me_dry", "props/flora/grass_soft_dry_large_tall", "props/flora/grass_soft_dry_small_tall" ].map(actorTemplate).concat(aBushesCity); const aWaterDecoratives = ["props/flora/reeds_pond_lush_a"].map(actorTemplate); const pForestPalms = [ tForestFloorFertile, ...oPalms.map(tree => tForestFloorFertile + TERRAIN_SEPARATOR + tree), tForestFloorFertile]; const heightScale = num => num * g_MapSettings.Size / 320; const minHeightSource = 3; const maxHeightSource = 800; const g_Map = new RandomMap(0, tSand); const mapSize = g_Map.getSize(); const mapCenter = g_Map.getCenter(); const mapBounds = g_Map.getBounds(); const numPlayers = getNumPlayers(); const clHill = g_Map.createTileClass(); const clCliff = g_Map.createTileClass(); const clDesert = g_Map.createTileClass(); const clFertileLand = g_Map.createTileClass(); const clWater = g_Map.createTileClass(); const clIrrigationCanal = g_Map.createTileClass(); const clPassage = g_Map.createTileClass(); const clPlayer = g_Map.createTileClass(); const clBaseResource = g_Map.createTileClass(); const clFood = g_Map.createTileClass(); const clForest = g_Map.createTileClass(); const clRock = g_Map.createTileClass(); const clMetal = g_Map.createTileClass(); const clTreasure = g_Map.createTileClass(); const clCity = g_Map.createTileClass(); const clPath = g_Map.createTileClass(); const clPathStatues = g_Map.createTileClass(); const clPathCrossing = g_Map.createTileClass(); const clStatue = g_Map.createTileClass(); const clWall = g_Map.createTileClass(); const clGate = g_Map.createTileClass(); const clTriggerPointCityPath = g_Map.createTileClass(); const clTriggerPointMap = g_Map.createTileClass(); const clSoldier = g_Map.createTileClass(); const clTower = g_Map.createTileClass(); const clFortress = g_Map.createTileClass(); const clTemple = g_Map.createTileClass(); const clRitualPlace = g_Map.createTileClass(); const clPyramid = g_Map.createTileClass(); const clHouse = g_Map.createTileClass(); const clBlacksmith = g_Map.createTileClass(); const clStable = g_Map.createTileClass(); const clElephantStables = g_Map.createTileClass(); const clCivicCenter = g_Map.createTileClass(); const clBarracks = g_Map.createTileClass(); const clBlemmyeCamp = g_Map.createTileClass(); const clNubaVillage = g_Map.createTileClass(); const clMarket = g_Map.createTileClass(); const clDecorative = g_Map.createTileClass(); -const riverAngle = Math.PI * 0.05; +const riverAngle = 0.05 * Math.PI; const hillRadius = scaleByMapSize(40, 120); const positionPyramids = new Vector2D(fractionToTiles(0.15), fractionToTiles(0.75)); const pathWidth = 4; const pathWidthCenter = 10; const pathWidthSecondary = 6; const placeNapataWall = getDifficulty() >= 3 && mapSize >= 192; const layoutFertileLandTextures = [ { "left": fractionToTiles(0), "right": fractionToTiles(0.04), "terrain": createTerrain(tGrassTransition1), "tileClass": clFertileLand }, { "left": fractionToTiles(0.04), "right": fractionToTiles(0.08), "terrain": createTerrain(tGrassTransition2), "tileClass": clDesert } ]; var layoutKushTemples = [ ...new Array(2).fill(0).map((v, i) => ({ "template": oTempleApedemak, "pathOffset": new Vector2D(0, 9), "minMapSize": i == 0 ? 320 : 0 })), { "template": oTempleAmun, "pathOffset": new Vector2D(0, 12), "minMapSize": 256 }, { "template": oWonderPtol, "pathOffset": new Vector2D(0, scaleByMapSize(9, 14)), "minMapSize": 0 }, { "template": oTempleAmun, "pathOffset": new Vector2D(0, 12), "minMapSize": 256 }, ...new Array(2).fill(0).map((v, i) => ({ "template": oTempleApedemak, "pathOffset": new Vector2D(0, 9), "minMapSize": i == 0 ? 320 : 0 })) ].filter(temple => mapSize >= temple.minMapSize); /** * The buildings are set as uncapturable, otherwise the player would gain the buildings via root territory and can delete them without effort. * Keep the entire city uncapturable as a consistent property of the city. */ const layoutKushCity = [ { "templateName": "uncapturable|" + oHouse, "difficulty": "Very Easy", "painters": new TileClassPainter(clHouse) }, { "templateName": "uncapturable|" + oFortress, "difficulty": "Medium", "constraints": [avoidClasses(clFortress, 25), new NearTileClassConstraint(clPath, 8)], "painters": new TileClassPainter(clFortress) }, { "templateName": "uncapturable|" + oCivicCenter, "difficulty": "Easy", "constraints": [avoidClasses(clCivicCenter, 60), new NearTileClassConstraint(clPath, 8)], "painters": new TileClassPainter(clCivicCenter) }, { "templateName": "uncapturable|" + oElephantStables, "difficulty": "Easy", "constraints": avoidClasses(clElephantStables, 10), "painters": new TileClassPainter(clElephantStables) }, { "templateName": "uncapturable|" + oStable, "difficulty": "Easy", "constraints": avoidClasses(clStable, 20), "painters": new TileClassPainter(clStable) }, { "templateName": "uncapturable|" + oBarracks, "difficulty": "Easy", "constraints": avoidClasses(clBarracks, 12), "painters": new TileClassPainter(clBarracks) }, { "templateName": "uncapturable|" + oTower, "difficulty": "Medium", "constraints": avoidClasses(clTower, 17), "painters": new TileClassPainter(clTower) }, { "templateName": "uncapturable|" + oMarket, "difficulty": "Very Easy", "constraints": avoidClasses(clMarket, 15), "painters": new TileClassPainter(clMarket) }, { "templateName": "uncapturable|" + oBlacksmith, "difficulty": "Very Easy", "constraints": avoidClasses(clBlacksmith, 30), "painters": new TileClassPainter(clBlacksmith) }, { "templateName": "uncapturable|" + oNubaVillage, "difficulty": "Easy", "constraints": avoidClasses(clNubaVillage, 30), "painters": new TileClassPainter(clNubaVillage) }, { "templateName": "uncapturable|" + oBlemmyeCamp, "difficulty": "Easy", "constraints": avoidClasses(clBlemmyeCamp, 30), "painters": new TileClassPainter(clBlemmyeCamp) } ].filter(building => getDifficulty() >= getDifficulties().find(difficulty => difficulty.Name == building.difficulty).Difficulty); g_WallStyles.napata = { "short": readyWallElement("uncapturable|" + oWallMedium), "medium": readyWallElement("uncapturable|" + oWallMedium), "tower": readyWallElement("uncapturable|" + oWallTower), "gate": readyWallElement("uncapturable|" + oWallGate), "overlap": 0.05 }; Engine.SetProgress(10); g_Map.log("Loading hill heightmap"); createArea( new MapBoundsPlacer(), new HeightmapPainter( translateHeightmap( new Vector2D(-12, scaleByMapSize(-12, -25)), undefined, convertHeightmap1Dto2D(Engine.LoadMapTerrain("maps/random/jebel_barkal.pmp").height)), minHeightSource, maxHeightSource)); const heightDesert = g_Map.getHeight(mapCenter); const heightFertileLand = heightDesert - heightScale(2); const heightShoreline = heightFertileLand - heightScale(0.5); const heightWaterLevel = heightFertileLand - heightScale(3); const heightPassage = heightWaterLevel - heightScale(1.5); const heightIrrigationCanal = heightWaterLevel - heightScale(4); const heightSeaGround = heightWaterLevel - heightScale(8); const heightHill = heightDesert + heightScale(4); const heightHilltop = heightHill + heightScale(90); const heightHillArchers = (heightHilltop + heightHill) / 2; const heightOffsetPath = heightScale(-2.5); const heightOffsetWalls = heightScale(2.5); const heightOffsetStatue = heightScale(2.5); g_Map.log("Flattening land"); createArea( new MapBoundsPlacer(), new ElevationPainter(heightDesert), new HeightConstraint(-Infinity, heightDesert)); // Fertile land paintRiver({ "parallel": true, "start": new Vector2D(mapBounds.left, mapBounds.bottom).rotateAround(-riverAngle, mapCenter), "end": new Vector2D(mapBounds.right, mapBounds.bottom).rotateAround(-riverAngle, mapCenter), "width": fractionToTiles(0.65), "fadeDist": 8, "deviation": 0, "heightLand": heightDesert, "heightRiverbed": heightFertileLand, "meanderShort": 40, "meanderLong": 0, "waterFunc": (position, height, riverFraction) => { createTerrain(tGrass).place(position); clFertileLand.add(position); }, "landFunc": (position, shoreDist1, shoreDist2) => { for (let riv of layoutFertileLandTextures) if (riv.left < +shoreDist1 && +shoreDist1 < riv.right || riv.left < -shoreDist2 && -shoreDist2 < riv.right) { riv.tileClass.add(position); riv.terrain.place(position); } } }); // Nile paintRiver({ "parallel": true, "start": new Vector2D(mapBounds.left, mapBounds.bottom).rotateAround(-riverAngle, mapCenter), "end": new Vector2D(mapBounds.right, mapBounds.bottom).rotateAround(-riverAngle, mapCenter), "width": fractionToTiles(0.2), "fadeDist": 4, "deviation": 0, "heightLand": heightFertileLand, "heightRiverbed": heightSeaGround, "meanderShort": 40, "meanderLong": 0 }); Engine.SetProgress(30); g_Map.log("Computing player locations"); -const playerIDs = primeSortAllPlayers(); -const playerPosition = playerPlacementCustomAngle( - fractionToTiles(0.38), +const playerIDs = sortAllPlayers(); +const playerPosition = playerPlacementArcs( + playerIDs, mapCenter, - i => Math.PI * (-0.42 / numPlayers * (i + i % 2) - (i % 2) / 2))[0]; + fractionToTiles(0.38), + riverAngle - 0.5 * Math.PI, + 0.05 * Math.PI, + 0.55 * Math.PI); if (!isNomad()) { g_Map.log("Marking player positions"); for (let position of playerPosition) addCivicCenterAreaToClass(position, clPlayer); } g_Map.log("Marking water"); createArea( new MapBoundsPlacer(), [ new TileClassPainter(clWater), new TileClassUnPainter(clFertileLand) ], new HeightConstraint(-Infinity, heightWaterLevel)); g_Map.log("Marking desert"); createArea( new MapBoundsPlacer(), new TileClassPainter(clDesert), [ new HeightConstraint(-Infinity, heightHill), avoidClasses(clWater, 0, clFertileLand, 0) ]); g_Map.log("Finding possible irrigation canal locations"); var irrigationCanalAreas = []; for (let i = 0; i < 30; ++i) { let x = fractionToTiles(randFloat(0, 1)); irrigationCanalAreas.push( createArea( new PathPlacer( new Vector2D(x, mapBounds.bottom).rotateAround(-riverAngle, mapCenter), new Vector2D(x, mapBounds.top).rotateAround(-riverAngle, mapCenter), 3, 0, 10, 0.1, 0.01, Infinity), undefined, avoidClasses(clDesert, 2))); } g_Map.log("Creating irrigation canals"); var irrigationCanalLocations = []; for (let area of irrigationCanalAreas) { if (!area.getPoints().length || area.getPoints().some(point => !avoidClasses(clPlayer, scaleByMapSize(8, 13), clIrrigationCanal, scaleByMapSize(15, 25)).allows(point))) continue; irrigationCanalLocations.push(pickRandom(area.getPoints()).clone().rotateAround(riverAngle, mapCenter).x); createArea( new MapBoundsPlacer(), [ new SmoothElevationPainter(ELEVATION_SET, heightIrrigationCanal, 1), new TileClassPainter(clIrrigationCanal) ], [new StayAreasConstraint([area]), new HeightConstraint(heightIrrigationCanal, heightDesert)]); } g_Map.log("Creating passages"); var areasPassages = []; irrigationCanalLocations.sort((a, b) => a - b); for (let i = 0; i < irrigationCanalLocations.length; ++i) { let previous = i == 0 ? mapBounds.left : irrigationCanalLocations[i - 1]; let next = i == irrigationCanalLocations.length - 1 ? mapBounds.right : irrigationCanalLocations[i + 1]; let x1 = (irrigationCanalLocations[i] + previous) / 2; let x2 = (irrigationCanalLocations[i] + next) / 2; let y; // The passages should be at different locations, so that enemies can't attack each other easily for (let tries = 0; tries < 100; ++tries) { y = randIntInclusive(0, mapCenter.y); let pos = new Vector2D((x1 + x2) / 2, y).rotateAround(-riverAngle, mapCenter).round(); if (g_Map.validTilePassable(new Vector2D(pos.x, pos.y)) && avoidClasses(clDesert, 12).allows(pos) && new HeightConstraint(heightIrrigationCanal, heightFertileLand).allows(pos)) break; } let area = createArea( new PathPlacer( new Vector2D(x1, y).rotateAround(-riverAngle, mapCenter), new Vector2D(x2, y).rotateAround(-riverAngle, mapCenter), 10, 0, 1, 0, 0, Infinity), [ new ElevationPainter(heightPassage), new TileClassPainter(clPassage) ], [ new HeightConstraint(-Infinity, heightPassage), stayClasses(clFertileLand, 2) ]); if (area && area.getPoints().length) areasPassages.push(area); } Engine.SetProgress(40); g_Map.log("Marking hill"); createArea( new MapBoundsPlacer(), new TileClassPainter(clHill), new HeightConstraint(heightHill, Infinity)); g_Map.log("Marking water"); const areaWater = createArea( new MapBoundsPlacer(), new TileClassPainter(clWater), new HeightConstraint(-Infinity, heightWaterLevel)); g_Map.log("Painting water and shoreline"); createArea( new MapBoundsPlacer(), new TerrainPainter(tWater), new HeightConstraint(-Infinity, heightShoreline)); g_Map.log("Painting hill"); const areaHill = createArea( new MapBoundsPlacer(), new TerrainPainter(tHillGround), new HeightConstraint(heightHill, Infinity)); g_Map.log("Painting hilltop"); const areaHilltop = createArea( new MapBoundsPlacer(), new TerrainPainter(tHilltop), [ new HeightConstraint(heightHilltop, Infinity), new SlopeConstraint(-Infinity, 2) ]); Engine.SetProgress(50); for (let i = 0; i < numPlayers; ++i) { let isDesert = clDesert.has(playerPosition[i]); placePlayerBase({ "playerID": playerIDs[i], "playerPosition": playerPosition[i], "PlayerTileClass": clPlayer, "BaseResourceClass": clBaseResource, "baseResourceConstraint": avoidClasses(clPlayer, 4, clWater, 4), "Walls": mapSize <= 256 || getDifficulty() >= 3 ? "towers" : "walls", "CityPatch": { "outerTerrain": isDesert ? tRoadDesert : tRoadFertileLand, "innerTerrain": isDesert ? tRoadDesert : tRoadFertileLand }, "Chicken": { "template": oGazelle, "distance": 15, "minGroupDistance": 2, "maxGroupDistance": 4, "minGroupCount": 2, "maxGroupCount": 3 }, "Berries": { "template": isDesert ? oBerryBushDesert : oBerryBushGrapes }, "Mines": { "types": [ { "template": oMetalLarge }, { "template": oStoneLarge } ] }, "Trees": { "template": isDesert ? oAcacia : pickRandom(oPalms), "count": isDesert ? scaleByMapSize(5, 10) : scaleByMapSize(15, 30) }, "Treasures": { "types": [ { "template": oWoodTreasure, "count": isDesert ? 4 : 0 }, { "template": oStoneTreasure, "count": isDesert ? 1 : 0 }, { "template": oMetalTreasure, "count": isDesert ? 1 : 0 } ] }, "Decoratives": { "template": isDesert ? aRock : pickRandom(aBushesFertileLand) } }); } g_Map.log("Placing pyramids"); const areaPyramids = createArea(new DiskPlacer(scaleByMapSize(5, 14), positionPyramids)); // Retry loops are needed due to the self-avoidance createObjectGroupsByAreas( new SimpleGroup( [new RandomObject( [oPyramidLarge, oPyramidSmall], scaleByMapSize(1, 6), scaleByMapSize(2, 8), scaleByMapSize(6, 8), scaleByMapSize(6, 14), Math.PI * 1.35, Math.PI * 1.5, scaleByMapSize(6, 8))], true, clPyramid), 0, undefined, 1, 50, [areaPyramids]); Engine.SetProgress(60); // The city is a circle segment of this maximum size g_Map.log("Computing city grid"); var gridCenter = new Vector2D(0, fractionToTiles(0.3)).rotate(-riverAngle).add(mapCenter).round(); var gridMaxAngle = scaleByMapSize(Math.PI / 3, Math.PI); var gridStartAngle = -Math.PI / 2 -gridMaxAngle / 2 + riverAngle; var gridRadius = y => hillRadius + 18 * y; var gridPointsX = layoutKushTemples.length; var gridPointsY = Math.floor(scaleByMapSize(2, 4)); var gridPointXCenter = Math.floor(gridPointsX / 2); var gridPointYCenter = Math.floor(gridPointsY / 2); // Maps from grid position to map position var cityGridPosition = []; var cityGridAngle = []; for (let y = 0; y < gridPointsY; ++y) [cityGridPosition[y], cityGridAngle[y]] = distributePointsOnCircularSegment( gridPointsX, gridMaxAngle, gridStartAngle, gridRadius(y), gridCenter); g_Map.log("Marking city path crossings"); for (let y in cityGridPosition) for (let x in cityGridPosition[y]) { cityGridPosition[y][x].round(); createArea( new DiskPlacer(pathWidth, cityGridPosition[y][x]), [ new TileClassPainter(clPath), new TileClassPainter(clPathCrossing) ]); } g_Map.log("Marking horizontal city paths"); for (let y = 0; y < gridPointsY; ++y) for (let x = 1; x < gridPointsX; ++x) { let width = y == gridPointYCenter ? pathWidthSecondary : pathWidth; createArea( new PathPlacer(cityGridPosition[y][x - 1], cityGridPosition[y][x], width, 0, 8, 0, 0, Infinity), new TileClassPainter(clPath)); } g_Map.log("Marking vertical city paths"); for (let y = 1; y < gridPointsY; ++y) for (let x = 0; x < gridPointsX; ++x) { let width = Math.abs(x - gridPointXCenter) == 0 ? pathWidthCenter : Math.abs(x - gridPointXCenter) == 1 ? pathWidthSecondary : pathWidth; createArea( new PathPlacer(cityGridPosition[y - 1][x], cityGridPosition[y][x], width, 0, 8, 0, 0, Infinity), new TileClassPainter(clPath)); } Engine.SetProgress(70); g_Map.log("Placing kushite temples"); var entitiesTemples = []; var templePosition = []; for (let i = 0; i < layoutKushTemples.length; ++i) { let x = i + (gridPointsX - layoutKushTemples.length) / 2; templePosition[i] = Vector2D.add(cityGridPosition[0][x], layoutKushTemples[i].pathOffset.rotate(-Math.PI / 2 - cityGridAngle[0][x])); entitiesTemples[i] = g_Map.placeEntityPassable(layoutKushTemples[i].template, 0, templePosition[i], cityGridAngle[0][x]); } g_Map.log("Marking temple area"); createArea( new EntitiesObstructionPlacer(entitiesTemples, 0, Infinity), new TileClassPainter(clTemple)); g_Map.log("Smoothing temple ground"); createArea( new MapBoundsPlacer(), new ElevationBlendingPainter(heightDesert, 0.8), new NearTileClassConstraint(clTemple, 0)); g_Map.log("Painting cliffs"); createArea( new MapBoundsPlacer(), [ new TerrainPainter(tHillCliff), new TileClassPainter(clCliff) ], [ stayClasses(clHill, 0), new SlopeConstraint(2, Infinity) ]); g_Map.log("Painting temple ground"); createArea( new MapBoundsPlacer(), new TerrainPainter(tPathWild), [ new NearTileClassConstraint(clTemple, 1), avoidClasses(clPath, 0, clCliff, 1) ]); g_Map.log("Placing lion statues in the central path"); var statueCount = scaleByMapSize(10, 40); var centralPathStart = cityGridPosition[0][gridPointXCenter]; var centralPathLength = centralPathStart.distanceTo(cityGridPosition[gridPointsY - 1][gridPointXCenter]); var centralPathAngle = cityGridAngle[0][gridPointXCenter]; for (let i = 0; i < 2; ++i) for (let stat = 0; stat < statueCount; ++stat) { let start = new Vector2D(0, pathWidthCenter * 3/4 * (i - 0.5)).rotate(centralPathAngle).add(centralPathStart); let position = new Vector2D(centralPathLength, 0).mult(stat / statueCount).rotate(-centralPathAngle).add(start).add(new Vector2D(0.5, 0.5)); if (!avoidClasses(clPathCrossing, 2).allows(position)) continue; g_Map.placeEntityPassable(pickRandom(aStatues), 0, position, centralPathAngle - Math.PI * (i + 0.5)); clPathStatues.add(position.round()); } g_Map.log("Placing guardian infantry in the central path"); var centralChampionsCount = scaleByMapSize(2, 40); for (let i = 0; i < 2; ++i) for (let champ = 0; champ < centralChampionsCount; ++champ) { let start = new Vector2D(0, pathWidthCenter * 1/2 * (i - 0.5)).rotate(-centralPathAngle).add(centralPathStart); let position = new Vector2D(centralPathLength, 0).mult(champ / centralChampionsCount).rotate(-centralPathAngle).add(start).add(new Vector2D(0.5, 0.5)); if (!avoidClasses(clPathCrossing, 2).allows(position)) continue; g_Map.placeEntityPassable(pickRandom(oKushChampions), 0, position, centralPathAngle - Math.PI * (i - 0.5)); clPathStatues.add(position.round()); } g_Map.log("Placing kushite statues in the secondary paths"); for (let x of [gridPointXCenter - 1, gridPointXCenter + 1]) { g_Map.placeEntityAnywhere(aStatueKush, 0, cityGridPosition[gridPointYCenter][x], cityGridAngle[gridPointYCenter][x]); clPathStatues.add(cityGridPosition[gridPointYCenter][x]); } g_Map.log("Creating ritual place near the wonder"); var ritualPosition = Vector2D.average([ templePosition[Math.floor(templePosition.length / 2) - 1], templePosition[Math.ceil(templePosition.length / 2) - 1], cityGridPosition[0][gridPointXCenter], cityGridPosition[0][gridPointXCenter - 1] ]).round(); var ritualAngle = (cityGridAngle[0][gridPointXCenter] + cityGridAngle[0][gridPointXCenter - 1]) / 2 + Math.PI / 2; g_Map.placeEntityPassable(aStatueKush, 0, ritualPosition, ritualAngle - Math.PI / 2); createArea( new DiskPlacer(scaleByMapSize(4, 6), ritualPosition), [ new LayeredPainter([tPathWild, tPath], [1]), new SmoothElevationPainter(ELEVATION_MODIFY, heightOffsetPath, 2), new TileClassPainter(clRitualPlace) ], avoidClasses(clCliff, 1)); createArea( new DiskPlacer(0, new Vector2D(-1, -1).add(ritualPosition)), new ElevationPainter(heightDesert + heightOffsetStatue)); g_Map.log("Placing healers at the ritual place"); var [healerPosition, healerAngle] = distributePointsOnCircularSegment( scaleByMapSize(2, 10), Math.PI, ritualAngle, scaleByMapSize(2, 3), ritualPosition); for (let i = 0; i < healerPosition.length; ++i) g_Map.placeEntityPassable(oKushHealer, 0, healerPosition[i], healerAngle[i] + Math.PI); g_Map.log("Placing statues at the ritual place"); var [statuePosition, statueAngle] = distributePointsOnCircularSegment( scaleByMapSize(4, 8), Math.PI, ritualAngle, scaleByMapSize(3, 4), ritualPosition); for (let i = 0; i < statuePosition.length; ++i) g_Map.placeEntityPassable(pickRandom(aStatues), 0, statuePosition[i], statueAngle[i] + Math.PI); g_Map.log("Placing palms at the ritual place"); var [palmPosition, palmAngle] = distributePointsOnCircularSegment( scaleByMapSize(6, 16), Math.PI, ritualAngle, scaleByMapSize(4, 5), ritualPosition); for (let i = 0; i < palmPosition.length; ++i) if (avoidClasses(clTemple, 1).allows(palmPosition[i])) g_Map.placeEntityPassable(oPalmPath, 0, palmPosition[i], randomAngle()); g_Map.log("Painting city paths"); var areaPaths = createArea( new MapBoundsPlacer(), [ new LayeredPainter([tPathWild, tPath], [1]), new SmoothElevationPainter(ELEVATION_MODIFY, heightOffsetPath, 1) ], stayClasses(clPath, 0)); g_Map.log("Placing triggerpoints on city paths"); createObjectGroupsByAreas( new SimpleGroup([new SimpleObject(oTriggerPointCityPath, 1, 1, 0, 0)], true, clTriggerPointCityPath), 0, [avoidClasses(clTriggerPointCityPath, 8), stayClasses(clPathCrossing, 2)], scaleByMapSize(20, 100), 30, [areaPaths]); g_Map.log("Placing city districts"); for (let y = 1; y < gridPointsY; ++y) for (let x = 1; x < gridPointsX; ++x) createArea( new ConvexPolygonPlacer([cityGridPosition[y - 1][x - 1], cityGridPosition[y - 1][x], cityGridPosition[y][x - 1], cityGridPosition[y][x]], Infinity), [ new TerrainPainter(tRoadDesert), new CityPainter(layoutKushCity, (-cityGridAngle[y][x - 1] - cityGridAngle[y][x]) / 2, 0), new TileClassPainter(clCity) ], new StaticConstraint(avoidClasses(clPath, 0))); if (placeNapataWall) { g_Map.log("Placing front walls"); let wallGridMaxAngleSummand = Math.PI / 32; let wallGridStartAngle = gridStartAngle - wallGridMaxAngleSummand / 2; let wallGridRadiusFront = gridRadius(gridPointsY - 1) + pathWidth - 1; let wallGridMaxAngleFront = gridMaxAngle + wallGridMaxAngleSummand; let entitiesWalls = placeCircularWall( gridCenter, wallGridRadiusFront, ["tower", "short", "tower", "gate", "tower", "medium", "tower", "short"], "napata", 0, wallGridStartAngle, wallGridMaxAngleFront, true, 0, 0); g_Map.log("Placing side and back walls"); let wallGridRadiusBack = hillRadius - scaleByMapSize(15, 25); let wallGridMaxAngleBack = gridMaxAngle + wallGridMaxAngleSummand; let wallGridPositionFront = distributePointsOnCircularSegment(gridPointsX, wallGridMaxAngleBack, wallGridStartAngle, wallGridRadiusFront, gridCenter)[0]; let wallGridPositionBack = distributePointsOnCircularSegment(gridPointsX, wallGridMaxAngleBack, wallGridStartAngle, wallGridRadiusBack, gridCenter)[0]; let wallGridPosition = [wallGridPositionFront[0], ...wallGridPositionBack, wallGridPositionFront[wallGridPositionFront.length - 1]]; for (let x = 1; x < wallGridPosition.length; ++x) entitiesWalls = entitiesWalls.concat( placeLinearWall( wallGridPosition[x - 1], wallGridPosition[x], ["tower", "gate", "tower", "short", "tower", "short", "tower"], "napata", 0, false, avoidClasses(clHill, 0, clTemple, 0))); g_Map.log("Marking walls"); createArea( new EntitiesObstructionPlacer(entitiesWalls, 0, Infinity), new TileClassPainter(clWall)); g_Map.log("Marking gates"); let entitiesGates = entitiesWalls.filter(entity => entity.templateName.endsWith(oWallGate)); createArea( new EntitiesObstructionPlacer(entitiesGates, 0, Infinity), new TileClassPainter(clGate)); g_Map.log("Painting wall terrain"); createArea( new MapBoundsPlacer(), [ new SmoothElevationPainter(ELEVATION_MODIFY, heightOffsetWalls, 2), new TerrainPainter(tPathWild) ], [ new NearTileClassConstraint(clWall, 1), avoidClasses(clCliff, 0) ]); g_Map.log("Painting gate terrain"); for (let entity of entitiesGates) createArea( new DiskPlacer(pathWidth, entity.GetPosition2D()), [ new LayeredPainter([tPathWild, tPath], [1]), new SmoothElevationPainter(ELEVATION_MODIFY, heightOffsetPath, 2), ], [ avoidClasses(clCliff, 0, clPath, 0, clCity, 0), new NearTileClassConstraint(clPath, pathWidth + 1) ]); } Engine.SetProgress(70); g_Map.log("Marking city bush area"); var areaCityBushes = createArea( new MapBoundsPlacer(), undefined, [ new NearTileClassConstraint(clPath, 1), avoidClasses( clPath, 0, clPyramid, 20, clRitualPlace, 8, clTemple, 3, clWall, 3, clTower, 1, clFortress, 1, clHouse, 1, clBlacksmith, 1, clElephantStables, 1, clStable, 1, clCivicCenter, 1, clBarracks, 1, clBlemmyeCamp, 1, clNubaVillage, 1, clMarket, 1) ]); g_Map.log("Marking city palm area"); var areaCityPalms = createArea( new MapBoundsPlacer(), undefined, [ new StayAreasConstraint([areaCityBushes]), avoidClasses(clElephantStables, 3) ]); g_Map.log("Placing city palms"); createObjectGroupsByAreas( new SimpleGroup([new SimpleObject(aPalmPath, 1, 1, 0, 0)], true, clForest), 0, avoidClasses(clForest, 3), scaleByMapSize(40, 400), 15, [areaCityPalms]); g_Map.log("Placing city bushes"); createObjectGroupsByAreas( new SimpleGroup([new RandomObject(aBushesCity, 1, 1, 0, 0)], true, clForest), 0, avoidClasses(clForest, 1), scaleByMapSize(20, 200), 15, [areaCityBushes]); if (placeNapataWall) { g_Map.log("Marking wall palm area"); var areaWallPalms = createArea( new MapBoundsPlacer(), undefined, new StaticConstraint([ new NearTileClassConstraint(clWall, 2), avoidClasses(clPath, 1, clWall, 1, clGate, 3, clTemple, 2, clHill, 6) ])); g_Map.log("Placing wall palms"); createObjectGroupsByAreas( new SimpleGroup([new SimpleObject(oPalmPath, 1, 1, 0, 0)], true, clForest), 0, avoidClasses(clForest, 2), scaleByMapSize(40, 200), 50, [areaWallPalms]); } createBumps(new StaticConstraint(avoidClasses(clPlayer, 6, clCity, 0, clWater, 2, clHill, 0, clPath, 0, clTemple, 4, clPyramid, 8)), scaleByMapSize(30, 300), 1, 8, 4, 0, 3); Engine.SetProgress(75); g_Map.log("Setting up common constraints"); const stayDesert = new StaticConstraint(stayClasses(clDesert, 0)); const stayFertileLand = new StaticConstraint(stayClasses(clFertileLand, 0)); const nearWater = new NearTileClassConstraint(clWater, 3); var avoidCollisions = new AndConstraint( [ new StaticConstraint(avoidClasses( clCliff, 0, clHill, 0, clPlayer, 15, clWater, 1, clPath, 2, clRitualPlace, 10, clTemple, 4, clPyramid, 7, clCity, 4, clWall, 4, clGate, 8)), avoidClasses(clForest, 1, clRock, 4, clMetal, 4, clFood, 6, clSoldier, 1, clTreasure, 1) ]); g_Map.log("Setting up common areas"); const areaDesert = createArea(new MapBoundsPlacer(), undefined, stayDesert); const areaFertileLand = createArea(new MapBoundsPlacer(), undefined, stayFertileLand); createForests( [tForestFloorFertile, tForestFloorFertile, tForestFloorFertile, pForestPalms, pForestPalms], [stayFertileLand, avoidClasses(clForest, 15), new StaticConstraint([avoidClasses(clWater, 2), avoidCollisions])], clForest, scaleByMapSize(250, 2000)); const avoidCollisionsMines = new StaticConstraint([ isNomad() ? new NullConstraint() : avoidClasses(clFertileLand, 10), avoidClasses( clWater, 4, clCliff, 4, clCity, 4, clRitualPlace, 10, clPlayer, 20, clForest, 4, clPyramid, 6, clTemple, 4, clPath, 4, clGate, 8)]); g_Map.log("Creating stone mines"); createMines( [ [new SimpleObject(oStoneSmall, 0, 2, 0, 4, 0, 2 * Math.PI, 1), new SimpleObject(oStoneLarge, 1, 1, 0, 4, 0, 2 * Math.PI, 4)], [new SimpleObject(oStoneSmall, 2, 5, 1, 3, 0, 2 * Math.PI, 1)] ], [avoidCollisionsMines, avoidClasses(clRock, 10)], clRock, scaleByMapSize(8, 26)); g_Map.log("Creating metal mines"); createMines( [ [new SimpleObject(oMetalSmall, 0, 2, 0, 4, 0, 2 * Math.PI, 1), new SimpleObject(oMetalLarge, 1, 1, 0, 4, 0, 2 * Math.PI, 4)], [new SimpleObject(oMetalSmall, 2, 5, 1, 3, 0, 2 * Math.PI, 1)] ], [avoidCollisionsMines, avoidClasses(clMetal, 10, clRock, 5)], clMetal, scaleByMapSize(8, 26)); g_Map.log("Placing triggerpoints for attackers"); createObjectGroups( new SimpleGroup([new SimpleObject(oTriggerPointAttackerPatrol, 1, 1, 0, 0)], true, clTriggerPointMap), 0, [avoidClasses(clCity, 8, clCliff, 4, clHill, 4, clWater, 0, clWall, 2, clForest, 1, clRock, 4, clMetal, 4, clTriggerPointMap, 15)], scaleByMapSize(20, 100), 30); g_Map.log("Creating berries"); createObjectGroupsByAreas( new SimpleGroup([new SimpleObject(oBerryBushGrapes, 4, 6, 1, 2)], true, clFood), 0, avoidCollisions, scaleByMapSize(3, 15), 50, [areaFertileLand]); g_Map.log("Creating rhinos"); createObjectGroupsByAreas( new SimpleGroup([new SimpleObject(oRhino, 1, 1, 0, 1)], true, clFood), 0, avoidCollisions, scaleByMapSize(2, 10), 50, [areaDesert]); g_Map.log("Creating warthogs"); createObjectGroupsByAreas( new SimpleGroup([new SimpleObject(oWarthog, 1, 1, 0, 1)], true, clFood), 0, avoidCollisions, scaleByMapSize(2, 10), 50, [areaFertileLand]); g_Map.log("Creating giraffes"); createObjectGroups( new SimpleGroup([new SimpleObject(oGiraffe, 2, 3, 2, 4), new SimpleObject(oGiraffeInfant, 2, 3, 2, 4)], true, clFood), 0, avoidCollisions, scaleByMapSize(2, 10), 50); g_Map.log("Creating gazelles"); createObjectGroups( new SimpleGroup([new SimpleObject(oGazelle, 5, 7, 2, 4)], true, clFood), 0, avoidCollisions, scaleByMapSize(2, 10), 50, [areaDesert]); if (!isNomad()) { g_Map.log("Creating lions"); createObjectGroupsByAreas( new SimpleGroup([new SimpleObject(oLion, 1, 2, 2, 4), new SimpleObject(oLioness, 2, 3, 2, 4)], true, clFood), 0, [avoidCollisions, avoidClasses(clPlayer, 20)], scaleByMapSize(2, 10), 50, [areaDesert]); } g_Map.log("Creating elephants"); createObjectGroupsByAreas( new SimpleGroup([new SimpleObject(oElephant, 2, 3, 2, 4), new SimpleObject(oElephantInfant, 2, 3, 2, 4)], true, clFood), 0, avoidCollisions, scaleByMapSize(2, 10), 50, [areaDesert]); g_Map.log("Creating crocodiles"); if (!isNomad()) createObjectGroupsByAreas( new SimpleGroup([new SimpleObject(oCrocodile, 2, 3, 3, 5)], true, clFood), 0, [nearWater, avoidCollisions], scaleByMapSize(1, 6), 50, [areaFertileLand]); Engine.SetProgress(85); g_Map.log("Marking irrigation canal tree area"); var areaIrrigationCanalTrees = createArea( new MapBoundsPlacer(), undefined, [ nearWater, avoidClasses(clPassage, 3), avoidCollisions ]); g_Map.log("Creating irrigation canal trees"); createObjectGroupsByAreas( new SimpleGroup([new RandomObject(oPalms, 1, 1, 1, 1)], true, clForest), 0, avoidClasses(clForest, 1), scaleByMapSize(100, 600), 50, [areaIrrigationCanalTrees]); createStragglerTrees( oPalms, [stayFertileLand, avoidCollisions], clForest, scaleByMapSize(50, 400), 200); createStragglerTrees( [oAcacia], [stayDesert, avoidCollisions], clForest, scaleByMapSize(50, 400), 200); g_Map.log("Placing archer groups on the hilltop"); createObjectGroupsByAreas( new SimpleGroup([new RandomObject([oKushCitizenArcher, oKushChampionArcher], scaleByMapSize(4, 10), scaleByMapSize(6, 20), 1, 4)], true, clSoldier), 0, new StaticConstraint([avoidClasses(clCliff, 1), new NearTileClassConstraint(clCliff, 5)]), scaleByMapSize(1, 5) / 3 * getDifficulty(), 250, [areaHilltop]); g_Map.log("Placing individual archers on the hill"); createObjectGroupsByAreas( new SimpleGroup([new RandomObject([oKushCitizenArcher, oKushChampionArcher], 1, 1, 1, 3)], true, clSoldier), 0, new StaticConstraint([ new HeightConstraint(heightHillArchers, heightHilltop), avoidClasses(clCliff, 1, clSoldier, 1), new NearTileClassConstraint(clCliff, 5) ]), scaleByMapSize(8, 100) / 3 * getDifficulty(), 250, [areaHill]); g_Map.log("Placing siege engines on the hilltop"); createObjectGroupsByAreas( new SimpleGroup([new RandomObject(oPtolSiege, 1, 1, 1, 3)], true, clSoldier), 0, new StaticConstraint([new NearTileClassConstraint(clCliff, 5), avoidClasses(clCliff, 1, clSoldier, 1)]), scaleByMapSize(1, 6) / 3 * getDifficulty(), 250, [areaHilltop]); const avoidCollisionsPyramids = new StaticConstraint([avoidCollisions, new NearTileClassConstraint(clPyramid, 10)]); if (!isNomad()) { g_Map.log("Placing soldiers near pyramids"); createObjectGroupsByAreas( new SimpleGroup([new SimpleObject(oKushCitizenArcher, 1, 1, 1, 1)], true, clSoldier), 0, avoidCollisionsPyramids, scaleByMapSize(3, 8), 250, [areaPyramids]); g_Map.log("Placing treasures at the pyramid"); createObjectGroupsByAreas( new SimpleGroup([new RandomObject(oTreasuresHill, 1, 1, 2, 2)], true, clTreasure), 0, avoidCollisionsPyramids, scaleByMapSize(1, 10), 250, [areaPyramids]); } g_Map.log("Placing treasures on the hilltop"); createObjectGroupsByAreas( new SimpleGroup([new RandomObject(oTreasuresHill, 1, 1, 2, 2)], true, clTreasure), 0, avoidClasses(clCliff, 1, clTreasure, 1), scaleByMapSize(8, 35), 250, [areaHilltop]); g_Map.log("Placing treasures in the city"); var pathBorderConstraint = new AndConstraint([ new StaticConstraint([new NearTileClassConstraint(clCity, 1)]), avoidClasses(clTreasure, 2, clStatue, 10, clPathStatues, 4, clWall, 2, clForest, 1) ]); createObjectGroupsByAreas( new SimpleGroup([new RandomObject(oTreasuresCity, 1, 1, 0, 2)], true, clTreasure), 0, pathBorderConstraint, scaleByMapSize(2, 60), 500, [areaPaths]); g_Map.log("Placing handcarts on the paths"); createObjectGroupsByAreas( new SimpleGroup([new SimpleObject(aHandcart, 1, 1, 1, 1)], true, clDecorative), 0, [pathBorderConstraint, avoidClasses(clDecorative, 10)], scaleByMapSize(0, 5), 250, [areaPaths]); g_Map.log("Placing fence in fertile land"); createObjectGroupsByAreas( new SimpleGroup([new SimpleObject(aPlotFence, 1, 1, 1, 1)], true, clDecorative), 0, new StaticConstraint(avoidCollisions, avoidClasses(clWater, 6, clDecorative, 10)), scaleByMapSize(1, 10), 250, [areaFertileLand]); g_Map.log("Creating fish"); createObjectGroups( new SimpleGroup([new SimpleObject(oFish, 3, 4, 2, 3)], true, clFood), 0, [new StaticConstraint(stayClasses(clWater, 6)), avoidClasses(clFood, 12)], scaleByMapSize(20, 120), 50); Engine.SetProgress(95); avoidCollisions = new StaticConstraint(avoidCollisions); createDecoration( aBushesDesert.map(bush => [new SimpleObject(bush, 0, 3, 2, 4)]), aBushesDesert.map(bush => scaleByMapSize(20, 150) * randIntInclusive(1, 3)), [stayDesert, avoidCollisions]); createDecoration( aBushesFertileLand.map(bush => [new SimpleObject(bush, 0, 4, 2, 4)]), aBushesFertileLand.map(bush => scaleByMapSize(20, 150) * randIntInclusive(1, 3)), [stayFertileLand, avoidCollisions]); createDecoration( [[new SimpleObject(aRock, 0, 4, 2, 4)]], [[scaleByMapSize(80, 500)]], [stayDesert, avoidCollisions]); createDecoration( aBushesFertileLand.map(bush => [new SimpleObject(bush, 0, 3, 2, 4)]), aBushesFertileLand.map(bush => scaleByMapSize(100, 800)), [new HeightConstraint(heightWaterLevel, heightShoreline), avoidCollisions]); g_Map.log("Creating reeds"); createObjectGroupsByAreas( new SimpleGroup([new RandomObject(aWaterDecoratives, 2, 4, 1, 2)], true), 0, new StaticConstraint(new NearTileClassConstraint(clFertileLand, 4)), scaleByMapSize(50, 400), 20, [areaWater]); g_Map.log("Creating reeds at the irrigation canals"); if (areasPassages.length) createObjectGroupsByAreas( new SimpleGroup([new RandomObject(aWaterDecoratives, 2, 4, 1, 2)], true), 0, undefined, scaleByMapSize(50, 200), 20, areasPassages); g_Map.log("Creating hawk"); for (let i = 0; i < scaleByMapSize(0, 2); ++i) g_Map.placeEntityAnywhere(oHawk, 0, mapCenter, randomAngle()); placePlayersNomad(clPlayer, [avoidClasses(clHill, 15, clSoldier, 20, clCity, 15), avoidCollisions]); setWindAngle(-0.43); setWaterHeight(heightWaterLevel + SEA_LEVEL); setWaterTint(0.161, 0.286, 0.353); setWaterColor(0.129, 0.176, 0.259); setWaterWaviness(8); setWaterMurkiness(0.87); setWaterType("lake"); setTerrainAmbientColor(0.58, 0.443, 0.353); setSunColor(0.733, 0.746, 0.574); setSunRotation(Math.PI / 2 * randFloat(-1, 1)); setSunElevation(Math.PI / 7); setFogFactor(0); setFogThickness(0); setFogColor(0.69, 0.616, 0.541); setPPEffect("hdr"); setPPContrast(0.67); setPPSaturation(0.42); setPPBloom(0.23); g_Map.ExportMap(); Index: ps/trunk/binaries/data/mods/public/maps/random/pyrenean_sierra.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/pyrenean_sierra.js (revision 21702) +++ ps/trunk/binaries/data/mods/public/maps/random/pyrenean_sierra.js (revision 21703) @@ -1,457 +1,463 @@ Engine.LoadLibrary("rmgen"); Engine.LoadLibrary("rmgen-common"); TILE_CENTERED_HEIGHT_MAP = true; const tGrassSpecific = ["new_alpine_grass_d","new_alpine_grass_d", "new_alpine_grass_e"]; const tGrass = ["new_alpine_grass_d", "new_alpine_grass_b", "new_alpine_grass_e"]; const tGrassMidRange = ["new_alpine_grass_b", "alpine_grass_a"]; const tGrassHighRange = ["new_alpine_grass_a", "alpine_grass_a", "alpine_grass_rocky"]; const tHighRocks = ["alpine_cliff_b", "alpine_cliff_c","alpine_cliff_c", "alpine_grass_rocky"]; const tSnowedRocks = ["alpine_cliff_b", "alpine_cliff_snow"]; const tTopSnow = ["alpine_snow_rocky","alpine_snow_a"]; const tTopSnowOnly = ["alpine_snow_a"]; const tDirtyGrass = ["new_alpine_grass_d","alpine_grass_d","alpine_grass_c", "alpine_grass_b"]; const tLushGrass = ["new_alpine_grass_a","new_alpine_grass_d"]; const tMidRangeCliffs = ["alpine_cliff_b","alpine_cliff_c"]; const tHighRangeCliffs = ["alpine_mountainside","alpine_cliff_snow" ]; const tSand = ["beach_c", "beach_d"]; const tSandTransition = ["beach_scrub_50_"]; const tWater = ["sand_wet_a","sand_wet_b","sand_wet_b","sand_wet_b"]; const tGrassLandForest = "alpine_forrestfloor"; const tGrassLandForest2 = "alpine_grass_d"; const tForestTransition = ["new_alpine_grass_d", "new_alpine_grass_b","alpine_grass_d"]; const tRoad = "new_alpine_citytile"; const tRoadWild = "new_alpine_citytile"; const oBeech = "gaia/flora_tree_euro_beech"; const oPine = "gaia/flora_tree_aleppo_pine"; const oBerryBush = "gaia/flora_bush_berry"; const oDeer = "gaia/fauna_deer"; const oFish = "gaia/fauna_fish"; const oRabbit = "gaia/fauna_rabbit"; const oStoneLarge = "gaia/geology_stonemine_alpine_quarry"; const oStoneSmall = "gaia/geology_stone_alpine_a"; const oMetalLarge = "gaia/geology_metal_alpine_slabs"; const aGrass = "actor|props/flora/grass_soft_small_tall.xml"; const aGrassShort = "actor|props/flora/grass_soft_large.xml"; const aRockLarge = "actor|geology/stone_granite_med.xml"; const aRockMedium = "actor|geology/stone_granite_med.xml"; const aBushMedium = "actor|props/flora/bush_medit_me.xml"; const aBushSmall = "actor|props/flora/bush_medit_sm.xml"; const pForestLand = [tGrassLandForest + TERRAIN_SEPARATOR + oPine,tGrassLandForest + TERRAIN_SEPARATOR + oBeech, tGrassLandForest2 + TERRAIN_SEPARATOR + oPine,tGrassLandForest2 + TERRAIN_SEPARATOR + oBeech, tGrassLandForest,tGrassLandForest2,tGrassLandForest2,tGrassLandForest2]; const pForestLandLight = [tGrassLandForest + TERRAIN_SEPARATOR + oPine,tGrassLandForest + TERRAIN_SEPARATOR + oBeech, tGrassLandForest2 + TERRAIN_SEPARATOR + oPine,tGrassLandForest2 + TERRAIN_SEPARATOR + oBeech, tGrassLandForest,tGrassLandForest2,tForestTransition,tGrassLandForest2, tGrassLandForest,tForestTransition,tGrassLandForest2,tForestTransition, tGrassLandForest2,tGrassLandForest2,tGrassLandForest2,tGrassLandForest2]; const pForestLandVeryLight = [ tGrassLandForest2 + TERRAIN_SEPARATOR + oPine,tGrassLandForest2 + TERRAIN_SEPARATOR + oBeech, tForestTransition,tGrassLandForest2,tForestTransition,tForestTransition,tForestTransition, tGrassLandForest,tForestTransition,tGrassLandForest2,tForestTransition, tGrassLandForest2,tGrassLandForest2,tGrassLandForest2,tGrassLandForest2]; const heightInit = -100; const heightOcean = -22; const heightWaterTerrain = -14; const heightBase = -6; const heightSand = -2; const heightSandTransition = 0; const heightGrass = 6; const heightWaterLevel = 8; const heightPyreneans = 15; const heightGrassMidRange = 18; const heightGrassHighRange = 30; const heightPassage = scaleByMapSize(25, 40); const heightHighRocks = heightPassage + 5; const heightSnowedRocks = heightHighRocks + 10; const heightMountain = heightHighRocks + 20; const heightOffsetHill = 7; const heightOffsetHillRandom = 2; var g_Map = new RandomMap(heightInit, tGrass); const numPlayers = getNumPlayers(); const mapSize = g_Map.getSize(); const mapCenter = g_Map.getCenter(); var clDirt = g_Map.createTileClass(); var clRock = g_Map.createTileClass(); var clMetal = g_Map.createTileClass(); var clFood = g_Map.createTileClass(); var clBaseResource = g_Map.createTileClass(); var clPass = g_Map.createTileClass(); var clPyrenneans = g_Map.createTileClass(); var clPlayer = g_Map.createTileClass(); var clHill = g_Map.createTileClass(); var clForest = g_Map.createTileClass(); var clWater = g_Map.createTileClass(); var startAngle = randomAngle(); var oceanAngle = startAngle + randFloat(-1, 1) * Math.PI / 12; var mountainLength = fractionToTiles(0.68); var mountainWidth = scaleByMapSize(15, 55); var mountainPeaks = 100 * scaleByMapSize(1, 10); var mountainOffset = randFloat(-1, 1) * scaleByMapSize(1, 12); var passageLength = scaleByMapSize(8, 50); var terrainPerHeight = [ { "maxHeight": heightGrass, "steepness": 5, "terrainGround": tGrass, "terrainSteep": tMidRangeCliffs }, { "maxHeight": heightGrassMidRange, "steepness": 8, "terrainGround": tGrassMidRange, "terrainSteep": tMidRangeCliffs }, { "maxHeight": heightGrassHighRange, "steepness": 8, "terrainGround": tGrassHighRange, "terrainSteep": tMidRangeCliffs }, { "maxHeight": heightHighRocks, "steepness": 8, "terrainGround": tHighRocks, "terrainSteep": tHighRangeCliffs }, { "maxHeight": heightSnowedRocks, "steepness": 7, "terrainGround": tSnowedRocks, "terrainSteep": tHighRangeCliffs }, { "maxHeight": Infinity, "steepness": 6, "terrainGround": tTopSnowOnly, "terrainSteep": tTopSnow } ]; g_Map.log("Creating initial sinusoidal noise"); var baseHeights = []; for (var ix = 0; ix < mapSize; ix++) { baseHeights.push([]); for (var iz = 0; iz < mapSize; iz++) { let position = new Vector2D(ix, iz); if (g_Map.inMapBounds(position)) { let height = heightBase + randFloat(-1, 1) + scaleByMapSize(1, 3) * (Math.cos(ix / scaleByMapSize(5, 30)) + Math.sin(iz / scaleByMapSize(5, 30))); g_Map.setHeight(position, height); baseHeights[ix].push(height); } else baseHeights[ix].push(heightInit); } } +var playerIDs = sortAllPlayers(); +var playerPosition = playerPlacementArcs( + playerIDs, + mapCenter, + fractionToTiles(0.35), + oceanAngle, + 0.1 * Math.PI, + 0.9 * Math.PI); + placePlayerBases({ - "PlayerPlacement": [primeSortAllPlayers(), ...playerPlacementCustomAngle( - fractionToTiles(0.35), - mapCenter, - i => oceanAngle + Math.PI * (i % 2 ? 1 : -1) * ((1/2 + 1/3 * (2/numPlayers * (i + 1 - i % 2) - 1))))], + "PlayerPlacement": [playerIDs, playerPosition], "PlayerTileClass": clPlayer, "BaseResourceClass": clBaseResource, "CityPatch": { "outerTerrain": tRoadWild, "innerTerrain": tRoad }, "Chicken": { }, "Berries": { "template": oBerryBush }, "Mines": { "types": [ { "template": oMetalLarge }, { "template": oStoneLarge } ] }, "Trees": { "template": oPine }, "Decoratives": { "template": aGrassShort } }); Engine.SetProgress(30); g_Map.log("Creating the pyreneans"); var mountainVec = new Vector2D(mountainLength, 0).rotate(-startAngle); var mountainStart = Vector2D.sub(mapCenter, Vector2D.div(mountainVec, 2)); var mountainDirection = mountainVec.clone().normalize(); createPyreneans(); paintTileClassBasedOnHeight(heightPyreneans, Infinity, Elevation_ExcludeMin_ExcludeMax, clPyrenneans); Engine.SetProgress(40); /** * Generates the mountain peak noise. * * @param {number} x - between 0 and 1 * @returns {number} between 0 and 1 */ function sigmoid(x, peakPosition) { return 1 / (1 + Math.exp(x)) * // If we're too far from the border, we flatten (0.2 - Math.max(0, Math.abs(0.5 - peakPosition) - 0.3)) * 5; } function createPyreneans() { for (let peak = 0; peak < mountainPeaks; ++peak) { let peakPosition = peak / mountainPeaks; let peakHeight = randFloat(0, 10); for (let distance = 0; distance < mountainWidth; distance += 1/3) { let rest = 2 * (1 - distance / mountainWidth); let sigmoidX = - 1 * (rest - 1.9) + - 4 * (rest - randFloat(0.9, 1.1)) * (rest - randFloat(0.9, 1.1)) * (rest - randFloat(0.9, 1.1)); for (let direction of [-1, 1]) { let pos = Vector2D.sum([ Vector2D.add(mountainStart, Vector2D.mult(mountainDirection, peakPosition * mountainLength)), new Vector2D(mountainOffset, 0).rotate(-peakPosition * Math.PI * 4), new Vector2D(distance, 0).rotate(-startAngle - direction * Math.PI / 2) ]).round(); g_Map.setHeight(pos, baseHeights[pos.x][pos.y] + (heightMountain + peakHeight + randFloat(-9, 9)) * sigmoid(sigmoidX, peakPosition)); } } } } g_Map.log("Creating passages"); var passageLocation = 0.35; var passageVec = mountainDirection.perpendicular().mult(passageLength); for (let passLoc of [passageLocation, 1 - passageLocation]) for (let direction of [1, -1]) { let passageStart = Vector2D.add(mountainStart, Vector2D.mult(mountainVec, passLoc)); let passageEnd = Vector2D.add(passageStart, Vector2D.mult(passageVec, direction)); createPassage({ "start": passageStart, "end": passageEnd, "startHeight": heightPassage, "startWidth": 7, "endWidth": 7, "smoothWidth": 2, "tileClass": clPass }); } Engine.SetProgress(50); g_Map.log("Smoothing the pyreneans"); createArea( new MapBoundsPlacer(), new SmoothingPainter(1, 0.3, 1), new NearTileClassConstraint(clPyrenneans, 1)); g_Map.log("Creating oceans"); for (let ocean of distributePointsOnCircle(2, oceanAngle, fractionToTiles(0.48), mapCenter)[0]) createArea( new ClumpPlacer(diskArea(fractionToTiles(0.18)), 0.9, 0.05, Infinity, ocean), [ new ElevationPainter(heightOcean), new TileClassPainter(clWater) ]); g_Map.log("Smoothing around the water"); createArea( new MapBoundsPlacer(), new SmoothingPainter(5, 0.9, 1), new NearTileClassConstraint(clWater, 5)); Engine.SetProgress(55); g_Map.log("Creating hills"); createAreas( new ClumpPlacer(scaleByMapSize(60, 120), 0.3, 0.06, Infinity), [ new SmoothElevationPainter(ELEVATION_MODIFY, heightOffsetHill, 4, heightOffsetHillRandom), new TerrainPainter(tGrassSpecific), new TileClassPainter(clHill) ], avoidClasses(clWater, 5, clPlayer, 20, clBaseResource, 6, clPyrenneans, 2), scaleByMapSize(5, 35)); g_Map.log("Creating forests"); var types = [[tForestTransition, pForestLandVeryLight, pForestLandLight, pForestLand]]; var size = scaleByMapSize(40, 115) * Math.PI; var num = Math.floor(scaleByMapSize(8,40) / types.length); for (let type of types) createAreas( new ClumpPlacer(size, 0.2, 0.1, Infinity), [ new LayeredPainter(type, [scaleByMapSize(1, 2), scaleByMapSize(3, 6), scaleByMapSize(3, 6)]), new TileClassPainter(clForest) ], avoidClasses(clPlayer, 20, clPyrenneans,0, clForest, 7, clWater, 2), num); Engine.SetProgress(60); g_Map.log("Creating lone trees"); var num = scaleByMapSize(80,400); var group = new SimpleGroup([new SimpleObject(oPine, 1,2, 1,3),new SimpleObject(oBeech, 1,2, 1,3)], true, clForest); createObjectGroupsDeprecated(group, 0, avoidClasses(clWater, 3, clForest, 1, clPlayer, 8,clPyrenneans, 1), num, 20 ); g_Map.log("Painting terrain by height and slope"); for (let i = 0; i < terrainPerHeight.length; ++i) for (let steep of [false, true]) createArea( new MapBoundsPlacer(), new TerrainPainter(steep ? terrainPerHeight[i].terrainSteep : terrainPerHeight[i].terrainGround), [ new NearTileClassConstraint(clPyrenneans, 2), new HeightConstraint(terrainPerHeight[i - 1] ? terrainPerHeight[i - 1].maxHeight : -Infinity, terrainPerHeight[i].maxHeight), steep ? new SlopeConstraint(terrainPerHeight[i].steepness, Infinity) : new SlopeConstraint(-Infinity, terrainPerHeight[i].steepness), ]); for (let x = 0; x < mapSize; ++x) for (let z = 0; z < mapSize; ++z) { let position = new Vector2D(x, z); let height = g_Map.getHeight(position); let heightDiff = g_Map.getSlope(position); let terrainShore = getShoreTerrain(position, height, heightDiff); if (terrainShore) createTerrain(terrainShore).place(position); } function getShoreTerrain(position, height, heightDiff) { if (height <= heightWaterTerrain) return tWater; if (height <= heightSand && new NearTileClassConstraint(clWater, 2).allows(position)) return heightDiff < 2.5 ? tSand : tMidRangeCliffs; // Notice the sand transition is also be painted below height -2 if (height <= heightSandTransition && new NearTileClassConstraint(clWater, 3).allows(position)) return heightDiff < 2.5 ? tSandTransition : tMidRangeCliffs; return undefined; } g_Map.log("Creating dirt patches"); for (let size of [scaleByMapSize(3, 20), scaleByMapSize(5, 40), scaleByMapSize(8, 60)]) createAreas( new ClumpPlacer(size, 0.3, 0.06, 0.5), [ new TerrainPainter(tDirtyGrass), new TileClassPainter(clDirt) ], avoidClasses(clWater, 3, clForest, 0, clPyrenneans,5, clHill, 0, clDirt, 5, clPlayer, 6), scaleByMapSize(15, 45)); g_Map.log("Creating grass patches"); for (let size of [scaleByMapSize(2, 32), scaleByMapSize(3, 48), scaleByMapSize(5, 80)]) createAreas( new ClumpPlacer(size, 0.3, 0.06, 0.5), new TerrainPainter(tLushGrass), avoidClasses(clWater, 3, clForest, 0, clPyrenneans,5, clHill, 0, clDirt, 5, clPlayer, 6), scaleByMapSize(15, 45)); Engine.SetProgress(70); // making more in dirt areas so as to appear different g_Map.log("Creating small grass tufts"); var group = new SimpleGroup( [new SimpleObject(aGrassShort, 1,2, 0,1, -Math.PI / 8, Math.PI / 8)] ); createObjectGroupsDeprecated(group, 0, avoidClasses(clWater, 2, clHill, 2, clPlayer, 5, clDirt, 0, clPyrenneans,2), scaleByMapSize(13, 200) ); createObjectGroupsDeprecated(group, 0, stayClasses(clDirt,1), scaleByMapSize(13, 200),10); g_Map.log("Creating large grass tufts"); group = new SimpleGroup( [new SimpleObject(aGrass, 2,4, 0,1.8, -Math.PI / 8, Math.PI / 8), new SimpleObject(aGrassShort, 3,6, 1.2,2.5, -Math.PI / 8, Math.PI / 8)] ); createObjectGroupsDeprecated(group, 0, avoidClasses(clWater, 3, clHill, 2, clPlayer, 5, clDirt, 1, clForest, 0, clPyrenneans,2), scaleByMapSize(13, 200) ); createObjectGroupsDeprecated(group, 0, stayClasses(clDirt,1), scaleByMapSize(13, 200),10); Engine.SetProgress(75); g_Map.log("Creating bushes"); group = new SimpleGroup( [new SimpleObject(aBushMedium, 1,2, 0,2), new SimpleObject(aBushSmall, 2,4, 0,2)] ); createObjectGroupsDeprecated(group, 0, avoidClasses(clWater, 2, clPlayer, 1, clPyrenneans, 1), scaleByMapSize(13, 200), 50 ); Engine.SetProgress(80); g_Map.log("Creating stone mines"); group = new SimpleGroup([new SimpleObject(oStoneSmall, 0, 2, 0, 4, 0, 2 * Math.PI, 1), new SimpleObject(oStoneLarge, 1, 1, 0, 4, 0, 2 * Math.PI, 4)], true, clRock); createObjectGroupsDeprecated(group, 0, avoidClasses(clWater, 3, clForest, 1, clPlayer, 20, clRock, 8, clPyrenneans, 1), scaleByMapSize(4,16), 100 ); g_Map.log("Creating small stone quarries"); group = new SimpleGroup([new SimpleObject(oStoneSmall, 2,5, 1,3)], true, clRock); createObjectGroupsDeprecated(group, 0, avoidClasses(clWater, 3, clForest, 1, clPlayer, 20, clRock, 8, clPyrenneans, 1), scaleByMapSize(4,16), 100 ); g_Map.log("Creating metal mines"); group = new SimpleGroup([new SimpleObject(oMetalLarge, 1,1, 0,4)], true, clMetal); createObjectGroupsDeprecated(group, 0, avoidClasses(clWater, 3, clForest, 1, clPlayer, 20, clMetal, 8, clRock, 5, clPyrenneans, 1), scaleByMapSize(4,16), 100 ); Engine.SetProgress(85); g_Map.log("Creating small decorative rocks"); group = new SimpleGroup( [new SimpleObject(aRockMedium, 1,3, 0,1)], true ); createObjectGroupsDeprecated( group, 0, avoidClasses(clWater, 0, clForest, 0, clPlayer, 0), scaleByMapSize(16, 262), 50 ); g_Map.log("Creating large decorative rocks"); group = new SimpleGroup( [new SimpleObject(aRockLarge, 1,2, 0,1), new SimpleObject(aRockMedium, 1,3, 0,2)], true ); createObjectGroupsDeprecated( group, 0, avoidClasses(clWater, 0, clForest, 0, clPlayer, 0), scaleByMapSize(8, 131), 50 ); Engine.SetProgress(90); g_Map.log("Creating deer"); group = new SimpleGroup( [new SimpleObject(oDeer, 5,7, 0,4)], true, clFood ); createObjectGroupsDeprecated(group, 0, avoidClasses(clWater, 3, clForest, 0, clPlayer, 20, clPyrenneans, 1, clFood, 15), 3 * numPlayers, 50 ); g_Map.log("Creating rabbit"); group = new SimpleGroup( [new SimpleObject(oRabbit, 2,3, 0,2)], true, clFood ); createObjectGroupsDeprecated(group, 0, avoidClasses(clWater, 3, clForest, 0, clPlayer, 20, clPyrenneans, 1, clFood,15), 3 * numPlayers, 50 ); g_Map.log("Creating berry bush"); group = new SimpleGroup( [new SimpleObject(oBerryBush, 5,7, 0,4)],true, clFood ); createObjectGroupsDeprecated(group, 0, avoidClasses(clWater, 3, clForest, 0, clPlayer, 20, clPyrenneans, 1, clFood, 10), randIntInclusive(1, 4) * numPlayers + 2, 50); g_Map.log("Creating fish"); group = new SimpleGroup( [new SimpleObject(oFish, 2,3, 0,2)], true, clFood ); createObjectGroupsDeprecated(group, 0, [avoidClasses(clFood, 15), stayClasses(clWater, 6)], 20 * numPlayers, 60 ); placePlayersNomad(clPlayer, avoidClasses(clWater, 4, clPyrenneans, 4, clForest, 1, clMetal, 4, clRock, 4, clFood, 2)); setSunElevation(Math.PI * randFloat(1/5, 1/3)); setSunRotation(randomAngle()); setSkySet("cumulus"); setSunColor(0.73,0.73,0.65); setTerrainAmbientColor(0.45,0.45,0.50); setUnitsAmbientColor(0.4,0.4,0.4); setWaterColor(0.263, 0.353, 0.616); setWaterTint(0.104, 0.172, 0.563); setWaterWaviness(5.0); setWaterType("ocean"); setWaterMurkiness(0.83); setWaterHeight(heightWaterLevel); g_Map.ExportMap(); Index: ps/trunk/binaries/data/mods/public/maps/random/rmgen-common/player.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/rmgen-common/player.js (revision 21702) +++ ps/trunk/binaries/data/mods/public/maps/random/rmgen-common/player.js (revision 21703) @@ -1,789 +1,843 @@ /** * @file These functions locate and place the starting entities of players. */ var g_NomadTreasureTemplates = { "food": "gaia/treasure/food_jars", "wood": "gaia/treasure/wood", "stone": "gaia/treasure/stone", "metal": "gaia/treasure/metal" }; /** * These are identifiers of functions that can generate parts of a player base. * There must be a function starting with placePlayerBase and ending with this name. * This is a global so mods can extend this from external files. */ var g_PlayerBaseFunctions = [ // Possibly mark player class first here and use it afterwards "CityPatch", // Create the largest and most important entities first "Trees", "Mines", "Treasures", "Berries", "Chicken", "Decoratives" ]; function isNomad() { return !!g_MapSettings.Nomad; } function getNumPlayers() { return g_MapSettings.PlayerData.length - 1; } function getCivCode(playerID) { return g_MapSettings.PlayerData[playerID].Civ; } function areAllies(playerID1, playerID2) { return g_MapSettings.PlayerData[playerID1].Team !== undefined && g_MapSettings.PlayerData[playerID2].Team !== undefined && g_MapSettings.PlayerData[playerID1].Team != -1 && g_MapSettings.PlayerData[playerID2].Team != -1 && g_MapSettings.PlayerData[playerID1].Team === g_MapSettings.PlayerData[playerID2].Team; } function getPlayerTeam(playerID) { if (g_MapSettings.PlayerData[playerID].Team === undefined) return -1; return g_MapSettings.PlayerData[playerID].Team; } /** * Gets the default starting entities for the civ of the given player, as defined by the civ file. */ function getStartingEntities(playerID) { return g_CivData[getCivCode(playerID)].StartEntities; } /** * Places the given entities at the given location (typically a civic center and starting units). * @param location - A Vector2D specifying tile coordinates. * @param civEntities - An array of objects with the Template property and optionally a Count property. * The first entity is placed in the center, the other ones surround it. */ function placeStartingEntities(location, playerID, civEntities, dist = 6, orientation = BUILDING_ORIENTATION) { // Place the central structure let i = 0; let firstTemplate = civEntities[i].Template; if (firstTemplate.startsWith("structures/")) { g_Map.placeEntityPassable(firstTemplate, playerID, location, orientation); ++i; } // Place entities surrounding it let space = 2; for (let j = i; j < civEntities.length; ++j) { let angle = orientation - Math.PI * (1 - j / 2); let count = civEntities[j].Count || 1; for (let num = 0; num < count; ++num) { let position = Vector2D.sum([ location, new Vector2D(dist, 0).rotate(-angle), new Vector2D(space * (-num + (count - 1) / 2), 0).rotate(angle) ]); g_Map.placeEntityPassable(civEntities[j].Template, playerID, position, angle); } } } /** * Places the default starting entities as defined by the civilization definition, optionally including city walls. */ function placeCivDefaultStartingEntities(position, playerID, wallType, dist = 6, orientation = BUILDING_ORIENTATION) { placeStartingEntities(position, playerID, getStartingEntities(playerID), dist, orientation); placeStartingWalls(position, playerID, wallType, orientation); } /** * If the map is large enough and the civilization defines them, places the initial city walls or towers. * @param {string|boolean} wallType - Either "towers" to only place the wall turrets or a boolean indicating enclosing city walls. */ function placeStartingWalls(position, playerID, wallType, orientation = BUILDING_ORIENTATION) { let civ = getCivCode(playerID); if (civ != "iber" || g_Map.getSize() <= 128) return; // TODO: should prevent trees inside walls // When fixing, remove the DeleteUponConstruction flag from template_gaia_flora.xml if (wallType == "towers") placePolygonalWall(position, 15, ["entry"], "tower", civ, playerID, orientation, 7); else if (wallType) placeGenericFortress(position, 20, playerID); } /** * Places the civic center and starting resources for all given players. */ function placePlayerBases(playerBaseArgs) { g_Map.log("Creating playerbases"); let [playerIDs, playerPosition] = playerBaseArgs.PlayerPlacement; for (let i = 0; i < getNumPlayers(); ++i) { playerBaseArgs.playerID = playerIDs[i]; playerBaseArgs.playerPosition = playerPosition[i]; placePlayerBase(playerBaseArgs); } } /** * Places the civic center and starting resources. */ function placePlayerBase(playerBaseArgs) { if (isNomad()) return; placeCivDefaultStartingEntities(playerBaseArgs.playerPosition, playerBaseArgs.playerID, playerBaseArgs.Walls !== undefined ? playerBaseArgs.Walls : true); if (playerBaseArgs.PlayerTileClass) addCivicCenterAreaToClass(playerBaseArgs.playerPosition, playerBaseArgs.PlayerTileClass); for (let functionID of g_PlayerBaseFunctions) { let funcName = "placePlayerBase" + functionID; let func = global[funcName]; if (!func) throw new Error("Could not find " + funcName); if (!playerBaseArgs[functionID]) continue; let args = playerBaseArgs[functionID]; // Copy some global arguments to the arguments for each function for (let prop of ["playerID", "playerPosition", "BaseResourceClass", "baseResourceConstraint"]) args[prop] = playerBaseArgs[prop]; func(args); } } function defaultPlayerBaseRadius() { return scaleByMapSize(15, 25); } /** * Marks the corner and center tiles of an area that is about the size of a Civic Center with the given TileClass. * Used to prevent resource collisions with the Civic Center. */ function addCivicCenterAreaToClass(position, tileClass) { createArea( new DiskPlacer(5, position), new TileClassPainter(tileClass)); } /** * Helper function. */ function getPlayerBaseArgs(playerBaseArgs) { let baseResourceConstraint = playerBaseArgs.BaseResourceClass && avoidClasses(playerBaseArgs.BaseResourceClass, 4); if (playerBaseArgs.baseResourceConstraint) baseResourceConstraint = new AndConstraint([baseResourceConstraint, playerBaseArgs.baseResourceConstraint]); return [ (property, defaultVal) => playerBaseArgs[property] === undefined ? defaultVal : playerBaseArgs[property], playerBaseArgs.playerPosition, baseResourceConstraint ]; } function placePlayerBaseCityPatch(args) { let [get, basePosition, baseResourceConstraint] = getPlayerBaseArgs(args); let painters = []; if (args.outerTerrain && args.innerTerrain) painters.push(new LayeredPainter([args.outerTerrain, args.innerTerrain], [get("width", 1)])); if (args.painters) painters = painters.concat(args.painters); createArea( new ClumpPlacer( Math.floor(diskArea(get("radius", defaultPlayerBaseRadius() / 3))), get("coherence", 0.6), get("smoothness", 0.3), get("failFraction", Infinity), basePosition), painters); } function placePlayerBaseChicken(args) { let [get, basePosition, baseResourceConstraint] = getPlayerBaseArgs(args); for (let i = 0; i < get("groupCount", 2); ++i) { let success = false; for (let tries = 0; tries < get("maxTries", 30); ++tries) { let position = new Vector2D(0, get("distance", 9)).rotate(randomAngle()).add(basePosition); if (createObjectGroup( new SimpleGroup( [ new SimpleObject( get("template", "gaia/fauna_chicken"), get("minGroupCount", 5), get("maxGroupCount", 5), get("minGroupDistance", 0), get("maxGroupDistance", 2)) ], true, args.BaseResourceClass, position), 0, baseResourceConstraint)) { success = true; break; } } if (!success) { error("Could not place chicken for player " + args.playerID); return; } } } function placePlayerBaseBerries(args) { let [get, basePosition, baseResourceConstraint] = getPlayerBaseArgs(args); for (let tries = 0; tries < get("maxTries", 30); ++tries) { let position = new Vector2D(0, get("distance", 12)).rotate(randomAngle()).add(basePosition); if (createObjectGroup( new SimpleGroup( [new SimpleObject(args.template, get("minCount", 5), get("maxCount", 5), get("maxDist", 1), get("maxDist", 3))], true, args.BaseResourceClass, position), 0, baseResourceConstraint)) return; } error("Could not place berries for player " + args.playerID); } function placePlayerBaseMines(args) { let [get, basePosition, baseResourceConstraint] = getPlayerBaseArgs(args); let angleBetweenMines = randFloat(get("minAngle", Math.PI / 6), get("maxAngle", Math.PI / 3)); let mineCount = args.types.length; let groupElements = []; if (args.groupElements) groupElements = groupElements.concat(args.groupElements); for (let tries = 0; tries < get("maxTries", 75); ++tries) { // First find a place where all mines can be placed let pos = []; let startAngle = randomAngle(); for (let i = 0; i < mineCount; ++i) { let angle = startAngle + angleBetweenMines * (i + (mineCount - 1) / 2); pos[i] = new Vector2D(0, get("distance", 12)).rotate(angle).add(basePosition).round(); if (!g_Map.validTilePassable(pos[i]) || !baseResourceConstraint.allows(pos[i])) { pos = undefined; break; } } if (!pos) continue; // Place the mines for (let i = 0; i < mineCount; ++i) { if (args.types[i].type && args.types[i].type == "stone_formation") { createStoneMineFormation(pos[i], args.types[i].template, args.types[i].terrain); args.BaseResourceClass.add(pos[i]); continue; } createObjectGroup( new SimpleGroup( [new SimpleObject(args.types[i].template, 1, 1, 0, 0)].concat(groupElements), true, args.BaseResourceClass, pos[i]), 0); } return; } error("Could not place mines for player " + args.playerID); } function placePlayerBaseTrees(args) { let [get, basePosition, baseResourceConstraint] = getPlayerBaseArgs(args); let num = Math.floor(get("count", scaleByMapSize(7, 20))); for (let x = 0; x < get("maxTries", 30); ++x) { let position = new Vector2D(0, randFloat(get("minDist", 11), get("maxDist", 13))).rotate(randomAngle()).add(basePosition).round(); if (createObjectGroup( new SimpleGroup( [new SimpleObject(args.template, num, num, get("minDistGroup", 0), get("maxDistGroup", 5))], false, args.BaseResourceClass, position), 0, baseResourceConstraint)) return; } error("Could not place starting trees for player " + args.playerID); } function placePlayerBaseTreasures(args) { let [get, basePosition, baseResourceConstraint] = getPlayerBaseArgs(args); for (let resourceTypeArgs of args.types) { get = (property, defaultVal) => resourceTypeArgs[property] === undefined ? defaultVal : resourceTypeArgs[property]; let success = false; for (let tries = 0; tries < get("maxTries", 30); ++tries) { let position = new Vector2D(0, randFloat(get("minDist", 11), get("maxDist", 13))).rotate(randomAngle()).add(basePosition).round(); if (createObjectGroup( new SimpleGroup( [new SimpleObject(resourceTypeArgs.template, get("count", 14), get("count", 14), get("minDistGroup", 1), get("maxDistGroup", 3))], false, args.BaseResourceClass, position), 0, baseResourceConstraint)) { success = true; break; } } if (!success) { error("Could not place treasure " + resourceTypeArgs.template + " for player " + args.playerID); return; } } } /** * Typically used for placing grass tufts around the civic centers. */ function placePlayerBaseDecoratives(args) { let [get, basePosition, baseResourceConstraint] = getPlayerBaseArgs(args); for (let i = 0; i < get("count", scaleByMapSize(2, 5)); ++i) { let success = false; for (let x = 0; x < get("maxTries", 30); ++x) { let position = new Vector2D(0, randIntInclusive(get("minDist", 8), get("maxDist", 11))).rotate(randomAngle()).add(basePosition).round(); if (createObjectGroup( new SimpleGroup( [new SimpleObject(args.template, get("minCount", 2), get("maxCount", 5), 0, 1)], false, args.BaseResourceClass, position), 0, baseResourceConstraint)) { success = true; break; } } if (!success) // Don't warn since the decoratives are not important return; } } function placePlayersNomad(playerClass, constraints) { if (!isNomad()) return undefined; g_Map.log("Placing nomad starting units"); let distance = scaleByMapSize(60, 240); let constraint = new StaticConstraint(constraints); let numPlayers = getNumPlayers(); let playerIDs = shuffleArray(sortAllPlayers()); let playerPosition = []; for (let i = 0; i < numPlayers; ++i) { let objects = getStartingEntities(playerIDs[i]).filter(ents => ents.Template.startsWith("units/")).map( ents => new SimpleObject(ents.Template, ents.Count || 1, ents.Count || 1, 1, 3)); // Add treasure if too few resources for a civic center let ccCost = Engine.GetTemplate("structures/" + getCivCode(playerIDs[i]) + "_civil_centre").Cost.Resources; for (let resourceType in ccCost) { let treasureTemplate = g_NomadTreasureTemplates[resourceType]; let count = Math.max(0, Math.ceil( (ccCost[resourceType] - (g_MapSettings.StartingResources || 0)) / Engine.GetTemplate(treasureTemplate).ResourceSupply.Amount)); objects.push(new SimpleObject(treasureTemplate, count, count, 3, 5)); } // Try place these entities at a random location let group = new SimpleGroup(objects, true, playerClass); let success = false; for (let distanceFactor of [1, 1/2, 1/4, 0]) if (createObjectGroups(group, playerIDs[i], new AndConstraint([constraint, avoidClasses(playerClass, distance * distanceFactor)]), 1, 200, false).length) { success = true; playerPosition[i] = group.centerPosition; break; } if (!success) throw new Error("Could not place starting units for player " + playerIDs[i] + "!"); } return [playerIDs, playerPosition]; } /** * Sorts an array of player IDs by team index. Players without teams come first. * Randomize order for players of the same team. */ function sortPlayers(playerIDs) { return shuffleArray(playerIDs).sort((playerID1, playerID2) => getPlayerTeam(playerID1) - getPlayerTeam(playerID2)); } /** * Randomize playerIDs but sort by team. * * @returns {Array} - every item is an array of player indices */ function sortAllPlayers() { let playerIDs = []; for (let i = 0; i < getNumPlayers(); ++i) playerIDs.push(i+1); return sortPlayers(playerIDs); } /** * Rearrange order so that teams of neighboring players alternate (if the given IDs are sorted by team). */ function primeSortPlayers(playerIDs) { let prime = []; for (let i = 0; i < Math.floor(playerIDs.length / 2); ++i) { prime.push(playerIDs[i]); prime.push(playerIDs[playerIDs.length - 1 - i]); } if (playerIDs.length % 2) prime.push(playerIDs[Math.floor(playerIDs.length / 2)]); return prime; } function primeSortAllPlayers() { return primeSortPlayers(sortAllPlayers()); } +/* + * Separates playerIDs into two arrays such that teammates are in the same array, + * unless everyone's on the same team in which case they'll be split in half. + */ +function partitionPlayers(playerIDs) +{ + let teamIDs = Array.from(new Set(playerIDs.map(getPlayerTeam))); + let teams = teamIDs.map(teamID => playerIDs.filter(playerID => getPlayerTeam(playerID) == teamID)); + if (teamIDs.indexOf(-1) != -1) + teams = teams.concat(teams.splice(teamIDs.indexOf(-1), 1)[0].map(playerID => [playerID])); + + if (teams.length == 1) + { + let idx = Math.floor(teams[0].length / 2); + teams = [teams[0].slice(idx), teams[0].slice(0, idx)]; + } + + teams.sort((a, b) => b.length - a.length); + + // Use the greedy algorithm: add the next team to the side with fewer players + return teams.reduce(([east, west], team) => + east.length > west.length ? + [east, west.concat(team)] : + [east.concat(team), west], + [[], []]); +} + /** * Determine player starting positions on a circular pattern. */ function playerPlacementCircle(radius, startingAngle = undefined, center = undefined) { let startAngle = startingAngle !== undefined ? startingAngle : randomAngle(); let [playerPosition, playerAngle] = distributePointsOnCircle(getNumPlayers(), startAngle, radius, center || g_Map.getCenter()); return [sortAllPlayers(), playerPosition.map(p => p.round()), playerAngle, startAngle]; } /** * Determine player starting positions on a circular pattern, with a custom angle for each player. * Commonly used for gulf terrains. */ function playerPlacementCustomAngle(radius, center, playerAngleFunc) { let playerPosition = []; let playerAngle = []; let numPlayers = getNumPlayers(); for (let i = 0; i < numPlayers; ++i) { playerAngle[i] = playerAngleFunc(i); playerPosition[i] = Vector2D.add(center, new Vector2D(radius, 0).rotate(-playerAngle[i])).round(); } return [playerPosition, playerAngle]; } /** + * Returns player starting positions equally spaced along an arc. + */ +function playerPlacementArc(playerIDs, center, radius, startAngle, endAngle) +{ + return distributePointsOnCircularSegment( + playerIDs.length + 2, + endAngle - startAngle, + startAngle, + radius, + center + )[0].slice(1, -1).map(p => p.round()); +} + +/** + * Returns player starting positions located on two symmetrically placed arcs, with teammates placed on the same arc. + */ +function playerPlacementArcs(playerIDs, center, radius, mapAngle, startAngle, endAngle) +{ + let [east, west] = partitionPlayers(playerIDs); + let eastPosition = playerPlacementArc(east, center, radius, mapAngle + startAngle, mapAngle + endAngle); + let westPosition = playerPlacementArc(west, center, radius, mapAngle - startAngle, mapAngle - endAngle); + return playerIDs.map(playerID => east.indexOf(playerID) != -1 ? + eastPosition[east.indexOf(playerID)] : + westPosition[west.indexOf(playerID)]); +} + +/** * Returns player starting positions located on two parallel lines, typically used by central river maps. * If there are two teams with an equal number of players, each team will occupy exactly one line. * Angle 0 means the players are placed in north to south direction, i.e. along the Z axis. */ function playerPlacementRiver(angle, width, center = undefined) { let numPlayers = getNumPlayers(); let numPlayersEven = numPlayers % 2 == 0; let mapSize = g_Map.getSize(); let centerPosition = center || g_Map.getCenter(); let playerPosition = []; for (let i = 0; i < numPlayers; ++i) { let currentPlayerEven = i % 2 == 0; let offsetDivident = numPlayersEven || currentPlayerEven ? (i + 1) % 2 : 0; let offsetDivisor = numPlayersEven ? 0 : currentPlayerEven ? +1 : -1; playerPosition[i] = new Vector2D( width * (i % 2) + (mapSize - width) / 2, fractionToTiles(((i - 1 + offsetDivident) / 2 + 1) / ((numPlayers + offsetDivisor) / 2 + 1)) ).rotateAround(angle, centerPosition).round(); } return [primeSortAllPlayers(), playerPosition]; } /** * Returns starting positions located on two parallel lines. * The locations on the first line are shifted in comparison to the other line. */ function playerPlacementLine(angle, center, width) { let playerPosition = []; let numPlayers = getNumPlayers(); for (let i = 0; i < numPlayers; ++i) playerPosition[i] = Vector2D.add( center, new Vector2D( fractionToTiles((i + 1) / (numPlayers + 1) - 0.5), width * (i % 2 - 1/2) ).rotate(angle) ).round(); return playerPosition; } /** * Returns a random location for each player that meets the given constraints and * orders the playerIDs so that players become grouped by team. */ function playerPlacementRandom(playerIDs, constraints = undefined) { let locations = []; let attempts = 0; let resets = 0; let mapCenter = g_Map.getCenter(); let playerMinDistSquared = Math.square(fractionToTiles(0.25)); let borderDistance = fractionToTiles(0.08); let area = createArea(new MapBoundsPlacer(), undefined, new AndConstraint(constraints)); for (let i = 0; i < getNumPlayers(); ++i) { let position = pickRandom(area.getPoints()); // Minimum distance between initial bases must be a quarter of the map diameter if (locations.some(loc => loc.distanceToSquared(position) < playerMinDistSquared) || position.distanceToSquared(mapCenter) > Math.square(mapCenter.x - borderDistance)) { --i; ++attempts; // Reset if we're in what looks like an infinite loop if (attempts > 500) { locations = []; i = -1; attempts = 0; ++resets; // Reduce minimum player distance progressively if (resets % 25 == 0) playerMinDistSquared *= 0.95; // If we only pick bad locations, stop trying to place randomly if (resets == 500) return undefined; } continue; } locations[i] = position; } return groupPlayersByArea(playerIDs, locations); } /** * Pick locations from the given set so that teams end up grouped. */ function groupPlayersByArea(playerIDs, locations) { playerIDs = sortPlayers(playerIDs); let minDist = Infinity; let minLocations; // Of all permutations of starting locations, find the one where // the sum of the distances between allies is minimal, weighted by teamsize. heapsPermute(shuffleArray(locations).slice(0, playerIDs.length), v => v.clone(), permutation => { let dist = 0; let teamDist = 0; let teamSize = 0; for (let i = 1; i < playerIDs.length; ++i) { let team1 = getPlayerTeam(playerIDs[i - 1]); let team2 = getPlayerTeam(playerIDs[i]); ++teamSize; if (team1 != -1 && team1 == team2) teamDist += permutation[i - 1].distanceTo(permutation[i]); else { dist += teamDist / teamSize; teamDist = 0; teamSize = 0; } } if (teamSize) dist += teamDist / teamSize; if (dist < minDist) { minDist = dist; minLocations = permutation; } }); return [playerIDs, minLocations]; } /** * Sorts the playerIDs so that team members are as close as possible on a ring. */ function groupPlayersCycle(startLocations) { let startLocationOrder = sortPointsShortestCycle(startLocations); let newStartLocations = []; for (let i = 0; i < startLocations.length; ++i) newStartLocations.push(startLocations[startLocationOrder[i]]); startLocations = newStartLocations; // Sort players by team let playerIDs = []; let teams = []; for (let i = 0; i < g_MapSettings.PlayerData.length - 1; ++i) { playerIDs.push(i+1); let t = g_MapSettings.PlayerData[i + 1].Team; if (teams.indexOf(t) == -1 && t !== undefined) teams.push(t); } playerIDs = sortPlayers(playerIDs); if (!teams.length) return [playerIDs, startLocations]; // Minimize maximum distance between players within a team let minDistance = Infinity; let bestShift; for (let s = 0; s < playerIDs.length; ++s) { let maxTeamDist = 0; for (let pi = 0; pi < playerIDs.length - 1; ++pi) { let t1 = getPlayerTeam(playerIDs[(pi + s) % playerIDs.length]); if (teams.indexOf(t1) === -1) continue; for (let pj = pi + 1; pj < playerIDs.length; ++pj) { if (t1 != getPlayerTeam(playerIDs[(pj + s) % playerIDs.length])) continue; maxTeamDist = Math.max( maxTeamDist, Math.euclidDistance2D( startLocations[pi].x, startLocations[pi].y, startLocations[pj].x, startLocations[pj].y)); } } if (maxTeamDist < minDistance) { minDistance = maxTeamDist; bestShift = s; } } if (bestShift) { let newPlayerIDs = []; for (let i = 0; i < playerIDs.length; ++i) newPlayerIDs.push(playerIDs[(i + bestShift) % playerIDs.length]); playerIDs = newPlayerIDs; } return [playerIDs, startLocations]; }