Source: defenseManager.js

/**
 * @class
 */
PETRA.DefenseManager = function(Config)
{
	// Array of "army" Objects.
	this.armies = [];
	this.Config = Config;
	this.targetList = [];
	this.armyMergeSize = this.Config.Defense.armyMergeSize;
	// Stats on how many enemies are currently attacking our allies
	// this.attackingArmies[enemy][ally] = number of enemy armies inside allied territory
	// this.attackingUnits[enemy][ally] = number of enemy units not in armies inside allied territory
	// this.attackedAllies[ally] = number of enemies attacking the ally
	this.attackingArmies = {};
	this.attackingUnits = {};
	this.attackedAllies = {};
};

PETRA.DefenseManager.prototype.update = function(gameState, events)
{
	Engine.ProfileStart("Defense Manager");

	this.territoryMap = gameState.ai.HQ.territoryMap;

	this.checkEvents(gameState, events);

	// Check if our potential targets are still valid.
	for (let i = 0; i < this.targetList.length; ++i)
	{
		let target = gameState.getEntityById(this.targetList[i]);
		if (!target || !target.position() || !gameState.isPlayerEnemy(target.owner()))
			this.targetList.splice(i--, 1);
	}

	// Count the number of enemies attacking our allies in the previous turn.
	// We'll be more cooperative if several enemies are attacking him simultaneously.
	this.attackedAllies = {};
	let attackingArmies = clone(this.attackingArmies);
	for (let enemy in this.attackingUnits)
	{
		if (!this.attackingUnits[enemy])
			continue;
		for (let ally in this.attackingUnits[enemy])
		{
			if (this.attackingUnits[enemy][ally] < 8)
				continue;
			if (attackingArmies[enemy] === undefined)
				attackingArmies[enemy] = {};
			if (attackingArmies[enemy][ally] === undefined)
				attackingArmies[enemy][ally] = 0;
			attackingArmies[enemy][ally] += 1;
		}
	}
	for (let enemy in attackingArmies)
	{
		for (let ally in attackingArmies[enemy])
		{
			if (this.attackedAllies[ally] === undefined)
				this.attackedAllies[ally] = 0;
			this.attackedAllies[ally] += 1;
		}
	}
	this.checkEnemyArmies(gameState);
	this.checkEnemyUnits(gameState);
	this.assignDefenders(gameState);

	Engine.ProfileStop();
};

PETRA.DefenseManager.prototype.makeIntoArmy = function(gameState, entityID, type = "default")
{
	if (type == "default")
	{
		for (let army of this.armies)
			if (army.getType() == type && army.addFoe(gameState, entityID))
				return;
	}

	this.armies.push(new PETRA.DefenseArmy(gameState, [entityID], type));
};

PETRA.DefenseManager.prototype.getArmy = function(partOfArmy)
{
	return this.armies.find(army => army.ID == partOfArmy);
};

