Source: attackPlan.js

/**
* @class
 * 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.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.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.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 = randFloat(0.8, 1.2);
	// and lower priority and smaller sizes for easier difficulty levels
	if (this.Config.difficulty < 2)
	{
		priority *= 0.4;
		variation *= 0.2;
	}
	else if (this.Config.difficulty < 3)
	{
		priority *= 0.8;
		variation *= 0.6;
	}

	if (this.Config.difficulty < 2)
	{
		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 (let 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);
	}

	// 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.byClasses(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.byClasses(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();
	const classes = [["Siege+Melee"], ["Siege+Ranged"], ["Elephant+Melee"]];
	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 (template.hasClasses(classes[i]))
					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.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[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)
		{
			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 (!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")
	{
		// 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("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 (let 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 (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 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 <= 2)
		return added;

	let num = 0;
	const numbase = {};
	let keep = this.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", 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
		{
			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) 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.hasClasses(["FastMoving", "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 || !ourUnit.position() || !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.hasClasses(["Structure", "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.hasClasses(["Structure", "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.hasClasses(["Champion", "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.hasClasses(["Palisade", "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", "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
		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("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("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
			{
				const nearby = !ent.hasClasses(["FastMoving", "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.counters(unitA))
							vala += 100;
						let valb = unitB.hasClass("Support") ? 50 : 0;
						if (ent.counters(unitB))
							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.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
				{
					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;
};