0ad/binaries/data/mods/public/simulation/ai/petra/attackPlan.js

2306 lines
76 KiB
JavaScript
Raw Normal View History

import { EntityCollection } from "simulation/ai/common-api/entitycollection.js";
import * as filters from "simulation/ai/common-api/filters.js";
import { SquareVectorDistance, VectorDistance, aiWarn } from "simulation/ai/common-api/utils.js";
import { Config } from "simulation/ai/petra/config.js";
import * as difficulty from "simulation/ai/petra/difficultyLevel.js";
import { allowCapture, dumpEntity, getHolder, getLandAccess, isSiegeUnit, returnResources } from
"simulation/ai/petra/entityExtend.js";
import { TrainingPlan } from "simulation/ai/petra/queueplanTraining.js";
import { Worker } from "simulation/ai/petra/worker.js";
/**
* 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.
* When @c deserialized is true, do not call any random function inside constructor
* as that would cause oos.
*/
export function AttackPlan(gameState, config, uniqueID, type = AttackPlan.TYPE_DEFAULT, data, deserialized)
{
this.Config = config;
this.name = uniqueID;
this.type = type;
this.state = AttackPlan.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;
const allAccesses = {};
for (const base of gameState.ai.HQ.baseManagers())
{
if (!base.anchor || !base.anchor.position())
continue;
const access = 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 (const ent of gameState.getOwnEntities().values())
{
if (!ent.position())
continue;
const access = 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 (const structure of gameState.getEnemyStructures().values())
{
if (this.target && structure.id() != this.target.id())
continue;
if (!structure.position())
continue;
const access = getLandAccess(gameState, structure);
if (access in allAccesses)
{
this.overseas = 0;
this.rallyPoint = allAccesses[access];
break;
}
else if (!this.overseas)
{
const sea = gameState.ai.HQ.getSeaBetweenIndices(gameState, rallyAccess, access);
if (!sea)
{
if (this.target)
{
aiWarn("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;
// 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 === AttackPlan.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.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 === AttackPlan.TYPE_RAID)
{
priority = 150;
this.unitStat.FastMoving = { "priority": 1, "minSize": 3, "targetSize": 4, "batchSize": 2, "classes": ["FastMoving+CitizenSoldier"],
"interests": [ ["strength", 1] ] };
this.neededShips = 1;
}
else if (type === AttackPlan.TYPE_HUGE_ATTACK)
{
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.RangedFastMoving = { "priority": 0.7, "minSize": 4, "targetSize": 20, "batchSize": 4, "classes": ["FastMoving+Ranged+CitizenSoldier"],
"interests": [["strength", 2]] };
this.unitStat.MeleeFastMoving = { "priority": 0.7, "minSize": 4, "targetSize": 20, "batchSize": 4, "classes": ["FastMoving+Melee+CitizenSoldier"],
"interests": [["strength", 2]] };
this.unitStat.ChampRangedFastMoving = { "priority": 1, "minSize": 3, "targetSize": 15, "batchSize": 3, "classes": ["FastMoving+Ranged+Champion"],
"interests": [["strength", 3]] };
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.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 = deserialized ? 1 : randFloat(0.8, 1.2);
// and lower priority and smaller sizes for easier difficulty levels
if (this.Config.difficulty < difficulty.EASY)
{
priority *= 0.4;
variation *= 0.2;
}
else if (this.Config.difficulty < difficulty.MEDIUM)
{
priority *= 0.8;
variation *= 0.6;
}
if (this.Config.difficulty < difficulty.EASY)
{
for (const cat in this.unitStat)
{
this.unitStat[cat].targetSize = Math.ceil(variation * this.unitStat[cat].targetSize);
this.unitStat[cat].minSize = Math.min(this.unitStat[cat].targetSize, Math.min(this.unitStat[cat].minSize, 2));
this.unitStat[cat].batchSize = this.unitStat[cat].minSize;
}
}
else
{
for (const cat in this.unitStat)
{
this.unitStat[cat].targetSize = Math.ceil(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 (const cat in this.unitStat)
{
this.unitStat[cat].targetSize = Math.ceil(this.Config.popScaling * this.unitStat[cat].targetSize);
this.unitStat[cat].minSize = Math.ceil(this.Config.popScaling * this.unitStat[cat].minSize);
}
if (!deserialized)
{
// 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 = AttackPlan.SIEGE_NOT_TESTED;
// 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;
}
AttackPlan.PREPARATION_FAILED = 0;
AttackPlan.PREPARATION_KEEP_GOING = 1;
AttackPlan.PREPARATION_START = 2;
AttackPlan.SIEGE_NOT_TESTED = 0;
AttackPlan.SIEGE_NO_TRAINER = 1;
/**
* Siege added in build orders
*/
AttackPlan.SIEGE_ADDED = 2;
AttackPlan.STATE_UNEXECUTED = "unexecuted";
AttackPlan.STATE_COMPLETING = "completing";
AttackPlan.STATE_ARRIVED = "arrived";
AttackPlan.TYPE_DEFAULT = "Attack";
AttackPlan.TYPE_HUGE_ATTACK = "HugeAttack";
AttackPlan.TYPE_RAID = "Raid";
AttackPlan.TYPE_RUSH = "Rush";
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(
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 (const cat in this.unitStat)
{
const Unit = this.unitStat[cat];
this.unit[cat] = this.unitCollection.filter(filters.byClasses(Unit.classes));
this.unit[cat].registerUpdates();
if (this.canBuildUnits)
this.buildOrders.push([0, Unit.classes, this.unit[cat], Unit, cat]);
}
};
AttackPlan.prototype.getName = function()
{
return this.name;
};
AttackPlan.prototype.getType = function()
{
return this.type;
};
AttackPlan.prototype.isStarted = function()
{
return this.state !== AttackPlan.STATE_UNEXECUTED && this.state !== AttackPlan.STATE_COMPLETING;
};
AttackPlan.prototype.isPaused = function()
{
return this.paused;
};
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.
*/
AttackPlan.prototype.canStart = function()
{
if (!this.canBuildUnits)
return true;
for (const unitCat in this.unitStat)
if (this.unit[unitCat].length < this.unitStat[unitCat].minSize)
return false;
return true;
};
AttackPlan.prototype.mustStart = function()
{
if (this.isPaused())
return false;
if (!this.canBuildUnits)
return this.unitCollection.hasEntities();
let MaxReachedEverywhere = true;
let MinReachedEverywhere = true;
for (const unitCat in this.unitStat)
{
const 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 === AttackPlan.TYPE_RAID && this.target && this.target.foundationProgress() &&
this.target.foundationProgress() > 50;
return false;
};
AttackPlan.prototype.forceStart = function()
{
for (const unitCat in this.unitStat)
{
const Unit = this.unitStat[unitCat];
Unit.targetSize = 0;
Unit.minSize = 0;
}
this.forced = true;
};
AttackPlan.prototype.emptyQueues = function()
{
this.queue.empty();
this.queueChamp.empty();
this.queueSiege.empty();
};
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. */
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;
const Unit = this.unitStat[name];
this.unit[name] = this.unitCollection.filter(filters.byClasses(Unit.classes));
this.unit[name].registerUpdates();
this.buildOrders.push([0, Unit.classes, this.unit[name], Unit, name]);
if (resetQueue)
this.emptyQueues();
}
};
AttackPlan.prototype.addSiegeUnits = function(gameState)
{
if (this.siegeState === AttackPlan.SIEGE_ADDED || this.state !== AttackPlan.STATE_UNEXECUTED)
return false;
const civ = gameState.getPlayerCiv();
const classes = [["Siege+Melee"], ["Siege+Ranged"], ["Elephant+Melee"]];
const hasTrainer = [false, false, false];
for (const ent of gameState.getOwnTrainingFacilities().values())
{
const trainables = ent.trainableEntities(civ);
if (!trainables)
continue;
for (const trainable of trainables)
{
if (gameState.isTemplateDisabled(trainable))
continue;
const template = gameState.getTemplate(trainable);
if (!template || !template.available(gameState))
continue;
for (let i = 0; i < classes.length; ++i)
if (template.hasClasses(classes[i]))
hasTrainer[i] = true;
}
}
if (hasTrainer.every(e => !e))
{
this.siegeState = AttackPlan.SIEGE_NO_TRAINER;
return false;
}
let i = this.name % classes.length;
for (let k = 0; k < classes.length; ++k)
{
if (hasTrainer[i])
break;
i = (i + 1) % classes.length;
}
this.siegeState = AttackPlan.SIEGE_ADDED;
let targetSize;
if (this.Config.difficulty < difficulty.MEDIUM)
targetSize = this.type === AttackPlan.TYPE_HUGE_ATTACK ? Math.max(this.Config.difficulty, 1) : Math.max(this.Config.difficulty - 1, 0);
else
targetSize = this.type === AttackPlan.TYPE_HUGE_ATTACK ? this.Config.difficulty + 1 : this.Config.difficulty - 1;
targetSize = Math.max(Math.round(this.Config.popScaling * targetSize), this.type === AttackPlan.TYPE_HUGE_ATTACK ? 1 : 0);
if (!targetSize)
return true;
// no minsize as we don't want the plan to fail at the last minute though.
const 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". */
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 === AttackPlan.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 = AttackPlan.STATE_UNEXECUTED;
this.target = undefined;
}
else
{
// check that all units have finished with their transport if needed
if (this.waitingForTransport())
return AttackPlan.PREPARATION_KEEP_GOING;
// bloqued units which cannot finish their order should not stop the attack
if (gameState.ai.elapsedTime < this.maxCompletingTime && this.hasForceOrder())
return AttackPlan.PREPARATION_KEEP_GOING;
return AttackPlan.PREPARATION_START;
}
}
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 AttackPlan.PREPARATION_KEEP_GOING;
if (this.type !== AttackPlan.TYPE_RAID || !this.forced) // Forced Raids have special purposes (as relic capture)
this.assignUnits(gameState);
if (this.type !== AttackPlan.TYPE_RAID && gameState.ai.HQ.attackManager.getAttackInPreparation(AttackPlan.TYPE_RAID) !== undefined)
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)
{
const am = gameState.ai.HQ.attackManager;
aiWarn(" attacks upcoming: raid " +
am.upcomingAttacks[AttackPlan.TYPE_RAID].length +
" rush " + am.upcomingAttacks[AttackPlan.TYPE_RUSH].length +
" attack " + am.upcomingAttacks[AttackPlan.TYPE_DEFAULT].length +
" huge " + am.upcomingAttacks[AttackPlan.TYPE_HUGE_ATTACK].length);
aiWarn(" attacks started: raid " + am.startedAttacks[AttackPlan.TYPE_RAID].length +
" rush " + am.startedAttacks[AttackPlan.TYPE_RUSH].length +
" attack " + am.startedAttacks[AttackPlan.TYPE_DEFAULT].length +
" huge " + am.startedAttacks[AttackPlan.TYPE_HUGE_ATTACK].length);
}
return AttackPlan.PREPARATION_FAILED;
}
}
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 AttackPlan.PREPARATION_KEEP_GOING;
}
}
else
{
if (this.canBuildUnits)
{
// We still have time left to recruit units and do stuffs.
if (this.siegeState === AttackPlan.SIEGE_NOT_TESTED ||
this.siegeState === AttackPlan.SIEGE_NO_TRAINER && 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 AttackPlan.PREPARATION_FAILED; // will abort the plan
}
return AttackPlan.PREPARATION_KEEP_GOING;
}
// if we're here, it means we must start
this.state = AttackPlan.STATE_COMPLETING;
// Raids have their predefined target
if (!this.target && !this.chooseTarget(gameState))
return AttackPlan.PREPARATION_FAILED;
if (!this.overseas)
this.getPathToTarget(gameState);
if (this.type === AttackPlan.TYPE_RAID)
this.maxCompletingTime = this.forced ? 0 : gameState.ai.elapsedTime + 20;
else
{
if (this.type === AttackPlan.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 (const unitIds of gameState.ai.HQ.attackManager.bombingAttacks.values())
{
for (const entId of unitIds.values())
{
const ent = gameState.getEntityById(entId);
if (!ent || ent.getMetadata(PlayerID, "plan") != this.name)
continue;
unitIds.delete(entId);
ent.stopMoving();
}
}
const rallyPoint = this.rallyPoint;
const rallyIndex = gameState.ai.accessibility.getAccessValue(rallyPoint);
for (const 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", Worker.ROLE_ATTACK);
ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_COMPLETING);
let queued = false;
if (ent.resourceCarrying() && ent.resourceCarrying().length)
queued = returnResources(gameState, ent);
const index = 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 AttackPlan.PREPARATION_KEEP_GOING;
};
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 (const order of this.buildOrders)
{
const 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;
const calcResult = va - vb;
if (calcResult !== 0)
return calcResult;
if (a[4] < b[4])
return 1;
if (a[4] > b[4])
return -1;
return 0;
});
if (this.Config.debug > 1 && gameState.ai.playedTurn%50 === 0)
{
aiWarn("====================================");
aiWarn("======== build order for plan " + this.name);
for (const order of this.buildOrders)
{
const specialData = "Plan_"+this.name+"_"+order[4];
const inTraining = gameState.countOwnQueuedEntitiesWithMetadata("special", specialData);
const queue1 = this.queue.countQueuedUnitsWithMetadata("special", specialData);
const queue2 = this.queueChamp.countQueuedUnitsWithMetadata("special", specialData);
const queue3 = this.queueSiege.countQueuedUnitsWithMetadata("special", specialData);
aiWarn(" >>> " + order[4] + " done " + order[2].length + " training " + inTraining +
" queue " + queue1 + " champ " + queue2 + " siege " + queue3 + " >> need " + order[3].targetSize);
}
aiWarn("====================================");
}
const firstOrder = this.buildOrders[0];
if (firstOrder[0] < firstOrder[3].targetSize)
{
// find the actual queue we want
let queue = this.queue;
if (firstOrder[4] == "Siege")
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)
{
const 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)
aiWarn("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)
aiWarn("attack template " + template + " added for plan " + this.name);
const max = firstOrder[3].batchSize;
const specialData = "Plan_" + this.name + "_" + firstOrder[4];
const data = { "plan": this.name, "special": specialData, "base": 0 };
data.role = gameState.getTemplate(template).hasClass("CitizenSoldier") ?
Worker.ROLE_WORKER : Worker.ROLE_ATTACK;
const trainingPlan = new TrainingPlan(gameState, template, data, max, max);
if (trainingPlan.template)
queue.addPlan(trainingPlan);
else if (this.Config.debug > 1)
{
aiWarn("training plan canceled because no template for " + template +
" build1 " + uneval(firstOrder[1]) +
" build3 " + uneval(firstOrder[3].interests));
}
}
}
}
};
AttackPlan.prototype.assignUnits = function(gameState)
{
const 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 (!this.canBuildUnits)
{
for (const 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 === AttackPlan.TYPE_RAID)
{
// Raids are quick attacks: assign all FastMoving soldiers except some for hunting.
let num = 0;
for (const ent of gameState.getOwnUnits().values())
{
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.
for (const ent of gameState.getOwnEntitiesByRole(undefined, true).values())
{
if (ent.hasClasses(["!Unit", "Ship", "Support"]) ||
!this.isAvailableUnit(gameState, ent) ||
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.
for (const 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 for the higher difficulties,
// 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
if (this.Config.difficulty <= difficulty.EASY)
return added;
let num = 0;
const numbase = {};
let keep = this.type !== AttackPlan.TYPE_RUSH ?
6 + 4 * gameState.getNumPlayerEnemies() + 8 * this.Config.personality.defensive : 8;
keep = Math.round(this.Config.popScaling * keep);
for (const ent of gameState.getOwnEntitiesByRole(Worker.ROLE_WORKER, true).values())
{
if (!ent.hasClass("CitizenSoldier") || !this.isAvailableUnit(gameState, ent))
continue;
const baseID = ent.getMetadata(PlayerID, "base");
if (baseID)
numbase[baseID] = numbase[baseID] ? ++numbase[baseID] : 1;
else
{
aiWarn("Petra problem ent without base ");
dumpEntity(ent);
continue;
}
if (num++ < keep || numbase[baseID] < 5)
continue;
if (this.type !== AttackPlan.TYPE_RUSH && ent.getMetadata(PlayerID, "subrole") !== Worker.SUBROLE_IDLE)
continue;
ent.setMetadata(PlayerID, "plan", plan);
this.unitCollection.updateEnt(ent);
added = true;
}
return added;
};
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) FastMoving unit to fasten raid preparation. */
AttackPlan.prototype.reassignFastUnit = function(gameState)
{
for (const ent of this.unitCollection.values())
{
if (!ent.position() || ent.getMetadata(PlayerID, "transport") !== undefined)
continue;
if (!ent.hasClasses(["FastMoving", "CitizenSoldier"]))
continue;
const raid = gameState.ai.HQ.attackManager.getAttackInPreparation(AttackPlan.TYPE_RAID);
ent.setMetadata(PlayerID, "plan", raid.name);
this.unitCollection.updateEnt(ent);
raid.unitCollection.updateEnt(ent);
return;
}
};
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)
const targetIndex = 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 (const base of gameState.ai.HQ.baseManagers())
{
const anchor = base.anchor;
if (!anchor || !anchor.position())
continue;
let dist = 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;
const sea = gameState.ai.HQ.getSeaBetweenIndices(gameState, rallyIndex, targetIndex);
if (sea)
{
this.overseas = sea;
gameState.ai.HQ.navalManager.setMinimalTransportShips(gameState, this.overseas, this.neededShips);
}
else
{
aiWarn("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
*/
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 EntityCollection(gameState.sharedScript);
const ent = gameState.getEntityById(this.uniqueTargetId);
if (ent)
targets.addEnt(ent);
}
else
{
if (this.type === AttackPlan.TYPE_RAID)
targets = this.raidTargetFinder(gameState);
else if (this.type === AttackPlan.TYPE_RUSH || this.type === AttackPlan.TYPE_DEFAULT)
{
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 (const 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 = SquareVectorDistance(ent.position(), position);
// In normal attacks, disfavor fields
if (this.type !== AttackPlan.TYPE_RUSH && this.type !== AttackPlan.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
*/
AttackPlan.prototype.defaultTargetFinder = function(gameState, playerEnemy)
{
let targets = new EntityCollection(gameState.sharedScript);
if (gameState.getVictoryConditions().has("wonder"))
{
for (const ent of
gameState.getEnemyStructures(playerEnemy).filter(filters.byClass("Wonder")).values())
{
targets.addEnt(ent);
}
}
if (gameState.getVictoryConditions().has("regicide"))
{
for (const ent of gameState.getEnemyUnits(playerEnemy).filter(filters.byClass("Hero"))
.values())
{
targets.addEnt(ent);
}
}
if (gameState.getVictoryConditions().has("capture_the_relic"))
{
for (const ent of gameState.updatingGlobalCollection("allRelics",
filters.byClass("Relic")).filter(relic => relic.owner() == playerEnemy).values())
{
targets.addEnt(ent);
}
}
targets = targets.filter(this.isValidTarget, this);
if (targets.hasEntities())
return targets;
const validTargets = gameState.getEnemyStructures(playerEnemy).filter(this.isValidTarget, this);
targets = validTargets.filter(filters.byClass("CivCentre"));
if (!targets.hasEntities())
targets = validTargets.filter(filters.byClass("ConquestCritical"));
// If there's nothing, attack anything else that's less critical
if (!targets.hasEntities())
targets = validTargets.filter(filters.byClass("Town"));
if (!targets.hasEntities())
targets = validTargets.filter(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(filters.byClass("ConquestCritical"))
.filter(filters.not(filters.byClass("Ship")));
}
return targets;
};
AttackPlan.prototype.isValidTarget = function(ent)
{
if (!ent.position())
return false;
if (this.sameLand && 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 */
AttackPlan.prototype.rushTargetFinder = function(gameState, playerEnemy)
{
let targets = new 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 (const building of buildings)
{
if (building.owner() == 0)
continue;
if (building.hasDefensiveFire())
continue;
if (!this.isValidTarget(building))
continue;
const pos = building.position();
let defended = false;
for (const defense of buildings)
{
if (!defense.hasDefensiveFire())
continue;
const dist = SquareVectorDistance(pos, defense.position());
if (dist < 6400) // TODO check on defense range rather than this fixed 80*80
{
defended = true;
break;
}
}
if (defended)
continue;
const dist = SquareVectorDistance(pos, this.position);
if (dist > minDist)
continue;
minDist = dist;
target = building;
}
if (target)
targets.addEnt(target);
if (!targets.hasEntities() && this.type === AttackPlan.TYPE_RUSH && playerEnemy)
targets = this.rushTargetFinder(gameState);
return targets;
};
/** Raid target finder aims at destructing foundations from which our defenseManager has attacked the builders */
AttackPlan.prototype.raidTargetFinder = function(gameState)
{
const targets = new EntityCollection(gameState.sharedScript);
for (const targetId of gameState.ai.HQ.defenseManager.targetList)
{
const 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
*/
AttackPlan.prototype.checkTargetObstruction = function(gameState, target, position)
{
if (getLandAccess(gameState, target) != gameState.ai.accessibility.getAccessValue(position))
return target;
const targetPos = target.position();
const startPos = { "x": position[0], "y": position[1] };
const endPos = { "x": targetPos[0], "y": targetPos[1] };
let blocker;
const path = Engine.ComputePath(startPos, endPos, gameState.getPassabilityClassMask("default"));
if (!path.length)
return undefined;
const pathPos = [path[0].x, path[0].y];
const dist = VectorDistance(pathPos, targetPos);
const radius = target.obstructionRadius().max;
for (const 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
Clean up market classes: template_structure_economic_market.xml → Market template_structure_military_dock.xml → Dock <Market/TradeType> node → Trade requirement for bartering: BarterMarket → Barter Change Petra accordingly Also changing: Renamed the Gates class to Gate, LongWall to WallLong (cf. WallTower), and StoneWall to Wall, to match their template file names. Adjusted AI code, technologies, and templates accordingly. Entity build restrictions: renamed Apadana to Palace; replaced UniqueStructure with IshtarGate and TempleOfVesta. added Council to the (unbuildable) spart_gerousia.xml, since it's functionally identical to athen_prytaneion.xml. Introduced the following classes: Council, IshtarGate, Library, TempleOfVesta, and Theater, to match their build restrictions. WallMedium and WallShort, to complement the already existing WallLong and WallTower classes. Stoa, TempleOfMars, and TriumphalArch, for completeness. Made the ArmyCamp, Colony, Gate, Lighthouse, Naval, Palace, Palisade, SiegeWall, and StoneWall classes visible. Deleted the now unused Apadana, Kennel, SpecialBuilding classes. Removed the Village class from outposts and palisades, because those are not supposed to count towards the phase technology requirements. Slightly rephrased the phase technology requirements tooltips. Corrected the <GenericName> of the various palisade files. Updated Structure tooltips from D2578 that have not already been committed elsewhere. Patch By: Nescio Differenital Revision: D2892 This was SVN commit r23865.
2020-07-21 12:45:30 -07:00
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.
const structPos = struct.position();
const x = pathPos[0] - structPos[0] + 1.8 * (targetPos[0] - pathPos[0]) / dist;
const y = pathPos[1] - structPos[1] + 1.8 * (targetPos[1] - pathPos[1]) / dist;
if (struct.get("Obstruction/Static"))
{
if (!struct.angle())
continue;
const angle = struct.angle();
const width = +struct.get("Obstruction/Static/@width");
const depth = +struct.get("Obstruction/Static/@depth");
const cosa = Math.cos(angle);
const sina = Math.sin(angle);
const u = x * cosa - y * sina;
const 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;
const angle = struct.angle();
let width = +struct.get("Obstruction/Obstructions/Door/@width");
let depth = +struct.get("Obstruction/Obstructions/Door/@depth");
const 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"));
const cosa = Math.cos(angle);
const sina = Math.sin(angle);
const u = x * cosa - y * sina;
const 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)
{
const u1 = (path[i-1].x - structPos[0]) * cosa - (path[i-1].y - structPos[1]) * sina;
const v1 = (path[i-1].x - structPos[0]) * sina + (path[i-1].y - structPos[1]) * cosa;
const u2 = (path[i].x - structPos[0]) * cosa - (path[i].y - structPos[1]) * sina;
const v2 = (path[i].x - structPos[0]) * sina + (path[i].y - structPos[1]) * cosa;
if (v1 * v2 < 0)
{
const 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"))
{
const r = +struct.get("Obstruction/Unit/@radius");
if (x*x + y*y < r*r)
{
blocker = struct;
break;
}
}
}
if (blocker)
{
this.isBlocked = true;
return blocker;
}
return target;
};
AttackPlan.prototype.getPathToTarget = function(gameState, fixedRallyPoint = false)
{
const startAccess = gameState.ai.accessibility.getAccessValue(this.rallyPoint);
const endAccess = getLandAccess(gameState, this.target);
if (startAccess != endAccess)
return false;
Engine.ProfileStart("AI Compute path");
const startPos = { "x": this.rallyPoint[0], "y": this.rallyPoint[1] };
const endPos = { "x": this.targetPos[0], "y": this.targetPos[1] };
const path = Engine.ComputePath(startPos, endPos, gameState.getPassabilityClassMask("large"));
this.path = [];
this.path.push(clone(this.targetPos));
for (const p in path)
this.path.push([path[p].x, path[p].y]);
2026-02-20 09:59:21 -08:00
this.path.push(clone(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 */
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.
*/
AttackPlan.prototype.StartAttack = function(gameState)
{
if (this.Config.debug > 1)
aiWarn("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 (const ent of this.unitCollection.values())
{
ent.setMetadata(PlayerID, "subrole", Worker.SUBROLE_WALKING);
const stance = ent.isPackable() ? "standground" : "aggressive";
if (ent.getStance() != stance)
ent.setStance(stance);
}
const rallyAccess = gameState.ai.accessibility.getAccessValue(this.rallyPoint);
const targetAccess = 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 (const 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 */
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.position)
{
Engine.ProfileStop();
return this.unitCollection.length;
}
if (this.state == "walking" && !this.UpdateWalking(gameState, events))
{
Engine.ProfileStop();
return 0;
}
if (this.state === AttackPlan.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", Worker.SUBROLE_ATTACKING);
});
if (this.type === AttackPlan.TYPE_RUSH) // try to find a better target for rush
{
const 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;
}
const time = gameState.ai.elapsedTime;
const attackedByStructure = {};
for (const evt of events.Attacked)
{
if (!this.unitCollection.hasEntId(evt.target))
continue;
const attacker = gameState.getEntityById(evt.attacker);
const ourUnit = gameState.getEntityById(evt.target);
if (!ourUnit || !ourUnit.position() || !attacker || !attacker.position())
continue;
if (!attacker.hasClass("Unit"))
{
attackedByStructure[evt.target] = true;
continue;
}
if (isSiegeUnit(ourUnit))
{ // if our siege units are attacked, we'll send some units to deal with enemies.
const collec = this.unitCollection.filter(filters.not(filters.byClass("Siege")))
.filterNearest(ourUnit.position(), 5);
for (const ent of collec.values())
{
if (isSiegeUnit(ent)) // needed as mauryan elephants are not filtered out
continue;
const shouldCapture = allowCapture(gameState, ent, attacker);
if (!ent.canAttackTarget(attacker, shouldCapture))
continue;
ent.attack(attacker.id(), shouldCapture);
ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
}
// And if this attacker is a non-ranged siege unit and our unit also, attack it
if (isSiegeUnit(attacker) && attacker.hasClass("Melee") &&
ourUnit.hasClass("Melee") && ourUnit.canAttackTarget(attacker,
allowCapture(gameState, ourUnit, attacker)))
{
ourUnit.attack(attacker.id(), 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 (isSiegeUnit(attacker))
{ // if our unit is attacked by a siege unit, we'll send some melee units to help it.
const collec = this.unitCollection.filter(filters.byClass("Melee"))
.filterNearest(ourUnit.position(), 5);
for (const ent of collec.values())
{
const shouldCapture = allowCapture(gameState, ent, attacker);
if (!ent.canAttackTarget(attacker, shouldCapture))
continue;
ent.attack(attacker.id(), shouldCapture);
ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
}
}
else
{
// Look first for nearby units to help us if possible
const collec = this.unitCollection.filterNearest(ourUnit.position(), 2);
for (const ent of collec.values())
{
const shouldCapture = allowCapture(gameState, ent, attacker);
if (isSiegeUnit(ent) || !ent.canAttackTarget(attacker, shouldCapture))
continue;
const orderData = ent.unitAIOrderData();
if (orderData && orderData.length && orderData[0].target)
{
if (orderData[0].target === attacker.id())
continue;
const target = gameState.getEntityById(orderData[0].target);
if (target && !target.hasClasses(["Structure", "Support"]))
continue;
}
ent.attack(attacker.id(), shouldCapture);
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
const orderData = ourUnit.unitAIOrderData();
if (orderData && orderData.length && orderData[0].target)
{
if (orderData[0].target === attacker.id())
continue;
const target = gameState.getEntityById(orderData[0].target);
if (target && !target.hasClasses(["Structure", "Support"]))
{
if (!target.hasClass("Ranged") || !attacker.hasClass("Melee"))
continue;
}
}
const shouldCapture = allowCapture(gameState, ourUnit, attacker);
if (ourUnit.canAttackTarget(attacker, shouldCapture))
{
ourUnit.attack(attacker.id(), shouldCapture);
ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
}
}
}
}
const enemyUnits = gameState.getEnemyUnits(this.targetPlayer);
const enemyStructures = gameState.getEnemyStructures(this.targetPlayer);
// Count the number of times an enemy is targeted, to prevent all units to follow the same target
const unitTargets = {};
for (const ent of this.unitCollection.values())
{
if (ent.hasClass("Ship")) // TODO What to do with ships
continue;
const orderData = ent.unitAIOrderData();
if (!orderData || !orderData.length || !orderData[0].target)
continue;
const targetId = orderData[0].target;
const target = gameState.getEntityById(targetId);
if (!target || target.hasClass("Structure"))
continue;
if (!(targetId in unitTargets))
{
if (isSiegeUnit(target) || target.hasClass("Hero"))
unitTargets[targetId] = -8;
else if (target.hasClasses(["Champion", "Ship"]))
unitTargets[targetId] = -5;
else
unitTargets[targetId] = -3;
}
++unitTargets[targetId];
}
const veto = {};
for (const target in unitTargets)
if (unitTargets[target] > 0)
veto[target] = true;
let targetClassesUnit;
let targetClassesSiege;
if (this.type === AttackPlan.TYPE_RUSH)
Clean up market classes: template_structure_economic_market.xml → Market template_structure_military_dock.xml → Dock <Market/TradeType> node → Trade requirement for bartering: BarterMarket → Barter Change Petra accordingly Also changing: Renamed the Gates class to Gate, LongWall to WallLong (cf. WallTower), and StoneWall to Wall, to match their template file names. Adjusted AI code, technologies, and templates accordingly. Entity build restrictions: renamed Apadana to Palace; replaced UniqueStructure with IshtarGate and TempleOfVesta. added Council to the (unbuildable) spart_gerousia.xml, since it's functionally identical to athen_prytaneion.xml. Introduced the following classes: Council, IshtarGate, Library, TempleOfVesta, and Theater, to match their build restrictions. WallMedium and WallShort, to complement the already existing WallLong and WallTower classes. Stoa, TempleOfMars, and TriumphalArch, for completeness. Made the ArmyCamp, Colony, Gate, Lighthouse, Naval, Palace, Palisade, SiegeWall, and StoneWall classes visible. Deleted the now unused Apadana, Kennel, SpecialBuilding classes. Removed the Village class from outposts and palisades, because those are not supposed to count towards the phase technology requirements. Slightly rephrased the phase technology requirements tooltips. Corrected the <GenericName> of the various palisade files. Updated Structure tooltips from D2578 that have not already been committed elsewhere. Patch By: Nescio Differenital Revision: D2892 This was SVN commit r23865.
2020-07-21 12:45:30 -07:00
targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Palisade", "Wall", "Tower", "Fortress"], "vetoEntities": veto };
else
{
if (this.target.hasClass("Fortress"))
Clean up market classes: template_structure_economic_market.xml → Market template_structure_military_dock.xml → Dock <Market/TradeType> node → Trade requirement for bartering: BarterMarket → Barter Change Petra accordingly Also changing: Renamed the Gates class to Gate, LongWall to WallLong (cf. WallTower), and StoneWall to Wall, to match their template file names. Adjusted AI code, technologies, and templates accordingly. Entity build restrictions: renamed Apadana to Palace; replaced UniqueStructure with IshtarGate and TempleOfVesta. added Council to the (unbuildable) spart_gerousia.xml, since it's functionally identical to athen_prytaneion.xml. Introduced the following classes: Council, IshtarGate, Library, TempleOfVesta, and Theater, to match their build restrictions. WallMedium and WallShort, to complement the already existing WallLong and WallTower classes. Stoa, TempleOfMars, and TriumphalArch, for completeness. Made the ArmyCamp, Colony, Gate, Lighthouse, Naval, Palace, Palisade, SiegeWall, and StoneWall classes visible. Deleted the now unused Apadana, Kennel, SpecialBuilding classes. Removed the Village class from outposts and palisades, because those are not supposed to count towards the phase technology requirements. Slightly rephrased the phase technology requirements tooltips. Corrected the <GenericName> of the various palisade files. Updated Structure tooltips from D2578 that have not already been committed elsewhere. Patch By: Nescio Differenital Revision: D2892 This was SVN commit r23865.
2020-07-21 12:45:30 -07:00
targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Palisade", "Wall"], "vetoEntities": veto };
else if (this.target.hasClasses(["Palisade", "Wall"]))
targetClassesUnit = { "attack": ["Unit", "Structure"], "avoid": ["Fortress"], "vetoEntities": veto };
else
Clean up market classes: template_structure_economic_market.xml → Market template_structure_military_dock.xml → Dock <Market/TradeType> node → Trade requirement for bartering: BarterMarket → Barter Change Petra accordingly Also changing: Renamed the Gates class to Gate, LongWall to WallLong (cf. WallTower), and StoneWall to Wall, to match their template file names. Adjusted AI code, technologies, and templates accordingly. Entity build restrictions: renamed Apadana to Palace; replaced UniqueStructure with IshtarGate and TempleOfVesta. added Council to the (unbuildable) spart_gerousia.xml, since it's functionally identical to athen_prytaneion.xml. Introduced the following classes: Council, IshtarGate, Library, TempleOfVesta, and Theater, to match their build restrictions. WallMedium and WallShort, to complement the already existing WallLong and WallTower classes. Stoa, TempleOfMars, and TriumphalArch, for completeness. Made the ArmyCamp, Colony, Gate, Lighthouse, Naval, Palace, Palisade, SiegeWall, and StoneWall classes visible. Deleted the now unused Apadana, Kennel, SpecialBuilding classes. Removed the Village class from outposts and palisades, because those are not supposed to count towards the phase technology requirements. Slightly rephrased the phase technology requirements tooltips. Corrected the <GenericName> of the various palisade files. Updated Structure tooltips from D2578 that have not already been committed elsewhere. Patch By: Nescio Differenital Revision: D2892 This was SVN commit r23865.
2020-07-21 12:45:30 -07:00
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", "Forge");
targetClassesSiege.avoid = targetClassesSiege.avoid.concat("House", "Storehouse", "Farmstead", "Field", "Forge");
}
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
const lgth = this.unitCollUpdateArray.length < 15 || this.startingAttack ? this.unitCollUpdateArray.length : 10;
for (let check = 0; check < lgth; check++)
{
const 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;
const 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;
const siegeUnit = isSiegeUnit(ent);
if (ent.isIdle())
needsUpdate = true;
else if (siegeUnit && targetId)
{
const 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)
{
const 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("FastMoving") && !ent.hasClass("Ranged") &&
target.hasClass("Civilian") && target.unitAIState().split(".")[1] == "FLEEING")
maybeUpdate = true;
}
// don't update too soon if not necessary
if (!needsUpdate)
{
if (!maybeUpdate)
continue;
const deltat = ent.unitAIState() === "INDIVIDUAL.COMBAT.APPROACHING" ? 10 : 5;
const lastAttackPlanUpdateTime = ent.getMetadata(PlayerID, "lastAttackPlanUpdateTime");
if (lastAttackPlanUpdateTime && time - lastAttackPlanUpdateTime < deltat)
continue;
}
ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time);
let range = 60;
const 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("FastMoving"))
range += 30;
range *= range;
const entAccess = getLandAccess(gameState, ent);
// Checking for gates if we're a siege unit.
if (siegeUnit)
{
const mStruct = enemyStructures.filter(enemy =>
{
if (!enemy.position() || !ent.canAttackTarget(enemy, allowCapture(gameState, ent, enemy)))
return false;
if (SquareVectorDistance(enemy.position(), ent.position()) > range)
return false;
if (enemy.foundationProgress() == 0)
return false;
if (getLandAccess(gameState, enemy) != entAccess)
return false;
return true;
}).toEntityArray();
if (mStruct.length)
{
mStruct.sort((structa, structb) =>
{
let vala = structa.costSum();
Clean up market classes: template_structure_economic_market.xml → Market template_structure_military_dock.xml → Dock <Market/TradeType> node → Trade requirement for bartering: BarterMarket → Barter Change Petra accordingly Also changing: Renamed the Gates class to Gate, LongWall to WallLong (cf. WallTower), and StoneWall to Wall, to match their template file names. Adjusted AI code, technologies, and templates accordingly. Entity build restrictions: renamed Apadana to Palace; replaced UniqueStructure with IshtarGate and TempleOfVesta. added Council to the (unbuildable) spart_gerousia.xml, since it's functionally identical to athen_prytaneion.xml. Introduced the following classes: Council, IshtarGate, Library, TempleOfVesta, and Theater, to match their build restrictions. WallMedium and WallShort, to complement the already existing WallLong and WallTower classes. Stoa, TempleOfMars, and TriumphalArch, for completeness. Made the ArmyCamp, Colony, Gate, Lighthouse, Naval, Palace, Palisade, SiegeWall, and StoneWall classes visible. Deleted the now unused Apadana, Kennel, SpecialBuilding classes. Removed the Village class from outposts and palisades, because those are not supposed to count towards the phase technology requirements. Slightly rephrased the phase technology requirements tooltips. Corrected the <GenericName> of the various palisade files. Updated Structure tooltips from D2578 that have not already been committed elsewhere. Patch By: Nescio Differenital Revision: D2892 This was SVN commit r23865.
2020-07-21 12:45:30 -07:00
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();
Clean up market classes: template_structure_economic_market.xml → Market template_structure_military_dock.xml → Dock <Market/TradeType> node → Trade requirement for bartering: BarterMarket → Barter Change Petra accordingly Also changing: Renamed the Gates class to Gate, LongWall to WallLong (cf. WallTower), and StoneWall to Wall, to match their template file names. Adjusted AI code, technologies, and templates accordingly. Entity build restrictions: renamed Apadana to Palace; replaced UniqueStructure with IshtarGate and TempleOfVesta. added Council to the (unbuildable) spart_gerousia.xml, since it's functionally identical to athen_prytaneion.xml. Introduced the following classes: Council, IshtarGate, Library, TempleOfVesta, and Theater, to match their build restrictions. WallMedium and WallShort, to complement the already existing WallLong and WallTower classes. Stoa, TempleOfMars, and TriumphalArch, for completeness. Made the ArmyCamp, Colony, Gate, Lighthouse, Naval, Palace, Palisade, SiegeWall, and StoneWall classes visible. Deleted the now unused Apadana, Kennel, SpecialBuilding classes. Removed the Village class from outposts and palisades, because those are not supposed to count towards the phase technology requirements. Slightly rephrased the phase technology requirements tooltips. Corrected the <GenericName> of the various palisade files. Updated Structure tooltips from D2578 that have not already been committed elsewhere. Patch By: Nescio Differenital Revision: D2892 This was SVN commit r23865.
2020-07-21 12:45:30 -07:00
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;
});
Clean up market classes: template_structure_economic_market.xml → Market template_structure_military_dock.xml → Dock <Market/TradeType> node → Trade requirement for bartering: BarterMarket → Barter Change Petra accordingly Also changing: Renamed the Gates class to Gate, LongWall to WallLong (cf. WallTower), and StoneWall to Wall, to match their template file names. Adjusted AI code, technologies, and templates accordingly. Entity build restrictions: renamed Apadana to Palace; replaced UniqueStructure with IshtarGate and TempleOfVesta. added Council to the (unbuildable) spart_gerousia.xml, since it's functionally identical to athen_prytaneion.xml. Introduced the following classes: Council, IshtarGate, Library, TempleOfVesta, and Theater, to match their build restrictions. WallMedium and WallShort, to complement the already existing WallLong and WallTower classes. Stoa, TempleOfMars, and TriumphalArch, for completeness. Made the ArmyCamp, Colony, Gate, Lighthouse, Naval, Palace, Palisade, SiegeWall, and StoneWall classes visible. Deleted the now unused Apadana, Kennel, SpecialBuilding classes. Removed the Village class from outposts and palisades, because those are not supposed to count towards the phase technology requirements. Slightly rephrased the phase technology requirements tooltips. Corrected the <GenericName> of the various palisade files. Updated Structure tooltips from D2578 that have not already been committed elsewhere. Patch By: Nescio Differenital Revision: D2892 This was SVN commit r23865.
2020-07-21 12:45:30 -07:00
if (mStruct[0].hasClass("Gate"))
ent.attack(mStruct[0].id(), allowCapture(gameState, ent, mStruct[0]));
else
{
const rand = randIntExclusive(0, mStruct.length * 0.2);
ent.attack(mStruct[rand].id(), allowCapture(gameState, ent, mStruct[rand]));
}
}
else
{
if (!ent.hasClass("Ranged"))
{
const 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
{
const nearby = !ent.hasClasses(["FastMoving", "Ranged"]);
const mUnit = enemyUnits.filter(enemy =>
{
if (!enemy.position() || !ent.canAttackTarget(enemy, allowCapture(gameState, ent, enemy)))
return false;
if (enemy.hasClass("Animal"))
return false;
if (nearby && enemy.hasClass("Civilian") && enemy.unitAIState().split(".")[1] == "FLEEING")
return false;
const dist = SquareVectorDistance(enemy.position(), ent.position());
if (dist > range)
return false;
if (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()] &&
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.counters(unitA))
vala += 100;
let valb = unitB.hasClass("Support") ? 50 : 0;
if (ent.counters(unitB))
valb += 100;
const distA = unitA.getMetadata(PlayerID, "distance");
const 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;
});
const rand = randIntExclusive(0, mUnit.length * 0.1);
ent.attack(mUnit[rand].id(), 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 (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.hasClasses(["Ranged", "Ship"]))
targetClasses = { "attack": ["Unit", "Structure"], "avoid": ["Ship"], "vetoEntities": veto };
else
targetClasses = { "attack": ["Unit", "Structure"], "vetoEntities": veto };
}
else if (!ent.hasClasses(["Ranged", "Ship"]))
targetClasses = { "attack": targetClassesUnit.attack, "avoid": targetClassesUnit.avoid.concat("Ship"), "vetoEntities": veto };
ent.attackMove(this.targetPos[0], this.targetPos[1], targetClasses);
}
else
{
const mStruct = enemyStructures.filter(enemy =>
{
if (this.isBlocked && enemy.id() != this.target.id())
return false;
if (!enemy.position() || !ent.canAttackTarget(enemy, allowCapture(gameState, ent, enemy)))
return false;
if (SquareVectorDistance(enemy.position(), ent.position()) > range)
return false;
if (getLandAccess(gameState, enemy) != entAccess)
return false;
return true;
}, this).toEntityArray();
if (mStruct.length)
{
mStruct.sort((structa, structb) =>
{
let vala = structa.costSum();
Clean up market classes: template_structure_economic_market.xml → Market template_structure_military_dock.xml → Dock <Market/TradeType> node → Trade requirement for bartering: BarterMarket → Barter Change Petra accordingly Also changing: Renamed the Gates class to Gate, LongWall to WallLong (cf. WallTower), and StoneWall to Wall, to match their template file names. Adjusted AI code, technologies, and templates accordingly. Entity build restrictions: renamed Apadana to Palace; replaced UniqueStructure with IshtarGate and TempleOfVesta. added Council to the (unbuildable) spart_gerousia.xml, since it's functionally identical to athen_prytaneion.xml. Introduced the following classes: Council, IshtarGate, Library, TempleOfVesta, and Theater, to match their build restrictions. WallMedium and WallShort, to complement the already existing WallLong and WallTower classes. Stoa, TempleOfMars, and TriumphalArch, for completeness. Made the ArmyCamp, Colony, Gate, Lighthouse, Naval, Palace, Palisade, SiegeWall, and StoneWall classes visible. Deleted the now unused Apadana, Kennel, SpecialBuilding classes. Removed the Village class from outposts and palisades, because those are not supposed to count towards the phase technology requirements. Slightly rephrased the phase technology requirements tooltips. Corrected the <GenericName> of the various palisade files. Updated Structure tooltips from D2578 that have not already been committed elsewhere. Patch By: Nescio Differenital Revision: D2892 This was SVN commit r23865.
2020-07-21 12:45:30 -07:00
if (structa.hasClass("Gate") && ent.canAttackClass("Wall"))
vala += 10000;
else if (structa.hasClass("ConquestCritical"))
vala += 100;
let valb = structb.costSum();
Clean up market classes: template_structure_economic_market.xml → Market template_structure_military_dock.xml → Dock <Market/TradeType> node → Trade requirement for bartering: BarterMarket → Barter Change Petra accordingly Also changing: Renamed the Gates class to Gate, LongWall to WallLong (cf. WallTower), and StoneWall to Wall, to match their template file names. Adjusted AI code, technologies, and templates accordingly. Entity build restrictions: renamed Apadana to Palace; replaced UniqueStructure with IshtarGate and TempleOfVesta. added Council to the (unbuildable) spart_gerousia.xml, since it's functionally identical to athen_prytaneion.xml. Introduced the following classes: Council, IshtarGate, Library, TempleOfVesta, and Theater, to match their build restrictions. WallMedium and WallShort, to complement the already existing WallLong and WallTower classes. Stoa, TempleOfMars, and TriumphalArch, for completeness. Made the ArmyCamp, Colony, Gate, Lighthouse, Naval, Palace, Palisade, SiegeWall, and StoneWall classes visible. Deleted the now unused Apadana, Kennel, SpecialBuilding classes. Removed the Village class from outposts and palisades, because those are not supposed to count towards the phase technology requirements. Slightly rephrased the phase technology requirements tooltips. Corrected the <GenericName> of the various palisade files. Updated Structure tooltips from D2578 that have not already been committed elsewhere. Patch By: Nescio Differenital Revision: D2892 This was SVN commit r23865.
2020-07-21 12:45:30 -07:00
if (structb.hasClass("Gate") && ent.canAttackClass("Wall"))
valb += 10000;
else if (structb.hasClass("ConquestCritical"))
valb += 100;
return valb - vala;
});
Clean up market classes: template_structure_economic_market.xml → Market template_structure_military_dock.xml → Dock <Market/TradeType> node → Trade requirement for bartering: BarterMarket → Barter Change Petra accordingly Also changing: Renamed the Gates class to Gate, LongWall to WallLong (cf. WallTower), and StoneWall to Wall, to match their template file names. Adjusted AI code, technologies, and templates accordingly. Entity build restrictions: renamed Apadana to Palace; replaced UniqueStructure with IshtarGate and TempleOfVesta. added Council to the (unbuildable) spart_gerousia.xml, since it's functionally identical to athen_prytaneion.xml. Introduced the following classes: Council, IshtarGate, Library, TempleOfVesta, and Theater, to match their build restrictions. WallMedium and WallShort, to complement the already existing WallLong and WallTower classes. Stoa, TempleOfMars, and TriumphalArch, for completeness. Made the ArmyCamp, Colony, Gate, Lighthouse, Naval, Palace, Palisade, SiegeWall, and StoneWall classes visible. Deleted the now unused Apadana, Kennel, SpecialBuilding classes. Removed the Village class from outposts and palisades, because those are not supposed to count towards the phase technology requirements. Slightly rephrased the phase technology requirements tooltips. Corrected the <GenericName> of the various palisade files. Updated Structure tooltips from D2578 that have not already been committed elsewhere. Patch By: Nescio Differenital Revision: D2892 This was SVN commit r23865.
2020-07-21 12:45:30 -07:00
if (mStruct[0].hasClass("Gate"))
ent.attack(mStruct[0].id(), false);
else
{
const rand = randIntExclusive(0, mStruct.length * 0.2);
ent.attack(mStruct[rand].id(), 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;
const target = gameState.getEntityById(unit.unitAIOrderData()[0].target);
if (!target)
return;
const dist = SquareVectorDistance(unit.position(), ent.position());
if (dist > distmin)
return;
distmin = dist;
if (!ent.canAttackTarget(target, allowCapture(gameState, ent, target)))
return;
attacker = target;
});
if (attacker)
ent.attack(attacker.id(), 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;
}
2026-02-20 09:59:21 -08:00
this.lastPosition = clone(this.position);
Engine.ProfileStop();
return this.unitCollection.length;
};
AttackPlan.prototype.UpdateTransporting = function(gameState, events)
{
let done = true;
for (const 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 = AttackPlan.STATE_ARRIVED;
return;
}
// if we are attacked while waiting the rest of the army, retaliate
for (const evt of events.Attacked)
{
if (!this.unitCollection.hasEntId(evt.target))
continue;
const attacker = gameState.getEntityById(evt.attacker);
if (!attacker || !gameState.getEntityById(evt.target))
continue;
for (const ent of this.unitCollection.values())
{
if (ent.getMetadata(PlayerID, "transport") !== undefined)
continue;
const shouldCapture = allowCapture(gameState, ent, attacker);
if (!ent.isIdle() || !ent.canAttackTarget(attacker, shouldCapture))
continue;
ent.attack(attacker.id(), shouldCapture);
}
break;
}
};
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 (const evt of events.Attacked)
{
if (!this.unitCollection.hasEntId(evt.target))
continue;
const 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 = AttackPlan.STATE_ARRIVED;
return true;
}
}
// basically haven't moved an inch: very likely stuck)
if (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 (const ent of this.unitCollection.filter(filters.byClass("Siege")).values())
{
const dist = SquareVectorDistance(ent.position(), this.position);
if (dist < farthest)
continue;
farthest = dist;
farthestEnt = ent;
}
if (farthestEnt)
farthestEnt.destroy();
}
if (gameState.ai.playedTurn % 5 === 0)
2026-02-20 09:59:21 -08:00
this.position5TurnsAgo = clone(this.position);
if (this.lastPosition && SquareVectorDistance(this.position, this.lastPosition) < 16 &&
this.path.length > 0)
{
if (!this.path[0][0] || !this.path[0][1])
aiWarn("Start: Problem with path " + uneval(this.path));
// We're stuck, presumably. Check if there are no walls just close to us.
for (const ent of gameState.getEnemyStructures().filter(filters.byClass(["Palisade", "Wall"]))
.values())
{
if (SquareVectorDistance(this.position, ent.position()) > 800)
continue;
const enemyClass = ent.hasClass("Wall") ? "Wall" : "Palisade";
// there are walls, so check if we can attack
if (this.unitCollection.filter(filters.byCanAttackClass(enemyClass)).hasEntities())
{
if (this.Config.debug > 1)
{
aiWarn("Attack Plan " + this.type + " " + this.name +
" has met walls and is not happy.");
}
this.state = AttackPlan.STATE_ARRIVED;
return true;
}
// abort plan
if (this.Config.debug > 1)
{
aiWarn("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 (SquareVectorDistance(this.position, this.targetPos) < 10000)
{
if (this.Config.debug > 1)
aiWarn("Attack Plan " + this.type + " " + this.name + " has arrived to destination.");
this.state = AttackPlan.STATE_ARRIVED;
return true;
}
else if (this.path.length && 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)
{
aiWarn("Attack Plan " + this.type + " " + this.name +
" has arrived to destination.");
}
this.state = AttackPlan.STATE_ARRIVED;
return true;
}
}
return true;
};
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)
{
const holder = 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)
{
aiWarn("Seems like our target for plan " + this.name +
" has been destroyed or captured. Switching.");
}
const 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
const attackManager = gameState.ai.HQ.attackManager;
for (const attackType in attackManager.startedAttacks)
{
for (const 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 != 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)
{
aiWarn("No new target found. Remaining units " +
this.unitCollection.length);
}
return false;
}
}
if (this.Config.debug > 1)
aiWarn("We will help one of our other attacks");
}
this.targetPos = this.target.position();
}
return true;
};
/** reset any units */
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())
{
const access = this.getAttackAccess(gameState);
let dist = Math.min();
if (this.rallyPoint && gameState.ai.accessibility.getAccessValue(this.rallyPoint) == access)
{
rallyPoint = this.rallyPoint;
dist = SquareVectorDistance(this.position, rallyPoint);
}
// Then check if we have a nearer base (in case this attack has captured one)
for (const base of gameState.ai.HQ.baseManagers())
{
if (!base.anchor || !base.anchor.position())
continue;
if (getLandAccess(gameState, base.anchor) != access)
continue;
const newdist = SquareVectorDistance(this.position, base.anchor.position());
if (newdist > dist)
continue;
dist = newdist;
rallyPoint = base.anchor.position();
}
}
for (const ent of this.unitCollection.values())
{
if (ent.getMetadata(PlayerID, "role") === Worker.ROLE_ATTACK)
ent.stopMoving();
if (rallyPoint)
ent.moveToRange(rallyPoint[0], rallyPoint[1], 0, 15);
this.removeUnit(ent);
}
}
for (const unitCat in this.unitStat)
this.unit[unitCat].unregister();
this.removeQueues(gameState);
};
AttackPlan.prototype.removeUnit = function(ent, update)
{
if (ent.getMetadata(PlayerID, "role") === Worker.ROLE_ATTACK)
{
if (ent.hasClass("CitizenSoldier"))
ent.setMetadata(PlayerID, "role", Worker.ROLE_WORKER);
else
ent.setMetadata(PlayerID, "role", undefined);
ent.setMetadata(PlayerID, "subrole", undefined);
}
ent.setMetadata(PlayerID, "plan", -1);
if (update)
this.unitCollection.updateEnt(ent);
};
AttackPlan.prototype.checkEvents = function(gameState, events)
{
for (const evt of events.EntityRenamed)
{
if (!this.target || this.target.id() != evt.entity)
continue;
if (this.type === AttackPlan.TYPE_RAID && !this.isStarted())
this.target = undefined;
else
this.target = gameState.getEntityById(evt.newentity);
if (this.target)
this.targetPos = this.target.position();
}
for (const evt of events.OwnershipChanged) // capture event
if (this.target && this.target.id() == evt.entity && gameState.isPlayerAlly(evt.to))
this.target = undefined;
for (const evt of events.PlayerDefeated)
{
if (this.targetPlayer !== evt.playerId)
continue;
this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this);
this.target = undefined;
}
2025-10-14 12:06:41 -07:00
this.unitCollection = gameState.getOwnUnits().filter(
filters.byMetadata(PlayerID, "plan", this.name));
this.unitCollection.registerUpdates();
if (!this.overseas || this.state !== AttackPlan.STATE_UNEXECUTED)
return;
// let's check if an enemy has built a structure at our access
for (const evt of events.Create)
{
const ent = gameState.getEntityById(evt.entity);
if (!ent || !ent.position() || !ent.hasClass("Structure"))
continue;
if (!gameState.isPlayerEnemy(ent.owner()))
continue;
const access = getLandAccess(gameState, ent);
for (const 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();
}
}
};
AttackPlan.prototype.waitingForTransport = function()
{
for (const ent of this.unitCollection.values())
if (ent.getMetadata(PlayerID, "transport") !== undefined)
return true;
return false;
};
AttackPlan.prototype.hasSiegeUnits = function()
{
for (const ent of this.unitCollection.values())
if (isSiegeUnit(ent))
return true;
return false;
};
AttackPlan.prototype.hasForceOrder = function(data, value)
{
for (const ent of this.unitCollection.values())
{
if (data && +ent.getMetadata(PlayerID, data) !== value)
continue;
const orders = ent.unitAIOrderData();
for (const 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.
*/
AttackPlan.prototype.getAttackAccess = function(gameState)
{
for (const ent of this.unitCollection.filterNearest(this.position, 1).values())
return getLandAccess(gameState, ent);
return 0;
};
AttackPlan.prototype.debugAttack = function()
{
aiWarn("---------- attack " + this.name);
for (const unitCat in this.unitStat)
{
const Unit = this.unitStat[unitCat];
aiWarn(unitCat + " num=" + this.unit[unitCat].length + " min=" + Unit.minSize + " need=" +
Unit.targetSize);
}
aiWarn("------------------------------");
};
AttackPlan.prototype.Serialize = function()
{
const properties = {
"name": this.name,
"type": this.type,
"state": this.state,
"forced": this.forced,
2026-02-20 09:59:21 -08:00
"rallyPoint": clone(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 !== undefined ? clone(this.position) : undefined,
"isBlocked": this.isBlocked,
"targetPlayer": this.targetPlayer,
"target": this.target !== undefined ? this.target.id() : undefined,
"targetPos": this.targetPos !== undefined ? clone(this.targetPos) : undefined,
"uniqueTargetId": this.uniqueTargetId,
"path": this.path,
"unitCollUpdateArray": this.unitCollUpdateArray
};
return { "properties": properties };
};
AttackPlan.prototype.Deserialize = function(gameState, data)
{
for (const key in data.properties)
this[key] = data.properties[key];
if (this.target)
this.target = gameState.getEntityById(this.target);
this.failed = undefined;
};