PETRA.DefenseManager.prototype.isDangerous = function(gameState, entity)
{
	if (!entity.position())
		return false;

	let territoryOwner = this.territoryMap.getOwner(entity.position());
	if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner))
		return false;
	// Check if the entity is trying to build a new base near our buildings,
	// and if yes, add this base in our target list.
	if (entity.unitAIState() && entity.unitAIState() == "INDIVIDUAL.REPAIR.REPAIRING")
	{
		let targetId = entity.unitAIOrderData()[0].target;
		if (this.targetList.indexOf(targetId) != -1)
			return true;
		let target = gameState.getEntityById(targetId);
		if (target)
		{
			let isTargetEnemy = gameState.isPlayerEnemy(target.owner());
			if (isTargetEnemy && territoryOwner == PlayerID)
			{
				if (target.hasClass("Structure"))
					this.targetList.push(targetId);
				return true;
			}
			else if (isTargetEnemy && target.hasClass("CivCentre"))
			{
				let myBuildings = gameState.getOwnStructures();
				for (let building of myBuildings.values())
				{
					if (building.foundationProgress() == 0)
						continue;
					if (API3.SquareVectorDistance(building.position(), entity.position()) > 30000)
						continue;
					this.targetList.push(targetId);
					return true;
				}
			}
		}
	}

	if (entity.attackTypes() === undefined || entity.hasClass("Support"))
		return false;
	let dist2Min = 6000;
	// TODO the 30 is to take roughly into account the structure size in following checks. Can be improved.
	if (entity.attackTypes().indexOf("Ranged") != -1)
		dist2Min = (entity.attackRange("Ranged").max + 30) * (entity.attackRange("Ranged").max + 30);

	for (let targetId of this.targetList)
	{
		let target = gameState.getEntityById(targetId);
		// The enemy base is either destroyed or built.
		if (!target || !target.position())
			continue;
		if (API3.SquareVectorDistance(target.position(), entity.position()) < dist2Min)
			return true;
	}

	let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre"));
	for (let cc of ccEnts.values())
	{
		if (!gameState.isEntityExclusiveAlly(cc) || cc.foundationProgress() == 0)
			continue;
		let cooperation = this.GetCooperationLevel(cc.owner());
		if (cooperation < 0.3 || cooperation < 0.6 && !!cc.foundationProgress())
			continue;
		if (API3.SquareVectorDistance(cc.position(), entity.position()) < dist2Min)
			return true;
	}

	for (let building of gameState.getOwnStructures().values())
	{
		if (building.foundationProgress() == 0 ||
		    API3.SquareVectorDistance(building.position(), entity.position()) > dist2Min)
			continue;
		if (!this.territoryMap.isBlinking(building.position()) || gameState.ai.HQ.isDefendable(building))
			return true;
	}

	if (gameState.isPlayerMutualAlly(territoryOwner))
	{
		// If ally attacked by more than 2 enemies, help him not only for cc but also for structures.
		if (territoryOwner != PlayerID && this.attackedAllies[territoryOwner] &&
		                                  this.attackedAllies[territoryOwner] > 1 &&
		                                  this.GetCooperationLevel(territoryOwner) > 0.7)
		{
			for (let building of gameState.getAllyStructures(territoryOwner).values())
			{
				if (building.foundationProgress() == 0 ||
				    API3.SquareVectorDistance(building.position(), entity.position()) > dist2Min)
					continue;
				if (!this.territoryMap.isBlinking(building.position()))
					return true;
			}
		}

		// Update the number of enemies attacking this ally.
		let enemy = entity.owner();
		if (this.attackingUnits[enemy] === undefined)
			this.attackingUnits[enemy] = {};
		if (this.attackingUnits[enemy][territoryOwner] === undefined)
			this.attackingUnits[enemy][territoryOwner] = 0;
		this.attackingUnits[enemy][territoryOwner] += 1;
	}

	return false;
};

PETRA.DefenseManager.prototype.checkEnemyUnits = function(gameState)
{
	const nbPlayers = gameState.sharedScript.playersData.length;
	let i = gameState.ai.playedTurn % nbPlayers;
	this.attackingUnits[i] = undefined;

	if (i == PlayerID)
	{
		if (!this.armies.length)
		{
			// Check if we can recover capture points from any of our notdecaying structures.
			for (let ent of gameState.getOwnStructures().values())
			{
				if (ent.decaying())
					continue;
				let capture = ent.capturePoints();
				if (capture === undefined)
					continue;
				let lost = 0;
				for (let j = 0; j < capture.length; ++j)
					if (gameState.isPlayerEnemy(j))
						lost += capture[j];
				if (lost < Math.ceil(0.25 * capture[i]))
					continue;
				this.makeIntoArmy(gameState, ent.id(), "capturing");
				break;
			}
		}
		return;
	}
	else if (!gameState.isPlayerEnemy(i))
		return;

	for (let ent of gameState.getEnemyUnits(i).values())
	{
		if (ent.getMetadata(PlayerID, "PartOfArmy") !== undefined)
			continue;

		// Keep animals attacking us or our allies.
		if (ent.hasClass("Animal"))
		{
			if (!ent.unitAIState() || ent.unitAIState().split(".")[1] != "COMBAT")
				continue;
			let orders = ent.unitAIOrderData();
			if (!orders || !orders.length || !orders[0].target)
				continue;
			let target = gameState.getEntityById(orders[0].target);
			if (!target || !gameState.isPlayerAlly(target.owner()))
				continue;
		}

		// TODO what to do for ships ?
		if (ent.hasClasses(["Ship", "Trader"]))
			continue;

		// Check if unit is dangerous "a priori".
		if (this.isDangerous(gameState, ent))
			this.makeIntoArmy(gameState, ent.id());
	}

	if (i != 0 || this.armies.length > 1 || !gameState.ai.HQ.hasActiveBase())
		return;
	// Look for possible gaia buildings inside our territory (may happen when enemy resign or after structure decay)
	// and attack it only if useful (and capturable) or dangereous.
	for (let ent of gameState.getEnemyStructures(i).values())
	{
		if (!ent.position() || ent.getMetadata(PlayerID, "PartOfArmy") !== undefined)
			continue;
		if (!ent.capturePoints() && !ent.hasDefensiveFire())
			continue;
		let owner = this.territoryMap.getOwner(ent.position());
		if (owner == PlayerID)
			this.makeIntoArmy(gameState, ent.id(), "capturing");
	}
};

