diff --git a/binaries/data/mods/public/simulation/components/Attack.js b/binaries/data/mods/public/simulation/components/Attack.js index 5de5820fbc..a5fd11a777 100644 --- a/binaries/data/mods/public/simulation/components/Attack.js +++ b/binaries/data/mods/public/simulation/components/Attack.js @@ -334,7 +334,7 @@ Attack.prototype.CanAttack = function(target, wantedTypes) }; /** - * Returns null if we have no preference or the lowest index of a preferred class. + * Returns undefined if we have no preference or the lowest index of a preferred class. */ Attack.prototype.GetPreference = function(target) { @@ -344,7 +344,7 @@ Attack.prototype.GetPreference = function(target) let targetClasses = cmpIdentity.GetClassesList(); - let minPref = null; + let minPref; for (let type of this.GetAttackTypes()) { let preferredClasses = this.GetPreferredClasses(type); @@ -354,7 +354,7 @@ Attack.prototype.GetPreference = function(target) { if (pref === 0) return pref; - if ((minPref === null || minPref > pref)) + if ((minPref === undefined || minPref > pref)) minPref = pref; } } diff --git a/binaries/data/mods/public/simulation/components/UnitAI.js b/binaries/data/mods/public/simulation/components/UnitAI.js index 80bfbd2e69..4aff58d285 100644 --- a/binaries/data/mods/public/simulation/components/UnitAI.js +++ b/binaries/data/mods/public/simulation/components/UnitAI.js @@ -996,7 +996,9 @@ UnitAI.prototype.UnitFsmSpec = { }, "Timer": function(msg) { + Engine.ProfileStart("FindWalkAndFightTargets"); this.FindWalkAndFightTargets(); + Engine.ProfileStop(); }, "MovementUpdate": function(msg) { @@ -2221,7 +2223,11 @@ UnitAI.prototype.UnitFsmSpec = { if (this.FinishOrder()) { if (this.IsWalkingAndFighting()) + { + Engine.ProfileStart("FindWalkAndFightTargets"); this.FindWalkAndFightTargets(); + Engine.ProfileStop(); + } return true; } @@ -6056,58 +6062,77 @@ UnitAI.prototype.FindWalkAndFightTargets = function() { if (this.IsFormationController()) { - var cmpUnitAI; - var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); - for (var ent of cmpFormation.members) + let cmpUnitAI; + let cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); + for (let ent of cmpFormation.members) { if (!(cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI))) continue; - var targets = cmpUnitAI.GetTargetsFromUnit(); - for (var targ of targets) - { - if (!cmpUnitAI.CanAttack(targ)) - continue; - if (this.order.data.targetClasses) - { - var cmpIdentity = Engine.QueryInterface(targ, IID_Identity); - var targetClasses = this.order.data.targetClasses; - if (targetClasses.attack && cmpIdentity && - !MatchesClassList(cmpIdentity.GetClassesList(), targetClasses.attack)) - continue; - if (targetClasses.avoid && cmpIdentity && - MatchesClassList(cmpIdentity.GetClassesList(), targetClasses.avoid)) - continue; - // Only used by the AIs to prevent some choices of targets - if (targetClasses.vetoEntities && targetClasses.vetoEntities[targ]) - continue; - } - this.PushOrderFront("Attack", { "target": targ, "force": false, "allowCapture": this.order.data.allowCapture }); + if (cmpUnitAI.FindWalkAndFightTargets()) return true; - } } return false; } - var targets = this.GetTargetsFromUnit(); - for (var targ of targets) + let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); + + let entities; + if (!this.losAttackRangeQuery || !this.GetStance().targetVisibleEnemies || !cmpAttack) + entities = []; + else { - if (!this.CanAttack(targ)) - continue; - if (this.order.data.targetClasses) + let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); + entities = cmpRangeManager.ResetActiveQuery(this.losAttackRangeQuery); + } + + let attackfilter = e => { + if (this?.order?.data?.targetClasses) { - var cmpIdentity = Engine.QueryInterface(targ, IID_Identity); - var targetClasses = this.order.data.targetClasses; + let cmpIdentity = Engine.QueryInterface(e, IID_Identity); + let targetClasses = this.order.data.targetClasses; if (cmpIdentity && targetClasses.attack && !MatchesClassList(cmpIdentity.GetClassesList(), targetClasses.attack)) - continue; + return false; if (cmpIdentity && targetClasses.avoid && MatchesClassList(cmpIdentity.GetClassesList(), targetClasses.avoid)) - continue; + return false; // Only used by the AIs to prevent some choices of targets - if (targetClasses.vetoEntities && targetClasses.vetoEntities[targ]) - continue; + if (targetClasses.vetoEntities && targetClasses.vetoEntities[e]) + return false; } - this.PushOrderFront("Attack", { "target": targ, "force": false, "allowCapture": this.order.data.allowCapture }); + let cmpOwnership = Engine.QueryInterface(e, IID_Ownership); + if (cmpOwnership && cmpOwnership.GetOwner() > 0) + return true; + let cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI); + return cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal()); + }; + + let prefs = {}; + let bestPref; + let targets = []; + let pref; + for (let v of entities) + { + if (this.CanAttack(v) && attackfilter(v)) + { + pref = cmpAttack.GetPreference(v); + if (pref === 0) + { + this.PushOrderFront("Attack", { "target": v, "force": false, "allowCapture": this?.order?.data?.allowCapture }); + return true; + } + targets.push(v); + } + prefs[v] = pref; + if (pref !== undefined && (bestPref === undefined || pref < bestPref)) + bestPref = pref; + } + + for (let targ of targets) + { + if (prefs[targ] !== bestPref) + continue; + this.PushOrderFront("Attack", { "target": targ, "force": false, "allowCapture": this?.order?.data?.allowCapture }); return true; } @@ -6118,39 +6143,6 @@ UnitAI.prototype.FindWalkAndFightTargets = function() return false; }; -UnitAI.prototype.GetTargetsFromUnit = function() -{ - if (!this.losAttackRangeQuery) - return []; - - if (!this.GetStance().targetVisibleEnemies) - return []; - - let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); - if (!cmpAttack) - return []; - - let attackfilter = function(e) { - if (!cmpAttack.CanAttack(e)) - return false; - - let cmpOwnership = Engine.QueryInterface(e, IID_Ownership); - if (cmpOwnership && cmpOwnership.GetOwner() > 0) - return true; - - let cmpUnitAI = Engine.QueryInterface(e, IID_UnitAI); - return cmpUnitAI && (!cmpUnitAI.IsAnimal() || cmpUnitAI.IsDangerousAnimal()); - }; - - let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); - let entities = cmpRangeManager.ResetActiveQuery(this.losAttackRangeQuery); - let targets = entities.filter(attackfilter).sort(function(a, b) { - return cmpAttack.CompareEntitiesByPreference(a, b); - }); - - return targets; -}; - UnitAI.prototype.GetQueryRange = function(iid) { let ret = { "min": 0, "max": 0 }; diff --git a/binaries/data/mods/public/simulation/components/tests/test_Attack.js b/binaries/data/mods/public/simulation/components/tests/test_Attack.js index ef86bd5e46..a2c30878e4 100644 --- a/binaries/data/mods/public/simulation/components/tests/test_Attack.js +++ b/binaries/data/mods/public/simulation/components/tests/test_Attack.js @@ -397,7 +397,7 @@ function testAttackPreference() TS_ASSERT_EQUALS(cmpAttack.GetPreference(attacker+1), 0); TS_ASSERT_EQUALS(cmpAttack.GetPreference(attacker+2), 1); - TS_ASSERT_EQUALS(cmpAttack.GetPreference(attacker+3), null); - TS_ASSERT_EQUALS(cmpAttack.GetPreference(attacker+4), null); + TS_ASSERT_EQUALS(cmpAttack.GetPreference(attacker+3), undefined); + TS_ASSERT_EQUALS(cmpAttack.GetPreference(attacker+4), undefined); } testAttackPreference(); diff --git a/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js b/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js index fb026b4b6e..dffb0e4723 100644 --- a/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js +++ b/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js @@ -449,3 +449,77 @@ TestFormationExiting(1); TestFormationExiting(2); TestMoveIntoFormationWhileAttacking(); + + +function TestWalkAndFightTargets() +{ + const ent = 10; + let unitAI = ConstructComponent(ent, "UnitAI", { + "FormationController": "false", + "DefaultStance": "aggressive", + "FleeDistance": 10 + }); + unitAI.OnCreate(); + unitAI.losAttackRangeQuery = true; + + // The result is stored here + let result; + unitAI.PushOrderFront = function(type, order) + { + if (type === "Attack" && order?.target) + result = order.target; + }; + + // Create some targets. + AddMock(ent+1, IID_UnitAI, { "IsAnimal": () => true, "IsDangerousAnimal": () => false }); + AddMock(ent+2, IID_Ownership, { "GetOwner": () => 2 }); + AddMock(ent+3, IID_Ownership, { "GetOwner": () => 2 }); + AddMock(ent+4, IID_Ownership, { "GetOwner": () => 2 }); + AddMock(ent+5, IID_Ownership, { "GetOwner": () => 2 }); + AddMock(ent+6, IID_Ownership, { "GetOwner": () => 2 }); + AddMock(ent+7, IID_Ownership, { "GetOwner": () => 2 }); + + unitAI.CanAttack = function(target) + { + return target !== ent+2 && target !== ent+7; + }; + + AddMock(ent, IID_Attack, { + "GetPreference": (target) => ({ + [ent+4]: 0, + [ent+5]: 1, + [ent+6]: 2, + [ent+7]: 0 + }?.[target]) + }); + + let runTest = function(ents, res) + { + result = undefined; + AddMock(SYSTEM_ENTITY, IID_RangeManager, { + "ResetActiveQuery": () => ents + }); + TS_ASSERT_EQUALS(unitAI.FindWalkAndFightTargets(), !!res); + TS_ASSERT_EQUALS(result, res); + }; + + // No entities. + runTest([]); + + // Entities that cannot be attacked. + runTest([ent+1, ent+2, ent+7]); + + // No preference, one attackable entity. + runTest([ent+1, ent+2, ent+3], ent+3); + + // Check preferences. + runTest([ent+1, ent+2, ent+3, ent+4], ent+4); + runTest([ent+1, ent+2, ent+3, ent+4, ent+5], ent+4); + runTest([ent+1, ent+2, ent+6, ent+3, ent+4, ent+5], ent+4); + runTest([ent+1, ent+2, ent+7, ent+6, ent+3, ent+4, ent+5], ent+4); + runTest([ent+1, ent+2, ent+7, ent+6, ent+3, ent+5], ent+5); + runTest([ent+1, ent+2, ent+7, ent+6, ent+3], ent+6); + runTest([ent+1, ent+2, ent+7, ent+3], ent+3); +} + +TestWalkAndFightTargets();