Source: ai/petra/baseManager.js

var PETRA = function(m)
{
/* Base Manager
 * Handles lower level economic stuffs.
 * Some tasks:
	-tasking workers: gathering/hunting/building/repairing?/scouting/plans.
	-giving feedback/estimates on GR
	-achieving building stuff plans (scouting/getting ressource/building) or other long-staying plans, if I ever get any.
	-getting good spots for dropsites
	-managing dropsite use in the base
		> warning HQ if we'll need more space
	-updating whatever needs updating, keeping track of stuffs (rebuilding needs…)
 */

m.BaseManager = function(gameState, Config)
{
	this.Config = Config;
	this.ID = gameState.ai.uniqueIDs.bases++;

	// anchor building: seen as the main building of the base. Needs to have territorial influence
	this.anchor = undefined;
	this.anchorId = undefined;
	this.accessIndex = undefined;

	// Maximum distance (from any dropsite) to look for resources
	// 3 areas are used: from 0 to max/4, from max/4 to max/2 and from max/2 to max 
	this.maxDistResourceSquare = 360*360;

	this.constructing = false;
	// Defenders to train in this cc when its construction is finished 
	this.neededDefenders = ((this.Config.difficulty > 2) ? 3 + 2*(this.Config.difficulty - 3) : 0);

	// vector for iterating, to check one use the HQ map.
	this.territoryIndices = [];
};

m.BaseManager.prototype.init = function(gameState, state)
{
	if (state == "unconstructed")
		this.constructing = true;
	else if (state != "captured")
		this.neededDefenders = 0;
	this.workerObject = new m.Worker(this);
	// entitycollections
	this.units = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "base", this.ID));
	this.workers = this.units.filter(API3.Filters.byMetadata(PlayerID, "role", "worker"));
	this.buildings = gameState.getOwnStructures().filter(API3.Filters.byMetadata(PlayerID, "base", this.ID));	

	this.units.registerUpdates();
	this.workers.registerUpdates();
	this.buildings.registerUpdates();
	
	// array of entity IDs, with each being
	this.dropsites = {};
	this.dropsiteSupplies = {};
	this.gatherers = {};
	for (let type of this.Config.resources)
	{
		this.dropsiteSupplies[type] = {"nearby": [], "medium": [], "faraway": []};
		this.gatherers[type] = {"nextCheck": 0, "used": 0, "lost": 0};
	}
};

m.BaseManager.prototype.assignEntity = function(gameState, ent)
{
	ent.setMetadata(PlayerID, "base", this.ID);
	this.units.updateEnt(ent);
	this.workers.updateEnt(ent);
	this.buildings.updateEnt(ent);
	if (ent.resourceDropsiteTypes() && !ent.hasClass("Elephant"))
		this.assignResourceToDropsite(gameState, ent);
};

m.BaseManager.prototype.setAnchor = function(gameState, anchorEntity)
{
	if (!anchorEntity.hasClass("Structure") || !anchorEntity.hasTerritoryInfluence())
	{
		warn("Error: Petra base " + this.ID + " has been assigned an anchor building that has no territorial influence. Please report this on the forum.");
		return false;
	}
	this.anchor = anchorEntity;
	this.anchorId = anchorEntity.id();
	this.anchor.setMetadata(PlayerID, "base", this.ID);
	this.anchor.setMetadata(PlayerID, "baseAnchor", true);
	this.buildings.updateEnt(this.anchor);
	this.accessIndex = gameState.ai.accessibility.getAccessValue(this.anchor.position());
	gameState.ai.HQ.resetActiveBase();
	// in case some of our other bases were destroyed, reaffect these destroyed bases to this base
	for (let base of gameState.ai.HQ.baseManagers)
	{
		if (base.anchor || base.newbaseID)
			continue;
		base.newbaseID = this.ID;
	}
	return true;
};

m.BaseManager.prototype.checkEvents = function (gameState, events, queues)
{
	for (let evt of events.Destroy)
	{
		// let's check we haven't lost an important building here.
		if (evt && !evt.SuccessfulFoundation && evt.entityObj && evt.metadata && evt.metadata[PlayerID] &&
			evt.metadata[PlayerID].base && evt.metadata[PlayerID].base == this.ID)
		{
			let ent = evt.entityObj;
			if (ent.resourceDropsiteTypes() && !ent.hasClass("Elephant"))
				this.removeDropsite(gameState, ent);
			if (evt.metadata[PlayerID].baseAnchor && evt.metadata[PlayerID].baseAnchor === true)
				this.anchorLost(gameState, ent);
		}
	}

	for (let evt of events.OwnershipChanged)	// capture event
	{
		if (evt.from !== PlayerID)
			continue;
		let ent = gameState.getEntityById(evt.entity);
		if (!ent || ent.getMetadata(PlayerID, "base") !== this.ID)
			continue;
		if (ent.resourceDropsiteTypes() && !ent.hasClass("Elephant"))
			this.removeDropsite(gameState, ent);
		if (ent.getMetadata(PlayerID, "baseAnchor") === true)
			this.anchorLost(gameState, ent);
	}

	for (let evt of events.ConstructionFinished)
	{
		if (!evt || !evt.newentity)
			continue;
		let ent = gameState.getEntityById(evt.newentity);
		if (!ent)
			continue;
		if (evt.newentity == evt.entity)  // repaired building
			continue;
			
		if (ent.getMetadata(PlayerID, "base") == this.ID)
			if (ent.resourceDropsiteTypes() && !ent.hasClass("Elephant"))
				this.assignResourceToDropsite(gameState, ent);
	}

	for (let evt of events.Create)
	{
		if (!evt || !evt.entity)
			continue;
		let ent = gameState.getEntityById(evt.entity);
		if (!ent)
			continue;
		// do necessary stuff here
	}

	for (let evt of events.EntityRenamed)
	{
		if (!this.anchorId || this.anchorId !== evt.entity)
			continue;
		this.anchorId = evt.newentity;
		this.anchor = gameState.getEntityById(evt.newentity);
		gameState.ai.HQ.resetActiveBase();
	}
};