PETRA.DefenseManager.prototype.checkEnemyArmies = function(gameState)
{
	for (let i = 0; i < this.armies.length; ++i)
	{
		let army = this.armies[i];
		// This returns a list of IDs: the units that broke away from the army for being too far.
		let breakaways = army.update(gameState);
		// Assume dangerosity.
		for (let breaker of breakaways)
			this.makeIntoArmy(gameState, breaker);

		if (army.getState() == 0)
		{
			if (army.getType() == "default")
				this.switchToAttack(gameState, army);
			army.clear(gameState);
			this.armies.splice(i--, 1);
		}
	}
	// Check if we can't merge it with another.
	for (let i = 0; i < this.armies.length - 1; ++i)
	{
		let army = this.armies[i];
		if (army.getType() != "default")
			continue;
		for (let j = i+1; j < this.armies.length; ++j)
		{
			let otherArmy = this.armies[j];
			if (otherArmy.getType() != "default" ||
				API3.SquareVectorDistance(army.foePosition, otherArmy.foePosition) > this.armyMergeSize)
				continue;
			// No need to clear here.
			army.merge(gameState, otherArmy);
			this.armies.splice(j--, 1);
		}
	}

	if (gameState.ai.playedTurn % 5 != 0)
		return;
	// Check if any army is no more dangerous (possibly because it has defeated us and destroyed our base).
	this.attackingArmies = {};
	for (let i = 0; i < this.armies.length; ++i)
	{
		let army = this.armies[i];
		army.recalculatePosition(gameState);
		let owner = this.territoryMap.getOwner(army.foePosition);
		if (!gameState.isPlayerEnemy(owner))
		{
			if (gameState.isPlayerMutualAlly(owner))
			{
				// Update the number of enemies attacking this ally.
				for (let id of army.foeEntities)
				{
					let ent = gameState.getEntityById(id);
					if (!ent)
						continue;
					let enemy = ent.owner();
					if (this.attackingArmies[enemy] === undefined)
						this.attackingArmies[enemy] = {};
					if (this.attackingArmies[enemy][owner] === undefined)
						this.attackingArmies[enemy][owner] = 0;
					this.attackingArmies[enemy][owner] += 1;
					break;
				}
			}
			continue;
		}
		// Enemy army back in its territory.
		else if (owner != 0)
		{
			army.clear(gameState);
			this.armies.splice(i--, 1);
			continue;
		}

		// Army in neutral territory.
		// TODO check smaller distance with all our buildings instead of only ccs with big distance.
		let stillDangerous = false;
		let bases = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre"));
		for (let base of bases.values())
		{
			if (!gameState.isEntityAlly(base))
				continue;
			let cooperation = this.GetCooperationLevel(base.owner());
			if (cooperation < 0.3 && !gameState.isEntityOwn(base))
				continue;
			if (API3.SquareVectorDistance(base.position(), army.foePosition) > 40000)
				continue;
			if(this.Config.debug > 1)
				API3.warn("army in neutral territory, but still near one of our CC");
			stillDangerous = true;
			break;
		}
		if (stillDangerous)
			continue;
		// Need to also check docks because of oversea bases.
		for (let dock of gameState.getOwnStructures().filter(API3.Filters.byClass("Dock")).values())
		{
			if (API3.SquareVectorDistance(dock.position(), army.foePosition) > 10000)
				continue;
			stillDangerous = true;
			break;
		}
		if (stillDangerous)
			continue;

		if (army.getType() == "default")
			this.switchToAttack(gameState, army);
		army.clear(gameState);
		this.armies.splice(i--, 1);
	}
};

