Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackPlan.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackPlan.js (revision 19546) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackPlan.js (revision 19547) @@ -1,1996 +1,1991 @@ 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"; 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; } // 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], ["cost",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], ["cost",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], ["cost",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], ["cost",1] ] }; this.unitStat.MeleeInfantry = { "priority": 0.7, "minSize": 5, "targetSize": 20, "batchSize": 5, "classes": ["Infantry", "Melee", "CitizenSoldier"], "interests": [ ["strength",3], ["cost",1] ] }; this.unitStat.ChampRangedInfantry = { "priority": 1, "minSize": 3, "targetSize": 18, "batchSize": 3, "classes": ["Infantry", "Ranged", "Champion"], "interests": [["strength",3], ["cost",1] ] }; this.unitStat.ChampMeleeInfantry = { "priority": 1, "minSize": 3, "targetSize": 18, "batchSize": 3, "classes": ["Infantry", "Melee", "Champion"], "interests": [ ["strength",3], ["cost",1] ] }; this.unitStat.RangedCavalry = { "priority": 0.7, "minSize": 4, "targetSize": 20, "batchSize": 4, "classes": ["Cavalry", "Ranged", "CitizenSoldier"], "interests": [ ["strength",2], ["cost",1] ] }; this.unitStat.MeleeCavalry = { "priority": 0.7, "minSize": 4, "targetSize": 20, "batchSize": 4, "classes": ["Cavalry", "Melee", "CitizenSoldier"], "interests": [ ["strength",2], ["cost",1] ] }; this.unitStat.ChampRangedCavalry = { "priority": 1, "minSize": 3, "targetSize": 15, "batchSize": 3, "classes": ["Cavalry", "Ranged", "Champion"], "interests": [ ["strength",3], ["cost",1] ] }; this.unitStat.ChampMeleeCavalry = { "priority": 1, "minSize": 3, "targetSize": 15, "batchSize": 3, "classes": ["Cavalry", "Melee", "Champion"], "interests": [ ["strength",2], ["cost",1] ] }; this.unitStat.Hero = { "priority": 1, "minSize": 0, "targetSize": 1, "batchSize": 1, "classes": ["Hero"], "interests": [ ["strength",2], ["cost",1] ] }; 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], ["cost",1.5], ["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], ["cost",1.5], ["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], ["cost",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.6; } 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.buildOrder = []; this.canBuildUnits = gameState.ai.HQ.canBuildUnits; // 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.buildOrder.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) { if (this.type === "Raid" && this.target && this.target.foundationProgress() && this.target.foundationProgress() > 60) return true; } return false; }; m.AttackPlan.prototype.forceStart = function() { for (let unitCat in this.unitStat) { let Unit = this.unitStat[unitCat]; Unit.targetSize = 0; Unit.minSize = 0; } }; /** 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.buildOrder.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.unitStat.Siege || this.state !== "unexecuted") return false; // no minsize as we don't want the plan to fail at the last minute though. let stat = { "priority": 1, "minSize": 0, "targetSize": 4, "batchSize": 2, "classes": ["Siege"], "interests": [ ["siegeStrength", 3], ["cost",1] ] }; if (gameState.getPlayerCiv() === "maur") stat.classes = ["Elephant", "Champion"]; if (this.Config.difficulty < 2) stat.targetSize = 1; else if (this.Config.difficulty < 3) stat.targetSize = 2; stat.targetSize = Math.round(this.Config.popScaling * stat.targetSize); 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.unitStat.Siege) { let numSiegeBuilder = 0; let playerCiv = gameState.getPlayerCiv(); if (playerCiv !== "mace" && playerCiv !== "maur") numSiegeBuilder += gameState.getOwnEntitiesByClass("Fortress", true).filter(API3.Filters.isBuilt()).length; if (playerCiv === "mace" || playerCiv === "maur" || playerCiv === "rome") numSiegeBuilder += gameState.countEntitiesByType(gameState.ai.HQ.bAdvanced[0], true); if (numSiegeBuilder > 0) this.addSiegeUnits(gameState); } this.trainMoreUnits(gameState); // may happen if we have no more training facilities and build orders are canceled if (!this.buildOrder.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.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}); } 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 i = 0; i < this.buildOrder.length; ++i) { let special = "Plan_" + this.name + "_" + this.buildOrder[i][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); this.buildOrder[i][0] = this.buildOrder[i][2].length + aQueued; } this.buildOrder.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.buildOrder) { 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.buildOrder[0]; if (firstOrder[0] < firstOrder[3].targetSize) { // find the actual queue we want let queue = this.queue; if (firstOrder[3].classes.indexOf("Siege") !== -1 || (gameState.getPlayerCiv() == "maur" && firstOrder[3].classes.indexOf("Elephant") !== -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.buildOrder.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" ? Math.round(this.Config.popScaling * (12 + 4*this.Config.personality.defensive)) : 6; 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) { // 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.type === "Raid") targets = this.raidTargetFinder(gameState); else if (this.type === "Rush" || this.type === "Attack") targets = this.rushTargetFinder(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 (!ent.position()) continue; if (sameLand && gameState.ai.accessibility.getAccessValue(ent.position()) !== land) continue; let dist = API3.SquareVectorDistance(ent.position(), position); // 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; // 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.getEnemyUnits(playerEnemy).filter(API3.Filters.byClass("Relic")); 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(); 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(gameState)) +/* 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.move(this.path[0][0], this.path[0][1]); } 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; for (let evt of events.Attacked) { if (!this.unitCollection.hasEntId(evt.target)) continue; let attacker = gameState.getEntityById(evt.attacker); if (!attacker || !attacker.position() || !attacker.hasClass("Unit")) continue; let ourUnit = gameState.getEntityById(evt.target); - if (this.isSiegeUnit(gameState, ourUnit)) + 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 (this.isSiegeUnit(gameState, ent)) // needed as mauryan elephants are not filtered out + 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 (this.isSiegeUnit(gameState, attacker) && attacker.hasClass("Melee") && ourUnit.hasClass("Melee")) + 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 (this.isSiegeUnit(gameState, attacker)) + 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 { // if units are attacked, abandon their 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 (this.isSiegeUnit(gameState, target) || target.hasClass("Hero")) + 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; 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 = this.isSiegeUnit(gameState, ent); + 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 (!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(gameState))) + 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.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()) { 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.byCanAttack("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; } //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.move(this.path[0][0], this.path[0][1]); 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 has been destroyed. Switching."); this.target = this.getNearestTarget(gameState, this.position, true); if (!this.target) { // 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())) 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, and we are on the same land as the rallyPoint, go back there let rallyPoint = this.rallyPoint; let withdrawal = this.isStarted() && !this.overseas; for (let ent of this.unitCollection.values()) { if (ent.getMetadata(PlayerID, "role") === "attack") ent.stopMoving(); if (withdrawal) ent.move(rallyPoint[0], rallyPoint[1]); 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(gameState) +m.AttackPlan.prototype.hasSiegeUnits = function() { for (let ent of this.unitCollection.values()) - if (this.isSiegeUnit(gameState, ent)) + 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.isSiegeUnit = function(gameState, ent) -{ - return ent.hasClass("Siege") || (ent.hasClass("Elephant") && ent.hasClass("Champion")); -}; - 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, "rallyPoint": this.rallyPoint, "overseas": this.overseas, "paused": this.paused, "maxCompletingTime": this.maxCompletingTime, "neededShips": this.neededShips, "unitStat": this.unitStat, "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, "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/defenseManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/defenseManager.js (revision 19546) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/defenseManager.js (revision 19547) @@ -1,732 +1,743 @@ var PETRA = function(m) { m.DefenseManager = function(Config) { this.armies = []; // array of "army" Objects this.Config = Config; this.targetList = []; this.armyMergeSize = this.Config.Defense.armyMergeSize; this.attackingArmies = {}; // stats on how many enemies are currently attacking our allies this.attackingUnits = {}; this.attackedAllies = {}; }; m.DefenseManager.prototype.update = function(gameState, events) { Engine.ProfileStart("Defense Manager"); this.territoryMap = gameState.ai.HQ.territoryMap; this.checkEvents(gameState, events); // Check if our potential targets are still valid for (let i = 0; i < this.targetList.length; ++i) { let target = gameState.getEntityById(this.targetList[i]); if (!target || !target.position() || !gameState.isPlayerEnemy(target.owner())) this.targetList.splice(i--, 1); } // Count the number of enemies attacking our allies in the previous turn // We'll be more cooperative if several enemies are attacking him simultaneously this.attackedAllies = {}; let attackingArmies = clone(this.attackingArmies); for (let enemy in this.attackingUnits) { if (!this.attackingUnits[enemy]) continue; for (let ally in this.attackingUnits[enemy]) { if (this.attackingUnits[enemy][ally] < 8) continue; if (attackingArmies[enemy] === undefined) attackingArmies[enemy] = {}; if (attackingArmies[enemy][ally] === undefined) attackingArmies[enemy][ally] = 0; attackingArmies[enemy][ally] += 1; } } for (let enemy in attackingArmies) { for (let ally in attackingArmies[enemy]) { if (this.attackedAllies[ally] === undefined) this.attackedAllies[ally] = 0; this.attackedAllies[ally] += 1; } } this.checkEnemyArmies(gameState); this.checkEnemyUnits(gameState); this.assignDefenders(gameState); Engine.ProfileStop(); }; m.DefenseManager.prototype.makeIntoArmy = function(gameState, entityID) { // Try to add it to an existing army. for (let army of this.armies) if (!army.isCapturing(gameState) && army.addFoe(gameState, entityID)) return; // over // Create a new army for it. let army = new m.DefenseArmy(gameState, [], [entityID]); this.armies.push(army); }; m.DefenseManager.prototype.getArmy = function(partOfArmy) { // Find the army corresponding to this ID partOfArmy for (let army of this.armies) if (army.ID === partOfArmy) return army; return undefined; }; m.DefenseManager.prototype.isDangerous = function(gameState, entity) { if (!entity.position()) return false; let territoryOwner = this.territoryMap.getOwner(entity.position()); if (territoryOwner !== 0 && !gameState.isPlayerAlly(territoryOwner)) return false; // check if the entity is trying to build a new base near our buildings, // and if yes, add this base in our target list if (entity.unitAIState() && entity.unitAIState() == "INDIVIDUAL.REPAIR.REPAIRING") { let targetId = entity.unitAIOrderData()[0].target; if (this.targetList.indexOf(targetId) !== -1) return true; let target = gameState.getEntityById(targetId); if (target) { let isTargetEnemy = gameState.isPlayerEnemy(target.owner()); if (isTargetEnemy && territoryOwner === PlayerID) { if (target.hasClass("Structure")) this.targetList.push(targetId); return true; } else if (isTargetEnemy && target.hasClass("CivCentre")) { let myBuildings = gameState.getOwnStructures(); for (let building of myBuildings.values()) { if (API3.SquareVectorDistance(building.position(), entity.position()) > 30000) continue; this.targetList.push(targetId); return true; } } } } if (entity.attackTypes() === undefined || entity.hasClass("Support")) return false; let dist2Min = 6000; // TODO the 30 is to take roughly into account the structure size in following checks. Can be improved if (entity.attackTypes().indexOf("Ranged") !== -1) dist2Min = (entity.attackRange("Ranged").max + 30) * (entity.attackRange("Ranged").max + 30); for (let targetId of this.targetList) { let target = gameState.getEntityById(targetId); if (!target || !target.position()) // the enemy base is either destroyed or built continue; if (API3.SquareVectorDistance(target.position(), entity.position()) < dist2Min) return true; } let ccEnts = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); for (let cc of ccEnts.values()) { if (!gameState.isEntityExclusiveAlly(cc) || cc.foundationProgress() === 0) continue; let cooperation = this.GetCooperationLevel(cc); if (cooperation < 0.6 && cc.foundationProgress() !== undefined) continue; if (cooperation < 0.3) continue; if (API3.SquareVectorDistance(cc.position(), entity.position()) < dist2Min) return true; } let myBuildings = gameState.getOwnStructures(); for (let building of myBuildings.values()) { if (building.foundationProgress() === 0) continue; if (API3.SquareVectorDistance(building.position(), entity.position()) < dist2Min) return true; } // Update the number of enemies attacking this ally if (gameState.isPlayerMutualAlly(territoryOwner)) { let enemy = entity.owner(); if (this.attackingUnits[enemy] === undefined) this.attackingUnits[enemy] = {}; if (this.attackingUnits[enemy][territoryOwner] === undefined) this.attackingUnits[enemy][territoryOwner] = 0; this.attackingUnits[enemy][territoryOwner] += 1; } return false; }; m.DefenseManager.prototype.checkEnemyUnits = function(gameState) { const nbPlayers = gameState.sharedScript.playersData.length; let i = gameState.ai.playedTurn % nbPlayers; this.attackingUnits[i] = undefined; if (i === PlayerID) { if (!this.armies.length) { // check if we can recover capture points from any of our notdecaying structures for (let ent of gameState.getOwnStructures().values()) { if (ent.decaying()) continue; let capture = ent.capturePoints(); if (capture === undefined) continue; let lost = 0; for (let j = 0; j < capture.length; ++j) if (gameState.isPlayerEnemy(j)) lost += capture[j]; if (lost < Math.ceil(0.25 * capture[i])) continue; this.makeIntoArmy(gameState, ent.id()); break; } } return; } else if (!gameState.isPlayerEnemy(i)) return; // loop through enemy units for (let ent of gameState.getEnemyUnits(i).values()) { if (ent.getMetadata(PlayerID, "PartOfArmy") !== undefined) continue; // keep animals attacking us or our allies if (ent.hasClass("Animal")) { if (!ent.unitAIState() || ent.unitAIState().split(".")[1] !== "COMBAT") continue; let orders = ent.unitAIOrderData(); if (!orders || !orders.length || !orders[0].target) continue; let target = gameState.getEntityById(orders[0].target); if (!target || !gameState.isPlayerAlly(target.owner())) continue; } // TODO what to do for ships ? if (ent.hasClass("Ship") || ent.hasClass("Trader")) continue; // check if unit is dangerous "a priori" if (this.isDangerous(gameState, ent)) this.makeIntoArmy(gameState, ent.id()); } if (i !== 0 || this.armies.length > 1 || gameState.ai.HQ.numActiveBase() === 0) return; // look for possible gaia buildings inside our territory (may happen when enemy resign or after structure decay) // and attack it only if useful (and capturable) or dangereous for (let ent of gameState.getEnemyStructures(i).values()) { if (!ent.position() || ent.getMetadata(PlayerID, "PartOfArmy") !== undefined) continue; if (!ent.capturePoints() && !ent.hasDefensiveFire()) continue; let owner = this.territoryMap.getOwner(ent.position()); if (owner === PlayerID) this.makeIntoArmy(gameState, ent.id()); } }; m.DefenseManager.prototype.checkEnemyArmies = function(gameState) { for (let i = 0; i < this.armies.length; ++i) { let army = this.armies[i]; // this returns a list of IDs: the units that broke away from the army for being too far. let breakaways = army.update(gameState); for (let breaker of breakaways) this.makeIntoArmy(gameState, breaker); // assume dangerosity if (army.getState() === 0) { army.clear(gameState); this.armies.splice(i--,1); continue; } } // Check if we can't merge it with another for (let i = 0; i < this.armies.length - 1; ++i) { let army = this.armies[i]; if (army.isCapturing(gameState)) continue; for (let j = i+1; j < this.armies.length; ++j) { let otherArmy = this.armies[j]; if (otherArmy.isCapturing(gameState) || API3.SquareVectorDistance(army.foePosition, otherArmy.foePosition) > this.armyMergeSize) continue; // no need to clear here. army.merge(gameState, otherArmy); this.armies.splice(j--,1); } } if (gameState.ai.playedTurn % 5 !== 0) return; // Check if any army is no more dangerous (possibly because it has defeated us and destroyed our base) this.attackingArmies = {}; for (let i = 0; i < this.armies.length; ++i) { let army = this.armies[i]; army.recalculatePosition(gameState); let owner = this.territoryMap.getOwner(army.foePosition); if (!gameState.isPlayerEnemy(owner)) { if (gameState.isPlayerMutualAlly(owner)) { // update the number of enemies attacking this ally for (let id of army.foeEntities) { let ent = gameState.getEntityById(id); if (!ent) continue; let enemy = ent.owner(); if (this.attackingArmies[enemy] === undefined) this.attackingArmies[enemy] = {}; if (this.attackingArmies[enemy][owner] === undefined) this.attackingArmies[enemy][owner] = 0; this.attackingArmies[enemy][owner] += 1; break; } } continue; } else if (owner !== 0) // enemy army back in its territory { army.clear(gameState); this.armies.splice(i--,1); continue; } // army in neutral territory // TODO check smaller distance with all our buildings instead of only ccs with big distance let stillDangerous = false; let bases = gameState.updatingGlobalCollection("allCCs", API3.Filters.byClass("CivCentre")); for (let base of bases.values()) { if (!gameState.isEntityAlly(base)) continue; let cooperation = this.GetCooperationLevel(base); if (cooperation < 0.3 && !gameState.isEntityOwn(base)) continue; if (API3.SquareVectorDistance(base.position(), army.foePosition) > 40000) continue; if(this.Config.debug > 1) API3.warn("army in neutral territory, but still near one of our CC"); stillDangerous = true; break; } if (stillDangerous) continue; army.clear(gameState); this.armies.splice(i--,1); } }; m.DefenseManager.prototype.assignDefenders = function(gameState) { if (this.armies.length === 0) return; let armiesNeeding = []; // let's add defenders for (let army of this.armies) { let needsDef = army.needsDefenders(gameState); if (needsDef === false) continue; // Okay for now needsDef is the total needed strength. // we're dumb so we don't choose if we have a defender shortage. armiesNeeding.push( {"army": army, "need": needsDef} ); } if (armiesNeeding.length === 0) return; // let's get our potential units let potentialDefenders = []; gameState.getOwnUnits().forEach(function(ent) { if (!ent.position()) return; if (ent.getMetadata(PlayerID, "plan") === -2 || ent.getMetadata(PlayerID, "plan") === -3) return; if (ent.hasClass("Support") || ent.attackTypes() === undefined) return; if (ent.hasClass("Siege") && !ent.hasClass("Melee")) return; if (ent.hasClass("FishingBoat") || ent.hasClass("Trader")) return; if (ent.getMetadata(PlayerID, "transport") !== undefined || ent.getMetadata(PlayerID, "transporter") !== undefined) return; if (gameState.ai.HQ.gameTypeManager.criticalEnts.has(ent.id())) return; if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") !== -1) { let subrole = ent.getMetadata(PlayerID, "subrole"); if (subrole && (subrole === "completing" || subrole === "walking" || subrole === "attacking")) return; } potentialDefenders.push(ent.id()); }); for (let a = 0; a < armiesNeeding.length; ++a) armiesNeeding[a].army.recalculatePosition(gameState); for (let i = 0; i < potentialDefenders.length; ++i) { let ent = gameState.getEntityById(potentialDefenders[i]); if (!ent.position()) continue; let aMin; let distMin; for (let a = 0; a < armiesNeeding.length; ++a) { let dist = API3.SquareVectorDistance(ent.position(), armiesNeeding[a].army.foePosition); if (aMin !== undefined && dist > distMin) continue; aMin = a; distMin = dist; } // if outside our territory (helping an ally or attacking a cc foundation), keep some troops in backup if (i < 12 && this.territoryMap.getOwner(armiesNeeding[aMin].army.foePosition) !== PlayerID) continue; armiesNeeding[aMin].need -= m.getMaxStrength(ent); armiesNeeding[aMin].army.addOwn(gameState, potentialDefenders[i]); armiesNeeding[aMin].army.assignUnit(gameState, potentialDefenders[i]); if (armiesNeeding[aMin].need <= 0) armiesNeeding.splice(aMin, 1); if (!armiesNeeding.length) break; } if (!armiesNeeding.length) return; // If shortage of defenders, produce infantry garrisoned in nearest civil centre let armiesPos = []; for (let a = 0; a < armiesNeeding.length; ++a) armiesPos.push(armiesNeeding[a].army.foePosition); gameState.ai.HQ.trainEmergencyUnits(gameState, armiesPos); }; m.DefenseManager.prototype.abortArmy = function(gameState, army) { army.clear(gameState); for (let i = 0; i < this.armies.length; ++i) { if (this.armies[i].ID !== army.ID) continue; this.armies.splice(i, 1); break; } }; /** * If our defense structures are attacked, garrison soldiers inside when possible * and if a support unit is attacked and has less than 55% health, garrison it inside the nearest healing structure * and if a ranged siege unit (not used for defense) is attacked, garrison it in the nearest fortress * If our hero is attacked in regicide game mode, the gameTypeManager will handle it */ m.DefenseManager.prototype.checkEvents = function(gameState, events) { // must be called every turn for all armies for (let army of this.armies) army.checkEvents(gameState, events); for (let evt of events.Attacked) { let target = gameState.getEntityById(evt.target); if (!target || !target.position()) continue; let attacker = gameState.getEntityById(evt.attacker); if (attacker && gameState.isEntityOwn(attacker) && gameState.isEntityEnemy(target) && !attacker.hasClass("Ship")) { // If enemies are in range of one of our defensive structures, garrison it for arrow multiplier if (attacker.position() && attacker.isGarrisonHolder() && attacker.getArrowMultiplier()) - this.garrisonRangedUnitsInside(gameState, attacker, {"attacker": target}); + this.garrisonUnitsInside(gameState, attacker, {"attacker": target}); } if (!gameState.isEntityOwn(target)) continue; // If attacked by one of our allies (he must trying to recover capture points), do not react if (attacker && gameState.isEntityAlly(attacker)) continue; if (target.hasClass("Ship")) // TODO integrate ships later need to be sure it is accessible continue; // If inside a started attack plan, let the plan deal with this unit let plan = target.getMetadata(PlayerID, "plan"); if (plan !== undefined && plan >= 0) { let attack = gameState.ai.HQ.attackManager.getPlan(plan); if (attack && attack.state !== "unexecuted") continue; } // Signal this attacker to our defense manager, except if we are in enemy territory // TODO treat ship attack if (attacker && attacker.position() && attacker.getMetadata(PlayerID, "PartOfArmy") === undefined && !attacker.hasClass("Structure") && !attacker.hasClass("Ship")) { let territoryOwner = this.territoryMap.getOwner(attacker.position()); if (territoryOwner === 0 || gameState.isPlayerAlly(territoryOwner)) this.makeIntoArmy(gameState, attacker.id()); } if (target.getMetadata(PlayerID, "PartOfArmy") !== undefined) { let army = this.getArmy(target.getMetadata(PlayerID, "PartOfArmy")); if (army.isCapturing(gameState)) { let abort = false; // if one of the units trying to capture a structure is attacked, // abort the army so that the unit can defend itself if (army.ownEntities.indexOf(target.id()) !== -1) abort = true; else if (army.foeEntities[0] === target.id() && target.owner() === PlayerID) { // else we may be trying to regain some capture point from one of our structure abort = true; let capture = target.capturePoints(); for (let j = 0; j < capture.length; ++j) { if (!gameState.isPlayerEnemy(j) || capture[j] === 0) continue; abort = false; break; } } if (abort) this.abortArmy(gameState, army); } continue; } // try to garrison any attacked support unit if low healthlevel if (target.hasClass("Support") && target.healthLevel() < 0.55 && !target.getMetadata(PlayerID, "transport") && plan !== -2 && plan !== -3) { this.garrisonAttackedUnit(gameState, target); continue; } // try to garrison any attacked range siege unit if (target.hasClass("Siege") && !target.hasClass("Melee") && !target.getMetadata(PlayerID, "transport") && plan !== -2 && plan !== -3) { this.garrisonSiegeUnit(gameState, target); continue; } if (!attacker || !attacker.position()) continue; if (target.isGarrisonHolder() && target.getArrowMultiplier()) - this.garrisonRangedUnitsInside(gameState, target, {"attacker": attacker}); + this.garrisonUnitsInside(gameState, target, {"attacker": attacker}); } }; -m.DefenseManager.prototype.garrisonRangedUnitsInside = function(gameState, target, data) +m.DefenseManager.prototype.garrisonUnitsInside = function(gameState, target, data) { let minGarrison = data.min ? data.min : target.garrisonMax(); let typeGarrison = data.type ? data.type : "protection"; if (gameState.ai.HQ.garrisonManager.numberOfGarrisonedUnits(target) >= minGarrison) return; if (target.hitpoints() < target.garrisonEjectHealth() * target.maxHitpoints()) return; if (data.attacker) { let attackTypes = target.attackTypes(); if (!attackTypes || attackTypes.indexOf("Ranged") === -1) return; let dist = API3.SquareVectorDistance(data.attacker.position(), target.position()); let range = target.attackRange("Ranged").max; if (dist >= range*range) return; } let index = gameState.ai.accessibility.getAccessValue(target.position()); let garrisonManager = gameState.ai.HQ.garrisonManager; let garrisonArrowClasses = target.getGarrisonArrowClasses(); let units = gameState.getOwnUnits().filter(ent => MatchesClassList(ent.classes(), garrisonArrowClasses)).filterNearest(target.position()); + let allowMelee = gameState.ai.HQ.garrisonManager.allowMelee(target); + if (allowMelee === undefined) + { + // Should be kept in sync with garrisonManager to avoid garrisoning-ungarrisoning some units + if (data.attacker) + allowMelee = data.attacker.hasClass("Structure") ? data.attacker.attackRange("Ranged") : !m.isSiegeUnit(data.attacker); + else + allowMelee = true; + } for (let ent of units.values()) { if (garrisonManager.numberOfGarrisonedUnits(target) >= minGarrison) break; if (!ent.position()) continue; + if (typeGarrison !== "decay" && !allowMelee && ent.attackTypes().indexOf("Melee") !== -1) + continue; if (ent.getMetadata(PlayerID, "transport") !== undefined) continue; let army = ent.getMetadata(PlayerID, "PartOfArmy") ? this.getArmy(ent.getMetadata(PlayerID, "PartOfArmy")) : undefined; if (!army && (ent.getMetadata(PlayerID, "plan") === -2 || ent.getMetadata(PlayerID, "plan") === -3)) continue; if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") >= 0) { let subrole = ent.getMetadata(PlayerID, "subrole"); // when structure decaying (usually because we've just captured it in enemy territory), also allow units from an attack plan if (typeGarrison !== "decay" && subrole && (subrole === "completing" || subrole === "walking" || subrole === "attacking")) continue; } if (gameState.ai.accessibility.getAccessValue(ent.position()) !== index) continue; if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") >= 0) { let attackPlan = gameState.ai.HQ.attackManager.getPlan(ent.getMetadata(PlayerID, "plan")); if (attackPlan) attackPlan.removeUnit(ent, true); } if (army) army.removeOwn(gameState, ent.id()); garrisonManager.garrison(gameState, ent, target, typeGarrison); } }; /** garrison a attacked siege ranged unit inside the nearest fortress */ m.DefenseManager.prototype.garrisonSiegeUnit = function(gameState, unit) { let distmin = Math.min(); let nearest; let unitAccess = gameState.ai.accessibility.getAccessValue(unit.position()); let garrisonManager = gameState.ai.HQ.garrisonManager; gameState.getAllyStructures().forEach(function(ent) { if (!MatchesClassList(unit.classes(), ent.garrisonableClasses())) return; if (garrisonManager.numberOfGarrisonedUnits(ent) >= ent.garrisonMax()) return; if (ent.hitpoints() < ent.garrisonEjectHealth() * ent.maxHitpoints()) return; if (m.getLandAccess(gameState, ent) !== unitAccess) return; let dist = API3.SquareVectorDistance(ent.position(), unit.position()); if (dist > distmin) return; distmin = dist; nearest = ent; }); if (nearest) garrisonManager.garrison(gameState, unit, nearest, "protection"); }; /** * Garrison a hurt unit inside a player-owned or allied structure * If emergency is true, the unit will be garrisoned in the closest possible structure * Otherwise, it will garrison in the closest healing structure */ m.DefenseManager.prototype.garrisonAttackedUnit = function(gameState, unit, emergency = false) { let distmin = Math.min(); let nearest; let unitAccess = gameState.ai.accessibility.getAccessValue(unit.position()); let garrisonManager = gameState.ai.HQ.garrisonManager; for (let ent of gameState.getAllyStructures().values()) { if (!emergency && !ent.buffHeal()) continue; if (!MatchesClassList(unit.classes(), ent.garrisonableClasses())) continue; if (garrisonManager.numberOfGarrisonedUnits(ent) >= ent.garrisonMax() && (!emergency || !ent.garrisoned().length)) continue; if (ent.hitpoints() < ent.garrisonEjectHealth() * ent.maxHitpoints()) continue; if (m.getLandAccess(gameState, ent) !== unitAccess) continue; let dist = API3.SquareVectorDistance(ent.position(), unit.position()); if (dist > distmin) continue; distmin = dist; nearest = ent; } if (!nearest) return; if (!emergency) { garrisonManager.garrison(gameState, unit, nearest, "protection"); return; } if (garrisonManager.numberOfGarrisonedUnits(nearest) >= nearest.garrisonMax()) // make room for this ent nearest.unload(nearest.garrisoned()[0]); garrisonManager.garrison(gameState, unit, nearest, nearest.buffHeal() ? "protection" : "emergency"); }; /** * Be more inclined to help an ally attacked by several enemies */ m.DefenseManager.prototype.GetCooperationLevel = function(ent) { let ally = ent.owner(); let cooperation = this.Config.personality.cooperative; if (this.attackedAllies[ally] && this.attackedAllies[ally] > 1) cooperation += 0.2 * (this.attackedAllies[ally] - 1); return cooperation; }; m.DefenseManager.prototype.Serialize = function() { let properties = { "targetList" : this.targetList, "armyMergeSize": this.armyMergeSize, "attackingUnits": this.attackingUnits, "attackingArmies": this.attackingArmies, "attackedAllies": this.attackedAllies }; let armies = []; for (let army of this.armies) armies.push(army.Serialize()); return { "properties": properties, "armies": armies }; }; m.DefenseManager.prototype.Deserialize = function(gameState, data) { for (let key in data.properties) this[key] = data.properties[key]; this.armies = []; for (let dataArmy of data.armies) { let army = new m.DefenseArmy(gameState, [], []); army.Deserialize(dataArmy); this.armies.push(army); } }; return m; }(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/entityExtend.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/entityExtend.js (revision 19546) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/entityExtend.js (revision 19547) @@ -1,314 +1,320 @@ var PETRA = function(m) { +/** returns true if this unit should be considered as a siege unit */ +m.isSiegeUnit = function(ent) +{ + return ent.hasClass("Siege") || (ent.hasClass("Elephant") && ent.hasClass("Champion")); +}; + /** returns some sort of DPS * health factor. If you specify a class, it'll use the modifiers against that class too. */ m.getMaxStrength = function(ent, againstClass) { let strength = 0; let attackTypes = ent.attackTypes(); if (!attackTypes) return strength; for (let type of attackTypes) { if (type == "Slaughter") continue; let attackStrength = ent.attackStrengths(type); for (let str in attackStrength) { let val = parseFloat(attackStrength[str]); if (againstClass) val *= ent.getMultiplierAgainst(type, againstClass); switch (str) { case "crush": strength += val * 0.085 / 3; break; case "hack": strength += val * 0.075 / 3; break; case "pierce": strength += val * 0.065 / 3; break; default: API3.warn("Petra: " + str + " unknown attackStrength in getMaxStrength"); } } let attackRange = ent.attackRange(type); if (attackRange) strength += attackRange.max * 0.0125; let attackTimes = ent.attackTimes(type); for (let str in attackTimes) { let val = parseFloat(attackTimes[str]); switch (str) { case "repeat": strength += val / 100000; break; case "prepare": strength -= val / 100000; break; default: API3.warn("Petra: " + str + " unknown attackTimes in getMaxStrength"); } } } let armourStrength = ent.armourStrengths(); for (let str in armourStrength) { let val = parseFloat(armourStrength[str]); switch (str) { case "crush": strength += val * 0.085 / 3; break; case "hack": strength += val * 0.075 / 3; break; case "pierce": strength += val * 0.065 / 3; break; default: API3.warn("Petra: " + str + " unknown armourStrength in getMaxStrength"); } } return strength * ent.maxHitpoints() / 100.0; }; /** Get access and cache it in metadata if not already done */ m.getLandAccess = function(gameState, ent) { let access = ent.getMetadata(PlayerID, "access"); if (!access) { access = gameState.ai.accessibility.getAccessValue(ent.position()); ent.setMetadata(PlayerID, "access", access); } return access; }; m.getSeaAccess = function(gameState, ent) { let sea = ent.getMetadata(PlayerID, "sea"); if (!sea) { sea = gameState.ai.accessibility.getAccessValue(ent.position(), true); if (sea < 2) // pre-positioned docks are sometimes not well positionned { let entPos = ent.position(); let radius = ent.footprintRadius(); for (let i = 0; i < 16; ++i) { let pos = [ entPos[0] + radius*Math.cos(i*Math.PI/8), entPos[1] + radius*Math.sin(i*Math.PI/8) ]; sea = gameState.ai.accessibility.getAccessValue(pos, true); if (sea >= 2) break; } } if (sea < 2) API3.warn("ERROR in Petra getSeaAccess because of position with sea index " + sea); ent.setMetadata(PlayerID, "sea", sea); } return sea; }; /** Decide if we should try to capture (returns true) or destroy (return false) */ m.allowCapture = function(gameState, ent, target) { if (!ent.canCapture() || !target.isCapturable()) return false; // always try to recapture cp from an allied, except if it's decaying if (gameState.isPlayerAlly(target.owner())) return !target.decaying(); let antiCapture = target.defaultRegenRate(); if (target.isGarrisonHolder() && target.garrisoned()) antiCapture += target.garrisonRegenRate() * target.garrisoned().length; if (target.decaying()) antiCapture -= target.territoryDecayRate(); let capture; let capturableTargets = gameState.ai.HQ.capturableTargets; if (!capturableTargets.has(target.id())) { capture = ent.captureStrength() * m.getAttackBonus(ent, target, "Capture"); capturableTargets.set(target.id(), { "strength": capture, "ents": new Set([ent.id()]) }); } else { let capturable = capturableTargets.get(target.id()); if (!capturable.ents.has(ent.id())) { capturable.strength += ent.captureStrength() * m.getAttackBonus(ent, target, "Capture"); capturable.ents.add(ent.id()); } capture = capturable.strength; } capture *= 1 / ( 0.1 + 0.9*target.healthLevel()); let sumCapturePoints = target.capturePoints().reduce((a, b) => a + b); if (target.hasDefensiveFire() && target.isGarrisonHolder() && target.garrisoned()) return capture > antiCapture + sumCapturePoints/50; return capture > antiCapture + sumCapturePoints/80; }; /** copy of GetAttackBonus from Attack.js */ m.getAttackBonus = function(ent, target, type) { let attackBonus = 1; if (!ent.get("Attack/" + type) || !ent.get("Attack/" + type + "/Bonuses")) return attackBonus; let bonuses = ent.get("Attack/" + type + "/Bonuses"); for (let key in bonuses) { let bonus = bonuses[key]; if (bonus.Civ && bonus.Civ !== target.civ()) continue; if (bonus.Classes && bonus.Classes.split(/\s+/).some(cls => !target.hasClass(cls))) continue; attackBonus *= bonus.Multiplier; } return attackBonus; }; /** Makes the worker deposit the currently carried resources at the closest accessible dropsite */ m.returnResources = function(gameState, ent) { if (!ent.resourceCarrying() || !ent.resourceCarrying().length || !ent.position()) return false; let resource = ent.resourceCarrying()[0].type; let closestDropsite; let distmin = Math.min(); let access = gameState.ai.accessibility.getAccessValue(ent.position()); let dropsiteCollection = gameState.playerData.hasSharedDropsites ? gameState.getAnyDropsites(resource) : gameState.getOwnDropsites(resource); for (let dropsite of dropsiteCollection.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; let dropsiteAccess = dropsite.getMetadata(PlayerID, "access"); if (!dropsiteAccess) { dropsiteAccess = gameState.ai.accessibility.getAccessValue(dropsite.position()); dropsite.setMetadata(PlayerID, "access", dropsiteAccess); } if (dropsiteAccess !== access) continue; let dist = API3.SquareVectorDistance(ent.position(), dropsite.position()); if (dist > distmin) continue; distmin = dist; closestDropsite = dropsite; } if (!closestDropsite) return false; ent.returnResources(closestDropsite); return true; }; /** is supply full taking into account gatherers affected during this turn */ m.IsSupplyFull = function(gameState, ent) { if (ent.isFull() === true) return true; let turnCache = gameState.ai.HQ.turnCache; let count = ent.resourceSupplyNumGatherers(); if (turnCache.resourceGatherer && turnCache.resourceGatherer[ent.id()]) count += turnCache.resourceGatherer[ent.id()]; if (count >= ent.maxGatherers()) return true; return false; }; /** * get the best base (in terms of distance and accessIndex) for an entity */ m.getBestBase = function(gameState, ent, onlyConstructedBase = false) { let pos = ent.position(); if (!pos) { let holder = m.getHolder(gameState, ent); if (!holder || !holder.position()) { API3.warn("Petra error: entity without position, but not garrisoned"); m.dumpEntity(ent); return gameState.ai.HQ.baseManagers[0]; } pos = holder.position(); } let distmin = Math.min(); let bestbase; let accessIndex = gameState.ai.accessibility.getAccessValue(pos); for (let base of gameState.ai.HQ.baseManagers) { if (!base.anchor || onlyConstructedBase && base.anchor.foundationProgress() !== undefined) continue; let dist = API3.SquareVectorDistance(base.anchor.position(), pos); if (base.accessIndex !== accessIndex) dist += 100000000; if (dist > distmin) continue; distmin = dist; bestbase = base; } if (!bestbase) bestbase = gameState.ai.HQ.baseManagers[0]; return bestbase; }; m.getHolder = function(gameState, ent) { for (let holder of gameState.getEntities().values()) { if (holder.isGarrisonHolder() && holder.garrisoned().indexOf(ent.id()) !== -1) return holder; } return undefined; }; /** * return true if it is not worth finishing this building (it would surely decay) * TODO implement the other conditions */ m.isNotWorthBuilding = function(gameState, ent) { if (gameState.ai.HQ.territoryMap.getOwner(ent.position()) !== PlayerID) { let buildTerritories = ent.buildTerritories(); if (buildTerritories && (!buildTerritories.length || (buildTerritories.length === 1 && buildTerritories[0] === "own"))) return true; } return false; }; m.dumpEntity = function(ent) { if (!ent) return; API3.warn(" >>> id " + ent.id() + " name " + ent.genericName() + " pos " + ent.position() + " state " + ent.unitAIState()); API3.warn(" base " + ent.getMetadata(PlayerID, "base") + " >>> role " + ent.getMetadata(PlayerID, "role") + " subrole " + ent.getMetadata(PlayerID, "subrole")); API3.warn("owner " + ent.owner() + " health " + ent.hitpoints() + " healthMax " + ent.maxHitpoints() + " foundationProgress " + ent.foundationProgress()); API3.warn(" garrisoning " + ent.getMetadata(PlayerID, "garrisoning") + " garrisonHolder " + ent.getMetadata(PlayerID, "garrisonHolder") + " plan " + ent.getMetadata(PlayerID, "plan") + " transport " + ent.getMetadata(PlayerID, "transport") + " gather-type " + ent.getMetadata(PlayerID, "gather-type") + " target-foundation " + ent.getMetadata(PlayerID, "target-foundation") + " PartOfArmy " + ent.getMetadata(PlayerID, "PartOfArmy")); }; return m; }(PETRA); Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/garrisonManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/garrisonManager.js (revision 19546) +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/garrisonManager.js (revision 19547) @@ -1,301 +1,343 @@ var PETRA = function(m) { /** * Manage the garrisonHolders * When a unit is ordered to garrison, it must be done through this.garrison() function so that * an object in this.holders is created. This object contains an array with the entities * in the process of being garrisoned. To have all garrisoned units, we must add those in holder.garrisoned(). * Futhermore garrison units have a metadata garrisonType describing its reason (protection, transport, ...) */ m.GarrisonManager = function() { this.holders = new Map(); this.decayingStructures = new Map(); }; m.GarrisonManager.prototype.update = function(gameState, events) { // First check for possible upgrade of a structure for (let evt of events.EntityRenamed) { for (let id of this.holders.keys()) { if (id !== evt.entity) continue; - let list = this.holders.get(id); + let data = this.holders.get(id); this.holders.delete(id); - this.holders.set(evt.newentity, list); + this.holders.set(evt.newentity, data); } for (let id of this.decayingStructures.keys()) { if (id !== evt.entity) continue; this.decayingStructures.delete(id); if (this.decayingStructures.has(evt.newentity)) continue; let ent = gameState.getEntityById(evt.newentity); if (!ent || !ent.territoryDecayRate() || !ent.garrisonRegenRate()) continue; let gmin = Math.ceil((ent.territoryDecayRate() - ent.defaultRegenRate()) / ent.garrisonRegenRate()); this.decayingStructures.set(evt.newentity, gmin); } } - for (let [id, list] of this.holders.entries()) + for (let [id, data] of this.holders.entries()) { + let list = data.list; let holder = gameState.getEntityById(id); if (!holder || !gameState.isPlayerAlly(holder.owner())) { // this holder was certainly destroyed or captured. Let's remove it for (let entId of list) { let ent = gameState.getEntityById(entId); if (ent && ent.getMetadata(PlayerID, "garrisonHolder") == id) { this.leaveGarrison(ent); ent.stopMoving(); } } this.holders.delete(id); continue; } // Update the list of garrisoned units for (let j = 0; j < list.length; ++j) { for (let evt of events.EntityRenamed) if (evt.entity === list[j]) list[j] = evt.newentity; let ent = gameState.getEntityById(list[j]); if (!ent) // unit must have been killed while garrisoning list.splice(j--, 1); else if (holder.garrisoned().indexOf(list[j]) !== -1) // unit is garrisoned { this.leaveGarrison(ent); list.splice(j--, 1); } else { if (ent.unitAIOrderData().some(order => order.target && order.target == id)) continue; if (ent.getMetadata(PlayerID, "garrisonHolder") == id) { // The garrison order must have failed this.leaveGarrison(ent); list.splice(j--, 1); } else { if (gameState.ai.Config.debug > 0) { API3.warn("Petra garrison error: unit " + ent.id() + " (" + ent.genericName() + ") is expected to garrison in " + id + " (" + holder.genericName() + "), but has no such garrison order " + uneval(ent.unitAIOrderData())); m.dumpEntity(ent); } list.splice(j--, 1); } } } if (!holder.position()) // could happen with siege unit inside a ship continue; if (gameState.ai.elapsedTime - holder.getMetadata(PlayerID, "holderTimeUpdate") > 3) { let range = holder.attackRange("Ranged") ? holder.attackRange("Ranged").max : 80; - let enemiesAround = false; + let around = { "defenseStructure": false, "inertStructure": false, "meleeSiege": false, "rangeSiege": false, "unit": false }; for (let ent of gameState.getEnemyEntities().values()) { if (!ent.position()) continue; if (ent.owner() === 0 && (!ent.unitAIState() || ent.unitAIState().split(".")[1] !== "COMBAT")) continue; let dist = API3.SquareVectorDistance(ent.position(), holder.position()); if (dist > range*range) continue; - enemiesAround = true; - break; + if (ent.hasClass("Structure")) + { + if (ent.attackRange("Ranged")) // TODO units on wall are not taken into account + around.defenseStructure = true; + else + around.inertStructure = true; + } + else if (m.isSiegeUnit(ent)) + { + if (ent.attackTypes().indexOf("Melee") !== -1) + around.meleeSiege = true; + else + around.rangeSiege = true; + } + else + { + around.unit = true; + break; + } } + // Keep defenseManager.garrisonUnitsInside in sync to avoid garrisoning-ungarrisoning some units + data.allowMelee = around.defenseStructure || around.unit; for (let entId of holder.garrisoned()) { let ent = gameState.getEntityById(entId); - if (ent.owner() === PlayerID && !this.keepGarrisoned(ent, holder, enemiesAround)) + if (ent.owner() === PlayerID && !this.keepGarrisoned(ent, holder, around)) holder.unload(entId); } for (let j = 0; j < list.length; ++j) { let ent = gameState.getEntityById(list[j]); - if (this.keepGarrisoned(ent, holder, enemiesAround)) + if (this.keepGarrisoned(ent, holder, around)) continue; if (ent.getMetadata(PlayerID, "garrisonHolder") == id) { this.leaveGarrison(ent); ent.stopMoving(); } list.splice(j--, 1); } if (this.numberOfGarrisonedUnits(holder) === 0) this.holders.delete(id); else holder.setMetadata(PlayerID, "holderTimeUpdate", gameState.ai.elapsedTime); } } // Warning new garrison orders (as in the following lines) should be done after having updated the holders // (or TODO we should add a test that the garrison order is from a previous turn when updating) for (let [id, gmin] of this.decayingStructures.entries()) { let ent = gameState.getEntityById(id); if (!ent || ent.owner() !== PlayerID) this.decayingStructures.delete(id); else if (this.numberOfGarrisonedUnits(ent) < gmin) - gameState.ai.HQ.defenseManager.garrisonRangedUnitsInside(gameState, ent, {"min": gmin, "type": "decay"}); + gameState.ai.HQ.defenseManager.garrisonUnitsInside(gameState, ent, {"min": gmin, "type": "decay"}); } }; /** TODO should add the units garrisoned inside garrisoned units */ m.GarrisonManager.prototype.numberOfGarrisonedUnits = function(holder) { if (!this.holders.has(holder.id())) return holder.garrisoned().length; - return holder.garrisoned().length + this.holders.get(holder.id()).length; + return holder.garrisoned().length + this.holders.get(holder.id()).list.length; +}; + +m.GarrisonManager.prototype.allowMelee = function(holder) +{ + if (!this.holders.has(holder.id())) + return undefined; + + return this.holders.get(holder.id()).allowMelee; }; /** This is just a pre-garrison state, while the entity walk to the garrison holder */ m.GarrisonManager.prototype.garrison = function(gameState, ent, holder, type) { if (this.numberOfGarrisonedUnits(holder) >= holder.garrisonMax()) return; this.registerHolder(gameState, holder); - this.holders.get(holder.id()).push(ent.id()); + this.holders.get(holder.id()).list.push(ent.id()); if (gameState.ai.Config.debug > 2) { warn("garrison unit " + ent.genericName() + " in " + holder.genericName() + " with type " + type); warn(" we try to garrison a unit with plan " + ent.getMetadata(PlayerID, "plan") + " and role " + ent.getMetadata(PlayerID, "role") + " and subrole " + ent.getMetadata(PlayerID, "subrole") + " and transport " + ent.getMetadata(PlayerID, "transport")); } if (ent.getMetadata(PlayerID, "plan") !== undefined) ent.setMetadata(PlayerID, "plan", -2); else ent.setMetadata(PlayerID, "plan", -3); ent.setMetadata(PlayerID, "subrole", "garrisoning"); ent.setMetadata(PlayerID, "garrisonHolder", holder.id()); ent.setMetadata(PlayerID, "garrisonType", type); ent.garrison(holder); }; /** This is the end of the pre-garrison state, either because the entity is really garrisoned or because it has changed its order (i.e. because the garrisonHolder was destroyed) This function is for internal use inside garrisonManager. From outside, you should also update the holder and then using cancelGarrison should be the preferred solution */ m.GarrisonManager.prototype.leaveGarrison = function(ent) { ent.setMetadata(PlayerID, "subrole", undefined); if (ent.getMetadata(PlayerID, "plan") === -2) ent.setMetadata(PlayerID, "plan", -1); else ent.setMetadata(PlayerID, "plan", undefined); ent.setMetadata(PlayerID, "garrisonHolder", undefined); }; /** Cancel a pre-garrison state */ m.GarrisonManager.prototype.cancelGarrison = function(ent) { ent.stopMoving(); this.leaveGarrison(ent); let holderId = ent.getMetadata(PlayerID, "garrisonHolder"); if (!holderId || !this.holders.has(holderId)) return; - let list = this.holders.get(holderId); + let list = this.holders.get(holderId).list; let index = list.indexOf(ent.id()); if (index !== -1) list.splice(index, 1); }; -m.GarrisonManager.prototype.keepGarrisoned = function(ent, holder, enemiesAround) +m.GarrisonManager.prototype.keepGarrisoned = function(ent, holder, around) { switch (ent.getMetadata(PlayerID, "garrisonType")) { case 'force': // force the ungarrisoning return false; case 'trade': // trader garrisoned in ship return true; case 'protection': // hurt unit for healing or infantry for defense - return ent.needsHeal() && holder.buffHeal() || - enemiesAround && (ent.hasClass("Support") || - MatchesClassList(ent.classes(), holder.getGarrisonArrowClasses()) || - MatchesClassList(ent.classes(), "Siege+!Melee")); + if (ent.needsHeal() && holder.buffHeal()) + return true; + if (MatchesClassList(ent.classes(), holder.getGarrisonArrowClasses())) + { + if (around.unit || around.defenseStructure) + return true; + else if (around.meleeSiege || around.rangeSiege) + return ent.attackTypes().indexOf("Melee") === -1; + else + return false; + } + if (ent.attackTypes() && ent.attackTypes().indexOf("Melee") !== -1) + return false; + if (around.unit) + return ent.hasClass("Support") || m.isSiegeUnit(ent); // only ranged siege here and below as melee siege already released above + if (m.isSiegeUnit(ent)) + return around.meleeSiege; + return false; case 'decay': return this.decayingStructures.has(holder.id()); case 'emergency': // f.e. hero in regicide mode - return enemiesAround; + return around.unit || around.defenseStructure || around.meleeSiege; default: if (ent.getMetadata(PlayerID, "onBoard") === "onBoard") // transport is not (yet ?) managed by garrisonManager return true; API3.warn("unknown type in garrisonManager " + ent.getMetadata(PlayerID, "garrisonType") + " for " + ent.id() + " inside " + holder.id()); ent.setMetadata(PlayerID, "garrisonType", "protection"); return true; } }; /** Add this holder in the list managed by the garrisonManager */ m.GarrisonManager.prototype.registerHolder = function(gameState, holder) { if (this.holders.has(holder.id())) // already registered return; - this.holders.set(holder.id(), []); + this.holders.set(holder.id(), { "list": [], "allowMelee": true }); holder.setMetadata(PlayerID, "holderTimeUpdate", gameState.ai.elapsedTime); }; /** * Garrison units in decaying structures to stop their decay * do it only for structures useful for defense, except if we are expanding (justCaptured=true) * in which case we also do it for structures useful for unit trainings (TODO only Barracks are done) */ m.GarrisonManager.prototype.addDecayingStructure = function(gameState, entId, justCaptured) { if (this.decayingStructures.has(entId)) return true; let ent = gameState.getEntityById(entId); if (!ent || (!(ent.hasClass("Barracks") && justCaptured) && !ent.hasDefensiveFire())) return false; if (!ent.territoryDecayRate() || !ent.garrisonRegenRate()) return false; let gmin = Math.ceil((ent.territoryDecayRate() - ent.defaultRegenRate()) / ent.garrisonRegenRate()); this.decayingStructures.set(entId, gmin); return true; }; m.GarrisonManager.prototype.removeDecayingStructure = function(entId) { if (!this.decayingStructures.has(entId)) return; this.decayingStructures.delete(entId); }; m.GarrisonManager.prototype.Serialize = function() { return { "holders": this.holders, "decayingStructures": this.decayingStructures }; }; m.GarrisonManager.prototype.Deserialize = function(data) { for (let key in data) this[key] = data[key]; }; return m; }(PETRA);