From 5800be133497969fc43347f8ec94e42954351d4e Mon Sep 17 00:00:00 2001 From: leper Date: Sun, 2 Dec 2012 01:52:27 +0000 Subject: [PATCH] Formation order queueing. Fixes #592, #593 and #1716. Allow changing the formation, while under attack. Fixes #1624. Patch by Simikolon. This was SVN commit r12911. --- .../public/simulation/components/Formation.js | 45 ++- .../public/simulation/components/UnitAI.js | 304 ++++++++++++++---- .../components/tests/test_UnitAI.js | 133 ++++++++ .../public/simulation/helpers/Commands.js | 6 +- 4 files changed, 421 insertions(+), 67 deletions(-) diff --git a/binaries/data/mods/public/simulation/components/Formation.js b/binaries/data/mods/public/simulation/components/Formation.js index 61b5ea81b7..87941fe716 100644 --- a/binaries/data/mods/public/simulation/components/Formation.js +++ b/binaries/data/mods/public/simulation/components/Formation.js @@ -122,7 +122,7 @@ Formation.prototype.RemoveMembers = function(ents) this.ComputeMotionParameters(); // Rearrange the remaining members - this.MoveMembersIntoFormation(true); + this.MoveMembersIntoFormation(true, true); }; /** @@ -173,12 +173,29 @@ Formation.prototype.CallMemberFunction = function(funcname, args) } }; +/** + * Call obj.functname(args) on UnitAI components of all members, + * and return true if all calls return true. + */ +Formation.prototype.TestAllMemberFunction = function(funcname, args) +{ + for each (var ent in this.members) + { + var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); + if (!cmpUnitAI[funcname].apply(cmpUnitAI, args)) + return false; + } + return true; +}; + /** * Set all members to form up into the formation shape. * If moveCenter is true, the formation center will be reinitialised * to the center of the units. + * If force is true, all individual orders of the formation units are replaced, + * otherwise the order to walk into formation is just pushed to the front. */ -Formation.prototype.MoveMembersIntoFormation = function(moveCenter) +Formation.prototype.MoveMembersIntoFormation = function(moveCenter, force) { var active = []; var positions = []; @@ -213,11 +230,23 @@ Formation.prototype.MoveMembersIntoFormation = function(moveCenter) var offset = offsets[i]; var cmpUnitAI = Engine.QueryInterface(offset.ent, IID_UnitAI); - cmpUnitAI.ReplaceOrder("FormationWalk", { - "target": this.entity, - "x": offset.x - avgoffset.x, - "z": offset.z - avgoffset.z - }); + + if (force) + { + cmpUnitAI.ReplaceOrder("FormationWalk", { + "target": this.entity, + "x": offset.x - avgoffset.x, + "z": offset.z - avgoffset.z + }); + } + else + { + cmpUnitAI.PushOrderFront("FormationWalk", { + "target": this.entity, + "x": offset.x - avgoffset.x, + "z": offset.z - avgoffset.z + }); + } } }; @@ -651,7 +680,7 @@ Formation.prototype.OnUpdate_Final = function(msg) var walkingDistance = cmpUnitAI.ComputeWalkingDistance(); var columnar = (walkingDistance > g_ColumnDistanceThreshold); if (columnar != this.columnar) - this.MoveMembersIntoFormation(false); + this.MoveMembersIntoFormation(false, true); // (disable moveCenter so we can't get stuck in a loop of switching // shape causing center to change causing shape to switch back) }; diff --git a/binaries/data/mods/public/simulation/components/UnitAI.js b/binaries/data/mods/public/simulation/components/UnitAI.js index 1f181454f2..83f016e640 100644 --- a/binaries/data/mods/public/simulation/components/UnitAI.js +++ b/binaries/data/mods/public/simulation/components/UnitAI.js @@ -602,6 +602,14 @@ var UnitFsmSpec = { this.MoveToPoint(this.order.data.x, this.order.data.z); this.SetNextState("WALKING"); }, + + "Order.MoveIntoFormation": function(msg) { + var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); + cmpFormation.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]); + + this.MoveToPoint(this.order.data.x, this.order.data.z); + this.SetNextState("FORMING"); + }, // Only used by other orders to walk there in formation "Order.WalkToTargetRange": function(msg) { @@ -618,27 +626,124 @@ var UnitFsmSpec = { }, "Order.Attack": function(msg) { - // TODO: we should move in formation towards the target, - // then break up into individuals when close enough to it + // TODO: on what should we base this range? + // Check if we are already in range, otherwise walk there + if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 20)) + { + if (!this.TargetIsAlive(msg.data.target)) + // The target was destroyed + this.FinishOrder(); + else + // Out of range; move there in formation + this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 20 }); + return; + } var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); + // We don't want to rearrange the formation if the individual units are carrying + // out a task and one of the members dies/leaves the formation. + cmpFormation.SetRearrange(false); cmpFormation.CallMemberFunction("Attack", [msg.data.target, false]); - // TODO: we should wait until the target is killed, then - // move on to the next queued order. - // Don't bother now, just disband the formation immediately. - cmpFormation.Disband(); + this.SetNextStateAlwaysEntering("MEMBER"); + }, + + "Order.Garrison": function(msg) { + // TODO: on what should we base this range? + // Check if we are already in range, otherwise walk there + if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10)) + { + if (!this.TargetIsAlive(msg.data.target)) + // The target was destroyed + this.FinishOrder(); + else + // Out of range; move there in formation + this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 }); + return; + } + + var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); + // We don't want to rearrange the formation if the individual units are carrying + // out a task and one of the members dies/leaves the formation. + cmpFormation.SetRearrange(false); + cmpFormation.CallMemberFunction("Garrison", [msg.data.target, false]); + + this.SetNextStateAlwaysEntering("MEMBER"); + }, + + "Order.Gather": function(msg) { + // TODO: on what should we base this range? + // Check if we are already in range, otherwise walk there + if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10)) + { + if (!this.CanGather(msg.data.target)) + // The target isn't gatherable + this.FinishOrder(); + // TODO: Should we issue a gather-near-position order + // if the target isn't gatherable/doesn't exist anymore? + else + // Out of range; move there in formation + this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 }); + return; + } + + var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); + // We don't want to rearrange the formation if the individual units are carrying + // out a task and one of the members dies/leaves the formation. + cmpFormation.SetRearrange(false); + cmpFormation.CallMemberFunction("Gather", [msg.data.target, false]); + + this.SetNextStateAlwaysEntering("MEMBER"); + }, + + "Order.GatherNearPosition": function(msg) { + // TODO: on what should we base this range? + // Check if we are already in range, otherwise walk there + if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 20)) + { + if (!this.TargetIsAlive(msg.data.target)) + // The target was destroyed + this.FinishOrder(); + else + // Out of range; move there in formation + this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 20 }); + return; + } + + var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); + // We don't want to rearrange the formation if the individual units are carrying + // out a task and one of the members dies/leaves the formation. + cmpFormation.SetRearrange(false); + cmpFormation.CallMemberFunction("GatherNearPosition", [msg.data.x, msg.data.z, msg.data.type, msg.data.template, false]); + + this.SetNextStateAlwaysEntering("MEMBER"); }, "Order.Heal": function(msg) { - // TODO: see notes in Order.Attack + // TODO: on what should we base this range? + // Check if we are already in range, otherwise walk there + if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10)) + { + if (!this.TargetIsAlive(msg.data.target)) + // The target was destroyed + this.FinishOrder(); + else + // Out of range; move there in formation + this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 }); + return; + } + var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); + // We don't want to rearrange the formation if the individual units are carrying + // out a task and one of the members dies/leaves the formation. + cmpFormation.SetRearrange(false); cmpFormation.CallMemberFunction("Heal", [msg.data.target, false]); - cmpFormation.Disband(); + + this.SetNextStateAlwaysEntering("MEMBER"); }, "Order.Repair": function(msg) { - // TODO on what should we base this range? + // TODO: on what should we base this range? // Check if we are already in range, otherwise walk there if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10)) { @@ -657,55 +762,50 @@ var UnitFsmSpec = { cmpFormation.SetRearrange(false); cmpFormation.CallMemberFunction("Repair", [msg.data.target, msg.data.autocontinue, false]); - this.SetNextState("REPAIR"); - }, - - "Order.Gather": function(msg) { - // TODO: see notes in Order.Attack - - // If the resource no longer exists, send a GatherNearPosition order - var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); - if (this.CanGather(msg.data.target)) - cmpFormation.CallMemberFunction("Gather", [msg.data.target, false]); - else - cmpFormation.CallMemberFunction("GatherNearPosition", [msg.data.lastPos.x, msg.data.lastPos.z, msg.data.type, msg.data.template, false]); - - cmpFormation.Disband(); - }, - - "Order.GatherNearPosition": function(msg) { - // TODO: see notes in Order.Attack - var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); - cmpFormation.CallMemberFunction("GatherNearPosition", [msg.data.x, msg.data.z, msg.data.type, msg.data.template, false]); - cmpFormation.Disband(); + this.SetNextStateAlwaysEntering("MEMBER"); }, "Order.ReturnResource": function(msg) { - // TODO: see notes in Order.Attack + // TODO: on what should we base this range? + // Check if we are already in range, otherwise walk there + if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10)) + { + if (!this.TargetIsAlive(msg.data.target)) + // The target was destroyed + this.FinishOrder(); + else + // Out of range; move there in formation + this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 }); + return; + } + var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); + // We don't want to rearrange the formation if the individual units are carrying + // out a task and one of the members dies/leaves the formation. + cmpFormation.SetRearrange(false); cmpFormation.CallMemberFunction("ReturnResource", [msg.data.target, false]); - cmpFormation.Disband(); + + this.SetNextStateAlwaysEntering("MEMBER"); }, - "Order.Garrison": function(msg) { - // TODO: see notes in Order.Attack - var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); - cmpFormation.CallMemberFunction("Garrison", [msg.data.target, false]); - cmpFormation.Disband(); - }, - "Order.Pack": function(msg) { - // TODO: see notes in Order.Attack var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); + // We don't want to rearrange the formation if the individual units are carrying + // out a task and one of the members dies/leaves the formation. + cmpFormation.SetRearrange(false); cmpFormation.CallMemberFunction("Pack", [false]); - cmpFormation.Disband(); + + this.SetNextStateAlwaysEntering("MEMBER"); }, - + "Order.Unpack": function(msg) { - // TODO: see notes in Order.Attack var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); + // We don't want to rearrange the formation if the individual units are carrying + // out a task and one of the members dies/leaves the formation. + cmpFormation.SetRearrange(false); cmpFormation.CallMemberFunction("Unpack", [false]); - cmpFormation.Disband(); + + this.SetNextStateAlwaysEntering("MEMBER"); }, "IDLE": { @@ -715,30 +815,70 @@ var UnitFsmSpec = { "MoveStarted": function(msg) { var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); cmpFormation.SetRearrange(true); - cmpFormation.MoveMembersIntoFormation(true); + cmpFormation.MoveMembersIntoFormation(true, true); }, "MoveCompleted": function(msg) { - if (this.FinishOrder()) - return; - - // If this was the last order, attempt to disband the formation. var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); + + if (this.FinishOrder()) + { + cmpFormation.CallMemberFunction("ResetFinishOrder", []); + return; + } + + // If this was the last order, attempt to disband the formation. cmpFormation.FindInPosition(); }, }, + + "FORMING": { + "MoveStarted": function(msg) { + var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); + cmpFormation.SetRearrange(true); + cmpFormation.MoveMembersIntoFormation(true, false); + }, - "REPAIR": { - "ConstructionFinished": function(msg) { - if (msg.data.entity != this.order.data.target) + "MoveCompleted": function(msg) { + var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); + + if (this.FinishOrder()) + { + cmpFormation.CallMemberFunction("ResetFinishOrder", []); + return; + } + + // If this was the last order, attempt to disband the formation. + cmpFormation.FindInPosition(); + } + }, + + "MEMBER": { + // Wait for individual members to finish + "enter": function(msg) { + this.StartTimer(1000, 1000); + }, + + "Timer": function(msg) { + var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); + + // Have all members finished the task? + if (!cmpFormation.TestAllMemberFunction("HasFinishedOrder", [])) return; + cmpFormation.CallMemberFunction("ResetFinishOrder", []); + + // Execute the next order if (this.FinishOrder()) return; - var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); + // No more order left. cmpFormation.Disband(); }, + + "leave": function(msg) { + this.StopTimer(); + }, }, }, @@ -746,6 +886,9 @@ var UnitFsmSpec = { // States for entities moving as part of a formation: "FORMATIONMEMBER": { "FormationLeave": function(msg) { + // We're not in a formation anymore, so no need to track this. + this.finishedOrder = false; + // Stop moving as soon as the formation disbands this.StopMoving(); @@ -807,6 +950,9 @@ var UnitFsmSpec = { // is done moving. The controller is notified, and will disband the // formation if all units are in formation and no orders remain. "MoveCompleted": function(msg) { + if(this.FinishOrder()) + return; + var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation); cmpFormation.SetInPosition(this.entity); }, @@ -1663,7 +1809,6 @@ var UnitFsmSpec = { "REPAIRING": { "enter": function() { - // If this order was forced, the player probably gave it, but now we've reached the target // switch to an unforced order (can be interrupted by attacks) if (this.order.data.force) @@ -2032,6 +2177,7 @@ UnitAI.prototype.Init = function() this.isGarrisoned = false; this.isIdle = false; this.lastFormationName = ""; + this.finishedOrder = false; // used to find if all formation members finished the order // For preventing increased action rate due to Stop orders or target death. this.lastAttacked = undefined; @@ -2045,6 +2191,21 @@ UnitAI.prototype.IsFormationController = function() return (this.template.FormationController == "true"); }; +UnitAI.prototype.IsFormationMember = function() +{ + return (this.formationController != INVALID_ENTITY); +}; + +UnitAI.prototype.HasFinishedOrder = function() +{ + return this.finishedOrder; +}; + +UnitAI.prototype.ResetFinishOrder = function() +{ + this.finishedOrder = false; +}; + UnitAI.prototype.IsAnimal = function() { return (this.template.NaturalBehaviour ? true : false); @@ -2332,6 +2493,22 @@ UnitAI.prototype.FinishOrder = function() else { this.SetNextState("IDLE"); + + // Check if there are queued formation orders + if (this.IsFormationMember()) + { + var cmpUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI); + if (cmpUnitAI) + { + // Inform the formation controller that we finished this task + this.finishedOrder = true; + // We don't want to carry out the default order + // if there are still queued formation orders left + if (cmpUnitAI.GetOrders().length > 1) + return true; + } + } + return false; } }; @@ -3089,6 +3266,24 @@ UnitAI.prototype.GetLastFormationName = function() return this.lastFormationName; }; +UnitAI.prototype.MoveIntoFormation = function(cmd) +{ + var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation); + if (!cmpFormation) + return; + + cmpFormation.LoadFormation(cmd.name); + + var cmpPosition = Engine.QueryInterface(this.entity, IID_Position); + if (!cmpPosition || !cmpPosition.IsInWorld()) + return; + + var pos = cmpPosition.GetPosition(); + + // Add new order to move into formation at the current position + this.PushOrderFront("MoveIntoFormation", { "x": pos.x, "z": pos.z, "force": true }); +}; + /** * Returns the estimated distance that this unit will travel before either * finishing all of its orders, or reaching a non-walk target (attack, gather, etc). @@ -3111,6 +3306,7 @@ UnitAI.prototype.ComputeWalkingDistance = function() switch (order.type) { case "Walk": + case "MoveIntoFormation": case "GatherNearPosition": // Add the distance to the target point var dx = order.data.x - pos.x; 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 a6bf357fde..1d3c0fa3ce 100644 --- a/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js +++ b/binaries/data/mods/public/simulation/components/tests/test_UnitAI.js @@ -146,6 +146,139 @@ function TestFormationExiting(mode) TS_FAIL("invalid mode"); } +function TestMoveIntoFormationWhileAttacking() +{ + ResetState(); + + var playerEntity = 5; + var controller = 10; + var enemy = 20; + var unit = 30; + var units = []; + var unitCount = 8; + var unitAIs = []; + + AddMock(SYSTEM_ENTITY, IID_Timer, { + SetInterval: function() { }, + SetTimeout: function() { }, + }); + + + AddMock(SYSTEM_ENTITY, IID_RangeManager, { + CreateActiveQuery: function(ent, minRange, maxRange, players, iid, flags) { + return 1; + }, + EnableActiveQuery: function(id) { }, + ResetActiveQuery: function(id) { return [enemy]; }, + DisableActiveQuery: function(id) { }, + GetEntityFlagMask: function(identifier) { }, + });; + + AddMock(SYSTEM_ENTITY, IID_PlayerManager, { + GetPlayerByID: function(id) { return playerEntity; }, + GetNumPlayers: function() { return 2; }, + }); + + AddMock(playerEntity, IID_Player, { + IsAlly: function() { return []; }, + IsEnemy: function() { return []; }, + }); + + // create units + for (var i = 0; i < unitCount; i++) { + + units.push(unit + i); + + var unitAI = ConstructComponent(unit + i, "UnitAI", { "FormationController": "false", "DefaultStance": "aggressive" }); + + AddMock(unit + i, IID_Identity, { + GetClassesList: function() { return []; }, + }); + + AddMock(unit + i, IID_Ownership, { + GetOwner: function() { return 1; }, + }); + + AddMock(unit + i, IID_Position, { + GetPosition: function() { return { "x": 0, "z": 0 }; }, + IsInWorld: function() { return true; }, + }); + + AddMock(unit + i, IID_UnitMotion, { + GetWalkSpeed: function() { return 1; }, + MoveToFormationOffset: function(target, x, z) { }, + IsInTargetRange: function(target, min, max) { return true; }, + MoveToTargetRange: function(target, min, max) { }, + StopMoving: function() { }, + }); + + AddMock(unit + i, IID_Vision, { + GetRange: function() { return 10; }, + }); + + AddMock(unit + i, IID_Attack, { + GetRange: function() { return 10; }, + GetBestAttack: function() { return "melee"; }, + GetBestAttackAgainst: function(t) { return "melee"; }, + GetTimers: function() { return { "prepare": 500, "repeat": 1000 }; }, + CanAttack: function(v) { return true; }, + CompareEntitiesByPreference: function(a, b) { return 0; }, + }); + + unitAI.OnCreate(); + + unitAI.SetupRangeQuery(1); + + unitAIs.push(unitAI); + } + + // create enemy + AddMock(enemy, IID_Health, { + GetHitpoints: function() { return 40; }, + }); + + var controllerFormation = ConstructComponent(controller, "Formation"); + var controllerAI = ConstructComponent(controller, "UnitAI", { "FormationController": "true", "DefaultStance": "aggressive" }); + + AddMock(controller, IID_Position, { + JumpTo: function(x, z) { this.x = x; this.z = z; }, + GetPosition: function() { return { "x": this.x, "z": this.z }; }, + IsInWorld: function() { return true; }, + }); + + AddMock(controller, IID_UnitMotion, { + SetUnitRadius: function(r) { }, + SetSpeed: function(speed) { }, + MoveToPointRange: function(x, z, minRange, maxRange) { }, + IsInTargetRange: function(target, min, max) { return true; }, + }); + + controllerAI.OnCreate(); + + controllerFormation.SetMembers(units); + + controllerAI.Attack(enemy, []); + + for each (var ent in unitAIs) { + TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING"); + } + + controllerAI.MoveIntoFormation({"name": "Circle"}); + + // let all units be in position + for each (var ent in unitAIs) { + controllerFormation.SetInPosition(ent); + } + + for each (var ent in unitAIs) { + TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING"); + } + + controllerFormation.Disband(); +} + TestFormationExiting(0); TestFormationExiting(1); TestFormationExiting(2); + +TestMoveIntoFormationWhileAttacking(); diff --git a/binaries/data/mods/public/simulation/helpers/Commands.js b/binaries/data/mods/public/simulation/helpers/Commands.js index 9a142c6014..9ebecc0547 100644 --- a/binaries/data/mods/public/simulation/helpers/Commands.js +++ b/binaries/data/mods/public/simulation/helpers/Commands.js @@ -370,11 +370,7 @@ function ProcessCommand(player, cmd) case "formation": var entities = FilterEntityList(cmd.entities, player, controlAllUnits); GetFormationUnitAIs(entities, player, cmd.name).forEach(function(cmpUnitAI) { - var cmpFormation = Engine.QueryInterface(cmpUnitAI.entity, IID_Formation); - if (!cmpFormation) - return; - cmpFormation.LoadFormation(cmd.name); - cmpFormation.MoveMembersIntoFormation(true); + cmpUnitAI.MoveIntoFormation(cmd); }); break;