PETRA.DefenseManager.prototype.assignDefenders = function(gameState)
{
	if (!this.armies.length)
		return;

	let armiesNeeding = [];
	// Let's add defenders.
	for (let army of this.armies)
	{
		let needsDef = army.needsDefenders(gameState);
		if (needsDef === false)
			continue;

		let armyAccess;
		for (let entId of army.foeEntities)
		{
			let ent = gameState.getEntityById(entId);
			if (!ent || !ent.position())
				continue;
			armyAccess = PETRA.getLandAccess(gameState, ent);
			break;
		}
		if (!armyAccess)
			API3.warn(" Petra error: attacking army " + army.ID + " without access");
		army.recalculatePosition(gameState);
		armiesNeeding.push({ "army": army, "access": armyAccess, "need": needsDef });
	}

	if (!armiesNeeding.length)
		return;

	// Let's get our potential units.
	let potentialDefenders = [];
	gameState.getOwnUnits().forEach(function(ent) {
		if (!ent.position())
			return;
		if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3)
			return;
		if (ent.hasClass("Support") || ent.attackTypes() === undefined)
			return;
		if (ent.hasClasses(["StoneThrower", "Support", "FishingBoat"]))
			return;
		if (ent.getMetadata(PlayerID, "transport") !== undefined ||
		    ent.getMetadata(PlayerID, "transporter") !== undefined)
			return;
		if (gameState.ai.HQ.victoryManager.criticalEnts.has(ent.id()))
			return;
		if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") != -1)
		{
			let subrole = ent.getMetadata(PlayerID, "subrole");
			if (subrole &&
				(subrole === PETRA.Worker.SUBROLE_COMPLETING || subrole === PETRA.Worker.SUBROLE_WALKING || subrole === PETRA.Worker.SUBROLE_ATTACKING))
				return;
		}
		potentialDefenders.push(ent.id());
	});

	for (let ipass = 0; ipass < 2; ++ipass)
	{
		// First pass only assign defenders with the right access.
		// Second pass assign all defenders.
		// TODO could sort them by distance.
		let backup = 0;
		for (let i = 0; i < potentialDefenders.length; ++i)
		{
			let ent = gameState.getEntityById(potentialDefenders[i]);
			if (!ent || !ent.position())
				continue;
			let aMin;
			let distMin;
			let access = ipass == 0 ? PETRA.getLandAccess(gameState, ent) : undefined;
			for (let a = 0; a < armiesNeeding.length; ++a)
			{
				if (access && armiesNeeding[a].access != access)
					continue;

				// Do not assign defender if it cannot attack at least part of the attacking army.
				if (!armiesNeeding[a].army.foeEntities.some(eEnt => {
					let eEntID = gameState.getEntityById(eEnt);
					return ent.canAttackTarget(eEntID, PETRA.allowCapture(gameState, ent, eEntID));
					}))
					continue;

				let dist = API3.SquareVectorDistance(ent.position(), armiesNeeding[a].army.foePosition);
				if (aMin !== undefined && dist > distMin)
					continue;
				aMin = a;
				distMin = dist;
			}

			// If outside our territory (helping an ally or attacking a cc foundation)
			// or if in another access, keep some troops in backup.
			if (backup < 12 && (aMin == undefined || distMin > 40000 &&
			        this.territoryMap.getOwner(armiesNeeding[aMin].army.foePosition) != PlayerID))
			{
				++backup;
				potentialDefenders[i] = undefined;
				continue;
			}
			else if (aMin === undefined)
				continue;

			armiesNeeding[aMin].need -= PETRA.getMaxStrength(ent, this.Config.debug, this.Config.DamageTypeImportance);
			armiesNeeding[aMin].army.addOwn(gameState, potentialDefenders[i]);
			armiesNeeding[aMin].army.assignUnit(gameState, potentialDefenders[i]);
			potentialDefenders[i] = undefined;

			if (armiesNeeding[aMin].need <= 0)
				armiesNeeding.splice(aMin, 1);
			if (!armiesNeeding.length)
				return;
		}
	}

	// If shortage of defenders, produce infantry garrisoned in nearest civil center.
	let armiesPos = [];
	for (let a = 0; a < armiesNeeding.length; ++a)
		armiesPos.push(armiesNeeding[a].army.foePosition);
	gameState.ai.HQ.trainEmergencyUnits(gameState, armiesPos);
};

PETRA.DefenseManager.prototype.abortArmy = function(gameState, army)
{
	army.clear(gameState);
	for (let i = 0; i < this.armies.length; ++i)
	{
		if (this.armies[i].ID != army.ID)
			continue;
		this.armies.splice(i, 1);
		break;
	}
};

