From 18e7d8a5189ee001c18d4e4d9a6efde52dca4d44 Mon Sep 17 00:00:00 2001 From: elexis Date: Wed, 29 Mar 2017 16:06:11 +0000 Subject: [PATCH] Survival Of The Fittest Overhaul Exponential attacker increase to prevent boring endless games. Add a gaia hero per player at later stages of the game (if the previous one isn't alive anymore). Remove hardcoded template array and include gimmick templates (gaia), most notably the fireraiser which we never saw before and the siege tower. Remove treasure picker female after defeat to prevent confusion with treasure on the minimap. Add flag to the spawnpoints, so that new players know where the enemies actually come from, as proposed by bbleft and Hannibal Barca. Move all balancing constants to the top of the file. Actively chose attacker composition instead of having it relate to the number of templates that exist. Add debug output option, so that we can replay games and see which enemy wave composition attacked at which time. Add dry run, so that we can test the balancing effects from a non-visual replay instead of having to play some game. Remove many unused variables and some pointless tile classes from the mapgen. Whitespace and various code style cleanup, moving code to shorter, more readable functions. Reviewed By: bb Differential Revision: https://code.wildfiregames.com/D145 This was SVN commit r19359. --- .../maps/random/survivalofthefittest.js | 276 +++++------ .../random/survivalofthefittest_triggers.js | 430 ++++++++++++------ 2 files changed, 403 insertions(+), 303 deletions(-) diff --git a/binaries/data/mods/public/maps/random/survivalofthefittest.js b/binaries/data/mods/public/maps/random/survivalofthefittest.js index fa8ab2203d..02aa38ff01 100644 --- a/binaries/data/mods/public/maps/random/survivalofthefittest.js +++ b/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 oTree2 = rBiomeE2(); 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 ix = Math.round(fractionToTiles(0.5)); +var iz = Math.round(fractionToTiles(0.5)); -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); +// 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 @@ for (var i = 0; i < numPlayers; i++) // 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 @@ for (var i = 0; i < numPlayers; i++) 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 -); - -// create bumps -createBumps([avoidClasses(clWater, 2, clPlayer, 10), stayClasses(clLand, 5)]); - -// create hills -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 + [tMainTerrain, tForestFloor1, tForestFloor2, pForest1, pForest2], + [avoidClasses(clPlayer, 20, clForest, 5, clHill, 0, clBaseResource,2, clWomen, 5), stayClasses(clLand, 4)], + clForest, + 1, + random_terrain ); +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); + 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(); diff --git a/binaries/data/mods/public/maps/random/survivalofthefittest_triggers.js b/binaries/data/mods/public/maps/random/survivalofthefittest_triggers.js index afdd8cc200..74ea5c488b 100644 --- a/binaries/data/mods/public/maps/random/survivalofthefittest_triggers.js +++ b/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]; + +/** + * Earliest and latest time when the first wave of attackers will be spawned. + */ +var firstWaveTime = [4, 6]; + +/** + * Smallest and largest 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 @@ var disabledTemplates = (civ) => [ "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,113 +91,225 @@ var treasures = "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.InitSurvival = function() +{ + this.InitStartingUnits(); + this.LoadAttackerTemplates(); + this.SetDisableTemplates(); + this.PlaceTreasures(); + this.InitializeEnemyWaves(); +}; + +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); + + for (let templateName of cmpTemplateManager.FindAllTemplates(false)) + { + if (!templateName.startsWith("units/") || templateName.endsWith("_unpacked") || templateName.endsWith("_barracks")) + continue; + + let identity = cmpTemplateManager.GetTemplate(templateName).Identity; + + 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); + } + + this.debugLog("Attacker templates:"); + this.debugLog(uneval(attackerUnitTemplates)); +}; + +Trigger.prototype.SetDisableTemplates = function() +{ + for (let i = 1; i < TriggerHelper.GetNumberOfPlayers(); ++i) + { + let cmpPlayer = QueryPlayerIDInterface(i); + cmpPlayer.SetDisabledTemplates(disabledTemplates(cmpPlayer.GetCiv())); + } +}; + +/** + * Remember civic centers and make women invincible. + */ +Trigger.prototype.InitStartingUnits = function() +{ + let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); + for (let i = 1; i < TriggerHelper.GetNumberOfPlayers(); ++i) + { + let playerEntities = cmpRangeManager.GetEntitiesByPlayer(i); + + for (let entity of playerEntities) + { + if (TriggerHelper.EntityHasClass(entity, "CivilCentre")) + this.playerCivicCenter[i] = entity; + else if (TriggerHelper.EntityHasClass(entity, "FemaleCitizen")) + { + this.treasureFemale[i] = entity; + + let cmpDamageReceiver = Engine.QueryInterface(entity, IID_DamageReceiver); + cmpDamageReceiver.SetInvulnerability(true); + + let cmpHealth = Engine.QueryInterface(entity, IID_Health); + cmpHealth.SetUndeletable(true); + } + } + } +}; + +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 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; + 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])); + + this.debugLog("Spawning " + totalAttackers + " attackers"); + + let attackerTemplates = []; + + // Add hero + if (currentMin > randFloat(...heroTime) && attackerUnitTemplates[civ].heroes.length) + { + 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 attackerTypeTemplates = attackerUnitTemplates[civ][attackerType]; + let attackerEntityRatios = new Array(attackerTypeTemplates.length).fill(1).map(i => randFloat(0, 1)); + let attackerEntityRatioSum = attackerEntityRatios.reduce((current, sum) => current + sum, 0); + + let remainder = attackerTypeCounts[attackerType]; + for (let i in attackerTypeTemplates) + { + let count = + +i == attackerTypeTemplates.length - 1 ? + remainder : + Math.round(attackerEntityRatios[i] / attackerEntityRatioSum * attackerTypeCounts[attackerType]); + + attackerTemplates.push({ + "template": attackerTypeTemplates[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")) { - let cmpPlayer = QueryOwnerInterface(point, IID_Player); - if (cmpPlayer.GetPlayerID() == 0 || cmpPlayer.GetState() != "active") - continue; - - let cmpPosition = Engine.QueryInterface(this.playerCivicCenter[cmpPlayer.GetPlayerID()], IID_Position); - if (!cmpPosition || !cmpPosition.IsInWorld) - continue; - let targetPos = cmpPosition.GetPosition(); - - for (let template of attackerTemplates) + if (dryRun) { - let entities = TriggerHelper.SpawnUnits(point, template, attackersPerTemplate, 0); + 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.z, + "z": targetPos.y, "queued": true, "targetClasses": undefined }); + + if (attackerTemplate.hero) + this.gaiaHeroes[cmpPlayer.GetPlayerID()] = entities[0]; } spawned = true; } @@ -155,77 +322,38 @@ Trigger.prototype.StartAnEnemyWave = function() "message": markForTranslation("An enemy wave is attacking!"), "translateMessage": true }); - this.DoAfterDelay(nextTime, "StartAnEnemyWave", {}); // The next wave will come in 3 minutes -}; - -Trigger.prototype.InitGame = 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); - let playerEntities = cmpRangeManager.GetEntitiesByPlayer(i); // Get all of each player's entities - - for (let entity of playerEntities) - { - if (TriggerHelper.EntityHasClass(entity, "CivilCentre")) - this.playerCivicCenter[i] = entity; - else if (TriggerHelper.EntityHasClass(entity, "FemaleCitizen")) - { - let cmpDamageReceiver = Engine.QueryInterface(entity, IID_DamageReceiver); - cmpDamageReceiver.SetInvulnerability(true); - - let cmpHealth = Engine.QueryInterface(entity, IID_Health); - cmpHealth.SetUndeletable(true); - } - } - } - - this.PlaceTreasures(); - - for (let i = 1; i < numberOfPlayers; ++i) - { - let cmpPlayer = QueryPlayerIDInterface(i); - let civ = cmpPlayer.GetCiv(); - cmpPlayer.SetDisabledTemplates(disabledTemplates(civ)); - } + 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); + + this.DoAfterDelay(randFloat(...treasureTime) * 60 * 1000, "PlaceTreasures", {}); }; -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", {}); -}; - -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 }); }