Index: ps/trunk/binaries/data/mods/public/maps/random/danubius.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/danubius.js +++ ps/trunk/binaries/data/mods/public/maps/random/danubius.js @@ -0,0 +1,849 @@ +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_B"; +const triggerPointShipUnloadLeft = "special/trigger_point_C"; +const triggerPointShipUnloadRight = "special/trigger_point_D"; +const triggerPointLandPatrolLeft = "special/trigger_point_E"; +const triggerPointLandPatrolRight = "special/trigger_point_F"; + +// 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 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_b"; +const oSkirmisher = "units/gaul_infantry_javelinist_b"; +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 aCampfire = "actor|props/special/eyecandy/campfire"; +const aBench = "actor|props/special/eyecandy/bench_1"; +const aRug = "actor|props/special/eyecandy/rug_stand_iber"; + +const treeTypes = [oOak, oOak2, oOak3, oOak4, oBeech, oBeech2, oAcacia]; + +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 +]; + +const smallMapSize = 192; +const mediumMapSize = 256; +const normalMapSize = 320; + +// 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 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 = randIntInclusive(8, 12); + +// 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 >= smallMapSize) +{ + log("Creating gallic villages..."); + let gaulCityRadius = 12; + let gaulCityBorderDistance = mapSize < mediumMapSize ? 10 : 18; + + // Whether to add a celtic ritual and a path from the gallic city leading to it + let addCelticRitual = randBool(0.9); + + // 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 + + createArea( + placer, + [ + new LayeredPainter([tShore, tRoad, tRoad], [1, 3]), + new SmoothElevationPainter(ELEVATION_SET, 5, 4), + 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 femaleRadius = mRadius * 0.3; + let maleRadius = mRadius * 0.4; + let benchRadius = mRadius * 0.5; + let rugRadius = mRadius * 0.6; + let goatRadius = mRadius * 0.8; + + wallStyles.celt_ritual = { + "female": new WallElement("female", oFemale, PI, femaleRadius, 0, 2 * PI / femaleCount), + "skirmisher": new WallElement("skirmisher", oSkirmisher, PI, maleRadius, 0, 2 * PI / maleCount), + "healer": new WallElement("healer", oHealer, PI, maleRadius, 0, 2 * PI / maleCount), + "fanatic": new WallElement("fanatic", oNakedFanatic, PI, maleRadius, 0, 2 * PI / maleCount), + "bench": new WallElement("bench", aBench, PI/2, benchRadius, 0, 2 * PI / benchCount), + "rug": new WallElement("rug", aRug, 0, rugRadius, 0, 2 * PI / rugCount), + "goat": new WallElement("goat", oGoat, PI, goatRadius, 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 >= normalMapSize ? 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 (let i = 0; i < numPlayers; ++i) + playerIDs.push(i + 1); +playerIDs = primeSortPlayers(sortPlayers(playerIDs)); + +// Place players +var playerX = []; +var playerZ = []; +for (let i = 0; i < numPlayers; ++i) +{ + let 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 (let i = 0; i < numPlayers; ++i) +{ + let id = playerIDs[i]; + log("Creating base for player " + id + "..."); + + let radius = scaleByMapSize(15, 25); + + let fx = fractionToTiles(playerX[i]); + let fz = fractionToTiles(playerZ[i]); + let ix = Math.floor(fx); + let iz = Math.floor(fz); + addToClass(ix, iz, clPlayer); + + // Create the city patch + let cityRadius = radius / 3; + createArea( + new ClumpPlacer(PI * cityRadius * cityRadius, 0.6, 0.3, 10, ix, iz), + new LayeredPainter([tShore, tRoad], [1]), + null); + + placeCivDefaultEntities(fx, fz, id, { 'iberWall': false }); + + placeDefaultChicken(fx, fz, clBaseResource); + + // Create berry bushes + let angle = randFloat(0, 2 * PI); + let dist = 10; + createObjectGroup( + new SimpleGroup( + [new SimpleObject(oBerryBush, 5, 5, 0, 3)], + true, + clBaseResource, + Math.round(fx + dist * Math.cos(angle)), + Math.round(fz + dist * Math.sin(angle)) + ), + 0); + + // Create metal mine + dist = scaleByMapSize(9, 14); + angle += randFloat(PI/4, PI/3); + createObjectGroup( + new SimpleGroup( + [new SimpleObject(oMetalLarge, 1, 1, 0, 0)], + true, + clBaseResource, + Math.round(fx + dist * Math.cos(angle)), + Math.round(fz + dist * Math.sin(angle)) + ), + 0); + + // Create stone mines + angle += randFloat(PI/3, PI/2); + createObjectGroup( + new SimpleGroup( + [new SimpleObject(oStoneLarge, 1, 1, 0, 2)], + true, + clBaseResource, + Math.round(fx + dist * Math.cos(angle)), + Math.round(fz + dist * Math.sin(angle)) + ), + 0); + + // Create starting trees + let num = 20; + angle += randFloat(-PI/3, PI * 4/3); + dist = randFloat(10, 14); + createObjectGroup( + new SimpleGroup( + [new SimpleObject(oOak, num, num, 0, 5)], + false, + clBaseResource, + Math.round(fx + dist * Math.cos(angle)), + Math.round(fz + dist * Math.sin(angle)) + ), + 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); + let z = iz / (mapSize + 1); + + // 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 = -3; + 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); + + 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 (randBool()) + 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, 2, 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, clPlayer, 10, clWater, 2, clDirt, 2, clHill, 1, clGauls, 5, clPath, 1), + scaleByMapSize(15, 45), + clDirt +); +RMS.SetProgress(55); + +log("Creating islands..."); +createAreas( + new ChainPlacer(Math.floor(scaleByMapSize(3, 4)), Math.floor(scaleByMapSize(4, 8)), Math.floor(scaleByMapSize(50, 80)), 0.5), + [ + new LayeredPainter([tWater, tShore, tIsland], [2, 1]), + new SmoothElevationPainter(ELEVATION_SET, 6, 4), + 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(65); + +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(70); + +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(75); + +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(80); + +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(85); + +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..."); +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(98); + +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 (randBool(2/3)) +{ + // Day + setSkySet("cumulus"); + + setSunColor(0.9, 0.8, 0.5); + + setFogFactor(0.05); + setFogThickness(0.25); + + setWaterColor(0.317, 0.396, 0.294); + setWaterTint(0.439, 0.403, 0.262); + + 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.074, 0.101, 0.090); + setWaterTint(0.129, 0.160, 0.137); +} + +setPPEffect("hdr"); +setWaterWaviness(2.0); +setWaterType("lake"); +setWaterMurkiness(0.97); +setWaterHeight(21); + +ExportMap(); Index: ps/trunk/binaries/data/mods/public/maps/random/danubius.json =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/danubius.json +++ ps/trunk/binaries/data/mods/public/maps/random/danubius.json @@ -0,0 +1,16 @@ +{ + "settings" : { + "Name" : "Danubius", + "Script" : "danubius.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" : "danubius.png", + "TriggerScripts" : [ + "scripts/TriggerHelper.js", + "random/danubius_triggers.js" + ] + } +} Index: ps/trunk/binaries/data/mods/public/maps/random/danubius_triggers.js =================================================================== --- ps/trunk/binaries/data/mods/public/maps/random/danubius_triggers.js +++ ps/trunk/binaries/data/mods/public/maps/random/danubius_triggers.js @@ -0,0 +1,654 @@ +// Ships respawn every few minutes, attack the closest warships, then patrol the sea. +// To prevent unlimited spawning of ships, no more than the amount of ships intended at a given time are spawned. + +// Ships are filled or refilled with new units. +// The number ships, number of units per ship, as well as ratio of siege engines, champion and heroes +// increases with time, while keeping an individual and randomized composition for each ship. +// Each hero exists at most once per map. + +// Every few minutes, equal amount of ships unload units at the sides of the river unless +// one side of the river was wiped from players. +// Siege engines attack defensive structures, units attack units then patrol that side of the river. + +const showDebugLog = false; + +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_b"; + +var citizenInfantryTemplates = [ + "gaul_infantry_javelinist_b", + "gaul_infantry_spearman_b", + "gaul_infantry_slinger_b" +]; + +var citizenCavalryTemplates = [ + "gaul_cavalry_javelinist_b", + "gaul_cavalry_swordsman_b" +]; + +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. + */ +var shipRespawnTime = () => randFloat(8, 10); + +/** + * Limit of ships on the map when spawning them. + * Have at least two ships, so that both sides will be visited. + */ +var shipCount = (t, numPlayers) => Math.max(2, Math.round(Math.min(1.5, t / 10) * numPlayers)); + +/** + * Order all ships to ungarrison at the shoreline. + */ +var shipUngarrisonInterval = () => randFloat(5, 7); + +/** + * Time between refillings of all ships with new soldiers. + */ +var shipFillInterval = () => randFloat(4, 5); + +/** + * Total count of gaia attackers per shipload. + */ +var attackersPerShip = t => Math.min(30, Math.round(t * 2)); + +/** + * Likelihood of adding a non-existing hero at t minutes. + */ +var heroProbability = t => Math.max(0, Math.min(1, (t - 25) / 60)); + +/** + * Percent of healers to add per shipload after potentially adding a hero and siege engines. + */ +var healerRatio = t => randFloat(0, 0.1); + +/** + * Percent of siege engines to add per shipload. + */ +var siegeRatio = t => t < 8 ? 0 : randFloat(0.03, 0.06); + +/** + * 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 units. + */ +var targetCount = 3; + +/** + * Number of trigger points to patrol when not having enemies to attack. + */ +var patrolCount = 5; + +/** + * Which units ships should focus when attacking and patroling. + */ +var shipTargetClass = "WarShip"; + +/** + * Which entities siege engines should focus when attacking and patroling. + */ +var siegeTargetClass = "Defensive"; + +/** + * Which entities units should focus when attacking and patroling. + */ +var unitTargetClass = "Unit -Ship"; + +/** + * Ungarrison ships when being in this range of the target. + */ +var shipUngarrisonDistance = 50; + +/** + * Currently formations are not working properly and enemies in vision range are often ignored. + * So only have a small chance of using formations. + */ +var formationProbability = 0.2; + +var unitFormations = [ + "box", + "battle_line", + "line_closed", + "column_closed" +]; + +/** + * 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 triggerPointShipPatrol = "B"; +var triggerPointUngarrisonLeft = "C"; +var triggerPointUngarrisonRight = "D"; +var triggerPointLandPatrolLeft = "E"; +var triggerPointLandPatrolRight = "F"; + +/** + * Which playerID to use for the opposing gallic reinforcements. + */ +var gaulPlayer = 0; + +Trigger.prototype.debugLog = function(txt) +{ + if (showDebugLog) + print( + "DEBUG [" + + Math.round(Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime() / 60 / 1000) + "] " + txt + "\n"); +}; + +/** + * Return a random amount of these templates whose sum is count. + */ +Trigger.prototype.RandomAttackerTemplates = function(templates, count) +{ + let ratios = new Array(templates.length).fill(1).map(i => randFloat(0, 1)); + let ratioSum = ratios.reduce((current, sum) => current + sum, 0); + + let remainder = count; + let templateCounts = {}; + + for (let i in templates) + { + let currentCount = +i == templates.length - 1 ? remainder : Math.round(ratios[i] / ratioSum * count); + if (!currentCount) + continue; + + templateCounts[templates[i]] = currentCount; + remainder -= currentCount; + } + + if (remainder != 0) + warn("Not as many templates as expected: " + count + " vs " + uneval(templateCounts)); + + return templateCounts; +}; + +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, buildingGarrison.units); +}; + +/** + * Garrisons all targetEnts that match the targetClass with newly spawned entities of the given template. + */ +Trigger.prototype.SpawnAndGarrisonBuilding = function(gaiaEnts, targetClass, templates) +{ + for (let gaiaEnt of gaiaEnts) + { + let cmpIdentity = Engine.QueryInterface(gaiaEnt, IID_Identity); + if (!cmpIdentity || !cmpIdentity.HasClass(targetClass)) + continue; + + let cmpGarrisonHolder = Engine.QueryInterface(gaiaEnt, IID_GarrisonHolder); + if (!cmpGarrisonHolder) + continue; + + let unitCounts = this.RandomAttackerTemplates(templates, cmpGarrisonHolder.GetCapacity()); + this.debugLog("Garrisoning " + uneval(unitCounts) + " at " + targetClass); + + for (let template in unitCounts) + for (let newEnt of TriggerHelper.SpawnUnits(gaiaEnt, "units/" + template, unitCounts[template], gaulPlayer)) + 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, gaulPlayer)) + Engine.QueryInterface(ent, IID_UnitAI).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) +{ + for (let ent of gaiaEnts) + { + let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); + if (!cmpIdentity || !cmpIdentity.HasClass("Human")) + continue; + + if (randBool(ritualProbability)) + this.ritualEnts.push(ent); + + let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); + if (cmpUnitAI) + cmpUnitAI.SwitchToStance("defensive"); + } + + this.DoRepeatedly(5 * 1000, "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); + if (!cmpIdentity) + continue; + + let animations = ritualAnimations[ + cmpIdentity.HasClass("Healer") ? "healer" : + cmpIdentity.HasClass("Female") ? "female" : "male"]; + + let cmpVisual = Engine.QueryInterface(ent, IID_Visual); + if (!cmpVisual) + continue; + + if (animations.indexOf(cmpVisual.GetAnimationName()) == -1) + cmpVisual.SelectAnimation(pickRandom(animations), false, 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.SpawnShips = 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, gaulPlayer)[0]); + + for (let ship of this.ships) + this.AttackAndPatrol([ship], shipTargetClass, triggerPointShipPatrol, "Ships"); + + this.DoAfterDelay(shipRespawnTime() * 60 * 1000, "SpawnShips", {}); + + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + cmpTimer.CancelTimer(this.fillShipsTimer); + + this.FillShips(); +}; + +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); + if (!cmpGarrisonHolder) + continue; + + let remainder = Math.max(0, attackerCount - cmpGarrisonHolder.GetEntities().length); + + let toSpawn = []; + + let siegeCount = Math.round(siegeRatio(time) * remainder); + if (siegeCount) + toSpawn.push({ "template": siegeTemplate, "count": siegeCount }); + remainder -= siegeCount; + + let heroTemplate = pickRandom(heroTemplates.filter(hTemp => this.heroes.every(hero => hTemp != hero.template))); + if (heroTemplate && remainder && randBool(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 championCount = Math.round(championRatio(time) * remainder); + let championTemplateCounts = this.RandomAttackerTemplates(championTemplates, championCount); + for (let template in championTemplateCounts) + { + let count = championTemplateCounts[template]; + toSpawn.push({ "template": template, "count": count }); + championCount -= count; + remainder -= count; + } + + let citizenTemplateCounts = this.RandomAttackerTemplates(citizenTemplates, remainder); + for (let template in citizenTemplateCounts) + { + let count = citizenTemplateCounts[template]; + toSpawn.push({ "template": template, "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)"); + + for (let spawn of toSpawn) + { + // Don't use TriggerHelper.SpawnUnits here because that is too slow, + // needlessly trying all spawn points near the ships footprint which all fail + + for (let i = 0; i < spawn.count; ++i) + { + let ent = Engine.AddEntity("units/" + spawn.template); + Engine.QueryInterface(ent, IID_Ownership).SetOwner(gaulPlayer); + + if (spawn.hero) + this.heroes.push({ "template": spawn.template, "ent": ent }); + + cmpGarrisonHolder.Garrison(ent); + } + } + } + + this.fillShipsTimer = this.DoAfterDelay(shipFillInterval() * 60 * 1000, "FillShips", {}); +}; + +/** + * Attack the closest enemy ships around, then patrol the sea. + */ +Trigger.prototype.AttackAndPatrol = function(attackers, targetClass, triggerPointRef, debugName) +{ + if (!attackers.length) + return; + + let allTargets = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities().filter(ent => { + let cmpIdentity = Engine.QueryInterface(ent, IID_Identity); + return cmpIdentity && MatchesClassList(cmpIdentity.GetClassesList(), targetClass); + }); + + let targets = allTargets.sort((ent1, ent2) => + DistanceBetweenEntities(attackers[0], ent1) - DistanceBetweenEntities(attackers[0], ent2)).slice(0, targetCount); + + this.debugLog(debugName + " " + uneval(attackers) + " attack " + uneval(targets)); + + ProcessCommand(gaulPlayer, { + "type": "stance", + "entities": attackers, + "name": "violent", + "queued": true + }); + + for (let target of targets) + ProcessCommand(gaulPlayer, { + "type": "attack", + "entities": attackers, + "target": target, + "queued": true, + "allowCapture": false + }); + + let patrolTargets = shuffleArray(this.GetTriggerPoints(triggerPointRef)).slice(0, patrolCount); + this.debugLog(debugName + " " + uneval(attackers) + " patrol to " + uneval(patrolTargets)); + + for (let patrolTarget of patrolTargets) + { + let pos = Engine.QueryInterface(patrolTarget, IID_Position).GetPosition2D(); + ProcessCommand(gaulPlayer, { + "type": "patrol", + "entities": attackers, + "x": pos.x, + "z": pos.y, + "targetClasses": { + "attack": [targetClass] + }, + "queued": true, + "allowCapture": false + }); + } +}; + +/** + * 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 riversides, + // 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.length) + sides.push({ + "ships": shipsLeft, + "ungarrisonPointRef": triggerPointUngarrisonLeft, + "landPointRef": triggerPointLandPatrolLeft + }); + + if (shipsRight.length) + 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 afterwards. + 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. + */ +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); + if (!cmpGarrisonHolder) + continue; + + 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(); + + if (randBool(formationProbability)) + ProcessCommand(gaulPlayer, { + "type": "formation", + "entities": others, + "name": "formations/" + pickRandom(unitFormations) + }); + + this.AttackAndPatrol(siegeEngines, siegeTargetClass, this.shipTarget[ship].landPointRef, "Siege"); + this.AttackAndPatrol(others, unitTargetClass, this.shipTarget[ship].landPointRef, "Units"); + delete this.shipTarget[ship]; + + this.AttackAndPatrol([ship], shipTargetClass, triggerPointShipPatrol, "Ships"); + } +}; + +Trigger.prototype.DanubiusOwnershipChange = function(data) +{ + if (data.to != -1) + return; + + let shipIdx = this.ships.indexOf(data.entity); + if (shipIdx != -1) + { + this.debugLog("Ship " + data.entity + " sunk"); + this.ships.splice(shipIdx, 1); + } + + let ritualIdx = this.ritualEnts.indexOf(data.entity); + if (ritualIdx != -1) + this.ritualEnts.splice(ritualIdx, 1); + + let heroIdx = this.heroes.findIndex(hero => hero.ent == data.entity); + if (ritualIdx != -1) + this.heroes.splice(heroIdx, 1); +}; + + +{ + let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); + + let gaiaEnts = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEntitiesByPlayer(0); + + cmpTrigger.ritualEnts = []; + + // To prevent spawning more than the limits, track IDs of current entities + cmpTrigger.ships = []; + cmpTrigger.heroes = []; + + // Maps from gaia ship entity ID to ungarrison trigger point entity ID and land patrol triggerpoint name + cmpTrigger.shipTarget = {}; + cmpTrigger.fillShipsTimer = undefined; + + cmpTrigger.StartCelticRitual(gaiaEnts); + cmpTrigger.GarrisonAllGallicBuildings(gaiaEnts); + cmpTrigger.SpawnCCDefenders(gaiaEnts); + + cmpTrigger.SpawnShips(); + cmpTrigger.DoAfterDelay(shipUngarrisonInterval() * 60 * 1000, "UngarrisonShipsOrder", {}); + cmpTrigger.DoRepeatedly(5 * 1000, "CheckShipRange", {}); + + cmpTrigger.RegisterTrigger("OnOwnershipChanged", "DanubiusOwnershipChange", { "enabled": true }); +} Index: ps/trunk/binaries/data/mods/public/simulation/components/Trigger.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/Trigger.js +++ ps/trunk/binaries/data/mods/public/simulation/components/Trigger.js @@ -280,15 +280,34 @@ /** * Execute a function after a certain delay. * - * @param {Number} time - delay in milleseconds - * @param {String} action - Name of the action function - * @param {Object} data - will be passed to the action function + * @param {Number} time - Delay in milliseconds. + * @param {String} action - Name of the action function. + * @param {Object} data - Arbitrary object that will be passed to the action function. + * @return {Number} The ID of the timer, so it can be stopped later. */ -Trigger.prototype.DoAfterDelay = function(miliseconds, action, data) +Trigger.prototype.DoAfterDelay = function(time, action, data) { let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + return cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_Trigger, "DoAction", time, { + "action": action, + "data": data + }); +}; - return cmpTimer.SetTimeout(SYSTEM_ENTITY, IID_Trigger, "DoAction", miliseconds, { +/** + * Execute a function each time a certain delay has passed. + * + * @param {Number} interval - Interval in milleseconds between consecutive calls. + * @param {String} action - Name of the action function. + * @param {Object} data - Arbitrary object that will be passed to the action function. + * @param {Number} [start] - Optional initial delay in milleseconds before starting the calls. + * If not given, interval will be used. + * @return {Number} the ID of the timer, so it can be stopped later. + */ +Trigger.prototype.DoRepeatedly = function(time, action, data, start) +{ + let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); + return cmpTimer.SetInterval(SYSTEM_ENTITY, IID_Trigger, "DoAction", start !== undefined ? start : time, time, { "action": action, "data": data });