Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/config.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/config.js (revision 19222) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/config.js (revision 19223) @@ -1,224 +1,226 @@ var PETRA = function(m) { m.Config = function(difficulty) { // 0 is sandbox, 1 is very easy, 2 is easy, 3 is medium, 4 is hard and 5 is very hard. this.difficulty = difficulty !== undefined ? difficulty : 3; // debug level: 0=none, 1=sanity checks, 2=debug, 3=detailed debug, -100=serializatio debug this.debug = 0; this.chat = true; // false to prevent AI's chats this.popScaling = 1; // scale factor depending on the max population this.Military = { "towerLapseTime" : 90, // Time to wait between building 2 towers "fortressLapseTime" : 390, // Time to wait between building 2 fortresses "popForBarracks1" : 25, "popForBarracks2" : 95, "popForBlacksmith" : 65, "numSentryTowers" : 1 }; this.Economy = { "popForTown" : 40, // How many units we want before aging to town. "workForCity" : 80, // How many workers we want before aging to city. "cityPhase" : 840, // time to start trying to reach city phase "popForMarket" : 50, "popForDock" : 25, "targetNumWorkers" : 40, // dummy, will be changed later "targetNumTraders" : 5, // Target number of traders "targetNumFishers" : 1, // Target number of fishers per sea "supportRatio" : 0.3, // fraction of support workers among the workforce "provisionFields" : 2 }; // Note: attack settings are set directly in attack_plan.js // defense this.Defense = { "defenseRatio" : 2, // ratio of defenders/attackers. "armyCompactSize" : 2000, // squared. Half-diameter of an army. "armyBreakawaySize" : 3500, // squared. "armyMergeSize" : 1400 // squared. }; this.buildings = { "base": { "default": [ "structures/{civ}_civil_centre" ], "ptol": [ "structures/{civ}_military_colony" ], "sele": [ "structures/{civ}_military_colony" ] }, "advanced": { "default": [], "athen": [ "structures/{civ}_gymnasion", "structures/{civ}_prytaneion", "structures/{civ}_theatron" ], "brit": [ "structures/{civ}_rotarymill" ], "cart": [ "structures/{civ}_embassy_celtic", "structures/{civ}_embassy_iberian", "structures/{civ}_embassy_italiote" ], "gaul": [ "structures/{civ}_rotarymill", "structures/{civ}_tavern" ], "iber": [ "structures/{civ}_monument" ], "mace": [ "structures/{civ}_siege_workshop", "structures/{civ}_library", "structures/{civ}_theatron" ], "maur": [ "structures/{civ}_elephant_stables", "structures/{civ}_pillar_ashoka" ], "pers": [ "structures/{civ}_stables", "structures/{civ}_apadana", "structures/{civ}_hall"], "ptol": [ "structures/{civ}_library" ], "rome": [ "structures/{civ}_army_camp" ], "sele": [ "structures/{civ}_library" ], "spart": [ "structures/{civ}_syssiton", "structures/{civ}_theatron" ] }, "naval": { "default": [], // "brit": [ "structures/{civ}_crannog" ], "cart": [ "structures/{civ}_super_dock" ] } }; this.priorities = { "villager": 30, // should be slightly lower than the citizen soldier one to not get all the food "citizenSoldier": 60, "trader": 50, "ships": 70, "house": 350, "dropsites": 200, "field": 400, "dock": 90, "corral": 60, "economicBuilding": 90, "militaryBuilding": 130, "defenseBuilding": 70, "civilCentre": 950, "majorTech": 700, "minorTech": 40, "emergency": 1000 // used only in emergency situations, should be the highest one }; this.personality = { "aggressive": 0.5, "cooperative": 0.5, "defensive": 0.5 }; // See m.QueueManager.prototype.wantedGatherRates() this.queues = { "firstTurn": { "food": 10, "wood": 10, "default": 0 }, "short": { "food": 200, "wood": 200, "default": 100 }, "medium": { "default": 0 }, "long": { "default": 0 } }; }; m.Config.prototype.setConfig = function(gameState) { // initialize personality traits if (this.difficulty > 1) { this.personality.aggressive = Math.random(); this.personality.cooperative = Math.random(); this.personality.defensive = Math.random(); } else { this.personality.aggressive = 0.1; this.personality.cooperative = 0.9; } + if (gameState.getAlliedVictory()) + this.personality.cooperative = Math.min(1, this.personality.cooperative + 0.15); // changing settings based on difficulty or personality if (this.difficulty < 2) { this.Economy.cityPhase = 240000; this.Economy.supportRatio = 0.5; this.Economy.provisionFields = 1; this.Military.numSentryTowers = this.personality.defensive > 0.66 ? 1 : 0; } else if (this.difficulty < 3) { this.Economy.cityPhase = 1800; this.Economy.supportRatio = 0.4; this.Economy.provisionFields = 1; this.Military.numSentryTowers = this.personality.defensive > 0.66 ? 1 : 0; } else { this.Military.towerLapseTime += Math.round(20*(this.personality.defensive - 0.5)); this.Military.fortressLapseTime += Math.round(60*(this.personality.defensive - 0.5)); if (this.difficulty == 3) this.Military.numSentryTowers = 1; else this.Military.numSentryTowers = 2; if (this.personality.defensive > 0.66) ++this.Military.numSentryTowers; else if (this.personality.defensive < 0.33) --this.Military.numSentryTowers; if (this.personality.aggressive > 0.7) { this.Military.popForBarracks1 = 12; this.Economy.popForTown = 55; this.Economy.popForMarket = 60; this.priorities.defenseBuilding = 60; } } let maxPop = gameState.getPopulationMax(); if (this.difficulty < 2) this.Economy.targetNumWorkers = Math.max(1, Math.min(40, maxPop)); else if (this.difficulty < 3) this.Economy.targetNumWorkers = Math.max(1, Math.min(60, Math.floor(maxPop/2))); else this.Economy.targetNumWorkers = Math.max(1, Math.min(120, Math.floor(maxPop/3))); this.Economy.targetNumTraders = 2 + this.difficulty; if (maxPop < 300) { this.popScaling = Math.sqrt(maxPop / 300); this.Military.popForBarracks1 = Math.min(Math.max(Math.floor(this.Military.popForBarracks1 * this.popScaling), 12), Math.floor(maxPop/5)); this.Military.popForBarracks2 = Math.min(Math.max(Math.floor(this.Military.popForBarracks2 * this.popScaling), 45), Math.floor(maxPop*2/3)); this.Military.popForBlacksmith = Math.min(Math.max(Math.floor(this.Military.popForBlacksmith * this.popScaling), 30), Math.floor(maxPop/2)); this.Economy.popForTown = Math.min(Math.max(Math.floor(this.Economy.popForTown * this.popScaling), 25), Math.floor(maxPop/2)); this.Economy.workForCity = Math.min(Math.max(Math.floor(this.Economy.workForCity * this.popScaling), 50), Math.floor(maxPop*2/3)); this.Economy.popForMarket = Math.min(Math.max(Math.floor(this.Economy.popForMarket * this.popScaling), 25), Math.floor(maxPop/2)); this.Economy.targetNumTraders = Math.round(this.Economy.targetNumTraders * this.popScaling); } this.Economy.targetNumWorkers = Math.max(this.Economy.targetNumWorkers, this.Economy.popForTown); if (this.debug < 2) return; API3.warn(" >>> Petra bot: personality = " + uneval(this.personality)); }; m.Config.prototype.Serialize = function() { var data = {}; for (let key in this) if (this.hasOwnProperty(key) && key != "debug") data[key] = this[key]; return data; }; m.Config.prototype.Deserialize = function(data) { for (let key in data) this[key] = data[key]; }; return m; }(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/worker.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/worker.js (revision 19222) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/worker.js (revision 19223) @@ -1,903 +1,904 @@ var PETRA = function(m) { /** * This class makes a worker do as instructed by the economy manager */ m.Worker = function(base) { this.ent = undefined; this.base = base; this.baseID = base.ID; }; m.Worker.prototype.update = function(gameState, ent) { if (!ent.position() || ent.getMetadata(PlayerID, "plan") === -2 || ent.getMetadata(PlayerID, "plan") === -3) return; // If we are waiting for a transport or we are sailing, just wait if (ent.getMetadata(PlayerID, "transport") !== undefined) return; // base 0 for unassigned entities has no accessIndex, so take the one from the entity if (this.baseID === gameState.ai.HQ.baseManagers[0].ID) this.accessIndex = gameState.ai.accessibility.getAccessValue(ent.position()); else this.accessIndex = this.base.accessIndex; let subrole = ent.getMetadata(PlayerID, "subrole"); if (!subrole) // subrole may-be undefined after a transport, garrisoning, army, ... { ent.setMetadata(PlayerID, "subrole", "idle"); this.base.reassignIdleWorkers(gameState, [ent]); this.update(gameState, ent); return; } this.ent = ent; let unitAIState = ent.unitAIState(); if (unitAIState === "INDIVIDUAL.GATHER.GATHERING" || unitAIState === "INDIVIDUAL.GATHER.APPROACHING" || unitAIState === "INDIVIDUAL.COMBAT.APPROACHING") { if (this.isInaccessibleSupply(gameState) && ((subrole === "hunter" && !this.startHunting(gameState)) || (subrole === "gatherer" && !this.startGathering(gameState)))) ent.stopMoving(); } else if (ent.getMetadata(PlayerID, "approachingTarget")) ent.setMetadata(PlayerID, "approachingTarget", undefined); let unitAIStateOrder = unitAIState.split(".")[1]; // If we're fighting or hunting, let's not start gathering // 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 === "fisher") this.startFishing(gameState); 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, m.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 === "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 (!m.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.hasClass("Field") && !supply.hasClass("Animal") && supply.resourceSupplyType().generic !== "treasure" && supplyId !== ent.getMetadata(PlayerID, "supply")) { let nbGatherers = supply.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supplyId); if (nbGatherers > 1 && supply.resourceSupplyAmount()/nbGatherers < 30) { gameState.ai.HQ.RemoveTCGatherer(supplyId); this.startGathering(gameState); } else { let gatherType = ent.getMetadata(PlayerID, "gather-type"); let nearby = this.base.dropsiteSupplies[gatherType].nearby; let isNearby = nearby.some(sup => sup.id === supplyId); if (nearby.length === 0 || isNearby) ent.setMetadata(PlayerID, "supply", supplyId); else { gameState.ai.HQ.RemoveTCGatherer(supplyId); this.startGathering(gameState); } } } } } else if (unitAIState === "INDIVIDUAL.RETURNRESOURCE.APPROACHING" && 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()) { let access = gameState.ai.accessibility.getAccessValue(ent.position()); let goalAccess = dropsite.getMetadata(PlayerID, "access"); if (!goalAccess || dropsite.hasClass("Elephant")) { goalAccess = gameState.ai.accessibility.getAccessValue(dropsite.position()); dropsite.setMetadata(PlayerID, "access", goalAccess); } if (access !== goalAccess) m.returnResources(gameState, this.ent); } } } else if (subrole === "builder") { if (unitAIStateOrder === "REPAIR") { // update our target in case UnitAI sent us to a different foundation because of autocontinue if (ent.unitAIOrderData()[0] && ent.unitAIOrderData()[0].target && ent.getMetadata(PlayerID, "target-foundation") !== ent.unitAIOrderData()[0].target) ent.setMetadata(PlayerID, "target-foundation", ent.unitAIOrderData()[0].target); // and check that the target still exists (useful in REPAIR.APPROACHING) let target = ent.getMetadata(PlayerID, "target-foundation"); if (target && gameState.getEntityById(target)) 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", "idle"); ent.setMetadata(PlayerID, "target-foundation", undefined); // If worker elephant, move away to avoid being trapped in between constructions if (ent.hasClass("Elephant")) this.moveAway(gameState); else if (this.baseID !== gameState.ai.HQ.baseManagers[0].ID) { // reassign it to something useful this.base.reassignIdleWorkers(gameState, [ent]); this.update(gameState, ent); return; } } else { let access = gameState.ai.accessibility.getAccessValue(ent.position()); let goalAccess = m.getLandAccess(gameState, target); + let queued = m.returnResources(gameState, ent); if (access === goalAccess) - ent.repair(target, target.hasClass("House")); // autocontinue=true for houses + ent.repair(target, target.hasClass("House"), queued); // autocontinue=true for houses else gameState.ai.HQ.navalManager.requireTransport(gameState, ent, access, goalAccess, target.position()); } } else if (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 (let 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); let access = gameState.ai.accessibility.getAccessValue(ent.position()); if (base.accessIndex === access) ent.move(basePos[0], basePos[1]); else gameState.ai.HQ.navalManager.requireTransport(gameState, ent, access, base.accessIndex, basePos); nowhereToHunt = false; break; } } if (nowhereToHunt) ent.setMetadata(PlayerID, "lastHuntSearch", gameState.ai.elapsedTime); } } else // Perform some sanity checks { if (unitAIStateOrder === "GATHER" || unitAIStateOrder === "RETURNRESOURCE") { // 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.RETURNRESOURCE.APPROACHING") { // Check that UnitAI does not send us to an inaccessible dropsite let dropsite = gameState.getEntityById(ent.unitAIOrderData()[0].target); if (dropsite && dropsite.position()) { let access = gameState.ai.accessibility.getAccessValue(ent.position()); let goalAccess = dropsite.getMetadata(PlayerID, "access"); if (!goalAccess || dropsite.hasClass("Elephant")) { goalAccess = gameState.ai.accessibility.getAccessValue(dropsite.position()); dropsite.setMetadata(PlayerID, "access", goalAccess); } if (access !== goalAccess) m.returnResources(gameState, ent); } } } } } else if (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); } } }; m.Worker.prototype.startGathering = function(gameState) { let access = gameState.ai.accessibility.getAccessValue(this.ent.position()); // First look for possible treasure if any if (this.gatherTreasure(gameState)) 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; let findSupply = function(ent, supplies) { let ret = false; 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 (m.IsSupplyFull(gameState, supplies[i].ent)) continue; let inaccessibleTime = supplies[i].ent.getMetadata(PlayerID, "inaccessibleTime"); if (inaccessibleTime && gameState.ai.elapsedTime < inaccessibleTime) continue; // check if available resource is worth one additionnal gatherer (except for farms) let nbGatherers = supplies[i].ent.resourceSupplyNumGatherers() + gameState.ai.HQ.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; gameState.ai.HQ.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.accessIndex === access) { if ((supply = findSupply(this.ent, this.base.dropsiteSupplies[resource].nearby))) { this.ent.gather(supply); return true; } // --> for food, try to gather from fields if any, otherwise build one if any if (resource === "food") { if ((supply = this.gatherNearestField(gameState, this.baseID))) { this.ent.gather(supply); return true; } else if (this.ent.isBuilder() && (supply = this.buildAnyField(gameState, this.baseID))) { this.ent.repair(supply); return true; } } if ((supply = findSupply(this.ent, this.base.dropsiteSupplies[resource].medium))) { 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 (let base of gameState.ai.HQ.baseManagers) { if (base.ID === this.baseID) continue; if (base.accessIndex !== access) continue; if ((supply = findSupply(this.ent, base.dropsiteSupplies[resource].nearby))) { 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 (let base of gameState.ai.HQ.baseManagers) { if (base.ID === this.baseID) continue; if (base.accessIndex !== access) continue; if ((supply = this.gatherNearestField(gameState, base.ID))) { this.ent.setMetadata(PlayerID, "base", base.ID); this.ent.gather(supply); return true; } if (this.ent.isBuilder() && (supply = this.buildAnyField(gameState, base.ID))) { this.ent.setMetadata(PlayerID, "base", base.ID); this.ent.repair(supply); return true; } } } for (let base of gameState.ai.HQ.baseManagers) { if (base.ID === this.baseID) continue; if (base.accessIndex !== access) continue; if ((supply = findSupply(this.ent, base.dropsiteSupplies[resource].medium))) { 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 || foundation.getMetadata(PlayerID, "access") !== access) return false; if (foundation.resourceDropsiteTypes() && foundation.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.repair(foundation); return true; } return false; }, this); if (shouldBuild) return true; // Still nothing ... try bases which need a transport for (let base of gameState.ai.HQ.baseManagers) { if (base.accessIndex === access) continue; if ((supply = findSupply(this.ent, base.dropsiteSupplies[resource].nearby))) { if (navalManager.requireTransport(gameState, this.ent, access, 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 (let base of gameState.ai.HQ.baseManagers) { if (base.accessIndex === access) continue; if ((supply = this.gatherNearestField(gameState, base.ID))) { if (navalManager.requireTransport(gameState, this.ent, access, base.accessIndex, supply.position())) { if (base.ID !== this.baseID) this.ent.setMetadata(PlayerID, "base", base.ID); return true; } } if ((supply = this.buildAnyField(gameState, base.ID))) { if (navalManager.requireTransport(gameState, this.ent, access, base.accessIndex, supply.position())) { if (base.ID !== this.baseID) this.ent.setMetadata(PlayerID, "base", base.ID); return true; } } } } for (let base of gameState.ai.HQ.baseManagers) { if (base.accessIndex === access) continue; if ((supply = findSupply(this.ent, base.dropsiteSupplies[resource].medium))) { if (navalManager.requireTransport(gameState, this.ent, access, 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 || foundation.getMetadata(PlayerID, "access") === access) return false; if (foundation.resourceDropsiteTypes() && foundation.resourceDropsiteTypes().indexOf(resource) !== -1) { let foundationAccess = m.getLandAccess(gameState, foundation); if (navalManager.requireTransport(gameState, this.ent, access, 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()); 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 if (this.accessIndex === access) { if ((supply = findSupply(this.ent, this.base.dropsiteSupplies[resource].faraway))) { this.ent.gather(supply); return true; } } for (let base of gameState.ai.HQ.baseManagers) { if (base.ID === this.baseID) continue; if (base.accessIndex !== access) continue; if ((supply = findSupply(this.ent, base.dropsiteSupplies[resource].faraway))) { this.ent.setMetadata(PlayerID, "base", base.ID); this.ent.gather(supply); return true; } } for (let base of gameState.ai.HQ.baseManagers) { if (base.accessIndex === access) continue; if ((supply = findSupply(this.ent, base.dropsiteSupplies[resource].faraway))) { if (navalManager.requireTransport(gameState, this.ent, access, 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) warn(" >>>>> worker with gather-type " + resource + " with nothing to gather "); this.ent.setMetadata(PlayerID, "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 */ m.Worker.prototype.startHunting = function(gameState, position) { // First look for possible treasure if any if (!position && this.gatherTreasure(gameState)) return true; let resources = gameState.getHuntableSupplies(); if (!resources.hasEntities()) return false; let nearestSupplyDist = Math.min(); let nearestSupply; let isCavalry = this.ent.hasClass("Cavalry"); let isRanged = this.ent.hasClass("Ranged"); let entPosition = position ? position : this.ent.position(); let access = gameState.ai.accessibility.getAccessValue(entPosition); let foodDropsites = (gameState.playerData.hasSharedDropsites ? gameState.getAnyDropsites("food") : gameState.getOwnDropsites("food")).toEntityArray(); let nearestDropsiteDist = function(supply) { let distMin = 1000000; let pos = supply.position(); for (let dropsite of foodDropsites) { 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 (access !== m.getLandAccess(gameState, dropsite)) continue; distMin = Math.min(distMin, API3.SquareVectorDistance(pos, dropsite.position())); } return distMin; }; resources.forEach(function(supply) { if (!supply.position()) return; let inaccessibleTime = supply.getMetadata(PlayerID, "inaccessibleTime"); if (inaccessibleTime && gameState.ai.elapsedTime < inaccessibleTime) return; if (m.IsSupplyFull(gameState, supply)) return; // check if available resource is worth one additionnal gatherer (except for farms) let nbGatherers = supply.resourceSupplyNumGatherers() + gameState.ai.HQ.GetTCGatherer(supply.id()); if (nbGatherers > 0 && supply.resourceSupplyAmount()/(1+nbGatherers) < 30) return; let canFlee = !supply.hasClass("Domestic") && supply.templateName().indexOf("resource|") == -1; // Only cavalry and range units should hunt fleeing animals if (canFlee && !isCavalry && !isRanged) return; let supplyAccess = gameState.ai.accessibility.getAccessValue(supply.position()); if (supplyAccess !== access) return; // measure the distance to the resource let dist = API3.SquareVectorDistance(entPosition, supply.position()); if (dist > nearestSupplyDist) return; // Only cavalry should hunt faraway if (!isCavalry && dist > 25000) return; // Avoid ennemy territory let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(supply.position()); if (territoryOwner !== 0 && !gameState.isPlayerAlly(territoryOwner)) // player is its own ally return; // And if in ally territory, don't hunt this ally's cattle if (territoryOwner !== 0 && territoryOwner !== PlayerID && supply.owner() === territoryOwner) return; let dropsiteDist = nearestDropsiteDist(supply); if (dropsiteDist > 35000) return; // Only cavalry should hunt far from dropsite (specially for non domestic animals which flee) if (!isCavalry && (dropsiteDist > 12000 || ((dropsiteDist > 7000 || territoryOwner === 0 ) && canFlee))) return; nearestSupplyDist = dist; nearestSupply = supply; }); if (nearestSupply) { if (position) return true; gameState.ai.HQ.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; }; m.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 = this.ent.getMetadata(PlayerID, "sea"); 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 !== m.getSeaAccess(gameState, dropsite)) continue; distMin = Math.min(distMin, API3.SquareVectorDistance(pos, dropsite.position())); } return distMin; }; let exhausted = true; resources.forEach(function(supply) { if (!supply.position()) return; // check that it is accessible if (gameState.ai.HQ.navalManager.getFishSea(gameState, supply) !== fisherSea) return; exhausted = false; if (m.IsSupplyFull(gameState, supply)) return; // check if available resource is worth one additionnal gatherer (except for farms) let nbGatherers = supply.resourceSupplyNumGatherers() + gameState.ai.HQ.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) { gameState.ai.HQ.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") === "fisher") this.ent.setMetadata(PlayerID, "subrole", "idle"); return false; }; m.Worker.prototype.gatherNearestField = function(gameState, baseID) { let ownFields = gameState.getOwnEntitiesByClass("Field", true).filter(API3.Filters.isBuilt()).filter(API3.Filters.byMetadata(PlayerID, "base", baseID)); let bestFarmEnt = false; let bestFarmDist = 10000000; for (let field of ownFields.values()) { if (m.IsSupplyFull(gameState, field)) continue; let dist = API3.SquareVectorDistance(field.position(), this.ent.position()); if (dist < bestFarmDist) { bestFarmEnt = field; bestFarmDist = dist; } } if (bestFarmEnt) { gameState.ai.HQ.AddTCGatherer(bestFarmEnt.id()); this.ent.setMetadata(PlayerID, "supply", bestFarmEnt.id()); } return bestFarmEnt; }; /** * 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. */ m.Worker.prototype.buildAnyField = function(gameState, baseID) { let baseFoundations = gameState.getOwnFoundations().filter(API3.Filters.byMetadata(PlayerID, "base", baseID)); let maxGatherers = gameState.getTemplate(gameState.applyCiv("structures/{civ}_field")).maxGatherers(); let bestFarmEnt = false; let bestFarmDist = 10000000; let pos = this.ent.position(); for (let found of baseFoundations.values()) { if (!found.hasClass("Field")) continue; let current = found.getBuildersNb(); if (current === undefined || current >= maxGatherers) continue; let dist = API3.SquareVectorDistance(found.position(), pos); if (dist > bestFarmDist) continue; bestFarmEnt = found; bestFarmDist = dist; } return bestFarmEnt; }; /** * Look for some treasure to gather */ m.Worker.prototype.gatherTreasure = function(gameState) { let rates = this.ent.resourceGatherRates(); if (!rates || !rates.treasure || rates.treasure <= 0) return false; let treasureFound; let distmin = Math.min(); let access = gameState.ai.accessibility.getAccessValue(this.ent.position()); for (let treasure of gameState.ai.HQ.treasures.values()) { if (m.IsSupplyFull(gameState, treasure)) continue; // let some time for the previous gatherer to reach the treasure befor trying again let lastGathered = treasure.getMetadata(PlayerID, "lastGathered"); if (lastGathered && gameState.ai.elapsedTime - lastGathered < 20) continue; if (access !== m.getLandAccess(gameState, treasure)) continue; let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(treasure.position()); if (territoryOwner !== 0 && !gameState.isPlayerAlly(territoryOwner)) continue; let dist = API3.SquareVectorDistance(this.ent.position(), treasure.position()); if (territoryOwner !== PlayerID && dist > 14000) // AI has no LOS, so restrict it a bit continue; if (dist > distmin) continue; distmin = dist; treasureFound = treasure; } if (!treasureFound) return false; treasureFound.setMetadata(PlayerID, "lastGathered", gameState.ai.elapsedTime); this.ent.gather(treasureFound); gameState.ai.HQ.AddTCGatherer(treasureFound.id()); this.ent.setMetadata(PlayerID, "supply", treasureFound.id()); return true; }; /** * 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) */ m.Worker.prototype.moveAway = function(gameState) { let gatherers = this.base.workersBySubrole(gameState, "gatherer"); let pos = this.ent.position(); let dist = Math.min(); let destination = pos; for (let gatherer of gatherers.values()) { if (!gatherer.position() || gatherer.getMetadata(PlayerID, "transport") !== undefined) continue; if (gatherer.isIdle()) continue; let distance = API3.SquareVectorDistance(pos, gatherer.position()); if (distance > dist) continue; dist = distance; destination = gatherer.position(); } this.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). */ m.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; let approachingTarget = this.ent.getMetadata(PlayerID, "approachingTarget"); 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, "carriedAmount", undefined); } let carriedAmount = this.ent.resourceCarrying().length ? this.ent.resourceCarrying()[0].amount : 0; if (this.ent.getMetadata(PlayerID, "carriedAmount") === undefined || this.ent.getMetadata(PlayerID, "carriedAmount") !== carriedAmount) { this.ent.setMetadata(PlayerID, "carriedAmount", carriedAmount); this.ent.setMetadata(PlayerID, "approachingTime", undefined); this.ent.setMetadata(PlayerID, "approachingPos", undefined); 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); } else if (gameState.ai.elapsedTime - approachingTime > 10) { target.setMetadata(PlayerID, "inaccessibleTime", gameState.ai.elapsedTime + 600); return true; } } return false; }; return m; }(PETRA);