/* we lost our anchor. Let's reaffect our units and buildings */
m.BaseManager.prototype.anchorLost = function (gameState, ent)
{
	this.anchor = undefined;
	this.anchorId = undefined;
	this.neededDefenders = 0;
	let bestbase = m.getBestBase(gameState, ent);
	this.newbaseID = bestbase.ID;
	for (let entity of this.units.values())
		bestbase.assignEntity(gameState, entity);
	for (let entity of this.buildings.values())
		bestbase.assignEntity(gameState, entity);
	gameState.ai.HQ.resetActiveBase();
	gameState.ai.HQ.updateTerritories(gameState);
};

/**
 * Assign the resources around the dropsites of this basis in three areas according to distance, and sort them in each area.
 * Moving resources (animals) and buildable resources (fields) are treated elsewhere.
 */
m.BaseManager.prototype.assignResourceToDropsite = function (gameState, dropsite)
{
	if (this.dropsites[dropsite.id()])
	{
		if (this.Config.debug > 0)
			warn("assignResourceToDropsite: dropsite already in the list. Should never happen");
		return;
	}

	let accessIndex = this.accessIndex;
	let dropsitePos = dropsite.position();
	let dropsiteId = dropsite.id();
	this.dropsites[dropsiteId] = true;

	if (this.ID === gameState.ai.HQ.baseManagers[0].ID)
	{
		accessIndex = dropsite.getMetadata(PlayerID, "access");
		if (!accessIndex)
		{
			accessIndex = gameState.ai.accessibility.getAccessValue(dropsitePos);
			dropsite.setMetadata(PlayerID, "access", accessIndex);
		}
	}

	let maxDistResourceSquare = this.maxDistResourceSquare;
	for (let type of dropsite.resourceDropsiteTypes())
	{
		let resources = gameState.getResourceSupplies(type);
		if (!resources.length)
			continue;

		let nearby = this.dropsiteSupplies[type].nearby;
		let medium = this.dropsiteSupplies[type].medium;
		let faraway = this.dropsiteSupplies[type].faraway;

		resources.forEach(function(supply)
		{
			if (!supply.position())
				return;
			if (supply.hasClass("Animal"))    // moving resources are treated differently
				return;
			if (supply.hasClass("Field"))     // fields are treated separately
				return;
			if (supply.resourceSupplyType().generic === "treasure")  // treasures are treated separately
				return;
			// quick accessibility check
			let access = supply.getMetadata(PlayerID, "access");
			if (!access)
			{
				access = gameState.ai.accessibility.getAccessValue(supply.position());
				supply.setMetadata(PlayerID, "access", access);
			}
			if (access !== accessIndex)
				return;

			let dist = API3.SquareVectorDistance(supply.position(), dropsitePos);
			if (dist < maxDistResourceSquare)
			{
				if (dist < maxDistResourceSquare/16)        // distmax/4
					nearby.push({ "dropsite": dropsiteId, "id": supply.id(), "ent": supply, "dist": dist }); 
				else if (dist < maxDistResourceSquare/4)    // distmax/2
					medium.push({ "dropsite": dropsiteId, "id": supply.id(), "ent": supply, "dist": dist });
				else
					faraway.push({ "dropsite": dropsiteId, "id": supply.id(), "ent": supply, "dist": dist });
			}
		});

		nearby.sort((r1, r2) => r1.dist - r2.dist);
		medium.sort((r1, r2) => r1.dist - r2.dist);
		faraway.sort((r1, r2) => r1.dist - r2.dist);

/*		var debug = false;
		if (debug)
		{
			faraway.forEach(function(res){ 
				Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [2,0,0]});
			});
			medium.forEach(function(res){ 
				Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [0,2,0]});
			});
			nearby.forEach(function(res){ 
				Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [res.ent.id()], "rgb": [0,0,2]});
			});
		} */
	}

	// Allows all allies to use this dropsite except if base anchor to be sure to keep
	// a minimum of resources for this base
	Engine.PostCommand(PlayerID, {
		"type": "set-dropsite-sharing",
		"entities": [dropsiteId],
		"shared": dropsiteId !== this.anchorId
	});
};

