Index: binaries/data/mods/public/maps/random/survivalofthefittest.js =================================================================== --- binaries/data/mods/public/maps/random/survivalofthefittest.js +++ binaries/data/mods/public/maps/random/survivalofthefittest.js @@ -1,23 +1,16 @@ RMS.LoadLibrary("rmgen"); -//random terrain textures var random_terrain = randomizeBiome(); const tMainTerrain = rBiomeT1(); const tForestFloor1 = rBiomeT2(); const tForestFloor2 = rBiomeT3(); const tCliff = rBiomeT4(); +const tHill = rBiomeT8(); const tTier1Terrain = rBiomeT5(); const tTier2Terrain = rBiomeT6(); const tTier3Terrain = rBiomeT7(); -const tHill = rBiomeT8(); -const tDirt = rBiomeT9(); -const tRoad = rBiomeT10(); -const tRoadWild = rBiomeT11(); const tTier4Terrain = rBiomeT12(); -const tShoreBlend = rBiomeT13(); -const tShore = rBiomeT14(); -const tWater = rBiomeT15(); // gaia entities const oTree1 = rBiomeE1(); @@ -25,131 +18,105 @@ const oTree3 = rBiomeE3(); const oTree4 = rBiomeE4(); const oTree5 = rBiomeE5(); -const oFruitBush = rBiomeE6(); -const oMainHuntableAnimal = rBiomeE8(); -const oFish = rBiomeE9(); -const oSecondaryHuntableAnimal = rBiomeE10(); -const oStoneLarge = rBiomeE11(); -const oStoneSmall = rBiomeE12(); -const oMetalLarge = rBiomeE13(); -const oWood = "gaia/special_treasure_wood"; -const oFood = "gaia/special_treasure_food_bin"; // decorative props const aGrass = rBiomeA1(); const aGrassShort = rBiomeA2(); -const aReeds = rBiomeA3(); -const aLillies = rBiomeA4(); const aRockLarge = rBiomeA5(); const aRockMedium = rBiomeA6(); const aBushMedium = rBiomeA7(); const aBushSmall = rBiomeA8(); -const aTree = rBiomeA9(); +const aWaypointFlag = "actor|props/special/common/waypoint_flag.xml"; const pForest1 = [tForestFloor2 + TERRAIN_SEPARATOR + oTree1, tForestFloor2 + TERRAIN_SEPARATOR + oTree2, tForestFloor2]; const pForest2 = [tForestFloor1 + TERRAIN_SEPARATOR + oTree4, tForestFloor1 + TERRAIN_SEPARATOR + oTree5, tForestFloor1]; -log("Initializing map..."); +const oTreasureSeeker = "skirmish/units/default_support_female_citizen"; +const oCivicCenter = "skirmish/structures/default_civil_centre"; +const oCitizenInfantry = "skirmish/units/default_infantry_melee_b"; + +const triggerPointAttacker = "special/trigger_point_A"; +const triggerPointTreasures = [ + "special/trigger_point_B", + "special/trigger_point_C", + "special/trigger_point_D" +]; +log("Initializing map..."); InitMap(); var numPlayers = getNumPlayers(); var mapSize = getMapSize(); -var mapArea = mapSize*mapSize; - -// create tile classes var clPlayer = createTileClass(); var clHill = createTileClass(); -var clHill2 = createTileClass(); var clForest = createTileClass(); -var clWater = createTileClass(); var clDirt = createTileClass(); -var clRock = createTileClass(); -var clMetal = createTileClass(); -var clFood = createTileClass(); var clBaseResource = createTileClass(); -var clSettlement = createTileClass(); var clLand = createTileClass(); var clWomen = createTileClass(); for (var ix = 0; ix < mapSize; ix++) -{ for (var iz = 0; iz < mapSize; iz++) - { - var x = ix / (mapSize + 1.0); - var z = iz / (mapSize + 1.0); - placeTerrain(ix, iz, tMainTerrain); - } -} + placeTerrain(ix, iz, tMainTerrain); -var fx = fractionToTiles(0.5); -var fz = fractionToTiles(0.5); -ix = round(fx); -iz = round(fz); - -var lSize = sqrt(sqrt(sqrt(scaleByMapSize(1, 6)))); - -var placer = new ClumpPlacer(mapArea * 0.065 * lSize, 0.7, 0.1, 10, ix, iz); -var terrainPainter = new LayeredPainter( - [tMainTerrain, tMainTerrain], // terrains - [3] // widths -); -var elevationPainter = new SmoothElevationPainter( - ELEVATION_SET, // type - 3, // elevation - 3 // blend radius -); -createArea(placer, [terrainPainter, elevationPainter, paintClass(clLand)], null); +var ix = Math.round(fractionToTiles(0.5)); +var iz = Math.round(fractionToTiles(0.5)); + +// Create the main treasure area in the middle of the map +createArea( + new ClumpPlacer(mapSize * mapSize * scaleByMapSize(0.065, 0.09), 0.7, 0.1, 10, ix, iz), + [ + new LayeredPainter([tMainTerrain, tMainTerrain], [3]), + new SmoothElevationPainter(ELEVATION_SET, 3, 3), + paintClass(clLand) + ], + null); // randomize player order var playerIDs = []; for (var i = 0; i < numPlayers; i++) -{ playerIDs.push(i+1); -} playerIDs = sortPlayers(playerIDs); // place players - var playerX = new Array(numPlayers); var playerZ = new Array(numPlayers); var attackerX = new Array(numPlayers); var attackerZ = new Array(numPlayers); var playerAngle = new Array(numPlayers); -var startAngle = randFloat(0, TWO_PI); -for (var i = 0; i < numPlayers; i++) +var startAngle = randFloat(0, 2 * PI); +for (let i = 0; i < numPlayers; ++i) { - playerAngle[i] = startAngle + i*TWO_PI/numPlayers; + playerAngle[i] = startAngle + i * 2 * PI / numPlayers; playerX[i] = 0.5 + 0.3*cos(playerAngle[i]); playerZ[i] = 0.5 + 0.3*sin(playerAngle[i]); attackerX[i] = 0.5 + 0.45*cos(playerAngle[i]); attackerZ[i] = 0.5 + 0.45*sin(playerAngle[i]); } -for (var i = 0; i < numPlayers; i++) +for (let i = 0; i < numPlayers; ++i) { var id = playerIDs[i]; log("Creating base for player " + id + "..."); - // some constants - var radius = scaleByMapSize(15,25); - var cliffRadius = 2; - var elevation = 20; + var radius = scaleByMapSize(15, 25); // place the attacker spawning trigger point var ax = round(fractionToTiles(attackerX[i])); var az = round(fractionToTiles(attackerZ[i])); - placeObject(ax, az, "special/trigger_point_A", id, PI); + placeObject(ax, az, triggerPointAttacker, id, PI); + placeObject(ax, az, aWaypointFlag, 0, PI/2); addToClass(ax, az, clPlayer); addToClass(round(fractionToTiles((attackerX[i] + playerX[i]) / 2)), round(fractionToTiles((attackerZ[i] + playerZ[i]) / 2)), clPlayer); // get the x and z in tiles - fx = fractionToTiles(playerX[i]); - fz = fractionToTiles(playerZ[i]); - ix = round(fx); - iz = round(fz); + let fx = fractionToTiles(playerX[i]); + let fz = fractionToTiles(playerZ[i]); + let ix = round(fx); + let iz = round(fz); + addToClass(ix, iz, clPlayer); addToClass(ix+5, iz, clPlayer); addToClass(ix, iz+5, clPlayer); @@ -159,36 +126,43 @@ // Place default civ starting entities var uDist = 6; var uSpace = 2; - placeObject(fx, fz, "skirmish/structures/default_civil_centre", id, BUILDING_ORIENTATION); + placeObject(fx, fz, oCivicCenter, id, BUILDING_ORIENTATION); var uAngle = BUILDING_ORIENTATION - PI / 2; var count = 4; - for (var numberofentities = 0; numberofentities < count; numberofentities++) + for (let numberofentities = 0; numberofentities < count; ++numberofentities) { var ux = fx + uDist * cos(uAngle) + numberofentities * uSpace * cos(uAngle + PI/2) - (0.75 * uSpace * floor(count / 2) * cos(uAngle + PI/2)); var uz = fz + uDist * sin(uAngle) + numberofentities * uSpace * sin(uAngle + PI/2) - (0.75 * uSpace * floor(count / 2) * sin(uAngle + PI/2)); - placeObject(ux, uz, "skirmish/units/default_infantry_melee_b", id, uAngle); + placeObject(ux, uz, oCitizenInfantry, id, uAngle); } placeDefaultDecoratives(fx, fz, aGrassShort, clBaseResource, radius); - var tang = startAngle + (i+0.5)*TWO_PI/numPlayers; - var placer = new PathPlacer(fractionToTiles(0.5), fractionToTiles(0.5), fractionToTiles(0.5 + 0.5*cos(tang)), fractionToTiles(0.5 + 0.5*sin(tang)), scaleByMapSize(14,24), 0.4, 3*(scaleByMapSize(1,3)), 0.2, 0.05); - var terrainPainter = new LayeredPainter( - [tMainTerrain, tMainTerrain], // terrains - [1] // widths - ); - var elevationPainter = new SmoothElevationPainter( - ELEVATION_SET, // type - 3, // elevation - 4 // blend radius - ); - createArea(placer, [terrainPainter, elevationPainter, paintClass(clWater)], null); + var tang = startAngle + (i + 0.5) * 2 * PI / numPlayers; + + var placer = new PathPlacer( + fractionToTiles(0.5), + fractionToTiles(0.5), + fractionToTiles(0.5 + 0.5 * Math.cos(tang)), + fractionToTiles(0.5 + 0.5 * Math.sin(tang)), + scaleByMapSize(14, 24), + 0.4, + 3 * scaleByMapSize(1, 3), + 0.2, + 0.05); + + createArea( + placer, + [ + new LayeredPainter([tMainTerrain, tMainTerrain], [1]), + new SmoothElevationPainter(ELEVATION_SET, 3, 4) + ], + null); - //creating female citizens var femaleLocation = getTIPIADBON([ix, iz], [mapSize / 2, mapSize / 2], [-3 , 3.5], 1, 3); if (femaleLocation !== undefined) { - placeObject(femaleLocation[0], femaleLocation[1], "skirmish/units/default_support_female_citizen", id, playerAngle[i] + PI); + placeObject(femaleLocation[0], femaleLocation[1], oTreasureSeeker, id, playerAngle[i] + PI); addToClass(floor(femaleLocation[0]), floor(femaleLocation[1]), clWomen); } } @@ -196,92 +170,90 @@ paintTerrainBasedOnHeight(3.12, 29, 1, tCliff); paintTileClassBasedOnHeight(3.12, 29, 1, clHill); -// create trigger points for treasures -var group = new SimpleGroup( [new SimpleObject("special/trigger_point_B", 1,1, 0,0)], true, clWomen); -createObjectGroups(group, 0, - [avoidClasses(clForest, 5, clPlayer, 5, clHill, 5), stayClasses(clLand, 5)], - scaleByMapSize(40, 140), 100 -); +for (let triggerPointTreasure of triggerPointTreasures) + createObjectGroups( + new SimpleGroup([new SimpleObject(triggerPointTreasure, 1, 1, 0, 0)], true, clWomen), + 0, + [avoidClasses(clForest, 5, clPlayer, 5, clHill, 5), stayClasses(clLand, 5)], + scaleByMapSize(40, 140), 100 + ); -group = new SimpleGroup( [new SimpleObject("special/trigger_point_C", 1,1, 0,0)], true, clWomen); -createObjectGroups(group, 0, - [avoidClasses(clForest, 5, clPlayer, 5, clHill, 5), stayClasses(clLand, 5)], - scaleByMapSize(40, 140), 100 -); +createBumps(stayClasses(clLand, 5)); -group = new SimpleGroup( [new SimpleObject("special/trigger_point_D", 1,1, 0,0)], true, clWomen); -createObjectGroups(group, 0, - [avoidClasses(clForest, 5, clPlayer, 5, clHill, 5), stayClasses(clLand, 5)], - scaleByMapSize(40, 140), 100 +createForests( + [tMainTerrain, tForestFloor1, tForestFloor2, pForest1, pForest2], + [avoidClasses(clPlayer, 20, clForest, 5, clHill, 0, clBaseResource,2, clWomen, 5), stayClasses(clLand, 4)], + clForest, + 1.0, + random_terrain ); -// create bumps -createBumps([avoidClasses(clWater, 2, clPlayer, 10), stayClasses(clLand, 5)]); - -// create hills -if (randInt(1,2) == 1) - createHills([tMainTerrain, tCliff, tHill], [avoidClasses(clPlayer, 20, clHill, 5, clBaseResource, 3, clWomen, 5), stayClasses(clLand, 5)], clHill, scaleByMapSize(10, 60) * numPlayers); +if (randBool()) + createHills( + [tMainTerrain, tCliff, tHill], + [avoidClasses(clPlayer, 20, clHill, 5, clBaseResource, 3, clWomen, 5), stayClasses(clLand, 5)], + clHill, + scaleByMapSize(10, 60) * numPlayers); else - createMountains(tCliff, [avoidClasses(clPlayer, 20, clHill, 5, clBaseResource, 3, clWomen, 5), stayClasses(clLand, 5)], clHill, scaleByMapSize(10, 60) * numPlayers); -createHills([tCliff, tCliff, tHill], avoidClasses(clPlayer, 20, clHill, 5, clBaseResource, 3, clWomen, 5, clLand, 5), clHill, scaleByMapSize(15, 90) * numPlayers, undefined, undefined, undefined, undefined, 55); - -// create forests -createForests( - [tMainTerrain, tForestFloor1, tForestFloor2, pForest1, pForest2], - [avoidClasses(clPlayer, 20, clForest, 5, clHill, 0, clBaseResource,2, clWomen, 5), stayClasses(clLand, 4)], - clForest, - 1.0, - random_terrain -); + createMountains( + tCliff, + [avoidClasses(clPlayer, 20, clHill, 5, clBaseResource, 3, clWomen, 5), stayClasses(clLand, 5)], + clHill, + scaleByMapSize(10, 60) * numPlayers); + +createHills( + [tCliff, tCliff, tHill], + avoidClasses(clPlayer, 20, clHill, 5, clBaseResource, 3, clWomen, 5, clLand, 5), + clHill, + scaleByMapSize(15, 90) * numPlayers, + undefined, + undefined, + undefined, + undefined, + 55); RMS.SetProgress(50); -// create dirt patches log("Creating dirt patches..."); createLayeredPatches( - [scaleByMapSize(3, 6), scaleByMapSize(5, 10), scaleByMapSize(8, 21)], - [[tMainTerrain,tTier1Terrain],[tTier1Terrain,tTier2Terrain], [tTier2Terrain,tTier3Terrain]], - [1,1], - [avoidClasses(clForest, 0, clHill, 0, clDirt, 5, clPlayer, 12, clWomen, 5), stayClasses(clLand, 5)] + [scaleByMapSize(3, 6), scaleByMapSize(5, 10), scaleByMapSize(8, 21)], + [[tMainTerrain, tTier1Terrain], [tTier1Terrain, tTier2Terrain], [tTier2Terrain, tTier3Terrain]], + [1, 1], + [avoidClasses(clForest, 0, clHill, 0, clDirt, 5, clPlayer, 12, clWomen, 5), stayClasses(clLand, 5)] ); -// create grass patches log("Creating grass patches..."); createPatches( - [scaleByMapSize(2, 4), scaleByMapSize(3, 7), scaleByMapSize(5, 15)], - tTier4Terrain, - [avoidClasses(clForest, 0, clHill, 0, clDirt, 5, clPlayer, 12, clWomen, 5), stayClasses(clLand, 5)] + [scaleByMapSize(2, 4), scaleByMapSize(3, 7), scaleByMapSize(5, 15)], + tTier4Terrain, + [avoidClasses(clForest, 0, clHill, 0, clDirt, 5, clPlayer, 12, clWomen, 5), stayClasses(clLand, 5)] ); -// create decoration var planetm = 1; - if (random_terrain == g_BiomeTropic) planetm = 8; -createDecoration -( - [[new SimpleObject(aRockMedium, 1,3, 0,1)], - [new SimpleObject(aRockLarge, 1,2, 0,1), new SimpleObject(aRockMedium, 1,3, 0,2)], - [new SimpleObject(aGrassShort, 1,2, 0,1, -PI/8,PI/8)], - [new SimpleObject(aGrass, 2,4, 0,1.8, -PI/8,PI/8), new SimpleObject(aGrassShort, 3,6, 1.2,2.5, -PI/8,PI/8)], - [new SimpleObject(aBushMedium, 1,2, 0,2), new SimpleObject(aBushSmall, 2,4, 0,2)] - ], - [ - scaleByMapSize(16, 262), - scaleByMapSize(8, 131), - planetm * scaleByMapSize(13, 200), - planetm * scaleByMapSize(13, 200), - planetm * scaleByMapSize(13, 200) - ], - [avoidClasses(clForest, 0, clPlayer, 0, clHill, 0), stayClasses(clLand, 5)] +createDecoration( + [ + [new SimpleObject(aRockMedium, 1, 3, 0, 1)], + [new SimpleObject(aRockLarge, 1, 2, 0, 1), new SimpleObject(aRockMedium, 1, 3, 0, 2)], + [new SimpleObject(aGrassShort, 1, 2, 0, 1, -PI/8, PI/8)], + [new SimpleObject(aGrass, 2,4, 0, 1.8, -PI/8, PI/8), new SimpleObject(aGrassShort, 3,6, 1.2, 2.5, -PI/8, PI/8)], + [new SimpleObject(aBushMedium, 1, 2, 0, 2), new SimpleObject(aBushSmall, 2, 4, 0, 2)] + ], + [ + scaleByMapSize(16, 262), + scaleByMapSize(8, 131), + planetm * scaleByMapSize(13, 200), + planetm * scaleByMapSize(13, 200), + planetm * scaleByMapSize(13, 200) + ], + [avoidClasses(clForest, 0, clPlayer, 0, clHill, 0), stayClasses(clLand, 5)] ); -// create straggler trees log("Creating straggler trees..."); -var types = [oTree1, oTree2, oTree4, oTree3]; // some variation -createStragglerTrees(types, [avoidClasses(clForest, 7, clHill, 1, clPlayer, 9, clMetal, 6, clRock, 6), stayClasses(clLand, 7)]); - +createStragglerTrees( + [oTree1, oTree2, oTree4, oTree3], + [avoidClasses(clForest, 7, clHill, 1, clPlayer, 9), stayClasses(clLand, 7)]); -// Export map data ExportMap(); Index: binaries/data/mods/public/maps/random/survivalofthefittest_triggers.js =================================================================== --- binaries/data/mods/public/maps/random/survivalofthefittest_triggers.js +++ binaries/data/mods/public/maps/random/survivalofthefittest_triggers.js @@ -1,3 +1,56 @@ +/** + * If set to true, it will print how many templates would be spawned if the players were not defeated. + */ +const dryRun = false; + +/** + * If enabled, prints the number of units to the command line output. + */ +const debugLog = false; + +/** + * Least and greatest number of minutes to pass between spawning new treasures. + */ +var treasureTime = [3, 5]; + +/** + * When the first wave of attackers will be spawned. + */ +var firstWaveTime = [4, 6]; + +/** + * Least and greatest number of minutes between two consecutive waves. + */ +var waveTime = [2, 4]; + +/** + * Roughly the number of attackers on the first wave. + */ +var initialAttackers = 5; + +/** + * Increase the number of attackers exponentially, by this percent value per minute. + */ +var percentPerMinute = 1.05; + +/** + * Greatest amount of attackers that can be spawned. + */ +var totalAttackerLimit = 150; + +/** + * Least and greatest amount of siege engines per wave. + */ +var siegeFraction = [0.2, 0.5]; + +/** + * Potentially / definitely spawn a gaia hero after this number of minutes. + */ +var heroTime = [20, 60]; + +/** + * The following templates can't be built by any player. + */ var disabledTemplates = (civ) => [ // Economic structures "structures/" + civ + "_corral", @@ -23,8 +76,10 @@ "structures/ptol_lighthouse" ]; -var treasures = -[ +/** + * Spawn these treasures in regular intervals. + */ +var treasures = [ "gaia/special_treasure_food_barrel", "gaia/special_treasure_food_bin", "gaia/special_treasure_food_crate", @@ -36,136 +91,82 @@ "gaia/special_treasure_wood" ]; -var attackerEntityTemplates = -[ - [ - "units/athen_champion_infantry", - "units/athen_champion_marine", - "units/athen_champion_ranged", - "units/athen_mechanical_siege_lithobolos_packed", - "units/athen_mechanical_siege_oxybeles_packed", - ], - [ - "units/brit_champion_cavalry", - "units/brit_champion_infantry", - "units/brit_mechanical_siege_ram", - ], - [ - "units/cart_champion_cavalry", - "units/cart_champion_elephant", - "units/cart_champion_infantry", - "units/cart_champion_pikeman", - ], - [ - "units/gaul_champion_cavalry", - "units/gaul_champion_fanatic", - "units/gaul_champion_infantry", - "units/gaul_mechanical_siege_ram", - ], - [ - "units/iber_champion_cavalry", - "units/iber_champion_infantry", - "units/iber_mechanical_siege_ram", - ], - [ - "units/mace_champion_cavalry", - "units/mace_champion_infantry_a", - "units/mace_champion_infantry_e", - "units/mace_mechanical_siege_lithobolos_packed", - "units/mace_mechanical_siege_oxybeles_packed", - ], - [ - "units/maur_champion_chariot", - "units/maur_champion_elephant", - "units/maur_champion_infantry", - "units/maur_champion_maiden", - "units/maur_champion_maiden_archer", - ], - [ - "units/pers_champion_cavalry", - "units/pers_champion_infantry", - "units/pers_champion_elephant", - ], - [ - "units/ptol_champion_cavalry", - "units/ptol_champion_elephant", - ], - [ - "units/rome_champion_cavalry", - "units/rome_champion_infantry", - "units/rome_mechanical_siege_ballista_packed", - "units/rome_mechanical_siege_scorpio_packed", - ], - [ - "units/sele_champion_cavalry", - "units/sele_champion_chariot", - "units/sele_champion_elephant", - "units/sele_champion_infantry_pikeman", - "units/sele_champion_infantry_swordsman", - ], - [ - "units/spart_champion_infantry_pike", - "units/spart_champion_infantry_spear", - "units/spart_champion_infantry_sword", - "units/spart_mechanical_siege_ram", - ], -]; +/** + * An object that maps from civ [f.e. "spart"] to an object + * that has the keys "champions", "siege" and "heroes", + * which is an array containing all these templates, + * trainable from a building or not. +}*/ +var attackerUnitTemplates = {}; -Trigger.prototype.StartAnEnemyWave = function() +Trigger.prototype.InitSurvival = function() { - let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer); - let attackerTemplates = attackerEntityTemplates[Math.floor(Math.random() * attackerEntityTemplates.length)]; - // A soldier for each 2-3 minutes of the game. Should be waves of 20 soldiers after an hour - let nextTime = Math.round(120000 + Math.random() * 60000); - let attackersPerTemplate = Math.ceil(cmpTimer.GetTime() / nextTime / attackerTemplates.length); - let spawned = false; + this.InitStartingUnits(); + this.LoadAttackerTemplates(); + this.SetDisableTemplates(); + this.PlaceTreasures(); + this.InitializeEnemyWaves(); +}; - for (let point of this.GetTriggerPoints("A")) - { - let cmpPlayer = QueryOwnerInterface(point, IID_Player); - if (cmpPlayer.GetPlayerID() == 0 || cmpPlayer.GetState() != "active") - continue; +Trigger.prototype.debugLog = function(txt) +{ + if (!debugLog) + return; + + print("DEBUG [" + Math.round(Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime() / 60 / 1000) + "] " + txt + "\n"); +}; + +Trigger.prototype.LoadAttackerTemplates = function() +{ + let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); - let cmpPosition = Engine.QueryInterface(this.playerCivicCenter[cmpPlayer.GetPlayerID()], IID_Position); - if (!cmpPosition || !cmpPosition.IsInWorld) + for (let templateName of cmpTemplateManager.FindAllTemplates(false)) + { + if (!templateName.startsWith("units/") || templateName.endsWith("_unpacked")) continue; - let targetPos = cmpPosition.GetPosition(); - for (let template of attackerTemplates) - { - let entities = TriggerHelper.SpawnUnits(point, template, attackersPerTemplate, 0); + let identity = cmpTemplateManager.GetTemplate(templateName).Identity; - ProcessCommand(0, { - "type": "attack-walk", - "entities": entities, - "x": targetPos.x, - "z": targetPos.z, - "queued": true, - "targetClasses": undefined - }); - } - spawned = true; + if (!attackerUnitTemplates[identity.Civ]) + attackerUnitTemplates[identity.Civ] = { + "heroes": [], + "champions": [], + "siege": [] + }; + + let classes = GetIdentityClasses(identity); + + // Notice some heroes are elephants and war elephants are champions + if (classes.indexOf("Hero") != -1) + attackerUnitTemplates[identity.Civ].heroes.push(templateName); + else if (classes.indexOf("Siege") != -1 || classes.indexOf("Elephant") != -1 && classes.indexOf("Melee") != -1) + attackerUnitTemplates[identity.Civ].siege.push(templateName); + else if (classes.indexOf("Champion") != -1) + attackerUnitTemplates[identity.Civ].champions.push(templateName); } - if (!spawned) - return; + this.debugLog("Attacker templates:"); + this.debugLog(uneval(attackerUnitTemplates)); +}; - let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); - cmpGUIInterface.PushNotification({ - "message": markForTranslation("An enemy wave is attacking!"), - "translateMessage": true - }); - this.DoAfterDelay(nextTime, "StartAnEnemyWave", {}); // The next wave will come in 3 minutes +Trigger.prototype.SetDisableTemplates = function() +{ + for (let i = 1; i < TriggerHelper.GetNumberOfPlayers(); ++i) + { + let cmpPlayer = QueryPlayerIDInterface(i); + cmpPlayer.SetDisabledTemplates(disabledTemplates(cmpPlayer.GetCiv())); + } }; -Trigger.prototype.InitGame = function() +/** + * Remember civic centers and make women invincible. + */ +Trigger.prototype.InitStartingUnits = function() { - let numberOfPlayers = TriggerHelper.GetNumberOfPlayers(); - // Find all of the civic centers, disable some structures - for (let i = 1; i < numberOfPlayers; ++i) + let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); + for (let i = 1; i < TriggerHelper.GetNumberOfPlayers(); ++i) { - let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); - let playerEntities = cmpRangeManager.GetEntitiesByPlayer(i); // Get all of each player's entities + let playerEntities = cmpRangeManager.GetEntitiesByPlayer(i); for (let entity of playerEntities) { @@ -173,6 +174,8 @@ this.playerCivicCenter[i] = entity; else if (TriggerHelper.EntityHasClass(entity, "FemaleCitizen")) { + this.treasureFemale[i] = entity; + let cmpDamageReceiver = Engine.QueryInterface(entity, IID_DamageReceiver); cmpDamageReceiver.SetInvulnerability(true); @@ -181,51 +184,177 @@ } } } +}; - this.PlaceTreasures(); +Trigger.prototype.InitializeEnemyWaves = function() +{ + let time = randFloat(...firstWaveTime) * 60 * 1000; + Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface).AddTimeNotification({ + "message": markForTranslation("The first wave will start in %(time)s!"), + "translateMessage": true + }, time); + this.DoAfterDelay(time, "StartAnEnemyWave", {}); +}; + +Trigger.prototype.StartAnEnemyWave = function() +{ + let currentMin = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime() / 60 / 1000; + let nextWaveTime = randFloat(...waveTime); + let civ = pickRandom(Object.keys(attackerUnitTemplates)); + + // Determine total attacker count of the current wave. + // Exponential increase with time, capped to the limit and fluctuating proportionally with the current wavetime. + let totalAttackers = Math.ceil(Math.min(totalAttackerLimit, + initialAttackers * Math.pow(percentPerMinute, currentMin) * nextWaveTime / waveTime[1])); - for (let i = 1; i < numberOfPlayers; ++i) + this.debugLog("Spawning " + totalAttackers + " attackers"); + + let attackerTemplates = []; + + // Add hero + if (currentMin > randFloat(...heroTime) && attackerUnitTemplates[civ].heroes.length) { - let cmpPlayer = QueryPlayerIDInterface(i); - let civ = cmpPlayer.GetCiv(); - cmpPlayer.SetDisabledTemplates(disabledTemplates(civ)); + this.debugLog("Spawning hero"); + + attackerTemplates.push({ + "template": pickRandom(attackerUnitTemplates[civ].heroes), + "count": 1, + "hero": true + }); + --totalAttackers; } + + // Random siege to champion ratio + let siegeRatio = randFloat(...siegeFraction); + let siegeCount = Math.round(siegeRatio * totalAttackers); + + this.debugLog("Siege Ratio: " + Math.round(siegeRatio * 100) + "%"); + + let attackerTypeCounts = { + "siege": siegeCount, + "champions": totalAttackers - siegeCount + }; + + this.debugLog("Spawning:" + uneval(attackerTypeCounts)); + + // Random ratio of the given templates + for (let attackerType in attackerTypeCounts) + { + let attackerTypes = attackerUnitTemplates[civ][attackerType]; + let attackerEntityRatios = new Array(attackerTypes.length).fill(1).map(i => randFloat(0, 1)); + let attackerEntityRatioSum = attackerEntityRatios.reduce((current, sum) => current + sum, 0); + attackerEntityRatios = attackerEntityRatios.map(ratio => ratio / attackerEntityRatioSum); + + let remainder = attackerTypeCounts[attackerType]; + for (let i in attackerTypes) + { + let count = + +i == attackerTypes.length - 1 ? + remainder : + Math.floor(attackerEntityRatios[i] * attackerTypeCounts[attackerType]); + + attackerTemplates.push({ + "template": attackerUnitTemplates[civ][attackerType][i], + "count": count + }); + remainder -= count; + } + if (remainder != 0) + warn("Didn't spawn as many attackers as intended: " + remainder); + } + + this.debugLog("Templates: " + uneval(attackerTemplates)); + + // Spawn the templates + let spawned = false; + for (let point of this.GetTriggerPoints("A")) + { + if (dryRun) + { + spawned = true; + break; + } + + let cmpPlayer = QueryOwnerInterface(point, IID_Player); + + // Trigger point owned by Gaia if the player is defeated + if (cmpPlayer.GetPlayerID() == 0) + continue; + + let targetPos = Engine.QueryInterface(this.playerCivicCenter[cmpPlayer.GetPlayerID()], IID_Position).GetPosition2D(); + + for (let attackerTemplate of attackerTemplates) + { + // Don't spawn gaia hero if the previous one is still alive + if (attackerTemplate.hero && this.gaiaHeroes[cmpPlayer.GetPlayerID()]) + { + let cmpHealth = Engine.QueryInterface(this.gaiaHeroes[cmpPlayer.GetPlayerID()], IID_Health); + if (cmpHealth && cmpHealth.GetHitpoints() != 0) + { + this.debugLog("Not spawning hero for player " + cmpPlayer.GetPlayerID() + " as the previous one is still alive"); + continue; + } + } + + if (dryRun) + continue; + + let entities = TriggerHelper.SpawnUnits(point, attackerTemplate.template, attackerTemplate.count, 0); + ProcessCommand(0, { + "type": "attack-walk", + "entities": entities, + "x": targetPos.x, + "z": targetPos.y, + "queued": true, + "targetClasses": undefined + }); + + if (attackerTemplate.hero) + this.gaiaHeroes[cmpPlayer.GetPlayerID()] = entities[0]; + } + spawned = true; + } + + if (!spawned) + return; + + let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); + cmpGUIInterface.PushNotification({ + "message": markForTranslation("An enemy wave is attacking!"), + "translateMessage": true + }); + this.DoAfterDelay(nextWaveTime * 60 * 1000, "StartAnEnemyWave", {}); }; Trigger.prototype.PlaceTreasures = function() { - let point = ["B", "C", "D"][Math.floor(Math.random() * 3)]; + let point = pickRandom(["B", "C", "D"]); let triggerPoints = this.GetTriggerPoints(point); for (let point of triggerPoints) - { - let template = treasures[Math.floor(Math.random() * treasures.length)]; - TriggerHelper.SpawnUnits(point, template, 1, 0); - } - this.DoAfterDelay(4*60*1000, "PlaceTreasures", {}); // Place more treasures after 4 minutes -}; + TriggerHelper.SpawnUnits(point, pickRandom(treasures), 1, 0); -Trigger.prototype.InitializeEnemyWaves = function() -{ - let time = (5 + Math.round(Math.random() * 10)) * 60 * 1000; - let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); - cmpGUIInterface.AddTimeNotification({ - "message": markForTranslation("The first wave will start in %(time)s!"), - "translateMessage": true - }, time); - this.DoAfterDelay(time, "StartAnEnemyWave", {}); + this.DoAfterDelay(randFloat(...treasureTime) * 60 * 1000, "PlaceTreasures", {}); }; -Trigger.prototype.DefeatPlayerOnceCCIsDestroyed = function(data) +Trigger.prototype.OnOwnershipChanged = function(data) { if (data.entity == this.playerCivicCenter[data.from]) TriggerHelper.DefeatPlayer(data.from); + else if (data.entity == this.treasureFemale[data.from]) + { + this.treasureFemale[data.from] = undefined; + Engine.DestroyEntity(data.entity); + } }; { let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); - cmpTrigger.playerCivicCenter = {}; - cmpTrigger.DoAfterDelay(1000, "InitializeEnemyWaves", {}); - cmpTrigger.RegisterTrigger("OnInitGame", "InitGame", { "enabled": true }); - cmpTrigger.RegisterTrigger("OnOwnershipChanged", "DefeatPlayerOnceCCIsDestroyed", { "enabled": true }); + + cmpTrigger.treasureFemale = []; + cmpTrigger.playerCivicCenter = []; + cmpTrigger.gaiaHeroes = []; + + cmpTrigger.RegisterTrigger("OnInitGame", "InitSurvival", { "enabled": true }); + cmpTrigger.RegisterTrigger("OnOwnershipChanged", "OnOwnershipChanged", { "enabled": true }); }