/**
 * If our defense structures are attacked, garrison soldiers inside when possible
 * and if a support unit is attacked and has less than 55% health, garrison it inside the nearest healing structure
 * and if a ranged siege unit (not used for defense) is attacked, garrison it in the nearest fortress.
 * If our hero is attacked with regicide victory condition, the victoryManager will handle it.
 */
PETRA.DefenseManager.prototype.checkEvents = function(gameState, events)
{
	// Must be called every turn for all armies.
	for (let army of this.armies)
		army.checkEvents(gameState, events);

	// Capture events.
	for (let evt of events.OwnershipChanged)
	{
		if (gameState.isPlayerMutualAlly(evt.from) && evt.to > 0)
		{
			let ent = gameState.getEntityById(evt.entity);
			// One of our cc has been captured.
			if (ent && ent.hasClass("CivCentre"))
				gameState.ai.HQ.attackManager.switchDefenseToAttack(gameState, ent, { "range": 150 });
		}
	}

	let allAttacked = {};
	for (let evt of events.Attacked)
		allAttacked[evt.target] = evt.attacker;

	for (let evt of events.Attacked)
	{
		let target = gameState.getEntityById(evt.target);
		if (!target || !target.position())
			continue;

		let attacker = gameState.getEntityById(evt.attacker);
		if (attacker && gameState.isEntityOwn(attacker) && gameState.isEntityEnemy(target) && !attacker.hasClass("Ship") &&
		   (!target.hasClass("Structure") || target.attackRange("Ranged")))
		{
			// If enemies are in range of one of our defensive structures, garrison it for arrow multiplier
			// (enemy non-defensive structure are not considered to stay in sync with garrisonManager).
			if (attacker.position() && attacker.isGarrisonHolder() && attacker.getArrowMultiplier() &&
			    (target.owner() != 0 || !target.hasClass("Unit") ||
			     target.unitAIState() && target.unitAIState().split(".")[1] == "COMBAT"))
				this.garrisonUnitsInside(gameState, attacker, { "attacker": target });
		}

		if (!gameState.isEntityOwn(target))
			continue;

		// If attacked by one of our allies (he must trying to recover capture points), do not react.
		if (attacker && gameState.isEntityAlly(attacker))
			continue;

		if (attacker && attacker.position() && target.hasClass("FishingBoat"))
		{
			let unitAIState = target.unitAIState();
			let unitAIStateOrder = unitAIState ? unitAIState.split(".")[1] : "";
			if (target.isIdle() || unitAIStateOrder == "GATHER")
			{
				let pos = attacker.position();
				let range = attacker.attackRange("Ranged") ? attacker.attackRange("Ranged").max + 15 : 25;
				if (range * range > API3.SquareVectorDistance(pos, target.position()))
					target.moveToRange(pos[0], pos[1], range, range + 5);
			}
			continue;
		}
		
		// TODO integrate other ships later, need to be sure it is accessible.
		if (target.hasClass("Ship"))
			continue;

		// If a building on a blinking tile is attacked, check if it can be defended.
		// Same thing for a building in an isolated base (not connected to a base with anchor).
		if (target.hasClass("Structure"))
		{
			let base = gameState.ai.HQ.getBaseByID(target.getMetadata(PlayerID, "base"));
			if (this.territoryMap.isBlinking(target.position()) && !gameState.ai.HQ.isDefendable(target) ||
			    !base || gameState.ai.HQ.baseManagers().every(b => !b.anchor || b.accessIndex != base.accessIndex))
			{
				let capture = target.capturePoints();
				if (!capture)
					continue;
				let captureRatio = capture[PlayerID] / capture.reduce((a, b) => a + b);
				if (captureRatio > 0.50 && captureRatio < 0.70)
					target.destroy();
				continue;
			}
		}


		// If inside a started attack plan, let the plan deal with this unit.
		let plan = target.getMetadata(PlayerID, "plan");
		if (plan !== undefined && plan >= 0)
		{
			let attack = gameState.ai.HQ.attackManager.getPlan(plan);
			if (attack && attack.state != PETRA.AttackPlan.STATE_UNEXECUTED)
				continue;
		}

		// Signal this attacker to our defense manager, except if we are in enemy territory.
		// TODO treat ship attack.
		if (attacker && attacker.position() && attacker.getMetadata(PlayerID, "PartOfArmy") === undefined &&
			!attacker.hasClasses(["Structure", "Ship"]))
		{
			let territoryOwner = this.territoryMap.getOwner(attacker.position());
			if (territoryOwner == 0 || gameState.isPlayerAlly(territoryOwner))
				this.makeIntoArmy(gameState, attacker.id());
		}

		if (target.getMetadata(PlayerID, "PartOfArmy") !== undefined)
		{
			let army = this.getArmy(target.getMetadata(PlayerID, "PartOfArmy"));
			if (army.getType() == "capturing")
			{
				let abort = false;
				// If one of the units trying to capture a structure is attacked,
				// abort the army so that the unit can defend itself
				if (army.ownEntities.indexOf(target.id()) != -1)
					abort = true;
				else if (army.foeEntities[0] == target.id() && target.owner() == PlayerID)
				{
					// else we may be trying to regain some capture point from one of our structure.
					abort = true;
					let capture = target.capturePoints();
					for (let j = 0; j < capture.length; ++j)
					{
						if (!gameState.isPlayerEnemy(j) || capture[j] == 0)
							continue;
						abort = false;
						break;
					}
				}
				if (abort)
					this.abortArmy(gameState, army);
			}
			continue;
		}

		// Try to garrison any attacked support unit if low health.
		if (target.hasClass("Support") && target.healthLevel() < this.Config.garrisonHealthLevel.medium &&
			!target.getMetadata(PlayerID, "transport") && plan != -2 && plan != -3)
		{
			this.garrisonAttackedUnit(gameState, target);
			continue;
		}

		// Try to garrison any attacked stone thrower.
		if (target.hasClass("StoneThrower") &&
			!target.getMetadata(PlayerID, "transport") && plan != -2 && plan != -3)
		{
			this.garrisonSiegeUnit(gameState, target);
			continue;
		}

		if (!attacker || !attacker.position())
			continue;

		if (target.isGarrisonHolder() && target.getArrowMultiplier())
			this.garrisonUnitsInside(gameState, target, { "attacker": attacker });

		if (target.hasClass("Unit") && attacker.hasClass("Unit"))
		{
			// Consider whether we should retaliate or continue our task.
			if (target.hasClass("Support") || target.attackTypes() === undefined)
				continue;
			let orderData = target.unitAIOrderData();
			let currentTarget = orderData && orderData.length && orderData[0].target ?
				gameState.getEntityById(orderData[0].target) : undefined;
			if (currentTarget)
			{
				let unitAIState = target.unitAIState();
				let unitAIStateOrder = unitAIState ? unitAIState.split(".")[1] : "";
				if (unitAIStateOrder == "COMBAT" && (currentTarget == attacker.id() ||
					!currentTarget.hasClasses(["Structure", "Support"])))
					continue;
				if (unitAIStateOrder == "REPAIR" && currentTarget.hasDefensiveFire())
					continue;
				if (unitAIStateOrder == "COMBAT" && !PETRA.isSiegeUnit(currentTarget) &&
				    gameState.ai.HQ.capturableTargets.has(orderData[0].target))
				{
					// Take the nearest unit also attacking this structure to help us.
					let capturableTarget = gameState.ai.HQ.capturableTargets.get(orderData[0].target);
					let minDist;
					let minEnt;
					let pos = attacker.position();
					capturableTarget.ents.delete(target.id());
					for (let entId of capturableTarget.ents)
					{
						if (allAttacked[entId])
							continue;
						let ent = gameState.getEntityById(entId);
						if (!ent || !ent.position() || !ent.canAttackTarget(attacker, PETRA.allowCapture(gameState, ent, attacker)))
							continue;
						// Check that the unit is still attacking the structure (since the last played turn).
						let state = ent.unitAIState();
						if (!state || !state.split(".")[1] || state.split(".")[1] != "COMBAT")
							continue;
						let entOrderData = ent.unitAIOrderData();
						if (!entOrderData || !entOrderData.length || !entOrderData[0].target ||
						     entOrderData[0].target != orderData[0].target)
							continue;
						let dist = API3.SquareVectorDistance(pos, ent.position());
						if (minEnt && dist > minDist)
							continue;
						minDist = dist;
						minEnt = ent;
					}
					if (minEnt)
					{
						capturableTarget.ents.delete(minEnt.id());
						minEnt.attack(attacker.id(), PETRA.allowCapture(gameState, minEnt, attacker));
					}
				}
			}
			let allowCapture = PETRA.allowCapture(gameState, target, attacker);
			if (target.canAttackTarget(attacker, allowCapture))
				target.attack(attacker.id(), allowCapture);
		}
	}
};

