Index: ps/trunk/binaries/data/mods/public/maps/random/jebel_barkal.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/jebel_barkal.js (revision 24616) +++ ps/trunk/binaries/data/mods/public/maps/random/jebel_barkal.js (revision 24617) @@ -1,1505 +1,1505 @@ /** * 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/tree/acacia"; const oPalmPath = "gaia/tree/cretan_date_palm_tall"; const oPalms = [ "gaia/tree/cretan_date_palm_tall", "gaia/tree/cretan_date_palm_short", "gaia/tree/palm_tropic", "gaia/tree/date_palm", "gaia/tree/senegal_date_palm", "gaia/tree/medit_fan_palm" ]; const oBerryBushGrapes = "gaia/fruit/grapes"; const oBerryBushDesert = "gaia/fruit/berry_05"; const oStoneLargeDesert = "gaia/rock/desert_large"; const oStoneSmallDesert = "gaia/rock/desert_small"; const oMetalLargeDesert = "gaia/ore/desert_large"; const oMetalSmallDesert = "gaia/ore/desert_small"; const oStoneLargeFertileLand = "gaia/rock/desert_large"; const oStoneSmallFertileLand = "gaia/rock/greece_small"; const oMetalLargeFertileLand = "gaia/ore/desert_large"; const oMetalSmallFertileLand = "gaia/ore/temperate_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_rhinoceros_white"; 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_nile"; const oFish = "gaia/fish/tilapia"; const oHawk = "birds/buzzard"; 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 && getDifficulty() >= 3 ? "structures/kush/defense_tower" : "structures/kush/sentry_tower"; const oHouse = "structures/kush/house"; const oMarket = "structures/kush/market"; const oForge = "structures/kush/forge"; const oBlemmyeCamp = "structures/kush/camp_blemmye"; const oNobaCamp = "structures/kush/camp_noba"; 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 oPalisadeMedium = "structures/palisades_medium"; const oPalisadeGate = "structures/palisades_gate"; const oPalisadeTower = "structures/palisades_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/siege_lithobolos_unpacked", "units/ptol/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 clRoad = 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 clForge = 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 clNobaCamp = g_Map.createTileClass(); const clMarket = g_Map.createTileClass(); const clDecorative = g_Map.createTileClass(); 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 = mapSize < 192 || getDifficulty() < 2 ? false : getDifficulty() < 3 ? "napata_palisade" : "napata_wall"; 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": oFortress, "difficulty": "Medium", "constraints": [avoidClasses(clFortress, 25), new NearTileClassConstraint(clPath, 8)], "painters": new TileClassPainter(clFortress) }, { "templateName": oCivicCenter, "difficulty": "Easy", "constraints": [avoidClasses(clCivicCenter, 60), new NearTileClassConstraint(clPath, 8)], "painters": new TileClassPainter(clCivicCenter) }, { "templateName": oElephantStables, "difficulty": "Easy", "constraints": avoidClasses(clElephantStables, 10), "painters": new TileClassPainter(clElephantStables) }, { "templateName": oStable, "difficulty": "Easy", "constraints": avoidClasses(clStable, 20), "painters": new TileClassPainter(clStable) }, { "templateName": oBarracks, "difficulty": "Easy", "constraints": avoidClasses(clBarracks, 12), "painters": new TileClassPainter(clBarracks) }, { "templateName": oTower, "difficulty": "Easy", "constraints": avoidClasses(clTower, 17), "painters": new TileClassPainter(clTower) }, { "templateName": "uncapturable|" + oMarket, "difficulty": "Very Easy", "constraints": avoidClasses(clMarket, 15), "painters": new TileClassPainter(clMarket) }, { "templateName": "uncapturable|" + oForge, "difficulty": "Very Easy", "constraints": avoidClasses(clForge, 30), "painters": new TileClassPainter(clForge) }, { "templateName": oNobaCamp, "difficulty": "Easy", "constraints": avoidClasses(clNobaCamp, 30), "painters": new TileClassPainter(clNobaCamp) }, { "templateName": 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_wall = { "short": readyWallElement("uncapturable|" + oWallMedium), "medium": readyWallElement("uncapturable|" + oWallMedium), "tower": readyWallElement("uncapturable|" + oWallTower), "gate": readyWallElement("uncapturable|" + oWallGate), "overlap": 0.05 }; g_WallStyles.napata_palisade = { "short": readyWallElement("uncapturable|" + oPalisadeMedium), "medium": readyWallElement("uncapturable|" + oPalisadeMedium), "tower": readyWallElement("uncapturable|" + oPalisadeTower), "gate": readyWallElement("uncapturable|" + oPalisadeGate), "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 heightOffsetRoad = heightScale(-1.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 var widthFertileLand = fractionToTiles(0.33); paintRiver({ "parallel": true, "start": new Vector2D(mapBounds.left, mapBounds.bottom).rotateAround(-riverAngle, mapCenter), "end": new Vector2D(mapBounds.right, mapBounds.bottom).rotateAround(-riverAngle, mapCenter), "width": 2 * widthFertileLand, "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 = sortAllPlayers(); const playerPosition = playerPlacementArcs( playerIDs, mapCenter, 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"); const avoidWater = new StaticConstraint(avoidClasses(clWater, 0)); createArea( new MapBoundsPlacer(), new TileClassPainter(clDesert), [ new HeightConstraint(-Infinity, heightHill), avoidWater, avoidClasses(clFertileLand, 0) ]); const stayDesert = new StaticConstraint(stayClasses(clDesert, 0)); const stayFertileLand = new StaticConstraint(stayClasses(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 previousPassageY = randIntInclusive(0, widthFertileLand); 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 = (previousPassageY + randIntInclusive(0.2 * widthFertileLand, 0.8 * widthFertileLand)) % widthFertileLand; 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) continue; previousPassageY = y; 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": isDesert ? oMetalLargeDesert : oMetalLargeFertileLand }, { "template": isDesert ? oStoneLargeDesert : oStoneLargeFertileLand } ] }, "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 gridMaxAngle = Math.min(scaleByMapSize(1/3, 1), 2/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, 5)); 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"); var areasCityPaths = []; for (let y = 0; y < gridPointsY; ++y) for (let x = 1; x < gridPointsX; ++x) { let width = y == gridPointYCenter ? pathWidthSecondary : pathWidth; areasCityPaths.push( 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; areasCityPaths.push( 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 = distributePointsOnCircularSegment( scaleByMapSize(6, 16), Math.PI, ritualAngle, scaleByMapSize(4, 5), ritualPosition)[0]; 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))); var entitiesGates; if (placeNapataWall) { g_Map.log("Placing front walls"); let wallGridMaxAngleSummand = scaleByMapSize(0.04, 0.03) * Math.PI; 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"], placeNapataWall, 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"], placeNapataWall, 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"); 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("Finding road starting points"); var roadStartLocations = shuffleArray( entitiesGates ? entitiesGates.map(entity => entity.GetPosition2D()) : [ ...cityGridPosition.map(gridPos => gridPos[0]), ...cityGridPosition.map(gridPos => gridPos[gridPos.length - 1]), ...cityGridPosition[cityGridPosition.length - 1] ]); g_Map.log("Finding possible roads"); var roadConstraint = new StaticConstraint( [ stayDesert, avoidClasses(clHill, 0, clCity, 0, clPyramid, 6, clPlayer, 16) ]); var areaCityPaths = new Area(areasCityPaths.reduce((points, area) => points.concat(area.getPoints()), [])); var areaRoads = []; for (let roadStart of roadStartLocations) { - if (areaRoads.length >= scaleByMapSize(2, 4)) + if (areaRoads.length >= scaleByMapSize(2, 5)) break; let closestPoint = areaCityPaths.getClosestPointTo(roadStart); roadConstraint = new StaticConstraint([roadConstraint, avoidClasses(clRoad, 20)]); for (let tries = 0; tries < 30; ++tries) { let area = createArea( new PathPlacer( Vector2D.add(closestPoint, new Vector2D(0, 3/4 * mapSize).rotate(closestPoint.angleTo(roadStart))), roadStart, - 4, + scaleByMapSize(5, 3), 0.1, 5, 0.5, 0, 0), new TileClassPainter(clRoad), roadConstraint); if (area && area.getPoints().length) { areaRoads.push(area); break; } } } g_Map.log("Painting roads"); createArea( new MapBoundsPlacer(), [ new SmoothElevationPainter(ELEVATION_MODIFY, heightOffsetRoad, 1), new LayeredPainter([tPathWild, tPath], [1]), ], [stayClasses(clRoad, 0), avoidClasses(clPath, 0)]); g_Map.log("Marking road palm area"); var areaRoadPalms = createArea( new MapBoundsPlacer(), undefined, [ new NearTileClassConstraint(clRoad, 1), avoidClasses(clRoad, 0, clPath, 1, clWall, 4, clGate, 4) ]); if (areaRoadPalms && areaRoadPalms.getPoints().length) { g_Map.log("Placing road palms"); createObjectGroupsByAreas( new SimpleGroup([new SimpleObject(oPalmPath, 1, 1, 0, 0)], true, clForest), 0, avoidClasses(clForest, 2, clGate, 7), scaleByMapSize(40, 250), 20, [areaRoadPalms]); g_Map.log("Placing road bushes"); createObjectGroupsByAreas( new SimpleGroup([new RandomObject(aBushesCity, 1, 1, 0, 0)], true, clForest), 0, avoidClasses(clForest, 1), scaleByMapSize(40, 200), 20, [areaRoadPalms]); } Engine.SetProgress(75); g_Map.log("Marking city bush area"); var areaCityBushes = createArea( new MapBoundsPlacer(), undefined, [ new NearTileClassConstraint(clPath, 1), avoidClasses( clPath, 0, clRoad, 0, clPyramid, 20, clRitualPlace, 8, clTemple, 3, clWall, 3, clTower, 1, clFortress, 1, clHouse, 1, clForge, 1, clElephantStables, 1, clStable, 1, clCivicCenter, 1, clBarracks, 1, clBlemmyeCamp, 1, clNobaCamp, 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, clRoad, 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, 250), 50, [areaWallPalms]); } createBumps( new StaticConstraint(avoidClasses(clPlayer, 6, clCity, 0, clWater, 2, clHill, 0, clPath, 0, clRoad, 0, clTemple, 4, clPyramid, 8, clWall, 0, clGate, 4)), scaleByMapSize(30, 300), 1, 8, 4, 0, 3); Engine.SetProgress(80); g_Map.log("Setting up common constraints and areas"); const nearWater = new NearTileClassConstraint(clWater, 3); var avoidCollisionsNomad = 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, 2, clSoldier, 1, clTreasure, 1) ]); var avoidCollisions = new AndConstraint( [ avoidCollisionsNomad, new StaticConstraint(avoidClasses(clRoad, 6, clFood, 6)) ]); 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)); g_Map.log("Creating mines"); const avoidCollisionsMines = [ avoidClasses(clRock, 10, clMetal, 10), new StaticConstraint(avoidClasses( clWater, 4, clCliff, 4, clCity, 4, clRitualPlace, 10, clPlayer, 20, clForest, 4, clPyramid, 6, clTemple, 4, clPath, 4, clRoad, 4, clGate, 8)) ]; const mineObjects = (templateSmall, templateLarge) => ({ "large": [ new SimpleObject(templateSmall, 0, 2, 0, 4, 0, 2 * Math.PI, 1), new SimpleObject(templateLarge, 1, 1, 0, 4, 0, 2 * Math.PI, 4) ], "small": [ new SimpleObject(templateSmall, 3, 4, 1, 3, 0, 2 * Math.PI, 1) ] }); const mineObjectsPerBiome = [ { "desert": mineObjects(oMetalSmallDesert, oMetalLargeDesert), "fertileLand": mineObjects(oMetalSmallFertileLand, oMetalLargeFertileLand), "tileClass": clMetal }, { "desert": mineObjects(oStoneSmallDesert, oStoneLargeDesert), "fertileLand": mineObjects(oStoneSmallFertileLand, oStoneLargeFertileLand), "tileClass": clRock } ]; for (let i = 0; i < scaleByMapSize(6, 22); ++i) { let mineObjectsBiome = pickRandom(mineObjectsPerBiome); for (let i in mineObjectsBiome.desert) createObjectGroupsByAreas( new SimpleGroup(mineObjectsBiome.desert[i], true, mineObjectsBiome.tileClass), 0, avoidCollisionsMines.concat([avoidClasses(clFertileLand, 12, mineObjectsBiome.tileClass, 15)]), 1, 60, [areaDesert]); } for (let i = 0; i < (isNomad() ? scaleByMapSize(6, 16) : scaleByMapSize(0, 8)); ++i) { let mineObjectsBiome = pickRandom(mineObjectsPerBiome); createObjectGroupsByAreas( new SimpleGroup(mineObjectsBiome.fertileLand.small, true, mineObjectsBiome.tileClass), 0, avoidCollisionsMines.concat([avoidClasses(clDesert, 5, clMetal, 15, clRock, 15, mineObjectsBiome.tileClass, 20)]), 1, 80, [areaFertileLand]); } 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, 120) * randIntInclusive(1, 3)), [stayDesert, avoidCollisions]); createDecoration( aBushesFertileLand.map(bush => [new SimpleObject(bush, 0, 4, 2, 4)]), aBushesFertileLand.map(bush => scaleByMapSize(20, 120) * 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"); for (let area of areasPassages) createObjectGroupsByAreas( new SimpleGroup([new RandomObject(aWaterDecoratives, 2, 4, 1, 2)], true), 0, undefined, 15, 20, [area]); 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, clWall, 20), avoidCollisionsNomad]); 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/rmgen-common/wall_builder.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/rmgen-common/wall_builder.js (revision 24616) +++ ps/trunk/binaries/data/mods/public/maps/random/rmgen-common/wall_builder.js (revision 24617) @@ -1,928 +1,935 @@ /** * @file Contains functionality to place walls on random maps. */ /** * Set some globals for this module. */ var g_WallStyles = loadWallsetsFromCivData(); var g_FortressTypes = createDefaultFortressTypes(); /** * Fetches wallsets from {civ}.json files, and then uses them to load * basic wall elements. */ function loadWallsetsFromCivData() { let wallsets = {}; for (let civ in g_CivData) { let civInfo = g_CivData[civ]; if (!civInfo.WallSets) continue; for (let path of civInfo.WallSets) { // File naming conventions: // - structures/wallset_{style} // - structures/{civ}/wallset_{style} let style = basename(path).split("_")[1]; if (path.split("/").indexOf(civ) != -1) style = civ + "/" + style; if (!wallsets[style]) wallsets[style] = loadWallset(Engine.GetTemplate(path), civ); } } return wallsets; } function loadWallset(wallsetPath, civ) { let newWallset = { "curves": [] }; let wallsetData = GetTemplateDataHelper(wallsetPath).wallSet; for (let element in wallsetData.templates) if (element == "curves") for (let filename of wallsetData.templates.curves) newWallset.curves.push(readyWallElement(filename, civ)); else newWallset[element] = readyWallElement(wallsetData.templates[element], civ); newWallset.overlap = wallsetData.minTowerOverlap * newWallset.tower.length; return newWallset; } /** * Fortress class definition * * We use "fortress" to describe a closed wall built of multiple wall * elements attached together surrounding a central point. We store the * abstract of the wall (gate, tower, wall, ...) and only apply the style * when we get to build it. * * @param {string} type - Descriptive string, example: "tiny". Not really needed (WallTool.wallTypes["type string"] is used). Mainly for custom wall elements. * @param {array} [wall] - Array of wall element strings. May be defined at a later point. * Example: ["medium", "cornerIn", "gate", "cornerIn", "medium", "cornerIn", "gate", "cornerIn"] * @param {Object} [centerToFirstElement] - Vector from the visual center of the fortress to the first wall element. * @param {number} [centerToFirstElement.x] * @param {number} [centerToFirstElement.y] */ function Fortress(type, wall=[], centerToFirstElement=undefined) { this.type = type; this.wall = wall; this.centerToFirstElement = centerToFirstElement; } function createDefaultFortressTypes() { let defaultFortresses = {}; /** * Define some basic default fortress types. */ let addFortress = (type, walls) => defaultFortresses[type] = { "wall": walls.concat(walls, walls, walls) }; addFortress("tiny", ["gate", "tower", "short", "cornerIn", "short", "tower"]); addFortress("small", ["gate", "tower", "medium", "cornerIn", "medium", "tower"]); addFortress("medium", ["gate", "tower", "long", "cornerIn", "long", "tower"]); addFortress("normal", ["gate", "tower", "medium", "cornerIn", "medium", "cornerOut", "medium", "cornerIn", "medium", "tower"]); addFortress("large", ["gate", "tower", "long", "cornerIn", "long", "cornerOut", "long", "cornerIn", "long", "tower"]); addFortress("veryLarge", ["gate", "tower", "medium", "cornerIn", "medium", "cornerOut", "long", "cornerIn", "long", "cornerOut", "medium", "cornerIn", "medium", "tower"]); addFortress("giant", ["gate", "tower", "long", "cornerIn", "long", "cornerOut", "long", "cornerIn", "long", "cornerOut", "long", "cornerIn", "long", "tower"]); /** * Define some fortresses based on those above, but designed for use * with the "palisades" wallset. */ for (let fortressType in defaultFortresses) { const fillTowersBetween = ["short", "medium", "long", "start", "end", "cornerIn", "cornerOut"]; const newKey = fortressType + "Palisades"; const oldWall = defaultFortresses[fortressType].wall; defaultFortresses[newKey] = { "wall": [] }; for (let j = 0; j < oldWall.length; ++j) { defaultFortresses[newKey].wall.push(oldWall[j]); if (j + 1 < oldWall.length && fillTowersBetween.indexOf(oldWall[j]) != -1 && fillTowersBetween.indexOf(oldWall[j + 1]) != -1) { defaultFortresses[newKey].wall.push("tower"); } } } return defaultFortresses; } /** * Define some helper functions */ /** * Get a wall element of a style. * * Valid elements: * long, medium, short, start, end, cornerIn, cornerOut, tower, fort, gate, entry, entryTower, entryFort * * Dynamic elements: * `gap_{x}` returns a non-blocking gap of length `x` meters. * `turn_{x}` returns a zero-length turn of angle `x` radians. * * Any other arbitrary string passed will be attempted to be used as: `structures/{civ}/{arbitrary_string}`. * * @param {string} element - What sort of element to fetch. * @param {string} [style] - The style from which this element should come from. * @returns {Object} The wall element requested. Or a tower element. */ function getWallElement(element, style) { style = validateStyle(style); if (g_WallStyles[style][element]) return g_WallStyles[style][element]; // Attempt to derive any unknown elements. // Defaults to a wall tower piece const quarterBend = Math.PI / 2; let wallset = g_WallStyles[style]; let civ = style.split("/")[0]; let ret = wallset.tower ? clone(wallset.tower) : { "angle": 0, "bend": 0, "length": 0, "indent": 0 }; switch (element) { case "cornerIn": if (wallset.curves) for (let curve of wallset.curves) if (curve.bend == quarterBend) ret = curve; if (ret.bend != quarterBend) { ret.angle += Math.PI / 4; ret.indent = ret.length / 4; ret.length = 0; ret.bend = Math.PI / 2; } break; case "cornerOut": if (wallset.curves) for (let curve of wallset.curves) if (curve.bend == quarterBend) { ret = clone(curve); ret.angle += Math.PI / 2; ret.indent -= ret.indent * 2; } if (ret.bend != quarterBend) { ret.angle -= Math.PI / 4; ret.indent = -ret.length / 4; ret.length = 0; } ret.bend = -Math.PI / 2; break; case "entry": ret.templateName = undefined; ret.length = wallset.gate.length; break; case "entryTower": ret.templateName = g_CivData[civ] ? "structures/" + civ + "/defense_tower" : "structures/palisades_watchtower"; ret.indent = ret.length * -3; ret.length = wallset.gate.length; break; case "entryFort": ret = clone(wallset.fort); ret.angle -= Math.PI; ret.length *= 1.5; ret.indent = ret.length; break; case "start": if (wallset.end) { ret = clone(wallset.end); ret.angle += Math.PI; } break; case "end": if (wallset.end) ret = wallset.end; break; default: if (element.startsWith("gap_")) { ret.templateName = undefined; ret.angle = 0; ret.length = +element.slice("gap_".length); } else if (element.startsWith("turn_")) { ret.templateName = undefined; ret.bend = +element.slice("turn_".length) * Math.PI; ret.length = 0; } else { if (!g_CivData[civ]) civ = Object.keys(g_CivData)[0]; let templateName = "structures/" + civ + "/" + element; if (Engine.TemplateExists(templateName)) { ret.indent = ret.length * (element == "outpost" || element.endsWith("_tower") ? -3 : 3.5); ret.templateName = templateName; ret.length = 0; } else warn("Unrecognised wall element: '" + element + "' (" + style + "). Defaulting to " + (wallset.tower ? "'tower'." : "a blank element.")); } } // Cache to save having to calculate this element again. g_WallStyles[style][element] = deepfreeze(ret); return ret; } /** * Prepare a wall element for inclusion in a style. * * @param {string} path - The template path to read values from */ function readyWallElement(path, civCode) { path = path.replace(/\{civ\}/g, civCode); let template = GetTemplateDataHelper(Engine.GetTemplate(path), null, null); let length = template.wallPiece ? template.wallPiece.length : template.obstruction.shape.width; return deepfreeze({ "templateName": path, "angle": template.wallPiece ? template.wallPiece.angle : Math.PI, "length": length / TERRAIN_TILE_SIZE, "indent": template.wallPiece ? template.wallPiece.indent / TERRAIN_TILE_SIZE : 0, "bend": template.wallPiece ? template.wallPiece.bend : 0 }); } /** * Returns a list of objects containing all information to place all the wall elements entities with placeObject (but the player ID) * Placing the first wall element at startX/startY placed with an angle given by orientation * An alignment can be used to get the "center" of a "wall" (more likely used for fortresses) with getCenterToFirstElement * * @param {Vector2D} position * @param {array} [wall] * @param {string} [style] * @param {number} [orientation] * @returns {array} */ function getWallAlignment(position, wall = [], style = "athen_stone", orientation = 0) { style = validateStyle(style); let alignment = []; let wallPosition = position.clone(); for (let i = 0; i < wall.length; ++i) { let element = getWallElement(wall[i], style); if (!element && i == 0) { warn("Not a valid wall element: style = " + style + ", wall[" + i + "] = " + wall[i] + "; " + uneval(element)); continue; } // Add wall elements entity placement arguments to the alignment alignment.push({ "position": Vector2D.sub(wallPosition, new Vector2D(element.indent, 0).rotate(-orientation)), "templateName": element.templateName, "angle": orientation + element.angle }); // Preset vars for the next wall element if (i + 1 < wall.length) { orientation += element.bend; let nextElement = getWallElement(wall[i + 1], style); if (!nextElement) { warn("Not a valid wall element: style = " + style + ", wall[" + (i + 1) + "] = " + wall[i + 1] + "; " + uneval(nextElement)); continue; } let distance = (element.length + nextElement.length) / 2 - g_WallStyles[style].overlap; // Corrections for elements with indent AND bending let indent = element.indent; let bend = element.bend; if (bend != 0 && indent != 0) { // Indent correction to adjust distance distance += indent * Math.sin(bend); // Indent correction to normalize indentation wallPosition.add(new Vector2D(indent).rotate(-orientation)); } // Set the next coordinates of the next element in the wall without indentation adjustment wallPosition.add(new Vector2D(distance, 0).rotate(-orientation).perpendicular()); } } return alignment; } /** * Center calculation works like getting the center of mass assuming all wall elements have the same "weight" * * Used to get centerToFirstElement of fortresses by default * * @param {number} alignment * @returns {Object} Vector from the center of the set of aligned wallpieces to the first wall element. */ function getCenterToFirstElement(alignment) { return alignment.reduce((result, align) => result.sub(Vector2D.div(align.position, alignment.length)), new Vector2D(0, 0)); } /** * Does not support bending wall elements like corners. * * @param {string} style * @param {array} wall * @returns {number} The sum length (in terrain cells, not meters) of the provided wall. */ function getWallLength(style, wall) { style = validateStyle(style); let length = 0; let overlap = g_WallStyles[style].overlap; for (let element of wall) length += getWallElement(element, style).length - overlap; return length; } /** * Makes sure the style exists and, if not, provides a fallback. * * @param {string} style * @param {number} [playerId] * @returns {string} Valid style. */ function validateStyle(style, playerId = 0) { if (!style || !g_WallStyles[style]) { if (playerId == 0) return Object.keys(g_WallStyles)[0]; style = getCivCode(playerId) + "/stone"; return !g_WallStyles[style] ? Object.keys(g_WallStyles)[0] : style; } return style; } /** * Define the different wall placer functions */ /** * Places an abitrary wall beginning at the location comprised of the array of elements provided. * * @param {Vector2D} position * @param {array} [wall] - Array of wall element types. Example: ["start", "long", "tower", "long", "end"] * @param {string} [style] - Wall style string. * @param {number} [playerId] - Identifier of the player for whom the wall will be placed. * @param {number} [orientation] - Angle at which the first wall element is placed. * 0 means "outside" or "front" of the wall is right (positive X) like placeObject * It will then be build towards top/positive Y (if no bending wall elements like corners are used) * Raising orientation means the wall is rotated counter-clockwise like placeObject */ function placeWall(position, wall = [], style, playerId = 0, orientation = 0, constraints = undefined) { style = validateStyle(style, playerId); let entities = []; let constraint = new StaticConstraint(constraints); for (let align of getWallAlignment(position, wall, style, orientation)) if (align.templateName && g_Map.inMapBounds(align.position) && constraint.allows(align.position.clone().floor())) entities.push(g_Map.placeEntityPassable(align.templateName, playerId, align.position, align.angle)); return entities; } /** * Places an abitrarily designed "fortress" (closed loop of wall elements) * centered around a given point. * * The fortress wall should always start with the main entrance (like * "entry" or "gate") to get the orientation correct. * * @param {Vector2D} centerPosition * @param {Object} [fortress] - If not provided, defaults to the predefined "medium" fortress type. * @param {string} [style] - Wall style string. * @param {number} [playerId] - Identifier of the player for whom the wall will be placed. * @param {number} [orientation] - Angle the first wall element (should be a gate or entrance) is placed. Default is 0 */ function placeCustomFortress(centerPosition, fortress, style, playerId = 0, orientation = 0, constraints = undefined) { fortress = fortress || g_FortressTypes.medium; style = validateStyle(style, playerId); // Calculate center if fortress.centerToFirstElement is undefined (default) let centerToFirstElement = fortress.centerToFirstElement; if (centerToFirstElement === undefined) centerToFirstElement = getCenterToFirstElement(getWallAlignment(new Vector2D(0, 0), fortress.wall, style)); // Placing the fortress wall let position = Vector2D.sum([ centerPosition, new Vector2D(centerToFirstElement.x, 0).rotate(-orientation), new Vector2D(centerToFirstElement.y, 0).perpendicular().rotate(-orientation) ]); return placeWall(position, fortress.wall, style, playerId, orientation, constraints); } /** * Places a predefined fortress centered around the provided point. * * @see Fortress * * @param {string} [type] - Predefined fortress type, as used as a key in g_FortressTypes. */ function placeFortress(centerPosition, type = "medium", style, playerId = 0, orientation = 0, constraints = undefined) { return placeCustomFortress(centerPosition, g_FortressTypes[type], style, playerId, orientation, constraints); } /** * Places a straight wall from a given point to another, using the provided * wall parts repeatedly. * * Note: Any "bending" wall pieces passed will be complained about. * * @param {Vector2D} startPosition - Approximate start point of the wall. * @param {Vector2D} targetPosition - Approximate end point of the wall. * @param {array} [wallPart=["tower", "long"]] * @param {number} [playerId] * @param {boolean} [endWithFirst] - If true, the first wall element will also be the last. */ function placeLinearWall(startPosition, targetPosition, wallPart = undefined, style, playerId = 0, endWithFirst = true, constraints = undefined) { wallPart = wallPart || ["tower", "long"]; style = validateStyle(style, playerId); // Check arguments for (let element of wallPart) if (getWallElement(element, style).bend != 0) warn("placeLinearWall : Bending is not supported by this function, but the following bending wall element was used: " + element); // Setup number of wall parts let totalLength = startPosition.distanceTo(targetPosition); let wallPartLength = getWallLength(style, wallPart); let numParts = Math.ceil(totalLength / wallPartLength); if (endWithFirst) numParts = Math.ceil((totalLength - getWallElement(wallPart[0], style).length) / wallPartLength); // Setup scale factor let scaleFactor = totalLength / (numParts * wallPartLength); if (endWithFirst) scaleFactor = totalLength / (numParts * wallPartLength + getWallElement(wallPart[0], style).length); // Setup angle let wallAngle = getAngle(startPosition.x, startPosition.y, targetPosition.x, targetPosition.y); let placeAngle = wallAngle - Math.PI / 2; // Place wall entities let entities = []; let position = startPosition.clone(); let overlap = g_WallStyles[style].overlap; let constraint = new StaticConstraint(constraints); for (let partIndex = 0; partIndex < numParts; ++partIndex) for (let elementIndex = 0; elementIndex < wallPart.length; ++elementIndex) { let wallEle = getWallElement(wallPart[elementIndex], style); let wallLength = (wallEle.length - overlap) / 2; let dist = new Vector2D(scaleFactor * wallLength, 0).rotate(-wallAngle); // Length correction position.add(dist); // Indent correction let place = Vector2D.add(position, new Vector2D(0, wallEle.indent).rotate(-wallAngle)); if (wallEle.templateName && g_Map.inMapBounds(place) && constraint.allows(place.clone().floor())) entities.push(g_Map.placeEntityPassable(wallEle.templateName, playerId, place, placeAngle + wallEle.angle)); position.add(dist); } if (endWithFirst) { let wallEle = getWallElement(wallPart[0], style); let wallLength = (wallEle.length - overlap) / 2; position.add(new Vector2D(scaleFactor * wallLength, 0).rotate(-wallAngle)); if (wallEle.templateName && g_Map.inMapBounds(position) && constraint.allows(position.clone().floor())) entities.push(g_Map.placeEntityPassable(wallEle.templateName, playerId, position, placeAngle + wallEle.angle)); } return entities; } /** * Places a (semi-)circular wall of repeated wall elements around a central * point at a given radius. * * The wall does not have to be closed, and can be left open in the form * of an arc if maxAngle < 2 * Pi. In this case, the orientation determines * where this open part faces, with 0 meaning "right" like an unrotated * building's drop-point. * * Note: Any "bending" wall pieces passed will be complained about. * * @param {Vector2D} center - Center of the circle or arc. * @param (number} radius - Approximate radius of the circle. (Given the maxBendOff argument) * @param {array} [wallPart] * @param {string} [style] * @param {number} [playerId] * @param {number} [orientation] - Angle at which the first wall element is placed. * @param {number} [maxAngle] - How far the wall should circumscribe the center. Default is Pi * 2 (for a full circle). * @param {boolean} [endWithFirst] - If true, the first wall element will also be the last. For full circles, the default is false. For arcs, true. * @param {number} [maxBendOff] Optional. How irregular the circle should be. 0 means regular circle, PI/2 means very irregular. Default is 0 (regular circle) */ function placeCircularWall(center, radius, wallPart, style, playerId = 0, orientation = 0, maxAngle = 2 * Math.PI, endWithFirst, maxBendOff = 0, constraints = undefined) { wallPart = wallPart || ["tower", "long"]; style = validateStyle(style, playerId); if (endWithFirst === undefined) endWithFirst = maxAngle < Math.PI * 2 - 0.001; // Can this be done better? // Check arguments if (maxBendOff > Math.PI / 2 || maxBendOff < 0) warn("placeCircularWall : maxBendOff should satisfy 0 < maxBendOff < PI/2 (~1.5rad) but it is: " + maxBendOff); for (let element of wallPart) if (getWallElement(element, style).bend != 0) warn("placeCircularWall : Bending is not supported by this function, but the following bending wall element was used: " + element); // Setup number of wall parts let totalLength = maxAngle * radius; let wallPartLength = getWallLength(style, wallPart); let numParts = Math.ceil(totalLength / wallPartLength); if (endWithFirst) numParts = Math.ceil((totalLength - getWallElement(wallPart[0], style).length) / wallPartLength); // Setup scale factor let scaleFactor = totalLength / (numParts * wallPartLength); if (endWithFirst) scaleFactor = totalLength / (numParts * wallPartLength + getWallElement(wallPart[0], style).length); // Place wall entities let entities = []; let constraint = new StaticConstraint(constraints); let actualAngle = orientation; let position = Vector2D.add(center, new Vector2D(radius, 0).rotate(-actualAngle)); let overlap = g_WallStyles[style].overlap; for (let partIndex = 0; partIndex < numParts; ++partIndex) for (let wallEle of wallPart) { wallEle = getWallElement(wallEle, style); // Width correction let addAngle = scaleFactor * (wallEle.length - overlap) / radius; let target = Vector2D.add(center, new Vector2D(radius, 0).rotate(-actualAngle - addAngle)); let place = Vector2D.average([position, target]); let placeAngle = actualAngle + addAngle / 2; // Indent correction place.sub(new Vector2D(wallEle.indent, 0).rotate(-placeAngle)); // Placement if (wallEle.templateName && g_Map.inMapBounds(place) && constraint.allows(place.clone().floor())) - entities.push(g_Map.placeEntityPassable(wallEle.templateName, playerId, place, placeAngle + wallEle.angle)); + { + let entity = g_Map.placeEntityPassable(wallEle.templateName, playerId, place, placeAngle + wallEle.angle); + if (entity) + entities.push(entity); + } // Prepare for the next wall element actualAngle += addAngle; position = Vector2D.add(center, new Vector2D(radius, 0).rotate(-actualAngle)); } if (endWithFirst) { let wallEle = getWallElement(wallPart[0], style); let addAngle = scaleFactor * wallEle.length / radius; let target = Vector2D.add(center, new Vector2D(radius, 0).rotate(-actualAngle - addAngle)); let place = Vector2D.average([position, target]); let placeAngle = actualAngle + addAngle / 2; if (g_Map.inMapBounds(place) && constraint.allows(place.clone().floor())) entities.push(g_Map.placeEntityPassable(wallEle.templateName, playerId, place, placeAngle + wallEle.angle)); } return entities; } /** * Places a polygonal wall of repeated wall elements around a central * point at a given radius. * * Note: Any "bending" wall pieces passed will be ignored. * * @param {Vector2D} centerPosition * @param {number} radius * @param {array} [wallPart] * @param {string} [cornerWallElement] - Wall element to be placed at the polygon's corners. * @param {string} [style] * @param {number} [playerId] * @param {number} [orientation] - Direction the first wall piece or opening in the wall faces. * @param {number} [numCorners] - How many corners the polygon will have. * @param {boolean} [skipFirstWall] - If the first linear wall part will be left opened as entrance. */ function placePolygonalWall(centerPosition, radius, wallPart, cornerWallElement = "tower", style, playerId = 0, orientation = 0, numCorners = 8, skipFirstWall = true, constraints = undefined) { wallPart = wallPart || ["long", "tower"]; style = validateStyle(style, playerId); let entities = []; let constraint = new StaticConstraint(constraints); let angleAdd = Math.PI * 2 / numCorners; let angleStart = orientation - angleAdd / 2; let corners = new Array(numCorners).fill(0).map((zero, i) => Vector2D.add(centerPosition, new Vector2D(radius, 0).rotate(-angleStart - i * angleAdd))); for (let i = 0; i < numCorners; ++i) { let angleToCorner = getAngle(corners[i].x, corners[i].y, centerPosition.x, centerPosition.y); if (g_Map.inMapBounds(corners[i]) && constraint.allows(corners[i].clone().floor())) - entities.push( - g_Map.placeEntityPassable(getWallElement(cornerWallElement, style).templateName, playerId, corners[i], angleToCorner)); + { + let entity = g_Map.placeEntityPassable(getWallElement(cornerWallElement, style).templateName, playerId, corners[i], angleToCorner); + if (entity) + entities.push(entity); + } if (!skipFirstWall || i != 0) { let cornerLength = getWallElement(cornerWallElement, style).length / 2; let cornerAngle = angleToCorner + angleAdd / 2; let targetCorner = (i + 1) % numCorners; let cornerPosition = new Vector2D(cornerLength, 0).rotate(-cornerAngle).perpendicular(); entities = entities.concat( placeLinearWall( // Adjustment to the corner element width (approximately) Vector2D.sub(corners[i], cornerPosition), Vector2D.add(corners[targetCorner], cornerPosition), wallPart, style, playerId, undefined, constraints)); } } return entities; } /** * Places an irregular polygonal wall consisting of parts semi-randomly * chosen from a provided assortment, built around a central point at a * given radius. * * Note: Any "bending" wall pieces passed will be ... I'm not sure. TODO: test what happens! * * Note: The wallPartsAssortment is last because it's the hardest to set. * * @param {Vector2D} centerPosition * @param {number} radius * @param {string} [cornerWallElement] - Wall element to be placed at the polygon's corners. * @param {string} [style] * @param {number} [playerId] * @param {number} [orientation] - Direction the first wallpiece or opening in the wall faces. * @param {number} [numCorners] - How many corners the polygon will have. * @param {number} [irregularity] - How irregular the polygon will be. 0 = regular, 1 = VERY irregular. * @param {boolean} [skipFirstWall] - If true, the first linear wall part will be left open as an entrance. * @param {array} [wallPartsAssortment] - An array of wall part arrays to choose from for each linear wall connecting the corners. */ function placeIrregularPolygonalWall(centerPosition, radius, cornerWallElement = "tower", style, playerId = 0, orientation = 0, numCorners, irregularity = 0.5, skipFirstWall = false, wallPartsAssortment = undefined, constraints = undefined) { style = validateStyle(style, playerId); numCorners = numCorners || randIntInclusive(5, 7); // Generating a generic wall part assortment with each wall part including 1 gate lengthened by walls and towers // NOTE: It might be a good idea to write an own function for that... let defaultWallPartsAssortment = [["short"], ["medium"], ["long"], ["gate", "tower", "short"]]; let centeredWallPart = ["gate"]; let extendingWallPartAssortment = [["tower", "long"], ["tower", "medium"]]; defaultWallPartsAssortment.push(centeredWallPart); for (let assortment of extendingWallPartAssortment) { let wallPart = centeredWallPart; for (let j = 0; j < radius; ++j) { if (j % 2 == 0) wallPart = wallPart.concat(assortment); else { assortment.reverse(); wallPart = assortment.concat(wallPart); assortment.reverse(); } defaultWallPartsAssortment.push(wallPart); } } // Setup optional arguments to the default wallPartsAssortment = wallPartsAssortment || defaultWallPartsAssortment; // Setup angles let angleToCover = Math.PI * 2; let angleAddList = []; for (let i = 0; i < numCorners; ++i) { // Randomize covered angles. Variety scales down with raising angle though... angleAddList.push(angleToCover / (numCorners - i) * (1 + randFloat(-irregularity, irregularity))); angleToCover -= angleAddList[angleAddList.length - 1]; } // Setup corners let corners = []; let angleActual = orientation - angleAddList[0] / 2; for (let i = 0; i < numCorners; ++i) { corners.push(Vector2D.add(centerPosition, new Vector2D(radius, 0).rotate(-angleActual))); if (i < numCorners - 1) angleActual += angleAddList[i + 1]; } // Setup best wall parts for the different walls (a bit confusing naming...) let wallPartLengths = []; let maxWallPartLength = 0; for (let wallPart of wallPartsAssortment) { let length = getWallLength(style, wallPart); wallPartLengths.push(length); if (length > maxWallPartLength) maxWallPartLength = length; } let wallPartList = []; // This is the list of the wall parts to use for the walls between the corners, not to confuse with wallPartsAssortment! for (let i = 0; i < numCorners; ++i) { let bestWallPart = []; // This is a simple wall part not a wallPartsAssortment! let bestWallLength = Infinity; let targetCorner = (i + 1) % numCorners; // NOTE: This is not quite the length the wall will be in the end. Has to be tweaked... let wallLength = corners[i].distanceTo(corners[targetCorner]); let numWallParts = Math.ceil(wallLength / maxWallPartLength); for (let partIndex = 0; partIndex < wallPartsAssortment.length; ++partIndex) { let linearWallLength = numWallParts * wallPartLengths[partIndex]; if (linearWallLength < bestWallLength && linearWallLength > wallLength) { bestWallPart = wallPartsAssortment[partIndex]; bestWallLength = linearWallLength; } } wallPartList.push(bestWallPart); } // Place Corners and walls let entities = []; let constraint = new StaticConstraint(constraints); for (let i = 0; i < numCorners; ++i) { let angleToCorner = getAngle(corners[i].x, corners[i].y, centerPosition.x, centerPosition.y); if (g_Map.inMapBounds(corners[i]) && constraint.allows(corners[i].clone().floor())) entities.push( g_Map.placeEntityPassable(getWallElement(cornerWallElement, style).templateName, playerId, corners[i], angleToCorner)); if (!skipFirstWall || i != 0) { let cornerLength = getWallElement(cornerWallElement, style).length / 2; let targetCorner = (i + 1) % numCorners; let startAngle = angleToCorner + angleAddList[i] / 2; let targetAngle = angleToCorner + angleAddList[targetCorner] / 2; entities = entities.concat( placeLinearWall( // Adjustment to the corner element width (approximately) Vector2D.sub(corners[i], new Vector2D(cornerLength, 0).perpendicular().rotate(-startAngle)), Vector2D.add(corners[targetCorner], new Vector2D(cornerLength, 0).rotate(-targetAngle - Math.PI / 2)), wallPartList[i], style, playerId, false, constraints)); } } return entities; } /** * Places a generic fortress with towers at the edges connected with long * walls and gates, positioned around a central point at a given radius. * * The difference between this and the other two Fortress placement functions * is that those place a predefined fortress, regardless of terrain type. * This function attempts to intelligently place a wall circuit around * the central point taking into account terrain and other obstacles. * * This is the default Iberian civ bonus starting wall. * * @param {Vector2D} center - The approximate center coordinates of the fortress * @param {number} [radius] - The approximate radius of the wall to be placed. * @param {number} [playerId] * @param {string} [style] * @param {number} [irregularity] - 0 = circle, 1 = very spiky * @param {number} [gateOccurence] - Integer number, every n-th walls will be a gate instead. * @param {number} [maxTries] - How often the function tries to find a better fitting shape. */ function placeGenericFortress(center, radius = 20, playerId = 0, style, irregularity = 0.5, gateOccurence = 3, maxTries = 100, constraints = undefined) { style = validateStyle(style, playerId); // Setup some vars let startAngle = randomAngle(); let actualOff = new Vector2D(radius, 0).rotate(-startAngle); let actualAngle = startAngle; let pointDistance = getWallLength(style, ["long", "tower"]); // Searching for a well fitting point derivation let tries = 0; let bestPointDerivation; let minOverlap = 1000; let overlap; while (tries < maxTries && minOverlap > g_WallStyles[style].overlap) { let pointDerivation = []; let distanceToTarget = 1000; while (true) { let indent = randFloat(-irregularity * pointDistance, irregularity * pointDistance); let tmp = new Vector2D(radius + indent, 0).rotate(-actualAngle - pointDistance / radius); let tmpAngle = getAngle(actualOff.x, actualOff.y, tmp.x, tmp.y); actualOff.add(new Vector2D(pointDistance, 0).rotate(-tmpAngle)); actualAngle = getAngle(0, 0, actualOff.x, actualOff.y); pointDerivation.push(actualOff.clone()); distanceToTarget = pointDerivation[0].distanceTo(actualOff); let numPoints = pointDerivation.length; if (numPoints > 3 && distanceToTarget < pointDistance) // Could be done better... { overlap = pointDistance - pointDerivation[numPoints - 1].distanceTo(pointDerivation[0]); if (overlap < minOverlap) { minOverlap = overlap; bestPointDerivation = pointDerivation; } break; } } ++tries; } log("placeGenericFortress: Reduced overlap to " + minOverlap + " after " + tries + " tries"); // Place wall let entities = []; let constraint = new StaticConstraint(constraints); for (let pointIndex = 0; pointIndex < bestPointDerivation.length; ++pointIndex) { let start = Vector2D.add(center, bestPointDerivation[pointIndex]); let target = Vector2D.add(center, bestPointDerivation[(pointIndex + 1) % bestPointDerivation.length]); let angle = getAngle(start.x, start.y, target.x, target.y); let element = (pointIndex + 1) % gateOccurence == 0 ? "gate" : "long"; element = getWallElement(element, style); if (element.templateName) { let pos = Vector2D.add(start, new Vector2D(start.distanceTo(target) / 2, 0).rotate(-angle)); if (g_Map.inMapBounds(pos) && constraint.allows(pos.clone().floor())) entities.push(g_Map.placeEntityPassable(element.templateName, playerId, pos, angle - Math.PI / 2 + element.angle)); } // Place tower start = Vector2D.add(center, bestPointDerivation[(pointIndex + bestPointDerivation.length - 1) % bestPointDerivation.length]); angle = getAngle(start.x, start.y, target.x, target.y); let tower = getWallElement("tower", style); let pos = Vector2D.add(center, bestPointDerivation[pointIndex]); if (g_Map.inMapBounds(pos) && constraint.allows(pos.clone().floor())) entities.push( g_Map.placeEntityPassable(tower.templateName, playerId, pos, angle - Math.PI / 2 + tower.angle)); } return entities; }