// completely remove the dropsite resources from our list.
m.BaseManager.prototype.removeDropsite = function (gameState, ent)
{
	if (!ent.id())
		return;

	var removeSupply = function(entId, supply){
		for (let i = 0; i < supply.length; ++i)
		{
			// exhausted resource, remove it from this list
			if (!supply[i].ent || !gameState.getEntityById(supply[i].id))
				supply.splice(i--, 1);
			// resource assigned to the removed dropsite, remove it
			else if (supply[i].dropsite == entId)
				supply.splice(i--, 1);
		}
	};

	for (let type in this.dropsiteSupplies)
	{
		removeSupply(ent.id(), this.dropsiteSupplies[type].nearby);
		removeSupply(ent.id(), this.dropsiteSupplies[type].medium);
		removeSupply(ent.id(), this.dropsiteSupplies[type].faraway);
	}

	this.dropsites[ent.id()] = undefined;
	return;
};

// Returns the position of the best place to build a new dropsite for the specified resource 
// TODO check dropsites of other bases ... may-be better to do a simultaneous liste of dp and foundations
m.BaseManager.prototype.findBestDropsiteLocation = function(gameState, resource)
{
	
	var template = gameState.getTemplate(gameState.applyCiv("structures/{civ}_storehouse"));
	var halfSize = 0;
	if (template.get("Footprint/Square"))
		halfSize = Math.max(+template.get("Footprint/Square/@depth"), +template.get("Footprint/Square/@width")) / 2;
	else if (template.get("Footprint/Circle"))
		halfSize = +template.get("Footprint/Circle/@radius");

	// This builds a map. The procedure is fairly simple. It adds the resource maps
	//	(which are dynamically updated and are made so that they will facilitate DP placement)
	// Then checks for a good spot in the territory. If none, and town/city phase, checks outside
	// The AI will currently not build a CC if it wouldn't connect with an existing CC.

	var obstructions = m.createObstructionMap(gameState, this.accessIndex, template);

	var dpEnts = gameState.getOwnEntitiesByClass("Storehouse", true).toEntityArray();
	var ccEnts = gameState.getOwnEntitiesByClass("CivCentre", true).toEntityArray();

	var bestIdx;
	var bestVal = 0;
	var radius = Math.ceil(template.obstructionRadius() / obstructions.cellSize);

	var territoryMap = gameState.ai.HQ.territoryMap;
	var width = territoryMap.width;
	var cellSize = territoryMap.cellSize;
    
	for (let j of this.territoryIndices)
	{
		let i = territoryMap.getNonObstructedTile(j, radius, obstructions);
		if (i < 0)  // no room around
			continue;

		// we add 3 times the needed resource and once the other two (not food)
		let total = 0;
		for (let res in gameState.sharedScript.resourceMaps)
		{
			if (res === "food")
				continue;
			total += gameState.sharedScript.resourceMaps[res].map[j];
			if (res === resource)
				total += 2*gameState.sharedScript.resourceMaps[res].map[j];
		}

		total = 0.7*total;   // Just a normalisation factor as the locateMap is limited to 255
		if (total <= bestVal)
			continue;

		let pos = [cellSize * (j%width+0.5), cellSize * (Math.floor(j/width)+0.5)];

		for (let dp of dpEnts)
		{
			let dpPos = dp.position();
			if (!dpPos)
				continue;
			let dist = API3.SquareVectorDistance(dpPos, pos);
			if (dist < 3600)
			{
				total = 0;
				break;
			}
			else if (dist < 6400)
				total *= (Math.sqrt(dist)-60)/20;
		}
		if (total <= bestVal)
			continue;

		for (let cc of ccEnts)
		{
			let ccPos = cc.position();
			if (!ccPos)
				continue;
			let dist = API3.SquareVectorDistance(ccPos, pos);
			if (dist < 3600)
			{
				total = 0;
				break;
			}
			else if (dist < 6400)
				total *= (Math.sqrt(dist)-60)/20;
		}
		if (total <= bestVal)
			continue;
		if (gameState.ai.HQ.isDangerousLocation(gameState, pos, halfSize))
			continue;
		bestVal = total;
		bestIdx = i;
	}

	if (this.Config.debug > 2)
		warn(" for dropsite best is " + bestVal);

	if (bestVal <= 0)
		return {"quality": bestVal, "pos": [0, 0]};

	var x = (bestIdx % obstructions.width + 0.5) * obstructions.cellSize;
	var z = (Math.floor(bestIdx / obstructions.width) + 0.5) * obstructions.cellSize;
	return {"quality": bestVal, "pos": [x, z]};
};

