Index: binaries/data/mods/public/simulation/components/UnitAI.js =================================================================== --- binaries/data/mods/public/simulation/components/UnitAI.js +++ binaries/data/mods/public/simulation/components/UnitAI.js @@ -193,6 +193,13 @@ // ignore }, + "OrderTargetRenamed": function() { + // By default, trigger an exit-reenter + // so that state preconditions are checked against the new entity + // (there is no reason to assume the target is still valid). + this.SetNextState(this.GetCurrentState()); + }, + // Formation handlers: "FormationLeave": function(msg) { @@ -1785,6 +1792,14 @@ return false; }, + "OrderTargetRenamed": function(msg) { + // To avoid replaying the panic sound, handle this explicitly. + let cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion); + if (this.CheckTargetRangeExplicit(this.order.data.target, this.order.data.distanceToFlee, -1) || + !cmpUnitMotion || !cmpUnitMotion.MoveToTargetRange(this.order.data.target, this.order.data.distanceToFlee, -1)) + this.FinishOrder(); + }, + "HealthChanged": function() { this.SetSpeedMultiplier(this.GetRunMultiplier()); }, @@ -1974,6 +1989,7 @@ "Timer": function(msg) { let target = this.order.data.target; + let attackType = this.order.data.attackType; // Check the target is still alive and attackable if (!this.CanAttack(target)) @@ -1996,11 +2012,17 @@ if (!cmpBuildingAI) { let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); - cmpAttack.PerformAttack(this.order.data.attackType, target); + cmpAttack.PerformAttack(attackType, target); } + // PerformAttack might have triggered messages that moved us to another state. + // (use 'ends with' to handle animals/formation members copying our state). + if (!this.GetCurrentState().endsWith("COMBAT.ATTACKING")) + return; + + // Check we can still reach the target for the next attack - if (this.CheckTargetAttackRange(target, this.order.data.attackType)) + if (this.CheckTargetAttackRange(target, attackType)) { if (this.resyncAnimation) { @@ -2957,7 +2979,7 @@ } // Unit was approaching and there's nothing to do now, so switch to walking - if (oldState === "INDIVIDUAL.REPAIR.APPROACHING") + if (oldState.endsWith("REPAIR.APPROACHING")) // We're already walking to the given point, so add this as a order. this.WalkToTarget(msg.data.newentity, true); }, @@ -4149,24 +4171,32 @@ UnitAI.prototype.OnGlobalEntityRenamed = function(msg) { let changed = false; - for (let order of this.orderQueue) + let currentOrderChanged = false; + for (let i = 0; i < this.orderQueue.length; ++i) { + let order = this.orderQueue[i]; if (order.data && order.data.target && order.data.target == msg.entity) { changed = true; + if (i == 0) + currentOrderChanged = true; order.data.target = msg.newentity; } if (order.data && order.data.formationTarget && order.data.formationTarget == msg.entity) { changed = true; + if (i == 0) + currentOrderChanged = true; order.data.formationTarget = msg.newentity; } } - if (this.repairTarget && this.repairTarget == msg.entity) - this.repairTarget = msg.newentity; + if (!changed) + return; - if (changed) - Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); + if (currentOrderChanged) + this.UnitFsm.ProcessMessage(this, { "type": "OrderTargetRenamed", "data": msg }); + + Engine.PostMessage(this.entity, MT_UnitAIOrderDataChanged, { "to": this.GetOrderData() }); }; UnitAI.prototype.OnAttacked = function(msg) Index: binaries/data/mods/public/simulation/components/tests/test_UnitAI.js =================================================================== --- binaries/data/mods/public/simulation/components/tests/test_UnitAI.js +++ binaries/data/mods/public/simulation/components/tests/test_UnitAI.js @@ -1,8 +1,10 @@ Engine.LoadHelperScript("FSM.js"); Engine.LoadHelperScript("Entity.js"); Engine.LoadHelperScript("Player.js"); +Engine.LoadHelperScript("Sound.js"); Engine.LoadComponentScript("interfaces/Attack.js"); Engine.LoadComponentScript("interfaces/Auras.js"); +Engine.LoadComponentScript("interfaces/Builder.js"); Engine.LoadComponentScript("interfaces/BuildingAI.js"); Engine.LoadComponentScript("interfaces/Capturable.js"); Engine.LoadComponentScript("interfaces/Resistance.js"); @@ -17,6 +19,87 @@ Engine.LoadComponentScript("Formation.js"); Engine.LoadComponentScript("UnitAI.js"); +/** + * Fairly straightforward test that entity renaming is handled + * by unitAI states. These ought to be augmented with integration tests, ideally. + */ +function TestTargetEntityRenaming(init_state, post_state, setup) +{ + ResetState(); + const player_ent = 5; + const target_ent = 6; + + AddMock(SYSTEM_ENTITY, IID_Timer, { + "SetInterval": () => {}, + "SetTimeout": () => {} + }); + AddMock(SYSTEM_ENTITY, IID_ObstructionManager, { + "IsInTargetRange": () => false + }); + + let unitAI = ConstructComponent(player_ent, "UnitAI", { + "FormationController": "false", + "DefaultStance": "aggressive", + "FleeDistance": 10 + }); + unitAI.OnCreate(); + + setup(unitAI, player_ent, target_ent); + + TS_ASSERT_EQUALS(unitAI.GetCurrentState(), init_state); + + unitAI.OnGlobalEntityRenamed({ + "entity": target_ent, + "newentity": target_ent + 1 + }); + + TS_ASSERT_EQUALS(unitAI.GetCurrentState(), post_state); +} + +TestTargetEntityRenaming( + "INDIVIDUAL.GARRISON.APPROACHING", "INDIVIDUAL.IDLE", + (unitAI, player_ent, target_ent) => { + unitAI.CanGarrison = (target) => target == target_ent; + unitAI.MoveToGarrisonRange = (target) => target == target_ent; + unitAI.AbleToMove = () => true; + + AddMock(target_ent, IID_GarrisonHolder, { + "CanPickup": () => false + }); + + unitAI.Garrison(target_ent, false); + } +); + +TestTargetEntityRenaming( + "INDIVIDUAL.REPAIR.REPAIRING", "INDIVIDUAL.IDLE", + (unitAI, player_ent, target_ent) => { + QueryBuilderListInterface = () => {}; + unitAI.CheckTargetRange = () => true; + unitAI.CanRepair = (target) => target == target_ent; + + unitAI.Repair(target_ent, false, false); + } +); + + +TestTargetEntityRenaming( + "INDIVIDUAL.FLEEING", "INDIVIDUAL.FLEEING", + (unitAI, player_ent, target_ent) => { + DistanceBetweenEntities = () => 10; + unitAI.CheckTargetRangeExplicit = () => false; + + AddMock(player_ent, IID_UnitMotion, { + "MoveToTargetRange": () => true, + "GetRunMultiplier": () => 1, + "SetSpeedMultiplier": () => {}, + "StopMoving": () => {} + }); + + unitAI.Flee(target_ent, false); + } +); + /* Regression test. * Tests the FSM behaviour of a unit when walking as part of a formation, * then exiting the formation.