Source: ai/petra/entityExtend.js

var PETRA = function(m)
{

// returns some sort of DPS * health factor. If you specify a class, it'll use the modifiers against that class too.
m.getMaxStrength = function(ent, againstClass)
{
	var strength = 0.0;
	var attackTypes = ent.attackTypes();
	if (!attackTypes)
		return strength;

	for (let type of attackTypes)
	{
		if (type == "Slaughter" || type == "Charged")
			continue;

		let attackStrength = ent.attackStrengths(type);
		for (let str in attackStrength)
		{
			let val = parseFloat(attackStrength[str]);
			if (againstClass)
				val *= ent.getMultiplierAgainst(type, againstClass);
			switch (str)
			{
			case "crush":
				strength += (val * 0.085) / 3;
				break;
			case "hack":
				strength += (val * 0.075) / 3;
				break;
			case "pierce":
				strength += (val * 0.065) / 3;
				break;
			default:
				API3.warn("Petra: " + str + " unknown attackStrength in getMaxStrength");
			}
		}

		let attackRange = ent.attackRange(type);
		if (attackRange)
			strength += (attackRange.max * 0.0125) ;

		let attackTimes = ent.attackTimes(type);
		for (let str in attackTimes)
		{
			let val = parseFloat(attackTimes[str]);
			switch (str)
			{
			case "repeat":
				strength += (val / 100000);
				break;
			case "prepare":
				strength -= (val / 100000);
				break;
			default:
				API3.warn("Petra: " + str + " unknown attackTimes in getMaxStrength");
			}
		}
	}

	var armourStrength = ent.armourStrengths();
	for (let str in armourStrength)
	{
		let val = parseFloat(armourStrength[str]);
		switch (str)
		{
		case "crush":
			strength += (val * 0.085) / 3;
			break;
		case "hack":
			strength += (val * 0.075) / 3;
			break;
		case "pierce":
			strength += (val * 0.065) / 3;
			break;
		default:
			API3.warn("Petra: " + str + " unknown armourStrength in getMaxStrength");
		}
	}

	return strength * ent.maxHitpoints() / 100.0;
};

// Decide if we should try to capture or destroy
m.allowCapture = function(ent, target)
{
	return !target.hasClass("Siege") || !ent.hasClass("Melee") ||
		!target.isGarrisonHolder() || !target.garrisoned().length;
};

// Makes the worker deposit the currently carried resources at the closest accessible dropsite
m.returnResources = function(gameState, ent)
{
	if (!ent.resourceCarrying() || !ent.resourceCarrying().length || !ent.position())
		return false;

	var resource = ent.resourceCarrying()[0].type;

	var closestDropsite;
	var distmin = Math.min();
	var access = gameState.ai.accessibility.getAccessValue(ent.position());
	gameState.getOwnDropsites(resource).forEach(function(dropsite) {
		if (!dropsite.position() || dropsite.getMetadata(PlayerID, "access") !== access)
			return;
		let dist = API3.SquareVectorDistance(ent.position(), dropsite.position());
		if (dist > distmin)
			return;
		distmin = dist;
		closestDropsite = dropsite;
	});
	
	if (!closestDropsite)
		return false;	
	ent.returnResources(closestDropsite);
	return true;
};

// is supply full taking into account gatherers affected during this turn
m.IsSupplyFull = function(gameState, ent)
{
	if (ent.isFull() === true)
	    return true;
	var turnCache = gameState.ai.HQ.turnCache;
	var count = ent.resourceSupplyNumGatherers();
	if (turnCache.resourceGatherer && turnCache.resourceGatherer[ent.id()])
		count += turnCache.resourceGatherer[ent.id()];
	if (count >= ent.maxGatherers())
		return true;
	return false;
};

/**
 * get the best base (in terms of distance and accessIndex) for an entity
 */
m.getBestBase = function(gameState, ent)
{
	var pos = ent.position();
	if (!pos)
	{
		let holder = m.getHolder(gameState, ent);
		if (!holder || !holder.position())
		{
			API3.warn("Petra error: entity without position, but not garrisoned");
			m.dumpEntity(ent);
			return gameState.ai.HQ.baseManagers[0];
		}
		pos = holder.position();
	}
	var distmin = Math.min();
	var bestbase;
	var accessIndex = gameState.ai.accessibility.getAccessValue(pos);
	for (let base of gameState.ai.HQ.baseManagers)
	{
		if (!base.anchor)
			continue;
		let dist = API3.SquareVectorDistance(base.anchor.position(), pos);
		if (base.accessIndex !== accessIndex)
			dist += 100000000;
		if (dist > distmin)
			continue;
		distmin = dist;
		bestbase = base;
	}
	if (!bestbase)
		bestbase = gameState.ai.HQ.baseManagers[0];
	return bestbase;
};

m.getHolder = function(gameState, ent)
{
	for (let holder of gameState.getEntities().values())
	{
		if (holder.isGarrisonHolder() && holder.garrisoned().indexOf(ent.id()) !== -1)
			return holder;
	}
	return undefined;
};

/**
 * return true if it is not worth finishing this building (it would surely decay) 
 * TODO implement the other conditions
 */
m.isNotWorthBuilding = function(gameState, ent)
{
	if (gameState.ai.HQ.territoryMap.getOwner(ent.position()) !== PlayerID)
	{
		let buildTerritories = ent.buildTerritories();
		if (buildTerritories && (!buildTerritories.length || (buildTerritories.length === 1 && buildTerritories[0] === "own")))
			return true;
	}
	return false;
};

m.dumpEntity = function(ent)
{
	if (!ent)
		return;
	API3.warn(" >>> id " + ent.id() + " name " + ent.genericName() + " pos " + ent.position() +
		  " state " + ent.unitAIState());
	API3.warn(" base " + ent.getMetadata(PlayerID, "base") + " >>> role " + ent.getMetadata(PlayerID, "role") +
		  " subrole " + ent.getMetadata(PlayerID, "subrole"));
	API3.warn("owner " + ent.owner() + " health " + ent.hitpoints() + " healthMax " + ent.maxHitpoints());
	API3.warn(" garrisoning " + ent.getMetadata(PlayerID, "garrisoning") + " garrisonHolder " + ent.getMetadata(PlayerID, "garrisonHolder") +
		  " plan " + ent.getMetadata(PlayerID, "plan")	+ " transport " + ent.getMetadata(PlayerID, "transport") +
		  " gather-type " + ent.getMetadata(PlayerID, "gather-type") + " target-foundation " + ent.getMetadata(PlayerID, "target-foundation") +
		  " PartOfArmy " + ent.getMetadata(PlayerID, "PartOfArmy"));
};

return m;
}(PETRA);