m.BaseManager.prototype.getResourceLevel = function (gameState, type, nearbyOnly = false)
{
	var count = 0;
	var check = {};
	for (let supply of this.dropsiteSupplies[type].nearby)
	{
		if (check[supply.id])    // avoid double counting as same resource can appear several time
			continue;
		check[supply.id] = true;
		count += supply.ent.resourceSupplyAmount();
	}
	if (nearbyOnly)
		return count;

	for (let supply of this.dropsiteSupplies[type].medium)
	{
		if (check[supply.id])
			continue;
		check[supply.id] = true;
		count += 0.6*supply.ent.resourceSupplyAmount();
	}
	return count;
};

// check our resource levels and react accordingly
m.BaseManager.prototype.checkResourceLevels = function (gameState, queues)
{
	for (let type of this.Config.resources)
	{
		if (type == "food")
		{
			if (gameState.ai.HQ.canBuild(gameState, "structures/{civ}_field"))	// let's see if we need to add new farms.
			{
				let count = this.getResourceLevel(gameState, type, (gameState.currentPhase() > 1));  // animals are not accounted
				let numFarms = gameState.getOwnStructures().filter(API3.Filters.byClass("Field")).length;  // including foundations
				let numQueue = queues.field.countQueuedUnits();

				// TODO  if not yet farms, add a check on time used/lost and build farmstead if needed
				if (numFarms + numQueue === 0)	// starting game, rely on fruits as long as we have enough of them
				{
					if (count < 600)
						queues.field.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_field", { "base": this.ID }));
				}
				else
				{
					let numFound = gameState.getOwnFoundations().filter(API3.Filters.byClass("Field")).length;
					let goal = this.Config.Economy.provisionFields;
					if (gameState.ai.HQ.saveResources || gameState.ai.HQ.saveSpace || count > 300 || numFarms > 5)
						goal = Math.max(goal-1, 1);
					if (numFound + numQueue < goal)
						queues.field.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_field", { "base": this.ID }));
				}
			}
		}
		else if (!queues.dropsites.length() && !gameState.getOwnFoundations().filter(API3.Filters.byClass("Storehouse")).length)
		{
			if (gameState.ai.playedTurn > this.gatherers[type].nextCheck)
			{
				var self = this;
				this.gatherersByType(gameState, type).forEach(function (ent) {
					if (ent.unitAIState() === "INDIVIDUAL.GATHER.GATHERING")
						++self.gatherers[type].used;
					else if (ent.unitAIState() === "INDIVIDUAL.RETURNRESOURCE.APPROACHING")
						++self.gatherers[type].lost;
				});
				// TODO  add also a test on remaining resources
				let total = this.gatherers[type].used + this.gatherers[type].lost;
				if (total > 150 || (total > 60 && type !== "wood"))
				{
					let ratio = this.gatherers[type].lost / total;
					if (ratio > 0.15)
					{
						let newDP = this.findBestDropsiteLocation(gameState, type);
						if (newDP.quality > 50 && gameState.ai.HQ.canBuild(gameState, "structures/{civ}_storehouse"))
							queues.dropsites.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_storehouse", { "base": this.ID, "type": type }, newDP.pos));
						else if (!gameState.getOwnFoundations().filter(API3.Filters.byClass("CivCentre")).length && !queues.civilCentre.length())
						{
							// No good dropsite, try to build a new base if no base already planned,
							// and if not possible, be less strict on dropsite quality
							if (!gameState.ai.HQ.buildNewBase(gameState, queues, type) && newDP.quality > Math.min(25, 50*0.15/ratio) &&
								gameState.ai.HQ.canBuild(gameState, "structures/{civ}_storehouse"))
								queues.dropsites.addPlan(new m.ConstructionPlan(gameState, "structures/{civ}_storehouse", { "base": this.ID, "type": type }, newDP.pos));
						}
					}
					this.gatherers[type].nextCheck = gameState.ai.playedTurn + 20;
					this.gatherers[type].used = 0;
					this.gatherers[type].lost = 0;
				}
				else if (total === 0)
					this.gatherers[type].nextCheck = gameState.ai.playedTurn + 10;
			}
		}
		else
		{
			this.gatherers[type].nextCheck = gameState.ai.playedTurn;
			this.gatherers[type].used = 0;
			this.gatherers[type].lost = 0;
		}
	}
	
};

// Adds the estimated gather rates from this base to the currentRates
m.BaseManager.prototype.getGatherRates = function(gameState, currentRates)
{
	for (let res in currentRates)
	{
		// I calculate the exact gathering rate for each unit.
		// I must then lower that to account for travel time.
		// Given that the faster you gather, the more travel time matters,
		// I use some logarithms.
		// TODO: this should take into account for unit speed and/or distance to target

		this.gatherersByType(gameState, res).forEach(function (ent) {
			if (ent.isIdle() || !ent.position())
				return;		
			let gRate = ent.currentGatherRate();
			if (gRate)
				currentRates[res] += Math.log(1+gRate)/1.1;
		});
		if (res === "food")
		{
			this.workersBySubrole(gameState, "hunter").forEach(function (ent) {
				if (ent.isIdle() || !ent.position())
					return;
				let gRate = ent.currentGatherRate();
				if (gRate)
					currentRates[res] += Math.log(1+gRate)/1.1;
			});
			this.workersBySubrole(gameState, "fisher").forEach(function (ent) {
				if (ent.isIdle() || !ent.position())
					return;
				let gRate = ent.currentGatherRate();
				if (gRate)
					currentRates[res] += Math.log(1+gRate)/1.1;
			});
		}
	}
};

