0ad/binaries/data/mods/public/simulation/ai/aegis/attack_plan.js
wraitii 9d02495a96 Fix a few bugs.
Improve the AI gamestate to make better use of entity collections,
should be very slightly faster, and it's cleaner.
Remove enemy watchers that were no longer used.

This was SVN commit r14574.
2014-01-12 01:07:07 +00:00

1241 lines
47 KiB
JavaScript
Executable File

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.getOwnStructures().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.getOwnUnits().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.getOwnUnits().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.getOwnUnits().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, false);
if (this.type === "rush")
NoRole = gameState.getOwnEntitiesByRole("worker", true);
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 = gameState.getEnemyStructures(this.targetPlayer).filter(m.Filters.byClass("CivCentre"));
if (targets.length == 0) {
targets = gameState.getEnemyStructures(this.targetPlayer).filter(m.Filters.byClass("ConquestCritical"));
}
// If there's nothing, attack anything else that's less critical
if (targets.length == 0) {
targets = gameState.getEnemyStructures(this.targetPlayer).filter(m.Filters.byClass("Town"));
}
if (targets.length == 0) {
targets = gameState.getEnemyStructures(this.targetPlayer).filter(m.Filters.byClass("Village"));
}
// no buildings, attack anything conquest critical, even units (it's assuming it won't move).
if (targets.length == 0) {
targets = gameState.getEnemyEntities(this.targetPlayer).filter(m.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);