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,6 +1,5 @@ RMS.LoadLibrary("rmgen"); -//random terrain textures var random_terrain = randomizeBiome(); const tMainTerrain = rBiomeT1(); @@ -11,13 +10,7 @@ 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,93 +18,70 @@ 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); +// 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); @@ -119,7 +89,7 @@ var playerAngle = new Array(numPlayers); var startAngle = randFloat(0, TWO_PI); -for (var i = 0; i < numPlayers; i++) +for (let i = 0; i < numPlayers; ++i) { playerAngle[i] = startAngle + i*TWO_PI/numPlayers; playerX[i] = 0.5 + 0.3*cos(playerAngle[i]); @@ -128,20 +98,18 @@ 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); @@ -150,6 +118,7 @@ fz = fractionToTiles(playerZ[i]); ix = round(fx); iz = round(fz); + addToClass(ix, iz, clPlayer); addToClass(ix+5, iz, clPlayer); addToClass(ix, iz+5, clPlayer); @@ -159,36 +128,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 +172,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 amount of time to pass between spawning new treasures. + */ +var treasureTime = [3, 5]; + +/** + * When the first wave of attackers will be spawned. + */ +var firstWaveTime = randFloat(4, 6); + +/** + * Least and greatest amount of time 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 number of attacker 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 amount of time. + */ +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,11 @@ "structures/ptol_lighthouse" ]; -var treasures = -[ +/** + * Spawn these treasures in regular intervals. + * TODO: spawn enemy gaia women simultaenously later + */ +var treasures = [ "gaia/special_treasure_food_barrel", "gaia/special_treasure_food_bin", "gaia/special_treasure_food_crate", @@ -36,105 +92,241 @@ "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", - ], -]; +var attackerEntityTemplates = { + "athen": { + "champions": [ + "athen_champion_infantry", + "athen_champion_marine", + "athen_champion_ranged", + ], + "siege": [ + "athen_mechanical_siege_lithobolos_packed", + "athen_mechanical_siege_oxybeles_packed", + ], + }, + "brit": { + "champions": [ + "brit_champion_cavalry", + "brit_champion_infantry", + ], + "siege": [ + "brit_mechanical_siege_ram", + ], + }, + "cart": { + "champions": [ + "cart_champion_cavalry", + "cart_champion_infantry", + "cart_champion_pikeman", + ], + "siege": [ + "cart_champion_elephant", + "cart_mechanical_siege_ballista_packed", + ], + }, + "gaul": { + "champions": [ + "gaul_champion_cavalry", + "gaul_champion_fanatic", + "gaul_champion_infantry", + ], + "siege": [ + "gaul_mechanical_siege_ram", + ], + }, + "iber": { + "champions": [ + "iber_champion_cavalry", + "iber_champion_infantry", + ], + "siege": [ + "iber_mechanical_siege_ram", + ], + }, + "mace": { + "champions": [ + "mace_champion_cavalry", + "mace_champion_infantry_a", + "mace_champion_infantry_e", + ], + "siege": [ + "mace_mechanical_siege_lithobolos_packed", + "mace_mechanical_siege_oxybeles_packed", + ], + }, + "maur": { + "champions": [ + "maur_champion_chariot", + "maur_champion_infantry", + "maur_champion_maiden", + "maur_champion_maiden_archer", + ], + "siege": [ + "maur_champion_elephant", + ], + }, + "pers": { + "champions": [ + "pers_champion_cavalry", + "pers_champion_infantry", + ], + "siege": [ + "pers_champion_elephant", + ], + }, + "ptol": { + "champions": [ + "ptol_champion_cavalry", + ], + "siege": [ + "ptol_champion_elephant", + ], + }, + "rome": { + "champions": [ + "rome_champion_cavalry", + "rome_champion_infantry", + ], + "siege": [ + "rome_mechanical_siege_ballista_packed", + "rome_mechanical_siege_scorpio_packed", + ], + }, + "sele": { + "champions": [ + "sele_champion_cavalry", + "sele_champion_chariot", + "sele_champion_infantry_pikeman", + "sele_champion_infantry_swordsman", + ], + "siege": [ + "sele_champion_elephant", + ], + }, + "spart": { + "champions": [ + "spart_champion_infantry_pike", + "spart_champion_infantry_spear", + "spart_champion_infantry_sword", + ], + "siege": [ + "spart_mechanical_siege_ram", + ], + } +}; + +Trigger.prototype.InitGame = function() +{ + this.InitStartingUnits(); + this.LoadHeroTemplates(); + this.PlaceTreasures(); + this.SetDisableTemplates(); +}; 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 = cmpTimer.GetTime() / 60 / 1000; + let nextWaveTime = randFloat(waveTime[0], waveTime[1]); + + let civ = pickRandom(Object.keys(attackerEntityTemplates)); + + // Determine total attacker count of the wave + let totalAttackers = Math.ceil(Math.min(totalAttackerLimit, + initialAttackers * Math.pow(percentPerMinute, currentMin) * nextWaveTime / waveTime[1])); + + if (debugLog) + print("DEBUG [" + Math.round(currentMin) + "] Spawning " + totalAttackers + " attackers\n"); + + let attackerTemplates = []; + + // Add hero + let spawnHero = currentMin > randFloat(heroTime[0], heroTime[1]); + if (spawnHero) + { + if (debugLog) + print("DEBUG Spawning hero\n"); + + attackerTemplates.push({ + "template": pickRandom(attackerEntityTemplates[civ].heroes), + "count": 1, + "hero": true + }); + --totalAttackers; + } + // Random siege to champion ratio + let siegeRatio = randFloat(siegeFraction[0], siegeFraction[1]); + let siegeCount = Math.round(siegeRatio * totalAttackers); + + if (debugLog) + print("DEBUG Siege Ratio: " + Math.round(siegeRatio * 100) + "%\n"); + + let attackerTypeCounts = { + "siege": siegeCount, + "champions": totalAttackers - siegeCount + }; + + if (debugLog) + print("DEBUG Spawning:" + uneval(attackerTypeCounts) + "\n"); + + // Random ratio of the given templates + for (let attackerType in attackerTypeCounts) + { + let attackerTypeCount = attackerTypeCounts[attackerType]; + for (let i in attackerEntityTemplates[civ][attackerType]) + { + let count = + +i == attackerEntityTemplates[civ][attackerType].length - 1 ? + attackerTypeCount : + randIntInclusive(0, attackerTypeCount); + + attackerTemplates.push({ + "template": attackerEntityTemplates[civ][attackerType][i], + "count": count + }); + attackerTypeCount -= count; + } + } + + // Spawn the templates + let spawned = false; + let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); for (let point of this.GetTriggerPoints("A")) { + if (dryRun) + { + spawned = true; + break; + } + let cmpPlayer = QueryOwnerInterface(point, IID_Player); - if (cmpPlayer.GetPlayerID() == 0 || cmpPlayer.GetState() != "active") + + // Trigger point owned by Gaia if the player is defeated + if (cmpPlayer.GetPlayerID() == 0) 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) + for (let attackerTemplate of attackerTemplates) { - let entities = TriggerHelper.SpawnUnits(point, template, attackersPerTemplate, 0); + // 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) + { + if (debugLog) + print("DEBUG Not spawning hero for player " + cmpPlayer.GetPlayerID() + " as the previous one is still alive\n"); + continue; + } + } + if (dryRun) + continue; + + let entities = TriggerHelper.SpawnUnits(point, "units/" + attackerTemplate.template, attackerTemplate.count, 0); ProcessCommand(0, { "type": "attack-walk", "entities": entities, @@ -143,6 +335,9 @@ "queued": true, "targetClasses": undefined }); + + if (attackerTemplate.hero) + this.gaiaHeroes[cmpPlayer.GetPlayerID()] = entities[0]; } spawned = true; } @@ -155,17 +350,40 @@ "message": markForTranslation("An enemy wave is attacking!"), "translateMessage": true }); - this.DoAfterDelay(nextTime, "StartAnEnemyWave", {}); // The next wave will come in 3 minutes + this.DoAfterDelay(nextWaveTime * 60 * 1000, "StartAnEnemyWave", {}); }; -Trigger.prototype.InitGame = function() +Trigger.prototype.LoadHeroTemplates = function() { - let numberOfPlayers = TriggerHelper.GetNumberOfPlayers(); - // Find all of the civic centers, disable some structures - for (let i = 1; i < numberOfPlayers; ++i) + this.gaiaHeroes = []; + + let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); + + for (let templateName of cmpTemplateManager.FindAllTemplates(false)) + { + if (templateName.substring(0, 6) != "units/") + continue; + + let identity = cmpTemplateManager.GetTemplate(templateName).Identity; + if (GetIdentityClasses(identity).indexOf("Hero") == -1) + continue; + + if (!attackerEntityTemplates[identity.Civ].heroes) + attackerEntityTemplates[identity.Civ].heroes = []; + + attackerEntityTemplates[identity.Civ].heroes.push(templateName.substring(6)); + } +}; + +/** + * Remember civic centers and make women invincible. + */ +Trigger.prototype.InitStartingUnits = function() +{ + 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 +391,8 @@ this.playerCivicCenter[i] = entity; else if (TriggerHelper.EntityHasClass(entity, "Female")) { + this.treasureFemale[i] = entity; + let cmpDamageReceiver = Engine.QueryInterface(entity, IID_DamageReceiver); cmpDamageReceiver.SetInvulnerability(true); @@ -181,32 +401,21 @@ } } } - - this.PlaceTreasures(); - - for (let i = 1; i < numberOfPlayers; ++i) - { - let cmpPlayer = QueryPlayerIDInterface(i); - let civ = cmpPlayer.GetCiv(); - cmpPlayer.SetDisabledTemplates(disabledTemplates(civ)); - } }; 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[0], treasureTime[1]) * 60 * 1000, "PlaceTreasures", {}); }; Trigger.prototype.InitializeEnemyWaves = function() { - let time = (5 + Math.round(Math.random() * 10)) * 60 * 1000; + let time = firstWaveTime * 60 * 1000; let cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); cmpGUIInterface.AddTimeNotification({ "message": markForTranslation("The first wave will start in %(time)s!"), @@ -215,17 +424,32 @@ 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); + } +}; + +Trigger.prototype.SetDisableTemplates = function() +{ + for (let i = 1; i < TriggerHelper.GetNumberOfPlayers(); ++i) + { + let cmpPlayer = QueryPlayerIDInterface(i); + cmpPlayer.SetDisabledTemplates(disabledTemplates(cmpPlayer.GetCiv())); + } }; { let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); - cmpTrigger.playerCivicCenter = {}; + cmpTrigger.treasureFemale = []; + cmpTrigger.playerCivicCenter = []; cmpTrigger.DoAfterDelay(1000, "InitializeEnemyWaves", {}); cmpTrigger.RegisterTrigger("OnInitGame", "InitGame", { "enabled": true }); - cmpTrigger.RegisterTrigger("OnOwnershipChanged", "DefeatPlayerOnceCCIsDestroyed", { "enabled": true }); + cmpTrigger.RegisterTrigger("OnOwnershipChanged", "OnOwnershipChanged", { "enabled": true }); }