m.BaseManager.prototype.assignRolelessUnits = function(gameState, roleless)
{
	if (!roleless)
		roleless = this.units.filter(API3.Filters.not(API3.Filters.byHasMetadata(PlayerID, "role"))).values();

	for (let ent of roleless)
	{
		if (ent.hasClass("Worker") || ent.hasClass("CitizenSoldier") || ent.hasClass("FishingBoat"))
			ent.setMetadata(PlayerID, "role", "worker");
		else if (ent.hasClass("Support") && ent.hasClass("Elephant"))
			ent.setMetadata(PlayerID, "role", "worker");
	}
};

// If the numbers of workers on the resources is unbalanced then set some of workers to idle so
// they can be reassigned by reassignIdleWorkers.
// TODO: actually this probably should be in the HQ.
m.BaseManager.prototype.setWorkersIdleByPriority = function(gameState)
{
	// change resource only towards one which is more needed, and if changing will not change this order
	var nb = 1;    // no more than 1 change per turn (otherwise we should update the rates)
	var mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState);
	var sumWanted = 0;
	var sumCurrent = 0;
	for (let need of mostNeeded)
	{
		sumWanted += need.wanted;
		sumCurrent += need.current;
	}
	var scale = 1;
	if (sumWanted > 0)
		scale = sumCurrent / sumWanted;

	for (let i = mostNeeded.length-1; i > 0; --i)
	{
		let lessNeed = mostNeeded[i];
		for (let j = 0; j < i; ++j) 
		{
			let moreNeed = mostNeeded[j];
			let lastFailed = gameState.ai.HQ.lastFailedGather[moreNeed.type];
			if (lastFailed && gameState.ai.elapsedTime - lastFailed < 20)
				continue;
			// If we assume a mean rate of 0.5 per gatherer, this diff should be > 1
			// but we require a bit more to avoid too frequent changes
			if ((scale*moreNeed.wanted - moreNeed.current) - (scale*lessNeed.wanted - lessNeed.current) > 1.5)
			{
				let only;
				// in average, females are less efficient for stone and metal, and citizenSoldiers for food
				let gatherers = this.gatherersByType(gameState, lessNeed.type);
				if (lessNeed.type === "food" && gatherers.filter(API3.Filters.byClass("CitizenSoldier")).length)
					only = "CitizenSoldier";
				else if ((lessNeed.type === "stone" || lessNeed.type === "metal") && moreNeed.type !== "stone" && moreNeed.type !== "metal" &&
					gatherers.filter(API3.Filters.byClass("Female")).length) 
					only = "Female";

				gatherers.forEach( function (ent) {
					if (!ent.canGather(moreNeed.type))
						return;
					if (nb == 0)
						return;
					if (only && !ent.hasClass(only))
						return;
					--nb;
					ent.stopMoving();
					ent.setMetadata(PlayerID, "gather-type", moreNeed.type);
					gameState.ai.HQ.AddTCResGatherer(moreNeed.type);
				});
				if (nb == 0)
					return;
			}
		}
	}
};

m.BaseManager.prototype.reassignIdleWorkers = function(gameState, idleWorkers)
{
	// Search for idle workers, and tell them to gather resources based on demand
	if (!idleWorkers)
	{
		let filter = API3.Filters.byMetadata(PlayerID, "subrole", "idle");
		idleWorkers = gameState.updatingCollection("idle-workers-base-" + this.ID, filter, this.workers).values();
	}
	
	for (let ent of idleWorkers)
	{
		// Check that the worker isn't garrisoned
		if (!ent.position())
			continue;
		// Support elephant can only be builders
		if (ent.hasClass("Support") && ent.hasClass("Elephant"))
		{
			ent.setMetadata(PlayerID, "subrole", "idle");
			continue;
		}

		if (ent.hasClass("Worker"))
		{
			// Just emergency repairing here. It is better managed in assignToFoundations
			if (ent.isBuilder() && this.anchor && this.anchor.needsRepair() &&
				gameState.getOwnEntitiesByMetadata("target-foundation", this.anchor.id()).length < 2)
				ent.repair(this.anchor);
			else if (ent.isGatherer())
			{
				let mostNeeded = gameState.ai.HQ.pickMostNeededResources(gameState);
				for (let needed of mostNeeded)
				{
					if (!ent.canGather(needed.type))
						continue;
					let lastFailed = gameState.ai.HQ.lastFailedGather[needed.type];
					if (lastFailed && gameState.ai.elapsedTime - lastFailed < 20)
						continue;
					ent.setMetadata(PlayerID, "subrole", "gatherer");
					ent.setMetadata(PlayerID, "gather-type", needed.type);
					gameState.ai.HQ.AddTCResGatherer(needed.type);
					break;
				}
			}
		}
		else if (ent.hasClass("Cavalry"))
			ent.setMetadata(PlayerID, "subrole", "hunter");
		else if (ent.hasClass("FishingBoat"))
			ent.setMetadata(PlayerID, "subrole", "fisher");
	}
};

