Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackPlan.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackPlan.js (revision 23915)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackPlan.js (revision 23916)
@@ -1,2173 +1,2173 @@
/**
* This is an attack plan:
* 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.
*/
PETRA.AttackPlan = function(gameState, Config, uniqueID, type, data)
{
this.Config = Config;
this.name = uniqueID;
this.type = type || "Attack";
this.state = "unexecuted";
this.forced = false; // true when this attacked has been forced to help an ally
if (data && data.target)
{
this.target = data.target;
this.targetPos = this.target.position();
this.targetPlayer = this.target.owner();
}
else
{
this.target = undefined;
this.targetPos = undefined;
this.targetPlayer = undefined;
}
this.uniqueTargetId = data && data.uniqueTargetId || undefined;
// get a starting rallyPoint ... will be improved later
let rallyPoint;
let rallyAccess;
let allAccesses = {};
for (let base of gameState.ai.HQ.baseManagers)
{
if (!base.anchor || !base.anchor.position())
continue;
let access = PETRA.getLandAccess(gameState, base.anchor);
if (!rallyPoint)
{
rallyPoint = base.anchor.position();
rallyAccess = access;
}
if (!allAccesses[access])
allAccesses[access] = base.anchor.position();
}
if (!rallyPoint) // no base ? take the position of any of our entities
{
for (let ent of gameState.getOwnEntities().values())
{
if (!ent.position())
continue;
let access = PETRA.getLandAccess(gameState, ent);
rallyPoint = ent.position();
rallyAccess = access;
allAccesses[access] = rallyPoint;
break;
}
if (!rallyPoint)
{
this.failed = true;
return false;
}
}
this.rallyPoint = rallyPoint;
this.overseas = 0;
if (gameState.ai.HQ.navalMap)
{
for (let structure of gameState.getEnemyStructures().values())
{
if (this.target && structure.id() != this.target.id())
continue;
if (!structure.position())
continue;
let access = PETRA.getLandAccess(gameState, structure);
if (access in allAccesses)
{
this.overseas = 0;
this.rallyPoint = allAccesses[access];
break;
}
else if (!this.overseas)
{
let sea = gameState.ai.HQ.getSeaBetweenIndices(gameState, rallyAccess, access);
if (!sea)
{
if (this.target)
{
API3.warn("Petra: " + this.type + " " + this.name + " has an inaccessible target " +
this.target.templateName() + " indices " + rallyAccess + " " + access);
this.failed = true;
return false;
}
continue;
}
this.overseas = sea;
gameState.ai.HQ.navalManager.setMinimalTransportShips(gameState, sea, 1);
}
}
}
this.paused = false;
this.maxCompletingTime = 0;
// priority of the queues we'll create.
let priority = 70;
// unitStat 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 available for transport
if (type == "Rush")
{
priority = 250;
this.unitStat.Infantry = { "priority": 1, "minSize": 10, "targetSize": 20, "batchSize": 2, "classes": ["Infantry"],
"interests": [["strength", 1], ["costsResource", 0.5, "stone"], ["costsResource", 0.6, "metal"]] };
- this.unitStat.Cavalry = { "priority": 1, "minSize": 2, "targetSize": 4, "batchSize": 2, "classes": ["Cavalry", "CitizenSoldier"],
+ this.unitStat.FastMoving = { "priority": 1, "minSize": 2, "targetSize": 4, "batchSize": 2, "classes": ["FastMoving", "CitizenSoldier"],
"interests": [["strength", 1]] };
if (data && data.targetSize)
this.unitStat.Infantry.targetSize = data.targetSize;
this.neededShips = 1;
}
else if (type == "Raid")
{
priority = 150;
- this.unitStat.Cavalry = { "priority": 1, "minSize": 3, "targetSize": 4, "batchSize": 2, "classes": ["Cavalry", "CitizenSoldier"],
+ this.unitStat.FastMoving = { "priority": 1, "minSize": 3, "targetSize": 4, "batchSize": 2, "classes": ["FastMoving", "CitizenSoldier"],
"interests": [ ["strength", 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": 20, "batchSize": 5, "classes": ["Infantry", "Ranged", "CitizenSoldier"],
"interests": [["strength", 3]] };
this.unitStat.MeleeInfantry = { "priority": 0.7, "minSize": 5, "targetSize": 20, "batchSize": 5, "classes": ["Infantry", "Melee", "CitizenSoldier"],
"interests": [["strength", 3]] };
this.unitStat.ChampRangedInfantry = { "priority": 1, "minSize": 3, "targetSize": 18, "batchSize": 3, "classes": ["Infantry", "Ranged", "Champion"],
"interests": [["strength", 3]] };
this.unitStat.ChampMeleeInfantry = { "priority": 1, "minSize": 3, "targetSize": 18, "batchSize": 3, "classes": ["Infantry", "Melee", "Champion"],
"interests": [["strength", 3]] };
- this.unitStat.RangedCavalry = { "priority": 0.7, "minSize": 4, "targetSize": 20, "batchSize": 4, "classes": ["Cavalry", "Ranged", "CitizenSoldier"],
+ this.unitStat.RangedFastMoving = { "priority": 0.7, "minSize": 4, "targetSize": 20, "batchSize": 4, "classes": ["FastMoving", "Ranged", "CitizenSoldier"],
"interests": [["strength", 2]] };
- this.unitStat.MeleeCavalry = { "priority": 0.7, "minSize": 4, "targetSize": 20, "batchSize": 4, "classes": ["Cavalry", "Melee", "CitizenSoldier"],
+ this.unitStat.MeleeFastMoving = { "priority": 0.7, "minSize": 4, "targetSize": 20, "batchSize": 4, "classes": ["FastMoving", "Melee", "CitizenSoldier"],
"interests": [["strength", 2]] };
- this.unitStat.ChampRangedCavalry = { "priority": 1, "minSize": 3, "targetSize": 15, "batchSize": 3, "classes": ["Cavalry", "Ranged", "Champion"],
+ this.unitStat.ChampRangedFastMoving = { "priority": 1, "minSize": 3, "targetSize": 15, "batchSize": 3, "classes": ["FastMoving", "Ranged", "Champion"],
"interests": [["strength", 3]] };
- this.unitStat.ChampMeleeCavalry = { "priority": 1, "minSize": 3, "targetSize": 15, "batchSize": 3, "classes": ["Cavalry", "Melee", "Champion"],
+ this.unitStat.ChampMeleeFastMoving = { "priority": 1, "minSize": 3, "targetSize": 15, "batchSize": 3, "classes": ["FastMoving", "Melee", "Champion"],
"interests": [["strength", 2]] };
this.unitStat.Hero = { "priority": 1, "minSize": 0, "targetSize": 1, "batchSize": 1, "classes": ["Hero"],
"interests": [["strength", 2]] };
this.neededShips = 5;
}
else
{
priority = 70;
this.unitStat.RangedInfantry = { "priority": 1, "minSize": 6, "targetSize": 16, "batchSize": 3, "classes": ["Infantry", "Ranged"],
"interests": [["canGather", 1], ["strength", 1.6], ["costsResource", 0.3, "stone"], ["costsResource", 0.3, "metal"]] };
this.unitStat.MeleeInfantry = { "priority": 1, "minSize": 6, "targetSize": 16, "batchSize": 3, "classes": ["Infantry", "Melee"],
"interests": [["canGather", 1], ["strength", 1.6], ["costsResource", 0.3, "stone"], ["costsResource", 0.3, "metal"]] };
- this.unitStat.Cavalry = { "priority": 1, "minSize": 2, "targetSize": 6, "batchSize": 2, "classes": ["Cavalry", "CitizenSoldier"],
+ this.unitStat.FastMoving = { "priority": 1, "minSize": 2, "targetSize": 6, "batchSize": 2, "classes": ["FastMoving", "CitizenSoldier"],
"interests": [["strength", 1]] };
this.neededShips = 3;
}
// Put some randomness on the attack size
let variation = randFloat(0.8, 1.2);
// and lower priority and smaller sizes for easier difficulty levels
if (this.Config.difficulty < 2)
{
priority *= 0.6;
variation *= 0.5;
}
else if (this.Config.difficulty < 3)
{
priority *= 0.8;
variation *= 0.8;
}
for (let cat in this.unitStat)
{
this.unitStat[cat].targetSize = Math.round(variation * this.unitStat[cat].targetSize);
this.unitStat[cat].minSize = Math.min(this.unitStat[cat].minSize, this.unitStat[cat].targetSize);
}
// change the sizes according to max population
this.neededShips = Math.ceil(this.Config.popScaling * this.neededShips);
for (let cat in this.unitStat)
{
this.unitStat[cat].targetSize = Math.round(this.Config.popScaling * this.unitStat[cat].targetSize);
this.unitStat[cat].minSize = Math.ceil(this.Config.popScaling * this.unitStat[cat].minSize);
}
// TODO: there should probably be one queue per type of training building
gameState.ai.queueManager.addQueue("plan_" + this.name, priority);
gameState.ai.queueManager.addQueue("plan_" + this.name +"_champ", priority+1);
gameState.ai.queueManager.addQueue("plan_" + this.name +"_siege", priority);
// each array is [ratio, [associated classes], associated EntityColl, associated unitStat, name ]
this.buildOrders = [];
this.canBuildUnits = gameState.ai.HQ.canBuildUnits;
this.siegeState = 0; // 0 = not yet tested, 1 = not yet any siege trainer, 2 = siege added in build orders
// some variables used during the attack
this.position5TurnsAgo = [0, 0];
this.lastPosition = [0, 0];
this.position = [0, 0];
this.isBlocked = false; // true when this attack faces walls
return true;
};
PETRA.AttackPlan.prototype.init = function(gameState)
{
this.queue = gameState.ai.queues["plan_" + this.name];
this.queueChamp = gameState.ai.queues["plan_" + this.name +"_champ"];
this.queueSiege = gameState.ai.queues["plan_" + this.name +"_siege"];
this.unitCollection = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "plan", this.name));
this.unitCollection.registerUpdates();
this.unit = {};
// defining the entity collections. Will look for units I own, that are part of this plan.
// Also defining the buildOrders.
for (let cat in this.unitStat)
{
let Unit = this.unitStat[cat];
this.unit[cat] = this.unitCollection.filter(API3.Filters.byClassesAnd(Unit.classes));
this.unit[cat].registerUpdates();
if (this.canBuildUnits)
this.buildOrders.push([0, Unit.classes, this.unit[cat], Unit, cat]);
}
};
PETRA.AttackPlan.prototype.getName = function()
{
return this.name;
};
PETRA.AttackPlan.prototype.getType = function()
{
return this.type;
};
PETRA.AttackPlan.prototype.isStarted = function()
{
return this.state !== "unexecuted" && this.state !== "completing";
};
PETRA.AttackPlan.prototype.isPaused = function()
{
return this.paused;
};
PETRA.AttackPlan.prototype.setPaused = function(boolValue)
{
this.paused = boolValue;
};
/**
* Returns true if the attack can be executed at the current time
* Basically it checks we have enough units.
*/
PETRA.AttackPlan.prototype.canStart = function()
{
if (!this.canBuildUnits)
return true;
for (let unitCat in this.unitStat)
if (this.unit[unitCat].length < this.unitStat[unitCat].minSize)
return false;
return true;
};
PETRA.AttackPlan.prototype.mustStart = function()
{
if (this.isPaused())
return false;
if (!this.canBuildUnits)
return this.unitCollection.hasEntities();
let MaxReachedEverywhere = true;
let MinReachedEverywhere = true;
for (let unitCat in this.unitStat)
{
let 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)
return this.type == "Raid" && this.target && this.target.foundationProgress() &&
this.target.foundationProgress() > 50;
return false;
};
PETRA.AttackPlan.prototype.forceStart = function()
{
for (let unitCat in this.unitStat)
{
let Unit = this.unitStat[unitCat];
Unit.targetSize = 0;
Unit.minSize = 0;
}
this.forced = true;
};
PETRA.AttackPlan.prototype.emptyQueues = function()
{
this.queue.empty();
this.queueChamp.empty();
this.queueSiege.empty();
};
PETRA.AttackPlan.prototype.removeQueues = function(gameState)
{
gameState.ai.queueManager.removeQueue("plan_" + this.name);
gameState.ai.queueManager.removeQueue("plan_" + this.name + "_champ");
gameState.ai.queueManager.removeQueue("plan_" + this.name + "_siege");
};
/** Adds a build order. If resetQueue is true, this will reset the queue. */
PETRA.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;
let Unit = this.unitStat[name];
this.unit[name] = this.unitCollection.filter(API3.Filters.byClassesAnd(Unit.classes));
this.unit[name].registerUpdates();
this.buildOrders.push([0, Unit.classes, this.unit[name], Unit, name]);
if (resetQueue)
this.emptyQueues();
}
};
PETRA.AttackPlan.prototype.addSiegeUnits = function(gameState)
{
if (this.siegeState == 2 || this.state !== "unexecuted")
return false;
let civ = gameState.getPlayerCiv();
let classes = [[ "Siege", "Melee"], ["Siege", "Ranged"], ["Elephant", "Melee", "Champion"]];
let hasTrainer = [false, false, false];
for (let ent of gameState.getOwnTrainingFacilities().values())
{
let trainables = ent.trainableEntities(civ);
if (!trainables)
continue;
for (let trainable of trainables)
{
if (gameState.isTemplateDisabled(trainable))
continue;
let template = gameState.getTemplate(trainable);
if (!template || !template.available(gameState))
continue;
for (let i = 0; i < classes.length; ++i)
if (classes[i].every(c => template.hasClass(c)))
hasTrainer[i] = true;
}
}
if (hasTrainer.every(e => !e))
return false;
let i = this.name % classes.length;
for (let k = 0; k < classes.length; ++k)
{
if (hasTrainer[i])
break;
i = ++i % classes.length;
}
this.siegeState = 2;
let targetSize;
if (this.Config.difficulty < 3)
targetSize = this.type == "HugeAttack" ? Math.max(this.Config.difficulty, 1) : Math.max(this.Config.difficulty - 1, 0);
else
targetSize = this.type == "HugeAttack" ? this.Config.difficulty + 1 : this.Config.difficulty - 1;
targetSize = Math.max(Math.round(this.Config.popScaling * targetSize), this.type == "HugeAttack" ? 1 : 0);
if (!targetSize)
return true;
// no minsize as we don't want the plan to fail at the last minute though.
let stat = { "priority": 1, "minSize": 0, "targetSize": targetSize, "batchSize": Math.min(targetSize, 2),
"classes": classes[i], "interests": [ ["siegeStrength", 3] ] };
this.addBuildOrder(gameState, "Siege", stat, true);
return true;
};
/** Three returns possible: 1 is "keep going", 0 is "failed plan", 2 is "start". */
PETRA.AttackPlan.prototype.updatePreparation = function(gameState)
{
// 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")
{
// if our target was destroyed, go back to "unexecuted" state
if (this.targetPlayer === undefined || !this.target || !gameState.getEntityById(this.target.id()))
{
this.state = "unexecuted";
this.target = undefined;
}
else
{
// 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 (gameState.ai.elapsedTime < this.maxCompletingTime && this.hasForceOrder())
return 1;
return 2;
}
}
if (this.Config.debug > 3 && gameState.ai.playedTurn % 50 === 0)
this.debugAttack();
// if we need a transport, wait for some transport ships
if (this.overseas && !gameState.ai.HQ.navalManager.seaTransportShips[this.overseas].length)
return 1;
if (this.type != "Raid" || !this.forced) // Forced Raids have special purposes (as relic capture)
this.assignUnits(gameState);
if (this.type != "Raid" && gameState.ai.HQ.attackManager.getAttackInPreparation("Raid") !== undefined)
- this.reassignCavUnit(gameState); // reassign some cav (if any) to fasten raid preparations
+ this.reassignFastUnit(gameState); // reassign some fast units (if any) to fasten raid preparations
// Fasten the end game.
if (gameState.ai.playedTurn % 5 == 0 && this.hasSiegeUnits())
{
let totEnemies = 0;
let hasEnemies = false;
for (let i = 1; i < gameState.sharedScript.playersData.length; ++i)
{
if (!gameState.isPlayerEnemy(i) || gameState.ai.HQ.attackManager.defeated[i])
continue;
hasEnemies = true;
totEnemies += gameState.getEnemyUnits(i).length;
}
if (hasEnemies && this.unitCollection.length > 20 + 2 * totEnemies)
this.forceStart();
}
// special case: if we've reached max pop, and we can start the plan, start it.
if (gameState.getPopulationMax() - gameState.getPopulation() < 5)
{
let lengthMin = 16;
if (gameState.getPopulationMax() < 300)
lengthMin -= Math.floor(8 * (300 - gameState.getPopulationMax()) / 300);
if (this.canStart() || this.unitCollection.length > lengthMin)
{
this.emptyQueues();
}
else // Abort the plan so that its units will be reassigned to other plans.
{
if (this.Config.debug > 1)
{
let am = gameState.ai.HQ.attackManager;
API3.warn(" attacks upcoming: raid " + am.upcomingAttacks.Raid.length +
" rush " + am.upcomingAttacks.Rush.length +
" attack " + am.upcomingAttacks.Attack.length +
" huge " + am.upcomingAttacks.HugeAttack.length);
API3.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())
{
if (gameState.countOwnQueuedEntitiesWithMetadata("plan", +this.name) > 0)
{
// keep on while the units finish being trained, then we'll start
this.emptyQueues();
return 1;
}
}
else
{
if (this.canBuildUnits)
{
// We still have time left to recruit units and do stuffs.
if (this.siegeState == 0 || this.siegeState == 1 && gameState.ai.playedTurn % 5 == 0)
this.addSiegeUnits(gameState);
this.trainMoreUnits(gameState);
// may happen if we have no more training facilities and build orders are canceled
if (!this.buildOrders.length)
return 0; // will abort the plan
}
return 1;
}
// if we're here, it means we must start
this.state = "completing";
// Raids have their predefined target
if (!this.target && !this.chooseTarget(gameState))
return 0;
if (!this.overseas)
this.getPathToTarget(gameState);
if (this.type == "Raid")
this.maxCompletingTime = this.forced ? 0 : gameState.ai.elapsedTime + 20;
else
{
if (this.type == "Rush" || this.forced)
this.maxCompletingTime = gameState.ai.elapsedTime + 40;
else
this.maxCompletingTime = gameState.ai.elapsedTime + 60;
// warn our allies so that they can help if possible
if (!this.requested)
Engine.PostCommand(PlayerID, { "type": "attack-request", "source": PlayerID, "player": this.targetPlayer });
}
// Remove those units which were in a temporary bombing attack
for (let unitIds of gameState.ai.HQ.attackManager.bombingAttacks.values())
{
for (let entId of unitIds.values())
{
let ent = gameState.getEntityById(entId);
if (!ent || ent.getMetadata(PlayerID, "plan") != this.name)
continue;
unitIds.delete(entId);
ent.stopMoving();
}
}
let rallyPoint = this.rallyPoint;
let rallyIndex = gameState.ai.accessibility.getAccessValue(rallyPoint);
for (let ent of this.unitCollection.values())
{
// For the time being, if occupied in a transport, remove the unit from this plan TODO improve that
if (ent.getMetadata(PlayerID, "transport") !== undefined || ent.getMetadata(PlayerID, "transporter") !== undefined)
{
ent.setMetadata(PlayerID, "plan", -1);
continue;
}
ent.setMetadata(PlayerID, "role", "attack");
ent.setMetadata(PlayerID, "subrole", "completing");
let queued = false;
if (ent.resourceCarrying() && ent.resourceCarrying().length)
queued = PETRA.returnResources(gameState, ent);
let index = PETRA.getLandAccess(gameState, ent);
if (index == rallyIndex)
ent.moveToRange(rallyPoint[0], rallyPoint[1], 0, 15, queued);
else
gameState.ai.HQ.navalManager.requireTransport(gameState, ent, index, rallyIndex, rallyPoint);
}
// reset all queued units
this.removeQueues(gameState);
return 1;
};
PETRA.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 (let order of this.buildOrders)
{
let special = "Plan_" + this.name + "_" + order[4];
let aQueued = gameState.countOwnQueuedEntitiesWithMetadata("special", special);
aQueued += this.queue.countQueuedUnitsWithMetadata("special", special);
aQueued += this.queueChamp.countQueuedUnitsWithMetadata("special", special);
aQueued += this.queueSiege.countQueuedUnitsWithMetadata("special", special);
order[0] = order[2].length + aQueued;
}
this.buildOrders.sort((a, b) => {
let va = a[0]/a[3].targetSize - a[3].priority;
if (a[0] >= a[3].targetSize)
va += 1000;
let vb = b[0]/b[3].targetSize - b[3].priority;
if (b[0] >= b[3].targetSize)
vb += 1000;
return va - vb;
});
if (this.Config.debug > 1 && gameState.ai.playedTurn%50 === 0)
{
API3.warn("====================================");
API3.warn("======== build order for plan " + this.name);
for (let order of this.buildOrders)
{
let specialData = "Plan_"+this.name+"_"+order[4];
let inTraining = gameState.countOwnQueuedEntitiesWithMetadata("special", specialData);
let queue1 = this.queue.countQueuedUnitsWithMetadata("special", specialData);
let queue2 = this.queueChamp.countQueuedUnitsWithMetadata("special", specialData);
let queue3 = this.queueSiege.countQueuedUnitsWithMetadata("special", specialData);
API3.warn(" >>> " + order[4] + " done " + order[2].length + " training " + inTraining +
" queue " + queue1 + " champ " + queue2 + " siege " + queue3 + " >> need " + order[3].targetSize);
}
API3.warn("====================================");
}
let firstOrder = this.buildOrders[0];
if (firstOrder[0] < firstOrder[3].targetSize)
{
// find the actual queue we want
let queue = this.queue;
if (firstOrder[3].classes.indexOf("Siege") != -1 || firstOrder[3].classes.indexOf("Elephant") != -1 &&
firstOrder[3].classes.indexOf("Melee") != -1 && firstOrder[3].classes.indexOf("Champion") != -1)
queue = this.queueSiege;
else if (firstOrder[3].classes.indexOf("Hero") != -1)
queue = this.queueSiege;
else if (firstOrder[3].classes.indexOf("Champion") != -1)
queue = this.queueChamp;
if (queue.length() <= 5)
{
let template = gameState.ai.HQ.findBestTrainableUnit(gameState, firstOrder[1], firstOrder[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 > 1)
API3.warn("attack no template found " + firstOrder[1]);
delete this.unitStat[firstOrder[4]]; // deleting the associated unitstat.
this.buildOrders.splice(0, 1);
}
else
{
if (this.Config.debug > 2)
API3.warn("attack template " + template + " added for plan " + this.name);
let max = firstOrder[3].batchSize;
let specialData = "Plan_" + this.name + "_" + firstOrder[4];
let data = { "plan": this.name, "special": specialData, "base": 0 };
data.role = gameState.getTemplate(template).hasClass("CitizenSoldier") ? "worker" : "attack";
let trainingPlan = new PETRA.TrainingPlan(gameState, template, data, max, max);
if (trainingPlan.template)
queue.addPlan(trainingPlan);
else if (this.Config.debug > 1)
API3.warn("training plan canceled because no template for " + template + " build1 " + uneval(firstOrder[1]) +
" build3 " + uneval(firstOrder[3].interests));
}
}
}
};
PETRA.AttackPlan.prototype.assignUnits = function(gameState)
{
let plan = this.name;
let added = false;
- // If we can not build units, assign all available except those affected to allied defense to the current attack
+ // If we can not build units, assign all available except those affected to allied defense to the current attack.
if (!this.canBuildUnits)
{
for (let ent of gameState.getOwnUnits().values())
{
if (ent.getMetadata(PlayerID, "allied") || !this.isAvailableUnit(gameState, ent))
continue;
ent.setMetadata(PlayerID, "plan", plan);
this.unitCollection.updateEnt(ent);
added = true;
}
return added;
}
if (this.type == "Raid")
{
- // Raid are fast cavalry attack: assign all cav except some for hunting
+ // Raids are quick attacks: assign all FastMoving soldiers except some for hunting.
let num = 0;
for (let ent of gameState.getOwnUnits().values())
{
- if (!ent.hasClass("Cavalry") || !this.isAvailableUnit(gameState, ent))
+ if (!ent.hasClass("FastMoving") || !this.isAvailableUnit(gameState, ent))
continue;
if (num++ < 2)
continue;
ent.setMetadata(PlayerID, "plan", plan);
this.unitCollection.updateEnt(ent);
added = true;
}
return added;
}
- // Assign all units without specific role
+ // Assign all units without specific role.
for (let ent of gameState.getOwnEntitiesByRole(undefined, true).values())
{
if (!ent.hasClass("Unit") || !this.isAvailableUnit(gameState, ent))
continue;
if (ent.hasClass("Ship") || ent.hasClass("Support") || ent.attackTypes() === undefined)
continue;
ent.setMetadata(PlayerID, "plan", plan);
this.unitCollection.updateEnt(ent);
added = true;
}
- // Add units previously in a plan, but which left it because needed for defense or attack finished
+ // Add units previously in a plan, but which left it because needed for defense or attack finished.
for (let ent of gameState.ai.HQ.attackManager.outOfPlan.values())
{
if (!this.isAvailableUnit(gameState, ent))
continue;
ent.setMetadata(PlayerID, "plan", plan);
this.unitCollection.updateEnt(ent);
added = true;
}
// Finally add also some workers,
// If Rush, assign all kind of workers, keeping only a minimum number of defenders
// Otherwise, assign only some idle workers if too much of them
let num = 0;
let numbase = {};
let keep = this.type != "Rush" ?
6 + 4 * gameState.getNumPlayerEnemies() + 8 * this.Config.personality.defensive : 8;
keep = Math.round(this.Config.popScaling * keep);
for (let ent of gameState.getOwnEntitiesByRole("worker", true).values())
{
if (!ent.hasClass("CitizenSoldier") || !this.isAvailableUnit(gameState, ent))
continue;
let baseID = ent.getMetadata(PlayerID, "base");
if (baseID)
numbase[baseID] = numbase[baseID] ? ++numbase[baseID] : 1;
else
{
API3.warn("Petra problem ent without base ");
PETRA.dumpEntity(ent);
continue;
}
if (num++ < keep || numbase[baseID] < 5)
continue;
if (this.type != "Rush" && ent.getMetadata(PlayerID, "subrole") != "idle")
continue;
ent.setMetadata(PlayerID, "plan", plan);
this.unitCollection.updateEnt(ent);
added = true;
}
return added;
};
PETRA.AttackPlan.prototype.isAvailableUnit = function(gameState, ent)
{
if (!ent.position())
return false;
if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") !== -1 ||
ent.getMetadata(PlayerID, "transport") !== undefined || ent.getMetadata(PlayerID, "transporter") !== undefined)
return false;
if (gameState.ai.HQ.victoryManager.criticalEnts.has(ent.id()) && (this.overseas || ent.healthLevel() < 0.8))
return false;
return true;
};
-/** Reassign one (at each turn) Cav unit to fasten raid preparation. */
-PETRA.AttackPlan.prototype.reassignCavUnit = function(gameState)
+/** Reassign one (at each turn) FastMoving unit to fasten raid preparation. */
+PETRA.AttackPlan.prototype.reassignFastUnit = function(gameState)
{
for (let ent of this.unitCollection.values())
{
if (!ent.position() || ent.getMetadata(PlayerID, "transport") !== undefined)
continue;
- if (!ent.hasClass("Cavalry") || !ent.hasClass("CitizenSoldier"))
+ if (!ent.hasClass("FastMoving") || !ent.hasClass("CitizenSoldier"))
continue;
let raid = gameState.ai.HQ.attackManager.getAttackInPreparation("Raid");
ent.setMetadata(PlayerID, "plan", raid.name);
this.unitCollection.updateEnt(ent);
raid.unitCollection.updateEnt(ent);
return;
}
};
PETRA.AttackPlan.prototype.chooseTarget = function(gameState)
{
if (this.targetPlayer === undefined)
{
this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this);
if (this.targetPlayer === undefined)
return false;
}
this.target = this.getNearestTarget(gameState, this.rallyPoint);
if (!this.target)
{
if (this.uniqueTargetId)
return false;
// may-be all our previous enemey target (if not recomputed here) have been destroyed ?
this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this);
if (this.targetPlayer !== undefined)
this.target = this.getNearestTarget(gameState, this.rallyPoint);
if (!this.target)
return false;
}
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)
let targetIndex = PETRA.getLandAccess(gameState, this.target);
let rallyIndex = gameState.ai.accessibility.getAccessValue(this.rallyPoint);
if (targetIndex != rallyIndex)
{
let distminSame = Math.min();
let rallySame;
let distminDiff = Math.min();
let rallyDiff;
for (let base of gameState.ai.HQ.baseManagers)
{
let anchor = base.anchor;
if (!anchor || !anchor.position())
continue;
let dist = API3.SquareVectorDistance(anchor.position(), this.targetPos);
if (base.accessIndex == targetIndex)
{
if (dist >= distminSame)
continue;
distminSame = dist;
rallySame = anchor.position();
}
else
{
dist /= Math.sqrt(gameState.ai.accessibility.regionSize[base.accessIndex]);
if (dist >= distminDiff)
continue;
distminDiff = dist;
rallyDiff = anchor.position();
}
}
if (rallySame)
{
this.rallyPoint = rallySame;
this.overseas = 0;
}
else if (rallyDiff)
{
rallyIndex = gameState.ai.accessibility.getAccessValue(rallyDiff);
this.rallyPoint = rallyDiff;
let sea = gameState.ai.HQ.getSeaBetweenIndices(gameState, rallyIndex, targetIndex);
if (sea)
{
this.overseas = sea;
gameState.ai.HQ.navalManager.setMinimalTransportShips(gameState, this.overseas, this.neededShips);
}
else
{
API3.warn("Petra: " + this.type + " " + this.name + " has an inaccessible target" +
" with indices " + rallyIndex + " " + targetIndex + " from " + this.target.templateName());
return false;
}
}
}
else if (this.overseas)
this.overseas = 0;
return true;
};
/**
* sameLand true means that we look for a target for which we do not need to take a transport
*/
PETRA.AttackPlan.prototype.getNearestTarget = function(gameState, position, sameLand)
{
this.isBlocked = false;
// Temporary variables needed by isValidTarget
this.gameState = gameState;
this.sameLand = sameLand && sameLand > 1 ? sameLand : false;
let targets;
if (this.uniqueTargetId)
{
targets = new API3.EntityCollection(gameState.sharedScript);
let ent = gameState.getEntityById(this.uniqueTargetId);
if (ent)
targets.addEnt(ent);
}
else
{
if (this.type == "Raid")
targets = this.raidTargetFinder(gameState);
else if (this.type == "Rush" || this.type == "Attack")
{
targets = this.rushTargetFinder(gameState, this.targetPlayer);
if (!targets.hasEntities() && (this.hasSiegeUnits() || this.forced))
targets = this.defaultTargetFinder(gameState, this.targetPlayer);
}
else
targets = this.defaultTargetFinder(gameState, this.targetPlayer);
}
if (!targets.hasEntities())
return undefined;
// picking the nearest target
let target;
let minDist = Math.min();
for (let ent of targets.values())
{
if (this.targetPlayer == 0 && gameState.getVictoryConditions().has("capture_the_relic") &&
(!ent.hasClass("Relic") || gameState.ai.HQ.victoryManager.targetedGaiaRelics.has(ent.id())))
continue;
// Do not bother with some pointless targets
if (!this.isValidTarget(ent))
continue;
let dist = API3.SquareVectorDistance(ent.position(), position);
// In normal attacks, disfavor fields
if (this.type != "Rush" && this.type != "Raid" && ent.hasClass("Field"))
dist += 100000;
if (dist < minDist)
{
minDist = dist;
target = ent;
}
}
if (!target)
return undefined;
// Check that we can reach this target
target = this.checkTargetObstruction(gameState, target, position);
if (!target)
return undefined;
if (this.targetPlayer == 0 && gameState.getVictoryConditions().has("capture_the_relic") && target.hasClass("Relic"))
gameState.ai.HQ.victoryManager.targetedGaiaRelics.set(target.id(), [this.name]);
// Rushes can change their enemy target if nothing found with the preferred enemy
// Obstruction also can change the enemy target
this.targetPlayer = target.owner();
return target;
};
/**
* Default target finder aims for conquest critical targets
* We must apply the *same* selection (isValidTarget) as done in getNearestTarget
*/
PETRA.AttackPlan.prototype.defaultTargetFinder = function(gameState, playerEnemy)
{
let targets = new API3.EntityCollection(gameState.sharedScript);
if (gameState.getVictoryConditions().has("wonder"))
for (let ent of gameState.getEnemyStructures(playerEnemy).filter(API3.Filters.byClass("Wonder")).values())
targets.addEnt(ent);
if (gameState.getVictoryConditions().has("regicide"))
for (let ent of gameState.getEnemyUnits(playerEnemy).filter(API3.Filters.byClass("Hero")).values())
targets.addEnt(ent);
if (gameState.getVictoryConditions().has("capture_the_relic"))
for (let ent of gameState.updatingGlobalCollection("allRelics", API3.Filters.byClass("Relic")).filter(relic => relic.owner() == playerEnemy).values())
targets.addEnt(ent);
targets = targets.filter(this.isValidTarget, this);
if (targets.hasEntities())
return targets;
let validTargets = gameState.getEnemyStructures(playerEnemy).filter(this.isValidTarget, this);
targets = validTargets.filter(API3.Filters.byClass("CivCentre"));
if (!targets.hasEntities())
targets = validTargets.filter(API3.Filters.byClass("ConquestCritical"));
// If there's nothing, attack anything else that's less critical
if (!targets.hasEntities())
targets = validTargets.filter(API3.Filters.byClass("Town"));
if (!targets.hasEntities())
targets = validTargets.filter(API3.Filters.byClass("Village"));
// No buildings, attack anything conquest critical, units included.
// TODO Should add naval attacks against the last remaining ships.
if (!targets.hasEntities())
targets = gameState.getEntities(playerEnemy).filter(API3.Filters.byClass("ConquestCritical")).
filter(API3.Filters.not(API3.Filters.byClass("Ship")));
return targets;
};
PETRA.AttackPlan.prototype.isValidTarget = function(ent)
{
if (!ent.position())
return false;
if (this.sameLand && PETRA.getLandAccess(this.gameState, ent) != this.sameLand)
return false;
return !ent.decaying() || ent.getDefaultArrow() || ent.isGarrisonHolder() && ent.garrisoned().length;
};
/** Rush target finder aims at isolated non-defended buildings */
PETRA.AttackPlan.prototype.rushTargetFinder = function(gameState, playerEnemy)
{
let targets = new API3.EntityCollection(gameState.sharedScript);
let buildings;
if (playerEnemy !== undefined)
buildings = gameState.getEnemyStructures(playerEnemy).toEntityArray();
else
buildings = gameState.getEnemyStructures().toEntityArray();
if (!buildings.length)
return targets;
this.position = this.unitCollection.getCentrePosition();
if (!this.position)
this.position = this.rallyPoint;
let target;
let minDist = Math.min();
for (let building of buildings)
{
if (building.owner() == 0)
continue;
if (building.hasDefensiveFire())
continue;
if (!this.isValidTarget(building))
continue;
let pos = building.position();
let defended = false;
for (let defense of buildings)
{
if (!defense.hasDefensiveFire())
continue;
let dist = API3.SquareVectorDistance(pos, defense.position());
if (dist < 6400) // TODO check on defense range rather than this fixed 80*80
{
defended = true;
break;
}
}
if (defended)
continue;
let dist = API3.SquareVectorDistance(pos, this.position);
if (dist > minDist)
continue;
minDist = dist;
target = building;
}
if (target)
targets.addEnt(target);
if (!targets.hasEntities() && this.type == "Rush" && playerEnemy)
targets = this.rushTargetFinder(gameState);
return targets;
};
/** Raid target finder aims at destructing foundations from which our defenseManager has attacked the builders */
PETRA.AttackPlan.prototype.raidTargetFinder = function(gameState)
{
let targets = new API3.EntityCollection(gameState.sharedScript);
for (let targetId of gameState.ai.HQ.defenseManager.targetList)
{
let target = gameState.getEntityById(targetId);
if (target && target.position())
targets.addEnt(target);
}
return targets;
};
/**
* Check that we can have a path to this target
* otherwise we may be blocked by walls and try to react accordingly
* This is done only when attacker and target are on the same land
*/
PETRA.AttackPlan.prototype.checkTargetObstruction = function(gameState, target, position)
{
if (PETRA.getLandAccess(gameState, target) != gameState.ai.accessibility.getAccessValue(position))
return target;
let targetPos = target.position();
let startPos = { "x": position[0], "y": position[1] };
let endPos = { "x": targetPos[0], "y": targetPos[1] };
let blocker;
let path = Engine.ComputePath(startPos, endPos, gameState.getPassabilityClassMask("default"));
if (!path.length)
return undefined;
let pathPos = [path[0].x, path[0].y];
let dist = API3.VectorDistance(pathPos, targetPos);
let radius = target.obstructionRadius().max;
for (let struct of gameState.getEnemyStructures().values())
{
if (!struct.position() || !struct.get("Obstruction") || struct.hasClass("Field"))
continue;
// we consider that we can reach the target, but nonetheless check that we did not cross any enemy gate
if (dist < radius + 10 && !struct.hasClass("Gate"))
continue;
// Check that we are really blocked by this structure, i.e. advancing by 1+0.8(clearance)m
// in the target direction would bring us inside its obstruction.
let structPos = struct.position();
let x = pathPos[0] - structPos[0] + 1.8 * (targetPos[0] - pathPos[0]) / dist;
let y = pathPos[1] - structPos[1] + 1.8 * (targetPos[1] - pathPos[1]) / dist;
if (struct.get("Obstruction/Static"))
{
if (!struct.angle())
continue;
let angle = struct.angle();
let width = +struct.get("Obstruction/Static/@width");
let depth = +struct.get("Obstruction/Static/@depth");
let cosa = Math.cos(angle);
let sina = Math.sin(angle);
let u = x * cosa - y * sina;
let v = x * sina + y * cosa;
if (Math.abs(u) < width/2 && Math.abs(v) < depth/2)
{
blocker = struct;
break;
}
}
else if (struct.get("Obstruction/Obstructions"))
{
if (!struct.angle())
continue;
let angle = struct.angle();
let width = +struct.get("Obstruction/Obstructions/Door/@width");
let depth = +struct.get("Obstruction/Obstructions/Door/@depth");
let doorHalfWidth = width / 2;
width += +struct.get("Obstruction/Obstructions/Left/@width");
depth = Math.max(depth, +struct.get("Obstruction/Obstructions/Left/@depth"));
width += +struct.get("Obstruction/Obstructions/Right/@width");
depth = Math.max(depth, +struct.get("Obstruction/Obstructions/Right/@depth"));
let cosa = Math.cos(angle);
let sina = Math.sin(angle);
let u = x * cosa - y * sina;
let v = x * sina + y * cosa;
if (Math.abs(u) < width/2 && Math.abs(v) < depth/2)
{
blocker = struct;
break;
}
// check that the path does not cross this gate (could happen if not locked)
for (let i = 1; i < path.length; ++i)
{
let u1 = (path[i-1].x - structPos[0]) * cosa - (path[i-1].y - structPos[1]) * sina;
let v1 = (path[i-1].x - structPos[0]) * sina + (path[i-1].y - structPos[1]) * cosa;
let u2 = (path[i].x - structPos[0]) * cosa - (path[i].y - structPos[1]) * sina;
let v2 = (path[i].x - structPos[0]) * sina + (path[i].y - structPos[1]) * cosa;
if (v1 * v2 < 0)
{
let u0 = (u1*v2 - u2*v1) / (v2-v1);
if (Math.abs(u0) > doorHalfWidth)
continue;
blocker = struct;
break;
}
}
if (blocker)
break;
}
else if (struct.get("Obstruction/Unit"))
{
let r = +this.get("Obstruction/Unit/@radius");
if (x*x + y*y < r*r)
{
blocker = struct;
break;
}
}
}
if (blocker)
{
this.isBlocked = true;
return blocker;
}
return target;
};
PETRA.AttackPlan.prototype.getPathToTarget = function(gameState, fixedRallyPoint = false)
{
let startAccess = gameState.ai.accessibility.getAccessValue(this.rallyPoint);
let endAccess = PETRA.getLandAccess(gameState, this.target);
if (startAccess != endAccess)
return false;
Engine.ProfileStart("AI Compute path");
let startPos = { "x": this.rallyPoint[0], "y": this.rallyPoint[1] };
let endPos = { "x": this.targetPos[0], "y": this.targetPos[1] };
let path = Engine.ComputePath(startPos, endPos, gameState.getPassabilityClassMask("large"));
this.path = [];
this.path.push(this.targetPos);
for (let p in path)
this.path.push([path[p].x, path[p].y]);
this.path.push(this.rallyPoint);
this.path.reverse();
// Change the rally point to something useful
if (!fixedRallyPoint)
this.setRallyPoint(gameState);
Engine.ProfileStop();
return true;
};
/** Set rally point at the border of our territory */
PETRA.AttackPlan.prototype.setRallyPoint = function(gameState)
{
for (let i = 0; i < this.path.length; ++i)
{
if (gameState.ai.HQ.territoryMap.getOwner(this.path[i]) === PlayerID)
continue;
if (i === 0)
this.rallyPoint = this.path[0];
else if (i > 1 && gameState.ai.HQ.isDangerousLocation(gameState, this.path[i-1], 20))
{
this.rallyPoint = this.path[i-2];
this.path.splice(0, i-2);
}
else
{
this.rallyPoint = this.path[i-1];
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.
*/
PETRA.AttackPlan.prototype.StartAttack = function(gameState)
{
if (this.Config.debug > 1)
API3.warn("start attack " + this.name + " with type " + this.type);
// if our target was destroyed during preparation, choose a new one
if ((this.targetPlayer === undefined || !this.target || !gameState.getEntityById(this.target.id())) &&
!this.chooseTarget(gameState))
return false;
// erase our queue. This will stop any leftover unit from being trained.
this.removeQueues(gameState);
for (let ent of this.unitCollection.values())
{
ent.setMetadata(PlayerID, "subrole", "walking");
let stance = ent.isPackable() ? "standground" : "aggressive";
if (ent.getStance() != stance)
ent.setStance(stance);
}
let rallyAccess = gameState.ai.accessibility.getAccessValue(this.rallyPoint);
let targetAccess = PETRA.getLandAccess(gameState, this.target);
if (rallyAccess == targetAccess)
{
if (!this.path)
this.getPathToTarget(gameState, true);
if (!this.path || !this.path[0][0] || !this.path[0][1])
return false;
this.overseas = 0;
this.state = "walking";
this.unitCollection.moveToRange(this.path[0][0], this.path[0][1], 0, 15);
}
else
{
this.overseas = gameState.ai.HQ.getSeaBetweenIndices(gameState, rallyAccess, targetAccess);
if (!this.overseas)
return false;
this.state = "transporting";
// TODO require a global transport for the collection,
// and put back its state to "walking" when the transport is finished
for (let ent of this.unitCollection.values())
gameState.ai.HQ.navalManager.requireTransport(gameState, ent, rallyAccess, targetAccess, this.targetPos);
}
return true;
};
/** Runs every turn after the attack is executed */
PETRA.AttackPlan.prototype.update = function(gameState, events)
{
if (!this.unitCollection.hasEntities())
return 0;
Engine.ProfileStart("Update Attack");
this.position = this.unitCollection.getCentrePosition();
// we are transporting our units, let's wait
// TODO instead of state "arrived", made a state "walking" with a new path
if (this.state == "transporting")
this.UpdateTransporting(gameState, events);
if (this.state == "walking" && !this.UpdateWalking(gameState, events))
{
Engine.ProfileStop();
return 0;
}
if (this.state == "arrived")
{
// let's proceed on with whatever happens now.
this.state = "";
this.startingAttack = true;
this.unitCollection.forEach(ent => {
ent.stopMoving();
ent.setMetadata(PlayerID, "subrole", "attacking");
});
if (this.type == "Rush") // try to find a better target for rush
{
let newtarget = this.getNearestTarget(gameState, this.position);
if (newtarget)
{
this.target = newtarget;
this.targetPos = this.target.position();
}
}
}
// basic state of attacking.
if (this.state == "")
{
// First update the target and/or its position if needed
if (!this.UpdateTarget(gameState))
{
Engine.ProfileStop();
return false;
}
let time = gameState.ai.elapsedTime;
let attackedByStructure = {};
for (let evt of events.Attacked)
{
if (!this.unitCollection.hasEntId(evt.target))
continue;
let attacker = gameState.getEntityById(evt.attacker);
let ourUnit = gameState.getEntityById(evt.target);
if (!ourUnit || !attacker || !attacker.position())
continue;
if (!attacker.hasClass("Unit"))
{
attackedByStructure[evt.target] = true;
continue;
}
if (PETRA.isSiegeUnit(ourUnit))
{ // if our siege units are attacked, we'll send some units to deal with enemies.
let collec = this.unitCollection.filter(API3.Filters.not(API3.Filters.byClass("Siege"))).filterNearest(ourUnit.position(), 5);
for (let ent of collec.values())
{
if (PETRA.isSiegeUnit(ent)) // needed as mauryan elephants are not filtered out
continue;
let allowCapture = PETRA.allowCapture(gameState, ent, attacker);
if (!ent.canAttackTarget(attacker, allowCapture))
continue;
ent.attack(attacker.id(), allowCapture);
ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
}
// And if this attacker is a non-ranged siege unit and our unit also, attack it
if (PETRA.isSiegeUnit(attacker) && attacker.hasClass("Melee") && ourUnit.hasClass("Melee") && ourUnit.canAttackTarget(attacker, PETRA.allowCapture(gameState, ourUnit, attacker)))
{
ourUnit.attack(attacker.id(), PETRA.allowCapture(gameState, ourUnit, attacker));
ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
}
}
else
{
if (this.isBlocked && !ourUnit.hasClass("Ranged") && attacker.hasClass("Ranged"))
{
// do not react if our melee units are attacked by ranged one and we are blocked by walls
// TODO check that the attacker is from behind the wall
continue;
}
else if (PETRA.isSiegeUnit(attacker))
{ // if our unit is attacked by a siege unit, we'll send some melee units to help it.
let collec = this.unitCollection.filter(API3.Filters.byClass("Melee")).filterNearest(ourUnit.position(), 5);
for (let ent of collec.values())
{
let allowCapture = PETRA.allowCapture(gameState, ent, attacker);
if (!ent.canAttackTarget(attacker, allowCapture))
continue;
ent.attack(attacker.id(), allowCapture);
ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
}
}
else
{
// Look first for nearby units to help us if possible
let collec = this.unitCollection.filterNearest(ourUnit.position(), 2);
for (let ent of collec.values())
{
let allowCapture = PETRA.allowCapture(gameState, ent, attacker);
if (PETRA.isSiegeUnit(ent) || !ent.canAttackTarget(attacker, allowCapture))
continue;
let orderData = ent.unitAIOrderData();
if (orderData && orderData.length && orderData[0].target)
{
if (orderData[0].target === attacker.id())
continue;
let target = gameState.getEntityById(orderData[0].target);
if (target && !target.hasClass("Structure") && !target.hasClass("Support"))
continue;
}
ent.attack(attacker.id(), allowCapture);
ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
}
// Then the unit under attack: abandon its target (if it was a structure or a support) and retaliate
// also if our unit is attacking a range unit and the attacker is a melee unit, retaliate
let orderData = ourUnit.unitAIOrderData();
if (orderData && orderData.length && orderData[0].target)
{
if (orderData[0].target === attacker.id())
continue;
let target = gameState.getEntityById(orderData[0].target);
if (target && !target.hasClass("Structure") && !target.hasClass("Support"))
{
if (!target.hasClass("Ranged") || !attacker.hasClass("Melee"))
continue;
}
}
let allowCapture = PETRA.allowCapture(gameState, ourUnit, attacker);
if (ourUnit.canAttackTarget(attacker, allowCapture))
{
ourUnit.attack(attacker.id(), allowCapture);
ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
}
}
}
}
let enemyUnits = gameState.getEnemyUnits(this.targetPlayer);
let enemyStructures = gameState.getEnemyStructures(this.targetPlayer);
// Count the number of times an enemy is targeted, to prevent all units to follow the same target
let unitTargets = {};
for (let ent of this.unitCollection.values())
{
if (ent.hasClass("Ship")) // TODO What to do with ships
continue;
let orderData = ent.unitAIOrderData();
if (!orderData || !orderData.length || !orderData[0].target)
continue;
let targetId = orderData[0].target;
let target = gameState.getEntityById(targetId);
if (!target || target.hasClass("Structure"))
continue;
if (!(targetId in unitTargets))
{
if (PETRA.isSiegeUnit(target) || target.hasClass("Hero"))
unitTargets[targetId] = -8;
else if (target.hasClass("Champion") || target.hasClass("Ship"))
unitTargets[targetId] = -5;
else
unitTargets[targetId] = -3;
}
++unitTargets[targetId];
}
let veto = {};
for (let target in unitTargets)
if (unitTargets[target] > 0)
veto[target] = true;
let targetClassesUnit;
let targetClassesSiege;
if (this.type == "Rush")
targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Palisade", "Wall", "Tower", "Fortress"], "vetoEntities": veto };
else
{
if (this.target.hasClass("Fortress"))
targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Palisade", "Wall"], "vetoEntities": veto };
else if (this.target.hasClass("Palisade") || this.target.hasClass("Wall"))
targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Fortress"], "vetoEntities": veto };
else
targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Palisade", "Wall", "Fortress"], "vetoEntities": veto };
}
if (this.target.hasClass("Structure"))
targetClassesSiege = { "attack": ["Structure"], "avoid": [], "vetoEntities": veto };
else
targetClassesSiege = { "attack": ["Unit", "Structure"], "avoid": [], "vetoEntities": veto };
// do not loose time destroying buildings which do not help enemy's defense and can be easily captured later
if (this.target.hasDefensiveFire())
{
targetClassesUnit.avoid = targetClassesUnit.avoid.concat("House", "Storehouse", "Farmstead", "Field", "Blacksmith");
targetClassesSiege.avoid = targetClassesSiege.avoid.concat("House", "Storehouse", "Farmstead", "Field", "Blacksmith");
}
if (this.unitCollUpdateArray === undefined || !this.unitCollUpdateArray.length)
this.unitCollUpdateArray = this.unitCollection.toIdArray();
// Let's check a few units each time we update (currently 10) except when attack starts
let lgth = this.unitCollUpdateArray.length < 15 || this.startingAttack ? this.unitCollUpdateArray.length : 10;
for (let check = 0; check < lgth; check++)
{
let ent = gameState.getEntityById(this.unitCollUpdateArray[check]);
if (!ent || !ent.position())
continue;
// Do not reassign units which have reacted to an attack in that same turn
if (ent.getMetadata(PlayerID, "lastAttackPlanUpdateTime") == time)
continue;
let targetId;
let orderData = ent.unitAIOrderData();
if (orderData && orderData.length && orderData[0].target)
targetId = orderData[0].target;
// update the order if needed
let needsUpdate = false;
let maybeUpdate = false;
let siegeUnit = PETRA.isSiegeUnit(ent);
if (ent.isIdle())
needsUpdate = true;
else if (siegeUnit && targetId)
{
let target = gameState.getEntityById(targetId);
if (!target || gameState.isPlayerAlly(target.owner()))
needsUpdate = true;
else if (unitTargets[targetId] && unitTargets[targetId] > 0)
{
needsUpdate = true;
--unitTargets[targetId];
}
else if (!target.hasClass("Structure"))
maybeUpdate = true;
}
else if (targetId)
{
let target = gameState.getEntityById(targetId);
if (!target || gameState.isPlayerAlly(target.owner()))
needsUpdate = true;
else if (unitTargets[targetId] && unitTargets[targetId] > 0)
{
needsUpdate = true;
--unitTargets[targetId];
}
else if (target.hasClass("Ship") && !ent.hasClass("Ship"))
maybeUpdate = true;
else if (attackedByStructure[ent.id()] && target.hasClass("Field"))
maybeUpdate = true;
- else if (!ent.hasClass("Cavalry") && !ent.hasClass("Ranged") &&
+ else if (!ent.hasClass("FastMoving") && !ent.hasClass("Ranged") &&
target.hasClass("FemaleCitizen") && target.unitAIState().split(".")[1] == "FLEEING")
maybeUpdate = true;
}
// don't update too soon if not necessary
if (!needsUpdate)
{
if (!maybeUpdate)
continue;
let deltat = ent.unitAIState() === "INDIVIDUAL.COMBAT.APPROACHING" ? 10 : 5;
let lastAttackPlanUpdateTime = ent.getMetadata(PlayerID, "lastAttackPlanUpdateTime");
if (lastAttackPlanUpdateTime && time - lastAttackPlanUpdateTime < deltat)
continue;
}
ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
let range = 60;
let attackTypes = ent.attackTypes();
if (this.isBlocked)
{
if (attackTypes && attackTypes.indexOf("Ranged") !== -1)
range = ent.attackRange("Ranged").max;
else if (attackTypes && attackTypes.indexOf("Melee") !== -1)
range = ent.attackRange("Melee").max;
else
range = 10;
}
else if (attackTypes && attackTypes.indexOf("Ranged") !== -1)
range = 30 + ent.attackRange("Ranged").max;
- else if (ent.hasClass("Cavalry"))
+ else if (ent.hasClass("FastMoving"))
range += 30;
range *= range;
let entAccess = PETRA.getLandAccess(gameState, ent);
// Checking for gates if we're a siege unit.
if (siegeUnit)
{
let mStruct = enemyStructures.filter(enemy => {
if (!enemy.position() || !ent.canAttackTarget(enemy, PETRA.allowCapture(gameState, ent, enemy)))
return false;
if (API3.SquareVectorDistance(enemy.position(), ent.position()) > range)
return false;
if (enemy.foundationProgress() == 0)
return false;
if (PETRA.getLandAccess(gameState, enemy) != entAccess)
return false;
return true;
}).toEntityArray();
if (mStruct.length)
{
mStruct.sort((structa, structb) => {
let vala = structa.costSum();
if (structa.hasClass("Gate") && ent.canAttackClass("Wall"))
vala += 10000;
else if (structa.hasDefensiveFire())
vala += 1000;
else if (structa.hasClass("ConquestCritical"))
vala += 200;
let valb = structb.costSum();
if (structb.hasClass("Gate") && ent.canAttackClass("Wall"))
valb += 10000;
else if (structb.hasDefensiveFire())
valb += 1000;
else if (structb.hasClass("ConquestCritical"))
valb += 200;
return valb - vala;
});
if (mStruct[0].hasClass("Gate"))
ent.attack(mStruct[0].id(), PETRA.allowCapture(gameState, ent, mStruct[0]));
else
{
let rand = randIntExclusive(0, mStruct.length * 0.2);
ent.attack(mStruct[rand].id(), PETRA.allowCapture(gameState, ent, mStruct[rand]));
}
}
else
{
if (!ent.hasClass("Ranged"))
{
let targetClasses = { "attack": targetClassesSiege.attack, "avoid": targetClassesSiege.avoid.concat("Ship"), "vetoEntities": veto };
ent.attackMove(this.targetPos[0], this.targetPos[1], targetClasses);
}
else
ent.attackMove(this.targetPos[0], this.targetPos[1], targetClassesSiege);
}
}
else
{
- let nearby = !ent.hasClass("Cavalry") && !ent.hasClass("Ranged");
+ let nearby = !ent.hasClass("FastMoving") && !ent.hasClass("Ranged");
let mUnit = enemyUnits.filter(enemy => {
if (!enemy.position() || !ent.canAttackTarget(enemy, PETRA.allowCapture(gameState, ent, enemy)))
return false;
if (enemy.hasClass("Animal"))
return false;
if (nearby && enemy.hasClass("FemaleCitizen") && enemy.unitAIState().split(".")[1] == "FLEEING")
return false;
let dist = API3.SquareVectorDistance(enemy.position(), ent.position());
if (dist > range)
return false;
if (PETRA.getLandAccess(gameState, enemy) != entAccess)
return false;
// if already too much units targeting this enemy, let's continue towards our main target
if (veto[enemy.id()] && API3.SquareVectorDistance(this.targetPos, ent.position()) > 2500)
return false;
enemy.setMetadata(PlayerID, "distance", Math.sqrt(dist));
return true;
}, this).toEntityArray();
if (mUnit.length)
{
mUnit.sort((unitA, unitB) => {
let vala = unitA.hasClass("Support") ? 50 : 0;
if (ent.countersClasses(unitA.classes()))
vala += 100;
let valb = unitB.hasClass("Support") ? 50 : 0;
if (ent.countersClasses(unitB.classes()))
valb += 100;
let distA = unitA.getMetadata(PlayerID, "distance");
let distB = unitB.getMetadata(PlayerID, "distance");
if (distA && distB)
{
vala -= distA;
valb -= distB;
}
if (veto[unitA.id()])
vala -= 20000;
if (veto[unitB.id()])
valb -= 20000;
return valb - vala;
});
let rand = randIntExclusive(0, mUnit.length * 0.1);
ent.attack(mUnit[rand].id(), PETRA.allowCapture(gameState, ent, mUnit[rand]));
}
// This may prove dangerous as we may be blocked by something we
// cannot attack. See similar behaviour at #5741.
else if (this.isBlocked && ent.canAttackTarget(this.target, false))
ent.attack(this.target.id(), false);
else if (API3.SquareVectorDistance(this.targetPos, ent.position()) > 2500)
{
let targetClasses = targetClassesUnit;
if (maybeUpdate && ent.unitAIState() === "INDIVIDUAL.COMBAT.APPROACHING") // we may be blocked by walls, attack everything
{
if (!ent.hasClass("Ranged") && !ent.hasClass("Ship"))
targetClasses = { "attack": ["Unit", "Structure"], "avoid": ["Ship"], "vetoEntities": veto };
else
targetClasses = { "attack": ["Unit", "Structure"], "vetoEntities": veto };
}
else if (!ent.hasClass("Ranged") && !ent.hasClass("Ship"))
targetClasses = { "attack": targetClassesUnit.attack, "avoid": targetClassesUnit.avoid.concat("Ship"), "vetoEntities": veto };
ent.attackMove(this.targetPos[0], this.targetPos[1], targetClasses);
}
else
{
let mStruct = enemyStructures.filter(enemy => {
if (this.isBlocked && enemy.id() != this.target.id())
return false;
if (!enemy.position() || !ent.canAttackTarget(enemy, PETRA.allowCapture(gameState, ent, enemy)))
return false;
if (API3.SquareVectorDistance(enemy.position(), ent.position()) > range)
return false;
if (PETRA.getLandAccess(gameState, enemy) != entAccess)
return false;
return true;
}, this).toEntityArray();
if (mStruct.length)
{
mStruct.sort((structa, structb) => {
let vala = structa.costSum();
if (structa.hasClass("Gate") && ent.canAttackClass("Wall"))
vala += 10000;
else if (structa.hasClass("ConquestCritical"))
vala += 100;
let valb = structb.costSum();
if (structb.hasClass("Gate") && ent.canAttackClass("Wall"))
valb += 10000;
else if (structb.hasClass("ConquestCritical"))
valb += 100;
return valb - vala;
});
if (mStruct[0].hasClass("Gate"))
ent.attack(mStruct[0].id(), false);
else
{
let rand = randIntExclusive(0, mStruct.length * 0.2);
ent.attack(mStruct[rand].id(), PETRA.allowCapture(gameState, ent, mStruct[rand]));
}
}
else if (needsUpdate) // really nothing let's try to help our nearest unit
{
let distmin = Math.min();
let attacker;
this.unitCollection.forEach(unit => {
if (!unit.position())
return;
if (unit.unitAIState().split(".")[1] != "COMBAT" || !unit.unitAIOrderData().length ||
!unit.unitAIOrderData()[0].target)
return;
let target = gameState.getEntityById(unit.unitAIOrderData()[0].target);
if (!target)
return;
let dist = API3.SquareVectorDistance(unit.position(), ent.position());
if (dist > distmin)
return;
distmin = dist;
if (!ent.canAttackTarget(target, PETRA.allowCapture(gameState, ent, target)))
return;
attacker = target;
});
if (attacker)
ent.attack(attacker.id(), PETRA.allowCapture(gameState, ent, attacker));
}
}
}
}
this.unitCollUpdateArray.splice(0, lgth);
this.startingAttack = false;
// check if this enemy has resigned
if (this.target && this.target.owner() === 0 && this.targetPlayer !== 0)
this.target = undefined;
}
this.lastPosition = this.position;
Engine.ProfileStop();
return this.unitCollection.length;
};
PETRA.AttackPlan.prototype.UpdateTransporting = function(gameState, events)
{
let done = true;
for (let ent of this.unitCollection.values())
{
if (this.Config.debug > 1 && ent.getMetadata(PlayerID, "transport") !== undefined)
Engine.PostCommand(PlayerID, { "type": "set-shading-color", "entities": [ent.id()], "rgb": [2, 2, 0] });
else if (this.Config.debug > 1)
Engine.PostCommand(PlayerID, { "type": "set-shading-color", "entities": [ent.id()], "rgb": [1, 1, 1] });
if (!done)
continue;
if (ent.getMetadata(PlayerID, "transport") !== undefined)
done = false;
}
if (done)
{
this.state = "arrived";
return;
}
// if we are attacked while waiting the rest of the army, retaliate
for (let evt of events.Attacked)
{
if (!this.unitCollection.hasEntId(evt.target))
continue;
let attacker = gameState.getEntityById(evt.attacker);
if (!attacker || !gameState.getEntityById(evt.target))
continue;
for (let ent of this.unitCollection.values())
{
if (ent.getMetadata(PlayerID, "transport") !== undefined)
continue;
let allowCapture = PETRA.allowCapture(gameState, ent, attacker);
if (!ent.isIdle() || !ent.canAttackTarget(attacker, allowCapture))
continue;
ent.attack(attacker.id(), allowCapture);
}
break;
}
};
PETRA.AttackPlan.prototype.UpdateWalking = function(gameState, events)
{
// 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.
let attackedNB = 0;
let attackedUnitNB = 0;
for (let evt of events.Attacked)
{
if (!this.unitCollection.hasEntId(evt.target))
continue;
let attacker = gameState.getEntityById(evt.attacker);
if (attacker && (attacker.owner() !== 0 || this.targetPlayer === 0))
{
attackedNB++;
if (attacker.hasClass("Unit"))
attackedUnitNB++;
}
}
// Are we arrived at destination ?
if (attackedNB > 1 && (attackedUnitNB || this.hasSiegeUnits()))
{
if (gameState.ai.HQ.territoryMap.getOwner(this.position) === this.targetPlayer || attackedNB > 3)
{
this.state = "arrived";
return true;
}
}
// 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
let farthest = 0;
let farthestEnt;
for (let ent of this.unitCollection.filter(API3.Filters.byClass("Siege")).values())
{
let dist = API3.SquareVectorDistance(ent.position(), this.position);
if (dist < farthest)
continue;
farthest = dist;
farthestEnt = ent;
}
if (farthestEnt)
farthestEnt.destroy();
}
if (gameState.ai.playedTurn % 5 === 0)
this.position5TurnsAgo = this.position;
if (this.lastPosition && API3.SquareVectorDistance(this.position, this.lastPosition) < 16 && this.path.length > 0)
{
if (!this.path[0][0] || !this.path[0][1])
API3.warn("Start: Problem with path " + uneval(this.path));
// We're stuck, presumably. Check if there are no walls just close to us.
for (let ent of gameState.getEnemyStructures().filter(API3.Filters.byClass(["Palisade", "Wall"])).values())
{
if (API3.SquareVectorDistance(this.position, ent.position()) > 800)
continue;
let enemyClass = ent.hasClass("Wall") ? "Wall" : "Palisade";
// there are walls, so check if we can attack
if (this.unitCollection.filter(API3.Filters.byCanAttackClass(enemyClass)).hasEntities())
{
if (this.Config.debug > 1)
API3.warn("Attack Plan " + this.type + " " + this.name + " has met walls and is not happy.");
this.state = "arrived";
return true;
}
// abort plan
if (this.Config.debug > 1)
API3.warn("Attack Plan " + this.type + " " + this.name + " has met walls and gives up.");
return false;
}
// this.unitCollection.move(this.path[0][0], this.path[0][1]);
this.unitCollection.moveIndiv(this.path[0][0], this.path[0][1]);
}
// check if our units are close enough from the next waypoint.
if (API3.SquareVectorDistance(this.position, this.targetPos) < 10000)
{
if (this.Config.debug > 1)
API3.warn("Attack Plan " + this.type + " " + this.name + " has arrived to destination.");
this.state = "arrived";
return true;
}
else if (this.path.length && API3.SquareVectorDistance(this.position, this.path[0]) < 1600)
{
this.path.shift();
if (this.path.length)
this.unitCollection.moveToRange(this.path[0][0], this.path[0][1], 0, 15);
else
{
if (this.Config.debug > 1)
API3.warn("Attack Plan " + this.type + " " + this.name + " has arrived to destination.");
this.state = "arrived";
return true;
}
}
return true;
};
PETRA.AttackPlan.prototype.UpdateTarget = function(gameState)
{
// First update the target position in case it's a unit (and check if it has garrisoned)
if (this.target && this.target.hasClass("Unit"))
{
this.targetPos = this.target.position();
if (!this.targetPos)
{
let holder = PETRA.getHolder(gameState, this.target);
if (holder && gameState.isPlayerEnemy(holder.owner()))
{
this.target = holder;
this.targetPos = holder.position();
}
else
this.target = undefined;
}
}
// Then update the target if needed:
if (this.targetPlayer === undefined || !gameState.isPlayerEnemy(this.targetPlayer))
{
this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this);
if (this.targetPlayer === undefined)
return false;
if (this.target && this.target.owner() !== this.targetPlayer)
this.target = undefined;
}
if (this.target && this.target.owner() === 0 && this.targetPlayer !== 0) // this enemy has resigned
this.target = undefined;
if (!this.target || !gameState.getEntityById(this.target.id()))
{
if (this.Config.debug > 1)
API3.warn("Seems like our target for plan " + this.name + " has been destroyed or captured. Switching.");
let accessIndex = this.getAttackAccess(gameState);
this.target = this.getNearestTarget(gameState, this.position, accessIndex);
if (!this.target)
{
if (this.uniqueTargetId)
return false;
// Check if we could help any current attack
let attackManager = gameState.ai.HQ.attackManager;
for (let attackType in attackManager.startedAttacks)
{
for (let attack of attackManager.startedAttacks[attackType])
{
if (attack.name == this.name)
continue;
if (!attack.target || !gameState.getEntityById(attack.target.id()) ||
!gameState.isPlayerEnemy(attack.target.owner()))
continue;
if (accessIndex != PETRA.getLandAccess(gameState, attack.target))
continue;
if (attack.target.owner() == 0 && attack.targetPlayer != 0) // looks like it has resigned
continue;
if (!gameState.isPlayerEnemy(attack.targetPlayer))
continue;
this.target = attack.target;
this.targetPlayer = attack.targetPlayer;
this.targetPos = this.target.position();
return true;
}
}
// If not, let's look for another enemy
if (!this.target)
{
this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this);
if (this.targetPlayer !== undefined)
this.target = this.getNearestTarget(gameState, this.position, accessIndex);
if (!this.target)
{
if (this.Config.debug > 1)
API3.warn("No new target found. Remaining units " + this.unitCollection.length);
return false;
}
}
if (this.Config.debug > 1)
API3.warn("We will help one of our other attacks");
}
this.targetPos = this.target.position();
}
return true;
};
/** reset any units */
PETRA.AttackPlan.prototype.Abort = function(gameState)
{
this.unitCollection.unregister();
if (this.unitCollection.hasEntities())
{
// If the attack was started, look for a good rallyPoint to withdraw
let rallyPoint;
if (this.isStarted())
{
let access = this.getAttackAccess(gameState);
let dist = Math.min();
if (this.rallyPoint && gameState.ai.accessibility.getAccessValue(this.rallyPoint) == access)
{
rallyPoint = this.rallyPoint;
dist = API3.SquareVectorDistance(this.position, rallyPoint);
}
// Then check if we have a nearer base (in case this attack has captured one)
for (let base of gameState.ai.HQ.baseManagers)
{
if (!base.anchor || !base.anchor.position())
continue;
if (PETRA.getLandAccess(gameState, base.anchor) != access)
continue;
let newdist = API3.SquareVectorDistance(this.position, base.anchor.position());
if (newdist > dist)
continue;
dist = newdist;
rallyPoint = base.anchor.position();
}
}
for (let ent of this.unitCollection.values())
{
if (ent.getMetadata(PlayerID, "role") == "attack")
ent.stopMoving();
if (rallyPoint)
ent.moveToRange(rallyPoint[0], rallyPoint[1], 0, 15);
this.removeUnit(ent);
}
}
for (let unitCat in this.unitStat)
this.unit[unitCat].unregister();
this.removeQueues(gameState);
};
PETRA.AttackPlan.prototype.removeUnit = function(ent, update)
{
if (ent.getMetadata(PlayerID, "role") == "attack")
{
if (ent.hasClass("CitizenSoldier"))
ent.setMetadata(PlayerID, "role", "worker");
else
ent.setMetadata(PlayerID, "role", undefined);
ent.setMetadata(PlayerID, "subrole", undefined);
}
ent.setMetadata(PlayerID, "plan", -1);
if (update)
this.unitCollection.updateEnt(ent);
};
PETRA.AttackPlan.prototype.checkEvents = function(gameState, events)
{
for (let evt of events.EntityRenamed)
{
if (!this.target || this.target.id() != evt.entity)
continue;
if (this.type == "Raid" && !this.isStarted())
this.target = undefined;
else
this.target = gameState.getEntityById(evt.newentity);
if (this.target)
this.targetPos = this.target.position();
}
for (let evt of events.OwnershipChanged) // capture event
if (this.target && this.target.id() == evt.entity && gameState.isPlayerAlly(evt.to))
this.target = undefined;
for (let evt of events.PlayerDefeated)
{
if (this.targetPlayer !== evt.playerId)
continue;
this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this);
this.target = undefined;
}
if (!this.overseas || this.state !== "unexecuted")
return;
// let's check if an enemy has built a structure at our access
for (let evt of events.Create)
{
let ent = gameState.getEntityById(evt.entity);
if (!ent || !ent.position() || !ent.hasClass("Structure"))
continue;
if (!gameState.isPlayerEnemy(ent.owner()))
continue;
let access = PETRA.getLandAccess(gameState, ent);
for (let base of gameState.ai.HQ.baseManagers)
{
if (!base.anchor || !base.anchor.position())
continue;
if (base.accessIndex != access)
continue;
this.overseas = 0;
this.rallyPoint = base.anchor.position();
}
}
};
PETRA.AttackPlan.prototype.waitingForTransport = function()
{
for (let ent of this.unitCollection.values())
if (ent.getMetadata(PlayerID, "transport") !== undefined)
return true;
return false;
};
PETRA.AttackPlan.prototype.hasSiegeUnits = function()
{
for (let ent of this.unitCollection.values())
if (PETRA.isSiegeUnit(ent))
return true;
return false;
};
PETRA.AttackPlan.prototype.hasForceOrder = function(data, value)
{
for (let ent of this.unitCollection.values())
{
if (data && +ent.getMetadata(PlayerID, data) !== value)
continue;
let orders = ent.unitAIOrderData();
for (let order of orders)
if (order.force)
return true;
}
return false;
};
/**
* The center position of this attack may be in an inaccessible area. So we use the access
* of the unit nearest to this center position.
*/
PETRA.AttackPlan.prototype.getAttackAccess = function(gameState)
{
for (let ent of this.unitCollection.filterNearest(this.position, 1).values())
return PETRA.getLandAccess(gameState, ent);
return 0;
};
PETRA.AttackPlan.prototype.debugAttack = function()
{
API3.warn("---------- attack " + this.name);
for (let unitCat in this.unitStat)
{
let Unit = this.unitStat[unitCat];
API3.warn(unitCat + " num=" + this.unit[unitCat].length + " min=" + Unit.minSize + " need=" + Unit.targetSize);
}
API3.warn("------------------------------");
};
PETRA.AttackPlan.prototype.Serialize = function()
{
let properties = {
"name": this.name,
"type": this.type,
"state": this.state,
"forced": this.forced,
"rallyPoint": this.rallyPoint,
"overseas": this.overseas,
"paused": this.paused,
"maxCompletingTime": this.maxCompletingTime,
"neededShips": this.neededShips,
"unitStat": this.unitStat,
"siegeState": this.siegeState,
"position5TurnsAgo": this.position5TurnsAgo,
"lastPosition": this.lastPosition,
"position": this.position,
"isBlocked": this.isBlocked,
"targetPlayer": this.targetPlayer,
"target": this.target !== undefined ? this.target.id() : undefined,
"targetPos": this.targetPos,
"uniqueTargetId": this.uniqueTargetId,
"path": this.path
};
return { "properties": properties };
};
PETRA.AttackPlan.prototype.Deserialize = function(gameState, data)
{
for (let key in data.properties)
this[key] = data.properties[key];
if (this.target)
this.target = gameState.getEntityById(this.target);
this.failed = undefined;
};
Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/baseManager.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/petra/baseManager.js (revision 23915)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/baseManager.js (revision 23916)
@@ -1,1106 +1,1106 @@
/**
* Base Manager
* Handles lower level economic stuffs.
* Some tasks:
* -tasking workers: gathering/hunting/building/repairing?/scouting/plans.
* -giving feedback/estimates on GR
* -achieving building stuff plans (scouting/getting ressource/building) or other long-staying plans.
* -getting good spots for dropsites
* -managing dropsite use in the base
* -updating whatever needs updating, keeping track of stuffs (rebuilding needs…)
*/
PETRA.BaseManager = function(gameState, Config)
{
this.Config = Config;
this.ID = gameState.ai.uniqueIDs.bases++;
// anchor building: seen as the main building of the base. Needs to have territorial influence
this.anchor = undefined;
this.anchorId = undefined;
this.accessIndex = undefined;
// Maximum distance (from any dropsite) to look for resources
// 3 areas are used: from 0 to max/4, from max/4 to max/2 and from max/2 to max
this.maxDistResourceSquare = 360*360;
this.constructing = false;
// Defenders to train in this cc when its construction is finished
this.neededDefenders = this.Config.difficulty > 2 ? 3 + 2*(this.Config.difficulty - 3) : 0;
// vector for iterating, to check one use the HQ map.
this.territoryIndices = [];
this.timeNextIdleCheck = 0;
};
PETRA.BaseManager.prototype.init = function(gameState, state)
{
if (state == "unconstructed")
this.constructing = true;
else if (state != "captured")
this.neededDefenders = 0;
this.workerObject = new PETRA.Worker(this);
// entitycollections
this.units = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "base", this.ID));
this.workers = this.units.filter(API3.Filters.byMetadata(PlayerID, "role", "worker"));
this.buildings = gameState.getOwnStructures().filter(API3.Filters.byMetadata(PlayerID, "base", this.ID));
this.mobileDropsites = this.units.filter(API3.Filters.isDropsite());
this.units.registerUpdates();
this.workers.registerUpdates();
this.buildings.registerUpdates();
this.mobileDropsites.registerUpdates();
// array of entity IDs, with each being
this.dropsites = {};
this.dropsiteSupplies = {};
this.gatherers = {};
for (let res of Resources.GetCodes())
{
this.dropsiteSupplies[res] = { "nearby": [], "medium": [], "faraway": [] };
this.gatherers[res] = { "nextCheck": 0, "used": 0, "lost": 0 };
}
};
PETRA.BaseManager.prototype.reset = function(gameState, state)
{
if (state == "unconstructed")
this.constructing = true;
else
this.constructing = false;
if (state != "captured" || this.Config.difficulty < 3)
this.neededDefenders = 0;
else
this.neededDefenders = 3 + 2 * (this.Config.difficulty - 3);
};
PETRA.BaseManager.prototype.assignEntity = function(gameState, ent)
{
ent.setMetadata(PlayerID, "base", this.ID);
this.units.updateEnt(ent);
this.workers.updateEnt(ent);
this.buildings.updateEnt(ent);
if (ent.resourceDropsiteTypes() && !ent.hasClass("Elephant"))
this.assignResourceToDropsite(gameState, ent);
};
PETRA.BaseManager.prototype.setAnchor = function(gameState, anchorEntity)
{
if (!anchorEntity.hasClass("CivCentre"))
API3.warn("Error: Petra base " + this.ID + " has been assigned " + ent.templateName() + " as anchor.");
else
{
this.anchor = anchorEntity;
this.anchorId = anchorEntity.id();
this.anchor.setMetadata(PlayerID, "baseAnchor", true);
gameState.ai.HQ.resetBaseCache();
}
anchorEntity.setMetadata(PlayerID, "base", this.ID);
this.buildings.updateEnt(anchorEntity);
this.accessIndex = PETRA.getLandAccess(gameState, anchorEntity);
return true;
};
/* we lost our anchor. Let's reassign our units and buildings */
PETRA.BaseManager.prototype.anchorLost = function(gameState, ent)
{
this.anchor = undefined;
this.anchorId = undefined;
this.neededDefenders = 0;
gameState.ai.HQ.resetBaseCache();
};
/** Set a building of an anchorless base */
PETRA.BaseManager.prototype.setAnchorlessEntity = function(gameState, ent)
{
if (!this.buildings.hasEntities())
{
if (!PETRA.getBuiltEntity(gameState, ent).resourceDropsiteTypes())
API3.warn("Error: Petra base " + this.ID + " has been assigned " + ent.templateName() + " as origin.");
this.accessIndex = PETRA.getLandAccess(gameState, ent);
}
else if (this.accessIndex != PETRA.getLandAccess(gameState, ent))
API3.warn(" Error: Petra base " + this.ID + " with access " + this.accessIndex +
" has been assigned " + ent.templateName() + " with access" + PETRA.getLandAccess(gameState, ent));
ent.setMetadata(PlayerID, "base", this.ID);
this.buildings.updateEnt(ent);
return true;
};
/**
* Assign the resources around the dropsites of this basis in three areas according to distance, and sort them in each area.
* Moving resources (animals) and buildable resources (fields) are treated elsewhere.
*/
PETRA.BaseManager.prototype.assignResourceToDropsite = function(gameState, dropsite)
{
if (this.dropsites[dropsite.id()])
{
if (this.Config.debug > 0)
warn("assignResourceToDropsite: dropsite already in the list. Should never happen");
return;
}
let accessIndex = this.accessIndex;
let dropsitePos = dropsite.position();
let dropsiteId = dropsite.id();
this.dropsites[dropsiteId] = true;
if (this.ID == gameState.ai.HQ.baseManagers[0].ID)
accessIndex = PETRA.getLandAccess(gameState, dropsite);
let maxDistResourceSquare = this.maxDistResourceSquare;
for (let type of dropsite.resourceDropsiteTypes())
{
let resources = gameState.getResourceSupplies(type);
if (!resources.length)
continue;
let nearby = this.dropsiteSupplies[type].nearby;
let medium = this.dropsiteSupplies[type].medium;
let faraway = this.dropsiteSupplies[type].faraway;
resources.forEach(function(supply)
{
if (!supply.position())
return;
if (supply.hasClass("Animal")) // moving resources are treated differently
return;
if (supply.hasClass("Field")) // fields are treated separately
return;
if (supply.resourceSupplyType().generic == "treasure") // treasures are treated separately
return;
// quick accessibility check
if (PETRA.getLandAccess(gameState, supply) != accessIndex)
return;
let dist = API3.SquareVectorDistance(supply.position(), dropsitePos);
if (dist < maxDistResourceSquare)
{
if (dist < maxDistResourceSquare/16) // distmax/4
nearby.push({ "dropsite": dropsiteId, "id": supply.id(), "ent": supply, "dist": dist });
else if (dist < maxDistResourceSquare/4) // distmax/2
medium.push({ "dropsite": dropsiteId, "id": supply.id(), "ent": supply, "dist": dist });
else
faraway.push({ "dropsite": dropsiteId, "id": supply.id(), "ent": supply, "dist": dist });
}
});
nearby.sort((r1, r2) => r1.dist - r2.dist);
medium.sort((r1, r2) => r1.dist - r2.dist);
faraway.sort((r1, r2) => r1.dist - r2.dist);
/*
let debug = false;
if (debug)
{
faraway.forEach(function(res){
Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [2,0,0]});
});
medium.forEach(function(res){
Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [0,2,0]});
});
nearby.forEach(function(res){
Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [0,0,2]});
});
}
*/
}
// Allows all allies to use this dropsite except if base anchor to be sure to keep
// a minimum of resources for this base
Engine.PostCommand(PlayerID, {
"type": "set-dropsite-sharing",
"entities": [dropsiteId],
"shared": dropsiteId != this.anchorId
});
};
// completely remove the dropsite resources from our list.
PETRA.BaseManager.prototype.removeDropsite = function(gameState, ent)
{
if (!ent.id())
return;
let removeSupply = function(entId, supply){
for (let i = 0; i < supply.length; ++i)
{
// exhausted resource, remove it from this list
if (!supply[i].ent || !gameState.getEntityById(supply[i].id))
supply.splice(i--, 1);
// resource assigned to the removed dropsite, remove it
else if (supply[i].dropsite == entId)
supply.splice(i--, 1);
}
};
for (let type in this.dropsiteSupplies)
{
removeSupply(ent.id(), this.dropsiteSupplies[type].nearby);
removeSupply(ent.id(), this.dropsiteSupplies[type].medium);
removeSupply(ent.id(), this.dropsiteSupplies[type].faraway);
}
this.dropsites[ent.id()] = undefined;
};
/**
* Returns the position of the best place to build a new dropsite for the specified resource
*/
PETRA.BaseManager.prototype.findBestDropsiteLocation = function(gameState, resource)
{
let template = gameState.getTemplate(gameState.applyCiv("structures/{civ}_storehouse"));
let halfSize = 0;
if (template.get("Footprint/Square"))
halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2;
else if (template.get("Footprint/Circle"))
halfSize = +template.get("Footprint/Circle/@radius");
// This builds a map. The procedure is fairly simple. It adds the resource maps
// (which are dynamically updated and are made so that they will facilitate DP placement)
// Then checks for a good spot in the territory. If none, and town/city phase, checks outside
// The AI will currently not build a CC if it wouldn't connect with an existing CC.
let obstructions = PETRA.createObstructionMap(gameState, this.accessIndex, template);
let ccEnts = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")).toEntityArray();
let dpEnts = gameState.getOwnStructures().filter(API3.Filters.byClassesOr(["Storehouse", "Dock"])).toEntityArray();
let bestIdx;
let bestVal = 0;
let radius = Math.ceil(template.obstructionRadius().max / obstructions.cellSize);
let territoryMap = gameState.ai.HQ.territoryMap;
let width = territoryMap.width;
let cellSize = territoryMap.cellSize;
for (let j of this.territoryIndices)
{
let i = territoryMap.getNonObstructedTile(j, radius, obstructions);
if (i < 0) // no room around
continue;
// we add 3 times the needed resource and once the others (except food)
let total = 2*gameState.sharedScript.resourceMaps[resource].map[j];
for (let res in gameState.sharedScript.resourceMaps)
if (res != "food")
total += gameState.sharedScript.resourceMaps[res].map[j];
total *= 0.7; // Just a normalisation factor as the locateMap is limited to 255
if (total <= bestVal)
continue;
let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)];
for (let dp of dpEnts)
{
let dpPos = dp.position();
if (!dpPos)
continue;
let dist = API3.SquareVectorDistance(dpPos, pos);
if (dist < 3600)
{
total = 0;
break;
}
else if (dist < 6400)
total *= (Math.sqrt(dist)-60)/20;
}
if (total <= bestVal)
continue;
for (let cc of ccEnts)
{
let ccPos = cc.position();
if (!ccPos)
continue;
let dist = API3.SquareVectorDistance(ccPos, pos);
if (dist < 3600)
{
total = 0;
break;
}
else if (dist < 6400)
total *= (Math.sqrt(dist)-60)/20;
}
if (total <= bestVal)
continue;
if (gameState.ai.HQ.isDangerousLocation(gameState, pos, halfSize))
continue;
bestVal = total;
bestIdx = i;
}
if (this.Config.debug > 2)
warn(" for dropsite best is " + bestVal);
if (bestVal <= 0)
return { "quality": bestVal, "pos": [0, 0] };
let x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize;
let z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize;
return { "quality": bestVal, "pos": [x, z] };
};
PETRA.BaseManager.prototype.getResourceLevel = function(gameState, type, nearbyOnly = false)
{
let count = 0;
let check = {};
for (let supply of this.dropsiteSupplies[type].nearby)
{
if (check[supply.id]) // avoid double counting as same resource can appear several time
continue;
check[supply.id] = true;
count += supply.ent.resourceSupplyAmount();
}
if (nearbyOnly)
return count;
for (let supply of this.dropsiteSupplies[type].medium)
{
if (check[supply.id])
continue;
check[supply.id] = true;
count += 0.6*supply.ent.resourceSupplyAmount();
}
return count;
};
/** check our resource levels and react accordingly */
PETRA.BaseManager.prototype.checkResourceLevels = function(gameState, queues)
{
for (let type of Resources.GetCodes())
{
if (type == "food")
{
if (gameState.ai.HQ.canBuild(gameState, "structures/{civ}_field")) // let's see if we need to add new farms.
{
let count = this.getResourceLevel(gameState, type, gameState.currentPhase() > 1); // animals are not accounted
let numFarms = gameState.getOwnStructures().filter(API3.Filters.byClass("Field")).length; // including foundations
let numQueue = queues.field.countQueuedUnits();
// TODO if not yet farms, add a check on time used/lost and build farmstead if needed
if (numFarms + numQueue == 0) // starting game, rely on fruits as long as we have enough of them
{
if (count < 600)
{
queues.field.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}_field", { "favoredBase": this.ID }));
gameState.ai.HQ.needFarm = true;
}
}
else if (!gameState.ai.HQ.maxFields || numFarms + numQueue < gameState.ai.HQ.maxFields)
{
let numFound = gameState.getOwnFoundations().filter(API3.Filters.byClass("Field")).length;
let goal = this.Config.Economy.provisionFields;
if (gameState.ai.HQ.saveResources || gameState.ai.HQ.saveSpace || count > 300 || numFarms > 5)
goal = Math.max(goal-1, 1);
if (numFound + numQueue < goal)
queues.field.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}_field", { "favoredBase": this.ID }));
}
else if (gameState.ai.HQ.needCorral && !gameState.getOwnEntitiesByClass("Corral", true).hasEntities() &&
!queues.corral.hasQueuedUnits() && gameState.ai.HQ.canBuild(gameState, "structures/{civ}_corral"))
queues.corral.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}_corral", { "favoredBase": this.ID }));
continue;
}
if (!gameState.getOwnEntitiesByClass("Corral", true).hasEntities() &&
!queues.corral.hasQueuedUnits() && gameState.ai.HQ.canBuild(gameState, "structures/{civ}_corral"))
{
let count = this.getResourceLevel(gameState, type, gameState.currentPhase() > 1); // animals are not accounted
if (count < 900)
{
queues.corral.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}_corral", { "favoredBase": this.ID }));
gameState.ai.HQ.needCorral = true;
}
}
continue;
}
// Non food stuff
if (!gameState.sharedScript.resourceMaps[type] || queues.dropsites.hasQueuedUnits() ||
gameState.getOwnFoundations().filter(API3.Filters.byClass("Storehouse")).hasEntities())
{
this.gatherers[type].nextCheck = gameState.ai.playedTurn;
this.gatherers[type].used = 0;
this.gatherers[type].lost = 0;
continue;
}
if (gameState.ai.playedTurn < this.gatherers[type].nextCheck)
continue;
for (let ent of this.gatherersByType(gameState, type).values())
{
if (ent.unitAIState() == "INDIVIDUAL.GATHER.GATHERING")
++this.gatherers[type].used;
else if (ent.unitAIState() == "INDIVIDUAL.RETURNRESOURCE.APPROACHING")
++this.gatherers[type].lost;
}
// TODO add also a test on remaining resources.
let total = this.gatherers[type].used + this.gatherers[type].lost;
if (total > 150 || total > 60 && type != "wood")
{
let ratio = this.gatherers[type].lost / total;
if (ratio > 0.15)
{
let newDP = this.findBestDropsiteLocation(gameState, type);
if (newDP.quality > 50 && gameState.ai.HQ.canBuild(gameState, "structures/{civ}_storehouse"))
queues.dropsites.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}_storehouse", { "base": this.ID, "type": type }, newDP.pos));
else if (!gameState.getOwnFoundations().filter(API3.Filters.byClass("CivCentre")).hasEntities() && !queues.civilCentre.hasQueuedUnits())
{
// No good dropsite, try to build a new base if no base already planned,
// and if not possible, be less strict on dropsite quality.
if ((!gameState.ai.HQ.canExpand || !gameState.ai.HQ.buildNewBase(gameState, queues, type)) &&
newDP.quality > Math.min(25, 50*0.15/ratio) &&
gameState.ai.HQ.canBuild(gameState, "structures/{civ}_storehouse"))
queues.dropsites.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}_storehouse", { "base": this.ID, "type": type }, newDP.pos));
}
}
this.gatherers[type].nextCheck = gameState.ai.playedTurn + 20;
this.gatherers[type].used = 0;
this.gatherers[type].lost = 0;
}
else if (total == 0)
this.gatherers[type].nextCheck = gameState.ai.playedTurn + 10;
}
};
/** Adds the estimated gather rates from this base to the currentRates */
PETRA.BaseManager.prototype.addGatherRates = function(gameState, currentRates)
{
for (let res in currentRates)
{
// I calculate the exact gathering rate for each unit.
// I must then lower that to account for travel time.
// Given that the faster you gather, the more travel time matters,
// I use some logarithms.
// TODO: this should take into account for unit speed and/or distance to target
this.gatherersByType(gameState, res).forEach(ent => {
if (ent.isIdle() || !ent.position())
return;
let gRate = ent.currentGatherRate();
if (gRate)
currentRates[res] += Math.log(1+gRate)/1.1;
});
if (res == "food")
{
this.workersBySubrole(gameState, "hunter").forEach(ent => {
if (ent.isIdle() || !ent.position())
return;
let gRate = ent.currentGatherRate();
if (gRate)
currentRates[res] += Math.log(1+gRate)/1.1;
});
this.workersBySubrole(gameState, "fisher").forEach(ent => {
if (ent.isIdle() || !ent.position())
return;
let gRate = ent.currentGatherRate();
if (gRate)
currentRates[res] += Math.log(1+gRate)/1.1;
});
}
}
};
PETRA.BaseManager.prototype.assignRolelessUnits = function(gameState, roleless)
{
if (!roleless)
roleless = this.units.filter(API3.Filters.not(API3.Filters.byHasMetadata(PlayerID, "role"))).values();
for (let ent of roleless)
{
if (ent.hasClass("Worker") || ent.hasClass("CitizenSoldier") || ent.hasClass("FishingBoat"))
ent.setMetadata(PlayerID, "role", "worker");
else if (ent.hasClass("Support") && ent.hasClass("Elephant"))
ent.setMetadata(PlayerID, "role", "worker");
}
};
/**
* If the numbers of workers on the resources is unbalanced then set some of workers to idle so
* they can be reassigned by reassignIdleWorkers.
* TODO: actually this probably should be in the HQ.
*/
PETRA.BaseManager.prototype.setWorkersIdleByPriority = function(gameState)
{
this.timeNextIdleCheck = gameState.ai.elapsedTime + 8;
// change resource only towards one which is more needed, and if changing will not change this order
let nb = 1; // no more than 1 change per turn (otherwise we should update the rates)
let mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState);
let sumWanted = 0;
let sumCurrent = 0;
for (let need of mostNeeded)
{
sumWanted += need.wanted;
sumCurrent += need.current;
}
let scale = 1;
if (sumWanted > 0)
scale = sumCurrent / sumWanted;
for (let i = mostNeeded.length-1; i > 0; --i)
{
let lessNeed = mostNeeded[i];
for (let j = 0; j < i; ++j)
{
let moreNeed = mostNeeded[j];
let lastFailed = gameState.ai.HQ.lastFailedGather[moreNeed.type];
if (lastFailed && gameState.ai.elapsedTime - lastFailed < 20)
continue;
// Ensure that the most wanted resource is not exhausted
if (moreNeed.type != "food" && gameState.ai.HQ.isResourceExhausted(moreNeed.type))
{
if (lessNeed.type != "food" && gameState.ai.HQ.isResourceExhausted(lessNeed.type))
continue;
// And if so, move the gatherer to the less wanted one.
nb = this.switchGatherer(gameState, moreNeed.type, lessNeed.type, nb);
if (nb == 0)
return;
}
// If we assume a mean rate of 0.5 per gatherer, this diff should be > 1
// but we require a bit more to avoid too frequent changes
if (scale*moreNeed.wanted - moreNeed.current - scale*lessNeed.wanted + lessNeed.current > 1.5 ||
lessNeed.type != "food" && gameState.ai.HQ.isResourceExhausted(lessNeed.type))
{
nb = this.switchGatherer(gameState, lessNeed.type, moreNeed.type, nb);
if (nb == 0)
return;
}
}
}
};
/**
* Switch some gatherers (limited to number) from resource "from" to resource "to"
* and return remaining number of possible switches.
* Prefer FemaleCitizen for food and CitizenSoldier for other resources.
*/
PETRA.BaseManager.prototype.switchGatherer = function(gameState, from, to, number)
{
let num = number;
let only;
let gatherers = this.gatherersByType(gameState, from);
if (from == "food" && gatherers.filter(API3.Filters.byClass("CitizenSoldier")).hasEntities())
only = "CitizenSoldier";
else if (to == "food" && gatherers.filter(API3.Filters.byClass("FemaleCitizen")).hasEntities())
only = "FemaleCitizen";
for (let ent of gatherers.values())
{
if (num == 0)
return num;
if (!ent.canGather(to))
continue;
if (only && !ent.hasClass(only))
continue;
--num;
ent.stopMoving();
ent.setMetadata(PlayerID, "gather-type", to);
gameState.ai.HQ.AddTCResGatherer(to);
}
return num;
};
PETRA.BaseManager.prototype.reassignIdleWorkers = function(gameState, idleWorkers)
{
// Search for idle workers, and tell them to gather resources based on demand
if (!idleWorkers)
{
let filter = API3.Filters.byMetadata(PlayerID, "subrole", "idle");
idleWorkers = gameState.updatingCollection("idle-workers-base-" + this.ID, filter, this.workers).values();
}
for (let ent of idleWorkers)
{
// Check that the worker isn't garrisoned
if (!ent.position())
continue;
// Support elephant can only be builders
if (ent.hasClass("Support") && ent.hasClass("Elephant"))
{
ent.setMetadata(PlayerID, "subrole", "idle");
continue;
}
if (ent.hasClass("Worker"))
{
// Just emergency repairing here. It is better managed in assignToFoundations
if (ent.isBuilder() && this.anchor && this.anchor.needsRepair() &&
gameState.getOwnEntitiesByMetadata("target-foundation", this.anchor.id()).length < 2)
ent.repair(this.anchor);
else if (ent.isGatherer())
{
let mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState);
for (let needed of mostNeeded)
{
if (!ent.canGather(needed.type))
continue;
let lastFailed = gameState.ai.HQ.lastFailedGather[needed.type];
if (lastFailed && gameState.ai.elapsedTime - lastFailed < 20)
continue;
if (needed.type != "food" && gameState.ai.HQ.isResourceExhausted(needed.type))
continue;
ent.setMetadata(PlayerID, "subrole", "gatherer");
ent.setMetadata(PlayerID, "gather-type", needed.type);
gameState.ai.HQ.AddTCResGatherer(needed.type);
break;
}
}
}
- else if (ent.hasClass("Cavalry"))
+ else if (PETRA.isFastMoving(ent))
ent.setMetadata(PlayerID, "subrole", "hunter");
else if (ent.hasClass("FishingBoat"))
ent.setMetadata(PlayerID, "subrole", "fisher");
}
};
PETRA.BaseManager.prototype.workersBySubrole = function(gameState, subrole)
{
return gameState.updatingCollection("subrole-" + subrole +"-base-" + this.ID, API3.Filters.byMetadata(PlayerID, "subrole", subrole), this.workers);
};
PETRA.BaseManager.prototype.gatherersByType = function(gameState, type)
{
return gameState.updatingCollection("workers-gathering-" + type +"-base-" + this.ID, API3.Filters.byMetadata(PlayerID, "gather-type", type), this.workersBySubrole(gameState, "gatherer"));
};
/**
* returns an entity collection of workers.
* They are idled immediatly and their subrole set to idle.
*/
PETRA.BaseManager.prototype.pickBuilders = function(gameState, workers, number)
{
let availableWorkers = this.workers.filter(ent => {
if (!ent.position() || !ent.isBuilder())
return false;
if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3)
return false;
if (ent.getMetadata(PlayerID, "transport"))
return false;
return true;
}).toEntityArray();
availableWorkers.sort((a, b) => {
let vala = 0;
let valb = 0;
if (a.getMetadata(PlayerID, "subrole") == "builder")
vala = 100;
if (b.getMetadata(PlayerID, "subrole") == "builder")
valb = 100;
if (a.getMetadata(PlayerID, "subrole") == "idle")
vala = -50;
if (b.getMetadata(PlayerID, "subrole") == "idle")
valb = -50;
if (a.getMetadata(PlayerID, "plan") === undefined)
vala = -20;
if (b.getMetadata(PlayerID, "plan") === undefined)
valb = -20;
return vala - valb;
});
let needed = Math.min(number, availableWorkers.length - 3);
for (let i = 0; i < needed; ++i)
{
availableWorkers[i].stopMoving();
availableWorkers[i].setMetadata(PlayerID, "subrole", "idle");
workers.addEnt(availableWorkers[i]);
}
return;
};
/**
* If we have some foundations, and we don't have enough builder-workers,
* try reassigning some other workers who are nearby
* AI tries to use builders sensibly, not completely stopping its econ.
*/
PETRA.BaseManager.prototype.assignToFoundations = function(gameState, noRepair)
{
let foundations = this.buildings.filter(API3.Filters.and(API3.Filters.isFoundation(), API3.Filters.not(API3.Filters.byClass("Field"))));
let damagedBuildings = this.buildings.filter(ent => ent.foundationProgress() === undefined && ent.needsRepair());
// Check if nothing to build
if (!foundations.length && !damagedBuildings.length)
return;
let workers = this.workers.filter(ent => ent.isBuilder());
let builderWorkers = this.workersBySubrole(gameState, "builder");
let idleBuilderWorkers = builderWorkers.filter(API3.Filters.isIdle());
// if we're constructing and we have the foundations to our base anchor, only try building that.
if (this.constructing && foundations.filter(API3.Filters.byMetadata(PlayerID, "baseAnchor", true)).hasEntities())
{
foundations = foundations.filter(API3.Filters.byMetadata(PlayerID, "baseAnchor", true));
let tID = foundations.toEntityArray()[0].id();
workers.forEach(ent => {
let target = ent.getMetadata(PlayerID, "target-foundation");
if (target && target != tID)
{
ent.stopMoving();
ent.setMetadata(PlayerID, "target-foundation", tID);
}
});
}
if (workers.length < 3)
{
let fromOtherBase = gameState.ai.HQ.bulkPickWorkers(gameState, this, 2);
if (fromOtherBase)
{
let baseID = this.ID;
fromOtherBase.forEach(worker => {
worker.setMetadata(PlayerID, "base", baseID);
worker.setMetadata(PlayerID, "subrole", "builder");
workers.updateEnt(worker);
builderWorkers.updateEnt(worker);
idleBuilderWorkers.updateEnt(worker);
});
}
}
let builderTot = builderWorkers.length - idleBuilderWorkers.length;
// Make the limit on number of builders depends on the available resources
let availableResources = gameState.ai.queueManager.getAvailableResources(gameState);
let builderRatio = 1;
for (let res of Resources.GetCodes())
{
if (availableResources[res] < 200)
{
builderRatio = 0.2;
break;
}
else if (availableResources[res] < 1000)
builderRatio = Math.min(builderRatio, availableResources[res] / 1000);
}
for (let target of foundations.values())
{
if (target.hasClass("Field"))
continue; // we do not build fields
if (gameState.ai.HQ.isNearInvadingArmy(target.position()))
if (!target.hasClass("CivCentre") && !target.hasClass("Wall") &&
(!target.hasClass("Wonder") || !gameState.getVictoryConditions().has("wonder")))
continue;
// if our territory has shrinked since this foundation was positioned, do not build it
if (PETRA.isNotWorthBuilding(gameState, target))
continue;
let assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target.id()).length;
let maxTotalBuilders = Math.ceil(workers.length * builderRatio);
if (maxTotalBuilders < 2 && workers.length > 1)
maxTotalBuilders = 2;
if (target.hasClass("House") && gameState.getPopulationLimit() < gameState.getPopulation() + 5 &&
gameState.getPopulationLimit() < gameState.getPopulationMax())
maxTotalBuilders += 2;
let targetNB = 2;
if (target.hasClass("Fortress") || target.hasClass("Wonder") ||
target.getMetadata(PlayerID, "phaseUp") == true)
targetNB = 7;
else if (target.hasClass("Barracks") || target.hasClass("Range") || target.hasClass("Stable") ||
target.hasClass("Tower") || target.hasClass("Market"))
targetNB = 4;
else if (target.hasClass("House") || target.hasClass("DropsiteWood"))
targetNB = 3;
if (target.getMetadata(PlayerID, "baseAnchor") == true ||
target.hasClass("Wonder") && gameState.getVictoryConditions().has("wonder"))
{
targetNB = 15;
maxTotalBuilders = Math.max(maxTotalBuilders, 15);
}
// if no base yet, everybody should build
if (gameState.ai.HQ.numActiveBases() == 0)
{
targetNB = workers.length;
maxTotalBuilders = targetNB;
}
if (assigned >= targetNB)
continue;
idleBuilderWorkers.forEach(function(ent) {
if (ent.getMetadata(PlayerID, "target-foundation") !== undefined)
return;
if (assigned >= targetNB || !ent.position() ||
API3.SquareVectorDistance(ent.position(), target.position()) > 40000)
return;
++assigned;
++builderTot;
ent.setMetadata(PlayerID, "target-foundation", target.id());
});
if (assigned >= targetNB || builderTot >= maxTotalBuilders)
continue;
let nonBuilderWorkers = workers.filter(function(ent) {
if (ent.getMetadata(PlayerID, "subrole") == "builder")
return false;
if (!ent.position())
return false;
if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3)
return false;
if (ent.getMetadata(PlayerID, "transport"))
return false;
return true;
}).toEntityArray();
let time = target.buildTime();
nonBuilderWorkers.sort((workerA, workerB) => {
let coeffA = API3.SquareVectorDistance(target.position(), workerA.position());
// elephant moves slowly, so when far away they are only useful if build time is long
if (workerA.hasClass("Elephant"))
coeffA *= 0.5 * (1 + Math.sqrt(coeffA)/5/time);
else if (workerA.getMetadata(PlayerID, "gather-type") == "food")
coeffA *= 3;
let coeffB = API3.SquareVectorDistance(target.position(), workerB.position());
if (workerB.hasClass("Elephant"))
coeffB *= 0.5 * (1 + Math.sqrt(coeffB)/5/time);
else if (workerB.getMetadata(PlayerID, "gather-type") == "food")
coeffB *= 3;
return coeffA - coeffB;
});
let current = 0;
let nonBuilderTot = nonBuilderWorkers.length;
while (assigned < targetNB && builderTot < maxTotalBuilders && current < nonBuilderTot)
{
++assigned;
++builderTot;
let ent = nonBuilderWorkers[current++];
ent.stopMoving();
ent.setMetadata(PlayerID, "subrole", "builder");
ent.setMetadata(PlayerID, "target-foundation", target.id());
}
}
for (let target of damagedBuildings.values())
{
// Don't repair if we're still under attack, unless it's a vital (civcentre or wall) building
// that's being destroyed.
if (gameState.ai.HQ.isNearInvadingArmy(target.position()))
{
if (target.healthLevel() > 0.5 ||
!target.hasClass("CivCentre") && !target.hasClass("Wall") &&
(!target.hasClass("Wonder") || !gameState.getVictoryConditions().has("wonder")))
continue;
}
else if (noRepair && !target.hasClass("CivCentre"))
continue;
if (target.decaying())
continue;
let assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target.id()).length;
let maxTotalBuilders = Math.ceil(workers.length * builderRatio);
let targetNB = 1;
if (target.hasClass("Fortress") || target.hasClass("Wonder"))
targetNB = 3;
if (target.getMetadata(PlayerID, "baseAnchor") == true ||
target.hasClass("Wonder") && gameState.getVictoryConditions().has("wonder"))
{
maxTotalBuilders = Math.ceil(workers.length * Math.max(0.3, builderRatio));
targetNB = 5;
if (target.healthLevel() < 0.3)
{
maxTotalBuilders = Math.ceil(workers.length * Math.max(0.6, builderRatio));
targetNB = 7;
}
}
if (assigned >= targetNB)
continue;
idleBuilderWorkers.forEach(function(ent) {
if (ent.getMetadata(PlayerID, "target-foundation") !== undefined)
return;
if (assigned >= targetNB || !ent.position() ||
API3.SquareVectorDistance(ent.position(), target.position()) > 40000)
return;
++assigned;
++builderTot;
ent.setMetadata(PlayerID, "target-foundation", target.id());
});
if (assigned >= targetNB || builderTot >= maxTotalBuilders)
continue;
let nonBuilderWorkers = workers.filter(function(ent) {
if (ent.getMetadata(PlayerID, "subrole") == "builder")
return false;
if (!ent.position())
return false;
if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3)
return false;
if (ent.getMetadata(PlayerID, "transport"))
return false;
return true;
});
let num = Math.min(nonBuilderWorkers.length, targetNB-assigned);
let nearestNonBuilders = nonBuilderWorkers.filterNearest(target.position(), num);
nearestNonBuilders.forEach(function(ent) {
++assigned;
++builderTot;
ent.stopMoving();
ent.setMetadata(PlayerID, "subrole", "builder");
ent.setMetadata(PlayerID, "target-foundation", target.id());
});
}
};
/** Return false when the base is not active (no workers on it) */
PETRA.BaseManager.prototype.update = function(gameState, queues, events)
{
if (this.ID == gameState.ai.HQ.baseManagers[0].ID) // base for unaffected units
{
// if some active base, reassigns the workers/buildings
// otherwise look for anything useful to do, i.e. treasures to gather
if (gameState.ai.HQ.numActiveBases() > 0)
{
for (let ent of this.units.values())
{
let bestBase = PETRA.getBestBase(gameState, ent);
if (bestBase.ID != this.ID)
bestBase.assignEntity(gameState, ent);
}
for (let ent of this.buildings.values())
{
let bestBase = PETRA.getBestBase(gameState, ent);
if (!bestBase)
{
if (ent.hasClass("Dock"))
API3.warn("Petra: dock in baseManager[0]. It may be useful to do an anchorless base for " + ent.templateName());
continue;
}
if (ent.resourceDropsiteTypes())
this.removeDropsite(gameState, ent);
bestBase.assignEntity(gameState, ent);
}
}
else if (gameState.ai.HQ.canBuildUnits)
{
this.assignToFoundations(gameState);
if (gameState.ai.elapsedTime > this.timeNextIdleCheck)
this.setWorkersIdleByPriority(gameState);
this.assignRolelessUnits(gameState);
this.reassignIdleWorkers(gameState);
for (let ent of this.workers.values())
this.workerObject.update(gameState, ent);
for (let ent of this.mobileDropsites.values())
this.workerObject.moveToGatherer(gameState, ent, false);
}
return false;
}
if (!this.anchor) // This anchor has been destroyed, but the base may still be usable
{
if (!this.buildings.hasEntities())
{
// Reassign all remaining entities to its nearest base
for (let ent of this.units.values())
{
let base = PETRA.getBestBase(gameState, ent, false, this.ID);
base.assignEntity(gameState, ent);
}
return false;
}
// If we have a base with anchor on the same land, reassign everything to it
let reassignedBase;
for (let ent of this.buildings.values())
{
if (!ent.position())
continue;
let base = PETRA.getBestBase(gameState, ent);
if (base.anchor)
reassignedBase = base;
break;
}
if (reassignedBase)
{
for (let ent of this.units.values())
reassignedBase.assignEntity(gameState, ent);
for (let ent of this.buildings.values())
{
if (ent.resourceDropsiteTypes())
this.removeDropsite(gameState, ent);
reassignedBase.assignEntity(gameState, ent);
}
return false;
}
this.assignToFoundations(gameState);
if (gameState.ai.elapsedTime > this.timeNextIdleCheck)
this.setWorkersIdleByPriority(gameState);
this.assignRolelessUnits(gameState);
this.reassignIdleWorkers(gameState);
for (let ent of this.workers.values())
this.workerObject.update(gameState, ent);
for (let ent of this.mobileDropsites.values())
this.workerObject.moveToGatherer(gameState, ent, false);
return true;
}
Engine.ProfileStart("Base update - base " + this.ID);
this.checkResourceLevels(gameState, queues);
this.assignToFoundations(gameState);
if (this.constructing)
{
let owner = gameState.ai.HQ.territoryMap.getOwner(this.anchor.position());
if(owner != 0 && !gameState.isPlayerAlly(owner))
{
// we're in enemy territory. If we're too close from the enemy, destroy us.
let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre"));
for (let cc of ccEnts.values())
{
if (cc.owner() != owner)
continue;
if (API3.SquareVectorDistance(cc.position(), this.anchor.position()) > 8000)
continue;
this.anchor.destroy();
gameState.ai.HQ.resetBaseCache();
break;
}
}
}
else if (this.neededDefenders && gameState.ai.HQ.trainEmergencyUnits(gameState, [this.anchor.position()]))
--this.neededDefenders;
if (gameState.ai.elapsedTime > this.timeNextIdleCheck &&
(gameState.currentPhase() > 1 || gameState.ai.HQ.phasing == 2))
this.setWorkersIdleByPriority(gameState);
this.assignRolelessUnits(gameState);
this.reassignIdleWorkers(gameState);
// check if workers can find something useful to do
for (let ent of this.workers.values())
this.workerObject.update(gameState, ent);
for (let ent of this.mobileDropsites.values())
this.workerObject.moveToGatherer(gameState, ent, false);
Engine.ProfileStop();
return true;
};
PETRA.BaseManager.prototype.Serialize = function()
{
return {
"ID": this.ID,
"anchorId": this.anchorId,
"accessIndex": this.accessIndex,
"maxDistResourceSquare": this.maxDistResourceSquare,
"constructing": this.constructing,
"gatherers": this.gatherers,
"neededDefenders": this.neededDefenders,
"territoryIndices": this.territoryIndices,
"timeNextIdleCheck": this.timeNextIdleCheck
};
};
PETRA.BaseManager.prototype.Deserialize = function(gameState, data)
{
for (let key in data)
this[key] = data[key];
this.anchor = this.anchorId !== undefined ? gameState.getEntityById(this.anchorId) : undefined;
};
Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/entityExtend.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/petra/entityExtend.js (revision 23915)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/entityExtend.js (revision 23916)
@@ -1,416 +1,423 @@
/** returns true if this unit should be considered as a siege unit */
PETRA.isSiegeUnit = function(ent)
{
return ent.hasClass("Siege") || ent.hasClass("Elephant") && ent.hasClass("Melee") && ent.hasClass("Champion");
};
+/** returns true if this unit should be considered as "fast". */
+PETRA.isFastMoving = function(ent)
+{
+ // TODO: use clever logic based on walkspeed comparisons.
+ return ent.hasClass("FastMoving");
+};
+
/** returns some sort of DPS * health factor. If you specify a class, it'll use the modifiers against that class too. */
PETRA.getMaxStrength = function(ent, debugLevel, DamageTypeImportance, againstClass)
{
let strength = 0;
let attackTypes = ent.attackTypes();
let damageTypes = Object.keys(DamageTypeImportance);
if (!attackTypes)
return strength;
for (let type of attackTypes)
{
if (type == "Slaughter")
continue;
let attackStrength = ent.attackStrengths(type);
for (let str in attackStrength)
{
let val = parseFloat(attackStrength[str]);
if (againstClass)
val *= ent.getMultiplierAgainst(type, againstClass);
if (DamageTypeImportance[str])
strength += DamageTypeImportance[str] * val / damageTypes.length;
else if (debugLevel > 0)
API3.warn("Petra: " + str + " unknown attackStrength in getMaxStrength (please add " + str + " to config.js).");
}
let attackRange = ent.attackRange(type);
if (attackRange)
strength += attackRange.max * 0.0125;
let attackTimes = ent.attackTimes(type);
for (let str in attackTimes)
{
let val = parseFloat(attackTimes[str]);
switch (str)
{
case "repeat":
strength += val / 100000;
break;
case "prepare":
strength -= val / 100000;
break;
default:
API3.warn("Petra: " + str + " unknown attackTimes in getMaxStrength");
}
}
}
let armourStrength = ent.armourStrengths();
for (let str in armourStrength)
{
let val = parseFloat(armourStrength[str]);
if (DamageTypeImportance[str])
strength += DamageTypeImportance[str] * val / damageTypes.length;
else if (debugLevel > 0)
API3.warn("Petra: " + str + " unknown armourStrength in getMaxStrength (please add " + str + " to config.js).");
}
return strength * ent.maxHitpoints() / 100.0;
};
/** Get access and cache it (except for units as it can change) in metadata if not already done */
PETRA.getLandAccess = function(gameState, ent)
{
if (ent.hasClass("Unit"))
return gameState.ai.accessibility.getAccessValue(ent.position());
let access = ent.getMetadata(PlayerID, "access");
if (!access)
{
access = gameState.ai.accessibility.getAccessValue(ent.position());
// Docks are sometimes not as expected
if (access < 2 && ent.buildPlacementType() == "shore")
{
let halfDepth = 0;
if (ent.get("Footprint/Square"))
halfDepth = +ent.get("Footprint/Square/@depth") / 2;
else if (ent.get("Footprint/Circle"))
halfDepth = +ent.get("Footprint/Circle/@radius");
let entPos = ent.position();
let cosa = Math.cos(ent.angle());
let sina = Math.sin(ent.angle());
for (let d = 3; d < halfDepth; d += 3)
{
let pos = [ entPos[0] - d * sina,
entPos[1] - d * cosa];
access = gameState.ai.accessibility.getAccessValue(pos);
if (access > 1)
break;
}
}
ent.setMetadata(PlayerID, "access", access);
}
return access;
};
/** Sea access always cached as it never changes */
PETRA.getSeaAccess = function(gameState, ent)
{
let sea = ent.getMetadata(PlayerID, "sea");
if (!sea)
{
sea = gameState.ai.accessibility.getAccessValue(ent.position(), true);
// Docks are sometimes not as expected
if (sea < 2 && ent.buildPlacementType() == "shore")
{
let entPos = ent.position();
let cosa = Math.cos(ent.angle());
let sina = Math.sin(ent.angle());
for (let d = 3; d < 15; d += 3)
{
let pos = [ entPos[0] + d * sina,
entPos[1] + d * cosa];
sea = gameState.ai.accessibility.getAccessValue(pos, true);
if (sea > 1)
break;
}
}
ent.setMetadata(PlayerID, "sea", sea);
}
return sea;
};
PETRA.setSeaAccess = function(gameState, ent)
{
PETRA.getSeaAccess(gameState, ent);
};
/** Decide if we should try to capture (returns true) or destroy (return false) */
PETRA.allowCapture = function(gameState, ent, target)
{
if (!target.isCapturable() || !ent.canCapture(target))
return false;
if (target.isInvulnerable())
return true;
// always try to recapture cp from an allied, except if it's decaying
if (gameState.isPlayerAlly(target.owner()))
return !target.decaying();
let antiCapture = target.defaultRegenRate();
if (target.isGarrisonHolder() && target.garrisoned())
antiCapture += target.garrisonRegenRate() * target.garrisoned().length;
if (target.decaying())
antiCapture -= target.territoryDecayRate();
let capture;
let capturableTargets = gameState.ai.HQ.capturableTargets;
if (!capturableTargets.has(target.id()))
{
capture = ent.captureStrength() * PETRA.getAttackBonus(ent, target, "Capture");
capturableTargets.set(target.id(), { "strength": capture, "ents": new Set([ent.id()]) });
}
else
{
let capturable = capturableTargets.get(target.id());
if (!capturable.ents.has(ent.id()))
{
capturable.strength += ent.captureStrength() * PETRA.getAttackBonus(ent, target, "Capture");
capturable.ents.add(ent.id());
}
capture = capturable.strength;
}
capture *= 1 / (0.1 + 0.9*target.healthLevel());
let sumCapturePoints = target.capturePoints().reduce((a, b) => a + b);
if (target.hasDefensiveFire() && target.isGarrisonHolder() && target.garrisoned())
return capture > antiCapture + sumCapturePoints/50;
return capture > antiCapture + sumCapturePoints/80;
};
PETRA.getAttackBonus = function(ent, target, type)
{
let attackBonus = 1;
if (!ent.get("Attack/" + type) || !ent.get("Attack/" + type + "/Bonuses"))
return attackBonus;
let bonuses = ent.get("Attack/" + type + "/Bonuses");
for (let key in bonuses)
{
let bonus = bonuses[key];
if (bonus.Civ && bonus.Civ !== target.civ())
continue;
if (bonus.Classes && bonus.Classes.split(/\s+/).some(cls => !target.hasClass(cls)))
continue;
attackBonus *= bonus.Multiplier;
}
return attackBonus;
};
/** Makes the worker deposit the currently carried resources at the closest accessible dropsite */
PETRA.returnResources = function(gameState, ent)
{
if (!ent.resourceCarrying() || !ent.resourceCarrying().length || !ent.position())
return false;
let resource = ent.resourceCarrying()[0].type;
let closestDropsite;
let distmin = Math.min();
let access = PETRA.getLandAccess(gameState, ent);
let dropsiteCollection = gameState.playerData.hasSharedDropsites ?
gameState.getAnyDropsites(resource) : gameState.getOwnDropsites(resource);
for (let dropsite of dropsiteCollection.values())
{
if (!dropsite.position())
continue;
let owner = dropsite.owner();
// owner !== PlayerID can only happen when hasSharedDropsites === true, so no need to test it again
if (owner !== PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner)))
continue;
if (PETRA.getLandAccess(gameState, dropsite) != access)
continue;
let dist = API3.SquareVectorDistance(ent.position(), dropsite.position());
if (dist > distmin)
continue;
distmin = dist;
closestDropsite = dropsite;
}
if (!closestDropsite)
return false;
ent.returnResources(closestDropsite);
return true;
};
/** is supply full taking into account gatherers affected during this turn */
PETRA.IsSupplyFull = function(gameState, ent)
{
return ent.isFull() === true ||
ent.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(ent.id()) >= ent.maxGatherers();
};
/**
* Get the best base (in terms of distance and accessIndex) for an entity.
* It should be on the same accessIndex for structures.
* If nothing found, return the base[0] for units and undefined for structures.
* If exclude is given, we exclude the base with ID = exclude.
*/
PETRA.getBestBase = function(gameState, ent, onlyConstructedBase = false, exclude = false)
{
let pos = ent.position();
let accessIndex;
if (!pos)
{
let holder = PETRA.getHolder(gameState, ent);
if (!holder || !holder.position())
{
API3.warn("Petra error: entity without position, but not garrisoned");
PETRA.dumpEntity(ent);
return gameState.ai.HQ.baseManagers[0];
}
pos = holder.position();
accessIndex = PETRA.getLandAccess(gameState, holder);
}
else
accessIndex = PETRA.getLandAccess(gameState, ent);
let distmin = Math.min();
let dist;
let bestbase;
for (let base of gameState.ai.HQ.baseManagers)
{
if (base.ID == gameState.ai.HQ.baseManagers[0].ID || exclude && base.ID == exclude)
continue;
if (onlyConstructedBase && (!base.anchor || base.anchor.foundationProgress() !== undefined))
continue;
if (ent.hasClass("Structure") && base.accessIndex != accessIndex)
continue;
if (base.anchor && base.anchor.position())
dist = API3.SquareVectorDistance(base.anchor.position(), pos);
else
{
let found = false;
for (let structure of base.buildings.values())
{
if (!structure.position())
continue;
dist = API3.SquareVectorDistance(structure.position(), pos);
found = true;
break;
}
if (!found)
continue;
}
if (base.accessIndex != accessIndex)
dist += 50000000;
if (!base.anchor)
dist += 50000000;
if (dist > distmin)
continue;
distmin = dist;
bestbase = base;
}
if (!bestbase && !ent.hasClass("Structure"))
bestbase = gameState.ai.HQ.baseManagers[0];
return bestbase;
};
PETRA.getHolder = function(gameState, ent)
{
for (let holder of gameState.getEntities().values())
{
if (holder.isGarrisonHolder() && holder.garrisoned().indexOf(ent.id()) !== -1)
return holder;
}
return undefined;
};
/** return the template of the built foundation if a foundation, otherwise return the entity itself */
PETRA.getBuiltEntity = function(gameState, ent)
{
if (ent.foundationProgress() !== undefined)
return gameState.getBuiltTemplate(ent.templateName());
return ent;
};
/**
* return true if it is not worth finishing this building (it would surely decay)
* TODO implement the other conditions
*/
PETRA.isNotWorthBuilding = function(gameState, ent)
{
if (gameState.ai.HQ.territoryMap.getOwner(ent.position()) !== PlayerID)
{
let buildTerritories = ent.buildTerritories();
if (buildTerritories && (!buildTerritories.length || buildTerritories.length === 1 && buildTerritories[0] === "own"))
return true;
}
return false;
};
/**
* Check if the straight line between the two positions crosses an enemy territory
*/
PETRA.isLineInsideEnemyTerritory = function(gameState, pos1, pos2, step=70)
{
let n = Math.floor(Math.sqrt(API3.SquareVectorDistance(pos1, pos2))/step) + 1;
let stepx = (pos2[0] - pos1[0]) / n;
let stepy = (pos2[1] - pos1[1]) / n;
for (let i = 1; i < n; ++i)
{
let pos = [pos1[0]+i*stepx, pos1[1]+i*stepy];
let owner = gameState.ai.HQ.territoryMap.getOwner(pos);
if (owner && gameState.isPlayerEnemy(owner))
return true;
}
return false;
};
PETRA.gatherTreasure = function(gameState, ent, water = false)
{
if (!gameState.ai.HQ.treasures.hasEntities())
return false;
if (!ent || !ent.position())
return false;
let rates = ent.resourceGatherRates();
if (!rates || !rates.treasure || rates.treasure <= 0)
return false;
let treasureFound;
let distmin = Math.min();
let access = water ? PETRA.getSeaAccess(gameState, ent) : PETRA.getLandAccess(gameState, ent);
for (let treasure of gameState.ai.HQ.treasures.values())
{
if (PETRA.IsSupplyFull(gameState, treasure))
continue;
// let some time for the previous gatherer to reach the treasure before trying again
let lastGathered = treasure.getMetadata(PlayerID, "lastGathered");
if (lastGathered && gameState.ai.elapsedTime - lastGathered < 20)
continue;
if (!water && access != PETRA.getLandAccess(gameState, treasure))
continue;
if (water && access != PETRA.getSeaAccess(gameState, treasure))
continue;
let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(treasure.position());
if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner))
continue;
let dist = API3.SquareVectorDistance(ent.position(), treasure.position());
if (dist > 120000 || territoryOwner != PlayerID && dist > 14000) // AI has no LOS, so restrict it a bit
continue;
if (dist > distmin)
continue;
distmin = dist;
treasureFound = treasure;
}
if (!treasureFound)
return false;
treasureFound.setMetadata(PlayerID, "lastGathered", gameState.ai.elapsedTime);
ent.gather(treasureFound);
gameState.ai.HQ.AddTCGatherer(treasureFound.id());
ent.setMetadata(PlayerID, "supply", treasureFound.id());
return true;
};
PETRA.dumpEntity = function(ent)
{
if (!ent)
return;
API3.warn(" >>> id " + ent.id() + " name " + ent.genericName() + " pos " + ent.position() +
" state " + ent.unitAIState());
API3.warn(" base " + ent.getMetadata(PlayerID, "base") + " >>> role " + ent.getMetadata(PlayerID, "role") +
" subrole " + ent.getMetadata(PlayerID, "subrole"));
API3.warn("owner " + ent.owner() + " health " + ent.hitpoints() + " healthMax " + ent.maxHitpoints() +
" foundationProgress " + ent.foundationProgress());
API3.warn(" garrisoning " + ent.getMetadata(PlayerID, "garrisoning") +
" garrisonHolder " + ent.getMetadata(PlayerID, "garrisonHolder") +
" plan " + ent.getMetadata(PlayerID, "plan") + " transport " + ent.getMetadata(PlayerID, "transport"));
API3.warn(" stance " + ent.getStance() + " transporter " + ent.getMetadata(PlayerID, "transporter") +
" gather-type " + ent.getMetadata(PlayerID, "gather-type") +
" target-foundation " + ent.getMetadata(PlayerID, "target-foundation") +
" PartOfArmy " + ent.getMetadata(PlayerID, "PartOfArmy"));
};
Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/startingStrategy.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/petra/startingStrategy.js (revision 23915)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/startingStrategy.js (revision 23916)
@@ -1,573 +1,573 @@
/**
* Determines the strategy to adopt when starting a new game,
* depending on the initial conditions
*/
PETRA.HQ.prototype.gameAnalysis = function(gameState)
{
// Analysis of the terrain and the different access regions
if (!this.regionAnalysis(gameState))
return;
this.attackManager.init(gameState);
this.buildManager.init(gameState);
this.navalManager.init(gameState);
this.tradeManager.init(gameState);
this.diplomacyManager.init(gameState);
// Make a list of buildable structures from the config file
this.structureAnalysis(gameState);
// Let's get our initial situation here.
let nobase = new PETRA.BaseManager(gameState, this.Config);
nobase.init(gameState);
nobase.accessIndex = 0;
this.baseManagers.push(nobase); // baseManagers[0] will deal with unit/structure without base
let ccEnts = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre"));
for (let cc of ccEnts.values())
if (cc.foundationProgress() === undefined)
this.createBase(gameState, cc);
else
this.createBase(gameState, cc, "unconstructed");
this.updateTerritories(gameState);
// Assign entities and resources in the different bases
this.assignStartingEntities(gameState);
// Sandbox difficulty should not try to expand
this.canExpand = this.Config.difficulty != 0;
// If no base yet, check if we can construct one. If not, dispatch our units to possible tasks/attacks
this.canBuildUnits = true;
if (!gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre")).hasEntities())
{
let template = gameState.applyCiv("structures/{civ}_civil_centre");
if (!gameState.isTemplateAvailable(template) || !gameState.getTemplate(template).available(gameState))
{
if (this.Config.debug > 1)
API3.warn(" this AI is unable to produce any units");
this.canBuildUnits = false;
this.dispatchUnits(gameState);
}
else
this.buildFirstBase(gameState);
}
// configure our first base strategy
if (this.baseManagers.length > 1)
this.configFirstBase(gameState);
};
/**
* Assign the starting entities to the different bases
*/
PETRA.HQ.prototype.assignStartingEntities = function(gameState)
{
for (let ent of gameState.getOwnEntities().values())
{
// do not affect merchant ship immediately to trade as they may-be useful for transport
if (ent.hasClass("Trader") && !ent.hasClass("Ship"))
this.tradeManager.assignTrader(ent);
let pos = ent.position();
if (!pos)
{
// TODO should support recursive garrisoning. Make a warning for now
if (ent.isGarrisonHolder() && ent.garrisoned().length)
API3.warn("Petra warning: support for garrisoned units inside garrisoned holders not yet implemented");
continue;
}
// make sure we have not rejected small regions with units (TODO should probably also check with other non-gaia units)
let gamepos = gameState.ai.accessibility.gamePosToMapPos(pos);
let index = gamepos[0] + gamepos[1]*gameState.ai.accessibility.width;
let land = gameState.ai.accessibility.landPassMap[index];
if (land > 1 && !this.landRegions[land])
this.landRegions[land] = true;
let sea = gameState.ai.accessibility.navalPassMap[index];
if (sea > 1 && !this.navalRegions[sea])
this.navalRegions[sea] = true;
// if garrisoned units inside, ungarrison them except if a ship in which case we will make a transport
// when a construction will start (see createTransportIfNeeded)
if (ent.isGarrisonHolder() && ent.garrisoned().length && !ent.hasClass("Ship"))
for (let id of ent.garrisoned())
ent.unload(id);
let bestbase;
let territorypos = this.territoryMap.gamePosToMapPos(pos);
let territoryIndex = territorypos[0] + territorypos[1]*this.territoryMap.width;
for (let i = 1; i < this.baseManagers.length; ++i)
{
let base = this.baseManagers[i];
if ((!ent.getMetadata(PlayerID, "base") || ent.getMetadata(PlayerID, "base") != base.ID) &&
base.territoryIndices.indexOf(territoryIndex) == -1)
continue;
base.assignEntity(gameState, ent);
bestbase = base;
break;
}
if (!bestbase) // entity outside our territory
{
if (ent.hasClass("Structure") && !ent.decaying() && ent.resourceDropsiteTypes())
bestbase = this.createBase(gameState, ent, "anchorless");
else
bestbase = PETRA.getBestBase(gameState, ent) || this.baseManagers[0];
bestbase.assignEntity(gameState, ent);
}
// now assign entities garrisoned inside this entity
if (ent.isGarrisonHolder() && ent.garrisoned().length)
for (let id of ent.garrisoned())
bestbase.assignEntity(gameState, gameState.getEntityById(id));
// and find something useful to do if we already have a base
if (pos && bestbase.ID !== this.baseManagers[0].ID)
{
bestbase.assignRolelessUnits(gameState, [ent]);
if (ent.getMetadata(PlayerID, "role") === "worker")
{
bestbase.reassignIdleWorkers(gameState, [ent]);
bestbase.workerObject.update(gameState, ent);
}
}
}
};
/**
* determine the main land Index (or water index if none)
* as well as the list of allowed (land andf water) regions
*/
PETRA.HQ.prototype.regionAnalysis = function(gameState)
{
let accessibility = gameState.ai.accessibility;
let landIndex;
let seaIndex;
let ccEnts = gameState.getOwnStructures().filter(API3.Filters.byClass("CivCentre"));
for (let cc of ccEnts.values())
{
let land = accessibility.getAccessValue(cc.position());
if (land > 1)
{
landIndex = land;
break;
}
}
if (!landIndex)
{
let civ = gameState.getPlayerCiv();
for (let ent of gameState.getOwnEntities().values())
{
if (!ent.position() || !ent.hasClass("Unit") && !ent.trainableEntities(civ))
continue;
let land = accessibility.getAccessValue(ent.position());
if (land > 1)
{
landIndex = land;
break;
}
let sea = accessibility.getAccessValue(ent.position(), true);
if (!seaIndex && sea > 1)
seaIndex = sea;
}
}
if (!landIndex && !seaIndex)
{
API3.warn("Petra error: it does not know how to interpret this map");
return false;
}
let passabilityMap = gameState.getPassabilityMap();
let totalSize = passabilityMap.width * passabilityMap.width;
let minLandSize = Math.floor(0.1*totalSize);
let minWaterSize = Math.floor(0.2*totalSize);
let cellArea = passabilityMap.cellSize * passabilityMap.cellSize;
for (let i = 0; i < accessibility.regionSize.length; ++i)
{
if (landIndex && i == landIndex)
this.landRegions[i] = true;
else if (accessibility.regionType[i] === "land" && cellArea*accessibility.regionSize[i] > 320)
{
if (landIndex)
{
let sea = this.getSeaBetweenIndices(gameState, landIndex, i);
if (sea && (accessibility.regionSize[i] > minLandSize || accessibility.regionSize[sea] > minWaterSize))
{
this.navalMap = true;
this.landRegions[i] = true;
this.navalRegions[sea] = true;
}
}
else
{
let traject = accessibility.getTrajectToIndex(seaIndex, i);
if (traject && traject.length === 2)
{
this.navalMap = true;
this.landRegions[i] = true;
this.navalRegions[seaIndex] = true;
}
}
}
else if (accessibility.regionType[i] === "water" && accessibility.regionSize[i] > minWaterSize)
{
this.navalMap = true;
this.navalRegions[i] = true;
}
else if (accessibility.regionType[i] === "water" && cellArea*accessibility.regionSize[i] > 3600)
this.navalRegions[i] = true;
}
if (this.Config.debug < 3)
return true;
for (let region in this.landRegions)
API3.warn(" >>> zone " + region + " taille " + cellArea*gameState.ai.accessibility.regionSize[region]);
API3.warn(" navalMap " + this.navalMap);
API3.warn(" landRegions " + uneval(this.landRegions));
API3.warn(" navalRegions " + uneval(this.navalRegions));
return true;
};
/**
* load units and buildings from the config files
* TODO: change that to something dynamic
*/
PETRA.HQ.prototype.structureAnalysis = function(gameState)
{
let civref = gameState.playerData.civ;
let civ = civref in this.Config.buildings ? civref : 'default';
this.bAdvanced = [];
for (let building of this.Config.buildings[civ])
if (gameState.isTemplateAvailable(gameState.applyCiv(building)))
this.bAdvanced.push(gameState.applyCiv(building));
};
/**
* build our first base
* if not enough resource, try first to do a dock
*/
PETRA.HQ.prototype.buildFirstBase = function(gameState)
{
if (gameState.ai.queues.civilCentre.hasQueuedUnits())
return;
let templateName = gameState.applyCiv("structures/{civ}_civil_centre");
if (gameState.isTemplateDisabled(templateName))
return;
let template = gameState.getTemplate(templateName);
if (!template)
return;
let total = gameState.getResources();
let goal = "civil_centre";
if (!total.canAfford(new API3.Resources(template.cost())))
{
let totalExpected = gameState.getResources();
// Check for treasures around available in some maps at startup
for (let ent of gameState.getOwnUnits().values())
{
if (!ent.position())
continue;
// If we can get a treasure around, just do it
if (ent.isIdle())
PETRA.gatherTreasure(gameState, ent);
// Then count the resources from the treasures being collected
let supplyId = ent.getMetadata(PlayerID, "supply");
if (!supplyId)
continue;
let supply = gameState.getEntityById(supplyId);
if (!supply || supply.resourceSupplyType().generic != "treasure")
continue;
let type = supply.resourceSupplyType().specific;
if (!(type in totalExpected))
continue;
totalExpected[type] += supply.resourceSupplyMax();
// If we can collect enough resources from these treasures, wait for them
if (totalExpected.canAfford(new API3.Resources(template.cost())))
return;
}
// not enough resource to build a cc, try with a dock to accumulate resources if none yet
if (!this.navalManager.docks.filter(API3.Filters.byClass("Dock")).hasEntities())
{
if (gameState.ai.queues.dock.hasQueuedUnits())
return;
templateName = gameState.applyCiv("structures/{civ}_dock");
if (gameState.isTemplateDisabled(templateName))
return;
template = gameState.getTemplate(templateName);
if (!template || !total.canAfford(new API3.Resources(template.cost())))
return;
goal = "dock";
}
}
if (!this.canBuild(gameState, templateName))
return;
// We first choose as startingPoint the point where we have the more units
let startingPoint = [];
for (let ent of gameState.getOwnUnits().values())
{
if (!ent.hasClass("Worker") && !(ent.hasClass("Support") && ent.hasClass("Elephant")))
continue;
- if (ent.hasClass("Cavalry"))
+ if (PETRA.isFastMoving(ent))
continue;
let pos = ent.position();
if (!pos)
{
let holder = PETRA.getHolder(gameState, ent);
if (!holder || !holder.position())
continue;
pos = holder.position();
}
let gamepos = gameState.ai.accessibility.gamePosToMapPos(pos);
let index = gamepos[0] + gamepos[1]*gameState.ai.accessibility.width;
let land = gameState.ai.accessibility.landPassMap[index];
let sea = gameState.ai.accessibility.navalPassMap[index];
let found = false;
for (let point of startingPoint)
{
if (land !== point.land || sea !== point.sea)
continue;
if (API3.SquareVectorDistance(point.pos, pos) > 2500)
continue;
point.weight += 1;
found = true;
break;
}
if (!found)
startingPoint.push({ "pos": pos, "land": land, "sea": sea, "weight": 1 });
}
if (!startingPoint.length)
return;
let imax = 0;
for (let i = 1; i < startingPoint.length; ++i)
if (startingPoint[i].weight > startingPoint[imax].weight)
imax = i;
if (goal == "dock")
{
let sea = startingPoint[imax].sea > 1 ? startingPoint[imax].sea : undefined;
gameState.ai.queues.dock.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}_dock", { "sea": sea, "proximity": startingPoint[imax].pos }));
}
else
gameState.ai.queues.civilCentre.addPlan(new PETRA.ConstructionPlan(gameState, "structures/{civ}_civil_centre", { "base": -1, "resource": "wood", "proximity": startingPoint[imax].pos }));
};
/**
* set strategy if game without construction:
* - if one of our allies has a cc, affect a small fraction of our army for his defense, the rest will attack
* - otherwise all units will attack
*/
PETRA.HQ.prototype.dispatchUnits = function(gameState)
{
let allycc = gameState.getExclusiveAllyEntities().filter(API3.Filters.byClass("CivCentre")).toEntityArray();
if (allycc.length)
{
if (this.Config.debug > 1)
API3.warn(" We have allied cc " + allycc.length + " and " + gameState.getOwnUnits().length + " units ");
let units = gameState.getOwnUnits();
let num = Math.max(Math.min(Math.round(0.08*(1+this.Config.personality.cooperative)*units.length), 20), 5);
let num1 = Math.floor(num / 2);
let num2 = num1;
// first pass to affect ranged infantry
units.filter(API3.Filters.byClassesAnd(["Infantry", "Ranged"])).forEach(ent => {
if (!num || !num1)
return;
if (ent.getMetadata(PlayerID, "allied"))
return;
let access = PETRA.getLandAccess(gameState, ent);
for (let cc of allycc)
{
if (!cc.position() || PETRA.getLandAccess(gameState, cc) != access)
continue;
--num;
--num1;
ent.setMetadata(PlayerID, "allied", true);
let range = 1.5 * cc.footprintRadius();
ent.moveToRange(cc.position()[0], cc.position()[1], range, range);
break;
}
});
// second pass to affect melee infantry
units.filter(API3.Filters.byClassesAnd(["Infantry", "Melee"])).forEach(ent => {
if (!num || !num2)
return;
if (ent.getMetadata(PlayerID, "allied"))
return;
let access = PETRA.getLandAccess(gameState, ent);
for (let cc of allycc)
{
if (!cc.position() || PETRA.getLandAccess(gameState, cc) != access)
continue;
--num;
--num2;
ent.setMetadata(PlayerID, "allied", true);
let range = 1.5 * cc.footprintRadius();
ent.moveToRange(cc.position()[0], cc.position()[1], range, range);
break;
}
});
// and now complete the affectation, including all support units
units.forEach(ent => {
if (!num && !ent.hasClass("Support"))
return;
if (ent.getMetadata(PlayerID, "allied"))
return;
let access = PETRA.getLandAccess(gameState, ent);
for (let cc of allycc)
{
if (!cc.position() || PETRA.getLandAccess(gameState, cc) != access)
continue;
if (!ent.hasClass("Support"))
--num;
ent.setMetadata(PlayerID, "allied", true);
let range = 1.5 * cc.footprintRadius();
ent.moveToRange(cc.position()[0], cc.position()[1], range, range);
break;
}
});
}
};
/**
* configure our first base expansion
* - if on a small island, favor fishing
* - count the available wood resource, and allow rushes only if enough (we should otherwise favor expansion)
*/
PETRA.HQ.prototype.configFirstBase = function(gameState)
{
if (this.baseManagers.length < 2)
return;
this.firstBaseConfig = true;
let startingSize = 0;
let startingLand = [];
for (let region in this.landRegions)
{
for (let base of this.baseManagers)
{
if (!base.anchor || base.accessIndex != +region)
continue;
startingSize += gameState.ai.accessibility.regionSize[region];
startingLand.push(base.accessIndex);
break;
}
}
let cell = gameState.getPassabilityMap().cellSize;
startingSize = startingSize * cell * cell;
if (this.Config.debug > 1)
API3.warn("starting size " + startingSize + "(cut at 24000 for fish pushing)");
if (startingSize < 25000)
{
this.saveSpace = true;
this.Config.Economy.popForDock = Math.min(this.Config.Economy.popForDock, 16);
let num = Math.max(this.Config.Economy.targetNumFishers, 2);
for (let land of startingLand)
{
for (let sea of gameState.ai.accessibility.regionLinks[land])
if (gameState.ai.HQ.navalRegions[sea])
this.navalManager.updateFishingBoats(sea, num);
}
this.maxFields = 1;
this.needCorral = true;
}
else if (startingSize < 60000)
this.maxFields = 2;
else
this.maxFields = false;
// - count the available food resource, and react accordingly
let startingFood = gameState.getResources().food;
let check = {};
for (let proxim of ["nearby", "medium", "faraway"])
{
for (let base of this.baseManagers)
{
for (let supply of base.dropsiteSupplies.food[proxim])
{
if (check[supply.id]) // avoid double counting as same resource can appear several time
continue;
check[supply.id] = true;
startingFood += supply.ent.resourceSupplyAmount();
}
}
}
if (startingFood < 800)
{
if (startingSize < 25000)
{
this.needFish = true;
this.Config.Economy.popForDock = 1;
}
else
this.needFarm = true;
}
// - count the available wood resource, and allow rushes only if enough (we should otherwise favor expansion)
let startingWood = gameState.getResources().wood;
check = {};
for (let proxim of ["nearby", "medium", "faraway"])
{
for (let base of this.baseManagers)
{
for (let supply of base.dropsiteSupplies.wood[proxim])
{
if (check[supply.id]) // avoid double counting as same resource can appear several time
continue;
check[supply.id] = true;
startingWood += supply.ent.resourceSupplyAmount();
}
}
}
if (this.Config.debug > 1)
API3.warn("startingWood: " + startingWood + " (cut at 8500 for no rush and 6000 for saveResources)");
if (startingWood < 6000)
{
this.saveResources = true;
this.Config.Economy.popPhase2 = Math.floor(0.75 * this.Config.Economy.popPhase2); // Switch to town phase sooner to be able to expand
if (startingWood < 2000 && this.needFarm)
{
this.needCorral = true;
this.needFarm = false;
}
}
if (startingWood > 8500 && this.canBuildUnits)
{
let allowed = Math.ceil((startingWood - 8500) / 3000);
// Not useful to prepare rushing if too long ceasefire
if (gameState.isCeasefireActive())
{
if (gameState.ceasefireTimeRemaining > 900)
allowed = 0;
else if (gameState.ceasefireTimeRemaining > 600 && allowed > 1)
allowed = 1;
}
this.attackManager.setRushes(allowed);
}
// immediatly build a wood dropsite if possible.
let template = gameState.applyCiv("structures/{civ}_storehouse");
if (!gameState.getOwnEntitiesByClass("Storehouse", true).hasEntities() && this.canBuild(gameState, template))
{
let newDP = this.baseManagers[1].findBestDropsiteLocation(gameState, "wood");
if (newDP.quality > 40)
{
// if we start with enough workers, put our available resources in this first dropsite
// same thing if our pop exceed the allowed one, as we will need several houses
let numWorkers = gameState.getOwnUnits().filter(API3.Filters.byClass("Worker")).length;
if (numWorkers > 12 && newDP.quality > 60 ||
gameState.getPopulation() > gameState.getPopulationLimit() + 20)
{
let cost = new API3.Resources(gameState.getTemplate(template).cost());
gameState.ai.queueManager.setAccounts(gameState, cost, "dropsites");
}
gameState.ai.queues.dropsites.addPlan(new PETRA.ConstructionPlan(gameState, template, { "base": this.baseManagers[1].ID }, newDP.pos));
}
}
// and build immediately a corral if needed
if (this.needCorral)
{
template = gameState.applyCiv("structures/{civ}_corral");
if (!gameState.getOwnEntitiesByClass("Corral", true).hasEntities() && this.canBuild(gameState, template))
gameState.ai.queues.corral.addPlan(new PETRA.ConstructionPlan(gameState, template, { "base": this.baseManagers[1].ID }));
}
};
Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/worker.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/ai/petra/worker.js (revision 23915)
+++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/worker.js (revision 23916)
@@ -1,1107 +1,1107 @@
/**
* This class makes a worker do as instructed by the economy manager
*/
PETRA.Worker = function(base)
{
this.ent = undefined;
this.base = base;
this.baseID = base.ID;
};
PETRA.Worker.prototype.update = function(gameState, ent)
{
if (!ent.position() || ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3)
return;
let subrole = ent.getMetadata(PlayerID, "subrole");
// If we are waiting for a transport or we are sailing, just wait
if (ent.getMetadata(PlayerID, "transport") !== undefined)
{
// Except if builder with their foundation destroyed, in which case cancel the transport if not yet on board
if (subrole == "builder" && ent.getMetadata(PlayerID, "target-foundation") !== undefined)
{
let plan = gameState.ai.HQ.navalManager.getPlan(ent.getMetadata(PlayerID, "transport"));
let target = gameState.getEntityById(ent.getMetadata(PlayerID, "target-foundation"));
if (!target && plan && plan.state == "boarding" && ent.position())
plan.removeUnit(gameState, ent);
}
// and gatherer if there are no more dropsite accessible in the base the ent is going to
if (subrole == "gatherer" || subrole == "hunter")
{
let plan = gameState.ai.HQ.navalManager.getPlan(ent.getMetadata(PlayerID, "transport"));
if (plan.state == "boarding" && ent.position())
{
let hasDropsite = false;
let gatherType = ent.getMetadata(PlayerID, "gather-type") || "food";
for (let structure of gameState.getOwnStructures().values())
{
if (PETRA.getLandAccess(gameState, structure) != plan.endIndex)
continue;
let resourceDropsiteTypes = PETRA.getBuiltEntity(gameState, structure).resourceDropsiteTypes();
if (!resourceDropsiteTypes || resourceDropsiteTypes.indexOf(gatherType) == -1)
continue;
hasDropsite = true;
break;
}
if (!hasDropsite)
{
for (let unit of gameState.getOwnUnits().filter(API3.Filters.byClass("Support")).values())
{
if (!unit.position() || PETRA.getLandAccess(gameState, unit) != plan.endIndex)
continue;
let resourceDropsiteTypes = unit.resourceDropsiteTypes();
if (!resourceDropsiteTypes || resourceDropsiteTypes.indexOf(gatherType) == -1)
continue;
hasDropsite = true;
break;
}
}
if (!hasDropsite)
plan.removeUnit(gameState, ent);
}
}
if (ent.getMetadata(PlayerID, "transport") !== undefined)
return;
}
this.entAccess = PETRA.getLandAccess(gameState, ent);
// base 0 for unassigned entities has no accessIndex, so take the one from the entity
if (this.baseID == gameState.ai.HQ.baseManagers[0].ID)
this.baseAccess = this.entAccess;
else
this.baseAccess = this.base.accessIndex;
if (!subrole) // subrole may-be undefined after a transport, garrisoning, army, ...
{
ent.setMetadata(PlayerID, "subrole", "idle");
this.base.reassignIdleWorkers(gameState, [ent]);
this.update(gameState, ent);
return;
}
this.ent = ent;
let unitAIState = ent.unitAIState();
if ((subrole == "hunter" || subrole == "gatherer") &&
(unitAIState == "INDIVIDUAL.GATHER.GATHERING" || unitAIState == "INDIVIDUAL.GATHER.APPROACHING" ||
unitAIState == "INDIVIDUAL.COMBAT.APPROACHING"))
{
if (this.isInaccessibleSupply(gameState))
{
if (this.retryWorking(gameState, subrole))
return;
ent.stopMoving();
}
if (unitAIState == "INDIVIDUAL.COMBAT.APPROACHING" && ent.unitAIOrderData().length)
{
let orderData = ent.unitAIOrderData()[0];
if (orderData && orderData.target)
{
// Check that we have not drifted too far when hunting
let target = gameState.getEntityById(orderData.target);
if (target && target.resourceSupplyType() && target.resourceSupplyType().generic == "food")
{
let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(target.position());
if (gameState.isPlayerEnemy(territoryOwner))
{
if (this.retryWorking(gameState, subrole))
return;
ent.stopMoving();
}
else if (!gameState.isPlayerAlly(territoryOwner))
{
- let distanceSquare = ent.hasClass("Cavalry") ? 90000 : 30000;
+ let distanceSquare = ent.isFastMoving() ? 90000 : 30000;
let targetAccess = PETRA.getLandAccess(gameState, target);
let foodDropsites = gameState.playerData.hasSharedDropsites ?
gameState.getAnyDropsites("food") : gameState.getOwnDropsites("food");
let hasFoodDropsiteWithinDistance = false;
for (let dropsite of foodDropsites.values())
{
if (!dropsite.position())
continue;
let owner = dropsite.owner();
// owner != PlayerID can only happen when hasSharedDropsites == true, so no need to test it again
if (owner != PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner)))
continue;
if (targetAccess != PETRA.getLandAccess(gameState, dropsite))
continue;
if (API3.SquareVectorDistance(target.position(), dropsite.position()) < distanceSquare)
{
hasFoodDropsiteWithinDistance = true;
break;
}
}
if (!hasFoodDropsiteWithinDistance)
{
if (this.retryWorking(gameState, subrole))
return;
ent.stopMoving();
}
}
}
}
}
}
else if (ent.getMetadata(PlayerID, "approachingTarget"))
{
ent.setMetadata(PlayerID, "approachingTarget", undefined);
ent.setMetadata(PlayerID, "alreadyTried", undefined);
}
let unitAIStateOrder = unitAIState.split(".")[1];
// If we're fighting or hunting, let's not start gathering except if inaccessible target
// but for fishers where UnitAI must have made us target a moving whale.
// Also, if we are attacking, do not capture
if (unitAIStateOrder == "COMBAT")
{
if (subrole == "fisher")
this.startFishing(gameState);
else if (unitAIState == "INDIVIDUAL.COMBAT.APPROACHING" && ent.unitAIOrderData().length &&
!ent.getMetadata(PlayerID, "PartOfArmy"))
{
let orderData = ent.unitAIOrderData()[0];
if (orderData && orderData.target)
{
let target = gameState.getEntityById(orderData.target);
if (target && (!target.position() || PETRA.getLandAccess(gameState, target) != this.entAccess))
{
if (this.retryWorking(gameState, subrole))
return;
ent.stopMoving();
}
}
}
else if (unitAIState == "INDIVIDUAL.COMBAT.ATTACKING" && ent.unitAIOrderData().length &&
!ent.getMetadata(PlayerID, "PartOfArmy"))
{
let orderData = ent.unitAIOrderData()[0];
if (orderData && orderData.target && orderData.attackType && orderData.attackType == "Capture")
{
// If we are here, an enemy structure must have targeted one of our workers
// and UnitAI sent it fight back with allowCapture=true
let target = gameState.getEntityById(orderData.target);
if (target && target.owner() > 0 && !gameState.isPlayerAlly(target.owner()))
ent.attack(orderData.target, PETRA.allowCapture(gameState, ent, target));
}
}
return;
}
// Okay so we have a few tasks.
// If we're gathering, we'll check that we haven't run idle.
// And we'll also check that we're gathering a resource we want to gather.
if (subrole == "gatherer")
{
if (ent.isIdle())
{
// if we aren't storing resources or it's the same type as what we're about to gather,
// let's just pick a new resource.
// TODO if we already carry the max we can -> returnresources
if (!ent.resourceCarrying() || !ent.resourceCarrying().length ||
ent.resourceCarrying()[0].type == ent.getMetadata(PlayerID, "gather-type"))
{
this.startGathering(gameState);
}
else if (!PETRA.returnResources(gameState, ent)) // try to deposit resources
{
// no dropsite, abandon old resources and start gathering new ones
this.startGathering(gameState);
}
}
else if (unitAIStateOrder == "GATHER")
{
// we're already gathering. But let's check if there is nothing better
// in case UnitAI did something bad
if (ent.unitAIOrderData().length)
{
let supplyId = ent.unitAIOrderData()[0].target;
let supply = gameState.getEntityById(supplyId);
if (supply && !supply.hasClass("Field") && !supply.hasClass("Animal") &&
supply.resourceSupplyType().generic != "treasure" &&
supplyId != ent.getMetadata(PlayerID, "supply"))
{
let nbGatherers = supply.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supplyId);
if (nbGatherers > 1 && supply.resourceSupplyAmount()/nbGatherers < 30)
{
gameState.ai.HQ.RemoveTCGatherer(supplyId);
this.startGathering(gameState);
}
else
{
let gatherType = ent.getMetadata(PlayerID, "gather-type");
let nearby = this.base.dropsiteSupplies[gatherType].nearby;
if (nearby.some(sup => sup.id == supplyId))
ent.setMetadata(PlayerID, "supply", supplyId);
else if (nearby.length)
{
gameState.ai.HQ.RemoveTCGatherer(supplyId);
this.startGathering(gameState);
}
else
{
let medium = this.base.dropsiteSupplies[gatherType].medium;
if (medium.length && !medium.some(sup => sup.id == supplyId))
{
gameState.ai.HQ.RemoveTCGatherer(supplyId);
this.startGathering(gameState);
}
else
ent.setMetadata(PlayerID, "supply", supplyId);
}
}
}
}
}
else if (unitAIState == "INDIVIDUAL.RETURNRESOURCE.APPROACHING")
{
if (gameState.ai.playedTurn % 10 == 0)
{
// Check from time to time that UnitAI does not send us to an inaccessible dropsite
let dropsite = gameState.getEntityById(ent.unitAIOrderData()[0].target);
if (dropsite && dropsite.position() && this.entAccess != PETRA.getLandAccess(gameState, dropsite))
PETRA.returnResources(gameState, this.ent);
}
// If gathering a sparse resource, we may have been sent to a faraway resource if the one nearby was full.
// Let's check if it is still the case. If so, we reset its metadata supplyId so that the unit will be
// reordered to gather after having returned the resources (when comparing its supplyId with the UnitAI one).
let gatherType = ent.getMetadata(PlayerID, "gather-type");
let influenceGroup = Resources.GetResource(gatherType).aiAnalysisInfluenceGroup;
if (influenceGroup && influenceGroup == "sparse")
{
let supplyId = ent.getMetadata(PlayerID, "supply");
if (supplyId)
{
let nearby = this.base.dropsiteSupplies[gatherType].nearby;
if (!nearby.some(sup => sup.id == supplyId))
{
if (nearby.length)
ent.setMetadata(PlayerID, "supply", undefined);
else
{
let medium = this.base.dropsiteSupplies[gatherType].medium;
if (!medium.some(sup => sup.id == supplyId) && medium.length)
ent.setMetadata(PlayerID, "supply", undefined);
}
}
}
}
}
}
else if (subrole == "builder")
{
if (unitAIStateOrder == "REPAIR")
{
// Update our target in case UnitAI sent us to a different foundation because of autocontinue
// and abandon it if UnitAI has sent us to build a field (as we build them only when needed)
if (ent.unitAIOrderData()[0] && ent.unitAIOrderData()[0].target &&
ent.getMetadata(PlayerID, "target-foundation") != ent.unitAIOrderData()[0].target)
{
let targetId = ent.unitAIOrderData()[0].target;
let target = gameState.getEntityById(targetId);
if (target && !target.hasClass("Field"))
{
ent.setMetadata(PlayerID, "target-foundation", targetId);
return;
}
ent.setMetadata(PlayerID, "target-foundation", undefined);
ent.setMetadata(PlayerID, "subrole", "idle");
ent.stopMoving();
if (this.baseID != gameState.ai.HQ.baseManagers[0].ID)
{
// reassign it to something useful
this.base.reassignIdleWorkers(gameState, [ent]);
this.update(gameState, ent);
return;
}
}
// Otherwise check that the target still exists (useful in REPAIR.APPROACHING)
let targetId = ent.getMetadata(PlayerID, "target-foundation");
if (targetId && gameState.getEntityById(targetId))
return;
ent.stopMoving();
}
// okay so apparently we aren't working.
// Unless we've been explicitely told to keep our role, make us idle.
let target = gameState.getEntityById(ent.getMetadata(PlayerID, "target-foundation"));
if (!target || target.foundationProgress() === undefined && target.needsRepair() === false)
{
ent.setMetadata(PlayerID, "subrole", "idle");
ent.setMetadata(PlayerID, "target-foundation", undefined);
// If worker elephant, move away to avoid being trapped in between constructions
if (ent.hasClass("Elephant"))
this.moveToGatherer(gameState, ent, true);
else if (this.baseID != gameState.ai.HQ.baseManagers[0].ID)
{
// reassign it to something useful
this.base.reassignIdleWorkers(gameState, [ent]);
this.update(gameState, ent);
return;
}
}
else
{
let goalAccess = PETRA.getLandAccess(gameState, target);
let queued = PETRA.returnResources(gameState, ent);
if (this.entAccess == goalAccess)
ent.repair(target, target.hasClass("House"), queued); // autocontinue=true for houses
else
gameState.ai.HQ.navalManager.requireTransport(gameState, ent, this.entAccess, goalAccess, target.position());
}
}
else if (subrole == "hunter")
{
let lastHuntSearch = ent.getMetadata(PlayerID, "lastHuntSearch");
if (ent.isIdle() && (!lastHuntSearch || gameState.ai.elapsedTime - lastHuntSearch > 20))
{
if (!this.startHunting(gameState))
{
// nothing to hunt around. Try another region if any
let nowhereToHunt = true;
for (let base of gameState.ai.HQ.baseManagers)
{
if (!base.anchor || !base.anchor.position())
continue;
let basePos = base.anchor.position();
if (this.startHunting(gameState, basePos))
{
ent.setMetadata(PlayerID, "base", base.ID);
if (base.accessIndex == this.entAccess)
ent.move(basePos[0], basePos[1]);
else
gameState.ai.HQ.navalManager.requireTransport(gameState, ent, this.entAccess, base.accessIndex, basePos);
nowhereToHunt = false;
break;
}
}
if (nowhereToHunt)
ent.setMetadata(PlayerID, "lastHuntSearch", gameState.ai.elapsedTime);
}
}
else // Perform some sanity checks
{
if (unitAIStateOrder == "GATHER" || unitAIStateOrder == "RETURNRESOURCE")
{
// we may have drifted towards ennemy territory during the hunt, if yes go home
let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(ent.position());
if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally
this.startHunting(gameState);
else if (unitAIState == "INDIVIDUAL.RETURNRESOURCE.APPROACHING")
{
// Check that UnitAI does not send us to an inaccessible dropsite
let dropsite = gameState.getEntityById(ent.unitAIOrderData()[0].target);
if (dropsite && dropsite.position() && this.entAccess != PETRA.getLandAccess(gameState, dropsite))
PETRA.returnResources(gameState, ent);
}
}
}
}
else if (subrole == "fisher")
{
if (ent.isIdle())
this.startFishing(gameState);
else // if we have drifted towards ennemy territory during the fishing, go home
{
let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(ent.position());
if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally
this.startFishing(gameState);
}
}
};
PETRA.Worker.prototype.retryWorking = function(gameState, subrole)
{
switch (subrole)
{
case "gatherer":
return this.startGathering(gameState);
case "hunter":
return this.startHunting(gameState);
case "fisher":
return this.startFishing(gameState);
case "builder":
return this.startBuilding(gameState);
default:
return false;
}
};
PETRA.Worker.prototype.startBuilding = function(gameState)
{
let target = gameState.getEntityById(this.ent.getMetadata(PlayerID, "target-foundation"));
if (!target || target.foundationProgress() === undefined && target.needsRepair() == false)
return false;
if (PETRA.getLandAccess(gameState, target) != this.entAccess)
return false;
this.ent.repair(target, target.hasClass("House")); // autocontinue=true for houses
return true;
};
PETRA.Worker.prototype.startGathering = function(gameState)
{
// First look for possible treasure if any
if (PETRA.gatherTreasure(gameState, this.ent))
return true;
let resource = this.ent.getMetadata(PlayerID, "gather-type");
// If we are gathering food, try to hunt first
if (resource == "food" && this.startHunting(gameState))
return true;
let findSupply = function(ent, supplies) {
let ret = false;
let gatherRates = ent.resourceGatherRates();
for (let i = 0; i < supplies.length; ++i)
{
// exhausted resource, remove it from this list
if (!supplies[i].ent || !gameState.getEntityById(supplies[i].id))
{
supplies.splice(i--, 1);
continue;
}
if (PETRA.IsSupplyFull(gameState, supplies[i].ent))
continue;
let inaccessibleTime = supplies[i].ent.getMetadata(PlayerID, "inaccessibleTime");
if (inaccessibleTime && gameState.ai.elapsedTime < inaccessibleTime)
continue;
let supplyType = supplies[i].ent.get("ResourceSupply/Type");
if (!gatherRates[supplyType])
continue;
// check if available resource is worth one additionnal gatherer (except for farms)
let nbGatherers = supplies[i].ent.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supplies[i].id);
if (supplies[i].ent.resourceSupplyType().specific != "grain" && nbGatherers > 0 &&
supplies[i].ent.resourceSupplyAmount()/(1+nbGatherers) < 30)
continue;
// not in ennemy territory
let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(supplies[i].ent.position());
if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally
continue;
gameState.ai.HQ.AddTCGatherer(supplies[i].id);
ent.setMetadata(PlayerID, "supply", supplies[i].id);
ret = supplies[i].ent;
break;
}
return ret;
};
let navalManager = gameState.ai.HQ.navalManager;
let supply;
// first look in our own base if accessible from our present position
if (this.baseAccess == this.entAccess)
{
supply = findSupply(this.ent, this.base.dropsiteSupplies[resource].nearby);
if (supply)
{
this.ent.gather(supply);
return true;
}
// --> for food, try to gather from fields if any, otherwise build one if any
if (resource == "food")
{
supply = this.gatherNearestField(gameState, this.baseID);
if (supply)
{
this.ent.gather(supply);
return true;
}
supply = this.buildAnyField(gameState, this.baseID);
if (supply)
{
this.ent.repair(supply);
return true;
}
}
supply = findSupply(this.ent, this.base.dropsiteSupplies[resource].medium);
if (supply)
{
this.ent.gather(supply);
return true;
}
}
// So if we're here we have checked our whole base for a proper resource (or it was not accessible)
// --> check other bases directly accessible
for (let base of gameState.ai.HQ.baseManagers)
{
if (base.ID == this.baseID)
continue;
if (base.accessIndex != this.entAccess)
continue;
supply = findSupply(this.ent, base.dropsiteSupplies[resource].nearby);
if (supply)
{
this.ent.setMetadata(PlayerID, "base", base.ID);
this.ent.gather(supply);
return true;
}
}
if (resource == "food") // --> for food, try to gather from fields if any, otherwise build one if any
{
for (let base of gameState.ai.HQ.baseManagers)
{
if (base.ID == this.baseID)
continue;
if (base.accessIndex != this.entAccess)
continue;
supply = this.gatherNearestField(gameState, base.ID);
if (supply)
{
this.ent.setMetadata(PlayerID, "base", base.ID);
this.ent.gather(supply);
return true;
}
supply = this.buildAnyField(gameState, base.ID);
if (supply)
{
this.ent.setMetadata(PlayerID, "base", base.ID);
this.ent.repair(supply);
return true;
}
}
}
for (let base of gameState.ai.HQ.baseManagers)
{
if (base.ID == this.baseID)
continue;
if (base.accessIndex != this.entAccess)
continue;
supply = findSupply(this.ent, base.dropsiteSupplies[resource].medium);
if (supply)
{
this.ent.setMetadata(PlayerID, "base", base.ID);
this.ent.gather(supply);
return true;
}
}
// Okay may-be we haven't found any appropriate dropsite anywhere.
// Try to help building one if any accessible foundation available
let foundations = gameState.getOwnFoundations().toEntityArray();
let shouldBuild = this.ent.isBuilder() && foundations.some(function(foundation) {
if (!foundation || PETRA.getLandAccess(gameState, foundation) != this.entAccess)
return false;
let structure = gameState.getBuiltTemplate(foundation.templateName());
if (structure.resourceDropsiteTypes() && structure.resourceDropsiteTypes().indexOf(resource) != -1)
{
if (foundation.getMetadata(PlayerID, "base") != this.baseID)
this.ent.setMetadata(PlayerID, "base", foundation.getMetadata(PlayerID, "base"));
this.ent.setMetadata(PlayerID, "target-foundation", foundation.id());
this.ent.setMetadata(PlayerID, "subrole", "builder");
this.ent.repair(foundation);
return true;
}
return false;
}, this);
if (shouldBuild)
return true;
// Still nothing ... try bases which need a transport
for (let base of gameState.ai.HQ.baseManagers)
{
if (base.accessIndex == this.entAccess)
continue;
supply = findSupply(this.ent, base.dropsiteSupplies[resource].nearby);
if (supply && navalManager.requireTransport(gameState, this.ent, this.entAccess, base.accessIndex, supply.position()))
{
if (base.ID != this.baseID)
this.ent.setMetadata(PlayerID, "base", base.ID);
return true;
}
}
if (resource == "food") // --> for food, try to gather from fields if any, otherwise build one if any
{
for (let base of gameState.ai.HQ.baseManagers)
{
if (base.accessIndex == this.entAccess)
continue;
supply = this.gatherNearestField(gameState, base.ID);
if (supply && navalManager.requireTransport(gameState, this.ent, this.entAccess, base.accessIndex, supply.position()))
{
if (base.ID != this.baseID)
this.ent.setMetadata(PlayerID, "base", base.ID);
return true;
}
supply = this.buildAnyField(gameState, base.ID);
if (supply && navalManager.requireTransport(gameState, this.ent, this.entAccess, base.accessIndex, supply.position()))
{
if (base.ID != this.baseID)
this.ent.setMetadata(PlayerID, "base", base.ID);
return true;
}
}
}
for (let base of gameState.ai.HQ.baseManagers)
{
if (base.accessIndex == this.entAccess)
continue;
supply = findSupply(this.ent, base.dropsiteSupplies[resource].medium);
if (supply && navalManager.requireTransport(gameState, this.ent, this.entAccess, base.accessIndex, supply.position()))
{
if (base.ID != this.baseID)
this.ent.setMetadata(PlayerID, "base", base.ID);
return true;
}
}
// Okay so we haven't found any appropriate dropsite anywhere.
// Try to help building one if any non-accessible foundation available
shouldBuild = this.ent.isBuilder() && foundations.some(function(foundation) {
if (!foundation || PETRA.getLandAccess(gameState, foundation) == this.entAccess)
return false;
let structure = gameState.getBuiltTemplate(foundation.templateName());
if (structure.resourceDropsiteTypes() && structure.resourceDropsiteTypes().indexOf(resource) != -1)
{
let foundationAccess = PETRA.getLandAccess(gameState, foundation);
if (navalManager.requireTransport(gameState, this.ent, this.entAccess, foundationAccess, foundation.position()))
{
if (foundation.getMetadata(PlayerID, "base") != this.baseID)
this.ent.setMetadata(PlayerID, "base", foundation.getMetadata(PlayerID, "base"));
this.ent.setMetadata(PlayerID, "target-foundation", foundation.id());
this.ent.setMetadata(PlayerID, "subrole", "builder");
return true;
}
}
return false;
}, this);
if (shouldBuild)
return true;
// Still nothing, we look now for faraway resources, first in the accessible ones, then in the others
// except for food when farms or corrals can be used
let allowDistant = true;
if (resource == "food")
{
if (gameState.ai.HQ.turnCache.allowDistantFood === undefined)
gameState.ai.HQ.turnCache.allowDistantFood =
!gameState.ai.HQ.canBuild(gameState, "structures/{civ}_field") &&
!gameState.ai.HQ.canBuild(gameState, "structures/{civ}_corral");
allowDistant = gameState.ai.HQ.turnCache.allowDistantFood;
}
if (allowDistant)
{
if (this.baseAccess == this.entAccess)
{
supply = findSupply(this.ent, this.base.dropsiteSupplies[resource].faraway);
if (supply)
{
this.ent.gather(supply);
return true;
}
}
for (let base of gameState.ai.HQ.baseManagers)
{
if (base.ID == this.baseID)
continue;
if (base.accessIndex != this.entAccess)
continue;
supply = findSupply(this.ent, base.dropsiteSupplies[resource].faraway);
if (supply)
{
this.ent.setMetadata(PlayerID, "base", base.ID);
this.ent.gather(supply);
return true;
}
}
for (let base of gameState.ai.HQ.baseManagers)
{
if (base.accessIndex == this.entAccess)
continue;
supply = findSupply(this.ent, base.dropsiteSupplies[resource].faraway);
if (supply && navalManager.requireTransport(gameState, this.ent, this.entAccess, base.accessIndex, supply.position()))
{
if (base.ID != this.baseID)
this.ent.setMetadata(PlayerID, "base", base.ID);
return true;
}
}
}
// If we are here, we have nothing left to gather ... certainly no more resources of this type
gameState.ai.HQ.lastFailedGather[resource] = gameState.ai.elapsedTime;
if (gameState.ai.Config.debug > 2)
API3.warn(" >>>>> worker with gather-type " + resource + " with nothing to gather ");
this.ent.setMetadata(PlayerID, "subrole", "idle");
return false;
};
/**
* if position is given, we only check if we could hunt from this position but do nothing
* otherwise the position of the entity is taken, and if something is found, we directly start the hunt
*/
PETRA.Worker.prototype.startHunting = function(gameState, position)
{
// First look for possible treasure if any
if (!position && PETRA.gatherTreasure(gameState, this.ent))
return true;
let resources = gameState.getHuntableSupplies();
if (!resources.hasEntities())
return false;
let nearestSupplyDist = Math.min();
let nearestSupply;
- let isCavalry = this.ent.hasClass("Cavalry");
+ let isFastMoving = PETRA.isFastMoving(this.ent);
let isRanged = this.ent.hasClass("Ranged");
let entPosition = position ? position : this.ent.position();
let foodDropsites = gameState.playerData.hasSharedDropsites ?
gameState.getAnyDropsites("food") : gameState.getOwnDropsites("food");
let hasFoodDropsiteWithinDistance = function(supplyPosition, supplyAccess, distSquare)
{
for (let dropsite of foodDropsites.values())
{
if (!dropsite.position())
continue;
let owner = dropsite.owner();
// owner != PlayerID can only happen when hasSharedDropsites == true, so no need to test it again
if (owner != PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner)))
continue;
if (supplyAccess != PETRA.getLandAccess(gameState, dropsite))
continue;
if (API3.SquareVectorDistance(supplyPosition, dropsite.position()) < distSquare)
return true;
}
return false;
};
let gatherRates = this.ent.resourceGatherRates();
for (let supply of resources.values())
{
if (!supply.position())
continue;
let inaccessibleTime = supply.getMetadata(PlayerID, "inaccessibleTime");
if (inaccessibleTime && gameState.ai.elapsedTime < inaccessibleTime)
continue;
let supplyType = supply.get("ResourceSupply/Type");
if (!gatherRates[supplyType])
continue;
if (PETRA.IsSupplyFull(gameState, supply))
continue;
- // check if available resource is worth one additionnal gatherer (except for farms)
+ // Check if available resource is worth one additionnal gatherer (except for farms).
let nbGatherers = supply.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supply.id());
if (nbGatherers > 0 && supply.resourceSupplyAmount()/(1+nbGatherers) < 30)
continue;
let canFlee = !supply.hasClass("Domestic") && supply.templateName().indexOf("resource|") == -1;
- // Only cavalry and range units should hunt fleeing animals
- if (canFlee && !isCavalry && !isRanged)
+ // Only FastMoving and Ranged units should hunt fleeing animals.
+ if (canFlee && !isFastMoving && !isRanged)
continue;
let supplyAccess = PETRA.getLandAccess(gameState, supply);
if (supplyAccess != this.entAccess)
continue;
- // measure the distance to the resource
+ // measure the distance to the resource.
let dist = API3.SquareVectorDistance(entPosition, supply.position());
if (dist > nearestSupplyDist)
continue;
- // Only cavalry should hunt faraway
- if (!isCavalry && dist > 25000)
+ // Only FastMoving should hunt faraway.
+ if (!isFastMoving && dist > 25000)
continue;
- // Avoid ennemy territory
+ // Avoid enemy territory.
let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(supply.position());
- if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally
+ if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // Player is its own ally.
continue;
- // And if in ally territory, don't hunt this ally's cattle
+ // And if in ally territory, don't hunt this ally's cattle.
if (territoryOwner != 0 && territoryOwner != PlayerID && supply.owner() == territoryOwner)
continue;
- // Only cavalry should hunt far from dropsite (specially for non domestic animals which flee)
- if (!isCavalry && canFlee && territoryOwner == 0)
+ // Only FastMoving should hunt far from dropsite (specially for non-Domestic animals which flee).
+ if (!isFastMoving && canFlee && territoryOwner == 0)
continue;
- let distanceSquare = isCavalry ? 35000 : (canFlee ? 7000 : 12000);
+ let distanceSquare = isFastMoving ? 35000 : (canFlee ? 7000 : 12000);
if (!hasFoodDropsiteWithinDistance(supply.position(), supplyAccess, distanceSquare))
continue;
nearestSupplyDist = dist;
nearestSupply = supply;
}
if (nearestSupply)
{
if (position)
return true;
gameState.ai.HQ.AddTCGatherer(nearestSupply.id());
this.ent.gather(nearestSupply);
this.ent.setMetadata(PlayerID, "supply", nearestSupply.id());
this.ent.setMetadata(PlayerID, "target-foundation", undefined);
return true;
}
return false;
};
PETRA.Worker.prototype.startFishing = function(gameState)
{
if (!this.ent.position())
return false;
let resources = gameState.getFishableSupplies();
if (!resources.hasEntities())
{
gameState.ai.HQ.navalManager.resetFishingBoats(gameState);
this.ent.destroy();
return false;
}
let nearestSupplyDist = Math.min();
let nearestSupply;
let fisherSea = PETRA.getSeaAccess(gameState, this.ent);
let fishDropsites = (gameState.playerData.hasSharedDropsites ? gameState.getAnyDropsites("food") : gameState.getOwnDropsites("food")).
filter(API3.Filters.byClass("Dock")).toEntityArray();
let nearestDropsiteDist = function(supply) {
let distMin = 1000000;
let pos = supply.position();
for (let dropsite of fishDropsites)
{
if (!dropsite.position())
continue;
let owner = dropsite.owner();
// owner != PlayerID can only happen when hasSharedDropsites == true, so no need to test it again
if (owner != PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner)))
continue;
if (fisherSea != PETRA.getSeaAccess(gameState, dropsite))
continue;
distMin = Math.min(distMin, API3.SquareVectorDistance(pos, dropsite.position()));
}
return distMin;
};
let exhausted = true;
let gatherRates = this.ent.resourceGatherRates();
resources.forEach(function(supply)
{
if (!supply.position())
return;
// check that it is accessible
if (gameState.ai.HQ.navalManager.getFishSea(gameState, supply) != fisherSea)
return;
exhausted = false;
let supplyType = supply.get("ResourceSupply/Type");
if (!gatherRates[supplyType])
return;
if (PETRA.IsSupplyFull(gameState, supply))
return;
// check if available resource is worth one additionnal gatherer (except for farms)
let nbGatherers = supply.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supply.id());
if (nbGatherers > 0 && supply.resourceSupplyAmount()/(1+nbGatherers) < 30)
return;
// Avoid ennemy territory
if (!gameState.ai.HQ.navalManager.canFishSafely(gameState, supply))
return;
// measure the distance from the resource to the nearest dropsite
let dist = nearestDropsiteDist(supply);
if (dist > nearestSupplyDist)
return;
nearestSupplyDist = dist;
nearestSupply = supply;
});
if (exhausted)
{
gameState.ai.HQ.navalManager.resetFishingBoats(gameState, fisherSea);
this.ent.destroy();
return false;
}
if (nearestSupply)
{
gameState.ai.HQ.AddTCGatherer(nearestSupply.id());
this.ent.gather(nearestSupply);
this.ent.setMetadata(PlayerID, "supply", nearestSupply.id());
this.ent.setMetadata(PlayerID, "target-foundation", undefined);
return true;
}
if (this.ent.getMetadata(PlayerID, "subrole") == "fisher")
this.ent.setMetadata(PlayerID, "subrole", "idle");
return false;
};
PETRA.Worker.prototype.gatherNearestField = function(gameState, baseID)
{
let ownFields = gameState.getOwnEntitiesByClass("Field", true).filter(API3.Filters.isBuilt()).filter(API3.Filters.byMetadata(PlayerID, "base", baseID));
let bestFarm;
let gatherRates = this.ent.resourceGatherRates();
for (let field of ownFields.values())
{
if (PETRA.IsSupplyFull(gameState, field))
continue;
let supplyType = field.get("ResourceSupply/Type");
if (!gatherRates[supplyType])
continue;
let rate = 1;
let diminishing = field.getDiminishingReturns();
if (diminishing < 1)
{
let num = field.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(field.id());
if (num > 0)
rate = Math.pow(diminishing, num);
}
// Add a penalty distance depending on rate
let dist = API3.SquareVectorDistance(field.position(), this.ent.position()) + (1 - rate) * 160000;
if (!bestFarm || dist < bestFarm.dist)
bestFarm = { "ent": field, "dist": dist, "rate": rate };
}
// If other field foundations available, better build them when rate becomes too small
if (!bestFarm || bestFarm.rate < 0.70 &&
gameState.getOwnFoundations().filter(API3.Filters.byClass("Field")).filter(API3.Filters.byMetadata(PlayerID, "base", baseID)).hasEntities())
return false;
gameState.ai.HQ.AddTCGatherer(bestFarm.ent.id());
this.ent.setMetadata(PlayerID, "supply", bestFarm.ent.id());
return bestFarm.ent;
};
/**
* WARNING with the present options of AI orders, the unit will not gather after building the farm.
* This is done by calling the gatherNearestField function when construction is completed.
*/
PETRA.Worker.prototype.buildAnyField = function(gameState, baseID)
{
if (!this.ent.isBuilder())
return false;
let bestFarmEnt = false;
let bestFarmDist = 10000000;
let pos = this.ent.position();
for (let found of gameState.getOwnFoundations().values())
{
if (found.getMetadata(PlayerID, "base") != baseID || !found.hasClass("Field"))
continue;
let current = found.getBuildersNb();
if (current === undefined ||
current >= gameState.getBuiltTemplate(found.templateName()).maxGatherers())
continue;
let dist = API3.SquareVectorDistance(found.position(), pos);
if (dist > bestFarmDist)
continue;
bestFarmEnt = found;
bestFarmDist = dist;
}
return bestFarmEnt;
};
/**
* Workers elephant should move away from the buildings they've built to avoid being trapped in between constructions.
* For the time being, we move towards the nearest gatherer (providing him a dropsite).
* BaseManager does also use that function to deal with its mobile dropsites.
*/
PETRA.Worker.prototype.moveToGatherer = function(gameState, ent, forced)
{
let pos = ent.position();
if (!pos || ent.getMetadata(PlayerID, "target-foundation") !== undefined)
return;
if (!forced && gameState.ai.elapsedTime < (ent.getMetadata(PlayerID, "nextMoveToGatherer") || 5))
return;
let gatherers = this.base.workersBySubrole(gameState, "gatherer");
let dist = Math.min();
let destination;
let access = PETRA.getLandAccess(gameState, ent);
let types = ent.resourceDropsiteTypes();
for (let gatherer of gatherers.values())
{
let gathererType = gatherer.getMetadata(PlayerID, "gather-type");
if (!gathererType || types.indexOf(gathererType) == -1)
continue;
if (!gatherer.position() || gatherer.getMetadata(PlayerID, "transport") !== undefined ||
PETRA.getLandAccess(gameState, gatherer) != access || gatherer.isIdle())
continue;
let distance = API3.SquareVectorDistance(pos, gatherer.position());
if (distance > dist)
continue;
dist = distance;
destination = gatherer.position();
}
ent.setMetadata(PlayerID, "nextMoveToGatherer", gameState.ai.elapsedTime + (destination ? 12 : 5));
if (destination && dist > 10)
ent.move(destination[0], destination[1]);
};
/**
* Check accessibility of the target when in approach (in RMS maps, we quite often have chicken or bushes
* inside obstruction of other entities). The resource will be flagged as inaccessible during 10 mn (in case
* it will be cleared later).
*/
PETRA.Worker.prototype.isInaccessibleSupply = function(gameState)
{
if (!this.ent.unitAIOrderData()[0] || !this.ent.unitAIOrderData()[0].target)
return false;
let targetId = this.ent.unitAIOrderData()[0].target;
let target = gameState.getEntityById(targetId);
if (!target)
return true;
if (!target.resourceSupplyType())
return false;
let approachingTarget = this.ent.getMetadata(PlayerID, "approachingTarget");
let carriedAmount = this.ent.resourceCarrying().length ? this.ent.resourceCarrying()[0].amount : 0;
if (!approachingTarget || approachingTarget != targetId)
{
this.ent.setMetadata(PlayerID, "approachingTarget", targetId);
this.ent.setMetadata(PlayerID, "approachingTime", undefined);
this.ent.setMetadata(PlayerID, "approachingPos", undefined);
this.ent.setMetadata(PlayerID, "carriedBefore", carriedAmount);
let alreadyTried = this.ent.getMetadata(PlayerID, "alreadyTried");
if (alreadyTried && alreadyTried != targetId)
this.ent.setMetadata(PlayerID, "alreadyTried", undefined);
}
let carriedBefore = this.ent.getMetadata(PlayerID, "carriedBefore");
if (carriedBefore != carriedAmount)
{
this.ent.setMetadata(PlayerID, "approachingTarget", undefined);
this.ent.setMetadata(PlayerID, "alreadyTried", undefined);
if (target.getMetadata(PlayerID, "inaccessibleTime"))
target.setMetadata(PlayerID, "inaccessibleTime", 0);
return false;
}
let inaccessibleTime = target.getMetadata(PlayerID, "inaccessibleTime");
if (inaccessibleTime && gameState.ai.elapsedTime < inaccessibleTime)
return true;
let approachingTime = this.ent.getMetadata(PlayerID, "approachingTime");
if (!approachingTime || gameState.ai.elapsedTime - approachingTime > 3)
{
let presentPos = this.ent.position();
let approachingPos = this.ent.getMetadata(PlayerID, "approachingPos");
if (!approachingPos || approachingPos[0] != presentPos[0] || approachingPos[1] != presentPos[1])
{
this.ent.setMetadata(PlayerID, "approachingTime", gameState.ai.elapsedTime);
this.ent.setMetadata(PlayerID, "approachingPos", presentPos);
return false;
}
if (gameState.ai.elapsedTime - approachingTime > 10)
{
if (this.ent.getMetadata(PlayerID, "alreadyTried"))
{
target.setMetadata(PlayerID, "inaccessibleTime", gameState.ai.elapsedTime + 600);
return true;
}
// let's try again to reach it
this.ent.setMetadata(PlayerID, "alreadyTried", targetId);
this.ent.setMetadata(PlayerID, "approachingTarget", undefined);
this.ent.gather(target);
return false;
}
}
return false;
};
Index: ps/trunk/binaries/data/mods/public/simulation/components/Identity.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/Identity.js (revision 23915)
+++ ps/trunk/binaries/data/mods/public/simulation/components/Identity.js (revision 23916)
@@ -1,187 +1,187 @@
function Identity() {}
Identity.prototype.Schema =
"Specifies various names and values associated with the entity, typically for GUI display to users." +
"" +
"athen" +
"Athenian Hoplite" +
"Hoplī́tēs Athēnaïkós" +
"units/athen_infantry_spearman.png" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"Basic" +
"Advanced" +
"Elite" +
"" +
"" +
"" +
"" +
- "" +
+ "" +
"" +
"tokens" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"tokens" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"" +
"";
Identity.prototype.Init = function()
{
this.classesList = GetIdentityClasses(this.template);
this.visibleClassesList = GetVisibleIdentityClasses(this.template);
if (this.template.Phenotype)
this.phenotype = pickRandom(this.GetPossiblePhenotypes());
else
this.phenotype = "default";
};
Identity.prototype.HasSomeFormation = function()
{
return this.GetFormationsList().length > 0;
};
Identity.prototype.GetCiv = function()
{
return this.template.Civ;
};
Identity.prototype.GetLang = function()
{
return this.template.Lang || "greek"; // ugly default
};
/**
* Get a list of possible Phenotypes.
* @return {string[]} A list of possible phenotypes.
*/
Identity.prototype.GetPossiblePhenotypes = function()
{
return this.template.Phenotype._string.split(/\s+/);
};
/**
* Get the current Phenotype.
* @return {string} The current phenotype.
*/
Identity.prototype.GetPhenotype = function()
{
return this.phenotype;
};
Identity.prototype.GetRank = function()
{
return this.template.Rank || "";
};
Identity.prototype.GetClassesList = function()
{
return this.classesList;
};
Identity.prototype.GetVisibleClassesList = function()
{
return this.visibleClassesList;
};
Identity.prototype.HasClass = function(name)
{
return this.GetClassesList().indexOf(name) != -1;
};
Identity.prototype.GetFormationsList = function()
{
if (this.template.Formations && this.template.Formations._string)
return this.template.Formations._string.split(/\s+/);
return [];
};
Identity.prototype.CanUseFormation = function(template)
{
return this.GetFormationsList().indexOf(template) != -1;
};
Identity.prototype.GetSelectionGroupName = function()
{
return this.template.SelectionGroupName || "";
};
Identity.prototype.GetGenericName = function()
{
return this.template.GenericName;
};
Identity.prototype.IsUndeletable = function()
{
return this.template.Undeletable == "true";
};
Engine.RegisterComponentType(IID_Identity, "Identity", Identity);
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry.xml (revision 23915)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_cavalry.xml (revision 23916)
@@ -1,100 +1,100 @@
31152.541000Field Palisade Wall100.00.00.021151007.5CavalryBasic
- Human CitizenSoldier
+ Human FastMoving CitizenSoldierCitizen Soldier Cavalry
special/formations/wedge
130105001502.01.0520202020128x256/ellipse.png128x256/ellipse_mask.pngvoice/{lang}/civ/civ_{phenotype}_walk.xmlvoice/{lang}/civ/civ_{phenotype}_attack.xmlvoice/{lang}/civ/civ_{phenotype}_gather.xmlvoice/{lang}/civ/civ_{phenotype}_garrison.xmlactor/mounted/movement/walk.xmlactor/mounted/movement/walk.xmlattack/impact/arrow_impact.xmlattack/weapon/sword_attack.xmlattack/weapon/bow_attack.xmlactor/fauna/death/death_horse.xmlinterface/alarm/alarm_create_cav.xml6.5292
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry.xml (revision 23915)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_champion_cavalry.xml (revision 23916)
@@ -1,60 +1,61 @@
7520130150801005.0240
+ FastMovingCavalryChampion Cavalry
special/formations/wedge
20015810voice/{lang}/civ/civ_{phenotype}_walk.xmlvoice/{lang}/civ/civ_{phenotype}_attack.xmlvoice/{lang}/civ/civ_{phenotype}_gather.xmlvoice/{lang}/civ/civ_{phenotype}_garrison.xmlactor/mounted/movement/walk.xmlactor/mounted/movement/walk.xmlattack/weapon/sword_attack.xmlactor/fauna/death/death_horse.xmlinterface/alarm/alarm_create_cav.xml6.5296
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_dog.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_dog.xml (revision 23915)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_dog.xml (revision 23916)
@@ -1,81 +1,81 @@
12172035001000Structure Ship Siege1501003.0110War DogCannot attack Structures, Ships, or Siege Engines.
- Human
+ Human FastMovingDog Melee10010128x256/ellipse.png128x256/ellipse_mask.pngvoice/global/civ_dog_move.xmlvoice/global/civ_dog_move.xmlvoice/global/civ_dog_move.xmlvoice/global/civ_dog_move.xmlactor/mounted/movement/walk.xmlactor/mounted/movement/walk.xmlattack/weapon/sword.xmlactor/fauna/death/death_animal_gen.xmlinterface/complete/building/complete_kennel.xml6.5WarDog1.5230
Index: ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry.xml (revision 23915)
+++ ps/trunk/binaries/data/mods/public/simulation/templates/template_unit_hero_cavalry.xml (revision 23916)
@@ -1,44 +1,45 @@
10825503002002505.01500
+ FastMovingCavalryHero Cavalry
special/formations/wedge
interface/alarm/alarm_create_cav.xmlactor/mounted/movement/walk.xmlactor/mounted/movement/walk.xmlactor/fauna/death/death_horse.xml6.52