var AEGIS = function(m) { /* This is an attack plan (despite the name, it's a relic of older times). * It deals with everything in an attack, from picking a target to picking a path to it * To making sure units rae built, and pushing elements to the queue manager otherwise * It also handles the actual attack, though much work is needed on that. * These should be extremely flexible with only minimal work. * There is a basic support for naval expeditions here. */ m.CityAttack = function CityAttack(gameState, HQ, Config, uniqueID, targetEnemy, type , targetFinder) { this.Config = Config; //This is the list of IDs of the units in the plan this.idList=[]; this.state = "unexecuted"; this.targetPlayer = targetEnemy; if (this.targetPlayer === -1 || this.targetPlayer === undefined) { // let's find our prefered target, basically counting our enemies units. var enemyCount = {}; for (var i = 1; i <=8; i++) enemyCount[i] = 0; gameState.getEntities().forEach(function(ent) { if (gameState.isEntityEnemy(ent) && ent.owner() !== 0) { enemyCount[ent.owner()]++; } }); var max = 0; for (var i in enemyCount) if (enemyCount[i] > max && +i !== PlayerID) { this.targetPlayer = +i; max = enemyCount[i]; } } if (this.targetPlayer === undefined || this.targetPlayer === -1) { this.failed = true; return false; } var CCs = gameState.getOwnEntities().filter(API3.Filters.byClass("CivCentre")); if (CCs.length === 0) { this.failed = true; return false; } m.debug ("Target (" + PlayerID +") = " +this.targetPlayer); this.targetFinder = targetFinder || this.defaultTargetFinder; this.type = type || "normal"; this.name = uniqueID; this.healthRecord = []; this.timeOfPlanStart = gameState.getTimeElapsed(); // we get the time at which we decided to start the attack this.maxPreparationTime = 210*1000; // in this case we want to have the attack ready by the 13th minute. Countdown. Minimum 2 minutes. if (type !== "superSized" && this.Config.difficulty >= 1) this.maxPreparationTime = 780000 - gameState.getTimeElapsed() < 120000 ? 120000 : 780000 - gameState.getTimeElapsed(); this.pausingStart = 0; this.totalPausingTime = 0; this.paused = false; this.onArrivalReaction = "proceedOnTargets"; // priority is relative. If all are 0, the only relevant criteria is "currentsize/targetsize". // if not, this is a "bonus". The higher the priority, the faster this unit will get built. // Should really be clamped to [0.1-1.5] (assuming 1 is default/the norm) // Eg: if all are priority 1, and the siege is 0.5, the siege units will get built // only once every other category is at least 50% of its target size. // note: siege build order is currently added by the military manager if a fortress is there. this.unitStat = {}; this.unitStat["RangedInfantry"] = { "priority" : 1, "minSize" : 4, "targetSize" : 10, "batchSize" : 5, "classes" : ["Infantry","Ranged"], "interests" : [ ["canGather", 2], ["strength",2], ["cost",1] ], "templates" : [] }; this.unitStat["MeleeInfantry"] = { "priority" : 1, "minSize" : 4, "targetSize" : 10, "batchSize" : 5, "classes" : ["Infantry","Melee"], "interests" : [ ["canGather", 2], ["strength",2], ["cost",1] ], "templates" : [] }; this.unitStat["MeleeCavalry"] = { "priority" : 1, "minSize" : 3, "targetSize" : 8 , "batchSize" : 3, "classes" : ["Cavalry","Melee"], "interests" : [ ["strength",2], ["cost",1] ], "templates" : [] }; this.unitStat["RangedCavalry"] = { "priority" : 1, "minSize" : 3, "targetSize" : 8 , "batchSize" : 3, "classes" : ["Cavalry","Ranged"], "interests" : [ ["strength",2], ["cost",1] ], "templates" : [] }; var priority = 50; if (type === "rush") { // we have 3 minutes to train infantry. delete this.unitStat["RangedInfantry"]; delete this.unitStat["MeleeInfantry"]; delete this.unitStat["MeleeCavalry"]; delete this.unitStat["RangedCavalry"]; this.unitStat["Infantry"] = { "priority" : 1, "minSize" : 10, "targetSize" : 30, "batchSize" : 1, "classes" : ["Infantry"], "interests" : [ ["strength",1], ["cost",1] ], "templates" : [] }; this.maxPreparationTime = 150*1000; priority = 120; } else if (type === "superSized") { // our first attack has started worst case at the 14th minute, we want to attack another time by the 21th minute, so we rock 6.5 minutes this.maxPreparationTime = 480000; // basically we want a mix of citizen soldiers so our barracks have a purpose, and champion units. this.unitStat["RangedInfantry"] = { "priority" : 1, "minSize" : 5, "targetSize" : 20, "batchSize" : 5, "classes" : ["Infantry","Ranged", "CitizenSoldier"], "interests" : [["strength",3], ["cost",1] ], "templates" : [] }; this.unitStat["MeleeInfantry"] = { "priority" : 1, "minSize" : 5, "targetSize" : 20, "batchSize" : 5, "classes" : ["Infantry","Melee", "CitizenSoldier" ], "interests" : [ ["strength",3], ["cost",1] ], "templates" : [] }; this.unitStat["ChampRangedInfantry"] = { "priority" : 1, "minSize" : 5, "targetSize" : 15, "batchSize" : 5, "classes" : ["Infantry","Ranged", "Champion"], "interests" : [["strength",3], ["cost",1] ], "templates" : [] }; this.unitStat["ChampMeleeInfantry"] = { "priority" : 1, "minSize" : 5, "targetSize" : 15, "batchSize" : 5, "classes" : ["Infantry","Melee", "Champion" ], "interests" : [ ["strength",3], ["cost",1] ], "templates" : [] }; this.unitStat["MeleeCavalry"] = { "priority" : 1, "minSize" : 3, "targetSize" : 18, "batchSize" : 3, "classes" : ["Cavalry","Melee", "CitizenSoldier" ], "interests" : [ ["strength",2], ["cost",1] ], "templates" : [] }; this.unitStat["RangedCavalry"] = { "priority" : 1, "minSize" : 3, "targetSize" : 18 , "batchSize" : 3, "classes" : ["Cavalry","Ranged", "CitizenSoldier"], "interests" : [ ["strength",2], ["cost",1] ], "templates" : [] }; this.unitStat["ChampMeleeInfantry"] = { "priority" : 0.8, "minSize" : 3, "targetSize" : 12, "batchSize" : 3, "classes" : ["Infantry","Melee", "Champion" ], "interests" : [ ["strength",3], ["cost",1] ], "templates" : [] }; this.unitStat["ChampMeleeCavalry"] = { "priority" : 0.8, "minSize" : 3, "targetSize" : 12, "batchSize" : 3, "classes" : ["Cavalry","Melee", "Champion" ], "interests" : [ ["strength",2], ["cost",1] ], "templates" : [] }; priority = 70; } // TODO: there should probably be one queue per type of training building gameState.ai.queueManager.addQueue("plan_" + this.name, priority); this.queue = gameState.ai.queues["plan_" + this.name]; gameState.ai.queueManager.addQueue("plan_" + this.name +"_champ", priority); this.queueChamp = gameState.ai.queues["plan_" + this.name +"_champ"]; /* this.unitStat["Siege"]["filter"] = function (ent) { var strength = [ent.attackStrengths("Melee")["crush"],ent.attackStrengths("Ranged")["crush"]]; return (strength[0] > 15 || strength[1] > 15); };*/ var filter = API3.Filters.and(API3.Filters.byMetadata(PlayerID, "plan",this.name),API3.Filters.byOwner(PlayerID)); this.unitCollection = gameState.getOwnEntities().filter(filter); this.unitCollection.registerUpdates(); this.unitCollection.length; this.unit = {}; // each array is [ratio, [associated classes], associated EntityColl, associated unitStat, name ] this.buildOrder = []; // defining the entity collections. Will look for units I own, that are part of this plan. // Also defining the buildOrders. for (var unitCat in this.unitStat) { var cat = unitCat; var Unit = this.unitStat[cat]; filter = API3.Filters.and(API3.Filters.byClassesAnd(Unit["classes"]),API3.Filters.and(API3.Filters.byMetadata(PlayerID, "plan",this.name),API3.Filters.byOwner(PlayerID))); this.unit[cat] = gameState.getOwnEntities().filter(filter); this.unit[cat].registerUpdates(); this.unit[cat].length; this.buildOrder.push([0, Unit["classes"], this.unit[cat], Unit, cat]); } /*if (gameState.getTimeElapsed() > 900000) // 15 minutes { this.unitStat.Cavalry.Ranged["minSize"] = 5; this.unitStat.Cavalry.Melee["minSize"] = 5; this.unitStat.Infantry.Ranged["minSize"] = 10; this.unitStat.Infantry.Melee["minSize"] = 10; this.unitStat.Cavalry.Ranged["targetSize"] = 10; this.unitStat.Cavalry.Melee["targetSize"] = 10; this.unitStat.Infantry.Ranged["targetSize"] = 20; this.unitStat.Infantry.Melee["targetSize"] = 20; this.unitStat.Siege["targetSize"] = 5; this.unitStat.Siege["minSize"] = 2; } else { this.maxPreparationTime = 180000; }*/ // todo: REACTIVATE (in all caps) if (type === "harass_raid" && 0 == 1) { this.targetFinder = this.raidingTargetFinder; this.onArrivalReaction = "huntVillagers"; this.type = "harass_raid"; // This is a Cavalry raid against villagers. A Cavalry Swordsman has a bonus against these. Only build these this.maxPreparationTime = 180000; // 3 minutes. if (gameState.playerData.civ === "hele") // hellenes have an ealry Cavalry Swordsman { this.unitCount.Cavalry.Melee = { "subCat" : ["Swordsman"] , "usesSubcategories" : true, "Swordsman" : undefined, "priority" : 1, "currentAmount" : 0, "minimalAmount" : 0, "preferedAmount" : 0 }; this.unitCount.Cavalry.Melee.Swordsman = { "priority" : 1, "currentAmount" : 0, "minimalAmount" : 4, "preferedAmount" : 7, "fallback" : "abort" }; } else { this.unitCount.Cavalry.Melee = { "subCat" : undefined , "usesSubcategories" : false, "priority" : 1, "currentAmount" : 0, "minimalAmount" : 4, "preferedAmount" : 7 }; } this.unitCount.Cavalry.Ranged["minimalAmount"] = 0; this.unitCount.Cavalry.Ranged["preferedAmount"] = 0; this.unitCount.Infantry.Ranged["minimalAmount"] = 0; this.unitCount.Infantry.Ranged["preferedAmount"] = 0; this.unitCount.Infantry.Melee["minimalAmount"] = 0; this.unitCount.Infantry.Melee["preferedAmount"] = 0; this.unitCount.Siege["preferedAmount"] = 0; } this.anyNotMinimal = true; // used for support plans var myFortresses = gameState.getOwnTrainingFacilities().filter(API3.Filters.byClass("GarrisonFortress")); if (myFortresses.length !== 0) { // make this our rallypoint for (var i in myFortresses._entities) { if (myFortresses._entities[i].position()) { this.rallyPoint = myFortresses._entities[i].position(); break; } } } else { if(gameState.ai.pathsToMe.length > 1) var position = [(gameState.ai.pathsToMe[0][0]+gameState.ai.pathsToMe[1][0])/2.0,(gameState.ai.pathsToMe[0][1]+gameState.ai.pathsToMe[1][1])/2.0]; else if (gameState.ai.pathsToMe.length !== 0) var position = [gameState.ai.pathsToMe[0][0],gameState.ai.pathsToMe[0][1]]; else var position = [-1,-1]; if (gameState.ai.accessibility.getAccessValue(position) !== gameState.ai.myIndex) var position = [-1,-1]; var nearestCCArray = CCs.filterNearest(position, 1).toEntityArray(); var CCpos = nearestCCArray[0].position(); this.rallyPoint = [0,0]; if (position[0] !== -1) { this.rallyPoint[0] = position[0]; this.rallyPoint[1] = position[1]; } else { this.rallyPoint[0] = CCpos[0]; this.rallyPoint[1] = CCpos[1]; } if (type == 'harass_raid') { this.rallyPoint[0] = (position[0]*3.9 + 0.1 * CCpos[0]) / 4.0; this.rallyPoint[1] = (position[1]*3.9 + 0.1 * CCpos[1]) / 4.0; } } // some variables for during the attack this.position5TurnsAgo = [0,0]; this.lastPosition = [0,0]; this.position = [0,0]; this.threatList = []; // sounds so FBI this.tactics = undefined; this.assignUnits(gameState); //m.debug ("Before"); //Engine.DumpHeap(); // get a good path to an estimated target. this.pathFinder = new API3.aStarPath(gameState,false,false, this.targetPlayer); //Engine.DumpImage("widthmap.png", this.pathFinder.widthMap, this.pathFinder.width,this.pathFinder.height,255); this.pathWidth = 6; // prefer a path far from entities. This will avoid units getting stuck in trees and also results in less straight paths. this.pathSampling = 2; this.onBoat = false; // tells us if our units are loaded on boats. this.needsShip = false; //m.debug ("after"); //Engine.DumpHeap(); return true; }; m.CityAttack.prototype.getName = function(){ return this.name; }; m.CityAttack.prototype.getType = function(){ return this.type; }; // Returns true if the attack can be executed at the current time // Basically his checks we have enough units. // We run a count of our units. m.CityAttack.prototype.canStart = function(gameState){ for (var unitCat in this.unitStat) { var Unit = this.unitStat[unitCat]; if (this.unit[unitCat].length < Unit["minSize"]) return false; } return true; // TODO: check if our target is valid and a few other stuffs (good moment to attack?) }; m.CityAttack.prototype.isStarted = function(){ if ((this.state !== "unexecuted")) m.debug ("Attack plan already started"); return !(this.state == "unexecuted"); }; m.CityAttack.prototype.isPaused = function(){ return this.paused; }; m.CityAttack.prototype.setPaused = function(gameState, boolValue){ if (!this.paused && boolValue === true) { this.pausingStart = gameState.getTimeElapsed(); this.paused = true; m.debug ("Pausing attack plan " +this.name); } else if (this.paused && boolValue === false) { this.totalPausingTime += gameState.getTimeElapsed() - this.pausingStart; this.paused = false; m.debug ("Unpausing attack plan " +this.name); } }; m.CityAttack.prototype.mustStart = function(gameState){ if (this.isPaused() || this.path === undefined) return false; var MaxReachedEverywhere = true; for (var unitCat in this.unitStat) { var Unit = this.unitStat[unitCat]; if (this.unit[unitCat].length < Unit["targetSize"]) { MaxReachedEverywhere = false; } } if (MaxReachedEverywhere || (gameState.getPopulationMax() - gameState.getPopulation() < 10 && this.canStart(gameState))) return true; return (this.maxPreparationTime + this.timeOfPlanStart + this.totalPausingTime < gameState.getTimeElapsed()); }; // Adds a build order. If resetQueue is true, this will reset the queue. m.CityAttack.prototype.addBuildOrder = function(gameState, name, unitStats, resetQueue) { if (!this.isStarted()) { m.debug ("Adding a build order for " + name); // no minsize as we don't want the plan to fail at the last minute though. this.unitStat[name] = unitStats; var Unit = this.unitStat[name]; var filter = API3.Filters.and(API3.Filters.byClassesAnd(Unit["classes"]),API3.Filters.and(API3.Filters.byMetadata(PlayerID, "plan",this.name),API3.Filters.byOwner(PlayerID))); this.unit[name] = gameState.getOwnEntities().filter(filter); this.unit[name].registerUpdates(); this.buildOrder.push([0, Unit["classes"], this.unit[name], Unit, name]); if (resetQueue) { this.queue.empty(); this.queueChamp.empty(); } } }; // Three returns possible: 1 is "keep going", 0 is "failed plan", 2 is "start" // 3 is a special case: no valid path returned. Right now I stop attacking alltogether. m.CityAttack.prototype.updatePreparation = function(gameState, HQ,events) { var self = this; if (this.path == undefined || this.target == undefined || this.path === "toBeContinued") { // find our target if (this.target == undefined) { var targets = this.targetFinder(gameState, HQ); if (targets.length === 0) targets = this.defaultTargetFinder(gameState, HQ); if (targets.length !== 0) { m.debug ("Aiming for " + targets); // picking a target var maxDist = -1; var index = 0; for (var i in targets._entities) { // we're sure it has a position has TargetFinder already checks that. var dist = API3.SquareVectorDistance(targets._entities[i].position(), this.rallyPoint); if (dist < maxDist || maxDist === -1) { maxDist = dist; index = i; } } this.target = targets._entities[index]; this.targetPos = this.target.position(); } } // when we have a target, we path to it. // I'd like a good high width sampling first. // Thus I will not do everything at once. // It will probably carry over a few turns but that's no issue. if (this.path === undefined) this.path = this.pathFinder.getPath(this.rallyPoint,this.targetPos, this.pathSampling, this.pathWidth,175);//,gameState); else if (this.path === "toBeContinued") this.path = this.pathFinder.continuePath();//gameState); if (this.path === undefined) { if (this.pathWidth == 6) { this.pathWidth = 2; delete this.path; } else { delete this.pathFinder; return 3; // no path. } } else if (this.path === "toBeContinued") { // carry on. } else if (this.path[1] === true && this.pathWidth == 2) { // okay so we need a ship. // Basically we'll add it as a new class to train compulsorily, and we'll recompute our path. if (!gameState.ai.HQ.waterMap) { m.debug ("This is actually a water map."); gameState.ai.HQ.waterMap = true; return 0; } m.debug ("We need a ship."); this.needsShip = true; this.pathWidth = 3; this.pathSampling = 3; this.path = this.path[0].reverse(); delete this.pathFinder; // Change the rally point to something useful (should avoid rams getting stuck in houses in my territory, which is dumb.) for (var i = 0; i < this.path.length; ++i) { // my pathfinder returns arrays in arrays in arrays. var waypointPos = this.path[i][0]; var territory = m.createTerritoryMap(gameState); if (territory.getOwner(waypointPos) !== PlayerID || this.path[i][1] === true) { // if we're suddenly out of our territory or this is the point where we change transportation method. if (i !== 0) this.rallyPoint = this.path[i-1][0]; else this.rallyPoint = this.path[0][0]; if (i >= 1) this.path.splice(0,i-1); break; } } } else if (this.path[1] === true && this.pathWidth == 6) { // retry with a smaller pathwidth: this.pathWidth = 2; delete this.path; } else { this.path = this.path[0].reverse(); delete this.pathFinder; // Change the rally point to something useful (should avoid rams getting stuck in houses in my territory, which is dumb.) for (var i = 0; i < this.path.length; ++i) { // my pathfinder returns arrays in arrays in arrays. var waypointPos = this.path[i][0]; var territory = m.createTerritoryMap(gameState); if (territory.getOwner(waypointPos) !== PlayerID || this.path[i][1] === true) { // if we're suddenly out of our territory or this is the point where we change transportation method. if (i !== 0) { this.rallyPoint = this.path[i-1][0]; } else this.rallyPoint = this.path[0][0]; if (i >= 1) this.path.splice(0,i-1); break; } } } } Engine.ProfileStart("Update Preparation"); // special case: if we're reached max pop, and we can start the plan, start it. if ((gameState.getPopulationMax() - gameState.getPopulation() < 10) && this.canStart()) { this.assignUnits(gameState); this.queue.empty(); this.queueChamp.empty(); if ( gameState.ai.playedTurn % 5 == 0) this.AllToRallyPoint(gameState, true); } else if (this.mustStart(gameState) && (gameState.countOwnQueuedEntitiesWithMetadata("plan", +this.name) > 0)) { // keep on while the units finish being trained, then we'll start this.assignUnits(gameState); this.queue.empty(); this.queueChamp.empty(); if (gameState.ai.playedTurn % 5 == 0) { this.AllToRallyPoint(gameState, true); // TODO: should use this time to let gatherers deposit resources. } Engine.ProfileStop(); return 1; } else if (!this.mustStart(gameState)) { // We still have time left to recruit units and do stuffs. // let's sort by training advancement, ie 'current size / target size' // count the number of queued units too. // substract priority. this.buildOrder.sort(function (a,b) { //}) { var aQueued = gameState.countOwnQueuedEntitiesWithMetadata("special","Plan_"+self.name+"_"+a[4]); aQueued += self.queue.countQueuedUnitsWithMetadata("special","Plan_"+self.name+"_"+a[4]); aQueued += self.queueChamp.countQueuedUnitsWithMetadata("special","Plan_"+self.name+"_"+a[4]); a[0] = (a[2].length + aQueued)/a[3]["targetSize"]; var bQueued = gameState.countOwnQueuedEntitiesWithMetadata("special","Plan_"+self.name+"_"+b[4]); bQueued += self.queue.countQueuedUnitsWithMetadata("special","Plan_"+self.name+"_"+b[4]); bQueued += self.queueChamp.countQueuedUnitsWithMetadata("special","Plan_"+self.name+"_"+b[4]); b[0] = (b[2].length + bQueued)/b[3]["targetSize"]; a[0] -= a[3]["priority"]; b[0] -= b[3]["priority"]; return (a[0]) - (b[0]); }); this.assignUnits(gameState); if (gameState.ai.playedTurn % 5 == 0) { this.AllToRallyPoint(gameState, false); this.unitCollection.setStance("standground"); // make sure units won't disperse out of control } Engine.ProfileStart("Creating units."); // gets the number in training of the same kind as the first one. var specialData = "Plan_"+this.name+"_"+this.buildOrder[0][4]; var inTraining = gameState.countOwnQueuedEntitiesWithMetadata("special",specialData); var queued = this.queue.countQueuedUnitsWithMetadata("special",specialData) + this.queueChamp.countQueuedUnitsWithMetadata("special",specialData) if (queued + inTraining + this.buildOrder[0][2].length <= this.buildOrder[0][3]["targetSize"]) { // find the actual queue we want var queue = this.queue; if (this.buildOrder[0][3]["classes"].indexOf("Champion") !== -1) queue = this.queueChamp; if (this.buildOrder[0][0] < 1 && queue.length() <= 5) { var template = HQ.findBestTrainableSoldier(gameState, this.buildOrder[0][1], this.buildOrder[0][3]["interests"] ); //m.debug ("tried " + uneval(this.buildOrder[0][1]) +", and " + template); // HACK (TODO replace) : if we have no trainable template... Then we'll simply remove the buildOrder, effectively removing the unit from the plan. if (template === undefined) { // TODO: this is a complete hack. delete this.unitStat[this.buildOrder[0][4]]; // deleting the associated unitstat. this.buildOrder.splice(0,1); } else { var max = this.buildOrder[0][3]["batchSize"]; // TODO: this should be plan dependant. if (gameState.getTimeElapsed() > 1800000) max *= 2; if (gameState.getTemplate(template).hasClass("CitizenSoldier")) queue.addItem( new m.TrainingPlan(gameState,template, { "role" : "worker", "plan" : this.name, "special" : specialData, "base" : 1 }, this.buildOrder[0][3]["batchSize"],max ) ); else queue.addItem( new m.TrainingPlan(gameState,template, { "role" : "attack", "plan" : this.name, "special" : specialData, "base" : 1 }, this.buildOrder[0][3]["batchSize"],max ) ); } } } /* if (!this.startedPathing && this.path === undefined) { // find our target var targets = this.targetFinder(gameState, HQ); if (targets.length === 0){ targets = this.defaultTargetFinder(gameState, HQ); } if (targets.length) { this.targetPos = undefined; var count = 0; while (!this.targetPos){ var rand = Math.floor((Math.random()*targets.length)); var target = targets.toEntityArray()[rand]; this.targetPos = target.position(); count++; if (count > 1000){ m.debug("No target with a valid position found"); return false; } } this.startedPathing = true; // Start pathfinding using the optimized version, with a minimal sampling of 2 this.pathFinder.getPath(this.rallyPoint,this.targetPos, false, 2, gameState); } } else if (this.startedPathing) { var path = this.pathFinder.continuePath(gameState); if (path !== "toBeContinued") { this.startedPathing = false; this.path = path; m.debug("Pathing ended"); } } */ Engine.ProfileStop(); Engine.ProfileStop(); // can happen for now if (this.buildOrder.length === 0) { m.debug ("Ending plan: no build orders"); return 0; // will abort the plan, should return something else } return 1; } this.unitCollection.forEach(function (entity) { entity.setMetadata(PlayerID, "role","attack"); }); Engine.ProfileStop(); // if we're here, it means we must start (and have no units in training left). // if we can, do, else, abort. if (this.canStart(gameState)) return 2; else return 0; return 0; }; m.CityAttack.prototype.assignUnits = function(gameState){ var self = this; // TODO: assign myself units that fit only, right now I'm getting anything. // Assign all no-roles that fit (after a plan aborts, for example). var NoRole = gameState.getOwnEntitiesByRole(undefined); if (this.type === "rush") NoRole = gameState.getOwnEntitiesByRole("worker"); NoRole.forEach(function(ent) { if (ent.hasClass("Unit") && ent.attackTypes() !== undefined) { if (ent.hasClasses(["CitizenSoldier", "Infantry"])) ent.setMetadata(PlayerID, "role", "worker"); else ent.setMetadata(PlayerID, "role", "attack"); ent.setMetadata(PlayerID, "plan", self.name); } }); }; // this sends a unit by ID back to the "rally point" m.CityAttack.prototype.ToRallyPoint = function(gameState,id) { // Move back to nearest rallypoint gameState.getEntityById(id).move(this.rallyPoint[0],this.rallyPoint[1]); } // this sends all units back to the "rally point" by entity collections. // It doesn't disturb ones that could be currently defending, even if the plan is not (yet) paused. m.CityAttack.prototype.AllToRallyPoint = function(gameState, evenWorkers) { var self = this; if (evenWorkers) { for (var unitCat in this.unit) { this.unit[unitCat].forEach(function (ent) { if (ent.getMetadata(PlayerID, "role") != "defence") { ent.setMetadata(PlayerID,"role", "attack"); ent.move(self.rallyPoint[0],self.rallyPoint[1]); } }); } } else { for (var unitCat in this.unit) { this.unit[unitCat].forEach(function (ent) { if (ent.getMetadata(PlayerID, "role") != "worker" && ent.getMetadata(PlayerID, "role") != "defence") ent.move(self.rallyPoint[0],self.rallyPoint[1]); }); } } } // Default target finder aims for conquest critical targets m.CityAttack.prototype.defaultTargetFinder = function(gameState, HQ){ var targets = undefined; targets = HQ.enemyWatchers[this.targetPlayer].getEnemyBuildings(gameState, "CivCentre",true); if (targets.length == 0) { targets = HQ.enemyWatchers[this.targetPlayer].getEnemyBuildings(gameState, "ConquestCritical"); } // If there's nothing, attack anything else that's less critical if (targets.length == 0) { targets = HQ.enemyWatchers[this.targetPlayer].getEnemyBuildings(gameState, "Town",true); } if (targets.length == 0) { targets = HQ.enemyWatchers[this.targetPlayer].getEnemyBuildings(gameState, "Village",true); } // no buildings, attack anything conquest critical, even units (it's assuming it won't move). if (targets.length == 0) { targets = gameState.getEnemyEntities().filter(API3.Filters.and( API3.Filters.byOwner(this.targetPlayer),API3.Filters.byClass("ConquestCritical"))); } return targets; }; // tupdate m.CityAttack.prototype.raidingTargetFinder = function(gameState, HQ, Target){ var targets = undefined; if (Target == "villager") { // let's aim for any resource dropsite. We assume villagers are in the neighborhood (note: the human player could certainly troll us... small (scouting) TODO here.) targets = gameState.entities.filter(function(ent) { return (ent.hasClass("Structure") && ent.resourceDropsiteTypes() !== undefined && !ent.hasClass("CivCentre") && ent.owner() === this.targetPlayer && ent.position()); }); if (targets.length == 0) { targets = gameState.entities.filter(function(ent) { return (ent.hasClass("CivCentre") && ent.resourceDropsiteTypes() !== undefined && ent.owner() === this.targetPlayer && ent.position()); }); } if (targets.length == 0) { // if we're here, it means they also don't have no CC... So I'll just take any building at this point. targets = gameState.entities.filter(function(ent) { return (ent.hasClass("Structure") && ent.owner() === this.targetPlayer && ent.position()); }); } return targets; } else { return this.defaultTargetFinder(gameState, HQ); } }; // Executes the attack plan, after this is executed the update function will be run every turn // If we're here, it's because we have in our IDlist enough units. // now the IDlist units are treated turn by turn m.CityAttack.prototype.StartAttack = function(gameState, HQ){ // check we have a target and a path. if (this.targetPos && this.path !== undefined) { // erase our queue. This will stop any leftover unit from being trained. gameState.ai.queueManager.removeQueue("plan_" + this.name); gameState.ai.queueManager.removeQueue("plan_" + this.name + "_champ"); var curPos = this.unitCollection.getCentrePosition(); this.unitCollection.forEach(function(ent) { ent.setMetadata(PlayerID, "subrole", "walking"); ent.setMetadata(PlayerID, "role", "attack") ;}); // optimize our collection now. this.unitCollection.freeze(); this.unitCollection.allowQuickIter(); this.unitCollection.move(this.path[0][0][0], this.path[0][0][1]); this.unitCollection.setStance("aggressive"); this.unitCollection.filter(API3.Filters.byClass("Siege")).setStance("defensive"); this.state = "walking"; } else { gameState.ai.gameFinished = true; m.debug ("I do not have any target. So I'll just assume I won the game."); return false; } return true; }; // Runs every turn after the attack is executed m.CityAttack.prototype.update = function(gameState, HQ, events){ var self = this; Engine.ProfileStart("Update Attack"); // we're marching towards the target // Check for attacked units in our band. var bool_attacked = false; // raids don't care about attacks much if (this.unitCollection.length === 0) { Engine.ProfileStop(); return 0; } this.position = this.unitCollection.getCentrePosition(); var IDs = this.unitCollection.toIdArray(); // this actually doesn't do anything right now. if (this.state === "walking") { var attackedNB = 0; var toProcess = {}; var armyToProcess = {}; // Let's check if any of our unit has been attacked. In case yes, we'll determine if we're simply off against an enemy army, a lone unit/builing // or if we reached the enemy base. Different plans may react differently. var attackedEvents = events["Attacked"]; for (var key in attackedEvents) { var e = attackedEvents[key]; if (IDs.indexOf(e.target) !== -1) { var attacker = gameState.getEntityById(e.attacker); var ourUnit = gameState.getEntityById(e.target); if (attacker && attacker.position() && attacker.hasClass("Unit") && attacker.owner() != 0 && attacker.owner() != PlayerID) { var territoryMap = m.createTerritoryMap(gameState); if ( +territoryMap.point(attacker.position()) - 64 === +this.targetPlayer) { attackedNB++; } //if (HQ.enemyWatchers[attacker.owner()]) { //toProcess[attacker.id()] = attacker; //var armyID = HQ.enemyWatchers[attacker.owner()].getArmyFromMember(attacker.id()); //armyToProcess[armyID[0]] = armyID[1]; //} } // if we're being attacked by a building, flee. if (attacker && ourUnit && attacker.hasClass("Structure")) { ourUnit.flee(attacker); } } } if (attackedNB > 4) { m.debug ("Attack Plan " +this.type +" " +this.name +" has arrived to destination."); // we must assume we've arrived at the end of the trail. this.state = "arrived"; } /* }&& this.type !== "harass_raid"){ // walking toward the target var sumAttackerPos = [0,0]; var numAttackers = 0; // let's check if one of our unit is not under attack, by any chance. for (var key in events){ var e = events[key]; if (e.type === "Attacked" && e.msg){ if (this.unitCollection.toIdArray().indexOf(e.msg.target) !== -1){ var attacker = HeadQuarters.entity(e.msg.attacker); if (attacker && attacker.position()){ sumAttackerPos[0] += attacker.position()[0]; sumAttackerPos[1] += attacker.position()[1]; numAttackers += 1; bool_attacked = true; // todo: differentiate depending on attacker type... If it's a ship, let's not do anythin, a building, depends on the attack type/ if (this.threatList.indexOf(e.msg.attacker) === -1) { var enemySoldiers = HeadQuarters.getEnemySoldiers().toEntityArray(); for (var j in enemySoldiers) { var enemy = enemySoldiers[j]; if (enemy.position() === undefined) // likely garrisoned continue; if (m.inRange(enemy.position(), attacker.position(), 1000) && this.threatList.indexOf(enemy.id()) === -1) this.threatList.push(enemy.id()); } this.threatList.push(e.msg.attacker); } } } } } if (bool_attacked > 0){ var avgAttackerPos = [sumAttackerPos[0]/numAttackers, sumAttackerPos[1]/numAttackers]; units.move(avgAttackerPos[0], avgAttackerPos[1]); // let's run towards it. this.tactics = new Tactics(gameState,HeadQuarters, this.idList,this.threatList,true); this.state = "attacking_threat"; } }else if (this.state === "attacking_threat"){ this.tactics.eventMetadataCleanup(events,HeadQuarters); var removeList = this.tactics.removeTheirDeads(HeadQuarters); this.tactics.removeMyDeads(HeadQuarters); for (var i in removeList){ this.threatList.splice(this.threatList.indexOf(removeList[i]),1); } if (this.threatList.length <= 0) { this.tactics.disband(HeadQuarters,events); this.tactics = undefined; this.state = "walking"; units.move(this.path[0][0], this.path[0][1]); }else { this.tactics.reassignAttacks(HeadQuarters); } }*/ } if (this.state === "walking"){ this.position = this.unitCollection.getCentrePosition(); // probably not too good. if (!this.position) { Engine.ProfileStop(); return undefined; // should spawn an error. } // basically haven't moved an inch: very likely stuck) if (API3.SquareVectorDistance(this.position, this.position5TurnsAgo) < 10 && this.path.length > 0 && gameState.ai.playedTurn % 5 === 0) { // check for stuck siege units var sieges = this.unitCollection.filter(API3.Filters.byClass("Siege")); var farthest = 0; var farthestEnt = -1; sieges.forEach (function (ent) { if (API3.SquareVectorDistance(ent.position(),self.position) > farthest) { farthest = API3.SquareVectorDistance(ent.position(),self.position); farthestEnt = ent; } }); if (farthestEnt !== -1) farthestEnt.destroy(); } if (gameState.ai.playedTurn % 5 === 0) this.position5TurnsAgo = this.position; if (this.lastPosition && API3.SquareVectorDistance(this.position, this.lastPosition) < 20 && this.path.length > 0) { this.unitCollection.moveIndiv(this.path[0][0][0], this.path[0][0][1]); // We're stuck, presumably. Check if there are no walls just close to us. If so, we're arrived, and we're gonna tear down some serious stone. var walls = gameState.getEnemyEntities().filter(API3.Filters.and(API3.Filters.byOwner(this.targetPlayer), API3.Filters.byClass("StoneWall"))); var nexttoWalls = false; walls.forEach( function (ent) { if (!nexttoWalls && API3.SquareVectorDistance(self.position, ent.position()) < 800) nexttoWalls = true; }); // there are walls but we can attack if (nexttoWalls && this.unitCollection.filter(API3.Filters.byCanAttack("StoneWall")).length !== 0) { m.debug ("Attack Plan " +this.type +" " +this.name +" has met walls and is not happy."); this.state = "arrived"; } else if (nexttoWalls) { // abort plan. m.debug ("Attack Plan " +this.type +" " +this.name +" has met walls and gives up."); Engine.ProfileStop(); return 0; } } // check if our land units are close enough from the next waypoint. if (API3.SquareVectorDistance(this.unitCollection.getCentrePosition(), this.targetPos) < 7500 || API3.SquareVectorDistance(this.unitCollection.getCentrePosition(), this.path[0][0]) < 650) { if (this.unitCollection.filter(API3.Filters.byClass("Siege")).length !== 0 && API3.SquareVectorDistance(this.unitCollection.getCentrePosition(), this.targetPos) >= 7500 && API3.SquareVectorDistance(this.unitCollection.filter(API3.Filters.byClass("Siege")).getCentrePosition(), this.path[0][0]) >= 650) { } else { // okay so here basically two cases. The first one is "we need a boat at this point". // the second one is "we need to unload at this point". The third is "normal". if (this.path[0][1] !== true) { this.path.shift(); if (this.path.length > 0){ this.unitCollection.move(this.path[0][0][0], this.path[0][0][1]); } else { m.debug ("Attack Plan " +this.type +" " +this.name +" has arrived to destination."); // we must assume we've arrived at the end of the trail. this.state = "arrived"; } } else { // TODO: make this require an escort later on. this.path.shift(); if (this.path.length === 0) { m.debug ("Attack Plan " +this.type +" " +this.name +" has arrived to destination."); // we must assume we've arrived at the end of the trail. this.state = "arrived"; } else { /* var plan = new m.TransportPlan(gameState, this.unitCollection.toIdArray(), this.path[0][0], false); this.tpPlanID = plan.ID; HQ.navalManager.transportPlans.push(plan); m.debug ("Transporting over sea"); this.state = "transporting"; */ // TODO: fix this above //right now we'll abort. Engine.ProfileStop(); return 0; } } } } } else if (this.state === "transporting") { // check that we haven't finished transporting, ie the plan if (!HQ.navalManager.checkActivePlan(this.tpPlanID)) { this.state = "walking"; } } // todo: re-implement raiding if (this.state === "arrived"){ // let's proceed on with whatever happens now. // There's a ton of TODOs on this part. if (this.onArrivalReaction == "proceedOnTargets") { this.state = ""; this.unitCollection.forEach( function (ent) { //}) { ent.stopMoving(); ent.setMetadata(PlayerID, "subrole", "attacking"); }); } else if (this.onArrivalReaction == "huntVillagers") { // let's get any villager and target them with a tactics manager var enemyCitizens = gameState.entities.filter(function(ent) { return (gameState.isEntityEnemy(ent) && ent.hasClass("Support") && ent.owner() !== 0 && ent.position()); }); var targetList = []; enemyCitizens.forEach( function (enemy) { if (m.inRange(enemy.position(), units.getCentrePosition(), 2500) && targetList.indexOf(enemy.id()) === -1) targetList.push(enemy.id()); }); if (targetList.length > 0) { this.tactics = new Tactics(gameState,HeadQuarters, this.idList,targetList); this.state = "huntVillagers"; var arrivedthisTurn = true; } else { this.state = ""; var arrivedthisTurn = true; } } } if (this.state === "") { // Units attacked will target their attacker unless they're siege. Then we take another non-siege unit to attack them. var attackedEvents = events["Attacked"]; for (var key in attackedEvents) { var e = attackedEvents[key]; if (IDs.indexOf(e.target) !== -1) { var attacker = gameState.getEntityById(e.attacker); var ourUnit = gameState.getEntityById(e.target); if (attacker && attacker.position() && attacker.hasClass("Unit") && attacker.owner() != 0 && attacker.owner() != PlayerID) { if (ourUnit.hasClass("Siege")) { var collec = this.unitCollection.filterNearest(ourUnit.position(), 8).filter(Filters.not(Filters.byClass("Siege"))).toEntityArray(); if (collec.length !== 0) { collec[0].attack(attacker.id()); if (collec.length !== 1) { collec[1].attack(attacker.id()); if (collec.length !== 2) { collec[2].attack(attacker.id()); } } } } else { ourUnit.attack(attacker.id()); } } } } var enemyUnits = gameState.getGEC("player-" +this.targetPlayer + "-units"); var enemyStructures = gameState.getGEC("player-" +this.targetPlayer + "-structures"); if (this.unitCollUpdateArray === undefined || this.unitCollUpdateArray.length === 0) { this.unitCollUpdateArray = this.unitCollection.toIdArray(); } else { // some stuffs for locality and speed var territoryMap = m.createTerritoryMap(gameState); var timeElapsed = gameState.getTimeElapsed(); // Let's check a few units each time we update. Currently 10 var lgth = Math.min(this.unitCollUpdateArray.length,10); for (var check = 0; check < lgth; check++) { var ent = gameState.getEntityById(this.unitCollUpdateArray[0]); if (!ent) continue; var orderData = ent.unitAIOrderData(); if (orderData.length !== 0) orderData = orderData[0]; else orderData = undefined; // if the unit is in my territory, make it move. if (territoryMap.point(ent.position()) - 64 === PlayerID) ent.move(this.targetPos[0],this.targetPos[1]); // update it. var needsUpdate = false; if (ent.isIdle()) needsUpdate = true; if (ent.hasClass("Siege") && (!orderData || !orderData["target"] || !gameState.getEntityById(orderData["target"]) || !gameState.getEntityById(orderData["target"]).hasClass("ConquestCritical")) ) needsUpdate = true; else if (!ent.hasClass("Siege") && orderData && orderData["target"] && gameState.getEntityById(orderData["target"]) && gameState.getEntityById(orderData["target"]).hasClass("Structure")) needsUpdate = true; // try to make it attack a unit instead if (timeElapsed - ent.getMetadata(PlayerID, "lastAttackPlanUpdateTime") < 10000) needsUpdate = false; if (needsUpdate === true || arrivedthisTurn) { ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", timeElapsed); var mStruct = enemyStructures.filter(function (enemy) { //}){ if (!enemy.position() || (enemy.hasClass("StoneWall") && ent.canAttackClass("StoneWall"))) { return false; } if (API3.SquareVectorDistance(enemy.position(),ent.position()) > 3000) { return false; } return true; }); var mUnit; if (ent.hasClass("Cavalry") && ent.countersClasses(["Support"])) { mUnit = enemyUnits.filter(function (enemy) { //}){ if (!enemy.position()) { return false; } if (!enemy.hasClass("Support")) return false; if (API3.SquareVectorDistance(enemy.position(),ent.position()) > 10000) { return false; } return true; }); } if (!(ent.hasClass("Cavalry") && ent.countersClasses(["Support"])) || mUnit.length === 0) { mUnit = enemyUnits.filter(function (enemy) { //}){ if (!enemy.position()) { return false; } if (API3.SquareVectorDistance(enemy.position(),ent.position()) > 10000) { return false; } return true; }); } var isGate = false; mUnit = mUnit.toEntityArray(); mStruct = mStruct.toEntityArray(); if (ent.hasClass("Siege")) { mStruct.sort(function (structa,structb) { //}){ var vala = structa.costSum(); if (structa.hasClass("Gates") && ent.canAttackClass("StoneWall")) // we hate gates { isGate = true; vala += 10000; } else if (structa.hasClass("ConquestCritical")) vala += 200; var valb = structb.costSum(); if (structb.hasClass("Gates") && ent.canAttackClass("StoneWall")) // we hate gates { isGate = true; valb += 10000; } else if (structb.hasClass("ConquestCritical")) valb += 200; //warn ("Structure " +structa.genericName() + " is worth " +vala); //warn ("Structure " +structb.genericName() + " is worth " +valb); return (valb - vala); }); // TODO: handle ballistas here if (mStruct.length !== 0) { if (isGate) ent.attack(mStruct[0].id()); else { var rand = Math.floor(Math.random() * mStruct.length*0.1); ent.attack(mStruct[+rand].id()); //m.debug ("Siege units attacking a structure from " +mStruct[+rand].owner() + " , " +mStruct[+rand].templateName()); } } else if (API3.SquareVectorDistance(self.targetPos, ent.position()) > 900 ){ //m.debug ("Siege units moving to " + uneval(self.targetPos)); ent.move(self.targetPos[0],self.targetPos[1]); } } else { if (mUnit.length !== 0) { var rand = Math.floor(Math.random() * mUnit.length*0.99); ent.attack(mUnit[(+rand)].id()); //m.debug ("Units attacking a unit from " +mUnit[+rand].owner() + " , " +mUnit[+rand].templateName()); } else if (API3.SquareVectorDistance(self.targetPos, ent.position()) > 900 ){ //m.debug ("Units moving to " + uneval(self.targetPos)); ent.move(self.targetPos[0],self.targetPos[1]); } else if (mStruct.length !== 0) { mStruct.sort(function (structa,structb) { //}){ var vala = structa.costSum(); if (structa.hasClass("Gates") && ent.canAttackClass("StoneWall")) // we hate gates { isGate = true; vala += 10000; } else if (structa.hasClass("ConquestCritical")) vala += 100; var valb = structb.costSum(); if (structb.hasClass("Gates") && ent.canAttackClass("StoneWall")) // we hate gates { isGate = true; valb += 10000; } else if (structb.hasClass("ConquestCritical")) valb += 100; return (valb - vala); }); if (isGate) ent.attack(mStruct[0].id()); else { var rand = Math.floor(Math.random() * mStruct.length*0.1); ent.attack(mStruct[+rand].id()); //m.debug ("Units attacking a structure from " +mStruct[+rand].owner() + " , " +mStruct[+rand].templateName()); } } } } } this.unitCollUpdateArray.splice(0,10); } // updating targets. if (!gameState.getEntityById(this.target.id())) { var targets = this.targetFinder(gameState, HQ); if (targets.length === 0){ targets = this.defaultTargetFinder(gameState, HQ); } if (targets.length) { m.debug ("Seems like our target has been destroyed. Switching."); m.debug ("Aiming for " + targets); // picking a target this.targetPos = undefined; var count = 0; while (!this.targetPos){ var rand = Math.floor((Math.random()*targets.length)); this.target = targets.toEntityArray()[rand]; this.targetPos = this.target.position(); count++; if (count > 1000){ m.debug("No target with a valid position found"); Engine.ProfileStop(); return false; } } } } // regularly update the target position in case it's a unit. if (this.target.hasClass("Unit")) this.targetPos = this.target.position(); } /* if (this.state === "huntVillagers") { this.tactics.eventMetadataCleanup(events,HeadQuarters); this.tactics.removeTheirDeads(HeadQuarters); this.tactics.removeMyDeads(HeadQuarters); if (this.tactics.isBattleOver()) { this.tactics.disband(HeadQuarters,events); this.tactics = undefined; this.state = ""; return 0; // assume over } else this.tactics.reassignAttacks(HeadQuarters); }*/ this.lastPosition = this.position; Engine.ProfileStop(); return this.unitCollection.length; }; m.CityAttack.prototype.totalCountUnits = function(gameState){ var totalcount = 0; for (var i in this.idList) { totalcount++; } return totalcount; }; // reset any units m.CityAttack.prototype.Abort = function(gameState){ this.unitCollection.forEach(function(ent) { ent.setMetadata(PlayerID, "role",undefined); ent.setMetadata(PlayerID, "subrole",undefined); ent.setMetadata(PlayerID, "plan",undefined); }); for (var unitCat in this.unitStat) { delete this.unitStat[unitCat]; delete this.unit[unitCat]; } delete this.unitCollection; gameState.ai.queueManager.removeQueue("plan_" + this.name); gameState.ai.queueManager.removeQueue("plan_" + this.name + "_champ"); }; return m; }(AEGIS);