m.BaseManager.prototype.workersBySubrole = function(gameState, subrole)
{
	return gameState.updatingCollection("subrole-" + subrole +"-base-" + this.ID, API3.Filters.byMetadata(PlayerID, "subrole", subrole), this.workers);
};

m.BaseManager.prototype.gatherersByType = function(gameState, type)
{
	return gameState.updatingCollection("workers-gathering-" + type +"-base-" + this.ID, API3.Filters.byMetadata(PlayerID, "gather-type", type), this.workersBySubrole(gameState, "gatherer"));
};


// returns an entity collection of workers.
// They are idled immediatly and their subrole set to idle.
m.BaseManager.prototype.pickBuilders = function(gameState, workers, number)
{
	var availableWorkers = this.workers.filter(function (ent) {
		if (!ent.position() || !ent.isBuilder())
			return false;
		if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3)
			return false;
		if (ent.getMetadata(PlayerID, "transport"))
			return false;
		return true;
	}).toEntityArray();
	availableWorkers.sort(function (a,b) {
		let vala = 0;
		let valb = 0;
		if (a.getMetadata(PlayerID, "subrole") == "builder")
			vala = 100;
		if (b.getMetadata(PlayerID, "subrole") == "builder")
			valb = 100;
		if (a.getMetadata(PlayerID, "subrole") == "idle")
			vala = -50;
		if (b.getMetadata(PlayerID, "subrole") == "idle")
			valb = -50;
		if (a.getMetadata(PlayerID, "plan") === undefined)
			vala = -20;
		if (b.getMetadata(PlayerID, "plan") === undefined)
			valb = -20;
		return (vala - valb);
	});
	var needed = Math.min(number, availableWorkers.length - 3);
	for (let i = 0; i < needed; ++i)
	{
		availableWorkers[i].stopMoving();
		availableWorkers[i].setMetadata(PlayerID, "subrole", "idle");
		workers.addEnt(availableWorkers[i]);
	}
	return;
};

