/**
* @class
* This class makes a worker do as instructed by the economy manager
*/
PETRA.Worker = function(base)
{
this.ent = undefined;
this.base = base;
this.baseID = base.ID;
};
PETRA.Worker.ROLE_ATTACK = "attack";
PETRA.Worker.ROLE_TRADER = "trader";
PETRA.Worker.ROLE_SWITCH_TO_TRADER = "switchToTrader";
PETRA.Worker.ROLE_WORKER = "worker";
PETRA.Worker.ROLE_CRITICAL_ENT_GUARD = "criticalEntGuard";
PETRA.Worker.ROLE_CRITICAL_ENT_HEALER = "criticalEntHealer";
PETRA.Worker.SUBROLE_DEFENDER = "defender";
PETRA.Worker.SUBROLE_IDLE = "idle";
PETRA.Worker.SUBROLE_BUILDER = "builder";
PETRA.Worker.SUBROLE_COMPLETING = "completing";
PETRA.Worker.SUBROLE_WALKING = "walking";
PETRA.Worker.SUBROLE_ATTACKING = "attacking";
PETRA.Worker.SUBROLE_GATHERER = "gatherer";
PETRA.Worker.SUBROLE_HUNTER = "hunter";
PETRA.Worker.SUBROLE_FISHER = "fisher";
PETRA.Worker.SUBROLE_GARRISONING = "garrisoning";
PETRA.Worker.prototype.update = function(gameState, ent)
{
if (!ent.position() || ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3)
return;
let subrole = ent.getMetadata(PlayerID, "subrole");
// If we are waiting for a transport or we are sailing, just wait
if (ent.getMetadata(PlayerID, "transport") !== undefined)
{
// Except if builder with their foundation destroyed, in which case cancel the transport if not yet on board
if (subrole === PETRA.Worker.SUBROLE_BUILDER && ent.getMetadata(PlayerID, "target-foundation") !== undefined)
{
let plan = gameState.ai.HQ.navalManager.getPlan(ent.getMetadata(PlayerID, "transport"));
let target = gameState.getEntityById(ent.getMetadata(PlayerID, "target-foundation"));
if (!target && plan && plan.state === PETRA.TransportPlan.BOARDING && ent.position())
plan.removeUnit(gameState, ent);
}
// and gatherer if there are no more dropsite accessible in the base the ent is going to
if (subrole === PETRA.Worker.SUBROLE_GATHERER || subrole === PETRA.Worker.SUBROLE_HUNTER)
{
let plan = gameState.ai.HQ.navalManager.getPlan(ent.getMetadata(PlayerID, "transport"));
if (plan.state === PETRA.TransportPlan.BOARDING && ent.position())
{
let hasDropsite = false;
let gatherType = ent.getMetadata(PlayerID, "gather-type") || "food";
for (let structure of gameState.getOwnStructures().values())
{
if (PETRA.getLandAccess(gameState, structure) != plan.endIndex)
continue;
let resourceDropsiteTypes = PETRA.getBuiltEntity(gameState, structure).resourceDropsiteTypes();
if (!resourceDropsiteTypes || resourceDropsiteTypes.indexOf(gatherType) == -1)
continue;
hasDropsite = true;
break;
}
if (!hasDropsite)
{
for (let unit of gameState.getOwnUnits().filter(API3.Filters.byClass("Support")).values())
{
if (!unit.position() || PETRA.getLandAccess(gameState, unit) != plan.endIndex)
continue;
let resourceDropsiteTypes = unit.resourceDropsiteTypes();
if (!resourceDropsiteTypes || resourceDropsiteTypes.indexOf(gatherType) == -1)
continue;
hasDropsite = true;
break;
}
}
if (!hasDropsite)
plan.removeUnit(gameState, ent);
}
}
if (ent.getMetadata(PlayerID, "transport") !== undefined)
return;
}
this.entAccess = PETRA.getLandAccess(gameState, ent);
// Base for unassigned entities has no accessIndex, so take the one from the entity.
if (this.baseID == gameState.ai.HQ.basesManager.baselessBase().ID)
this.baseAccess = this.entAccess;
else
this.baseAccess = this.base.accessIndex;
if (subrole == undefined) // subrole may-be undefined after a transport, garrisoning, army, ...
{
ent.setMetadata(PlayerID, "subrole", PETRA.Worker.SUBROLE_IDLE);
this.base.reassignIdleWorkers(gameState, [ent]);
this.update(gameState, ent);
return;
}
this.ent = ent;
let unitAIState = ent.unitAIState();
if ((subrole === PETRA.Worker.SUBROLE_HUNTER || subrole === PETRA.Worker.SUBROLE_GATHERER) &&
(unitAIState == "INDIVIDUAL.GATHER.GATHERING" || unitAIState == "INDIVIDUAL.GATHER.APPROACHING" ||
unitAIState == "INDIVIDUAL.COMBAT.APPROACHING"))
{
if (this.isInaccessibleSupply(gameState))
{
if (this.retryWorking(gameState, subrole))
return;
ent.stopMoving();
}
if (unitAIState == "INDIVIDUAL.COMBAT.APPROACHING" && ent.unitAIOrderData().length)
{
let orderData = ent.unitAIOrderData()[0];
if (orderData && orderData.target)
{
// Check that we have not drifted too far when hunting
let target = gameState.getEntityById(orderData.target);
if (target && target.resourceSupplyType() && target.resourceSupplyType().generic == "food")
{
let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(target.position());
if (gameState.isPlayerEnemy(territoryOwner))
{
if (this.retryWorking(gameState, subrole))
return;
ent.stopMoving();
}
else if (!gameState.isPlayerAlly(territoryOwner))
{
let distanceSquare = PETRA.isFastMoving(ent) ? 90000 : 30000;
let targetAccess = PETRA.getLandAccess(gameState, target);
let foodDropsites = gameState.playerData.hasSharedDropsites ?
gameState.getAnyDropsites("food") : gameState.getOwnDropsites("food");
let hasFoodDropsiteWithinDistance = false;
for (let dropsite of foodDropsites.values())
{
if (!dropsite.position())
continue;
let owner = dropsite.owner();
// owner != PlayerID can only happen when hasSharedDropsites == true, so no need to test it again
if (owner != PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner)))
continue;
if (targetAccess != PETRA.getLandAccess(gameState, dropsite))
continue;
if (API3.SquareVectorDistance(target.position(), dropsite.position()) < distanceSquare)
{
hasFoodDropsiteWithinDistance = true;
break;
}
}
if (!hasFoodDropsiteWithinDistance)
{
if (this.retryWorking(gameState, subrole))
return;
ent.stopMoving();
}
}
}
}
}
}
else if (ent.getMetadata(PlayerID, "approachingTarget"))
{
ent.setMetadata(PlayerID, "approachingTarget", undefined);
ent.setMetadata(PlayerID, "alreadyTried", undefined);
}
let unitAIStateOrder = unitAIState.split(".")[1];
// If we're fighting or hunting, let's not start gathering except if inaccessible target
// but for fishers where UnitAI must have made us target a moving whale.
// Also, if we are attacking, do not capture
if (unitAIStateOrder == "COMBAT")
{
if (subrole === PETRA.Worker.SUBROLE_FISHER)
this.startFishing(gameState);
else if (unitAIState == "INDIVIDUAL.COMBAT.APPROACHING" && ent.unitAIOrderData().length &&
!ent.getMetadata(PlayerID, "PartOfArmy"))
{
let orderData = ent.unitAIOrderData()[0];
if (orderData && orderData.target)
{
let target = gameState.getEntityById(orderData.target);
if (target && (!target.position() || PETRA.getLandAccess(gameState, target) != this.entAccess))
{
if (this.retryWorking(gameState, subrole))
return;
ent.stopMoving();
}
}
}
else if (unitAIState == "INDIVIDUAL.COMBAT.ATTACKING" && ent.unitAIOrderData().length &&
!ent.getMetadata(PlayerID, "PartOfArmy"))
{
let orderData = ent.unitAIOrderData()[0];
if (orderData && orderData.target && orderData.attackType && orderData.attackType == "Capture")
{
// If we are here, an enemy structure must have targeted one of our workers
// and UnitAI sent it fight back with allowCapture=true
let target = gameState.getEntityById(orderData.target);
if (target && target.owner() > 0 && !gameState.isPlayerAlly(target.owner()))
ent.attack(orderData.target, PETRA.allowCapture(gameState, ent, target));
}
}
return;
}
// Okay so we have a few tasks.
// If we're gathering, we'll check that we haven't run idle.
// And we'll also check that we're gathering a resource we want to gather.
if (subrole === PETRA.Worker.SUBROLE_GATHERER)
{
if (ent.isIdle())
{
// if we aren't storing resources or it's the same type as what we're about to gather,
// let's just pick a new resource.
// TODO if we already carry the max we can -> returnresources
if (!ent.resourceCarrying() || !ent.resourceCarrying().length ||
ent.resourceCarrying()[0].type == ent.getMetadata(PlayerID, "gather-type"))
{
this.startGathering(gameState);
}
else if (!PETRA.returnResources(gameState, ent)) // try to deposit resources
{
// no dropsite, abandon old resources and start gathering new ones
this.startGathering(gameState);
}
}
else if (unitAIStateOrder == "GATHER")
{
// we're already gathering. But let's check if there is nothing better
// in case UnitAI did something bad
if (ent.unitAIOrderData().length)
{
let supplyId = ent.unitAIOrderData()[0].target;
let supply = gameState.getEntityById(supplyId);
if (supply && !supply.hasClasses(["Field", "Animal"]) &&
supplyId != ent.getMetadata(PlayerID, "supply"))
{
const nbGatherers = supply.resourceSupplyNumGatherers() + this.base.GetTCGatherer(supplyId);
if (nbGatherers > 1 && supply.resourceSupplyAmount()/nbGatherers < 30)
{
this.base.RemoveTCGatherer(supplyId);
this.startGathering(gameState);
}
else
{
let gatherType = ent.getMetadata(PlayerID, "gather-type");
let nearby = this.base.dropsiteSupplies[gatherType].nearby;
if (nearby.some(sup => sup.id == supplyId))
ent.setMetadata(PlayerID, "supply", supplyId);
else if (nearby.length)
{
this.base.RemoveTCGatherer(supplyId);
this.startGathering(gameState);
}
else
{
let medium = this.base.dropsiteSupplies[gatherType].medium;
if (medium.length && !medium.some(sup => sup.id == supplyId))
{
this.base.RemoveTCGatherer(supplyId);
this.startGathering(gameState);
}
else
ent.setMetadata(PlayerID, "supply", supplyId);
}
}
}
}
if (unitAIState == "INDIVIDUAL.GATHER.RETURNINGRESOURCE.APPROACHING")
{
if (gameState.ai.playedTurn % 10 == 0)
{
// Check from time to time that UnitAI does not send us to an inaccessible dropsite
let dropsite = gameState.getEntityById(ent.unitAIOrderData()[0].target);
if (dropsite && dropsite.position() && this.entAccess != PETRA.getLandAccess(gameState, dropsite))
PETRA.returnResources(gameState, this.ent);
}
// If gathering a sparse resource, we may have been sent to a faraway resource if the one nearby was full.
// Let's check if it is still the case. If so, we reset its metadata supplyId so that the unit will be
// reordered to gather after having returned the resources (when comparing its supplyId with the UnitAI one).
let gatherType = ent.getMetadata(PlayerID, "gather-type");
let influenceGroup = Resources.GetResource(gatherType).aiAnalysisInfluenceGroup;
if (influenceGroup && influenceGroup == "sparse")
{
let supplyId = ent.getMetadata(PlayerID, "supply");
if (supplyId)
{
let nearby = this.base.dropsiteSupplies[gatherType].nearby;
if (!nearby.some(sup => sup.id == supplyId))
{
if (nearby.length)
ent.setMetadata(PlayerID, "supply", undefined);
else
{
let medium = this.base.dropsiteSupplies[gatherType].medium;
if (!medium.some(sup => sup.id == supplyId) && medium.length)
ent.setMetadata(PlayerID, "supply", undefined);
}
}
}
}
}
}
}
else if (subrole === PETRA.Worker.SUBROLE_BUILDER)
{
if (unitAIStateOrder == "REPAIR")
{
// Update our target in case UnitAI sent us to a different foundation because of autocontinue
// and abandon it if UnitAI has sent us to build a field (as we build them only when needed)
if (ent.unitAIOrderData()[0] && ent.unitAIOrderData()[0].target &&
ent.getMetadata(PlayerID, "target-foundation") != ent.unitAIOrderData()[0].target)
{
let targetId = ent.unitAIOrderData()[0].target;
let target = gameState.getEntityById(targetId);
if (target && !target.hasClass("Field"))
{
ent.setMetadata(PlayerID, "target-foundation", targetId);
return;
}
ent.setMetadata(PlayerID, "target-foundation", undefined);
ent.setMetadata(PlayerID, "subrole", PETRA.Worker.SUBROLE_IDLE);
ent.stopMoving();
if (this.baseID != gameState.ai.HQ.basesManager.baselessBase().ID)
{
// reassign it to something useful
this.base.reassignIdleWorkers(gameState, [ent]);
this.update(gameState, ent);
return;
}
}
// Otherwise check that the target still exists (useful in REPAIR.APPROACHING)
let targetId = ent.getMetadata(PlayerID, "target-foundation");
if (targetId && gameState.getEntityById(targetId))
return;
ent.stopMoving();
}
// okay so apparently we aren't working.
// Unless we've been explicitely told to keep our role, make us idle.
let target = gameState.getEntityById(ent.getMetadata(PlayerID, "target-foundation"));
if (!target || target.foundationProgress() === undefined && target.needsRepair() === false)
{
ent.setMetadata(PlayerID, "subrole", PETRA.Worker.SUBROLE_IDLE);
ent.setMetadata(PlayerID, "target-foundation", undefined);
// If worker elephant, move away to avoid being trapped in between constructions
if (ent.hasClass("Elephant"))
this.moveToGatherer(gameState, ent, true);
else if (this.baseID != gameState.ai.HQ.basesManager.baselessBase().ID)
{
// reassign it to something useful
this.base.reassignIdleWorkers(gameState, [ent]);
this.update(gameState, ent);
return;
}
}
else
{
let goalAccess = PETRA.getLandAccess(gameState, target);
let queued = PETRA.returnResources(gameState, ent);
if (this.entAccess == goalAccess)
ent.repair(target, target.hasClass("House"), queued); // autocontinue=true for houses
else
gameState.ai.HQ.navalManager.requireTransport(gameState, ent, this.entAccess, goalAccess, target.position());
}
}
else if (subrole === PETRA.Worker.SUBROLE_HUNTER)
{
let lastHuntSearch = ent.getMetadata(PlayerID, "lastHuntSearch");
if (ent.isIdle() && (!lastHuntSearch || gameState.ai.elapsedTime - lastHuntSearch > 20))
{
if (!this.startHunting(gameState))
{
// nothing to hunt around. Try another region if any
let nowhereToHunt = true;
for (const base of gameState.ai.HQ.baseManagers())
{
if (!base.anchor || !base.anchor.position())
continue;
let basePos = base.anchor.position();
if (this.startHunting(gameState, basePos))
{
ent.setMetadata(PlayerID, "base", base.ID);
if (base.accessIndex == this.entAccess)
ent.move(basePos[0], basePos[1]);
else
gameState.ai.HQ.navalManager.requireTransport(gameState, ent, this.entAccess, base.accessIndex, basePos);
nowhereToHunt = false;
break;
}
}
if (nowhereToHunt)
ent.setMetadata(PlayerID, "lastHuntSearch", gameState.ai.elapsedTime);
}
}
else // Perform some sanity checks
{
if (unitAIStateOrder == "GATHER")
{
// we may have drifted towards ennemy territory during the hunt, if yes go home
let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(ent.position());
if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally
this.startHunting(gameState);
else if (unitAIState == "INDIVIDUAL.GATHER.RETURNINGRESOURCE.APPROACHING")
{
// Check that UnitAI does not send us to an inaccessible dropsite
let dropsite = gameState.getEntityById(ent.unitAIOrderData()[0].target);
if (dropsite && dropsite.position() && this.entAccess != PETRA.getLandAccess(gameState, dropsite))
PETRA.returnResources(gameState, ent);
}
}
}
}
else if (subrole === PETRA.Worker.SUBROLE_FISHER)
{
if (ent.isIdle())
this.startFishing(gameState);
else // if we have drifted towards ennemy territory during the fishing, go home
{
let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(ent.position());
if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally
this.startFishing(gameState);
}
}
};
PETRA.Worker.prototype.retryWorking = function(gameState, subrole)
{
switch (subrole)
{
case PETRA.Worker.SUBROLE_GATHERER:
return this.startGathering(gameState);
case PETRA.Worker.SUBROLE_HUNTER:
return this.startHunting(gameState);
case PETRA.Worker.SUBROLE_FISHER:
return this.startFishing(gameState);
case PETRA.Worker.SUBROLE_BUILDER:
return this.startBuilding(gameState);
default:
return false;
}
};
PETRA.Worker.prototype.startBuilding = function(gameState)
{
let target = gameState.getEntityById(this.ent.getMetadata(PlayerID, "target-foundation"));
if (!target || target.foundationProgress() === undefined && target.needsRepair() == false)
return false;
if (PETRA.getLandAccess(gameState, target) != this.entAccess)
return false;
this.ent.repair(target, target.hasClass("House")); // autocontinue=true for houses
return true;
};
PETRA.Worker.prototype.startGathering = function(gameState)
{
// First look for possible treasure if any
if (PETRA.gatherTreasure(gameState, this.ent))
return true;
let resource = this.ent.getMetadata(PlayerID, "gather-type");
// If we are gathering food, try to hunt first
if (resource == "food" && this.startHunting(gameState))
return true;
const findSupply = function(worker, supplies) {
const ent = worker.ent;
let ret = false;
let gatherRates = ent.resourceGatherRates();
for (let i = 0; i < supplies.length; ++i)
{
// exhausted resource, remove it from this list
if (!supplies[i].ent || !gameState.getEntityById(supplies[i].id))
{
supplies.splice(i--, 1);
continue;
}
if (PETRA.IsSupplyFull(gameState, supplies[i].ent))
continue;
let inaccessibleTime = supplies[i].ent.getMetadata(PlayerID, "inaccessibleTime");
if (inaccessibleTime && gameState.ai.elapsedTime < inaccessibleTime)
continue;
let supplyType = supplies[i].ent.get("ResourceSupply/Type");
if (!gatherRates[supplyType])
continue;
// check if available resource is worth one additionnal gatherer (except for farms)
const nbGatherers = supplies[i].ent.resourceSupplyNumGatherers() + worker.base.GetTCGatherer(supplies[i].id);
if (supplies[i].ent.resourceSupplyType().specific != "grain" && nbGatherers > 0 &&
supplies[i].ent.resourceSupplyAmount()/(1+nbGatherers) < 30)
continue;
// not in ennemy territory
let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(supplies[i].ent.position());
if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally
continue;
worker.base.AddTCGatherer(supplies[i].id);
ent.setMetadata(PlayerID, "supply", supplies[i].id);
ret = supplies[i].ent;
break;
}
return ret;
};
let navalManager = gameState.ai.HQ.navalManager;
let supply;
// first look in our own base if accessible from our present position
if (this.baseAccess == this.entAccess)
{
supply = findSupply(this, this.base.dropsiteSupplies[resource].nearby);
if (supply)
{
this.ent.gather(supply);
return true;
}
// --> for food, try to gather from fields if any, otherwise build one if any
if (resource == "food")
{
supply = this.gatherNearestField(gameState, this.baseID);
if (supply)
{
this.ent.gather(supply);
return true;
}
supply = this.buildAnyField(gameState, this.baseID);
if (supply)
{
this.ent.repair(supply);
return true;
}
}
supply = findSupply(this, this.base.dropsiteSupplies[resource].medium);
if (supply)
{
this.ent.gather(supply);
return true;
}
}
// So if we're here we have checked our whole base for a proper resource (or it was not accessible)
// --> check other bases directly accessible
for (const base of gameState.ai.HQ.baseManagers())
{
if (base.ID == this.baseID)
continue;
if (base.accessIndex != this.entAccess)
continue;
supply = findSupply(this, base.dropsiteSupplies[resource].nearby);
if (supply)
{
this.ent.setMetadata(PlayerID, "base", base.ID);
this.ent.gather(supply);
return true;
}
}
if (resource == "food") // --> for food, try to gather from fields if any, otherwise build one if any
{
for (const base of gameState.ai.HQ.baseManagers())
{
if (base.ID == this.baseID)
continue;
if (base.accessIndex != this.entAccess)
continue;
supply = this.gatherNearestField(gameState, base.ID);
if (supply)
{
this.ent.setMetadata(PlayerID, "base", base.ID);
this.ent.gather(supply);
return true;
}
supply = this.buildAnyField(gameState, base.ID);
if (supply)
{
this.ent.setMetadata(PlayerID, "base", base.ID);
this.ent.repair(supply);
return true;
}
}
}
for (const base of gameState.ai.HQ.baseManagers())
{
if (base.ID == this.baseID)
continue;
if (base.accessIndex != this.entAccess)
continue;
supply = findSupply(this, base.dropsiteSupplies[resource].medium);
if (supply)
{
this.ent.setMetadata(PlayerID, "base", base.ID);
this.ent.gather(supply);
return true;
}
}
// Okay may-be we haven't found any appropriate dropsite anywhere.
// Try to help building one if any accessible foundation available
let foundations = gameState.getOwnFoundations().toEntityArray();
let shouldBuild = this.ent.isBuilder() && foundations.some(function(foundation) {
if (!foundation || PETRA.getLandAccess(gameState, foundation) != this.entAccess)
return false;
let structure = gameState.getBuiltTemplate(foundation.templateName());
if (structure.resourceDropsiteTypes() && structure.resourceDropsiteTypes().indexOf(resource) != -1)
{
if (foundation.getMetadata(PlayerID, "base") != this.baseID)
this.ent.setMetadata(PlayerID, "base", foundation.getMetadata(PlayerID, "base"));
this.ent.setMetadata(PlayerID, "target-foundation", foundation.id());
this.ent.setMetadata(PlayerID, "subrole", PETRA.Worker.SUBROLE_BUILDER);
this.ent.repair(foundation);
return true;
}
return false;
}, this);
if (shouldBuild)
return true;
// Still nothing ... try bases which need a transport
for (const base of gameState.ai.HQ.baseManagers())
{
if (base.accessIndex == this.entAccess)
continue;
supply = findSupply(this, base.dropsiteSupplies[resource].nearby);
if (supply && navalManager.requireTransport(gameState, this.ent, this.entAccess, base.accessIndex, supply.position()))
{
if (base.ID != this.baseID)
this.ent.setMetadata(PlayerID, "base", base.ID);
return true;
}
}
if (resource == "food") // --> for food, try to gather from fields if any, otherwise build one if any
{
for (const base of gameState.ai.HQ.baseManagers())
{
if (base.accessIndex == this.entAccess)
continue;
supply = this.gatherNearestField(gameState, base.ID);
if (supply && navalManager.requireTransport(gameState, this.ent, this.entAccess, base.accessIndex, supply.position()))
{
if (base.ID != this.baseID)
this.ent.setMetadata(PlayerID, "base", base.ID);
return true;
}
supply = this.buildAnyField(gameState, base.ID);
if (supply && navalManager.requireTransport(gameState, this.ent, this.entAccess, base.accessIndex, supply.position()))
{
if (base.ID != this.baseID)
this.ent.setMetadata(PlayerID, "base", base.ID);
return true;
}
}
}
for (const base of gameState.ai.HQ.baseManagers())
{
if (base.accessIndex == this.entAccess)
continue;
supply = findSupply(this, base.dropsiteSupplies[resource].medium);
if (supply && navalManager.requireTransport(gameState, this.ent, this.entAccess, base.accessIndex, supply.position()))
{
if (base.ID != this.baseID)
this.ent.setMetadata(PlayerID, "base", base.ID);
return true;
}
}
// Okay so we haven't found any appropriate dropsite anywhere.
// Try to help building one if any non-accessible foundation available
shouldBuild = this.ent.isBuilder() && foundations.some(function(foundation) {
if (!foundation || PETRA.getLandAccess(gameState, foundation) == this.entAccess)
return false;
let structure = gameState.getBuiltTemplate(foundation.templateName());
if (structure.resourceDropsiteTypes() && structure.resourceDropsiteTypes().indexOf(resource) != -1)
{
let foundationAccess = PETRA.getLandAccess(gameState, foundation);
if (navalManager.requireTransport(gameState, this.ent, this.entAccess, foundationAccess, foundation.position()))
{
if (foundation.getMetadata(PlayerID, "base") != this.baseID)
this.ent.setMetadata(PlayerID, "base", foundation.getMetadata(PlayerID, "base"));
this.ent.setMetadata(PlayerID, "target-foundation", foundation.id());
this.ent.setMetadata(PlayerID, "subrole", PETRA.Worker.SUBROLE_BUILDER);
return true;
}
}
return false;
}, this);
if (shouldBuild)
return true;
// Still nothing, we look now for faraway resources, first in the accessible ones, then in the others
// except for food when farms or corrals can be used
let allowDistant = true;
if (resource == "food")
{
if (gameState.ai.HQ.turnCache.allowDistantFood === undefined)
gameState.ai.HQ.turnCache.allowDistantFood =
!gameState.ai.HQ.canBuild(gameState, "structures/{civ}/field") &&
!gameState.ai.HQ.canBuild(gameState, "structures/{civ}/corral");
allowDistant = gameState.ai.HQ.turnCache.allowDistantFood;
}
if (allowDistant)
{
if (this.baseAccess == this.entAccess)
{
supply = findSupply(this, this.base.dropsiteSupplies[resource].faraway);
if (supply)
{
this.ent.gather(supply);
return true;
}
}
for (const base of gameState.ai.HQ.baseManagers())
{
if (base.ID == this.baseID)
continue;
if (base.accessIndex != this.entAccess)
continue;
supply = findSupply(this, base.dropsiteSupplies[resource].faraway);
if (supply)
{
this.ent.setMetadata(PlayerID, "base", base.ID);
this.ent.gather(supply);
return true;
}
}
for (const base of gameState.ai.HQ.baseManagers())
{
if (base.accessIndex == this.entAccess)
continue;
supply = findSupply(this, base.dropsiteSupplies[resource].faraway);
if (supply && navalManager.requireTransport(gameState, this.ent, this.entAccess, base.accessIndex, supply.position()))
{
if (base.ID != this.baseID)
this.ent.setMetadata(PlayerID, "base", base.ID);
return true;
}
}
}
// If we are here, we have nothing left to gather ... certainly no more resources of this type
gameState.ai.HQ.lastFailedGather[resource] = gameState.ai.elapsedTime;
if (gameState.ai.Config.debug > 2)
API3.warn(" >>>>> worker with gather-type " + resource + " with nothing to gather ");
this.ent.setMetadata(PlayerID, "subrole", PETRA.Worker.SUBROLE_IDLE);
return false;
};
/**
* if position is given, we only check if we could hunt from this position but do nothing
* otherwise the position of the entity is taken, and if something is found, we directly start the hunt
*/
PETRA.Worker.prototype.startHunting = function(gameState, position)
{
// First look for possible treasure if any
if (!position && PETRA.gatherTreasure(gameState, this.ent))
return true;
let resources = gameState.getHuntableSupplies();
if (!resources.hasEntities())
return false;
let nearestSupplyDist = Math.min();
let nearestSupply;
let isFastMoving = PETRA.isFastMoving(this.ent);
let isRanged = this.ent.hasClass("Ranged");
let entPosition = position ? position : this.ent.position();
let foodDropsites = gameState.playerData.hasSharedDropsites ?
gameState.getAnyDropsites("food") : gameState.getOwnDropsites("food");
let hasFoodDropsiteWithinDistance = function(supplyPosition, supplyAccess, distSquare)
{
for (let dropsite of foodDropsites.values())
{
if (!dropsite.position())
continue;
let owner = dropsite.owner();
// owner != PlayerID can only happen when hasSharedDropsites == true, so no need to test it again
if (owner != PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner)))
continue;
if (supplyAccess != PETRA.getLandAccess(gameState, dropsite))
continue;
if (API3.SquareVectorDistance(supplyPosition, dropsite.position()) < distSquare)
return true;
}
return false;
};
let gatherRates = this.ent.resourceGatherRates();
for (let supply of resources.values())
{
if (!supply.position())
continue;
let inaccessibleTime = supply.getMetadata(PlayerID, "inaccessibleTime");
if (inaccessibleTime && gameState.ai.elapsedTime < inaccessibleTime)
continue;
let supplyType = supply.get("ResourceSupply/Type");
if (!gatherRates[supplyType])
continue;
if (PETRA.IsSupplyFull(gameState, supply))
continue;
// Check if available resource is worth one additionnal gatherer (except for farms).
const nbGatherers = supply.resourceSupplyNumGatherers() + this.base.GetTCGatherer(supply.id());
if (nbGatherers > 0 && supply.resourceSupplyAmount()/(1+nbGatherers) < 30)
continue;
let canFlee = !supply.hasClass("Domestic") && supply.templateName().indexOf("resource|") == -1;
// Only FastMoving and Ranged units should hunt fleeing animals.
if (canFlee && !isFastMoving && !isRanged)
continue;
let supplyAccess = PETRA.getLandAccess(gameState, supply);
if (supplyAccess != this.entAccess)
continue;
// measure the distance to the resource.
let dist = API3.SquareVectorDistance(entPosition, supply.position());
if (dist > nearestSupplyDist)
continue;
// Only FastMoving should hunt faraway.
if (!isFastMoving && dist > 25000)
continue;
// Avoid enemy territory.
let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(supply.position());
if (territoryOwner != 0 && !gameState.isPlayerAlly(territoryOwner)) // Player is its own ally.
continue;
// And if in ally territory, don't hunt this ally's cattle.
if (territoryOwner != 0 && territoryOwner != PlayerID && supply.owner() == territoryOwner)
continue;
// Only FastMoving should hunt far from dropsite (specially for non-Domestic animals which flee).
if (!isFastMoving && canFlee && territoryOwner == 0)
continue;
let distanceSquare = isFastMoving ? 35000 : (canFlee ? 7000 : 12000);
if (!hasFoodDropsiteWithinDistance(supply.position(), supplyAccess, distanceSquare))
continue;
nearestSupplyDist = dist;
nearestSupply = supply;
}
if (nearestSupply)
{
if (position)
return true;
this.base.AddTCGatherer(nearestSupply.id());
this.ent.gather(nearestSupply);
this.ent.setMetadata(PlayerID, "supply", nearestSupply.id());
this.ent.setMetadata(PlayerID, "target-foundation", undefined);
return true;
}
return false;
};
PETRA.Worker.prototype.startFishing = function(gameState)
{
if (!this.ent.position())
return false;
let resources = gameState.getFishableSupplies();
if (!resources.hasEntities())
{
gameState.ai.HQ.navalManager.resetFishingBoats(gameState);
this.ent.destroy();
return false;
}
let nearestSupplyDist = Math.min();
let nearestSupply;
let fisherSea = PETRA.getSeaAccess(gameState, this.ent);
let fishDropsites = (gameState.playerData.hasSharedDropsites ? gameState.getAnyDropsites("food") : gameState.getOwnDropsites("food")).
filter(API3.Filters.byClass("Dock")).toEntityArray();
let nearestDropsiteDist = function(supply) {
let distMin = 1000000;
let pos = supply.position();
for (let dropsite of fishDropsites)
{
if (!dropsite.position())
continue;
let owner = dropsite.owner();
// owner != PlayerID can only happen when hasSharedDropsites == true, so no need to test it again
if (owner != PlayerID && (!dropsite.isSharedDropsite() || !gameState.isPlayerMutualAlly(owner)))
continue;
if (fisherSea != PETRA.getSeaAccess(gameState, dropsite))
continue;
distMin = Math.min(distMin, API3.SquareVectorDistance(pos, dropsite.position()));
}
return distMin;
};
let exhausted = true;
let gatherRates = this.ent.resourceGatherRates();
resources.forEach((supply) => {
if (!supply.position())
return;
// check that it is accessible
if (gameState.ai.HQ.navalManager.getFishSea(gameState, supply) != fisherSea)
return;
exhausted = false;
let supplyType = supply.get("ResourceSupply/Type");
if (!gatherRates[supplyType])
return;
if (PETRA.IsSupplyFull(gameState, supply))
return;
// check if available resource is worth one additionnal gatherer (except for farms)
const nbGatherers = supply.resourceSupplyNumGatherers() + this.base.GetTCGatherer(supply.id());
if (nbGatherers > 0 && supply.resourceSupplyAmount()/(1+nbGatherers) < 30)
return;
// Avoid ennemy territory
if (!gameState.ai.HQ.navalManager.canFishSafely(gameState, supply))
return;
// measure the distance from the resource to the nearest dropsite
let dist = nearestDropsiteDist(supply);
if (dist > nearestSupplyDist)
return;
nearestSupplyDist = dist;
nearestSupply = supply;
});
if (exhausted)
{
gameState.ai.HQ.navalManager.resetFishingBoats(gameState, fisherSea);
this.ent.destroy();
return false;
}
if (nearestSupply)
{
this.base.AddTCGatherer(nearestSupply.id());
this.ent.gather(nearestSupply);
this.ent.setMetadata(PlayerID, "supply", nearestSupply.id());
this.ent.setMetadata(PlayerID, "target-foundation", undefined);
return true;
}
if (this.ent.getMetadata(PlayerID, "subrole") === PETRA.Worker.SUBROLE_FISHER)
this.ent.setMetadata(PlayerID, "subrole", PETRA.Worker.SUBROLE_IDLE);
return false;
};
PETRA.Worker.prototype.gatherNearestField = function(gameState, baseID)
{
let ownFields = gameState.getOwnEntitiesByClass("Field", true).filter(API3.Filters.isBuilt()).filter(API3.Filters.byMetadata(PlayerID, "base", baseID));
let bestFarm;
let gatherRates = this.ent.resourceGatherRates();
for (let field of ownFields.values())
{
if (PETRA.IsSupplyFull(gameState, field))
continue;
let supplyType = field.get("ResourceSupply/Type");
if (!gatherRates[supplyType])
continue;
let rate = 1;
let diminishing = field.getDiminishingReturns();
if (diminishing < 1)
{
const num = field.resourceSupplyNumGatherers() + this.base.GetTCGatherer(field.id());
if (num > 0)
rate = Math.pow(diminishing, num);
}
// Add a penalty distance depending on rate
let dist = API3.SquareVectorDistance(field.position(), this.ent.position()) + (1 - rate) * 160000;
if (!bestFarm || dist < bestFarm.dist)
bestFarm = { "ent": field, "dist": dist, "rate": rate };
}
// If other field foundations available, better build them when rate becomes too small
if (!bestFarm || bestFarm.rate < 0.70 &&
gameState.getOwnFoundations().filter(API3.Filters.byClass("Field")).filter(API3.Filters.byMetadata(PlayerID, "base", baseID)).hasEntities())
return false;
this.base.AddTCGatherer(bestFarm.ent.id());
this.ent.setMetadata(PlayerID, "supply", bestFarm.ent.id());
return bestFarm.ent;
};
/**
* WARNING with the present options of AI orders, the unit will not gather after building the farm.
* This is done by calling the gatherNearestField function when construction is completed.
*/
PETRA.Worker.prototype.buildAnyField = function(gameState, baseID)
{
if (!this.ent.isBuilder())
return false;
let bestFarmEnt = false;
let bestFarmDist = 10000000;
let pos = this.ent.position();
for (let found of gameState.getOwnFoundations().values())
{
if (found.getMetadata(PlayerID, "base") != baseID || !found.hasClass("Field"))
continue;
let current = found.getBuildersNb();
if (current === undefined ||
current >= gameState.getBuiltTemplate(found.templateName()).maxGatherers())
continue;
let dist = API3.SquareVectorDistance(found.position(), pos);
if (dist > bestFarmDist)
continue;
bestFarmEnt = found;
bestFarmDist = dist;
}
return bestFarmEnt;
};
/**
* Workers elephant should move away from the buildings they've built to avoid being trapped in between constructions.
* For the time being, we move towards the nearest gatherer (providing him a dropsite).
* BaseManager does also use that function to deal with its mobile dropsites.
*/
PETRA.Worker.prototype.moveToGatherer = function(gameState, ent, forced)
{
let pos = ent.position();
if (!pos || ent.getMetadata(PlayerID, "target-foundation") !== undefined)
return;
if (!forced && gameState.ai.elapsedTime < (ent.getMetadata(PlayerID, "nextMoveToGatherer") || 5))
return;
const gatherers = this.base.workersBySubrole(gameState, PETRA.Worker.SUBROLE_GATHERER);
let dist = Math.min();
let destination;
let access = PETRA.getLandAccess(gameState, ent);
let types = ent.resourceDropsiteTypes();
for (let gatherer of gatherers.values())
{
let gathererType = gatherer.getMetadata(PlayerID, "gather-type");
if (!gathererType || types.indexOf(gathererType) == -1)
continue;
if (!gatherer.position() || gatherer.getMetadata(PlayerID, "transport") !== undefined ||
PETRA.getLandAccess(gameState, gatherer) != access || gatherer.isIdle())
continue;
let distance = API3.SquareVectorDistance(pos, gatherer.position());
if (distance > dist)
continue;
dist = distance;
destination = gatherer.position();
}
ent.setMetadata(PlayerID, "nextMoveToGatherer", gameState.ai.elapsedTime + (destination ? 12 : 5));
if (destination && dist > 10)
ent.move(destination[0], destination[1]);
};
/**
* Check accessibility of the target when in approach (in RMS maps, we quite often have chicken or bushes
* inside obstruction of other entities). The resource will be flagged as inaccessible during 10 mn (in case
* it will be cleared later).
*/
PETRA.Worker.prototype.isInaccessibleSupply = function(gameState)
{
if (!this.ent.unitAIOrderData()[0] || !this.ent.unitAIOrderData()[0].target)
return false;
let targetId = this.ent.unitAIOrderData()[0].target;
let target = gameState.getEntityById(targetId);
if (!target)
return true;
if (!target.resourceSupplyType())
return false;
let approachingTarget = this.ent.getMetadata(PlayerID, "approachingTarget");
let carriedAmount = this.ent.resourceCarrying().length ? this.ent.resourceCarrying()[0].amount : 0;
if (!approachingTarget || approachingTarget != targetId)
{
this.ent.setMetadata(PlayerID, "approachingTarget", targetId);
this.ent.setMetadata(PlayerID, "approachingTime", undefined);
this.ent.setMetadata(PlayerID, "approachingPos", undefined);
this.ent.setMetadata(PlayerID, "carriedBefore", carriedAmount);
let alreadyTried = this.ent.getMetadata(PlayerID, "alreadyTried");
if (alreadyTried && alreadyTried != targetId)
this.ent.setMetadata(PlayerID, "alreadyTried", undefined);
}
let carriedBefore = this.ent.getMetadata(PlayerID, "carriedBefore");
if (carriedBefore != carriedAmount)
{
this.ent.setMetadata(PlayerID, "approachingTarget", undefined);
this.ent.setMetadata(PlayerID, "alreadyTried", undefined);
if (target.getMetadata(PlayerID, "inaccessibleTime"))
target.setMetadata(PlayerID, "inaccessibleTime", 0);
return false;
}
let inaccessibleTime = target.getMetadata(PlayerID, "inaccessibleTime");
if (inaccessibleTime && gameState.ai.elapsedTime < inaccessibleTime)
return true;
let approachingTime = this.ent.getMetadata(PlayerID, "approachingTime");
if (!approachingTime || gameState.ai.elapsedTime - approachingTime > 3)
{
let presentPos = this.ent.position();
let approachingPos = this.ent.getMetadata(PlayerID, "approachingPos");
if (!approachingPos || approachingPos[0] != presentPos[0] || approachingPos[1] != presentPos[1])
{
this.ent.setMetadata(PlayerID, "approachingTime", gameState.ai.elapsedTime);
this.ent.setMetadata(PlayerID, "approachingPos", presentPos);
return false;
}
if (gameState.ai.elapsedTime - approachingTime > 10)
{
if (this.ent.getMetadata(PlayerID, "alreadyTried"))
{
target.setMetadata(PlayerID, "inaccessibleTime", gameState.ai.elapsedTime + 600);
return true;
}
// let's try again to reach it
this.ent.setMetadata(PlayerID, "alreadyTried", targetId);
this.ent.setMetadata(PlayerID, "approachingTarget", undefined);
this.ent.gather(target);
return false;
}
}
return false;
};