PETRA.DefenseManager.prototype.garrisonUnitsInside = function(gameState, target, data)
{
	if (target.hitpoints() < target.garrisonEjectHealth() * target.maxHitpoints())
		return false;
	let minGarrison = data.min || target.garrisonMax();
	if (gameState.ai.HQ.garrisonManager.numberOfGarrisonedSlots(target) >= minGarrison)
		return false;
	if (data.attacker)
	{
		let attackTypes = target.attackTypes();
		if (!attackTypes || attackTypes.indexOf("Ranged") == -1)
			return false;
		let dist = API3.SquareVectorDistance(data.attacker.position(), target.position());
		let range = target.attackRange("Ranged").max;
		if (dist >= range*range)
			return false;
	}
	let access = PETRA.getLandAccess(gameState, target);
	let garrisonManager = gameState.ai.HQ.garrisonManager;
	let garrisonArrowClasses = target.getGarrisonArrowClasses();
	const typeGarrison = data.type || PETRA.GarrisonManager.TYPE_PROTECTION;
	let allowMelee = gameState.ai.HQ.garrisonManager.allowMelee(target);
	if (allowMelee === undefined)
	{
		// Should be kept in sync with garrisonManager to avoid garrisoning-ungarrisoning some units.
		if (data.attacker)
			allowMelee = data.attacker.hasClass("Structure") ? data.attacker.attackRange("Ranged") : !PETRA.isSiegeUnit(data.attacker);
		else
			allowMelee = true;
	}
	let units = gameState.getOwnUnits().filter(ent => {
		if (!ent.position())
			return false;
		if (!ent.hasClasses(garrisonArrowClasses))
			return false;
		if (typeGarrison !== PETRA.GarrisonManager.TYPE_DECAY && !allowMelee && ent.attackTypes().indexOf("Melee") != -1)
			return false;
		if (ent.getMetadata(PlayerID, "transport") !== undefined)
			return false;
		let army = ent.getMetadata(PlayerID, "PartOfArmy") ? this.getArmy(ent.getMetadata(PlayerID, "PartOfArmy")) : undefined;
		if (!army && (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3))
			return false;
		if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") >= 0)
		{
			let subrole = ent.getMetadata(PlayerID, "subrole");
			// When structure decaying (usually because we've just captured it in enemy territory), also allow units from an attack plan.
			if (typeGarrison !== PETRA.GarrisonManager.TYPE_DECAY && subrole &&
				(subrole === PETRA.Worker.SUBROLE_COMPLETING || subrole === PETRA.Worker.SUBROLE_WALKING || subrole === PETRA.Worker.SUBROLE_ATTACKING))
				return false;
		}
		if (PETRA.getLandAccess(gameState, ent) != access)
			return false;
		return true;
	}).filterNearest(target.position());

	let ret = false;
	for (let ent of units.values())
	{
		if (garrisonManager.numberOfGarrisonedSlots(target) >= minGarrison)
			break;
		if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") >= 0)
		{
			let attackPlan = gameState.ai.HQ.attackManager.getPlan(ent.getMetadata(PlayerID, "plan"));
			if (attackPlan)
				attackPlan.removeUnit(ent, true);
		}
		let army = ent.getMetadata(PlayerID, "PartOfArmy") ? this.getArmy(ent.getMetadata(PlayerID, "PartOfArmy")) : undefined;
		if (army)
			army.removeOwn(gameState, ent.id());
		garrisonManager.garrison(gameState, ent, target, typeGarrison);
		ret = true;
	}
	return ret;
};

