Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackPlan.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackPlan.js (revision 20679) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackPlan.js (revision 20680) @@ -1,2117 +1,2118 @@ var PETRA = function(m) { /** * This is an attack plan: * It deals with everything in an attack, from picking a target to picking a path to it * To making sure units are built, and pushing elements to the queue manager otherwise * It also handles the actual attack, though much work is needed on that. */ m.AttackPlan = function(gameState, Config, uniqueID, type, data) { this.Config = Config; this.name = uniqueID; this.type = type || "Attack"; this.state = "unexecuted"; this.forced = false; // true when this attacked has been forced to help an ally if (data && data.target) { this.target = data.target; this.targetPos = this.target.position(); this.targetPlayer = this.target.owner(); } else { this.target = undefined; this.targetPos = undefined; this.targetPlayer = undefined; } this.uniqueTargetId = data && data.uniqueTargetId || undefined; // get a starting rallyPoint ... will be improved later let rallyPoint; let rallyAccess; let allAccesses = {}; for (let base of gameState.ai.HQ.baseManagers) { if (!base.anchor || !base.anchor.position()) continue; let access = gameState.ai.accessibility.getAccessValue(base.anchor.position()); if (!rallyPoint) { rallyPoint = base.anchor.position(); rallyAccess = access; } if (!allAccesses[access]) allAccesses[access] = base.anchor.position(); } if (!rallyPoint) // no base ? take the position of any of our entities { for (let ent of gameState.getOwnEntities().values()) { if (!ent.position()) continue; let access = gameState.ai.accessibility.getAccessValue(ent.position()); rallyPoint = ent.position(); rallyAccess = access; allAccesses[access] = rallyPoint; break; } if (!rallyPoint) { this.failed = true; return false; } } this.rallyPoint = rallyPoint; this.overseas = 0; if (gameState.ai.HQ.navalMap) { for (let structure of gameState.getEnemyStructures().values()) { if (!structure.position()) continue; let access = gameState.ai.accessibility.getAccessValue(structure.position()); if (access in allAccesses) { this.overseas = 0; this.rallyPoint = allAccesses[access]; break; } else if (!this.overseas) { let sea = gameState.ai.HQ.getSeaBetweenIndices(gameState, rallyAccess, access); if (!sea) continue; this.overseas = sea; gameState.ai.HQ.navalManager.setMinimalTransportShips(gameState, sea, 1); } } } this.paused = false; this.maxCompletingTime = 0; // priority of the queues we'll create. let priority = 70; // unitStat priority is relative. If all are 0, the only relevant criteria is "currentsize/targetsize". // if not, this is a "bonus". The higher the priority, the faster this unit will get built. // Should really be clamped to [0.1-1.5] (assuming 1 is default/the norm) // Eg: if all are priority 1, and the siege is 0.5, the siege units will get built // only once every other category is at least 50% of its target size. // note: siege build order is currently added by the military manager if a fortress is there. this.unitStat = {}; // neededShips is the minimal number of ships which should be available for transport if (type === "Rush") { priority = 250; this.unitStat.Infantry = { "priority": 1, "minSize": 10, "targetSize": 20, "batchSize": 2, "classes": ["Infantry"], "interests": [ ["strength", 1], ["costsResource", 0.5, "stone"], ["costsResource", 0.6, "metal"] ] }; this.unitStat.Cavalry = { "priority": 1, "minSize": 2, "targetSize": 4, "batchSize": 2, "classes": ["Cavalry", "CitizenSoldier"], "interests": [ ["strength", 1] ] }; if (data && data.targetSize) this.unitStat.Infantry.targetSize = data.targetSize; this.neededShips = 1; } else if (type === "Raid") { priority = 150; this.unitStat.Cavalry = { "priority": 1, "minSize": 3, "targetSize": 4, "batchSize": 2, "classes": ["Cavalry", "CitizenSoldier"], "interests": [ ["strength", 1] ] }; this.neededShips = 1; } else if (type === "HugeAttack") { priority = 90; // basically we want a mix of citizen soldiers so our barracks have a purpose, and champion units. this.unitStat.RangedInfantry = { "priority": 0.7, "minSize": 5, "targetSize": 20, "batchSize": 5, "classes": ["Infantry", "Ranged", "CitizenSoldier"], "interests": [["strength", 3] ] }; this.unitStat.MeleeInfantry = { "priority": 0.7, "minSize": 5, "targetSize": 20, "batchSize": 5, "classes": ["Infantry", "Melee", "CitizenSoldier"], "interests": [ ["strength", 3] ] }; this.unitStat.ChampRangedInfantry = { "priority": 1, "minSize": 3, "targetSize": 18, "batchSize": 3, "classes": ["Infantry", "Ranged", "Champion"], "interests": [["strength", 3] ] }; this.unitStat.ChampMeleeInfantry = { "priority": 1, "minSize": 3, "targetSize": 18, "batchSize": 3, "classes": ["Infantry", "Melee", "Champion"], "interests": [ ["strength", 3] ] }; this.unitStat.RangedCavalry = { "priority": 0.7, "minSize": 4, "targetSize": 20, "batchSize": 4, "classes": ["Cavalry", "Ranged", "CitizenSoldier"], "interests": [ ["strength", 2] ] }; this.unitStat.MeleeCavalry = { "priority": 0.7, "minSize": 4, "targetSize": 20, "batchSize": 4, "classes": ["Cavalry", "Melee", "CitizenSoldier"], "interests": [ ["strength", 2] ] }; this.unitStat.ChampRangedCavalry = { "priority": 1, "minSize": 3, "targetSize": 15, "batchSize": 3, "classes": ["Cavalry", "Ranged", "Champion"], "interests": [ ["strength", 3] ] }; this.unitStat.ChampMeleeCavalry = { "priority": 1, "minSize": 3, "targetSize": 15, "batchSize": 3, "classes": ["Cavalry", "Melee", "Champion"], "interests": [ ["strength", 2] ] }; this.unitStat.Hero = { "priority": 1, "minSize": 0, "targetSize": 1, "batchSize": 1, "classes": ["Hero"], "interests": [ ["strength", 2] ] }; this.neededShips = 5; } else { priority = 70; this.unitStat.RangedInfantry = { "priority": 1, "minSize": 6, "targetSize": 16, "batchSize": 3, "classes": ["Infantry","Ranged"], "interests": [ ["canGather", 1], ["strength", 1.6], ["costsResource", 0.3, "stone"], ["costsResource", 0.3, "metal"] ] }; this.unitStat.MeleeInfantry = { "priority": 1, "minSize": 6, "targetSize": 16, "batchSize": 3, "classes": ["Infantry","Melee"], "interests": [ ["canGather", 1], ["strength", 1.6], ["costsResource", 0.3, "stone"], ["costsResource", 0.3, "metal"] ] }; this.unitStat.Cavalry = { "priority": 1, "minSize": 2, "targetSize": 6, "batchSize": 2, "classes": ["Cavalry", "CitizenSoldier"], "interests": [ ["strength", 1] ] }; this.neededShips = 3; } // Put some randomness on the attack size let variation = randFloat(0.8, 1.2); // and lower priority and smaller sizes for easier difficulty levels if (this.Config.difficulty < 2) { priority *= 0.6; variation *= 0.5; } else if (this.Config.difficulty < 3) { priority *= 0.8; variation *= 0.8; } for (let cat in this.unitStat) { this.unitStat[cat].targetSize = Math.round(variation * this.unitStat[cat].targetSize); this.unitStat[cat].minSize = Math.min(this.unitStat[cat].minSize, this.unitStat[cat].targetSize); } // change the sizes according to max population this.neededShips = Math.ceil(this.Config.popScaling * this.neededShips); for (let cat in this.unitStat) { this.unitStat[cat].targetSize = Math.round(this.Config.popScaling * this.unitStat[cat].targetSize); this.unitStat[cat].minSize = Math.floor(this.Config.popScaling * this.unitStat[cat].minSize); } // TODO: there should probably be one queue per type of training building gameState.ai.queueManager.addQueue("plan_" + this.name, priority); gameState.ai.queueManager.addQueue("plan_" + this.name +"_champ", priority+1); gameState.ai.queueManager.addQueue("plan_" + this.name +"_siege", priority); // each array is [ratio, [associated classes], associated EntityColl, associated unitStat, name ] this.buildOrders = []; this.canBuildUnits = gameState.ai.HQ.canBuildUnits; this.siegeState = 0; // 0 = not yet tested, 1 = not yet any siege trainer, 2 = siege added in build orders // some variables used during the attack this.position5TurnsAgo = [0,0]; this.lastPosition = [0,0]; this.position = [0,0]; this.isBlocked = false; // true when this attack faces walls return true; }; m.AttackPlan.prototype.init = function(gameState) { this.queue = gameState.ai.queues["plan_" + this.name]; this.queueChamp = gameState.ai.queues["plan_" + this.name +"_champ"]; this.queueSiege = gameState.ai.queues["plan_" + this.name +"_siege"]; this.unitCollection = gameState.getOwnUnits().filter(API3.Filters.byMetadata(PlayerID, "plan", this.name)); this.unitCollection.registerUpdates(); this.unit = {}; // defining the entity collections. Will look for units I own, that are part of this plan. // Also defining the buildOrders. for (let cat in this.unitStat) { let Unit = this.unitStat[cat]; this.unit[cat] = this.unitCollection.filter(API3.Filters.byClassesAnd(Unit.classes)); this.unit[cat].registerUpdates(); if (this.canBuildUnits) this.buildOrders.push([0, Unit.classes, this.unit[cat], Unit, cat]); } }; m.AttackPlan.prototype.getName = function() { return this.name; }; m.AttackPlan.prototype.getType = function() { return this.type; }; m.AttackPlan.prototype.isStarted = function() { return this.state !== "unexecuted" && this.state !== "completing"; }; m.AttackPlan.prototype.isPaused = function() { return this.paused; }; m.AttackPlan.prototype.setPaused = function(boolValue) { this.paused = boolValue; }; /** * Returns true if the attack can be executed at the current time * Basically it checks we have enough units. */ m.AttackPlan.prototype.canStart = function() { if (!this.canBuildUnits) return true; for (let unitCat in this.unitStat) if (this.unit[unitCat].length < this.unitStat[unitCat].minSize) return false; return true; }; m.AttackPlan.prototype.mustStart = function() { if (this.isPaused()) return false; if (!this.canBuildUnits) return this.unitCollection.hasEntities(); let MaxReachedEverywhere = true; let MinReachedEverywhere = true; for (let unitCat in this.unitStat) { let Unit = this.unitStat[unitCat]; if (this.unit[unitCat].length < Unit.targetSize) MaxReachedEverywhere = false; if (this.unit[unitCat].length < Unit.minSize) { MinReachedEverywhere = false; break; } } if (MaxReachedEverywhere) return true; if (MinReachedEverywhere) return this.type == "Raid" && this.target && this.target.foundationProgress() && this.target.foundationProgress() > 50; return false; }; m.AttackPlan.prototype.forceStart = function() { for (let unitCat in this.unitStat) { let Unit = this.unitStat[unitCat]; Unit.targetSize = 0; Unit.minSize = 0; } this.forced = true; }; /** Adds a build order. If resetQueue is true, this will reset the queue. */ m.AttackPlan.prototype.addBuildOrder = function(gameState, name, unitStats, resetQueue) { if (!this.isStarted()) { // no minsize as we don't want the plan to fail at the last minute though. this.unitStat[name] = unitStats; let Unit = this.unitStat[name]; this.unit[name] = this.unitCollection.filter(API3.Filters.byClassesAnd(Unit.classes)); this.unit[name].registerUpdates(); this.buildOrders.push([0, Unit.classes, this.unit[name], Unit, name]); if (resetQueue) { this.queue.empty(); this.queueChamp.empty(); this.queueSiege.empty(); } } }; m.AttackPlan.prototype.addSiegeUnits = function(gameState) { if (this.siegeState == 2 || this.state !== "unexecuted") return false; let civ = gameState.getPlayerCiv(); let classes = [[ "Siege", "Melee"], ["Siege", "Ranged"], ["Elephant", "Melee", "Champion"]]; let hasTrainer = [false, false, false]; for (let ent of gameState.getOwnTrainingFacilities().values()) { let trainables = ent.trainableEntities(civ); if (!trainables) continue; for (let trainable of trainables) { if (gameState.isTemplateDisabled(trainable)) continue; let template = gameState.getTemplate(trainable); if (!template || !template.available(gameState)) continue; for (let i = 0; i < classes.length; ++i) if (classes[i].every(c => template.hasClass(c))) hasTrainer[i] = true; } } if (hasTrainer.every(e => !e)) return false; let i = this.name % classes.length; for (let k = 0; k < classes.length; ++k) { if (hasTrainer[i]) break; i = ++i % classes.length; } this.siegeState = 2; let targetSize; if (this.Config.difficulty < 3) targetSize = this.type == "HugeAttack" ? Math.max(this.Config.difficulty, 1) : Math.max(this.Config.difficulty - 1, 0); else targetSize = this.type == "HugeAttack" ? this.Config.difficulty + 1 : this.Config.difficulty - 1; targetSize = Math.max(Math.round(this.Config.popScaling * targetSize), this.type == "HugeAttack" ? 1 : 0); if (!targetSize) return true; // no minsize as we don't want the plan to fail at the last minute though. let stat = { "priority": 1, "minSize": 0, "targetSize": targetSize, "batchSize": Math.min(targetSize, 2), "classes": classes[i], "interests": [ ["siegeStrength", 3] ] }; this.addBuildOrder(gameState, "Siege", stat, true); return true; }; /** Three returns possible: 1 is "keep going", 0 is "failed plan", 2 is "start". */ m.AttackPlan.prototype.updatePreparation = function(gameState) { // the completing step is used to return resources and regroup the units // so we check that we have no more forced order before starting the attack if (this.state === "completing") { // if our target was destroyed, go back to "unexecuted" state if (this.targetPlayer === undefined || !this.target || !gameState.getEntityById(this.target.id())) { this.state = "unexecuted"; this.target = undefined; } else { // check that all units have finished with their transport if needed if (this.waitingForTransport()) return 1; // bloqued units which cannot finish their order should not stop the attack if (gameState.ai.elapsedTime < this.maxCompletingTime && this.hasForceOrder()) return 1; return 2; } } if (this.Config.debug > 3 && gameState.ai.playedTurn % 50 === 0) this.debugAttack(); // if we need a transport, wait for some transport ships if (this.overseas && !gameState.ai.HQ.navalManager.seaTransportShips[this.overseas].length) return 1; this.assignUnits(gameState); if (this.type !== "Raid" && gameState.ai.HQ.attackManager.getAttackInPreparation("Raid") !== undefined) this.reassignCavUnit(gameState); // reassign some cav (if any) to fasten raid preparations // special case: if we've reached max pop, and we can start the plan, start it. if (gameState.getPopulationMax() - gameState.getPopulation() < 5) { let lengthMin = 16; if (gameState.getPopulationMax() < 300) lengthMin -= Math.floor(8 * (300 - gameState.getPopulationMax()) / 300); if (this.canStart() || this.unitCollection.length > lengthMin) { this.queue.empty(); this.queueChamp.empty(); this.queueSiege.empty(); } else // Abort the plan so that its units will be reassigned to other plans. { if (this.Config.debug > 1) { let am = gameState.ai.HQ.attackManager; API3.warn(" attacks upcoming: raid " + am.upcomingAttacks.Raid.length + " rush " + am.upcomingAttacks.Rush.length + " attack " + am.upcomingAttacks.Attack.length + " huge " + am.upcomingAttacks.HugeAttack.length); API3.warn(" attacks started: raid " + am.startedAttacks.Raid.length + " rush " + am.startedAttacks.Rush.length + " attack " + am.startedAttacks.Attack.length + " huge " + am.startedAttacks.HugeAttack.length); } return 0; } } else if (this.mustStart() && gameState.countOwnQueuedEntitiesWithMetadata("plan", +this.name) > 0) { // keep on while the units finish being trained, then we'll start this.queue.empty(); this.queueChamp.empty(); this.queueSiege.empty(); return 1; } else if (!this.mustStart()) { if (this.canBuildUnits) { // We still have time left to recruit units and do stuffs. if (this.siegeState == 0 || this.siegeState == 1 && gameState.ai.playedTurn % 5 == 0) this.addSiegeUnits(gameState); this.trainMoreUnits(gameState); // may happen if we have no more training facilities and build orders are canceled if (!this.buildOrders.length) return 0; // will abort the plan } return 1; } // if we're here, it means we must start this.state = "completing"; if (!this.chooseTarget(gameState)) return 0; if (!this.overseas) this.getPathToTarget(gameState); if (this.type == "Raid") this.maxCompletingTime = gameState.ai.elapsedTime + 20; else { if (this.type == "Rush" || this.forced) this.maxCompletingTime = gameState.ai.elapsedTime + 40; else this.maxCompletingTime = gameState.ai.elapsedTime + 60; // warn our allies so that they can help if possible if (!this.requested) Engine.PostCommand(PlayerID, {"type": "attack-request", "source": PlayerID, "player": this.targetPlayer}); } // Remove those units which were in a temporary bombing attack for (let unitIds of gameState.ai.HQ.attackManager.bombingAttacks.values()) { for (let entId of unitIds.values()) { let ent = gameState.getEntityById(entId); if (!ent || ent.getMetadata(PlayerID, "plan") != this.name) continue; unitIds.delete(entId); ent.stopMoving(); } } let rallyPoint = this.rallyPoint; let rallyIndex = gameState.ai.accessibility.getAccessValue(rallyPoint); for (let ent of this.unitCollection.values()) { // For the time being, if occupied in a transport, remove the unit from this plan TODO improve that if (ent.getMetadata(PlayerID, "transport") !== undefined || ent.getMetadata(PlayerID, "transporter") !== undefined) { ent.setMetadata(PlayerID, "plan", -1); continue; } ent.setMetadata(PlayerID, "role", "attack"); ent.setMetadata(PlayerID, "subrole", "completing"); let queued = false; if (ent.resourceCarrying() && ent.resourceCarrying().length) queued = m.returnResources(gameState, ent); let index = gameState.ai.accessibility.getAccessValue(ent.position()); if (index === rallyIndex) ent.moveToRange(rallyPoint[0], rallyPoint[1], 0, 15, queued); else gameState.ai.HQ.navalManager.requireTransport(gameState, ent, index, rallyIndex, rallyPoint); } // reset all queued units let plan = this.name; gameState.ai.queueManager.removeQueue("plan_" + plan); gameState.ai.queueManager.removeQueue("plan_" + plan + "_champ"); gameState.ai.queueManager.removeQueue("plan_" + plan + "_siege"); return 1; }; m.AttackPlan.prototype.trainMoreUnits = function(gameState) { // let's sort by training advancement, ie 'current size / target size' // count the number of queued units too. // substract priority. for (let order of this.buildOrders) { let special = "Plan_" + this.name + "_" + order[4]; let aQueued = gameState.countOwnQueuedEntitiesWithMetadata("special", special); aQueued += this.queue.countQueuedUnitsWithMetadata("special", special); aQueued += this.queueChamp.countQueuedUnitsWithMetadata("special", special); aQueued += this.queueSiege.countQueuedUnitsWithMetadata("special", special); order[0] = order[2].length + aQueued; } this.buildOrders.sort(function (a,b) { let va = a[0]/a[3].targetSize - a[3].priority; if (a[0] >= a[3].targetSize) va += 1000; let vb = b[0]/b[3].targetSize - b[3].priority; if (b[0] >= b[3].targetSize) vb += 1000; return va - vb; }); if (this.Config.debug > 1 && gameState.ai.playedTurn%50 === 0) { API3.warn("===================================="); API3.warn("======== build order for plan " + this.name); for (let order of this.buildOrders) { let specialData = "Plan_"+this.name+"_"+order[4]; let inTraining = gameState.countOwnQueuedEntitiesWithMetadata("special", specialData); let queue1 = this.queue.countQueuedUnitsWithMetadata("special", specialData); let queue2 = this.queueChamp.countQueuedUnitsWithMetadata("special", specialData); let queue3 = this.queueSiege.countQueuedUnitsWithMetadata("special", specialData); API3.warn(" >>> " + order[4] + " done " + order[2].length + " training " + inTraining + " queue " + queue1 + " champ " + queue2 + " siege " + queue3 + " >> need " + order[3].targetSize); } API3.warn("===================================="); } let firstOrder = this.buildOrders[0]; if (firstOrder[0] < firstOrder[3].targetSize) { // find the actual queue we want let queue = this.queue; if (firstOrder[3].classes.indexOf("Siege") != -1 || firstOrder[3].classes.indexOf("Elephant") != -1 && firstOrder[3].classes.indexOf("Melee") != -1 && firstOrder[3].classes.indexOf("Champion") != -1) queue = this.queueSiege; else if (firstOrder[3].classes.indexOf("Hero") != -1) queue = this.queueSiege; else if (firstOrder[3].classes.indexOf("Champion") != -1) queue = this.queueChamp; if (queue.length() <= 5) { let template = gameState.ai.HQ.findBestTrainableUnit(gameState, firstOrder[1], firstOrder[3].interests); // HACK (TODO replace) : if we have no trainable template... Then we'll simply remove the buildOrder, // effectively removing the unit from the plan. if (template === undefined) { if (this.Config.debug > 1) API3.warn("attack no template found " + firstOrder[1]); delete this.unitStat[firstOrder[4]]; // deleting the associated unitstat. this.buildOrders.splice(0,1); } else { if (this.Config.debug > 2) API3.warn("attack template " + template + " added for plan " + this.name); let max = firstOrder[3].batchSize; let specialData = "Plan_" + this.name + "_" + firstOrder[4]; let data = { "plan": this.name, "special": specialData, "base": 0 }; data.role = gameState.getTemplate(template).hasClass("CitizenSoldier") ? "worker" : "attack"; let trainingPlan = new m.TrainingPlan(gameState, template, data, max, max); if (trainingPlan.template) queue.addPlan(trainingPlan); else if (this.Config.debug > 1) API3.warn("training plan canceled because no template for " + template + " build1 " + uneval(firstOrder[1]) + " build3 " + uneval(firstOrder[3].interests)); } } } }; m.AttackPlan.prototype.assignUnits = function(gameState) { let plan = this.name; let added = false; // If we can not build units, assign all available except those affected to allied defense to the current attack if (!this.canBuildUnits) { for (let ent of gameState.getOwnUnits().values()) { if (ent.getMetadata(PlayerID, "allied") || !this.isAvailableUnit(gameState, ent)) continue; ent.setMetadata(PlayerID, "plan", plan); this.unitCollection.updateEnt(ent); added = true; } return added; } if (this.type === "Raid") { // Raid are fast cavalry attack: assign all cav except some for hunting let num = 0; for (let ent of gameState.getOwnUnits().values()) { if (!ent.hasClass("Cavalry") || !this.isAvailableUnit(gameState, ent)) continue; if (num++ < 2) continue; ent.setMetadata(PlayerID, "plan", plan); this.unitCollection.updateEnt(ent); added = true; } return added; } // Assign all units without specific role for (let ent of gameState.getOwnEntitiesByRole(undefined, true).values()) { if (!ent.hasClass("Unit") || !this.isAvailableUnit(gameState, ent)) continue; if (ent.hasClass("Ship") || ent.hasClass("Support") || ent.attackTypes() === undefined) continue; ent.setMetadata(PlayerID, "plan", plan); this.unitCollection.updateEnt(ent); added = true; } // Add units previously in a plan, but which left it because needed for defense or attack finished for (let ent of gameState.ai.HQ.attackManager.outOfPlan.values()) { if (!this.isAvailableUnit(gameState, ent)) continue; ent.setMetadata(PlayerID, "plan", plan); this.unitCollection.updateEnt(ent); added = true; } // Finally add also some workers, // If Rush, assign all kind of workers, keeping only a minimum number of defenders // Otherwise, assign only some idle workers if too much of them let num = 0; let numbase = {}; let keep = this.type != "Rush" ? 6 + 4 * gameState.getNumPlayerEnemies() + 6 * this.Config.personality.defensive : 8; keep = Math.round(this.Config.popScaling * keep); for (let ent of gameState.getOwnEntitiesByRole("worker", true).values()) { if (!ent.hasClass("CitizenSoldier") || !this.isAvailableUnit(gameState, ent)) continue; let baseID = ent.getMetadata(PlayerID, "base"); if (baseID) numbase[baseID] = numbase[baseID] ? ++numbase[baseID] : 1; else { API3.warn("Petra problem ent without base "); m.dumpEntity(ent); continue; } if (this.type !== "Rush" && ent.getMetadata(PlayerID, "subrole") !== "idle") continue; if (num++ < keep || numbase[baseID] < 5) continue; ent.setMetadata(PlayerID, "plan", plan); this.unitCollection.updateEnt(ent); added = true; } return added; }; m.AttackPlan.prototype.isAvailableUnit = function(gameState, ent) { if (!ent.position()) return false; if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") !== -1 || ent.getMetadata(PlayerID, "transport") !== undefined || ent.getMetadata(PlayerID, "transporter") !== undefined) return false; if (gameState.ai.HQ.gameTypeManager.criticalEnts.has(ent.id()) && (this.overseas || ent.healthLevel() < 0.8)) return false; return true; }; /** Reassign one (at each turn) Cav unit to fasten raid preparation. */ m.AttackPlan.prototype.reassignCavUnit = function(gameState) { let found; for (let ent of this.unitCollection.values()) { if (!ent.position() || ent.getMetadata(PlayerID, "transport") !== undefined) continue; if (!ent.hasClass("Cavalry") || !ent.hasClass("CitizenSoldier")) continue; found = ent; break; } if (!found) return; let raid = gameState.ai.HQ.attackManager.getAttackInPreparation("Raid"); found.setMetadata(PlayerID, "plan", raid.name); this.unitCollection.updateEnt(found); raid.unitCollection.updateEnt(found); }; m.AttackPlan.prototype.chooseTarget = function(gameState) { if (this.targetPlayer === undefined) { this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this); if (this.targetPlayer === undefined) return false; } this.target = this.getNearestTarget(gameState, this.rallyPoint); if (!this.target) { if (this.uniqueTargetId) return false; // may-be all our previous enemey target (if not recomputed here) have been destroyed ? this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this); if (this.targetPlayer !== undefined) this.target = this.getNearestTarget(gameState, this.rallyPoint); if (!this.target) return false; } this.targetPos = this.target.position(); // redefine a new rally point for this target if we have a base on the same land // find a new one on the pseudo-nearest base (dist weighted by the size of the island) let targetIndex = gameState.ai.accessibility.getAccessValue(this.targetPos); let rallyIndex = gameState.ai.accessibility.getAccessValue(this.rallyPoint); if (targetIndex !== rallyIndex) { let distminSame = Math.min(); let rallySame; let distminDiff = Math.min(); let rallyDiff; for (let base of gameState.ai.HQ.baseManagers) { let anchor = base.anchor; if (!anchor || !anchor.position()) continue; let dist = API3.SquareVectorDistance(anchor.position(), this.targetPos); if (base.accessIndex === targetIndex) { if (dist >= distminSame) continue; distminSame = dist; rallySame = anchor.position(); } else { dist = dist / Math.sqrt(gameState.ai.accessibility.regionSize[base.accessIndex]); if (dist >= distminDiff) continue; distminDiff = dist; rallyDiff = anchor.position(); } } if (rallySame) { this.rallyPoint = rallySame; this.overseas = 0; } else if (rallyDiff) { rallyIndex = gameState.ai.accessibility.getAccessValue(rallyDiff); this.rallyPoint = rallyDiff; this.overseas = gameState.ai.HQ.getSeaBetweenIndices(gameState, rallyIndex, targetIndex); if (this.overseas) gameState.ai.HQ.navalManager.setMinimalTransportShips(gameState, this.overseas, this.neededShips); else return false; } } else if (this.overseas) this.overseas = 0; return true; }; /** * sameLand true means that we look for a target for which we do not need to take a transport */ m.AttackPlan.prototype.getNearestTarget = function(gameState, position, sameLand) { this.isBlocked = false; let targets; if (this.uniqueTargetId) { targets = new API3.EntityCollection(gameState.sharedScript); let ent = gameState.getEntityById(this.uniqueTargetId); if (ent) targets.addEnt(ent); } else { if (this.type === "Raid") targets = this.raidTargetFinder(gameState); else if (this.type === "Rush" || this.type === "Attack") { targets = this.rushTargetFinder(gameState, this.targetPlayer); if (!targets.hasEntities() && (this.hasSiegeUnits() || this.forced)) targets = this.defaultTargetFinder(gameState, this.targetPlayer); } else targets = this.defaultTargetFinder(gameState, this.targetPlayer); } if (!targets.hasEntities()) return undefined; let land = gameState.ai.accessibility.getAccessValue(position); // picking the nearest target let target; let minDist = Math.min(); for (let ent of targets.values()) { if (this.targetPlayer === 0 && gameState.getGameType() === "capture_the_relic" && (!ent.hasClass("Relic") || gameState.ai.HQ.gameTypeManager.targetedGaiaRelics.has(ent.id()))) continue; if (!ent.position()) continue; if (sameLand && gameState.ai.accessibility.getAccessValue(ent.position()) !== land) continue; let dist = API3.SquareVectorDistance(ent.position(), position); // Do not bother with decaying structure if they are not dangerous if (ent.decaying() && !ent.getDefaultArrow() && (!ent.isGarrisonHolder() || !ent.garrisoned().length)) continue; // In normal attacks, disfavor fields if (this.type !== "Rush" && this.type !== "Raid" && ent.hasClass("Field")) dist += 100000; if (dist < minDist) { minDist = dist; target = ent; } } if (!target) return undefined; // Check that we can reach this target target = this.checkTargetObstruction(gameState, target, position); if (!target) return undefined; if (this.targetPlayer === 0 && gameState.getGameType() === "capture_the_relic" && target.hasClass("Relic")) gameState.ai.HQ.gameTypeManager.targetedGaiaRelics.add(target.id()); // Rushes can change their enemy target if nothing found with the preferred enemy // Obstruction also can change the enemy target this.targetPlayer = target.owner(); return target; }; /** Default target finder aims for conquest critical targets */ m.AttackPlan.prototype.defaultTargetFinder = function(gameState, playerEnemy) { let targets; if (gameState.getGameType() === "wonder") targets = gameState.getEnemyStructures(playerEnemy).filter(API3.Filters.byClass("Wonder")); else if (gameState.getGameType() === "regicide") targets = gameState.getEnemyUnits(playerEnemy).filter(API3.Filters.byClass("Hero")); else if (gameState.getGameType() === "capture_the_relic") targets = gameState.updatingGlobalCollection("allRelics", API3.Filters.byClass("Relic")).filter(relic => relic.owner() === playerEnemy); if (targets && targets.hasEntities()) return targets; targets = gameState.getEnemyStructures(playerEnemy).filter(API3.Filters.byClass("CivCentre")); if (!targets.hasEntities()) targets = gameState.getEnemyStructures(playerEnemy).filter(API3.Filters.byClass("ConquestCritical")); // If there's nothing, attack anything else that's less critical if (!targets.hasEntities()) targets = gameState.getEnemyStructures(playerEnemy).filter(API3.Filters.byClass("Town")); if (!targets.hasEntities()) targets = gameState.getEnemyStructures(playerEnemy).filter(API3.Filters.byClass("Village")); // no buildings, attack anything conquest critical, even units if (!targets.hasEntities()) targets = gameState.getEntities(playerEnemy).filter(API3.Filters.byClass("ConquestCritical")); return targets; }; /** Rush target finder aims at isolated non-defended buildings */ m.AttackPlan.prototype.rushTargetFinder = function(gameState, playerEnemy) { let targets = new API3.EntityCollection(gameState.sharedScript); let buildings; if (playerEnemy !== undefined) buildings = gameState.getEnemyStructures(playerEnemy).toEntityArray(); else buildings = gameState.getEnemyStructures().toEntityArray(); if (!buildings.length) return targets; this.position = this.unitCollection.getCentrePosition(); if (!this.position) this.position = this.rallyPoint; let target; let minDist = Math.min(); for (let building of buildings) { if (building.owner() === 0) continue; if (building.hasDefensiveFire()) continue; let pos = building.position(); let defended = false; for (let defense of buildings) { if (!defense.hasDefensiveFire()) continue; let dist = API3.SquareVectorDistance(pos, defense.position()); if (dist < 6400) // TODO check on defense range rather than this fixed 80*80 { defended = true; break; } } if (defended) continue; let dist = API3.SquareVectorDistance(pos, this.position); if (dist > minDist) continue; minDist = dist; target = building; } if (target) targets.addEnt(target); if (!targets.hasEntities()) { if (this.type === "Attack") targets = this.defaultTargetFinder(gameState, playerEnemy); else if (this.type === "Rush" && playerEnemy) targets = this.rushTargetFinder(gameState); } return targets; }; /** Raid target finder aims at destructing foundations from which our defenseManager has attacked the builders */ m.AttackPlan.prototype.raidTargetFinder = function(gameState) { let targets = new API3.EntityCollection(gameState.sharedScript); for (let targetId of gameState.ai.HQ.defenseManager.targetList) { let target = gameState.getEntityById(targetId); if (target && target.position()) targets.addEnt(target); } return targets; }; /** * Check that we can have a path to this target * otherwise we may be blocked by walls and try to react accordingly * This is done only when attacker and target are on the same land */ m.AttackPlan.prototype.checkTargetObstruction = function(gameState, target, position) { let targetPos = target.position(); if (gameState.ai.accessibility.getAccessValue(targetPos) !== gameState.ai.accessibility.getAccessValue(position)) return target; let startPos = { "x": position[0], "y": position[1] }; let endPos = { "x": targetPos[0], "y": targetPos[1] }; let blocker; let path = Engine.ComputePath(startPos, endPos, gameState.getPassabilityClassMask("default")); if (!path.length) return undefined; let pathPos = [path[0].x, path[0].y]; let dist = API3.VectorDistance(pathPos, targetPos); let radius = target.obstructionRadius().max; for (let struct of gameState.getEnemyStructures().values()) { if (!struct.position() || !struct.get("Obstruction") || struct.hasClass("Field")) continue; // we consider that we can reach the target, but nonetheless check that we did not cross any enemy gate if (dist < radius + 10 && !struct.hasClass("Gates")) continue; // Check that we are really blocked by this structure, i.e. advancing by 1+0.8(clearance)m // in the target direction would bring us inside its obstruction. let structPos = struct.position(); let x = pathPos[0] - structPos[0] + 1.8 * (targetPos[0] - pathPos[0]) / dist; let y = pathPos[1] - structPos[1] + 1.8 * (targetPos[1] - pathPos[1]) / dist; if (struct.get("Obstruction/Static")) { if (!struct.angle()) continue; let angle = struct.angle(); let width = +struct.get("Obstruction/Static/@width"); let depth = +struct.get("Obstruction/Static/@depth"); let cosa = Math.cos(angle); let sina = Math.sin(angle); let u = x * cosa - y * sina; let v = x * sina + y * cosa; if (Math.abs(u) < width/2 && Math.abs(v) < depth/2) { blocker = struct; break; } } else if (struct.get("Obstruction/Obstructions")) { if (!struct.angle()) continue; let angle = struct.angle(); let width = +struct.get("Obstruction/Obstructions/Door/@width"); let depth = +struct.get("Obstruction/Obstructions/Door/@depth"); let doorHalfWidth = width / 2; width += +struct.get("Obstruction/Obstructions/Left/@width"); depth = Math.max(depth, +struct.get("Obstruction/Obstructions/Left/@depth")); width += +struct.get("Obstruction/Obstructions/Right/@width"); depth = Math.max(depth, +struct.get("Obstruction/Obstructions/Right/@depth")); let cosa = Math.cos(angle); let sina = Math.sin(angle); let u = x * cosa - y * sina; let v = x * sina + y * cosa; if (Math.abs(u) < width/2 && Math.abs(v) < depth/2) { blocker = struct; break; } // check that the path does not cross this gate (could happen if not locked) for (let i = 1; i < path.length; ++i) { let u1 = (path[i-1].x - structPos[0]) * cosa - (path[i-1].y - structPos[1]) * sina; let v1 = (path[i-1].x - structPos[0]) * sina + (path[i-1].y - structPos[1]) * cosa; let u2 = (path[i].x - structPos[0]) * cosa - (path[i].y - structPos[1]) * sina; let v2 = (path[i].x - structPos[0]) * sina + (path[i].y - structPos[1]) * cosa; if (v1 * v2 < 0) { let u0 = (u1*v2 - u2*v1) / (v2-v1); if (Math.abs(u0) > doorHalfWidth) continue; blocker = struct; break; } } if (blocker) break; } else if (struct.get("Obstruction/Unit")) { let r = +this.get("Obstruction/Unit/@radius"); if (x*x + y*y < r*r) { blocker = struct; break; } } } if (blocker && blocker.hasClass("StoneWall")) { /* if (this.hasSiegeUnits()) { */ this.isBlocked = true; return blocker; /* } return undefined; */ } else if (blocker) { this.isBlocked = true; return blocker; } return target; }; m.AttackPlan.prototype.getPathToTarget = function(gameState) { let startAccess = gameState.ai.accessibility.getAccessValue(this.rallyPoint); let endAccess = gameState.ai.accessibility.getAccessValue(this.targetPos); if (startAccess != endAccess) return false; Engine.ProfileStart("AI Compute path"); let startPos = { "x": this.rallyPoint[0], "y": this.rallyPoint[1] }; let endPos = { "x": this.targetPos[0], "y": this.targetPos[1] }; let path = Engine.ComputePath(startPos, endPos, gameState.getPassabilityClassMask("large")); this.path = []; this.path.push(this.targetPos); for (let p in path) this.path.push([path[p].x, path[p].y]); this.path.push(this.rallyPoint); this.path.reverse(); // Change the rally point to something useful this.setRallyPoint(gameState); Engine.ProfileStop(); return true; }; /** Set rally point at the border of our territory */ m.AttackPlan.prototype.setRallyPoint = function(gameState) { for (let i = 0; i < this.path.length; ++i) { if (gameState.ai.HQ.territoryMap.getOwner(this.path[i]) === PlayerID) continue; if (i === 0) this.rallyPoint = this.path[0]; else if (i > 1 && gameState.ai.HQ.isDangerousLocation(gameState, this.path[i-1], 20)) { this.rallyPoint = this.path[i-2]; this.path.splice(0, i-2); } else { this.rallyPoint = this.path[i-1]; this.path.splice(0, i-1); } break; } }; /** * Executes the attack plan, after this is executed the update function will be run every turn * If we're here, it's because we have enough units. */ m.AttackPlan.prototype.StartAttack = function(gameState) { if (this.Config.debug > 1) API3.warn("start attack " + this.name + " with type " + this.type); // if our target was destroyed during preparation, choose a new one if (this.targetPlayer === undefined || !this.target || !gameState.getEntityById(this.target.id())) { if (!this.chooseTarget(gameState)) return false; } // check we have a target and a path. if (this.targetPos && (this.overseas || this.path)) { // erase our queue. This will stop any leftover unit from being trained. gameState.ai.queueManager.removeQueue("plan_" + this.name); gameState.ai.queueManager.removeQueue("plan_" + this.name + "_champ"); gameState.ai.queueManager.removeQueue("plan_" + this.name + "_siege"); for (let ent of this.unitCollection.values()) ent.setMetadata(PlayerID, "subrole", "walking"); this.unitCollection.setStance("aggressive"); if (gameState.ai.accessibility.getAccessValue(this.targetPos) === gameState.ai.accessibility.getAccessValue(this.rallyPoint)) { if (!this.path[0][0] || !this.path[0][1]) { if (this.Config.debug > 1) API3.warn("StartAttack: Problem with path " + uneval(this.path)); return false; } this.state = "walking"; this.unitCollection.moveToRange(this.path[0][0], this.path[0][1], 0, 15); } else { this.state = "transporting"; let startIndex = gameState.ai.accessibility.getAccessValue(this.rallyPoint); let endIndex = gameState.ai.accessibility.getAccessValue(this.targetPos); let endPos = this.targetPos; // TODO require a global transport for the collection, // and put back its state to "walking" when the transport is finished for (let ent of this.unitCollection.values()) gameState.ai.HQ.navalManager.requireTransport(gameState, ent, startIndex, endIndex, endPos); } } else { gameState.ai.gameFinished = true; API3.warn("I do not have any target. So I'll just assume I won the game."); return false; } return true; }; /** Runs every turn after the attack is executed */ m.AttackPlan.prototype.update = function(gameState, events) { if (!this.unitCollection.hasEntities()) return 0; Engine.ProfileStart("Update Attack"); this.position = this.unitCollection.getCentrePosition(); let self = this; // we are transporting our units, let's wait // TODO instead of state "arrived", made a state "walking" with a new path if (this.state === "transporting") this.UpdateTransporting(gameState, events); if (this.state === "walking" && !this.UpdateWalking(gameState, events)) { Engine.ProfileStop(); return 0; } if (this.state === "arrived") { // let's proceed on with whatever happens now. this.state = ""; this.startingAttack = true; this.unitCollection.forEach( function (ent) { ent.stopMoving(); ent.setMetadata(PlayerID, "subrole", "attacking"); }); if (this.type === "Rush") // try to find a better target for rush { let newtarget = this.getNearestTarget(gameState, this.position); if (newtarget) { this.target = newtarget; this.targetPos = this.target.position(); } } } // basic state of attacking. if (this.state === "") { // First update the target and/or its position if needed if (!this.UpdateTarget(gameState, events)) { Engine.ProfileStop(); return false; } let time = gameState.ai.elapsedTime; let attackedByStructure = {}; for (let evt of events.Attacked) { if (!this.unitCollection.hasEntId(evt.target)) continue; let attacker = gameState.getEntityById(evt.attacker); let ourUnit = gameState.getEntityById(evt.target); if (!ourUnit || !attacker || !attacker.position()) continue; if (!attacker.hasClass("Unit")) { attackedByStructure[evt.target] = true; continue; } if (m.isSiegeUnit(ourUnit)) { // if our siege units are attacked, we'll send some units to deal with enemies. let collec = this.unitCollection.filter(API3.Filters.not(API3.Filters.byClass("Siege"))).filterNearest(ourUnit.position(), 5); for (let ent of collec.values()) { if (m.isSiegeUnit(ent)) // needed as mauryan elephants are not filtered out continue; ent.attack(attacker.id(), m.allowCapture(gameState, ent, attacker)); ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); } // And if this attacker is a non-ranged siege unit and our unit also, attack it if (m.isSiegeUnit(attacker) && attacker.hasClass("Melee") && ourUnit.hasClass("Melee")) { ourUnit.attack(attacker.id(), m.allowCapture(gameState, ourUnit, attacker)); ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); } } else { if (this.isBlocked && !ourUnit.hasClass("Ranged") && attacker.hasClass("Ranged")) { // do not react if our melee units are attacked by ranged one and we are blocked by walls // TODO check that the attacker is from behind the wall continue; } else if (m.isSiegeUnit(attacker)) { // if our unit is attacked by a siege unit, we'll send some melee units to help it. let collec = this.unitCollection.filter(API3.Filters.byClass("Melee")).filterNearest(ourUnit.position(), 5); for (let ent of collec.values()) { ent.attack(attacker.id(), m.allowCapture(gameState, ent, attacker)); ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); } } else { // Look first for nearby units to help us if possible let collec = this.unitCollection.filterNearest(ourUnit.position(), 2); for (let ent of collec.values()) { if (m.isSiegeUnit(ent)) continue; let orderData = ent.unitAIOrderData(); if (orderData && orderData.length && orderData[0].target) { if (orderData[0].target === attacker.id()) continue; let target = gameState.getEntityById(orderData[0].target); if (target && !target.hasClass("Structure") && !target.hasClass("Support")) continue; } ent.attack(attacker.id(), m.allowCapture(gameState, ent, attacker)); ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); } // Then the unit under attack: abandon its target (if it was a structure or a support) and retaliate // also if our unit is attacking a range unit and the attacker is a melee unit, retaliate let orderData = ourUnit.unitAIOrderData(); if (orderData && orderData.length && orderData[0].target) { if (orderData[0].target === attacker.id()) continue; let target = gameState.getEntityById(orderData[0].target); if (target && !target.hasClass("Structure") && !target.hasClass("Support")) { if (!target.hasClass("Ranged") || !attacker.hasClass("Melee")) continue; } } ourUnit.attack(attacker.id(), m.allowCapture(gameState, ourUnit, attacker)); ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); } } } let enemyUnits = gameState.getEnemyUnits(this.targetPlayer); let enemyStructures = gameState.getEnemyStructures(this.targetPlayer); // Count the number of times an enemy is targeted, to prevent all units to follow the same target let unitTargets = {}; for (let ent of this.unitCollection.values()) { if (ent.hasClass("Ship")) // TODO What to do with ships continue; let orderData = ent.unitAIOrderData(); if (!orderData || !orderData.length || !orderData[0].target) continue; let targetId = orderData[0].target; let target = gameState.getEntityById(targetId); if (!target || target.hasClass("Structure")) continue; if (!(targetId in unitTargets)) { if (m.isSiegeUnit(target) || target.hasClass("Hero")) unitTargets[targetId] = -8; else if (target.hasClass("Champion") || target.hasClass("Ship")) unitTargets[targetId] = -5; else unitTargets[targetId] = -3; } ++unitTargets[targetId]; } let veto = {}; for (let target in unitTargets) if (unitTargets[target] > 0) veto[target] = true; let targetClassesUnit; let targetClassesSiege; if (this.type === "Rush") targetClassesUnit = {"attack": ["Unit", "Structure"], "avoid": ["Palisade", "StoneWall", "Tower", "Fortress"], "vetoEntities": veto}; else { if (this.target.hasClass("Fortress")) targetClassesUnit = {"attack": ["Unit", "Structure"], "avoid": ["Palisade", "StoneWall"], "vetoEntities": veto}; else if (this.target.hasClass("Palisade") || this.target.hasClass("StoneWall")) targetClassesUnit = {"attack": ["Unit", "Structure"], "avoid": ["Fortress"], "vetoEntities": veto}; else targetClassesUnit = {"attack": ["Unit", "Structure"], "avoid": ["Palisade", "StoneWall", "Fortress"], "vetoEntities": veto}; } if (this.target.hasClass("Structure")) targetClassesSiege = {"attack": ["Structure"], "avoid": [], "vetoEntities": veto}; else targetClassesSiege = {"attack": ["Unit", "Structure"], "avoid": [], "vetoEntities": veto}; // do not loose time destroying buildings which do not help enemy's defense and can be easily captured later if (this.target.hasDefensiveFire()) { targetClassesUnit.avoid = targetClassesUnit.avoid.concat("House", "Storehouse", "Farmstead", "Field", "Blacksmith"); targetClassesSiege.avoid = targetClassesSiege.avoid.concat("House", "Storehouse", "Farmstead", "Field", "Blacksmith"); } if (this.unitCollUpdateArray === undefined || !this.unitCollUpdateArray.length) this.unitCollUpdateArray = this.unitCollection.toIdArray(); // Let's check a few units each time we update (currently 10) except when attack starts let lgth = this.unitCollUpdateArray.length < 15 || this.startingAttack ? this.unitCollUpdateArray.length : 10; for (let check = 0; check < lgth; check++) { let ent = gameState.getEntityById(this.unitCollUpdateArray[check]); if (!ent || !ent.position()) continue; // Do not reaffect units which have reacted to an attack in that same turn if (ent.getMetadata(PlayerID, "lastAttackPlanUpdateTime") == time) continue; let targetId; let orderData = ent.unitAIOrderData(); if (orderData && orderData.length && orderData[0].target) targetId = orderData[0].target; // update the order if needed let needsUpdate = false; let maybeUpdate = false; let siegeUnit = m.isSiegeUnit(ent); if (ent.isIdle()) needsUpdate = true; else if (siegeUnit && targetId) { let target = gameState.getEntityById(targetId); if (!target || gameState.isPlayerAlly(target.owner())) needsUpdate = true; else if (unitTargets[targetId] && unitTargets[targetId] > 0) { needsUpdate = true; --unitTargets[targetId]; } else if (!target.hasClass("Structure")) maybeUpdate = true; } else if (targetId) { let target = gameState.getEntityById(targetId); if (!target || gameState.isPlayerAlly(target.owner())) needsUpdate = true; else if (unitTargets[targetId] && unitTargets[targetId] > 0) { needsUpdate = true; --unitTargets[targetId]; } else if (target.hasClass("Ship") && !ent.hasClass("Ship")) maybeUpdate = true; else if (attackedByStructure[ent.id()] && target.hasClass("Field")) maybeUpdate = true; else if (!ent.hasClass("Cavalry") && !ent.hasClass("Ranged") && target.hasClass("FemaleCitizen") && target.unitAIState().split(".")[1] == "FLEEING") maybeUpdate = true; } // don't update too soon if not necessary if (!needsUpdate) { if (!maybeUpdate) continue; let deltat = ent.unitAIState() === "INDIVIDUAL.COMBAT.APPROACHING" ? 10 : 5; let lastAttackPlanUpdateTime = ent.getMetadata(PlayerID, "lastAttackPlanUpdateTime"); if (lastAttackPlanUpdateTime && time - lastAttackPlanUpdateTime < deltat) continue; } ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); let range = 60; let attackTypes = ent.attackTypes(); if (this.isBlocked) { if (attackTypes && attackTypes.indexOf("Ranged") !== -1) range = ent.attackRange("Ranged").max; else if (attackTypes && attackTypes.indexOf("Melee") !== -1) range = ent.attackRange("Melee").max; else range = 10; } else if (attackTypes && attackTypes.indexOf("Ranged") !== -1) range = 30 + ent.attackRange("Ranged").max; else if (ent.hasClass("Cavalry")) range += 30; range = range * range; let entIndex = gameState.ai.accessibility.getAccessValue(ent.position()); // Checking for gates if we're a siege unit. if (siegeUnit) { let mStruct = enemyStructures.filter(function (enemy) { if (!enemy.position() || enemy.hasClass("StoneWall") && !ent.canAttackClass("StoneWall")) return false; if (API3.SquareVectorDistance(enemy.position(), ent.position()) > range) return false; if (enemy.foundationProgress() === 0) return false; if (gameState.ai.accessibility.getAccessValue(enemy.position()) !== entIndex) return false; return true; }).toEntityArray(); if (mStruct.length) { mStruct.sort(function (structa, structb) { let vala = structa.costSum(); if (structa.hasClass("Gates") && ent.canAttackClass("StoneWall")) vala += 10000; else if (structa.hasDefensiveFire()) vala += 1000; else if (structa.hasClass("ConquestCritical")) vala += 200; let valb = structb.costSum(); if (structb.hasClass("Gates") && ent.canAttackClass("StoneWall")) valb += 10000; else if (structb.hasDefensiveFire()) valb += 1000; else if (structb.hasClass("ConquestCritical")) valb += 200; return valb - vala; }); if (mStruct[0].hasClass("Gates")) ent.attack(mStruct[0].id(), m.allowCapture(gameState, ent, mStruct[0])); else { let rand = randIntExclusive(0, mStruct.length * 0.2); ent.attack(mStruct[rand].id(), m.allowCapture(gameState, ent, mStruct[rand])); } } else { if (!ent.hasClass("Ranged")) { let targetClasses = {"attack": targetClassesSiege.attack, "avoid": targetClassesSiege.avoid.concat("Ship"), "vetoEntities": veto}; ent.attackMove(this.targetPos[0], this.targetPos[1], targetClasses); } else ent.attackMove(this.targetPos[0], this.targetPos[1], targetClassesSiege); } } else { let nearby = !ent.hasClass("Cavalry") && !ent.hasClass("Ranged"); let mUnit = enemyUnits.filter(function (enemy) { if (!enemy.position()) return false; if (enemy.hasClass("Animal")) return false; if (nearby && enemy.hasClass("FemaleCitizen") && enemy.unitAIState().split(".")[1] == "FLEEING") return false; let dist = API3.SquareVectorDistance(enemy.position(), ent.position()); if (dist > range) return false; if (gameState.ai.accessibility.getAccessValue(enemy.position()) !== entIndex) return false; // if already too much units targeting this enemy, let's continue towards our main target if (veto[enemy.id()] && API3.SquareVectorDistance(self.targetPos, ent.position()) > 2500) return false; enemy.setMetadata(PlayerID, "distance", Math.sqrt(dist)); return true; }).toEntityArray(); if (mUnit.length !== 0) { mUnit.sort(function (unitA,unitB) { let vala = unitA.hasClass("Support") ? 50 : 0; if (ent.countersClasses(unitA.classes())) vala += 100; let valb = unitB.hasClass("Support") ? 50 : 0; if (ent.countersClasses(unitB.classes())) valb += 100; let distA = unitA.getMetadata(PlayerID, "distance"); let distB = unitB.getMetadata(PlayerID, "distance"); if( distA && distB) { vala -= distA; valb -= distB; } if (veto[unitA.id()]) vala -= 20000; if (veto[unitB.id()]) valb -= 20000; return valb - vala; }); let rand = randIntExclusive(0, mUnit.length * 0.1); ent.attack(mUnit[rand].id(), m.allowCapture(gameState, ent, mUnit[rand])); } else if (this.isBlocked) ent.attack(this.target.id(), false); else if (API3.SquareVectorDistance(this.targetPos, ent.position()) > 2500 ) { let targetClasses = targetClassesUnit; if (maybeUpdate && ent.unitAIState() === "INDIVIDUAL.COMBAT.APPROACHING") // we may be blocked by walls, attack everything { if (!ent.hasClass("Ranged") && !ent.hasClass("Ship")) targetClasses = {"attack": ["Unit", "Structure"], "avoid": ["Ship"], "vetoEntities": veto}; else targetClasses = {"attack": ["Unit", "Structure"], "vetoEntities": veto}; } else if (!ent.hasClass("Ranged") && !ent.hasClass("Ship")) targetClasses = {"attack": targetClassesUnit.attack, "avoid": targetClassesUnit.avoid.concat("Ship"), "vetoEntities": veto}; ent.attackMove(this.targetPos[0], this.targetPos[1], targetClasses); } else { let mStruct = enemyStructures.filter(function (enemy) { if (self.isBlocked && enemy.id() !== this.target.id()) return false; if (!enemy.position() || enemy.hasClass("StoneWall") && !ent.canAttackClass("StoneWall")) return false; if (API3.SquareVectorDistance(enemy.position(), ent.position()) > range) return false; if (gameState.ai.accessibility.getAccessValue(enemy.position()) !== entIndex) return false; return true; }).toEntityArray(); if (mStruct.length !== 0) { mStruct.sort(function (structa,structb) { let vala = structa.costSum(); if (structa.hasClass("Gates") && ent.canAttackClass("StoneWall")) vala += 10000; else if (structa.hasClass("ConquestCritical")) vala += 100; let valb = structb.costSum(); if (structb.hasClass("Gates") && ent.canAttackClass("StoneWall")) valb += 10000; else if (structb.hasClass("ConquestCritical")) valb += 100; return valb - vala; }); if (mStruct[0].hasClass("Gates")) ent.attack(mStruct[0].id(), false); else { let rand = randIntExclusive(0, mStruct.length * 0.2); ent.attack(mStruct[rand].id(), m.allowCapture(gameState, ent, mStruct[rand])); } } else if (needsUpdate) // really nothing let's try to help our nearest unit { let distmin = Math.min(); let attacker; this.unitCollection.forEach( function (unit) { if (!unit.position()) return; if (unit.unitAIState().split(".")[1] !== "COMBAT" || !unit.unitAIOrderData().length || !unit.unitAIOrderData()[0].target) return; if (!gameState.getEntityById(unit.unitAIOrderData()[0].target)) return; let dist = API3.SquareVectorDistance(unit.position(), ent.position()); if (dist > distmin) return; distmin = dist; attacker = gameState.getEntityById(unit.unitAIOrderData()[0].target); }); if (attacker) ent.attack(attacker.id(), m.allowCapture(gameState, ent, attacker)); } } } } this.unitCollUpdateArray.splice(0, lgth); this.startingAttack = false; // check if this enemy has resigned if (this.target && this.target.owner() === 0 && this.targetPlayer !== 0) this.target = undefined; } this.lastPosition = this.position; Engine.ProfileStop(); return this.unitCollection.length; }; m.AttackPlan.prototype.UpdateTransporting = function(gameState, events) { let done = true; for (let ent of this.unitCollection.values()) { if (this.Config.debug > 1 && ent.getMetadata(PlayerID, "transport") !== undefined) Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [2,2,0]}); else if (this.Config.debug > 1) Engine.PostCommand(PlayerID,{"type": "set-shading-color", "entities": [ent.id()], "rgb": [1,1,1]}); if (!done) continue; if (ent.getMetadata(PlayerID, "transport") !== undefined) done = false; } if (done) { this.state = "arrived"; return; } // if we are attacked while waiting the rest of the army, retaliate for (let evt of events.Attacked) { if (!this.unitCollection.hasEntId(evt.target)) continue; let attacker = gameState.getEntityById(evt.attacker); if (!attacker || !gameState.getEntityById(evt.target)) continue; for (let ent of this.unitCollection.values()) { if (ent.getMetadata(PlayerID, "transport") !== undefined) continue; if (!ent.isIdle()) continue; ent.attack(attacker.id(), m.allowCapture(gameState, ent, attacker)); } break; } }; m.AttackPlan.prototype.UpdateWalking = function(gameState, events) { // we're marching towards the target // Let's check if any of our unit has been attacked. // In case yes, we'll determine if we're simply off against an enemy army, a lone unit/building // or if we reached the enemy base. Different plans may react differently. let attackedNB = 0; let attackedUnitNB = 0; for (let evt of events.Attacked) { if (!this.unitCollection.hasEntId(evt.target)) continue; let attacker = gameState.getEntityById(evt.attacker); if (attacker && (attacker.owner() !== 0 || this.targetPlayer === 0)) { attackedNB++; if (attacker.hasClass("Unit")) attackedUnitNB++; } } // Are we arrived at destination ? if (attackedNB > 1 && (attackedUnitNB || this.hasSiegeUnits())) { if (gameState.ai.HQ.territoryMap.getOwner(this.position) === this.targetPlayer || attackedNB > 3) { this.state = "arrived"; return true; } } // basically haven't moved an inch: very likely stuck) if (API3.SquareVectorDistance(this.position, this.position5TurnsAgo) < 10 && this.path.length > 0 && gameState.ai.playedTurn % 5 === 0) { // check for stuck siege units let farthest = 0; let farthestEnt; for (let ent of this.unitCollection.filter(API3.Filters.byClass("Siege")).values()) { let dist = API3.SquareVectorDistance(ent.position(), this.position); if (dist < farthest) continue; farthest = dist; farthestEnt = ent; } if (farthestEnt) farthestEnt.destroy(); } if (gameState.ai.playedTurn % 5 === 0) this.position5TurnsAgo = this.position; - if (this.lastPosition && API3.SquareVectorDistance(this.position, this.lastPosition) < 20 && this.path.length > 0) + if (this.lastPosition && API3.SquareVectorDistance(this.position, this.lastPosition) < 16 && this.path.length > 0) { if (!this.path[0][0] || !this.path[0][1]) API3.warn("Start: Problem with path " + uneval(this.path)); - // We're stuck, presumably. Check if there are no walls just close to us. If so, we're arrived, and we're gonna tear down some serious stone. - let nexttoWalls = false; - for (let ent of gameState.getEnemyStructures().filter(API3.Filters.byClass("StoneWall")).values()) + // We're stuck, presumably. Check if there are no walls just close to us. + for (let ent of gameState.getEnemyStructures().filter(API3.Filters.byClass(["Palisade", "StoneWall"])).values()) { - if (!nexttoWalls && API3.SquareVectorDistance(this.position, ent.position()) < 800) - nexttoWalls = true; - } - // there are walls but we can attack - if (nexttoWalls && this.unitCollection.filter(API3.Filters.byCanAttackClass("StoneWall")).hasEntities()) - { - if (this.Config.debug > 1) - API3.warn("Attack Plan " + this.type + " " + this.name + " has met walls and is not happy."); - this.state = "arrived"; - return true; - } - else if (nexttoWalls) // abort plan - { - if (this.Config.debug > 1) - API3.warn("Attack Plan " + this.type + " " + this.name + " has met walls and gives up."); - return false; + if (API3.SquareVectorDistance(this.position, ent.position()) > 800) + continue; + let enemyClass = ent.hasClass("StoneWall") ? "StoneWall" : "Palisade"; + // there are walls, so check if we can attack + if (this.unitCollection.filter(API3.Filters.byCanAttackClass(enemyClass)).hasEntities()) + { + if (this.Config.debug > 1) + API3.warn("Attack Plan " + this.type + " " + this.name + " has met walls and is not happy."); + this.state = "arrived"; + return true; + } + else // abort plan + { + if (this.Config.debug > 1) + API3.warn("Attack Plan " + this.type + " " + this.name + " has met walls and gives up."); + return false; + } + break; } //this.unitCollection.move(this.path[0][0], this.path[0][1]); this.unitCollection.moveIndiv(this.path[0][0], this.path[0][1]); } // check if our units are close enough from the next waypoint. if (API3.SquareVectorDistance(this.position, this.targetPos) < 10000) { if (this.Config.debug > 1) API3.warn("Attack Plan " + this.type + " " + this.name + " has arrived to destination."); this.state = "arrived"; return true; } else if (this.path.length && API3.SquareVectorDistance(this.position, this.path[0]) < 1600) { this.path.shift(); if (this.path.length) this.unitCollection.moveToRange(this.path[0][0], this.path[0][1], 0, 15); else { if (this.Config.debug > 1) API3.warn("Attack Plan " + this.type + " " + this.name + " has arrived to destination."); this.state = "arrived"; return true; } } return true; }; m.AttackPlan.prototype.UpdateTarget = function(gameState, events) { // First update the target position in case it's a unit (and check if it has garrisoned) if (this.target && this.target.hasClass("Unit")) { this.targetPos = this.target.position(); if (!this.targetPos) { let holder = m.getHolder(gameState, this.target); if (holder && gameState.isPlayerEnemy(holder.owner())) { this.target = holder; this.targetPos = holder.position(); } else this.target = undefined; } } // Then update the target if needed: if (this.targetPlayer === undefined || !gameState.isPlayerEnemy(this.targetPlayer)) { this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this); if (this.targetPlayer === undefined) return false; if (this.target && this.target.owner() !== this.targetPlayer) this.target = undefined; } if (this.target && this.target.owner() === 0 && this.targetPlayer !== 0) // this enemy has resigned this.target = undefined; if (!this.target || !gameState.getEntityById(this.target.id())) { if (this.Config.debug > 1) API3.warn("Seems like our target for plan " + this.name + " has been destroyed or captured. Switching."); this.target = this.getNearestTarget(gameState, this.position, true); if (!this.target) { if (this.uniqueTargetId) return false; // Check if we could help any current attack let attackManager = gameState.ai.HQ.attackManager; let accessIndex = gameState.ai.accessibility.getAccessValue(this.position); for (let attackType in attackManager.startedAttacks) { for (let attack of attackManager.startedAttacks[attackType]) { if (attack.name === this.name) continue; if (!attack.target || !gameState.getEntityById(attack.target.id()) || !gameState.isPlayerEnemy(attack.target.owner())) continue; if (accessIndex !== gameState.ai.accessibility.getAccessValue(attack.targetPos)) continue; if (attack.target.owner() === 0 && attack.targetPlayer !== 0) // looks like it has resigned continue; if (!gameState.isPlayerEnemy(attack.targetPlayer)) continue; this.target = attack.target; this.targetPlayer = attack.targetPlayer; this.targetPos = this.target.position(); return true; } } // If not, let's look for another enemy if (!this.target) { this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this); if (this.targetPlayer !== undefined) this.target = this.getNearestTarget(gameState, this.position, true); if (!this.target) { if (this.Config.debug > 1) API3.warn("No new target found. Remaining units " + this.unitCollection.length); return false; } } if (this.Config.debug > 1) API3.warn("We will help one of our other attacks"); } this.targetPos = this.target.position(); } return true; }; /** reset any units */ m.AttackPlan.prototype.Abort = function(gameState) { this.unitCollection.unregister(); if (this.unitCollection.hasEntities()) { // If the attack was started, look for a good rallyPoint to withdraw let rallyPoint; if (this.isStarted()) { let access = gameState.ai.accessibility.getAccessValue(this.position); let dist = Math.min(); if (this.rallyPoint && gameState.ai.accessibility.getAccessValue(this.rallyPoint) == access) { rallyPoint = this.rallyPoint; dist = API3.SquareVectorDistance(this.position, rallyPoint); } // Then check if we have a nearer base (in case this attack has captured one) for (let base of gameState.ai.HQ.baseManagers) { if (!base.anchor || !base.anchor.position()) continue; if (gameState.ai.accessibility.getAccessValue(base.anchor.position()) != access) continue; let newdist = API3.SquareVectorDistance(this.position, base.anchor.position()); if (newdist > dist) continue; dist = newdist; rallyPoint = base.anchor.position(); } } for (let ent of this.unitCollection.values()) { if (ent.getMetadata(PlayerID, "role") === "attack") ent.stopMoving(); if (rallyPoint) ent.moveToRange(rallyPoint[0], rallyPoint[1], 0, 15); this.removeUnit(ent); } } for (let unitCat in this.unitStat) this.unit[unitCat].unregister(); gameState.ai.queueManager.removeQueue("plan_" + this.name); gameState.ai.queueManager.removeQueue("plan_" + this.name + "_champ"); gameState.ai.queueManager.removeQueue("plan_" + this.name + "_siege"); }; m.AttackPlan.prototype.removeUnit = function(ent, update) { if (ent.hasClass("CitizenSoldier") && ent.getMetadata(PlayerID, "role") !== "worker") { ent.setMetadata(PlayerID, "role", "worker"); ent.setMetadata(PlayerID, "subrole", undefined); } ent.setMetadata(PlayerID, "plan", -1); if (update) this.unitCollection.updateEnt(ent); }; m.AttackPlan.prototype.checkEvents = function(gameState, events) { for (let evt of events.EntityRenamed) { if (!this.target || this.target.id() != evt.entity) continue; this.target = gameState.getEntityById(evt.newentity); if (this.target) this.targetPos = this.target.position(); } for (let evt of events.OwnershipChanged) // capture event if (this.target && this.target.id() == evt.entity && gameState.isPlayerAlly(evt.to)) this.target = undefined; for (let evt of events.PlayerDefeated) { if (this.targetPlayer !== evt.playerId) continue; this.targetPlayer = gameState.ai.HQ.attackManager.getEnemyPlayer(gameState, this); this.target = undefined; } if (!this.overseas || this.state !== "unexecuted") return; // let's check if an enemy has built a structure at our access for (let evt of events.Create) { let ent = gameState.getEntityById(evt.entity); if (!ent || !ent.position() || !ent.hasClass("Structure")) continue; if (!gameState.isPlayerEnemy(ent.owner())) continue; let access = gameState.ai.accessibility.getAccessValue(ent.position()); for (let base of gameState.ai.HQ.baseManagers) { if (!base.anchor || !base.anchor.position()) continue; if (base.accessIndex !== access) continue; this.overseas = 0; this.rallyPoint = base.anchor.position(); } } }; m.AttackPlan.prototype.waitingForTransport = function() { for (let ent of this.unitCollection.values()) if (ent.getMetadata(PlayerID, "transport") !== undefined) return true; return false; }; m.AttackPlan.prototype.hasSiegeUnits = function() { for (let ent of this.unitCollection.values()) if (m.isSiegeUnit(ent)) return true; return false; }; m.AttackPlan.prototype.hasForceOrder = function(data, value) { for (let ent of this.unitCollection.values()) { if (data && +ent.getMetadata(PlayerID, data) !== value) continue; let orders = ent.unitAIOrderData(); for (let order of orders) if (order.force) return true; } return false; }; m.AttackPlan.prototype.debugAttack = function() { API3.warn("---------- attack " + this.name); for (let unitCat in this.unitStat) { let Unit = this.unitStat[unitCat]; API3.warn(unitCat + " num=" + this.unit[unitCat].length + " min=" + Unit.minSize + " need=" + Unit.targetSize); } API3.warn("------------------------------"); }; m.AttackPlan.prototype.Serialize = function() { let properties = { "name": this.name, "type": this.type, "state": this.state, "forced": this.forced, "rallyPoint": this.rallyPoint, "overseas": this.overseas, "paused": this.paused, "maxCompletingTime": this.maxCompletingTime, "neededShips": this.neededShips, "unitStat": this.unitStat, "siegeState": this.siegeState, "position5TurnsAgo": this.position5TurnsAgo, "lastPosition": this.lastPosition, "position": this.position, "isBlocked": this.isBlocked, "targetPlayer": this.targetPlayer, "target": this.target !== undefined ? this.target.id() : undefined, "targetPos": this.targetPos, "uniqueTargetId": this.uniqueTargetId, "path": this.path }; return { "properties": properties}; }; m.AttackPlan.prototype.Deserialize = function(gameState, data) { for (let key in data.properties) this[key] = data.properties[key]; if (this.target) this.target = gameState.getEntityById(this.target); this.failed = undefined; }; return m; }(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/config.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/config.js (revision 20679) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/config.js (revision 20680) @@ -1,257 +1,257 @@ var PETRA = function(m) { m.Config = function(difficulty, behavior) { // 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; // for instance "balanced", "aggressive" or "defensive" this.behavior = behavior || "random"; // 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 = { "popPhase2" : 38, // How many units we want before aging to phase2. "workPhase3" : 65, // How many workers we want before aging to phase3. "workPhase4" : 80, // How many workers we want before aging to phase4 or higher. "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.35, // fraction of support workers among the workforce "provisionFields" : 2 }; // Note: attack settings are set directly in attack_plan.js // defense this.Defense = { "defenseRatio" : { "ally": 1.4, "neutral": 1.8, "own": 2 }, // ratio of defenders/attackers. "armyCompactSize" : 2000, // squared. Half-diameter of an army. "armyBreakawaySize" : 3500, // squared. "armyMergeSize" : 1400 // squared. }; this.buildings = { "advanced": { "default": [], "athen": [ "structures/{civ}_gymnasion", "structures/{civ}_prytaneion", "structures/{civ}_theatron", "structures/{civ}_royal_stoa" ], "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}_library", "structures/{civ}_theatron" ], "maur": [ "structures/{civ}_pillar_ashoka" ], "pers": [ "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", "structures/{civ}_royal_stoa" ] } }; this.priorities = { "villager": 30, // should be slightly lower than the citizen soldier one to not get all the food "citizenSoldier": 60, "trader": 50, "healer": 20, "ships": 70, "house": 350, "dropsites": 200, "field": 400, "dock": 90, "corral": 60, "economicBuilding": 90, "militaryBuilding": 130, "defenseBuilding": 70, "civilCentre": 950, "majorTech": 700, "minorTech": 40, "wonder": 1000, "emergency": 1000 // used only in emergency situations, should be the highest one }; // Default personality (will be updated in setConfig) 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 } }; this.garrisonHealthLevel = { "low": 0.4, "medium": 0.55, "high": 0.7 }; }; m.Config.prototype.setConfig = function(gameState) { if (this.difficulty > 0) { // Setup personality traits according to the user choice: // The parameter used to define the personality is basically the aggressivity or (1-defensiveness) // as they are anticorrelated, although some small smearing to decorelate them will be added. // And for each user choice, this parameter can vary between min and max let personalityList = { "random": { "min": 0, "max": 1 }, "defensive": { "min": 0, "max": 0.27 }, "balanced": { "min": 0.37, "max": 0.63 }, "aggressive": { "min": 0.73, "max": 1 } }; let behavior = randFloat(-0.5, 0.5); // make agressive and defensive quite anticorrelated (aggressive ~ 1 - defensive) but not completelety let variation = 0.15 * randFloat(-1, 1) * Math.sqrt(Math.square(0.5) - Math.square(behavior)); let aggressive = Math.max(Math.min(behavior + variation, 0.5), -0.5) + 0.5; let defensive = Math.max(Math.min(-behavior + variation, 0.5), -0.5) + 0.5; let min = personalityList[this.behavior].min; let max = personalityList[this.behavior].max; this.personality = { "aggressive": min + aggressive * (max - min), "defensive": 1 - max + defensive * (max - min), "cooperative": randFloat(0, 1) }; } // Petra usually uses the continuous values of personality.aggressive and personality.defensive // to define its behavior according to personality. But when discontinuous behavior is needed, // it uses the following personalityCut which should be set such that: // behavior="aggressive" => personality.aggressive > personalityCut.strong && // personality.defensive < personalityCut.weak // and inversely for behavior="defensive" this.personalityCut = { "weak": 0.3, "medium": 0.5, "strong": 0.7 }; if (gameState.playerData.teamsLocked) this.personality.cooperative = Math.min(1, this.personality.cooperative + 0.30); else 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.supportRatio = 0.5; this.Economy.provisionFields = 1; this.Military.numSentryTowers = this.personality.defensive > 0.66 ? 1 : 0; } else if (this.difficulty < 3) { 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)); + 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) + if (this.personality.aggressive > this.personalityCut.strong) { this.Military.popForBarracks1 = 12; this.Economy.popPhase2 = 50; this.Economy.popForMarket = 60; this.priorities.defenseBuilding = 60; this.priorities.healer = 10; } } 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 (gameState.getGameType() === "wonder") { this.Economy.workPhase3 = Math.floor(0.9 * this.Economy.workPhase3); this.Economy.workPhase4 = Math.floor(0.9 * this.Economy.workPhase4); } 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.popPhase2 = Math.min(Math.max(Math.floor(this.Economy.popPhase2 * this.popScaling), 20), Math.floor(maxPop/2)); this.Economy.workPhase3 = Math.min(Math.max(Math.floor(this.Economy.workPhase3 * this.popScaling), 40), Math.floor(maxPop*2/3)); this.Economy.workPhase4 = Math.min(Math.max(Math.floor(this.Economy.workPhase4 * this.popScaling), 45), 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.popPhase2); this.Economy.workPhase3 = Math.min(this.Economy.workPhase3, this.Economy.targetNumWorkers); this.Economy.workPhase4 = Math.min(this.Economy.workPhase4, this.Economy.targetNumWorkers); if (this.difficulty < 2) this.Economy.workPhase3 = Infinity; // prevent the phasing to city phase 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 20679) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/worker.js (revision 20680) @@ -1,1037 +1,1037 @@ 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 ((subrole === "hunter" || subrole === "gatherer") && (unitAIState === "INDIVIDUAL.GATHER.GATHERING" || unitAIState === "INDIVIDUAL.GATHER.APPROACHING" || unitAIState === "INDIVIDUAL.COMBAT.APPROACHING")) { if (this.isInaccessibleSupply(gameState) && !this.retryGathering(gameState, subrole)) ent.stopMoving(); // Check that we have not drifted too far if (unitAIState === "INDIVIDUAL.COMBAT.APPROACHING" && ent.unitAIOrderData().length) { let orderData = ent.unitAIOrderData()[0]; if (orderData && orderData.target) { let supply = gameState.getEntityById(orderData.target); if (supply && supply.resourceSupplyType() && supply.resourceSupplyType().generic === "food") { let territoryOwner = gameState.ai.HQ.territoryMap.getOwner(supply.position()); if (gameState.isPlayerEnemy(territoryOwner) && !this.retryGathering(gameState, subrole)) ent.stopMoving(); else if (!gameState.isPlayerAlly(territoryOwner)) { let distanceSquare = ent.hasClass("Cavalry") ? 90000 : 30000; let supplyAccess = gameState.ai.accessibility.getAccessValue(supply.position()); 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 (supplyAccess !== m.getLandAccess(gameState, dropsite)) continue; if (API3.SquareVectorDistance(supply.position(), dropsite.position()) < distanceSquare) { hasFoodDropsiteWithinDistance = true; break; } } if (!hasFoodDropsiteWithinDistance && !this.retryGathering(gameState, subrole)) 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 // 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; if (nearby.some(sup => sup.id == supplyId)) ent.setMetadata(PlayerID, "supply", supplyId); else if (nearby.length) { gameState.ai.HQ.RemoveTCGatherer(supplyId); this.startGathering(gameState); } else { let medium = this.base.dropsiteSupplies[gatherType].medium; if (medium.length && !medium.some(sup => sup.id == supplyId)) { gameState.ai.HQ.RemoveTCGatherer(supplyId); this.startGathering(gameState); } else ent.setMetadata(PlayerID, "supply", supplyId); } } } } } else if (unitAIState == "INDIVIDUAL.RETURNRESOURCE.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()) { 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); } } // 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 === "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", "idle"); ent.stopMoving(); if (this.baseID !== gameState.ai.HQ.baseManagers[0].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", "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"), 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.retryGathering = function(gameState, subrole) { switch (subrole) { case "gatherer": return this.startGathering(gameState); case "hunter": return this.startHunting(gameState); case "fisher": return this.startFishing(gameState); default: return false; } }; m.Worker.prototype.startGathering = function(gameState) { let access = gameState.ai.accessibility.getAccessValue(this.ent.position()); // First look for possible treasure if any if (m.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; let findSupply = function(ent, supplies) { 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 (m.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) 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) { supply = findSupply(this.ent, 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.ent, 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 (let base of gameState.ai.HQ.baseManagers) { if (base.ID === this.baseID) continue; if (base.accessIndex !== access) continue; supply = findSupply(this.ent, 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 (let base of gameState.ai.HQ.baseManagers) { if (base.ID === this.baseID) continue; if (base.accessIndex !== access) 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 (let base of gameState.ai.HQ.baseManagers) { if (base.ID === this.baseID) continue; if (base.accessIndex !== access) continue; supply = findSupply(this.ent, 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 || foundation.getMetadata(PlayerID, "access") !== access) 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.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; supply = findSupply(this.ent, base.dropsiteSupplies[resource].nearby); if (supply && 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; supply = this.gatherNearestField(gameState, base.ID); if (supply && navalManager.requireTransport(gameState, this.ent, access, 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, 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; supply = findSupply(this.ent, base.dropsiteSupplies[resource].medium); if (supply && 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; let structure = gameState.getBuiltTemplate(foundation.templateName()); if (structure.resourceDropsiteTypes() && structure.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 // 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.accessIndex === access) { supply = findSupply(this.ent, this.base.dropsiteSupplies[resource].faraway); if (supply) { 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; supply = findSupply(this.ent, base.dropsiteSupplies[resource].faraway); if (supply) { 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; supply = findSupply(this.ent, base.dropsiteSupplies[resource].faraway); if (supply && 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 && m.gatherTreasure(gameState, this.ent)) 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"); 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 !== m.getLandAccess(gameState, dropsite)) continue; if (API3.SquareVectorDistance(supplyPosition, dropsite.position()) < distSquare) return true; } return false; }; let gatherRates = this.ent.resourceGatherRates(); resources.forEach(function(supply) { if (!supply.position()) return; let inaccessibleTime = supply.getMetadata(PlayerID, "inaccessibleTime"); if (inaccessibleTime && gameState.ai.elapsedTime < inaccessibleTime) return; let supplyType = supply.get("ResourceSupply/Type"); if (!gatherRates[supplyType]) 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; // Only cavalry should hunt far from dropsite (specially for non domestic animals which flee) if (!isCavalry && canFlee && territoryOwner === 0) return; let distanceSquare = isCavalry ? 35000 : ( canFlee ? 7000 : 12000); if (!hasFoodDropsiteWithinDistance(supply.position(), supplyAccess, distanceSquare)) 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; let gatherRates = this.ent.resourceGatherRates(); 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; let supplyType = supply.get("ResourceSupply/Type"); if (!gatherRates[supplyType]) 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; // 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 bestFarm; let gatherRates = this.ent.resourceGatherRates(); for (let field of ownFields.values()) { if (m.IsSupplyFull(gameState, field)) continue; let supplyType = field.get("ResourceSupply/Type"); if (!gatherRates[supplyType]) continue; let rate = 1; let diminishing = field.getDiminishingReturns(); if (diminishing < 1) { let num = field.resourceSupplyNumGatherers() + gameState.ai.HQ.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; gameState.ai.HQ.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. */ m.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 fieldTemplate = gameState.getBuiltTemplate(found.templateName()); let current = found.getBuildersNb(); - if (current === undefined || current >= fieldTemplate.maxGatherers()) + 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) */ 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; 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; }; return m; }(PETRA);