m.BaseManager.prototype.assignToFoundations = function(gameState, noRepair)
{
	// If we have some foundations, and we don't have enough builder-workers,
	// try reassigning some other workers who are nearby	
	// AI tries to use builders sensibly, not completely stopping its econ.
	
	// TODO: this is not perfect performance-wise.
	var foundations = this.buildings.filter(API3.Filters.and(API3.Filters.isFoundation(),API3.Filters.not(API3.Filters.byClass("Field")))).toEntityArray();
	
	var damagedBuildings = this.buildings.filter(ent => ent.foundationProgress() === undefined && ent.needsRepair());
	
	// Check if nothing to build
	if (!foundations.length && !damagedBuildings.length)
		return;

	var workers = this.workers.filter(ent => ent.isBuilder());
	var builderWorkers = this.workersBySubrole(gameState, "builder");
	var idleBuilderWorkers = builderWorkers.filter(API3.Filters.isIdle());

	// if we're constructing and we have the foundations to our base anchor, only try building that.
	if (this.constructing == true && this.buildings.filter(API3.Filters.and(API3.Filters.isFoundation(), API3.Filters.byMetadata(PlayerID, "baseAnchor", true))).length != 0)
	{
		foundations = this.buildings.filter(API3.Filters.byMetadata(PlayerID, "baseAnchor", true)).toEntityArray();
		let tID = foundations[0].id();
		workers.forEach(function (ent) {
			let target = ent.getMetadata(PlayerID, "target-foundation");
			if (target && target != tID)
			{
				ent.stopMoving();
				ent.setMetadata(PlayerID, "target-foundation", tID);
			}
		});
	}

	if (workers.length < 2)
	{
		let fromOtherBase = gameState.ai.HQ.bulkPickWorkers(gameState, this, 2);
		if(fromOtherBase)
		{
			let baseID = this.ID;
			fromOtherBase.forEach(function (worker) {
				worker.setMetadata(PlayerID, "base", baseID);
				worker.setMetadata(PlayerID, "subrole", "builder");
				workers.updateEnt(worker);
				builderWorkers.updateEnt(worker);
				idleBuilderWorkers.updateEnt(worker);
			});
		}
	}

	var builderTot = builderWorkers.length - idleBuilderWorkers.length;	
	
	for (let target of foundations)
	{
		if (target.hasClass("Field"))
			continue; // we do not build fields

		if (gameState.ai.HQ.isNearInvadingArmy(target.position()))
			if (!target.hasClass("CivCentre") && !target.hasClass("StoneWall") && (!target.hasClass("Wonder") || gameState.getGameType() !== "wonder"))
				continue;

		// if our territory has shrinked since this foundation was positioned, do not build it
		if (m.isNotWorthBuilding(gameState, target))
			continue;

		let assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target.id()).length;
		let maxTotalBuilders = Math.ceil(workers.length * 0.2);
		if (target.hasClass("House") && gameState.getPopulationLimit() < (gameState.getPopulation() + 5) &&
			gameState.getPopulationLimit() < gameState.getPopulationMax())
			maxTotalBuilders = maxTotalBuilders + 2;
		let targetNB = 2;
		if (target.hasClass("House") || target.hasClass("DropsiteWood"))
			targetNB = 3;
		else if (target.hasClass("Barracks") || target.hasClass("DefenseTower") || target.hasClass("Market"))
			targetNB = 4;
		else if (target.hasClass("Fortress"))
			targetNB = 7;
		if (target.getMetadata(PlayerID, "baseAnchor") == true || (target.hasClass("Wonder") && gameState.getGameType() === "wonder"))
		{
			targetNB = 15;
			maxTotalBuilders = Math.max(maxTotalBuilders, 15);
		}
		// if no base yet, everybody should build
		if (gameState.ai.HQ.numActiveBase() === 0)
		{
			targetNB = workers.length;
			maxTotalBuilders = targetNB;
		}

		if (assigned < targetNB)
		{
			idleBuilderWorkers.forEach(function(ent) {
				if (ent.getMetadata(PlayerID, "target-foundation") !== undefined)
					return;
				if (assigned >= targetNB || !ent.position() || API3.SquareVectorDistance(ent.position(), target.position()) > 40000)
					return;
				assigned++;
				builderTot++;
				ent.setMetadata(PlayerID, "target-foundation", target.id());
			});
			if (assigned < targetNB && builderTot < maxTotalBuilders)
			{
				let nonBuilderWorkers = workers.filter(function(ent) {
					if (ent.getMetadata(PlayerID, "subrole") === "builder")
						return false;
					if (!ent.position())
						return false;
					if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3)
						return false;
					if (ent.getMetadata(PlayerID, "transport"))
						return false;
					return true;
				}).toEntityArray();
				let time = target.buildTime();
				nonBuilderWorkers.sort(function (workerA,workerB)
				{
					let coeffA = API3.SquareVectorDistance(target.position(),workerA.position());
					// elephant moves slowly, so when far away they are only useful if build time is long
					if (workerA.hasClass("Elephant"))
						coeffA *= 0.5 * (1 + (Math.sqrt(coeffA)/150)*(30/time));
					else if (workerA.getMetadata(PlayerID, "gather-type") === "food")
						coeffA *= 3;
					let coeffB = API3.SquareVectorDistance(target.position(),workerB.position());
					if (workerB.hasClass("Elephant"))
						coeffB *= 0.5 * (1 + (Math.sqrt(coeffB)/150)*(30/time));
					else if (workerB.getMetadata(PlayerID, "gather-type") === "food")
						coeffB *= 3;
					return (coeffA - coeffB);						
				});
				let current = 0;
				let nonBuilderTot = nonBuilderWorkers.length;
				while (assigned < targetNB && builderTot < maxTotalBuilders && current < nonBuilderTot)
				{
					assigned++;
					builderTot++;
					let ent = nonBuilderWorkers[current++];
					ent.stopMoving();
					ent.setMetadata(PlayerID, "subrole", "builder");
					ent.setMetadata(PlayerID, "target-foundation", target.id());
				}
			}
		}
	}

	for (let target of damagedBuildings.values())
	{
		// don't repair if we're still under attack, unless it's a vital (civcentre or wall) building that's getting destroyed.
		if (gameState.ai.HQ.isNearInvadingArmy(target.position()))
			if (target.healthLevel() > 0.5 ||
				(!target.hasClass("CivCentre") && !target.hasClass("StoneWall") && (!target.hasClass("Wonder") || gameState.getGameType() !== "wonder")))
				continue;
		else if (noRepair && !target.hasClass("CivCentre"))
			continue;
		
		if (target.decaying())
			continue;
		
		let assigned = gameState.getOwnEntitiesByMetadata("target-foundation", target.id()).length;
		let maxTotalBuilders = Math.ceil(workers.length * 0.2);
		let targetNB = 1;
		if (target.hasClass("Fortress"))
			targetNB = 3;
		if (target.getMetadata(PlayerID, "baseAnchor") == true || (target.hasClass("Wonder") && gameState.getGameType() === "wonder"))
		{
			maxTotalBuilders = Math.ceil(workers.length * 0.3);
			targetNB = 5;
			if (target.healthLevel() < 0.3)
			{
				maxTotalBuilders = Math.ceil(workers.length * 0.6);
				targetNB = 7;
			}

		}

		if (assigned < targetNB)
		{
			idleBuilderWorkers.forEach(function(ent) {
				if (ent.getMetadata(PlayerID, "target-foundation") !== undefined)
					return;
				if (assigned >= targetNB || !ent.position() || API3.SquareVectorDistance(ent.position(), target.position()) > 40000)
					return;
				assigned++;
				builderTot++;
				ent.setMetadata(PlayerID, "target-foundation", target.id());
			});
			if (assigned < targetNB && builderTot < maxTotalBuilders)
			{
				let nonBuilderWorkers = workers.filter(function(ent) {
					if (ent.getMetadata(PlayerID, "subrole") === "builder")
						return false;
					if (!ent.position())
						return false;
					if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3)
						return false;
					if (ent.getMetadata(PlayerID, "transport"))
						return false;
					return true;
				});
				let num = Math.min(nonBuilderWorkers.length, targetNB-assigned);
				let nearestNonBuilders = nonBuilderWorkers.filterNearest(target.position(), num);
				
				nearestNonBuilders.forEach(function(ent) {
					assigned++;
					builderTot++;
					ent.stopMoving();
					ent.setMetadata(PlayerID, "subrole", "builder");
					ent.setMetadata(PlayerID, "target-foundation", target.id());
				});
			}
		}
	}
};

