Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/entity.js @@ -744,6 +744,34 @@ return false; }, + /** + * Derived from Attack.js' similary named function. + * @return {boolean} - Whether an entity can attack a given target. + */ + "canAttackTarget": function(target, allowCapture) + { + let attackTypes = this.get("Attack"); + if (!attackTypes) + return false; + + let canCapture = allowCapture && this.canCapture(target); + let armourStrengths = target.get("Armour"); + if (!armourStrengths) + return canCapture; + + for (let type in attackTypes) + { + if (type == "Capture" ? !canCapture : target.isInvulnerable()) + continue; + + let restrictedClasses = this.get("Attack/" + type + "/RestrictedClasses/_string"); + if (!restrictedClasses || !MatchesClassList(target.classes(), restrictedClasses)) + return true; + }; + + return false; + }, + "move": function(x, z, queued = false) { Engine.PostCommand(PlayerID, { "type": "walk", "entities": [this.id()], "x": x, "z": z, "queued": queued }); return this; Index: ps/trunk/binaries/data/mods/public/simulation/ai/common-api/filters.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/common-api/filters.js +++ ps/trunk/binaries/data/mods/public/simulation/ai/common-api/filters.js @@ -82,6 +82,11 @@ "dynamicProperties": [] }), + "byCanAttackTarget": target => ({ + "func": ent => ent.canAttackTarget(target), + "dynamicProperties": [] + }), + "isGarrisoned": () => ({ "func": ent => ent.position() === undefined, "dynamicProperties": [] Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackManager.js +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackManager.js @@ -180,6 +180,9 @@ let access = PETRA.getLandAccess(gameState, ent); for (let struct of gameState.getEnemyStructures().values()) { + if (!ent.canAttackTarget(struct, PETRA.allowCapture(gameState, ent, struct))) + continue; + let structPos = struct.position(); let x; let z; Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackPlan.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackPlan.js +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/attackPlan.js @@ -1327,11 +1327,15 @@ { if (PETRA.isSiegeUnit(ent)) // needed as mauryan elephants are not filtered out continue; - ent.attack(attacker.id(), PETRA.allowCapture(gameState, ent, attacker)); + + let allowCapture = PETRA.allowCapture(gameState, ent, attacker); + if (!ent.canAttackTarget(attacker, allowCapture)) + continue; + ent.attack(attacker.id(), allowCapture); ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); } // And if this attacker is a non-ranged siege unit and our unit also, attack it - if (PETRA.isSiegeUnit(attacker) && attacker.hasClass("Melee") && ourUnit.hasClass("Melee")) + if (PETRA.isSiegeUnit(attacker) && attacker.hasClass("Melee") && ourUnit.hasClass("Melee") && ourUnit.canAttackTarget(attacker, PETRA.allowCapture(gameState, ourUnit, attacker))) { ourUnit.attack(attacker.id(), PETRA.allowCapture(gameState, ourUnit, attacker)); ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); @@ -1350,7 +1354,10 @@ let collec = this.unitCollection.filter(API3.Filters.byClass("Melee")).filterNearest(ourUnit.position(), 5); for (let ent of collec.values()) { - ent.attack(attacker.id(), PETRA.allowCapture(gameState, ent, attacker)); + let allowCapture = PETRA.allowCapture(gameState, ent, attacker); + if (!ent.canAttackTarget(attacker, allowCapture)) + continue; + ent.attack(attacker.id(), allowCapture); ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); } } @@ -1360,7 +1367,8 @@ let collec = this.unitCollection.filterNearest(ourUnit.position(), 2); for (let ent of collec.values()) { - if (PETRA.isSiegeUnit(ent)) + let allowCapture = PETRA.allowCapture(gameState, ent, attacker); + if (PETRA.isSiegeUnit(ent) || !ent.canAttackTarget(attacker, allowCapture)) continue; let orderData = ent.unitAIOrderData(); if (orderData && orderData.length && orderData[0].target) @@ -1371,7 +1379,7 @@ if (target && !target.hasClass("Structure") && !target.hasClass("Support")) continue; } - ent.attack(attacker.id(), PETRA.allowCapture(gameState, ent, attacker)); + ent.attack(attacker.id(), allowCapture); ent.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); } // Then the unit under attack: abandon its target (if it was a structure or a support) and retaliate @@ -1388,8 +1396,12 @@ continue; } } - ourUnit.attack(attacker.id(), PETRA.allowCapture(gameState, ourUnit, attacker)); - ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); + let allowCapture = PETRA.allowCapture(gameState, ourUnit, attacker); + if (ourUnit.canAttackTarget(attacker, allowCapture)) + { + ourUnit.attack(attacker.id(), allowCapture); + ourUnit.setMetadata(PlayerID, "lastAttackPlanUpdateTime", time); + } } } } @@ -1540,7 +1552,7 @@ if (siegeUnit) { let mStruct = enemyStructures.filter(enemy => { - if (!enemy.position() || enemy.hasClass("StoneWall") && !ent.canAttackClass("StoneWall")) + if (!enemy.position() || !ent.canAttackTarget(enemy, PETRA.allowCapture(gameState, ent, enemy))) return false; if (API3.SquareVectorDistance(enemy.position(), ent.position()) > range) return false; @@ -1592,7 +1604,7 @@ { let nearby = !ent.hasClass("Cavalry") && !ent.hasClass("Ranged"); let mUnit = enemyUnits.filter(enemy => { - if (!enemy.position()) + if (!enemy.position() || !ent.canAttackTarget(enemy, PETRA.allowCapture(gameState, ent, enemy))) return false; if (enemy.hasClass("Animal")) return false; @@ -1634,7 +1646,9 @@ let rand = randIntExclusive(0, mUnit.length * 0.1); ent.attack(mUnit[rand].id(), PETRA.allowCapture(gameState, ent, mUnit[rand])); } - else if (this.isBlocked) + // This may prove dangerous as we may be blocked by something we + // cannot attack. See similar behaviour at #5741. + else if (this.isBlocked && ent.canAttackTarget(this.target, false)) ent.attack(this.target.id(), false); else if (API3.SquareVectorDistance(this.targetPos, ent.position()) > 2500) { @@ -1655,7 +1669,7 @@ let mStruct = enemyStructures.filter(enemy => { if (this.isBlocked && enemy.id() != this.target.id()) return false; - if (!enemy.position() || enemy.hasClass("StoneWall") && !ent.canAttackClass("StoneWall")) + if (!enemy.position() || !ent.canAttackTarget(enemy, PETRA.allowCapture(gameState, ent, enemy))) return false; if (API3.SquareVectorDistance(enemy.position(), ent.position()) > range) return false; @@ -1696,13 +1710,16 @@ if (unit.unitAIState().split(".")[1] != "COMBAT" || !unit.unitAIOrderData().length || !unit.unitAIOrderData()[0].target) return; - if (!gameState.getEntityById(unit.unitAIOrderData()[0].target)) + let target = gameState.getEntityById(unit.unitAIOrderData()[0].target); + if (!target) return; let dist = API3.SquareVectorDistance(unit.position(), ent.position()); if (dist > distmin) return; distmin = dist; - attacker = gameState.getEntityById(unit.unitAIOrderData()[0].target); + if (!ent.canAttackTarget(target, PETRA.allowCapture(gameState, ent, target))) + return; + attacker = target; }); if (attacker) ent.attack(attacker.id(), PETRA.allowCapture(gameState, ent, attacker)); @@ -1756,9 +1773,11 @@ { if (ent.getMetadata(PlayerID, "transport") !== undefined) continue; - if (!ent.isIdle()) + + let allowCapture = PETRA.allowCapture(gameState, ent, attacker); + if (!ent.isIdle() || !ent.canAttackTarget(attacker, allowCapture)) continue; - ent.attack(attacker.id(), PETRA.allowCapture(gameState, ent, attacker)); + ent.attack(attacker.id(), allowCapture); } break; } Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/defenseArmy.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/defenseArmy.js +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/defenseArmy.js @@ -312,6 +312,9 @@ if (!eEnt || !eEnt.position()) // probably can't happen. continue; + if (!ent.canAttackTarget(eEnt, PETRA.allowCapture(gameState, ent, eEnt))) + continue; + if (eEnt.hasClass("Unit") && eEnt.unitAIOrderData() && eEnt.unitAIOrderData().length && eEnt.unitAIOrderData()[0].target && eEnt.unitAIOrderData()[0].target == entID) { // being attacked >>> target the unit Index: ps/trunk/binaries/data/mods/public/simulation/ai/petra/defenseManager.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/ai/petra/defenseManager.js +++ ps/trunk/binaries/data/mods/public/simulation/ai/petra/defenseManager.js @@ -464,6 +464,14 @@ { if (access && armiesNeeding[a].access != access) continue; + + // Do not assign defender if it cannot attack at least part of the attacking army. + if (!armiesNeeding[a].army.foeEntities.some(eEnt => { + let eEntID = gameState.getEntityById(eEnt); + return ent.canAttackTarget(eEntID, PETRA.allowCapture(gameState, ent, eEntID)); + })) + continue; + let dist = API3.SquareVectorDistance(ent.position(), armiesNeeding[a].army.foePosition); if (aMin !== undefined && dist > distMin) continue; @@ -705,7 +713,7 @@ if (allAttacked[entId]) continue; let ent = gameState.getEntityById(entId); - if (!ent || !ent.position()) + if (!ent || !ent.position() || !ent.canAttackTarget(attacker, PETRA.allowCapture(gameState, ent, attacker))) continue; // Check that the unit is still attacking the structure (since the last played turn). let state = ent.unitAIState(); @@ -728,7 +736,9 @@ } } } - target.attack(attacker.id(), PETRA.allowCapture(gameState, target, attacker)); + let allowCapture = PETRA.allowCapture(gameState, target, attacker); + if (target.canAttackTarget(attacker, allowCapture)) + target.attack(attacker.id(), allowCapture); } } };