/* * Military Manager. * Basically this deals with constructing defense and attack buildings, but it's not very developped yet. * There's a lot of work still to do here. * It also handles the attack plans (see attack_plan.js) * Not completely cleaned up from the original version in qBot. */ var MilitaryAttackManager = function() { this.fortressStartTime = 0; this.fortressLapseTime = Config.Military.fortressLapseTime * 1000; this.defenceBuildingTime = Config.Military.defenceBuildingTime * 1000; this.attackPlansStartTime = Config.Military.attackPlansStartTime * 1000; this.defenceManager = new Defence(); this.TotalAttackNumber = 0; this.upcomingAttacks = { "CityAttack" : [] }; this.startedAttacks = { "CityAttack" : [] }; }; MilitaryAttackManager.prototype.init = function(gameState) { var civ = gameState.playerData.civ; // load units and buildings from the config files if (civ in Config.buildings.moderate){ this.bModerate = Config.buildings.moderate[civ]; }else{ this.bModerate = Config.buildings.moderate['default']; } if (civ in Config.buildings.advanced){ this.bAdvanced = Config.buildings.advanced[civ]; }else{ this.bAdvanced = Config.buildings.advanced['default']; } if (civ in Config.buildings.fort){ this.bFort = Config.buildings.fort[civ]; }else{ this.bFort = Config.buildings.fort['default']; } for (var i in this.bAdvanced){ this.bAdvanced[i] = gameState.applyCiv(this.bAdvanced[i]); } for (var i in this.bFort){ this.bFort[i] = gameState.applyCiv(this.bFort[i]); } // TODO: figure out how to make this generic for (var i in this.attackManagers){ this.availableAttacks[i] = new this.attackManagers[i](gameState, this); } var enemies = gameState.getEnemyEntities(); var filter = Filters.byClassesOr(["CitizenSoldier", "Champion", "Hero", "Siege"]); this.enemySoldiers = enemies.filter(filter); // TODO: cope with diplomacy changes this.enemySoldiers.registerUpdates(); // each enemy watchers keeps a list of entity collections about the enemy it watches // It also keeps track of enemy armies, merging/splitting as needed this.enemyWatchers = {}; this.ennWatcherIndex = []; for (var i = 1; i <= 8; i++) if (PlayerID != i && gameState.isPlayerEnemy(i)) { this.enemyWatchers[i] = new enemyWatcher(gameState, i); this.ennWatcherIndex.push(i); this.defenceManager.enemyArmy[i] = []; } }; // picks the best template based on parameters and classes MilitaryAttackManager.prototype.findBestTrainableUnit = function(gameState, classes, parameters) { var units = gameState.findTrainableUnits(classes); if (units.length === 0) return undefined; units.sort(function(a, b) { //}) { var aDivParam = 0, bDivParam = 0; var aTopParam = 0, bTopParam = 0; for (var i in parameters) { var param = parameters[i]; if (param[0] == "base") { aTopParam = param[1]; bTopParam = param[1]; } if (param[0] == "strength") { aTopParam += getMaxStrength(a[1]) * param[1]; bTopParam += getMaxStrength(b[1]) * param[1]; } if (param[0] == "siegeStrength") { aTopParam += getMaxStrength(a[1], "Structure") * param[1]; bTopParam += getMaxStrength(b[1], "Structure") * param[1]; } if (param[0] == "speed") { aTopParam += a[1].walkSpeed() * param[1]; bTopParam += b[1].walkSpeed() * param[1]; } if (param[0] == "cost") { aDivParam += a[1].costSum() * param[1]; bDivParam += b[1].costSum() * param[1]; } if (param[0] == "canGather") { // checking against wood, could be anything else really. if (a[1].resourceGatherRates() && a[1].resourceGatherRates()["wood.tree"]) aTopParam *= param[1]; if (b[1].resourceGatherRates() && b[1].resourceGatherRates()["wood.tree"]) bTopParam *= param[1]; } } return -(aTopParam/(aDivParam+1)) + (bTopParam/(bDivParam+1)); }); return units[0][0]; }; // Deals with building fortresses and towers. // Currently build towers next to every useful dropsites. // TODO: Fortresses are placed randomly atm. MilitaryAttackManager.prototype.buildDefences = function(gameState, queues){ var workersNumber = gameState.getOwnEntitiesByRole("worker").filter(Filters.not(Filters.byHasMetadata(PlayerID,"plan"))).length; if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv('structures/{civ}_defense_tower')) + queues.defenceBuilding.totalLength() < gameState.getEntityLimits()["DefenseTower"] && queues.defenceBuilding.totalLength() < 4 && gameState.currentPhase() > 1 && queues.defenceBuilding.totalLength() < 3) { gameState.getOwnEntities().forEach(function(dropsiteEnt) { if (dropsiteEnt.resourceDropsiteTypes() && dropsiteEnt.getMetadata(PlayerID, "defenseTower") !== true && (dropsiteEnt.getMetadata(PlayerID, "resource-quantity-wood") > 400 || dropsiteEnt.getMetadata(PlayerID, "resource-quantity-stone") > 500 || dropsiteEnt.getMetadata(PlayerID, "resource-quantity-metal") > 500) ){ var position = dropsiteEnt.position(); if (position){ queues.defenceBuilding.addItem(new BuildingConstructionPlan(gameState, 'structures/{civ}_defense_tower', position)); } dropsiteEnt.setMetadata(PlayerID, "defenseTower", true); } }); } var numFortresses = 0; for (var i in this.bFort){ numFortresses += gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bFort[i])); } if (queues.defenceBuilding.totalLength() < 1 && (gameState.currentPhase() > 2 || gameState.isResearching("phase_city_generic"))) { if (workersNumber >= 80 && gameState.getTimeElapsed() > numFortresses * this.fortressLapseTime + this.fortressStartTime) { if (!this.fortressStartTime) this.fortressStartTime = gameState.getTimeElapsed(); queues.defenceBuilding.addItem(new BuildingConstructionPlan(gameState, this.bFort[0])); debug ("Building a fortress"); } } if (gameState.countEntitiesByType(gameState.applyCiv(this.bFort[i])) >= 1) { // let's add a siege building plan to the current attack plan if there is none currently. if (this.upcomingAttacks["CityAttack"].length !== 0) { var attack = this.upcomingAttacks["CityAttack"][0]; if (!attack.unitStat["Siege"]) { // no minsize as we don't want the plan to fail at the last minute though. var stat = { "priority" : 1.1, "minSize" : 0, "targetSize" : 4, "batchSize" : 2, "classes" : ["Siege"], "interests" : [ ["siegeStrength", 3], ["cost",1] ] ,"templates" : [] }; if (gameState.civ() == "cart" || gameState.civ() == "maur") stat["classes"] = ["Elephant"]; attack.addBuildOrder(gameState, "Siege", stat, true); } } } }; // Deals with constructing military buildings (barracks, stables…) // They are mostly defined by Config.js. This is unreliable since changes could be done easily. // TODO: We need to determine these dynamically. Also doesn't build fortresses since the above function does that. // TODO: building placement is bad. Choice of buildings is also fairly dumb. MilitaryAttackManager.prototype.constructTrainingBuildings = function(gameState, queues) { Engine.ProfileStart("Build buildings"); var workersNumber = gameState.getOwnEntitiesByRole("worker").filter(Filters.not(Filters.byHasMetadata(PlayerID, "plan"))).length; if (workersNumber > 30 && (gameState.currentPhase() > 1 || gameState.isResearching("phase_town_generic") || gameState.isResearching("phase_town_athens"))) { if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bModerate[0])) + queues.militaryBuilding.totalLength() < 1) { debug ("Trying to build barracks"); queues.militaryBuilding.addItem(new BuildingConstructionPlan(gameState, this.bModerate[0])); } } if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bModerate[0])) < 2 && workersNumber > 85) if (queues.militaryBuilding.totalLength() < 1) queues.militaryBuilding.addItem(new BuildingConstructionPlan(gameState, this.bModerate[0])); if (gameState.countEntitiesByType(gameState.applyCiv(this.bModerate[0])) === 2 && gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bModerate[0])) < 3 && workersNumber > 125) if (queues.militaryBuilding.totalLength() < 1) { queues.militaryBuilding.addItem(new BuildingConstructionPlan(gameState, this.bModerate[0])); if (gameState.civ() == "gaul" || gameState.civ() == "brit" || gameState.civ() == "iber") { queues.militaryBuilding.addItem(new BuildingConstructionPlan(gameState, this.bModerate[0])); queues.militaryBuilding.addItem(new BuildingConstructionPlan(gameState, this.bModerate[0])); } } //build advanced military buildings if (workersNumber >= 75 && gameState.currentPhase() > 2){ if (queues.militaryBuilding.totalLength() === 0){ var inConst = 0; for (var i in this.bAdvanced) inConst += gameState.countFoundationsWithType(gameState.applyCiv(this.bAdvanced[i])); if (inConst == 0 && this.bAdvanced && this.bAdvanced.length !== 0) { var i = Math.floor(Math.random() * this.bAdvanced.length); if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bAdvanced[i])) < 1){ queues.militaryBuilding.addItem(new BuildingConstructionPlan(gameState, this.bAdvanced[i])); } } } } if (gameState.civ() !== "gaul" && gameState.civ() !== "brit" && gameState.civ() !== "iber" && workersNumber > 130 && gameState.currentPhase() > 2) { var Const = 0; for (var i in this.bAdvanced) Const += gameState.countEntitiesByType(gameState.applyCiv(this.bAdvanced[i])); if (inConst == 1) { var i = Math.floor(Math.random() * this.bAdvanced.length); if (gameState.countEntitiesAndQueuedByType(gameState.applyCiv(this.bAdvanced[i])) < 1){ queues.militaryBuilding.addItem(new BuildingConstructionPlan(gameState, this.bAdvanced[i])); queues.militaryBuilding.addItem(new BuildingConstructionPlan(gameState, this.bAdvanced[i])); } } } Engine.ProfileStop(); }; // TODO: use pop(). Currently unused as this is too gameable. MilitaryAttackManager.prototype.garrisonAllFemales = function(gameState) { var buildings = gameState.getOwnEntities().filter(Filters.byCanGarrison()).toEntityArray(); var females = gameState.getOwnEntities().filter(Filters.byClass("Support")); var cache = {}; females.forEach( function (ent) { for (var i in buildings) { if (ent.position()) { var struct = buildings[i]; if (!cache[struct.id()]) cache[struct.id()] = 0; if (struct.garrisoned() && struct.garrisonMax() - struct.garrisoned().length - cache[struct.id()] > 0) { ent.garrison(struct); cache[struct.id()]++; break; } } } }); this.hasGarrisonedFemales = true; }; MilitaryAttackManager.prototype.ungarrisonAll = function(gameState) { this.hasGarrisonedFemales = false; var buildings = gameState.getOwnEntities().filter(Filters.byCanGarrison()).toEntityArray(); buildings.forEach( function (struct) { if (struct.garrisoned() && struct.garrisoned().length) struct.unloadAll(); }); }; MilitaryAttackManager.prototype.pausePlan = function(gameState, planName) { for (var attackType in this.upcomingAttacks) { for (var i in this.upcomingAttacks[attackType]) { var attack = this.upcomingAttacks[attackType][i]; if (attack.getName() == planName) attack.setPaused(gameState, true); } } for (var attackType in this.startedAttacks) { for (var i in this.startedAttacks[attackType]) { var attack = this.startedAttacks[attackType][i]; if (attack.getName() == planName) attack.setPaused(gameState, true); } } } MilitaryAttackManager.prototype.unpausePlan = function(gameState, planName) { for (var attackType in this.upcomingAttacks) { for (var i in this.upcomingAttacks[attackType]) { var attack = this.upcomingAttacks[attackType][i]; if (attack.getName() == planName) attack.setPaused(gameState, false); } } for (var attackType in this.startedAttacks) { for (var i in this.startedAttacks[attackType]) { var attack = this.startedAttacks[attackType][i]; if (attack.getName() == planName) attack.setPaused(gameState, false); } } } MilitaryAttackManager.prototype.pauseAllPlans = function(gameState) { for (var attackType in this.upcomingAttacks) { for (var i in this.upcomingAttacks[attackType]) { var attack = this.upcomingAttacks[attackType][i]; attack.setPaused(gameState, true); } } for (var attackType in this.startedAttacks) { for (var i in this.startedAttacks[attackType]) { var attack = this.startedAttacks[attackType][i]; attack.setPaused(gameState, true); } } } MilitaryAttackManager.prototype.unpauseAllPlans = function(gameState) { for (var attackType in this.upcomingAttacks) { for (var i in this.upcomingAttacks[attackType]) { var attack = this.upcomingAttacks[attackType][i]; attack.setPaused(gameState, false); } } for (var attackType in this.startedAttacks) { for (var i in this.startedAttacks[attackType]) { var attack = this.startedAttacks[attackType][i]; attack.setPaused(gameState, false); } } } MilitaryAttackManager.prototype.update = function(gameState, queues, events) { var self = this; Engine.ProfileStart("military update"); this.gameState = gameState; Engine.ProfileStart("Constructing military buildings and building defences"); this.constructTrainingBuildings(gameState, queues); if(gameState.getTimeElapsed() > this.defenceBuildingTime) this.buildDefences(gameState, queues); Engine.ProfileStop(); this.defenceManager.update(gameState, events, this); Engine.ProfileStart("Looping through attack plans"); // TODO: implement some form of check before starting a new attack plans. Sometimes it is not the priority. if (1) { for (var attackType in this.upcomingAttacks) { for (var i = 0;i < this.upcomingAttacks[attackType].length; ++i) { var attack = this.upcomingAttacks[attackType][i]; // okay so we'll get the support plan if (!attack.isStarted()) { var updateStep = attack.updatePreparation(gameState, this,events); // now we're gonna check if the preparation time is over if (updateStep === 1 || attack.isPaused() ) { // just chillin' } else if (updateStep === 0 || updateStep === 3) { debug ("Military Manager: " +attack.getType() +" plan " +attack.getName() +" aborted."); if (updateStep === 3) { this.attackPlansEncounteredWater = true; debug("No attack path found. Aborting."); } attack.Abort(gameState, this); this.upcomingAttacks[attackType].splice(i--,1); } else if (updateStep === 2) { var chatText = "I am launching an attack against " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; if (Math.random() < 0.2) chatText = "Attacking " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; else if (Math.random() < 0.3) chatText = "I have sent an army against " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; else if (Math.random() < 0.3) chatText = "I'm starting an attack against " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; gameState.ai.chatTeam(chatText); debug ("Military Manager: Starting " +attack.getType() +" plan " +attack.getName()); attack.StartAttack(gameState,this); this.startedAttacks[attackType].push(attack); this.upcomingAttacks[attackType].splice(i--,1); } } else { var chatText = "I am launching an attack against " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; if (Math.random() < 0.2) chatText = "Attacking " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; else if (Math.random() < 0.3) chatText = "I have sent an army against " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; else if (Math.random() < 0.3) chatText = "I'm starting an attack against " + gameState.sharedScript.playersData[attack.targetPlayer].name + "."; gameState.ai.chatTeam(chatText); debug ("Military Manager: Starting " +attack.getType() +" plan " +attack.getName()); this.startedAttacks[attackType].push(attack); this.upcomingAttacks[attackType].splice(i--,1); } } } } for (var attackType in this.startedAttacks) { for (var i = 0; i < this.startedAttacks[attackType].length; ++i) { var attack = this.startedAttacks[attackType][i]; // okay so then we'll update the attack. if (!attack.isPaused()) { var remaining = attack.update(gameState,this,events); if (remaining == 0 || remaining == undefined) { debug ("Military Manager: " +attack.getType() +" plan " +attack.getName() +" is now finished."); attack.Abort(gameState); this.startedAttacks[attackType].splice(i--,1); } } } } // Note: these indications of "rush" are currently unused. if (gameState.ai.strategy === "rush" && this.startedAttacks["CityAttack"].length !== 0) { // and then we revert. gameState.ai.strategy = "normal"; Config.Economy.femaleRatio = 0.4; gameState.ai.modules.economy.targetNumWorkers = Math.max(Math.floor(gameState.getPopulationMax()*0.55), 1); } else if (gameState.ai.strategy === "rush" && this.upcomingAttacks["CityAttack"].length === 0) { Lalala = new CityAttack(gameState, this,this.TotalAttackNumber, -1, "rush") this.TotalAttackNumber++; this.upcomingAttacks["CityAttack"].push(Lalala); debug ("Starting a little something"); } else if (gameState.ai.strategy !== "rush") { // creating plans after updating because an aborted plan might be reused in that case. if (gameState.countEntitiesByType(gameState.applyCiv(this.bModerate[0])) >= 1 && !this.attackPlansEncounteredWater && gameState.getTimeElapsed() > this.attackPlansStartTime) { if (gameState.countEntitiesByType(gameState.applyCiv("structures/{civ}_dock")) === 0 && gameState.ai.waterMap) { // wait till we get a dock. } else { // basically only the first plan, really. if (this.upcomingAttacks["CityAttack"].length == 0 && (gameState.getTimeElapsed() < 12*60000 || Config.difficulty < 1)) { var Lalala = new CityAttack(gameState, this,this.TotalAttackNumber, -1); if (Lalala.failed) { this.attackPlansEncounteredWater = true; // hack } else { debug ("Military Manager: Creating the plan " +this.TotalAttackNumber); this.TotalAttackNumber++; this.upcomingAttacks["CityAttack"].push(Lalala); } } else if (this.upcomingAttacks["CityAttack"].length == 0 && Config.difficulty !== 0) { var Lalala = new CityAttack(gameState, this,this.TotalAttackNumber, -1, "superSized"); if (Lalala.failed) { this.attackPlansEncounteredWater = true; // hack } else { debug ("Military Manager: Creating the super sized plan " +this.TotalAttackNumber); this.TotalAttackNumber++; this.upcomingAttacks["CityAttack"].push(Lalala); } } } } } /* // very old relic. This should be reimplemented someday so the code stays here. if (this.HarassRaiding && this.preparingRaidNumber + this.startedRaidNumber < 1 && gameState.getTimeElapsed() < 780000) { var Lalala = new CityAttack(gameState, this,this.totalStartedAttackNumber, -1, "harass_raid"); if (!Lalala.createSupportPlans(gameState, this, )) { debug ("Military Manager: harrassing plan not a valid option"); this.HarassRaiding = false; } else { debug ("Military Manager: Creating the harass raid plan " +this.totalStartedAttackNumber); this.totalStartedAttackNumber++; this.preparingRaidNumber++; this.currentAttacks.push(Lalala); } } */ Engine.ProfileStop(); Engine.ProfileStop(); };