/** Garrison a attacked siege ranged unit inside the nearest fortress. */
PETRA.DefenseManager.prototype.garrisonSiegeUnit = function(gameState, unit)
{
	let distmin = Math.min();
	let nearest;
	let unitAccess = PETRA.getLandAccess(gameState, unit);
	let garrisonManager = gameState.ai.HQ.garrisonManager;
	for (let ent of gameState.getAllyStructures().values())
	{
		if (!ent.isGarrisonHolder())
			continue;
		if (!unit.hasClasses(ent.garrisonableClasses()))
			continue;
		if (garrisonManager.numberOfGarrisonedSlots(ent) >= ent.garrisonMax())
			continue;
		if (ent.hitpoints() < ent.garrisonEjectHealth() * ent.maxHitpoints())
			continue;
		if (PETRA.getLandAccess(gameState, ent) != unitAccess)
			continue;
		let dist = API3.SquareVectorDistance(ent.position(), unit.position());
		if (dist > distmin)
			continue;
		distmin = dist;
		nearest = ent;
	}
	if (nearest)
		garrisonManager.garrison(gameState, unit, nearest, PETRA.GarrisonManager.TYPE_PROTECTION);
	return nearest !== undefined;
};

/**
 * Garrison a hurt unit inside a player-owned or allied structure.
 * If emergency is true, the unit will be garrisoned in the closest possible structure.
 * Otherwise, it will garrison in the closest healing structure.
 */
