Index: binaries/data/mods/public/maps/random/danube.js =================================================================== --- /dev/null +++ binaries/data/mods/public/maps/random/danube.js @@ -0,0 +1,830 @@ +RMS.LoadLibrary("rmgen"); + +// Spawn ships away from the shoreline, but patrol close to the shoreline +const triggerPointShipSpawn = "special/trigger_point_A"; +const triggerPointShipPatrol = "special/trigger_point_F"; +const triggerPointShipUnloadLeft = "special/trigger_point_B"; +const triggerPointShipUnloadRight = "special/trigger_point_C"; +const triggerPointLandPatrolLeft = "special/trigger_point_D"; +const triggerPointLandPatrolRight = "special/trigger_point_E"; + +// Terrain textures +const tRoad = "steppe_river_rocks"; +const tIsland = ["temp_grass_long_b_aut", "temp_grass_plants_aut", "temp_forestfloor_aut"]; +const tCliff = "temp_cliff_a"; +const tForestFloor = "temp_forestfloor_aut"; +const tGrass = "medit_shrubs_golden"; +const tGrass2 ="grass_mediterranean_dry_1024test"; +const tGrass3 = "medit_grass_field_b"; +const tShore = "temp_dirt_gravel_b"; +const tWater = "steppe_river_rocks_wet"; +const tSeaDepths = "medit_sea_depths"; + +// Gaia entities +const oBerryBush = "gaia/flora_bush_berry"; +const oDeer = "gaia/fauna_deer"; +const oFish = "gaia/fauna_fish"; +const oSheep = "gaia/fauna_sheep"; +const oGoat = "gaia/fauna_goat"; +const oWolf = "gaia/fauna_wolf"; +const oHawk = "gaia/fauna_hawk"; +const oRabbit = "gaia/fauna_rabbit"; +const oBoar = "gaia/fauna_boar"; +const oBear = "gaia/fauna_bear"; +const oStoneLarge = "gaia/geology_stonemine_temperate_quarry"; +const oStoneSmall = "gaia/geology_stone_temperate"; +const oStoneRuins = "gaia/special_ruins_standing_stone"; +const oMetalLarge = "gaia/geology_metal_mediterranean_slabs"; +const oApple = "gaia/flora_tree_apple"; +const oAcacia = "gaia/flora_tree_acacia"; +const oOak = "gaia/flora_tree_oak_aut"; +const oOak2 = "gaia/flora_tree_oak_aut_new"; +const oOak3 = "gaia/flora_tree_oak_dead"; +const oOak4 = "gaia/flora_tree_oak"; +const oPopolar = "gaia/flora_tree_poplar_lombardy"; +const oBeech = "gaia/flora_tree_euro_beech_aut"; +const oBeech2 = "gaia/flora_tree_euro_beech"; +const oTreasures = [ + "gaia/special_treasure_food_barrel", + "gaia/special_treasure_food_bin", + "gaia/special_treasure_stone", + "gaia/special_treasure_wood", + "gaia/special_treasure_metal" +]; + +const oCivicCenter = "structures/gaul_civil_centre"; +const oHouse = "structures/gaul_house"; +const oTemple = "structures/gaul_temple"; +const oTavern = "structures/gaul_tavern"; +const oTower= "structures/gaul_defense_tower"; +const oOutpost = "structures/gaul_outpost"; + +const oHut = "other/celt_hut"; +const oLongHouse = "other/celt_longhouse"; +const oPalisadeTower = "other/palisades_rocks_watchtower"; +const oTallSpikes = "other/palisades_tall_spikes"; +const oAngleSpikes = "other/palisades_angle_spike"; + +const oFemale = "units/gaul_support_female_citizen"; +const oHealer = "units/gaul_support_healer_e"; +const oSkirmisher = "units/gaul_infantry_javelinist_e"; +const oNakedFanatic = "units/gaul_champion_fanatic"; + +// Decorative props +const aBush1 = "actor|props/flora/bush_tempe_sm.xml"; +const aBush2 = "actor|props/flora/bush_tempe_me.xml"; +const aBush3 = "actor|props/flora/bush_tempe_la.xml"; +const aBush4 = "actor|props/flora/bush_tempe_me.xml"; +const aRock1 = "actor|geology/stone_granite_med.xml"; +const aRock2 = "actor|geology/stone_granite_boulder.xml"; +const aRock3 = "actor|geology/stone_granite_greek_boulder.xml"; +const aRock4 = "actor|geology/stonemine_alpine_a.xml"; +const aFerns = "actor|props/flora/ferns.xml"; +const aBucket = "actor|props/structures/celts/blacksmith_bucket"; +const aBarrel = "actor|props/structures/gauls/storehouse_barrel_b"; +const aTartan = "actor|props/structures/celts/tartan_a"; +const aWheel = "actor|props/special/eyecandy/wheel_laying"; +const aWell = "actor|props/special/eyecandy/well_1_b"; +const aWoodcord = "actor|props/special/eyecandy/woodcord"; +const aWaterLog = "actor|props/flora/water_log.xml"; +const aBlacksmithDecor = "actor|props/structures/hellenes/blacksmith_spears"; +const aCampfire = "actor|props/special/eyecandy/campfire"; +const aBench = "actor|props/special/eyecandy/bench_1"; +const aRug = "actor|props/special/eyecandy/rug_stand_iber"; + +const pForest1 = [ + tForestFloor, + tForestFloor + TERRAIN_SEPARATOR + oOak, + tForestFloor + TERRAIN_SEPARATOR + oOak2, + tForestFloor + TERRAIN_SEPARATOR + oOak3, + tForestFloor + TERRAIN_SEPARATOR + oOak4, + tForestFloor +]; + +const pForest2 = [ + tForestFloor, + tForestFloor + TERRAIN_SEPARATOR + oPopolar, + tForestFloor + TERRAIN_SEPARATOR + oBeech, + tForestFloor + TERRAIN_SEPARATOR + oBeech2, + tForestFloor + TERRAIN_SEPARATOR + oAcacia, + tForestFloor +]; + +// Minimum distance from the map border to ship ungarrison points +const ShorelineDistance = 15; + +log("Initializing map..."); +InitMap(); + +const numPlayers = getNumPlayers(); +const mapSize = getMapSize(); + +var clPlayer = createTileClass(); +var clForest = createTileClass(); +var clWater = createTileClass(); +var clLand = [createTileClass(), createTileClass()]; +var clLandPatrolPoint = [createTileClass(), createTileClass()]; +var clShore = [createTileClass(), createTileClass()]; +var clShoreUngarrisonPoint = [createTileClass(), createTileClass()]; +var clShip = createTileClass(); +var clShipPatrol = createTileClass(); +var clDirt = createTileClass(); +var clRock = createTileClass(); +var clMetal = createTileClass(); +var clFood = createTileClass(); +var clBaseResource = createTileClass(); +var clGrass = createTileClass(); +var clHill = createTileClass(); +var clIsland = createTileClass(); +var clTreasure = createTileClass(); +var clWaterLog = createTileClass(); +var clGauls = createTileClass(); +var clTower = createTileClass(); +var clOutpost = createTileClass(); +var clPath = createTileClass(); +var clRitualPlace = createTileClass(); + +// Percentage of the mapsize that the river takes up +const waterWidth = 0.3; + +// How many treasures will be placed near the gallic civic centers +var gallicCCTreasureCount = 10; + +// How many treasures will be placed randomly on the map at most +var randomTreasureCount = randIntInclusive(0, 3 * numPlayers); + +// Place a gaia village on small maps and larger +if (mapSize >= 192) +{ + log("Creating gallic villages..."); + let gaulCityRadius = 12; + let gaulCityBorderDistance = mapSize < 256 ? 10 : 18; + + // Whether to add a celtic ritual and apath from the gallic city leading to it + let addCelticRitual = true; + + // One village left and right of the river + for (let i = 0; i < 2; ++i) + { + let gX = i == 0 ? gaulCityBorderDistance : mapSize - gaulCityBorderDistance; + let gZ = mapSize / 2; + + if (addCelticRitual) + { + // Don't position the meeting place at the center of the map + let mLocation = randFloat(0.1, 0.4) * (randBool() ? 1 : -1); + + // Center of the meeting place + let mX = i == 0 ? + mapSize * waterWidth : + mapSize * (1 - waterWidth); + let mZ = gZ + mapSize * mLocation; + + // Radius of the meeting place + let mRadius = scaleByMapSize(4, 6); + + // Create a path connecting the gaia city with a meeting place at the shoreline. + // To avoid the path going through the palisade wall, start it at the gate, not at the city center. + let placer = new PathPlacer( + gX + gaulCityRadius * (i == 0 ? 1 : -1), + gZ, + mX, + mZ, + 4, // width + 0.4, // waviness + 4, // smoothness + 0.2, // offset + 0.05); // tapering + + let terrainPainter = new LayeredPainter([tShore, tRoad, tRoad], [1, 3]); + let elevationPainter = new SmoothElevationPainter(ELEVATION_SET, 5, 4); + createArea(placer, [terrainPainter, elevationPainter, paintClass(clPath)]); + + // Create the meeting place near the shoreline at the end of the path + createArea( + new ClumpPlacer(mRadius * mRadius * PI, 0.6, 0.3, 10, mX, mZ), + [new LayeredPainter([tShore, tShore], [1]), paintClass(clPath), paintClass(clRitualPlace)], + null); + + placeObject(mX, mZ, aCampfire, 0, randFloat(0, 2 * PI)); + + let femaleCount = Math.round(mRadius * 2); + let maleCount = Math.round(mRadius * 3); + let benchCount = Math.round(mRadius * 2); + let rugCount = Math.round(mRadius * 2.5); + let goatCount = Math.round(mRadius * 1.5); + + let femaleRadus = mRadius * 0.3; + let maleRadus = mRadius * 0.4; + let benchRadius = mRadius * 0.5; + let rugRadus = mRadius * 0.6; + let goatRadus = mRadius * 0.8; + + wallStyles.celt_ritual = { + "female": new WallElement("female", oFemale, PI, femaleRadus, 0, 2 * PI / femaleCount), + "skirmisher": new WallElement("skirmisher", oSkirmisher, PI, maleRadus, 0, 2 * PI / maleCount), + "healer": new WallElement("healer", oHealer, PI, maleRadus, 0, 2 * PI / maleCount), + "fanatic": new WallElement("fanatic", oNakedFanatic, PI, maleRadus, 0, 2 * PI / maleCount), + "bench": new WallElement("bench", aBench, PI/2, benchRadius, 0, 2 * PI / benchCount), + "rug": new WallElement("rug", aRug, 0, rugRadus, 0, 2 * PI / rugCount), + "goat": new WallElement("goat", oGoat, PI, goatRadus, 0, 2 * PI / goatCount), + }; + + placeCustomFortress(mX, mZ, new Fortress("celt ritual females", new Array(femaleCount).fill("female")), "celt_ritual", 0, 0); + + placeCustomFortress(mX, mZ, new Fortress("celt ritual males", new Array(maleCount).fill(0).map(i => + pickRandom(["skirmisher", "healer", "fanatic"]))), "celt_ritual", 0, 0); + + placeCustomFortress(mX, mZ, new Fortress("celt ritual bench", new Array(benchCount).fill("bench")), "celt_ritual", 0, 0); + placeCustomFortress(mX, mZ, new Fortress("celt ritual rug", new Array(rugCount).fill("rug")), "celt_ritual", 0, 0); + placeCustomFortress(mX, mZ, new Fortress("celt ritual goat", new Array(goatCount).fill("goat")), "celt_ritual", 0, 0); + } + + placeObject(gX, gZ, oCivicCenter, 0, BUILDING_ORIENTATION + PI * 3/2 * i); + + // Create the city patch + createArea( + new ClumpPlacer(gaulCityRadius * gaulCityRadius * PI, 0.6, 0.3, 10, gX, gZ), + [new LayeredPainter([tShore, tShore], [1]), paintClass(clGauls)], + null); + + // Place palisade fortress and some city buildings + // Use actors to avoid players capturing the buildings + wallStyles.gaul.house = new WallElement("house", oHouse, PI, 0, 4); + wallStyles.gaul.hut = new WallElement("hut", oHut, PI, 0, 4); + wallStyles.gaul.longhouse = new WallElement("longhouse", oLongHouse, PI, 0, 4); + wallStyles.gaul.tavern = new WallElement("tavern", oTavern, PI*3/2, 0, 4); + wallStyles.gaul.temple = new WallElement("temple", oTemple, PI*3/2, 0, 4); + wallStyles.gaul.defense_tower = new WallElement("defense_tower", + mapSize >= 256 ? oTower : oPalisadeTower, PI/2, 0, 4); + wallStyles.gaul.palisade_tower = wallStyles.palisades.tower; + + // Replace stone walls with palisade walls + for (let template of ["gate", "wallLong", "cornerIn", "cornerOut"]) + wallStyles.gaul[template] = wallStyles.palisades[template]; + + let wall = [ + "gate", "hut", "palisade_tower", "wallLong", "wallLong", + "cornerIn", "defense_tower", "wallLong", "wallLong", "temple", + "palisade_tower", "wallLong", "house", "wallLong", "palisade_tower", "longhouse", "wallLong", "wallLong", + "cornerIn", "defense_tower", "wallLong", "tavern", "wallLong", "palisade_tower"]; + wall = wall.concat(wall); + placeCustomFortress(gX, gZ, new Fortress("Geto-Dacian Tribal Confederation", wall), "gaul", 0, PI); + + // Place spikes + wallStyles.palisades.tall_spikes = new WallElement("tall_spikes", oTallSpikes, PI/2, 2); + wallStyles.palisades.spikeIn = new WallElement("spikeIn", oAngleSpikes, -PI/4, 2.1, 0.7, PI/2); + wallStyles.palisades.spikeMid = new WallElement("spikeIn", oAngleSpikes, -PI/2, 0.7); + wallStyles.palisades.gateGap = new WallElement("gateGap", undefined, PI, 3.6); + + let manySpikes = new Array(4).fill("tall_spikes"); + let spikes= [ + "gateGap", + "spikeMid", ...manySpikes, + "spikeIn", ...manySpikes, + "spikeMid", ...manySpikes, + "spikeMid", ...manySpikes, + "spikeIn", ...manySpikes, + "spikeMid" + ]; + spikes = spikes.concat(spikes); + placeCustomFortress(gX, gZ, new Fortress("spikes", spikes), "palisades", 0, PI); + + // Place treasure, potentially inside buildings + for (let i = 0; i < gallicCCTreasureCount; ++i) + placeObject( + gX + randFloat(-0.8, 0.8) * gaulCityRadius, + gZ + randFloat(-0.8, 0.8) * gaulCityRadius, + pickRandom(oTreasures), + 0, + randFloat(0, 2 * PI)); + } +} +RMS.SetProgress(10); + +// Randomize player order +var playerIDs = []; +for (var i = 0; i < numPlayers; i++) + playerIDs.push(i+1); +playerIDs = primeSortPlayers(sortPlayers(playerIDs)); + +// Place players +var playerX = new Array(numPlayers); +var playerZ = new Array(numPlayers); +var iop = 0; +for (var i = 0; i < numPlayers; i++) +{ + iop = i - 1; + + if (numPlayers % 2 == 0) + playerZ[i] = ((iop + Math.abs(iop % 2))/2 + 1) / (numPlayers / 2 + 1); + else if (iop % 2) + playerZ[i] = ((iop + Math.abs(iop % 2))/2 + 1) / (((numPlayers + 1) / 2) + 1); + else + playerZ[i] = (iop/2 + 1) / (((numPlayers - 1) / 2) + 1); + + playerX[i] = 0.2 + 0.6 * (i % 2); +} + +for (var i = 0; i < numPlayers; ++i) +{ + var id = playerIDs[i]; + log("Creating base for player " + id + "..."); + + var radius = scaleByMapSize(15, 25); + + var fx = fractionToTiles(playerX[i]); + var fz = fractionToTiles(playerZ[i]); + var ix = floor(fx); + var iz = floor(fz); + addToClass(ix, iz, clPlayer); + + // Create the city patch + var cityRadius = radius/3; + var placer = new ClumpPlacer(PI*cityRadius*cityRadius, 0.6, 0.3, 10, ix, iz); + var painter = new LayeredPainter([tShore, tRoad], [1]); + createArea(placer, painter, null); + + placeCivDefaultEntities(fx, fz, id, { 'iberWall': false }); + + placeDefaultChicken(fx, fz, clBaseResource); + + // Create berry bushes + var bbAngle = randFloat(0, TWO_PI); + var bbDist = 10; + var bbX = round(fx + bbDist * cos(bbAngle)); + var bbZ = round(fz + bbDist * sin(bbAngle)); + var group = new SimpleGroup( + [new SimpleObject(oBerryBush, 5,5, 0,3)], + true, clBaseResource, bbX, bbZ + ); + createObjectGroup(group, 0); + + // Create metal mine + var mDist = scaleByMapSize(9, 14); + bbAngle += randFloat(PI/4, PI/3); + var mX = round(fx + mDist * cos(bbAngle)); + var mZ = round(fz + mDist * sin(bbAngle)); + group = new SimpleGroup( + [new SimpleObject(oMetalLarge, 1,1, 0,0)], + true, clBaseResource, mX, mZ + ); + createObjectGroup(group, 0); + + // Create stone mines + bbAngle += randFloat(PI/3, PI/2); + mX = round(fx + mDist * cos(bbAngle)); + mZ = round(fz + mDist * sin(bbAngle)); + group = new SimpleGroup( + [new SimpleObject(oStoneLarge, 1,1, 0,2)], + true, clBaseResource, mX, mZ + ); + createObjectGroup(group, 0); + + // Create starting trees + var num = 20; + bbAngle += randFloat(-PI/3, 4*PI/3); + var tDist = randFloat(10, 14); + var tX = round(fx + tDist * cos(bbAngle)); + var tZ = round(fz + tDist * sin(bbAngle)); + group = new SimpleGroup( + [new SimpleObject(oOak, num, num, 0,5)], + false, clBaseResource, tX, tZ + ); + createObjectGroup(group, 0, avoidClasses(clBaseResource, 4)); + + placeDefaultDecoratives(fx, fz, aBush1, clBaseResource, radius); +} +RMS.SetProgress(20); + +log("Creating the river"); +var theta = randFloat(0, 0.8); +var theta2 = randFloat(0, 1.2); +var seed = randFloat(3, 5); +var seed2 = randFloat(2, 6); +var fadeDist = 0.05; + +for (let ix = 0; ix < mapSize; ++ix) + for (let iz = 0; iz < mapSize; ++iz) + { + let x = ix / (mapSize + 1.0); + let z = iz / (mapSize + 1.0); + + // add the rough shape of the water + let km = 30 / scaleByMapSize(35, 100); + let cu = km * rndRiver(theta + z * mapSize / 128, seed); + let cu2 = km * rndRiver(theta2 + z * mapSize / 128, seed2); + + if (x < cu + 0.5 - waterWidth / 2) + { + addToClass(ix, iz, clLand[0]); + continue; + } + + if (x > cu2 + 0.5 + waterWidth / 2) + { + addToClass(ix, iz, clLand[1]); + continue; + } + + let height; + if (x < cu + 0.5 + fadeDist - waterWidth / 2) + height = 2 - 5 * (1 - ((cu + 0.5 + fadeDist - waterWidth / 2) - x) / fadeDist); + else if (x > (cu2 + 0.5 - fadeDist + waterWidth / 2)) + height = 2 - 5 * (1 - (x - (cu2 + 0.5 - fadeDist + waterWidth / 2)) / fadeDist); + else + height = -3.0; + + setHeight(ix, iz, height); + if (height < 0.7) + addToClass(ix, iz, clWater); + + // Distinguish left and right shoreline + if (0 < height && height < 1 && iz > ShorelineDistance && iz < mapSize - ShorelineDistance) + addToClass(ix, iz, clShore[ix < mapSize / 2 ? 0 : 1]); + } +RMS.SetProgress(30); + +log("Creating shores..."); +paintTerrainBasedOnHeight(-20, 1, 0, tWater); +paintTerrainBasedOnHeight(1, 2, 0, tShore); +RMS.SetProgress(35); + +log("Creating bumps..."); +createBumps(avoidClasses(clPlayer, 6, clWater, 2, clPath, 1), scaleByMapSize(30, 300), 1, 8, 4, 0, 3); +RMS.SetProgress(40); + +log("Creating hills..."); +if (randInt(1,2) == 1) + createHills([tCliff, tCliff, tCliff], avoidClasses(clPlayer, 18, clHill, 20, clWater, 2, clGauls, 5, clPath, 1), clHill, scaleByMapSize(3, 15)); +else + createMountains(tCliff, avoidClasses(clPlayer, 18, clHill, 20, clWater, 3, clGauls, 5, clPath, 1), clHill, scaleByMapSize(3, 15)); +RMS.SetProgress(45); + +log("Creating forests..."); +createForests( + [tForestFloor, tForestFloor, tForestFloor, pForest1, pForest2], + avoidClasses(clPlayer, 16, clForest, 17, clWater, 5, clHill, 2, clGauls, 5, clPath, 1), + clForest +); +RMS.SetProgress(50); + +log("Creating grass patches..."); +createLayeredPatches( + [scaleByMapSize(3, 6), scaleByMapSize(5, 10), scaleByMapSize(8, 21)], + [[tGrass, tGrass2],[tGrass2, tGrass3], [tGrass3, tGrass]], + [1,1], + avoidClasses(clForest, 0, clGrass, 2, clPlayer, 10, clWater, 2, clDirt, 2, clHill, 1, clGauls, 5, clPath, 1) +); +RMS.SetProgress(55); + +log("Creating islands..."); +placer = new ChainPlacer(floor(scaleByMapSize(3, 4)), floor(scaleByMapSize(4, 8)), floor(scaleByMapSize(50, 80)), 0.5); +var terrainPainter = new LayeredPainter([tWater, tShore, tIsland], [2, 1]); +var elevationPainter = new SmoothElevationPainter(ELEVATION_SET, 6, 4); +createAreas( + placer, + [terrainPainter, elevationPainter, paintClass(clIsland)], + [avoidClasses(clIsland, 30), stayClasses (clWater, 8)], + scaleByMapSize(1, 4) * numPlayers +); +RMS.SetProgress(60); + +log("Creating island bumps..."); +createBumps(stayClasses(clIsland, 2), scaleByMapSize(50, 400), 1, 8, 4, 0, 3); + +log("Paint seabed..."); +paintTerrainBasedOnHeight(-20, -3, 3, tSeaDepths); + +log("Creating island metal mines..."); +createObjectGroups( + new SimpleGroup([new SimpleObject(oMetalLarge, 1,1, 0,4)], true, clMetal), + 0, + [avoidClasses(clMetal, 50, clRock, 10), stayClasses(clIsland, 5)], + 500, 1 +); + +log("Creating island stone mines..."); +createObjectGroups( + new SimpleGroup([new SimpleObject(oStoneLarge, 1,1, 0,4)], true, clRock), + 0, + [avoidClasses(clMetal, 10, clRock, 50), stayClasses(clIsland, 5)], + 500, 1 +); + +RMS.SetProgress(70); + +log("Creating island towers..."); +createObjectGroups( + new SimpleGroup([new SimpleObject(oTower, 1,1, 0,4)], true, clTower), + 0, + [avoidClasses(clMetal, 4, clRock, 4, clTower, 20), stayClasses(clIsland, 7)], + 500, 1 +); + +log("Creating island outposts..."); +createObjectGroups( + new SimpleGroup([new SimpleObject(oOutpost, 1,1, 0,4)], true, clOutpost), + 0, + [avoidClasses(clMetal, 4, clRock, 4, clTower, 5, clOutpost, 20), stayClasses(clIsland, 7)], + 500, 1 +); + +log("Creating metal mines..."); +createObjectGroups( + new SimpleGroup([new SimpleObject(oMetalLarge, 1,1, 0,4)], true, clMetal), + 0, + [avoidClasses(clForest, 4, clBaseResource, 20, clMetal, 50, clRock, 20, clWater, 4, clHill, 4, clGauls, 5, clPath, 5)], + 500, 1 +); + +log("Creating stone mines..."); +createObjectGroups( + new SimpleGroup([new SimpleObject(oStoneLarge, 1,1, 0,4)], true, clRock), + 0, + [avoidClasses(clForest, 4, clBaseResource, 20, clMetal, 20, clRock, 50, clWater, 4, clHill, 4, clGauls, 5, clPath, 5)], + 500, 1 +); + +log("Creating stone ruins..."); +createObjectGroups( + new SimpleGroup([new SimpleObject(oStoneRuins, 1,1, 0,4)], true, clRock), + 0, + [avoidClasses(clForest, 2, clPlayer, 12, clMetal, 6, clRock, 25, clWater, 4, clHill, 4, clGauls, 5, clPath, 1)], + 500, 1 + ); +RMS.SetProgress(65); + +log("Creating decoratives..."); +for (let i = 0; i < 2; ++i) + createDecoration( + [ + [new SimpleObject(aRock1, 1,1, 0,1)], + [new SimpleObject(aRock2, 1,1, 0,1)], + [new SimpleObject(aRock3, 1,1, 0,1)], + [new SimpleObject(aRock4, 1,1, 0,1)], + + [new SimpleObject(aBush1, 1,3, 0,2)], + [new SimpleObject(aBush2, 1,2, 0,1)], + [new SimpleObject(aBush3, 1,3, 0,2)], + [new SimpleObject(aBush4, 1,2, 0,1)], + + [new SimpleObject(aFerns, 2,5, 2,4)], + ], + [ + scaleByMapSize(5, 80), + scaleByMapSize(5, 80), + scaleByMapSize(5, 80), + scaleByMapSize(5, 80), + + scaleByMapSize(5, 80), + scaleByMapSize(5, 80), + scaleByMapSize(5, 80), + scaleByMapSize(5, 80), + + scaleByMapSize(20, 80), + ], + i == 0 ? + avoidClasses(clWater, 4, clForest, 1, clPlayer, 16, clRock, 4, clMetal, 4, clHill, 4, clGauls, 5, clPath, 1) : + [stayClasses(clIsland, 4) , avoidClasses(clForest, 1, clRock, 4, clMetal, 4)] + ); +RMS.SetProgress(70); + +log("Creating fish..."); +createFood( + [ + [new SimpleObject(oFish, 2,3, 0,2)] + ], + [ + 20 * scaleByMapSize(5, 20) + ], + [avoidClasses(clIsland, 2, clFood, 10, clPath, 1), stayClasses(clWater, 5)], + clFood +); +RMS.SetProgress(75); + +log("Creating huntable animals..."); +createFood( + [ + [new SimpleObject(oSheep, 5,5, 0,4)], + [new SimpleObject(oGoat, 5,5, 0,4)], + [new SimpleObject(oRabbit, 5,8, 0,4)], + [new SimpleObject(oDeer, 4,6, 0,2)], + [new SimpleObject(oHawk, 1,1, 0,4)] + ], + [ + scaleByMapSize(5, 20), + scaleByMapSize(5, 20), + scaleByMapSize(5, 20), + scaleByMapSize(5, 20), + scaleByMapSize(5, 10) + ], + avoidClasses(clIsland, 2, clFood, 10, clWater, 5, clPlayer, 16, clHill, 2, clGauls, 5, clPath, 1), + clFood +); + +log("Creating violent animals..."); +createFood( + [ + [new SimpleObject(oWolf, 1,3, 0,4)], + [new SimpleObject(oBoar, 1,1, 0,4)], + [new SimpleObject(oBear, 1,1, 0,4)] + ], + [ + scaleByMapSize(5, 20), + scaleByMapSize(5, 20), + scaleByMapSize(5, 20) + ], + avoidClasses(clIsland, 2, clFood, 10, clWater, 5, clPlayer, 24, clHill, 2, clGauls, 5, clPath, 1), + clFood +); +RMS.SetProgress(80); + +log("Creating fruits..."); +createFood( + [ + [new SimpleObject(oApple, 3,5, 4,7)], + [new SimpleObject(oBerryBush, 4,6, 0,4)] + ], + [ + scaleByMapSize(5, 20), + scaleByMapSize(5, 20) + ], + avoidClasses(clWater, 5, clForest, 2, clPlayer, 16, clHill, 4, clFood, 10, clMetal, 4, clRock, 4, clGauls, 5, clPath, 1), + clFood +); +RMS.SetProgress(90); + +log("Creating straggler trees..."); +var treeTypes = [oOak, oOak2, oOak3, oOak4, oBeech, oBeech2, oAcacia]; +createStragglerTrees( + treeTypes, + avoidClasses(clForest, 2, clWater, 8, clPlayer, 16, clMetal, 4, clRock, 4, clFood, 1, clHill, 2, clGauls, 5, clPath, 5), clForest); + +log("Creating island straggler trees..."); +g_numStragglerTrees *= 7; +createStragglerTrees(treeTypes, [stayClasses(clIsland, 4), avoidClasses(clMetal, 4, clRock, 4, clTower, 4, clOutpost, 4)], clForest); +RMS.SetProgress(95); + +log("Creating animals on islands..."); +createFood( + [ + [new SimpleObject(oSheep, 4,6, 0,4)], + [new SimpleObject(oGoat, 4,6, 0,4)], + [new SimpleObject(oRabbit, 5,8, 0,4)], + ], + [ + 10 * scaleByMapSize(5, 20), + 10 * scaleByMapSize(5, 20), + 10 * scaleByMapSize(5, 20), + ], + [avoidClasses(clRock, 4, clMetal, 4, clFood, 3, clForest, 1, clOutpost, 2, clTower, 2), stayClasses(clIsland, 4)], + clFood +); +RMS.SetProgress(85); + +log("Creating treasures..."); +for (let i = 0; i < randomTreasureCount; ++i) + createObjectGroups( + new SimpleGroup( + [new SimpleObject(pickRandom(oTreasures), 1,1, 0,2)], + true, clTreasure + ), + 0, + avoidClasses(clForest, 1, clPlayer, 15, clHill, 1, clWater, 5, clFood, 1, clRock, 4, clMetal, 4, clTreasure, 10, clGauls, 5), + 1, + 50 + ); + +log("Creating gallic decoratives..."); +createDecoration( + [ + [new SimpleObject(aBucket, 1,1, 0,1)], + [new SimpleObject(aBarrel, 1,1, 0,1)], + [new SimpleObject(aTartan, 3,3, 4, 4, PI/4, PI/2)], + [new SimpleObject(aWheel, 2,4, 1, 2)], + [new SimpleObject(aWell, 1,1, 0,2)], + [new SimpleObject(aWoodcord, 1,2, 2,2, PI/2, PI/2)] + ], + [ + scaleByMapSize(2, 10), + scaleByMapSize(2, 10), + scaleByMapSize(2, 10), + scaleByMapSize(2, 10), + scaleByMapSize(3, 4), + scaleByMapSize(2, 10) + ], + avoidClasses(clForest, 1, clPlayer, 10, clBaseResource, 5, clHill, 1, clFood, 1, clWater, 5, clRock, 4, clMetal, 4, clGauls, 5, clPath, 1) +); + +log("Creating spawn points for ships..."); +createObjectGroups( + new SimpleGroup([new SimpleObject(triggerPointShipSpawn, 1, 1, 0, 0)], true, clShip), + 0, + [avoidClasses(clShip, 5, clIsland, 4), stayClasses(clWater, 10)], + 10000, + 1000 +); + +log("Creating patrol points for ships..."); +createObjectGroups( + new SimpleGroup([new SimpleObject(triggerPointShipPatrol, 1, 1, 0, 0)], true, clShipPatrol), + 0, + [avoidClasses(clShipPatrol, 5, clIsland, 3), stayClasses(clWater, 4)], + 10000, + 1000 +); + +log("Creating ungarrison points for ships..."); +for (let i = 0; i < 2; ++i) + createObjectGroups( + new SimpleGroup( + [new SimpleObject( + i == 0 ? triggerPointShipUnloadLeft : triggerPointShipUnloadRight, + 1, 1, + 0, 0)], + true, + clShoreUngarrisonPoint[i]), + 0, + [avoidClasses(clShoreUngarrisonPoint[i], 4), stayClasses(clShore[i], 0)], + 20000, + 1 + ); + +log("Creating patrol points for land attackers..."); +for (let i = 0; i < 2; ++i) + createObjectGroups( + new SimpleGroup( + [new SimpleObject( + i == 0 ? triggerPointLandPatrolLeft : triggerPointLandPatrolRight, + 1, 1, + 0, 0)], + true, + clLandPatrolPoint[i]), + 0, + [ + avoidClasses(clWater, 5, clForest, 3, clHill, 3, clFood, 1, clRock, 5, clMetal, 5, clPlayer, 10, clGauls, 5, clLandPatrolPoint[i], 5), + stayClasses(clLand[i], 0) + ], + 10000, + 100 + ); + +log("Creating water logs..."); +createObjectGroups( + new SimpleGroup([new SimpleObject(aWaterLog, 1, 1, 0, 0)], true, clWaterLog), + 0, + [avoidClasses(clShip, 3, clIsland, 4), stayClasses(clWater, 4)], + scaleByMapSize(15, 60), + 100 +); + +if (randIntInclusive(1, 3) > 1) +{ + // Day + setSkySet("cumulus"); + + setSunColor(0.9, 0.8, 0.5); + + setFogFactor(0.05); + setFogThickness(0.25); + + setWaterColor(0.2, 0.3, 0.3); + setWaterTint(0.5, 1, 1); + + setPPContrast(0.62); + setPPSaturation(0.51); + setPPBloom(0.12); +} +else +{ + // Night + setSkySet("dark"); + + setSunColor(0.4, 0.9, 1.2); + setSunElevation(0.13499); + setSunRotation(-2.5); + + setTerrainAmbientColor(0.25, 0.3, 0.45); + setUnitsAmbientColor(0.3, 0.35, 0.5); + + setFogFactor(0.004); + setFogThickness(0.25); + setFogColor(0.35, 0.45, 0.5); + + setWaterColor(0.2, 0.25, 0.5); + setWaterTint(0, 0, 0); + + //setPPBrightness(0); + //setPPContrast(1.09961); + //setPPSaturation(0.99); + //setPPBloom(0.1999); +} + +setPPEffect("hdr"); +setWaterWaviness(2.0); +setWaterType("lake"); +setWaterMurkiness(1); +setWaterHeight(21); + +ExportMap(); Index: binaries/data/mods/public/maps/random/danube.json =================================================================== --- /dev/null +++ binaries/data/mods/public/maps/random/danube.json @@ -0,0 +1,16 @@ +{ + "settings" : { + "Name" : "Danube", + "Script" : "danube.js", + "Description" : "Players start along the banks of the river Danube, in the period following the expansion into Pannonia by the Celtic Boii tribe. Seeking to consolidate their hold on this land, celtic reinforcements have been sent to root out the remaining foreign cultures. Players not only have to vie for power amongst themselves, but also beat back these ruthless invaders. (Ultimately, the Boii were defeated by the rising power of the Dacians, hence leading to the reemergence of the Geto-Dacian Confederation under King Burebista.)", + "BaseTerrain" : ["temp_grass_aut", "temp_grass_plants_aut", "temp_grass_c_aut", "temp_grass_d_aut"], + "BaseHeight" : 4, + "Keywords": ["new", "trigger"], + "CircularMap" : true, + "Preview" : "danube.png", + "TriggerScripts" : [ + "scripts/TriggerHelper.js", + "random/danube_triggers.js" + ] + } +} Index: binaries/data/mods/public/maps/random/danube_triggers.js =================================================================== --- /dev/null +++ binaries/data/mods/public/maps/random/danube_triggers.js @@ -0,0 +1,562 @@ +const showDebugLog = false; + +// Spawn behavior: +// Ships spawn every N-M minutes. +// Ensure that no more than P(t) ships exist at time t. +// Fill ships with R(t) units at time t. + +// Increase champion to citizen champion ratio from 0 to 100% in the first 60min +// Randomize whether infantry cavalry +// Cavalry should focus females, skirmishers + +// Ship behavior: +// if there are no enemy ships around, target the docks +// if there are no enemy ships nor docks, patrol the shoreline between trigger points +// if there are enemy ships, target them + +var shipTemplate = "gaul_ship_trireme"; +var siegeTemplate = "gaul_mechanical_siege_ram"; + +var heroTemplates = [ + "gaul_hero_britomartus", + "gaul_hero_vercingetorix", + "gaul_hero_brennus" +]; + +var femaleTemplate = "gaul_support_female_citizen"; +var healerTemplate = "gaul_support_healer_e"; + +var citizenInfantryTemplates = [ + "gaul_infantry_javelinist_e", + "gaul_infantry_spearman_e", + "gaul_infantry_slinger_e", +]; + +var citizenCavalryTemplates = [ + "gaul_cavalry_javelinist_e", + "gaul_cavalry_swordsman_e", +]; + +var citizenTemplates = [...citizenInfantryTemplates, ...citizenCavalryTemplates]; + +var championInfantryTemplates = [ + "gaul_champion_fanatic", + "gaul_champion_infantry" +]; + +var championCavalryTemplates = [ + "gaul_champion_cavalry" +]; + +var championTemplates = [...championInfantryTemplates, ...championCavalryTemplates]; + +var ccDefenders = [ + { "count": 8, "template": "units/" + pickRandom(citizenInfantryTemplates) }, + { "count": 8, "template": "units/" + pickRandom(championInfantryTemplates) }, + { "count": 4, "template": "units/" + pickRandom(championCavalryTemplates) }, + { "count": 4, "template": "units/" + healerTemplate }, + { "count": 5, "template": "units/" + femaleTemplate }, + { "count": 10, "template": "gaia/fauna_sheep" } +]; + +var gallicBuildingGarrison = [ + { + "buildings": ["House"], + "units": [femaleTemplate, healerTemplate] + }, + { + "buildings": ["CivCentre", "Temple"], + "units": championTemplates + }, + { + "buildings": ["DefenseTower", "Outpost"], + "units": championInfantryTemplates + } +]; + +/** + * Notice if gaia becomes too strong, players will just turtle and try to outlast the players on the other side. + * However we want interaction and fights between the teams. + * This can be accomplished by not wiping out players buildings entirely. + */ + +/** + * Time between two consecutive waves after t minutes game duration. + */ +var shipRespawnTime = () => randFloat(2, 4); + +/** + * Limit of ships on the map when spawning them. + */ +var shipCount = (t, numPlayers) => Math.round(Math.min(3, t / 5) * numPlayers); + +/** + * Order all ships to ungarrison at the shoreline + */ +var shipUngarrisonInterval = () => randFloat(3, 5); + +/** + * Total count of gaia attackers per shipload. + */ +var attackersPerShip = t => Math.round(Math.min(30, 10 + t * 2 / 3)); + +/** + * Likelihood of adding a non-existing hero at that time. + */ +var heroProbability = t => Math.max(0, Math.min(1, (t - 25) / 60)); + +/** + * Percent of healers to add per shipload after potentially adding a hero. + */ +var healerRatio = t => randFloat(0, 0.1); + +/** + * Percent of siege engines to add per shipload after adding heroes and healers. + */ +var siegeRatio = t => randFloat(0, 0.08); + +/** + * Percent of champions to be added after spawning heroes, healers and siege engines. + * Rest will be citizen soldiers. + */ +var championRatio = t => Math.min(1, Math.max(0, (t - 25) / 75)); + +/** + * Ships and land units will queue attack orders for this amount of closest ships. + */ +var targetCount = 3; + +/** + * Number of trigger points to patrol when not having enemies to attack. + */ +var patrolCount = 5; + +/** + * Which units ships should attack when patroling. + */ +var shipTargetClass = "WarShip"; + +/** + * Which units ships should attack when patroling. + */ +var siegeTargetClass = "Defensive"; + +/** + * Which units ships should attack when patroling. + */ +var unitTargetClass = "Unit-Ship"; + +/** + * Ungarrison ships when being in this range of the target. + */ +var shipUngarrisonDistance = 50; + +/** + * Chance for the units at the meeting place to participate in the ritual. + */ +var ritualProbability = 0.75; + +/** + * Units celebrating at the meeting place will perform one of these animations + * if idle and switch back when becoming idle again. + */ +var ritualAnimations = { + "female": ["attack_slaughter"], + "male": ["attack_capture", "promotion", "attack_slaughter"], + "healer": ["attack_capture", "promotion", "heal"] +}; + +var triggerPointShipSpawn = "A"; +var triggerPointUngarrisonLeft = "B"; +var triggerPointUngarrisonRight = "C"; +var triggerPointLandPatrolLeft = "D"; +var triggerPointLandPatrolRight = "E"; +var triggerPointShipPatrol = "F"; + +Trigger.prototype.debugLog = function(txt) +{ + if (showDebugLog) + print( + "[" + + Math.round(Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime() / 60 / 1000) + "] " + txt + "\n"); +}; + +Trigger.prototype.GarrisonAllGallicBuildings = function(gaiaEnts) +{ + this.debugLog("Garrisoning all gallic buildings"); + + for (let buildingGarrison of gallicBuildingGarrison) + for (let building of buildingGarrison.buildings) + this.SpawnAndGarrisonBuilding(gaiaEnts, building, pickRandom(buildingGarrison.units)); +}; + +/** + * Garrisons all targetEnts that match the targetClass with newly spawned entities of the given template. + */ +Trigger.prototype.SpawnAndGarrisonBuilding = function(targetEntities, targetClass, template) +{ + for (let gaiaEnt of targetEntities) + { + let cmpIdentity = Engine.QueryInterface(gaiaEnt, IID_Identity); + if (!cmpIdentity || !cmpIdentity.HasClass(targetClass)) + continue; + + let cmpGarrisonHolder = Engine.QueryInterface(gaiaEnt, IID_GarrisonHolder); + + let newEnts = TriggerHelper.SpawnUnits( + gaiaEnt, + "units/" + template, + cmpGarrisonHolder.GetCapacity() - cmpGarrisonHolder.GetEntities(), + 0); + + this.debugLog("Garrisoning " + newEnts.length + " " + template + " at " + targetClass); + + for (let newEnt of newEnts) + Engine.QueryInterface(gaiaEnt, IID_GarrisonHolder).Garrison(newEnt); + } +}; + +/** + * Spawn units of the template at each gaia Civic Center and set them to defensive. + */ +Trigger.prototype.SpawnCCDefenders = function(gaiaEnts) +{ + this.debugLog("To defend CCs, spawning " + uneval(ccDefenders)); + + for (let gaiaEnt of gaiaEnts) + { + let cmpIdentity = Engine.QueryInterface(gaiaEnt, IID_Identity); + if (!cmpIdentity || !cmpIdentity.HasClass("CivCentre")) + continue; + + for (let ccDefender of ccDefenders) + { + for (let ent of TriggerHelper.SpawnUnits(gaiaEnt, ccDefender.template, ccDefender.count, 0)) + { + let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); + if (cmpUnitAI) + cmpUnitAI.SwitchToStance("defensive"); + } + } + } +}; + +/** + * Remember most Humans present at the beginning of the match (before spawning any unit) and + * make them defensive. + */ +Trigger.prototype.StartCelticRitual = function(gaiaEnts) +{ + this.ritualEnts = []; + + for (let ent of gaiaEnts) + { + let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); + if (!cmpIdentity || !cmpIdentity.HasClass("Human")) + continue; + + if (randFloat(0, 1) < ritualProbability) + this.ritualEnts.push(ent); + + let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); + if (cmpUnitAI) + cmpUnitAI.SwitchToStance("defensive"); + } + + this.UpdateCelticRitual(); +}; + +/** + * Play one of the given animations for most participants if and only if they are idle. + */ +Trigger.prototype.UpdateCelticRitual = function() +{ + for (let ent of this.ritualEnts) + { + let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); + if (!cmpUnitAI || cmpUnitAI.GetCurrentState() != "INDIVIDUAL.IDLE") + continue; + + let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); + + let animations = ritualAnimations[ + cmpIdentity.HasClass("Healer") ? "healer" : + cmpIdentity.HasClass("Female") ? "female" : "male"]; + + let cmpVisual = Engine.QueryInterface(ent, IID_Visual); + if (!cmpVisual || animations.indexOf(cmpVisual.GetAnimationName()) != -1) + continue; + + cmpUnitAI.SelectAnimation(pickRandom(animations)); + } + + this.DoAfterDelay(5 * 1000, "UpdateCelticRitual", {}); +}; + +Trigger.prototype.CheckShipSunk = function(data) +{ + if (this.ships.indexOf(data.entity) != -1 && data.to == -1) + { + this.debugLog("Ship " + data.entity + " sunk"); + this.ships.splice(this.ships.indexOf(data.entity), 1); + } +}; + +/** + * Spawn ships with a unique attacker composition each until + * the number of ships is reached that is supposed to exist at the given time. + */ +Trigger.prototype.SpawnAndGarrisonShips = function() +{ + let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime() / 60 / 1000; + let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers(); + + let shipSpawnCount = shipCount(time, numPlayers) - this.ships.length; + this.debugLog("Spawning " + shipSpawnCount + " ships"); + + while (this.ships.length < shipSpawnCount) + this.ships.push(TriggerHelper.SpawnUnits(pickRandom(this.GetTriggerPoints(triggerPointShipSpawn)), "units/" + shipTemplate, 1, 0)[0]); + + this.FillShips(); + + this.AttackAndPatrol(this.ships, shipTargetClass, triggerPointShipPatrol, "Ships"); + + this.DoAfterDelay(shipRespawnTime() * 60 * 1000, "SpawnAndGarrisonShips", {}); +}; + +/** + * Spawns attacker units + */ +Trigger.prototype.FillShips = function() +{ + let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime() / 60 / 1000; + let attackerCount = attackersPerShip(time); + + for (let ship of this.ships) + { + let cmpGarrisonHolder = Engine.QueryInterface(ship, IID_GarrisonHolder); + + let remainder = Math.max(0, attackerCount - cmpGarrisonHolder.GetEntities().length); + + let toSpawn = []; + + let heroTemplate = pickRandom(heroTemplates.filter(hTemp => this.heroes.every(hero => hTemp != hero.template))); + if (heroTemplate && remainder && randFloat(0, 1) < heroProbability(time)) + { + toSpawn.push({ "template": heroTemplate, "count": 1, "hero": true }); + --remainder; + } + + let healerCount = Math.round(healerRatio(time) * remainder); + if (healerCount) + toSpawn.push({ "template": healerTemplate, "count": healerCount }); + remainder -= healerCount; + + let siegeCount = Math.round(siegeRatio(time) * remainder); + if (siegeCount) + toSpawn.push({ "template": siegeTemplate, "count": siegeCount }); + remainder -= siegeCount; + + let championCount = Math.round(championRatio(time) * remainder); + for (let i in championTemplates) + { + let count = +i == championTemplates.length - 1 ? championCount : randIntInclusive(0, championCount); + if (count) + toSpawn.push({ "template": championTemplates[i], "count": count }); + championCount -= count; + remainder -= count; + } + + for (let i in citizenTemplates) + { + let count = +i == citizenTemplates.length - 1 ? remainder : randIntInclusive(0, remainder); + if (count) + toSpawn.push({ "template": citizenTemplates[i], "count": count }); + remainder -= count; + } + + this.debugLog("Filling ship " + ship + " with " + uneval(toSpawn)); + + if (remainder != 0) + warn("Didn't spawn as many attackers as were intended (" + remainder + " remaining)"); + + let units = []; + for (let spawn of toSpawn) + { + let ents = TriggerHelper.SpawnUnits(ship, "units/" + spawn.template, spawn.count, 0); + units = units.concat(ents); + + if (spawn.hero) + this.heroes.push({ "template": spawn.template, "ent": ents[0] }); + } + + for (let unit of units) + Engine.QueryInterface(ship, IID_GarrisonHolder).Garrison(unit); + } +}; + +/** + * Attack the closest enemy ships around, then patrol the sea. + */ +Trigger.prototype.AttackAndPatrol = function(attackers, targetClass, triggerPointRef, debugName) +{ + let allTargets = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities().filter(ent => { + + let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); + if (!cmpIdentity || !MatchesClassList(cmpIdentity.GetClassesList(), targetClass)) + return false; + }); + + for (let attacker of attackers) + { + let targets = allTargets.sort((ent1, ent2) => + DistanceBetweenEntities(attacker, ent1) - DistanceBetweenEntities(attacker, ent2)).slice(0, targetCount); + + this.debugLog(debugName + " " + attacker + " attacks " + uneval(targets)); + + let cmpUnitAI = Engine.QueryInterface(attacker, IID_UnitAI); + + for (let target of targets) + cmpUnitAI.Attack(target, true, false); + + let patrolTargets = shuffleArray(this.GetTriggerPoints(triggerPointRef)).slice(0, patrolCount); + this.debugLog(debugName + " " + attacker + " patrols to " + uneval(patrolTargets)); + + for (let patrolTarget of patrolTargets) + { + let pos = Engine.QueryInterface(patrolTarget, IID_Position).GetPosition2D(); + cmpUnitAI.Patrol(pos.x, pos.y, targetClass, true); + } + } +}; + +/** + * Order all ships to abort naval warfare and move to the shoreline all few minutes. + */ +Trigger.prototype.UngarrisonShipsOrder = function() +{ + // To avoid unloading unlimited amounts of units on empty islands, + // only ungarrison on riversides where player buildings exist + + let ungarrisonLeft = false; + let ungarrisonRight = false; + + let mapSize = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain).GetTilesPerSide() * 4; + + for (let ent of Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities()) + { + let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); + if (!cmpIdentity || !cmpIdentity.HasClass("Structure")) + continue; + + if (Engine.QueryInterface(ent, IID_Position).GetPosition2D().x < mapSize / 2) + ungarrisonLeft = true; + else + ungarrisonRight = true; + + if (ungarrisonLeft && ungarrisonRight) + break; + } + + if (!ungarrisonLeft && !ungarrisonRight) + return; + + // Determine which ships should ungarrison on which side of the river + let shipsLeft = []; + let shipsRight = []; + + if (ungarrisonLeft && ungarrisonRight) + { + shipsLeft = shuffleArray(this.ships).slice(0, Math.round(this.ships.length / 2)); + shipsRight = this.ships.filter(ship => shipsLeft.indexOf(ship) == -1); + } + else if (ungarrisonLeft) + shipsLeft = this.ships; + else if (ungarrisonRight) + shipsRight = this.ships; + + // Determine which ships should ungarrison and patrol at which trigger point names + let sides = []; + if (shipsLeft) + sides.push({ + "ships": shipsLeft, + "ungarrisonPointRef": triggerPointUngarrisonLeft, + "landPointRef": triggerPointLandPatrolLeft + }); + + if (shipsRight) + sides.push({ + "ships": shipsRight, + "ungarrisonPointRef": triggerPointUngarrisonRight, + "landPointRef": triggerPointLandPatrolRight + }); + + // Order those ships to move to a randomly chosen trigger point on the determined side of the river, + // remember that chosen ungarrison point and the name of the trigger points where the ungarrisoned units should patrol then + for (let side of sides) + for (let ship of side.ships) + { + let ungarrisonPoint = pickRandom(this.GetTriggerPoints(side.ungarrisonPointRef)); + let ungarrisonPos = Engine.QueryInterface(ungarrisonPoint, IID_Position).GetPosition2D(); + + this.debugLog("Ship " + ship + " will ungarrison at " + side.ungarrisonPointRef + + " (" + ungarrisonPos.x + "," + ungarrisonPos.y + ")"); + + Engine.QueryInterface(ship, IID_UnitAI).Walk(ungarrisonPos.x, ungarrisonPos.y, false); + this.shipTarget[ship] = { "landPointRef": side.landPointRef, "ungarrisonPoint": ungarrisonPoint }; + } + + this.DoAfterDelay(shipUngarrisonInterval() * 60 * 1000, "UngarrisonShipsOrder", {}); +}; + +/** + * Check frequently whether the ships are close enough to unload at the shoreline or + * whether they are stuck at the shoreline trying to attack unreachable enemies. + */ +Trigger.prototype.CheckShipRange = function() +{ + for (let ship of this.ships) + { + if (!this.shipTarget[ship] || DistanceBetweenEntities(ship, this.shipTarget[ship].ungarrisonPoint) > shipUngarrisonDistance) + continue; + + let cmpGarrisonHolder = Engine.QueryInterface(ship, IID_GarrisonHolder); + + let attackers = cmpGarrisonHolder.GetEntities() + let siegeEngines = attackers.filter(ent => Engine.QueryInterface(ent, IID_Identity).HasClass("Siege")); + let others = attackers.filter(ent => siegeEngines.indexOf(ent) == -1); + + this.debugLog("Ungarrisoning ship " + ship + " at " + uneval(this.shipTarget[ship])); + cmpGarrisonHolder.UnloadAll(); + + this.AttackAndPatrol(siegeEngines, siegeTargetClass, this.shipTarget[ship].landPointRef, "Siege"); + this.AttackAndPatrol(others, unitTargetClass, this.shipTarget[ship].landPointRef, "Units"); + + delete this.shipTarget[ship]; + } + + this.DoAfterDelay(5 * 1000, "CheckShipRange", {}); +}; + + +{ + let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); + + let gaiaEnts = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEntitiesByPlayer(0); + cmpTrigger.StartCelticRitual(gaiaEnts); + cmpTrigger.GarrisonAllGallicBuildings(gaiaEnts); + cmpTrigger.SpawnCCDefenders(gaiaEnts); + + // Entity IDs of all gaia ships and heroes that currently exist on the map + cmpTrigger.ships = []; + cmpTrigger.heroes = []; + + // Maps from gaia ship entity ID to ungarrison trigger point entity ID and land patrol triggerpoint name + cmpTrigger.shipTarget = {}; + + cmpTrigger.UngarrisonShipsOrder(); + cmpTrigger.CheckShipRange(); + cmpTrigger.SpawnAndGarrisonShips(); + + cmpTrigger.RegisterTrigger("OnOwnershipChanged", "CheckShipSunk", { "enabled": true }); +}