forked from 0ad/0ad
1443 lines
49 KiB
JavaScript
1443 lines
49 KiB
JavaScript
var PETRA = 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 are 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.
|
|
*/
|
|
|
|
// enemy is the targeted player or undefined, while target is the entity targeted or undefined
|
|
m.AttackPlan = function(gameState, Config, uniqueID, type, enemy, target)
|
|
{
|
|
this.Config = Config;
|
|
this.name = uniqueID;
|
|
this.type = type || "Attack";
|
|
this.state = "unexecuted";
|
|
|
|
this.targetPlayer = enemy;
|
|
if (this.targetPlayer === undefined)
|
|
{
|
|
if (target)
|
|
this.targetPlayer = target.owner();
|
|
else
|
|
this.targetPlayer = this.getEnemyPlayer(gameState);
|
|
}
|
|
if (this.targetPlayer === undefined)
|
|
{
|
|
this.failed = true;
|
|
return false;
|
|
}
|
|
|
|
// get a starting rallyPoint ... will be improved later
|
|
this.rallyPoint = undefined;
|
|
for each (var base in gameState.ai.HQ.baseManagers)
|
|
{
|
|
if (!base.anchor || !base.anchor.position())
|
|
continue;
|
|
this.rallyPoint = base.anchor.position();
|
|
break;
|
|
}
|
|
if (!this.rallyPoint)
|
|
{
|
|
this.failed = true;
|
|
return false;
|
|
}
|
|
|
|
this.overseas = false;
|
|
this.paused = false;
|
|
this.completingTurn = 0;
|
|
|
|
// priority of the queues we'll create.
|
|
var priority = 70;
|
|
|
|
// 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 = {};
|
|
|
|
// neededShips is the minimal number of ships which should be availabe for transport
|
|
if (type === "Rush")
|
|
{
|
|
priority = 250;
|
|
this.unitStat["Infantry"] = { "priority": 1, "minSize": 10, "targetSize": 26, "batchSize": 2, "classes": ["Infantry"], "interests": [ ["strength",1], ["cost",1], ["costsResource", 0.5, "stone"], ["costsResource", 0.6, "metal"] ] };
|
|
this.neededShips = 1;
|
|
}
|
|
else if (type === "Raid")
|
|
{
|
|
priority = 150;
|
|
this.unitStat["Cavalry"] = { "priority": 1, "minSize": 3, "targetSize": 4, "batchSize": 2, "classes": ["Cavalry", "CitizenSoldier"], "interests": [ ["strength",1], ["cost",1] ] };
|
|
this.neededShips = 1;
|
|
}
|
|
else if (type === "HugeAttack")
|
|
{
|
|
priority = 90;
|
|
// basically we want a mix of citizen soldiers so our barracks have a purpose, and champion units.
|
|
this.unitStat["RangedInfantry"] = { "priority": 0.7, "minSize": 5, "targetSize": 15, "batchSize": 5, "classes": ["Infantry","Ranged", "CitizenSoldier"], "interests": [["strength",3], ["cost",1] ] };
|
|
this.unitStat["MeleeInfantry"] = { "priority": 0.7, "minSize": 5, "targetSize": 15, "batchSize": 5, "classes": ["Infantry","Melee", "CitizenSoldier" ], "interests": [ ["strength",3], ["cost",1] ] };
|
|
this.unitStat["ChampRangedInfantry"] = { "priority": 1, "minSize": 5, "targetSize": 25, "batchSize": 5, "classes": ["Infantry","Ranged", "Champion"], "interests": [["strength",3], ["cost",1] ] };
|
|
this.unitStat["ChampMeleeInfantry"] = { "priority": 1, "minSize": 5, "targetSize": 20, "batchSize": 5, "classes": ["Infantry","Melee", "Champion" ], "interests": [ ["strength",3], ["cost",1] ] };
|
|
this.unitStat["MeleeCavalry"] = { "priority": 0.7, "minSize": 3, "targetSize": 15, "batchSize": 3, "classes": ["Cavalry","Melee", "CitizenSoldier" ], "interests": [ ["strength",2], ["cost",1] ] };
|
|
this.unitStat["RangedCavalry"] = { "priority": 0.7, "minSize": 3, "targetSize": 15, "batchSize": 3, "classes": ["Cavalry","Ranged", "CitizenSoldier"], "interests": [ ["strength",2], ["cost",1] ] };
|
|
this.unitStat["ChampMeleeInfantry"] = { "priority": 1, "minSize": 3, "targetSize": 18, "batchSize": 3, "classes": ["Infantry","Melee", "Champion" ], "interests": [ ["strength",3], ["cost",1] ] };
|
|
this.unitStat["ChampMeleeCavalry"] = { "priority": 1, "minSize": 3, "targetSize": 18, "batchSize": 3, "classes": ["Cavalry","Melee", "Champion" ], "interests": [ ["strength",2], ["cost",1] ] };
|
|
this.neededShips = 5;
|
|
}
|
|
else
|
|
{
|
|
priority = 70;
|
|
this.unitStat["RangedInfantry"] = { "priority": 1, "minSize": 6, "targetSize": 18, "batchSize": 3, "classes": ["Infantry","Ranged"], "interests": [ ["canGather", 1], ["strength",1.6], ["cost",1.5], ["costsResource", 0.3, "stone"], ["costsResource", 0.3, "metal"] ] };
|
|
this.unitStat["MeleeInfantry"] = { "priority": 1, "minSize": 6, "targetSize": 18, "batchSize": 3, "classes": ["Infantry","Melee"], "interests": [ ["canGather", 1], ["strength",1.6], ["cost",1.5], ["costsResource", 0.3, "stone"], ["costsResource", 0.3, "metal"] ] };
|
|
this.neededShips = 3;
|
|
}
|
|
|
|
// 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+1);
|
|
this.queueChamp = gameState.ai.queues["plan_" + this.name +"_champ"];
|
|
gameState.ai.queueManager.addQueue("plan_" + this.name +"_siege", priority);
|
|
this.queueSiege = gameState.ai.queues["plan_" + this.name +"_siege"];
|
|
/*
|
|
this.unitStat["Siege"]["filter"] = function (ent) {
|
|
var strength = [ent.attackStrengths("Melee")["crush"],ent.attackStrengths("Ranged")["crush"]];
|
|
return (strength[0] > 15 || strength[1] > 15);
|
|
};*/
|
|
|
|
this.unitCollection = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "plan", this.name));
|
|
this.unitCollection.registerUpdates();
|
|
|
|
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];
|
|
|
|
var filter = API3.Filters.and(API3.Filters.byClassesAnd(Unit["classes"]), API3.Filters.byMetadata(PlayerID, "plan",this.name));
|
|
this.unit[cat] = gameState.getOwnUnits().filter(filter);
|
|
this.unit[cat].registerUpdates();
|
|
this.buildOrder.push([0, Unit["classes"], this.unit[cat], Unit, cat]);
|
|
}
|
|
|
|
// some variables for during the attack
|
|
this.position5TurnsAgo = [0,0];
|
|
this.lastPosition = [0,0];
|
|
this.position = [0,0];
|
|
|
|
// 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;
|
|
|
|
return true;
|
|
};
|
|
|
|
m.AttackPlan.prototype.getName = function()
|
|
{
|
|
return this.name;
|
|
};
|
|
|
|
m.AttackPlan.prototype.getType = function()
|
|
{
|
|
return this.type;
|
|
};
|
|
|
|
m.AttackPlan.prototype.isStarted = function()
|
|
{
|
|
return (this.state !== "unexecuted" && this.state !== "completing");
|
|
};
|
|
|
|
m.AttackPlan.prototype.isPaused = function()
|
|
{
|
|
return this.paused;
|
|
};
|
|
|
|
m.AttackPlan.prototype.setPaused = function(boolValue)
|
|
{
|
|
this.paused = boolValue;
|
|
};
|
|
|
|
m.AttackPlan.prototype.getEnemyPlayer = function(gameState)
|
|
{
|
|
var enemyPlayer = undefined;
|
|
// let's find our prefered target enemy, basically counting our enemies units.
|
|
var enemyCount = {};
|
|
var enemyDefense = {};
|
|
for (var i = 1; i < gameState.sharedScript.playersData.length; ++i)
|
|
{
|
|
enemyCount[i] = 0;
|
|
enemyDefense[i] = 0;
|
|
}
|
|
gameState.getEntities().forEach(function(ent) {
|
|
if (gameState.isEntityEnemy(ent) && ent.owner() !== 0)
|
|
{
|
|
enemyCount[ent.owner()]++;
|
|
if (ent.hasClass("Tower") || ent.hasClass("Fortress"))
|
|
enemyDefense[ent.owner()]++;
|
|
}
|
|
});
|
|
var max = 0;
|
|
for (var i in enemyCount)
|
|
{
|
|
if (this.type === "Rush" && enemyDefense[i] > 6) // No rush if enemy too well defended (iberians)
|
|
continue;
|
|
if (enemyCount[i] > max)
|
|
{
|
|
enemyPlayer = +i;
|
|
max = enemyCount[i];
|
|
}
|
|
}
|
|
return enemyPlayer;
|
|
};
|
|
|
|
// Returns true if the attack can be executed at the current time
|
|
// Basically it checks we have enough units.
|
|
m.AttackPlan.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;
|
|
};
|
|
|
|
m.AttackPlan.prototype.mustStart = function(gameState)
|
|
{
|
|
if (this.isPaused() || this.path === undefined)
|
|
return false;
|
|
var MaxReachedEverywhere = true;
|
|
var MinReachedEverywhere = true;
|
|
for (var unitCat in this.unitStat)
|
|
{
|
|
var Unit = this.unitStat[unitCat];
|
|
if (this.unit[unitCat].length < Unit["targetSize"])
|
|
MaxReachedEverywhere = false;
|
|
if (this.unit[unitCat].length < Unit["minSize"])
|
|
{
|
|
MinReachedEverywhere = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (MaxReachedEverywhere)
|
|
return true;
|
|
if (MinReachedEverywhere)
|
|
{
|
|
if ((gameState.getPopulationMax() - gameState.getPopulation() < 10) ||
|
|
(this.type === "Raid" && this.target && this.target.foundationProgress() && this.target.foundationProgress() > 60))
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
// Adds a build order. If resetQueue is true, this will reset the queue.
|
|
m.AttackPlan.prototype.addBuildOrder = function(gameState, name, unitStats, resetQueue)
|
|
{
|
|
if (!this.isStarted())
|
|
{
|
|
// 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.byMetadata(PlayerID, "plan",this.name));
|
|
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();
|
|
this.queueSiege.empty();
|
|
}
|
|
}
|
|
};
|
|
|
|
m.AttackPlan.prototype.addSiegeUnits = function(gameState)
|
|
{
|
|
if (this.unitStat["Siege"] || this.state !== "unexecuted")
|
|
return false;
|
|
// no minsize as we don't want the plan to fail at the last minute though.
|
|
var stat = { "priority": 1., "minSize": 0, "targetSize": 4, "batchSize": 2, "classes": ["Siege"],
|
|
"interests": [ ["siegeStrength", 3], ["cost",1] ] };
|
|
if (gameState.civ() === "maur")
|
|
stat["classes"] = ["Elephant", "Champion"];
|
|
this.addBuildOrder(gameState, "Siege", stat, true);
|
|
return true;
|
|
};
|
|
|
|
// 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.AttackPlan.prototype.updatePreparation = function(gameState, events)
|
|
{
|
|
// the completing step is used to return resources and regroup the units
|
|
// so we check that we have no more forced order before starting the attack
|
|
if (this.state === "completing")
|
|
{
|
|
// check that all units have finished with their transport if needed
|
|
if (this.waitingForTransport())
|
|
return 1;
|
|
// bloqued units which cannot finish their order should not stop the attack
|
|
if (this.completingTurn + 60 < gameState.ai.playedTurn && this.hasForceOrder())
|
|
return 1;
|
|
return 2;
|
|
}
|
|
|
|
if (this.Config.debug > 2 && gameState.ai.playedTurn % 50 === 0)
|
|
this.debugAttack();
|
|
|
|
// find our target
|
|
if (this.target === undefined)
|
|
{
|
|
this.target = this.getNearestTarget(gameState, this.rallyPoint);
|
|
if (!this.target)
|
|
{
|
|
var oldTargetPlayer = this.targetPlayer;
|
|
// may-be all our previous enemey targets have been destroyed ?
|
|
this.targetPlayer = this.getEnemyPlayer(gameState);
|
|
if (this.Config.debug > 0)
|
|
warn(" === no more target for enemy player " + oldTargetPlayer + " let us switch against player " + this.targetPlayer);
|
|
this.target = this.getNearestTarget(gameState, this.rallyPoint);
|
|
}
|
|
if (!this.target)
|
|
return 0;
|
|
this.targetPos = this.target.position();
|
|
// redefine a new rally point for this target if we have a base on the same land
|
|
// find a new one on the pseudo-nearest base (dist weighted by the size of the island)
|
|
var targetIndex = gameState.ai.accessibility.getAccessValue(this.targetPos);
|
|
var rallyIndex = gameState.ai.accessibility.getAccessValue(this.rallyPoint);
|
|
if (targetIndex !== rallyIndex)
|
|
{
|
|
var distminSame = Math.min();
|
|
var rallySame = undefined;
|
|
var distminDiff = Math.min();
|
|
var rallyDiff = undefined;
|
|
for each (var base in gameState.ai.HQ.baseManagers)
|
|
{
|
|
var anchor = base.anchor;
|
|
if (!anchor || !anchor.position())
|
|
continue;
|
|
var dist = API3.SquareVectorDistance(anchor.position(), this.targetPos);
|
|
if (base.accessIndex === targetIndex)
|
|
{
|
|
if (dist < distminSame)
|
|
{
|
|
distminSame = dist;
|
|
rallySame = anchor.position();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
dist = dist / Math.sqrt(gameState.ai.accessibility.regionSize[base.accessIndex]);
|
|
if (dist < distminDiff)
|
|
{
|
|
distminDiff = dist;
|
|
rallyDiff = anchor.position();
|
|
}
|
|
}
|
|
}
|
|
if (rallySame)
|
|
this.rallyPoint = rallySame;
|
|
else if (rallyDiff)
|
|
{
|
|
this.overseas = true;
|
|
this.rallyPoint = rallyDiff;
|
|
var sea = gameState.ai.HQ.getSeaIndex(gameState, rallyIndex, targetIndex);
|
|
if (sea !== undefined)
|
|
gameState.ai.HQ.navalManager.setMinimalTransportShips(gameState, sea, this.neededShips);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 === "toBeContinued")
|
|
{
|
|
var ret = this.getPathToTarget(gameState);
|
|
if (ret >= 0)
|
|
return ret;
|
|
}
|
|
|
|
this.assignUnits(gameState);
|
|
|
|
// special case: if we've reached max pop, and we can start the plan, start it.
|
|
if (gameState.getPopulationMax() - gameState.getPopulation() < 10)
|
|
{
|
|
if (this.canStart())
|
|
{
|
|
this.queue.empty();
|
|
this.queueChamp.empty();
|
|
this.queueSiege.empty();
|
|
}
|
|
else // Abort the plan so that its units will be reassigned to other plans.
|
|
{
|
|
if (this.Config.debug > 0)
|
|
{
|
|
var am = gameState.ai.HQ.attackManager;
|
|
warn(" attacks upcoming: raid " + am.upcomingAttacks["Raid"].length
|
|
+ " rush " + am.upcomingAttacks["Rush"].length
|
|
+ " attack " + am.upcomingAttacks["Attack"].length
|
|
+ " huge " + am.upcomingAttacks["HugeAttack"].length);
|
|
warn(" attacks started: raid " + am.startedAttacks["Raid"].length
|
|
+ " rush " + am.startedAttacks["Rush"].length
|
|
+ " attack " + am.startedAttacks["Attack"].length
|
|
+ " huge " + am.startedAttacks["HugeAttack"].length);
|
|
}
|
|
return 0;
|
|
}
|
|
}
|
|
else if (this.mustStart(gameState) && (gameState.countOwnQueuedEntitiesWithMetadata("plan", +this.name) > 0))
|
|
{
|
|
// keep on while the units finish being trained, then we'll start
|
|
this.queue.empty();
|
|
this.queueChamp.empty();
|
|
this.queueSiege.empty();
|
|
return 1;
|
|
}
|
|
else if (!this.mustStart(gameState))
|
|
{
|
|
// We still have time left to recruit units and do stuffs.
|
|
this.trainMoreUnits(gameState);
|
|
// may happen if we have no more training facilities and build orders are canceled
|
|
if (this.buildOrder.length === 0)
|
|
return 0; // will abort the plan
|
|
return 1;
|
|
}
|
|
|
|
// if we're here, it means we must start (and have no units in training left).
|
|
this.state = "completing";
|
|
this.completingTurn = gameState.ai.playedTurn;
|
|
|
|
var rallyPoint = this.rallyPoint;
|
|
var rallyIndex = gameState.ai.accessibility.getAccessValue(rallyPoint);
|
|
this.unitCollection.forEach(function (entity) {
|
|
// For the time being, if occupied in a transport, remove the unit from this plan TODO improve that
|
|
if (entity.getMetadata(PlayerID, "transport") !== undefined || entity.getMetadata(PlayerID, "transporter") !== undefined)
|
|
{
|
|
entity.setMetadata(PlayerID, "plan", -1);
|
|
return;
|
|
}
|
|
entity.setMetadata(PlayerID, "role", "attack");
|
|
entity.setMetadata(PlayerID, "subrole", "completing");
|
|
var queued = false;
|
|
if (entity.resourceCarrying() && entity.resourceCarrying().length)
|
|
{
|
|
if (!entity.getMetadata(PlayerID, "worker-object"))
|
|
entity.setMetadata(PlayerID, "worker-object", new m.Worker(entity));
|
|
queued = entity.getMetadata(PlayerID, "worker-object").returnResources(gameState);
|
|
}
|
|
var index = gameState.ai.accessibility.getAccessValue(entity.position());
|
|
if (index === rallyIndex)
|
|
entity.move(rallyPoint[0], rallyPoint[1], queued);
|
|
else
|
|
gameState.ai.HQ.navalManager.requireTransport(gameState, entity, index, rallyIndex, rallyPoint);
|
|
});
|
|
|
|
// reset all queued units
|
|
var plan = this.name;
|
|
gameState.ai.queueManager.removeQueue("plan_" + plan);
|
|
gameState.ai.queueManager.removeQueue("plan_" + plan + "_champ");
|
|
gameState.ai.queueManager.removeQueue("plan_" + plan + "_siege");
|
|
return 1;
|
|
};
|
|
|
|
|
|
m.AttackPlan.prototype.trainMoreUnits = function(gameState)
|
|
{
|
|
// let's sort by training advancement, ie 'current size / target size'
|
|
// count the number of queued units too.
|
|
// substract priority.
|
|
for (var i = 0; i < this.buildOrder.length; ++i)
|
|
{
|
|
var special = "Plan_" + this.name + "_" + this.buildOrder[i][4];
|
|
var aQueued = gameState.countOwnQueuedEntitiesWithMetadata("special", special);
|
|
aQueued += this.queue.countQueuedUnitsWithMetadata("special", special);
|
|
aQueued += this.queueChamp.countQueuedUnitsWithMetadata("special", special);
|
|
aQueued += this.queueSiege.countQueuedUnitsWithMetadata("special", special);
|
|
this.buildOrder[i][0] = this.buildOrder[i][2].length + aQueued;
|
|
}
|
|
this.buildOrder.sort(function (a,b) {
|
|
var va = a[0]/a[3]["targetSize"] - a[3]["priority"];
|
|
if (a[0] >= a[3]["targetSize"])
|
|
va += 1000;
|
|
var vb = b[0]/b[3]["targetSize"] - b[3]["priority"];
|
|
if (b[0] >= b[3]["targetSize"])
|
|
vb += 1000;
|
|
return va - vb;
|
|
});
|
|
|
|
if (this.Config.debug > 0 && gameState.ai.playedTurn%50 === 0)
|
|
{
|
|
warn("====================================");
|
|
warn("======== build order for plan " + this.name);
|
|
for each (var order in this.buildOrder)
|
|
{
|
|
var specialData = "Plan_"+this.name+"_"+order[4];
|
|
var inTraining = gameState.countOwnQueuedEntitiesWithMetadata("special", specialData);
|
|
var queue1 = this.queue.countQueuedUnitsWithMetadata("special", specialData);
|
|
var queue2 = this.queueChamp.countQueuedUnitsWithMetadata("special", specialData);
|
|
var queue3 = this.queueSiege.countQueuedUnitsWithMetadata("special", specialData);
|
|
warn(" >>> " + order[4] + " done " + order[2].length + " training " + inTraining
|
|
+ " queue " + queue1 + " champ " + queue2 + " siege " + queue3 + " >> need " + order[3].targetSize);
|
|
}
|
|
warn("------------------------------------");
|
|
gameState.ai.queueManager.printQueues(gameState);
|
|
warn("====================================");
|
|
warn("====================================");
|
|
}
|
|
|
|
if (this.buildOrder[0][0] < this.buildOrder[0][3]["targetSize"])
|
|
{
|
|
// if (this.Config.debug > 0)
|
|
// warn(" we have less than nominal Try to train more units");
|
|
// find the actual queue we want
|
|
var queue = this.queue;
|
|
if (this.buildOrder[0][3]["classes"].indexOf("Siege") !== -1 ||
|
|
(gameState.civ() == "maur" && this.buildOrder[0][3]["classes"].indexOf("Elephant") !== -1 && this.buildOrder[0][3]["classes"].indexOf("Champion")))
|
|
queue = this.queueSiege;
|
|
else if (this.buildOrder[0][3]["classes"].indexOf("Champion") !== -1)
|
|
queue = this.queueChamp;
|
|
|
|
if (queue.length() <= 5)
|
|
{
|
|
var template = gameState.ai.HQ.findBestTrainableUnit(gameState, this.buildOrder[0][1], this.buildOrder[0][3]["interests"]);
|
|
// 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)
|
|
{
|
|
if (this.Config.debug > 0)
|
|
warn("attack no template found " + this.buildOrder[0][1]);
|
|
delete this.unitStat[this.buildOrder[0][4]]; // deleting the associated unitstat.
|
|
this.buildOrder.splice(0,1);
|
|
}
|
|
else
|
|
{
|
|
if (this.Config.debug > 1)
|
|
warn("attack template " + template + " added for plan " + this.name);
|
|
var max = this.buildOrder[0][3]["batchSize"];
|
|
var specialData = "Plan_" + this.name + "_" + this.buildOrder[0][4];
|
|
if (gameState.getTemplate(template).hasClass("CitizenSoldier"))
|
|
var trainingPlan = new m.TrainingPlan(gameState, template, { "role": "worker", "plan": this.name, "special": specialData, "base": 0 }, max, max);
|
|
else
|
|
var trainingPlan = new m.TrainingPlan(gameState, template, { "role": "attack", "plan": this.name, "special": specialData, "base": 0 }, max, max);
|
|
if (trainingPlan.template)
|
|
queue.addItem(trainingPlan);
|
|
else if (this.Config.debug > 0)
|
|
{
|
|
warn("training plan canceled because no template for " + template + " build1 " + uneval(this.buildOrder[0][1])
|
|
+ " build3 " + uneval(this.buildOrder[0][3]["interests"]));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
m.AttackPlan.prototype.assignUnits = function(gameState)
|
|
{
|
|
var plan = this.name;
|
|
|
|
// 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).
|
|
if (this.type === "Raid")
|
|
{
|
|
var candidates = gameState.getOwnUnits().filter(API3.Filters.byClass(["Cavalry"]));
|
|
var num = 0;
|
|
candidates.forEach(function(ent) {
|
|
if (!ent.position())
|
|
return;
|
|
if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") !== -1)
|
|
return;
|
|
if (ent.getMetadata(PlayerID, "transport") !== undefined || ent.getMetadata(PlayerID, "transporter") !== undefined)
|
|
return;
|
|
if (num++ > 1)
|
|
ent.setMetadata(PlayerID, "plan", plan);
|
|
});
|
|
return;
|
|
}
|
|
|
|
var noRole = gameState.getOwnEntitiesByRole(undefined, false).filter(API3.Filters.byClass(["Unit"]));
|
|
noRole.forEach(function(ent) {
|
|
if (!ent.position())
|
|
return;
|
|
if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") !== -1)
|
|
return;
|
|
if (ent.getMetadata(PlayerID, "transport") !== undefined || ent.getMetadata(PlayerID, "transporter") !== undefined)
|
|
return;
|
|
if (ent.hasClass("Support") || ent.attackTypes() === undefined)
|
|
return;
|
|
ent.setMetadata(PlayerID, "plan", plan);
|
|
});
|
|
// Add units previously in a plan, but which left it because needed for defense or attack finished
|
|
gameState.ai.HQ.attackManager.outOfPlan.forEach(function(ent) {
|
|
if (!ent.position())
|
|
return;
|
|
if (ent.getMetadata(PlayerID, "transport") !== undefined || ent.getMetadata(PlayerID, "transporter") !== undefined)
|
|
return;
|
|
ent.setMetadata(PlayerID, "plan", plan);
|
|
});
|
|
|
|
if (this.type !== "Rush")
|
|
return;
|
|
// For a rush, assign also workers (but keep a minimum number of defenders)
|
|
var worker = gameState.getOwnEntitiesByRole("worker", true).filter(API3.Filters.byClass(["Unit"]));
|
|
var num = 0;
|
|
worker.forEach(function(ent) {
|
|
if (!ent.position())
|
|
return;
|
|
if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") !== -1)
|
|
return;
|
|
if (ent.getMetadata(PlayerID, "transport") !== undefined)
|
|
return;
|
|
if (ent.hasClass("Support") || ent.attackTypes() === undefined)
|
|
return;
|
|
if (num++ > 8)
|
|
ent.setMetadata(PlayerID, "plan", plan);
|
|
});
|
|
};
|
|
|
|
// sameLand true means that we look for a target for which we do not need to take a transport
|
|
m.AttackPlan.prototype.getNearestTarget = function(gameState, position, sameLand)
|
|
{
|
|
if (this.type === "Raid")
|
|
var targets = this.raidTargetFinder(gameState);
|
|
else if (this.type === "Rush" || this.type === "Attack")
|
|
var targets = this.rushTargetFinder(gameState);
|
|
else
|
|
var targets = this.defaultTargetFinder(gameState);
|
|
if (targets.length === 0)
|
|
return undefined;
|
|
|
|
var land = gameState.ai.accessibility.getAccessValue(position);
|
|
|
|
// picking the nearest target
|
|
var minDist = -1;
|
|
var index = undefined;
|
|
for (var i in targets._entities)
|
|
{
|
|
if (!targets._entities[i].position())
|
|
continue;
|
|
if (sameLand && gameState.ai.accessibility.getAccessValue(targets._entities[i].position()) !== land)
|
|
continue;
|
|
var dist = API3.SquareVectorDistance(targets._entities[i].position(), position);
|
|
if (dist < minDist || minDist === -1)
|
|
{
|
|
minDist = dist;
|
|
index = i;
|
|
}
|
|
}
|
|
if (!index)
|
|
return undefined;
|
|
return targets._entities[index];
|
|
};
|
|
|
|
// Default target finder aims for conquest critical targets
|
|
m.AttackPlan.prototype.defaultTargetFinder = function(gameState)
|
|
{
|
|
var targets = gameState.getEnemyStructures(this.targetPlayer).filter(API3.Filters.byClass("CivCentre"));
|
|
if (targets.length == 0)
|
|
targets = gameState.getEnemyStructures(this.targetPlayer).filter(API3.Filters.byClass("ConquestCritical"));
|
|
// If there's nothing, attack anything else that's less critical
|
|
if (targets.length == 0)
|
|
targets = gameState.getEnemyStructures(this.targetPlayer).filter(API3.Filters.byClass("Town"));
|
|
if (targets.length == 0)
|
|
targets = gameState.getEnemyStructures(this.targetPlayer).filter(API3.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(API3.Filters.byClass("ConquestCritical"));
|
|
return targets;
|
|
};
|
|
|
|
// Rush target finder aims at isolated non-defended buildings
|
|
m.AttackPlan.prototype.rushTargetFinder = function(gameState)
|
|
{
|
|
var targets = new API3.EntityCollection(gameState.sharedScript);
|
|
var buildings = gameState.getEnemyStructures().toEntityArray();
|
|
if (buildings.length === 0)
|
|
return targets;
|
|
|
|
this.position = this.unitCollection.getCentrePosition();
|
|
if (!this.position)
|
|
{
|
|
var ourCC = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")).toEntityArray();
|
|
this.position = ourCC[0].position();
|
|
}
|
|
|
|
var minDist = Math.min();
|
|
var target = undefined;
|
|
for each (var building in buildings)
|
|
{
|
|
if (building.owner() === 0)
|
|
continue;
|
|
// TODO check on Arrow count
|
|
if (building.hasClass("CivCentre") || building.hasClass("Tower") || building.hasClass("Fortress"))
|
|
continue;
|
|
var pos = building.position();
|
|
var defended = false;
|
|
for each (var defense in buildings)
|
|
{
|
|
if (!defense.hasClass("CivCentre") && !defense.hasClass("Tower") && !defense.hasClass("Fortress"))
|
|
continue;
|
|
var dist = API3.SquareVectorDistance(pos, defense.position());
|
|
if (dist < 4900) // TODO check on defense range rather than this fixed 80*80
|
|
{
|
|
defended = true;
|
|
break;
|
|
}
|
|
}
|
|
if (defended)
|
|
continue;
|
|
var dist = API3.SquareVectorDistance(pos, this.position);
|
|
if (dist > minDist)
|
|
continue;
|
|
minDist = dist;
|
|
target = building;
|
|
}
|
|
if (target)
|
|
targets.addEnt(target);
|
|
|
|
if (targets.length == 0 && this.type === "Attack")
|
|
targets = this.defaultTargetFinder(gameState);
|
|
|
|
return targets;
|
|
};
|
|
|
|
// Raid target finder aims at destructing foundations from which our defenseManager has attacked the builders
|
|
m.AttackPlan.prototype.raidTargetFinder = function(gameState)
|
|
{
|
|
var targets = new API3.EntityCollection(gameState.sharedScript);
|
|
for each (var targetId in gameState.ai.HQ.defenseManager.targetList)
|
|
{
|
|
var target = gameState.getEntityById(targetId);
|
|
if (target && target.position())
|
|
targets.addEnt(target);
|
|
}
|
|
return targets
|
|
};
|
|
|
|
m.AttackPlan.prototype.getPathToTarget = function(gameState)
|
|
{
|
|
if (this.path === undefined)
|
|
this.path = this.pathFinder.getPath(this.rallyPoint, this.targetPos, this.pathSampling, this.pathWidth, 175);
|
|
else if (this.path === "toBeContinued")
|
|
this.path = this.pathFinder.continuePath();
|
|
|
|
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")
|
|
{
|
|
return 1; // 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.navalMap)
|
|
{
|
|
gameState.ai.HQ.navalMap = true;
|
|
return 0;
|
|
}
|
|
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 our territor)
|
|
this.setRallyPoint(gameState);
|
|
}
|
|
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 our territor)
|
|
this.setRallyPoint(gameState);
|
|
}
|
|
return -1; // ok
|
|
};
|
|
|
|
m.AttackPlan.prototype.setRallyPoint = function(gameState)
|
|
{
|
|
for (var i = 0; i < this.path.length; ++i)
|
|
{
|
|
// my pathfinder returns arrays in arrays in arrays.
|
|
var waypointPos = this.path[i][0];
|
|
if (gameState.ai.HQ.territoryMap.getOwner(waypointPos) !== PlayerID || this.path[i][1] === true)
|
|
{
|
|
// Set rally point at the border of our territory
|
|
// or where we need to change transportation method.
|
|
if (i !== 0)
|
|
this.rallyPoint = this.path[i-1][0];
|
|
else
|
|
this.rallyPoint = this.path[0][0];
|
|
|
|
if (i >= 2)
|
|
this.path.splice(0, i-1);
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
// 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 enough units.
|
|
m.AttackPlan.prototype.StartAttack = function(gameState)
|
|
{
|
|
if (this.Config.debug)
|
|
warn("start attack " + this.name + " with type " + this.type);
|
|
|
|
if (!this.target) // our target was destroyed during our preparation
|
|
{
|
|
if (!this.targetPos) // should not happen
|
|
return false;
|
|
var targetIndex = gameState.ai.accessibility.getAccessValue(this.targetPos);
|
|
var rallyIndex = gameState.ai.accessibility.getAccessValue(this.rallyPoint);
|
|
if (targetIndex === rallyIndex)
|
|
{
|
|
// If on the same index: if we are doing a raid, look for a better target,
|
|
// otherwise proceed with the previous target position
|
|
// and we will look for a better target there
|
|
if (this.type === "Raid")
|
|
{
|
|
this.target = this.getNearestTarget(gameState, this.rallyPoint);
|
|
if (!this.target)
|
|
return false;
|
|
this.targetPos = this.target.position();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Not on the same index, do not loose time to go to previous targetPos if nothing there
|
|
// so directly look for a new target right now
|
|
this.target = this.getNearestTarget(gameState, this.rallyPoint);
|
|
if (!this.target)
|
|
return false;
|
|
this.targetPos = this.target.position();
|
|
}
|
|
}
|
|
|
|
// 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");
|
|
gameState.ai.queueManager.removeQueue("plan_" + this.name + "_siege");
|
|
|
|
var curPos = this.unitCollection.getCentrePosition();
|
|
|
|
this.unitCollection.forEach(function(ent) {
|
|
ent.setMetadata(PlayerID, "subrole", "walking");
|
|
});
|
|
// optimize our collection now.
|
|
this.unitCollection.allowQuickIter();
|
|
|
|
this.unitCollection.setStance("aggressive");
|
|
|
|
if (gameState.ai.accessibility.getAccessValue(this.targetPos) === gameState.ai.accessibility.getAccessValue(this.rallyPoint))
|
|
{
|
|
if (!this.path[0][0][0] || !this.path[0][0][1])
|
|
{
|
|
if (this.Config.debug > 0)
|
|
warn("StartAttack: Problem with path " + uneval(this.path));
|
|
return false;
|
|
}
|
|
this.state = "walking";
|
|
this.unitCollection.move(this.path[0][0][0], this.path[0][0][1]);
|
|
}
|
|
else
|
|
{
|
|
this.state = "transporting";
|
|
var startIndex = gameState.ai.accessibility.getAccessValue(this.rallyPoint);
|
|
var endIndex = gameState.ai.accessibility.getAccessValue(this.targetPos);
|
|
var endPos = this.targetPos;
|
|
// TODO require a global transport for the collection,
|
|
// and put back its state to "walking" when the transport is finished
|
|
this.unitCollection.forEach(function (entity) {
|
|
gameState.ai.HQ.navalManager.requireTransport(gameState, entity, startIndex, endIndex, endPos);
|
|
});
|
|
}
|
|
}
|
|
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.AttackPlan.prototype.update = function(gameState, events)
|
|
{
|
|
if (this.unitCollection.length === 0)
|
|
return 0;
|
|
|
|
Engine.ProfileStart("Update Attack");
|
|
|
|
this.position = this.unitCollection.getCentrePosition();
|
|
var IDs = this.unitCollection.toIdArray();
|
|
|
|
var self = this;
|
|
|
|
// we are transporting our units, let's wait
|
|
// TODO retaliate if attacked while waiting for the rest of the units
|
|
// and instead of state "arrived", made a state "walking" with a new path
|
|
if (this.state === "transporting")
|
|
{
|
|
var done = true;
|
|
this.unitCollection.forEach(function (entity) {
|
|
if (self.Config.debug > 0 && entity.getMetadata(PlayerID, "transport") !== undefined)
|
|
Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [entity.id()], "rgb": [2,2,0]});
|
|
else if (self.Config.debug > 0)
|
|
Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [entity.id()], "rgb": [1,1,1]});
|
|
if (!done)
|
|
return;
|
|
if (entity.getMetadata(PlayerID, "transport") !== undefined)
|
|
done = false;
|
|
});
|
|
|
|
if (done)
|
|
this.state = "arrived";
|
|
else
|
|
{
|
|
// if we are attacked while waiting the rest of the army, retaliate
|
|
var attackedEvents = events["Attacked"];
|
|
for (var key in attackedEvents)
|
|
{
|
|
var e = attackedEvents[key];
|
|
if (IDs.indexOf(e.target) === -1)
|
|
continue;
|
|
var attacker = gameState.getEntityById(e.attacker);
|
|
var ourUnit = gameState.getEntityById(e.target);
|
|
if (!attacker || !ourUnit)
|
|
continue;
|
|
this.unitCollection.forEach(function (entity) {
|
|
if (entity.getMetadata(PlayerID, "transport") !== undefined)
|
|
return;
|
|
if (!entity.isIdle())
|
|
return;
|
|
entity.attack(attacker.id());
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// this actually doesn't do anything right now.
|
|
if (this.state === "walking")
|
|
{
|
|
// we're marching towards the target
|
|
// 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/building
|
|
// or if we reached the enemy base. Different plans may react differently.
|
|
var attackedNB = 0;
|
|
var attackedEvents = events["Attacked"];
|
|
for (var key in attackedEvents)
|
|
{
|
|
var e = attackedEvents[key];
|
|
if (IDs.indexOf(e.target) === -1)
|
|
continue;
|
|
var attacker = gameState.getEntityById(e.attacker);
|
|
var ourUnit = gameState.getEntityById(e.target);
|
|
|
|
if (attacker && attacker.position() && attacker.hasClass("Unit") && attacker.owner() != 0)
|
|
attackedNB++;
|
|
// if we're being attacked by a building, flee.
|
|
if (attacker && ourUnit && attacker.hasClass("Structure"))
|
|
ourUnit.flee(attacker);
|
|
}
|
|
// Are we arrived at destination ?
|
|
if ((gameState.ai.HQ.territoryMap.getOwner(this.position) === this.targetPlayer && attackedNB > 1) || attackedNB > 3)
|
|
this.state = "arrived";
|
|
}
|
|
|
|
if (this.state === "walking")
|
|
{
|
|
// 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) {
|
|
var dist = API3.SquareVectorDistance(ent.position(), self.position);
|
|
if (dist > farthest)
|
|
{
|
|
farthest = dist;
|
|
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)
|
|
{
|
|
if (!this.path[0][0][0] || !this.path[0][0][1])
|
|
warn("Start: Problem with path " + uneval(this.path));
|
|
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)
|
|
{
|
|
if (this.Config.debug > 0)
|
|
warn("Attack Plan " +this.type +" " +this.name +" has met walls and is not happy.");
|
|
this.state = "arrived";
|
|
}
|
|
else if (nexttoWalls) // abort plan
|
|
{
|
|
if (this.Config.debug > 0)
|
|
warn("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.position, this.targetPos) < 9000 ||
|
|
API3.SquareVectorDistance(this.position, this.path[0][0]) < 650)
|
|
{
|
|
if (this.unitCollection.filter(API3.Filters.byClass("Siege")).length !== 0
|
|
&& API3.SquareVectorDistance(this.position, this.targetPos) >= 9000
|
|
&& API3.SquareVectorDistance(this.unitCollection.filter(API3.Filters.byClass("Siege")).getCentrePosition(), this.path[0][0]) >= 650)
|
|
{
|
|
}
|
|
else
|
|
{
|
|
// okay so here basically two cases. First case is "we've arrived"
|
|
// Second case is "either we need a boat, or we need to unload"
|
|
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
|
|
{
|
|
if (this.Config.debug > 0)
|
|
warn("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)
|
|
{
|
|
if (this.Config.debug)
|
|
warn("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;
|
|
gameState.ai.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 (!gameState.ai.HQ.navalManager.checkActivePlan(this.tpPlanID))
|
|
this.state = "walking";
|
|
} */
|
|
|
|
|
|
if (this.state === "arrived")
|
|
{
|
|
// let's proceed on with whatever happens now.
|
|
// There's a ton of TODOs on this part.
|
|
this.state = "";
|
|
this.startingAttack = true;
|
|
this.unitCollection.forEach( function (ent) {
|
|
ent.stopMoving();
|
|
ent.setMetadata(PlayerID, "subrole", "attacking");
|
|
});
|
|
if (this.type === "Rush") // try to find a better target for rush
|
|
{
|
|
var targets = this.rushTargetFinder(gameState);
|
|
if (targets.length !== 0)
|
|
{
|
|
for (var i in targets._entities)
|
|
{
|
|
this.target = targets._entities[i];
|
|
break;
|
|
}
|
|
this.targetPos = this.target.position();
|
|
}
|
|
}
|
|
}
|
|
|
|
// basic state of attacking.
|
|
if (this.state === "")
|
|
{
|
|
var attackedEvents = events["Attacked"];
|
|
for (var key in attackedEvents)
|
|
{
|
|
var e = attackedEvents[key];
|
|
if (IDs.indexOf(e.target) === -1)
|
|
continue;
|
|
var attacker = gameState.getEntityById(e.attacker);
|
|
if (!attacker || !attacker.position() || !attacker.hasClass("Unit"))
|
|
continue;
|
|
var ourUnit = gameState.getEntityById(e.target);
|
|
if (this.isSiegeUnit(gameState, ourUnit))
|
|
{
|
|
// if siege units are attacked, we'll send some units to deal with enemies.
|
|
var collec = this.unitCollection.filter(API3.Filters.not(API3.Filters.byClass("Siege"))).filterNearest(ourUnit.position(), 5).toEntityArray();
|
|
for each (var ent in collec)
|
|
if (!this.isSiegeUnit(gameState, ent))
|
|
ent.attack(attacker.id());
|
|
}
|
|
else
|
|
{
|
|
// if other units are attacked, abandon their target (if it was a structure or a support) and retaliate
|
|
var orderData = ourUnit.unitAIOrderData();
|
|
if (orderData.length !== 0 && orderData[0]["target"])
|
|
{
|
|
var target = gameState.getEntityById(orderData[0]["target"]);
|
|
if (target && !target.hasClass("Structure") && !target.hasClass("Support"))
|
|
continue;
|
|
}
|
|
ourUnit.attack(attacker.id());
|
|
}
|
|
}
|
|
|
|
var enemyUnits = gameState.getEnemyUnits(this.targetPlayer);
|
|
var enemyStructures = gameState.getEnemyStructures(this.targetPlayer);
|
|
|
|
if (this.unitCollUpdateArray === undefined || this.unitCollUpdateArray.length === 0)
|
|
this.unitCollUpdateArray = this.unitCollection.toIdArray();
|
|
|
|
var timeElapsed = gameState.getTimeElapsed();
|
|
|
|
// Let's check a few units each time we update (currently 10) except when attack starts
|
|
if (this.unitCollUpdateArray.length < 15 || this.startingAttack)
|
|
var lgth = this.unitCollUpdateArray.length;
|
|
else
|
|
var lgth = 10;
|
|
this.startingAttack = false;
|
|
for (var check = 0; check < lgth; check++)
|
|
{
|
|
var ent = gameState.getEntityById(this.unitCollUpdateArray[check]);
|
|
if (!ent || !ent.position())
|
|
continue;
|
|
|
|
var orderData = ent.unitAIOrderData();
|
|
if (orderData.length !== 0)
|
|
orderData = orderData[0];
|
|
else
|
|
orderData = undefined;
|
|
|
|
// update the order if needed
|
|
var needsUpdate = false;
|
|
var maybeUpdate = false;
|
|
var siegeUnit = this.isSiegeUnit(gameState, ent);
|
|
if (ent.isIdle())
|
|
needsUpdate = true;
|
|
else if (siegeUnit && orderData && orderData["target"])
|
|
{
|
|
var target = gameState.getEntityById(orderData["target"]);
|
|
if (!target)
|
|
needsUpdate = true;
|
|
else if(!target.hasClass("Structure"))
|
|
maybeUpdate = true;
|
|
}
|
|
else if (!ent.hasClass("Cavalry") && !ent.hasClass("Ranged") && orderData && orderData["target"])
|
|
{
|
|
var target = gameState.getEntityById(orderData["target"]);
|
|
if (!target)
|
|
needsUpdate = true;
|
|
else if (target.hasClass("Female") && target.unitAIState().split(".")[1] == "FLEEING")
|
|
maybeUpdate = true;
|
|
}
|
|
|
|
// don't update too soon if not necessary
|
|
if (!needsUpdate)
|
|
{
|
|
if (!maybeUpdate)
|
|
continue;
|
|
var lastAttackPlanUpdateTime = ent.getMetadata(PlayerID, "lastAttackPlanUpdateTime");
|
|
if (lastAttackPlanUpdateTime && (timeElapsed - lastAttackPlanUpdateTime) < 5000)
|
|
continue;
|
|
}
|
|
ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", timeElapsed);
|
|
|
|
// let's filter targets further based on this unit.
|
|
var entIndex = gameState.ai.accessibility.getAccessValue(ent.position());
|
|
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;
|
|
if (siegeUnit && enemy.foundationProgress() === 0)
|
|
return false;
|
|
if (gameState.ai.accessibility.getAccessValue(enemy.position()) !== entIndex)
|
|
return false;
|
|
return true;
|
|
});
|
|
var nearby = (!ent.hasClass("Cavalry") && !ent.hasClass("Ranged"));
|
|
var mUnit = enemyUnits.filter(function (enemy) {
|
|
if (!enemy.position())
|
|
return false;
|
|
if (enemy.hasClass("Animal"))
|
|
return false;
|
|
if (nearby && enemy.hasClass("Female") && enemy.unitAIState().split(".")[1] == "FLEEING")
|
|
return false;
|
|
var dist = API3.SquareVectorDistance(enemy.position(), ent.position());
|
|
if (dist > 10000)
|
|
return false;
|
|
if (nearby && dist > 3600)
|
|
return false;
|
|
if (gameState.ai.accessibility.getAccessValue(enemy.position()) !== entIndex)
|
|
return false;
|
|
return true;
|
|
});
|
|
// Checking for gates if we're a siege unit.
|
|
mUnit = mUnit.toEntityArray();
|
|
mStruct = mStruct.toEntityArray();
|
|
if (siegeUnit)
|
|
{
|
|
if (mStruct.length !== 0)
|
|
{
|
|
mStruct.sort(function (structa,structb)
|
|
{
|
|
var vala = structa.costSum();
|
|
if (structa.hasClass("Gates") && ent.canAttackClass("StoneWall"))
|
|
vala += 10000;
|
|
else if (structa.hasClass("ConquestCritical"))
|
|
vala += 200;
|
|
var valb = structb.costSum();
|
|
if (structb.hasClass("Gates") && ent.canAttackClass("StoneWall"))
|
|
valb += 10000;
|
|
else if (structb.hasClass("ConquestCritical"))
|
|
valb += 200;
|
|
return (valb - vala);
|
|
});
|
|
if (mStruct[0].hasClass("Gates"))
|
|
ent.attack(mStruct[0].id());
|
|
else
|
|
{
|
|
var rand = Math.floor(Math.random() * mStruct.length * 0.2);
|
|
ent.attack(mStruct[+rand].id());
|
|
}
|
|
}
|
|
else if (API3.SquareVectorDistance(self.targetPos, ent.position()) > 900)
|
|
ent.attackMove(self.targetPos[0], self.targetPos[1]);
|
|
}
|
|
else
|
|
{
|
|
if (mUnit.length !== 0)
|
|
{
|
|
mUnit.sort(function (unitA,unitB) {
|
|
var vala = unitA.hasClass("Support") ? 50 : 0;
|
|
if (ent.countersClasses(unitA.classes()))
|
|
vala += 100;
|
|
var valb = unitB.hasClass("Support") ? 50 : 0;
|
|
if (ent.countersClasses(unitB.classes()))
|
|
valb += 100;
|
|
return valb - vala;
|
|
});
|
|
var rand = Math.floor(Math.random() * mUnit.length * 0.1);
|
|
ent.attack(mUnit[rand].id());
|
|
}
|
|
else if (API3.SquareVectorDistance(self.targetPos, ent.position()) > 2500 )
|
|
ent.attackMove(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"))
|
|
vala += 10000;
|
|
else if (structa.hasClass("ConquestCritical"))
|
|
vala += 100;
|
|
var valb = structb.costSum();
|
|
if (structb.hasClass("Gates") && ent.canAttackClass("StoneWall"))
|
|
valb += 10000;
|
|
else if (structb.hasClass("ConquestCritical"))
|
|
valb += 100;
|
|
return (valb - vala);
|
|
});
|
|
if (mStruct[0].hasClass("Gates"))
|
|
ent.attack(mStruct[0].id());
|
|
else
|
|
{
|
|
var rand = Math.floor(Math.random() * mStruct.length * 0.2);
|
|
ent.attack(mStruct[rand].id());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
this.unitCollUpdateArray.splice(0, lgth);
|
|
|
|
// updating targets.
|
|
if (!this.target || !gameState.getEntityById(this.target.id()))
|
|
{
|
|
if (this.Config.debug > 0)
|
|
warn("Seems like our target has been destroyed. Switching.");
|
|
this.target = this.getNearestTarget(gameState, this.position, true);
|
|
if (!this.target)
|
|
{
|
|
if (this.Config.debug > 0)
|
|
warn("No new target found. Remaining units " + this.unitCollection.length);
|
|
Engine.ProfileStop();
|
|
return false;
|
|
}
|
|
this.targetPos = this.target.position();
|
|
}
|
|
|
|
// regularly update the target position in case it's a unit.
|
|
if (this.target.hasClass("Unit"))
|
|
this.targetPos = this.target.position();
|
|
}
|
|
this.lastPosition = this.position;
|
|
Engine.ProfileStop();
|
|
|
|
return this.unitCollection.length;
|
|
};
|
|
|
|
// reset any units
|
|
m.AttackPlan.prototype.Abort = function(gameState)
|
|
{
|
|
// Do not use QuickIter with forEach when forEach removes elements
|
|
this.unitCollection.preventQuickIter();
|
|
// If the attack was started, and we are on the same land as the rallyPoint, go back there
|
|
var rallyPoint = this.rallyPoint;
|
|
var planIndex = gameState.ai.accessibility.getAccessValue(this.position);
|
|
var withdrawal = (this.isStarted() && !this.overseas);
|
|
this.unitCollection.forEach(function(ent) {
|
|
ent.stopMoving();
|
|
if (withdrawal)
|
|
ent.move(rallyPoint[0], rallyPoint[1]);
|
|
if (ent.hasClass("CitizenSoldier") && ent.getMetadata(PlayerID, "role") !== "worker")
|
|
{
|
|
ent.setMetadata(PlayerID, "role", "worker");
|
|
ent.setMetadata(PlayerID, "subrole", undefined);
|
|
}
|
|
ent.setMetadata(PlayerID, "plan", -1);
|
|
});
|
|
|
|
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");
|
|
gameState.ai.queueManager.removeQueue("plan_" + this.name + "_siege");
|
|
};
|
|
|
|
m.AttackPlan.prototype.checkEvents = function(gameState, events, queues)
|
|
{
|
|
if (this.state === "unexecuted")
|
|
return;
|
|
var TrainingEvents = events["TrainingFinished"];
|
|
for (var i in TrainingEvents)
|
|
{
|
|
var evt = TrainingEvents[i];
|
|
for each (var id in evt.entities)
|
|
{
|
|
var ent = gameState.getEntityById(id);
|
|
if (!ent || ent.getMetadata(PlayerID, "plan") === undefined)
|
|
continue;
|
|
if (ent.getMetadata(PlayerID, "plan") === this.name)
|
|
ent.setMetadata(PlayerID, "plan", -1);
|
|
}
|
|
}
|
|
};
|
|
|
|
m.AttackPlan.prototype.waitingForTransport = function()
|
|
{
|
|
var waiting = false;
|
|
this.unitCollection.forEach(function (ent) {
|
|
if (ent.getMetadata(PlayerID, "transport") !== undefined)
|
|
waiting = true;
|
|
});
|
|
return waiting;
|
|
};
|
|
|
|
m.AttackPlan.prototype.hasForceOrder = function(data, value)
|
|
{
|
|
var forced = false;
|
|
this.unitCollection.forEach(function (ent) {
|
|
if (data && +(ent.getMetadata(PlayerID, data)) !== value)
|
|
return;
|
|
var orders = ent.unitAIOrderData();
|
|
for each (var order in orders)
|
|
if (order.force)
|
|
forced = true;
|
|
});
|
|
return forced;
|
|
};
|
|
|
|
m.AttackPlan.prototype.isSiegeUnit = function(gameState, ent)
|
|
{
|
|
return (ent.hasClass("Siege") ||
|
|
(gameState.civ() === "maur" && ent.hasClass("Elephant") && ent.hasClass("Champion")));
|
|
};
|
|
|
|
m.AttackPlan.prototype.debugAttack = function()
|
|
{
|
|
warn("---------- attack " + this.name);
|
|
for (var unitCat in this.unitStat)
|
|
{
|
|
var Unit = this.unitStat[unitCat];
|
|
warn(unitCat + " num=" + this.unit[unitCat].length + " min=" + Unit["minSize"] + " need=" + Unit["targetSize"]);
|
|
}
|
|
warn("------------------------------");
|
|
};
|
|
|
|
return m;
|
|
}(PETRA);
|