PETRA.DefenseManager.prototype.garrisonAttackedUnit = function(gameState, unit, emergency = false)
{
	let distmin = Math.min();
	let nearest;
	let unitAccess = PETRA.getLandAccess(gameState, unit);
	let garrisonManager = gameState.ai.HQ.garrisonManager;
	for (let ent of gameState.getAllyStructures().values())
	{
		if (!ent.isGarrisonHolder())
			continue;
		if (!emergency && !ent.buffHeal())
			continue;
		if (!unit.hasClasses(ent.garrisonableClasses()))
			continue;
		if (garrisonManager.numberOfGarrisonedSlots(ent) >= ent.garrisonMax() &&
		    (!emergency || !ent.garrisoned().length))
			continue;
		if (ent.hitpoints() < ent.garrisonEjectHealth() * ent.maxHitpoints())
			continue;
		if (PETRA.getLandAccess(gameState, ent) != unitAccess)
			continue;
		let dist = API3.SquareVectorDistance(ent.position(), unit.position());
		if (dist > distmin)
			continue;
		distmin = dist;
		nearest = ent;
	}
	if (!nearest)
		return false;

	if (!emergency)
	{
		garrisonManager.garrison(gameState, unit, nearest, PETRA.GarrisonManager.TYPE_PROTECTION);
		return true;
	}
	if (garrisonManager.numberOfGarrisonedSlots(nearest) >= nearest.garrisonMax()) // make room for this ent
		nearest.unload(nearest.garrisoned()[0]);

	garrisonManager.garrison(gameState, unit, nearest, nearest.buffHeal() ? PETRA.GarrisonManager.TYPE_PROTECTION : PETRA.GarrisonManager.TYPE_EMERGENCY);
	return true;
};

/**
 * Be more inclined to help an ally attacked by several enemies.
 */
PETRA.DefenseManager.prototype.GetCooperationLevel = function(ally)
{
	let cooperation = this.Config.personality.cooperative;
	if (this.attackedAllies[ally] && this.attackedAllies[ally] > 1)
		cooperation += 0.2 * (this.attackedAllies[ally] - 1);
	return cooperation;
};

/**
 * Switch a defense army into an attack if needed.
 */
PETRA.DefenseManager.prototype.switchToAttack = function(gameState, army)
{
	if (!army)
		return;
	for (let targetId of this.targetList)
	{
		let target = gameState.getEntityById(targetId);
		if (!target || !target.position() || !gameState.isPlayerEnemy(target.owner()))
			continue;
		let targetAccess = PETRA.getLandAccess(gameState, target);
		let targetPos = target.position();
		for (let entId of army.ownEntities)
		{
			let ent = gameState.getEntityById(entId);
			if (!ent || !ent.position() || PETRA.getLandAccess(gameState, ent) != targetAccess)
				continue;
			if (API3.SquareVectorDistance(targetPos, ent.position()) > 14400)
				continue;
			gameState.ai.HQ.attackManager.switchDefenseToAttack(gameState, target, { "armyID": army.ID, "uniqueTarget": true });
			return;
		}
	}
};

PETRA.DefenseManager.prototype.Serialize = function()
{
	let properties = {
		"targetList": this.targetList,
		"armyMergeSize": this.armyMergeSize,
		"attackingUnits": this.attackingUnits,
		"attackingArmies": this.attackingArmies,
		"attackedAllies": this.attackedAllies
	};

	let armies = [];
	for (let army of this.armies)
		armies.push(army.Serialize());

	return { "properties": properties, "armies": armies };
};

PETRA.DefenseManager.prototype.Deserialize = function(gameState, data)
{
	for (let key in data.properties)
		this[key] = data.properties[key];

	this.armies = [];
	for (let dataArmy of data.armies)
	{
		let army = new PETRA.DefenseArmy(gameState, []);
		army.Deserialize(dataArmy);
		this.armies.push(army);
	}
};