m.BaseManager.prototype.update = function(gameState, queues, events)
{
	if (this.ID === gameState.ai.HQ.baseManagers[0].ID)	// base for unaffected units
	{
		// if some active base, reassigns the workers/buildings
		// otherwise look for anything useful to do, i.e. treasures to gather
		if (gameState.ai.HQ.numActiveBase() > 0)
		{
			for (let ent of this.units.values())
				m.getBestBase(gameState, ent).assignEntity(gameState, ent);
			for (let ent of this.buildings.values())
			{
				if (ent.resourceDropsiteTypes() && !ent.hasClass("Elephant"))
					this.removeDropsite(gameState, ent);
				m.getBestBase(gameState, ent).assignEntity(gameState, ent);
			}
		}
		else if (gameState.ai.HQ.canBuildUnits)
		{
			this.assignToFoundations(gameState);
			if (gameState.ai.playedTurn % 4 === 0)
				this.setWorkersIdleByPriority(gameState);
			this.assignRolelessUnits(gameState);
			this.reassignIdleWorkers(gameState);
			for (let ent of this.workers.values())
				this.workerObject.update(gameState, ent);
		}
		return;
	}

	if (!this.anchor)   // this base has been destroyed
	{
		// transfer possible remaining units (probably they were in training during previous transfers)
		if (this.newbaseID)
		{
			let newbaseID = this.newbaseID;
			for (let ent of this.units.values())
				ent.setMetadata(PlayerID, "base", newbaseID);
			for (let ent of this.buildings.values())
				ent.setMetadata(PlayerID, "base", newbaseID);
		}
		return;
	}

	if (this.anchor.getMetadata(PlayerID, "access") != this.accessIndex)
		API3.warn("Petra baseManager " + this.ID + " problem with accessIndex " + this.accessIndex +
			  " while metadata access is " + this.anchor.getMetadata(PlayerID, "access"));

	Engine.ProfileStart("Base update - base " + this.ID);

	this.checkResourceLevels(gameState, queues);
	this.assignToFoundations(gameState);

	if (this.constructing)
	{
		let owner = gameState.ai.HQ.territoryMap.getOwner(this.anchor.position());
		if(owner !== 0 && !gameState.isPlayerAlly(owner))
		{
			// we're in enemy territory. If we're too close from the enemy, destroy us.
			let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre"));
			for (let cc of ccEnts.values())
			{
				if (cc.owner() !== owner)
					continue;
				if (API3.SquareVectorDistance(cc.position(), this.anchor.position()) > 8000)
					continue;
				this.anchor.destroy();
				gameState.ai.HQ.resetActiveBase();
				break;
			}
		}
	}
	else if (this.neededDefenders && gameState.ai.HQ.trainEmergencyUnits(gameState, [this.anchor.position()]))
		--this.neededDefenders;

	if (gameState.ai.playedTurn % 2 === 0 && gameState.currentPhase() > 1)
		this.setWorkersIdleByPriority(gameState);

	this.assignRolelessUnits(gameState);
	this.reassignIdleWorkers(gameState);
	// check if workers can find something useful to do
	for (let ent of this.workers.values())
		this.workerObject.update(gameState, ent);

	Engine.ProfileStop();
};

m.BaseManager.prototype.Serialize = function()
{
	return {
		"ID": this.ID,
		"anchorId": this.anchorId,
		"accessIndex": this.accessIndex,
		"maxDistResourceSquare": this.maxDistResourceSquare,
		"constructing": this.constructing,
		"gatherers": this.gatherers,
		"neededDefenders": this.neededDefenders,
		"territoryIndices": this.territoryIndices
	};
};

m.BaseManager.prototype.Deserialize = function(gameState, data)
{
	for (let key in data)
		this[key] = data[key];

	this.anchor = ((this.anchorId !== undefined) ? gameState.getEntityById(this.anchorId) : undefined);
};

return m;

}(PETRA);