Ticket #1716: formation-order-queues_formation-atack.patch

File formation-order-queues_formation-atack.patch, 20.2 KB (added by Simon, 11 years ago)

Adds a fix for #1624 based on the prior submitted patch by leper

  • binaries/data/mods/public/simulation/components/Formation.js

     
    5050    }
    5151
    5252    this.inPosition.push(ent);
    53     if (this.inPosition.length >= this.members.length)
     53    if (this.inPosition.length >= this.members.length) {
    5454        this.Disband();
     55    }
    5556};
    5657
    5758/**
     
    8182Formation.prototype.SetMembers = function(ents)
    8283{
    8384    this.members = ents;
    84 
    8585    for each (var ent in this.members)
    8686    {
    8787        var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
     
    122122    this.ComputeMotionParameters();
    123123
    124124    // Rearrange the remaining members
    125     this.MoveMembersIntoFormation(true);
     125    this.MoveMembersIntoFormation(true, true);
    126126};
    127127
    128128/**
     
    174174};
    175175
    176176/**
     177 * Call obj.functname(args) on UnitAI components of all members,
     178 * and return true if all calls return true.
     179 */
     180Formation.prototype.TestAllMemberFunction = function(funcname, args)
     181{
     182    for each (var ent in this.members)
     183    {
     184        var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
     185        if (!cmpUnitAI[funcname].apply(cmpUnitAI, args))
     186            return false;
     187    }
     188    return true;
     189};
     190
     191/**
    177192 * Set all members to form up into the formation shape.
    178  * If moveCenter is true, the formation center will be reinitialised
     193 * If moveCenter is true, the formation center will be reinitialized
    179194 * to the center of the units.
     195 * If force is true, all individual orders of the formation units are replaced,
     196 * otherwise the order to walk into formation is just pushed to the front.
    180197 */
    181 Formation.prototype.MoveMembersIntoFormation = function(moveCenter)
     198Formation.prototype.MoveMembersIntoFormation = function(moveCenter, force)
    182199{
    183200    var active = [];
    184201    var positions = [];
     
    213230        var offset = offsets[i];
    214231
    215232        var cmpUnitAI = Engine.QueryInterface(offset.ent, IID_UnitAI);
    216         cmpUnitAI.ReplaceOrder("FormationWalk", {
    217             "target": this.entity,
    218             "x": offset.x - avgoffset.x,
    219             "z": offset.z - avgoffset.z
    220         });
     233       
     234        if (force)
     235        {
     236            cmpUnitAI.ReplaceOrder("FormationWalk", {
     237                "target": this.entity,
     238                "x": offset.x - avgoffset.x,
     239                "z": offset.z - avgoffset.z
     240            });
     241        }
     242        else
     243        {
     244            cmpUnitAI.PushOrderFront("FormationWalk", {
     245                "target": this.entity,
     246                "x": offset.x - avgoffset.x,
     247                "z": offset.z - avgoffset.z
     248            });
     249        }
    221250    }
    222251};
    223252
     
    651680    var walkingDistance = cmpUnitAI.ComputeWalkingDistance();
    652681    var columnar = (walkingDistance > g_ColumnDistanceThreshold);
    653682    if (columnar != this.columnar)
    654         this.MoveMembersIntoFormation(false);
     683        this.MoveMembersIntoFormation(false, true);
    655684        // (disable moveCenter so we can't get stuck in a loop of switching
    656685        // shape causing center to change causing shape to switch back)
    657686};
  • binaries/data/mods/public/simulation/components/tests/test_UnitAI.js

     
    145145        TS_FAIL("invalid mode");
    146146}
    147147
     148function TestMoveIntoFormationWhileAttacking()
     149{
     150    ResetState();
     151
     152    var playerEntity = 5;
     153    var controller = 10;
     154    var enemy = 20;
     155    var unit = 30;
     156    var units = [];
     157    var unitCount = 8;
     158    var unitAIs = [];
     159
     160    AddMock(SYSTEM_ENTITY, IID_Timer, {
     161        SetInterval: function() { },
     162        SetTimeout: function() { },
     163    });
     164
     165
     166    AddMock(SYSTEM_ENTITY, IID_RangeManager, {
     167        CreateActiveQuery: function(ent, minRange, maxRange, players, iid, flags) {
     168            return 1;
     169        },
     170        EnableActiveQuery: function(id) { },
     171        ResetActiveQuery: function(id) { return [enemy]; },
     172        DisableActiveQuery: function(id) { },
     173        GetEntityFlagMask: function(identifier) { },
     174    });;
     175
     176    AddMock(SYSTEM_ENTITY, IID_PlayerManager, {
     177        GetPlayerByID: function(id) { return playerEntity; },
     178        GetNumPlayers: function() { return 2; },
     179    });
     180
     181    AddMock(playerEntity, IID_Player, {
     182        IsAlly: function() { return []; },
     183        IsEnemy: function() { return []; },
     184    });
     185
     186    // create units
     187    for (var i = 0; i < unitCount; i++) {
     188   
     189        units.push(unit + i);
     190       
     191        var unitAI = ConstructComponent(unit + i, "UnitAI", { "FormationController": "false", "DefaultStance": "aggressive" });
     192   
     193        AddMock(unit + i, IID_Identity, {
     194            GetClassesList: function() { return []; },
     195        });
     196   
     197        AddMock(unit + i, IID_Ownership, {
     198            GetOwner: function() { return 1; },
     199        });
     200   
     201        AddMock(unit + i, IID_Position, {
     202            GetPosition: function() { return { "x": 0, "z": 0 }; },
     203            IsInWorld: function() { return true; },
     204        });
     205   
     206        AddMock(unit + i, IID_UnitMotion, {
     207            GetWalkSpeed: function() { return 1; },
     208            MoveToFormationOffset: function(target, x, z) { },
     209            IsInTargetRange: function(target, min, max) { return true; },
     210            MoveToTargetRange: function(target, min, max) { },
     211            StopMoving: function() { },
     212        });
     213   
     214        AddMock(unit + i, IID_Vision, {
     215            GetRange: function() { return 10; },
     216        });
     217   
     218        AddMock(unit + i, IID_Attack, {
     219            GetRange: function() { return 10; },
     220            GetBestAttack: function() { return "melee"; },
     221            GetBestAttackAgainst: function(t) { return "melee"; },
     222            GetTimers: function() { return { "prepare": 500, "repeat": 1000 }; },
     223            CanAttack: function(v) { return true; },
     224            CompareEntitiesByPreference: function(a, b) { return 0; },
     225        });
     226       
     227        unitAI.OnCreate();
     228       
     229        unitAI.SetupRangeQuery(1);
     230       
     231        unitAIs.push(unitAI);
     232    }
     233   
     234    // create enemy
     235    AddMock(enemy, IID_Health, {
     236        GetHitpoints: function() { return 40; },
     237    });
     238
     239    var controllerFormation = ConstructComponent(controller, "Formation");
     240    var controllerAI = ConstructComponent(controller, "UnitAI", { "FormationController": "true", "DefaultStance": "aggressive" });
     241
     242    AddMock(controller, IID_Position, {
     243        JumpTo: function(x, z) { this.x = x; this.z = z; },
     244        GetPosition: function() { return { "x": this.x, "z": this.z }; },
     245        IsInWorld: function() { return true; },
     246    });
     247
     248    AddMock(controller, IID_UnitMotion, {
     249        SetUnitRadius: function(r) { },
     250        SetSpeed: function(speed) { },
     251        MoveToPointRange: function(x, z, minRange, maxRange) { },
     252        IsInTargetRange: function(target, min, max) { return true; },
     253    });
     254
     255    controllerAI.OnCreate();
     256
     257    controllerFormation.SetMembers(units);
     258   
     259    controllerAI.Attack(enemy, []);
     260   
     261    for each (var ent in unitAIs) {
     262        TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING");
     263    }
     264   
     265    controllerAI.MoveIntoFormation({"name": "Circle"});
     266   
     267    // let all units be in position
     268    for each (var ent in unitAIs) {
     269        controllerFormation.SetInPosition(ent);
     270    }
     271
     272    for each (var ent in unitAIs) {
     273        TS_ASSERT_EQUALS(unitAI.fsmStateName, "INDIVIDUAL.COMBAT.ATTACKING");
     274    }
     275   
     276    controllerFormation.Disband();
     277}
     278
    148279TestFormationExiting(0);
    149280TestFormationExiting(1);
    150281TestFormationExiting(2);
     282
     283TestMoveIntoFormationWhileAttacking();
     284 No newline at end of file
  • binaries/data/mods/public/simulation/components/UnitAI.js

     
    496496            this.MoveToPoint(this.order.data.x, this.order.data.z);
    497497            this.SetNextState("WALKING");
    498498        },
     499       
     500        "Order.MoveIntoFormation": function(msg) {
     501            var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
     502            cmpFormation.CallMemberFunction("SetHeldPosition", [msg.data.x, msg.data.z]);
    499503
     504            this.MoveToPoint(this.order.data.x, this.order.data.z);
     505            this.SetNextState("FORMING");
     506        },
     507
    500508        // Only used by other orders to walk there in formation
    501509        "Order.WalkToTargetRange": function(msg) {
    502510            if(this.MoveToTargetRangeExplicit(this.order.data.target, this.order.data.min, this.order.data.max))
     
    512520        },
    513521
    514522        "Order.Attack": function(msg) {
    515             // TODO: we should move in formation towards the target,
    516             // then break up into individuals when close enough to it
     523            // TODO on what should we base this range?
     524            // Check if we are already in range, otherwise walk there
     525            if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 20))
     526            {
     527                if (!this.TargetIsAlive(msg.data.target))
     528                    // The target was destroyed
     529                    this.FinishOrder();
     530                else
     531                    // Out of range; move there in formation
     532                    this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 20 });
     533                return;
     534            }
    517535
    518536            var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
     537            // We don't want to rearrange the formation if the individual units are carrying
     538            // out a task and one of the members dies/leaves the formation.
     539            cmpFormation.SetRearrange(false);
    519540            cmpFormation.CallMemberFunction("Attack", [msg.data.target, false]);
    520541
    521             // TODO: we should wait until the target is killed, then
    522             // move on to the next queued order.
    523             // Don't bother now, just disband the formation immediately.
    524             cmpFormation.Disband();
     542            this.SetNextStateAlwaysEntering("MEMBER");
    525543        },
    526544
    527545        "Order.Heal": function(msg) {
    528             // TODO: see notes in Order.Attack
     546            // TODO on what should we base this range?
     547            // Check if we are already in range, otherwise walk there
     548            if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10))
     549            {
     550                if (!this.TargetIsAlive(msg.data.target))
     551                    // The target was destroyed
     552                    this.FinishOrder();
     553                else
     554                    // Out of range; move there in formation
     555                    this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 });
     556                return;
     557            }
     558
    529559            var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
     560            // We don't want to rearrange the formation if the individual units are carrying
     561            // out a task and one of the members dies/leaves the formation.
     562            cmpFormation.SetRearrange(false);
    530563            cmpFormation.CallMemberFunction("Heal", [msg.data.target, false]);
    531             cmpFormation.Disband();
     564
     565            this.SetNextStateAlwaysEntering("MEMBER");
    532566        },
    533567
    534568        "Order.Repair": function(msg) {
     
    551585            cmpFormation.SetRearrange(false);
    552586            cmpFormation.CallMemberFunction("Repair", [msg.data.target, msg.data.autocontinue, false]);
    553587
    554             this.SetNextState("REPAIR");
     588            this.SetNextStateAlwaysEntering("REPAIR");
    555589        },
    556590
    557591        "Order.Gather": function(msg) {
    558             // TODO: see notes in Order.Attack
     592            // TODO on what should we base this range?
     593            // Check if we are already in range, otherwise walk there
     594            if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10))
     595            {
     596                if (!this.CanGather(msg.data.target))
     597                    // The target isn't gatherable
     598                    this.FinishOrder();
     599                    // TODO should be a GatherNearPosition order.
     600                // else if (!this.TargetIsAlive(msg.data.target))
     601                    // gather near position
     602                    //cmpFormation.CallMemberFunction("GatherNearPosition", [msg.data.lastPos.x, msg.data.lastPos.z, msg.data.type, msg.data.template, false]);
     603                else
     604                    // Out of range; move there in formation
     605                    this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 });
     606                return;
     607            }
    559608
    560             // If the resource no longer exists, send a GatherNearPosition order
    561609            var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
    562             if (this.CanGather(msg.data.target))
    563                 cmpFormation.CallMemberFunction("Gather", [msg.data.target, false]);
    564             else
    565                 cmpFormation.CallMemberFunction("GatherNearPosition", [msg.data.lastPos.x, msg.data.lastPos.z, msg.data.type, msg.data.template, false]);
     610            // We don't want to rearrange the formation if the individual units are carrying
     611            // out a task and one of the members dies/leaves the formation.
     612            cmpFormation.SetRearrange(false);
     613            cmpFormation.CallMemberFunction("Gather", [msg.data.target, false]);
    566614
    567             cmpFormation.Disband();
     615            this.SetNextStateAlwaysEntering("MEMBER");
    568616        },
    569617
    570618        "Order.GatherNearPosition": function(msg) {
    571             // TODO: see notes in Order.Attack
     619            // TODO on what should we base this range?
     620            // Check if we are already in range, otherwise walk there
     621            if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 20))
     622            {
     623                if (!this.TargetIsAlive(msg.data.target))
     624                    // The target was destroyed
     625                    this.FinishOrder();
     626                else
     627                    // Out of range; move there in formation
     628                    this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 20 });
     629                return;
     630            }
     631
    572632            var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
     633            // We don't want to rearrange the formation if the individual units are carrying
     634            // out a task and one of the members dies/leaves the formation.
     635            cmpFormation.SetRearrange(false);
    573636            cmpFormation.CallMemberFunction("GatherNearPosition", [msg.data.x, msg.data.z, msg.data.type, msg.data.template, false]);
    574             cmpFormation.Disband();
     637
     638            this.SetNextStateAlwaysEntering("MEMBER");
    575639        },
    576640
    577641        "Order.ReturnResource": function(msg) {
    578             // TODO: see notes in Order.Attack
     642            // TODO on what should we base this range?
     643            // Check if we are already in range, otherwise walk there
     644            if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10))
     645            {
     646                if (!this.TargetIsAlive(msg.data.target))
     647                    // The target was destroyed
     648                    this.FinishOrder();
     649                else
     650                    // Out of range; move there in formation
     651                    this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 });
     652                return;
     653            }
     654
    579655            var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
     656            // We don't want to rearrange the formation if the individual units are carrying
     657            // out a task and one of the members dies/leaves the formation.
     658            cmpFormation.SetRearrange(false);
    580659            cmpFormation.CallMemberFunction("ReturnResource", [msg.data.target, false]);
    581             cmpFormation.Disband();
     660
     661            this.SetNextStateAlwaysEntering("MEMBER");
    582662        },
    583663
    584664        "Order.Garrison": function(msg) {
    585             // TODO: see notes in Order.Attack
     665            // TODO on what should we base this range?
     666            // Check if we are already in range, otherwise walk there
     667            if (!this.CheckTargetRangeExplicit(msg.data.target, 0, 10))
     668            {
     669                if (!this.TargetIsAlive(msg.data.target))
     670                    // The target was destroyed
     671                    this.FinishOrder();
     672                else
     673                    // Out of range; move there in formation
     674                    this.PushOrderFront("WalkToTargetRange", { "target": msg.data.target, "min": 0, "max": 10 });
     675                return;
     676            }
     677
    586678            var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
     679            // We don't want to rearrange the formation if the individual units are carrying
     680            // out a task and one of the members dies/leaves the formation.
     681            cmpFormation.SetRearrange(false);
    587682            cmpFormation.CallMemberFunction("Garrison", [msg.data.target, false]);
    588             cmpFormation.Disband();
     683
     684            this.SetNextStateAlwaysEntering("MEMBER");
    589685        },
    590686
    591687        "IDLE": {
     
    595691            "MoveStarted": function(msg) {
    596692                var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
    597693                cmpFormation.SetRearrange(true);
    598                 cmpFormation.MoveMembersIntoFormation(true);
     694                cmpFormation.MoveMembersIntoFormation(true, true);
    599695            },
    600696
    601697            "MoveCompleted": function(msg) {
    602698                if (this.FinishOrder())
    603699                    return;
    604                    
     700
    605701                // If this was the last order, attempt to disband the formation.
    606702                var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
    607703                cmpFormation.FindInPosition();
    608704            },
    609705        },
     706       
     707        "FORMING": {
     708            "MoveStarted": function(msg) {
     709                var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
     710                cmpFormation.SetRearrange(true);
     711                cmpFormation.MoveMembersIntoFormation(true, false);
     712            },
    610713
     714            "MoveCompleted": function(msg) {
     715                if (this.FinishOrder())
     716                    return;
     717
     718                // If this was the last order, attempt to disband the formation.
     719                var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
     720                cmpFormation.FindInPosition();
     721            }
     722        },
     723
    611724        "REPAIR": {
    612725            "ConstructionFinished": function(msg) {
    613726                if (msg.data.entity != this.order.data.target)
     
    620733                cmpFormation.Disband();
    621734            },
    622735        },
     736
     737        "MEMBER": {
     738            // Wait for individual members to finish
     739            "enter": function(msg) {
     740                this.StartTimer(1000, 1000);
     741            },
     742
     743            "Timer": function(msg) {
     744                var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
     745
     746                // Have all members finished the task?
     747                if (!cmpFormation.TestAllMemberFunction("HasFinishedOrder", []))
     748                    return;
     749
     750                cmpFormation.CallMemberFunction("ResetFinishOrder", []);
     751               
     752                // Execute the next order
     753                if (this.FinishOrder())
     754                    return;
     755
     756                // No more order left.
     757                cmpFormation.Disband();
     758            },
     759
     760            "leave": function(msg) {
     761                this.StopTimer();
     762            },
     763        },
    623764    },
    624765
    625766
    626767    // States for entities moving as part of a formation:
    627768    "FORMATIONMEMBER": {
    628769        "FormationLeave": function(msg) {
     770            // We're not in a formation anymore, so no need to track this.
     771            this.finishedOrder = false;
     772
    629773            // Stop moving as soon as the formation disbands
    630774            this.StopMoving();
    631775
     
    685829            // is done moving. The controller is notified, and will disband the
    686830            // formation if all units are in formation and no orders remain.
    687831            "MoveCompleted": function(msg) {
     832                if(this.FinishOrder())
     833                    return;
    688834                var cmpFormation = Engine.QueryInterface(this.formationController, IID_Formation);
    689835                cmpFormation.SetInPosition(this.entity);
    690836            },
     
    18491995    this.isGarrisoned = false;
    18501996    this.isIdle = false;
    18511997    this.lastFormationName = "";
     1998    this.finishedOrder = false; // used to find if all formation members finished the order
    18521999
    18532000    // For preventing increased action rate due to Stop orders or target death.
    18542001    this.lastAttacked = undefined;
     
    18622009    return (this.template.FormationController == "true");
    18632010};
    18642011
     2012UnitAI.prototype.IsFormationMember = function()
     2013{
     2014    return (this.formationController != INVALID_ENTITY);
     2015};
     2016
     2017UnitAI.prototype.HasFinishedOrder = function()
     2018{
     2019    return this.finishedOrder;
     2020};
     2021
     2022UnitAI.prototype.ResetFinishOrder = function()
     2023{
     2024    this.finishedOrder = false;
     2025};
     2026
    18652027UnitAI.prototype.IsAnimal = function()
    18662028{
    18672029    return (this.template.NaturalBehaviour ? true : false);
     
    21492311    else
    21502312    {
    21512313        this.SetNextState("IDLE");
     2314
     2315        // Check if there are queued formation orders
     2316        if (this.IsFormationMember())
     2317        {
     2318            var cmpUnitAI = Engine.QueryInterface(this.formationController, IID_UnitAI);
     2319            if (cmpUnitAI)
     2320            {
     2321                // Inform the formation controller that we finished this task
     2322                this.finishedOrder = true;
     2323                // We don't want to carry out the default order
     2324                // if there are still queued formation orders left
     2325                if (cmpUnitAI.GetOrders().length > 1)
     2326                    return true;
     2327            }
     2328        }
     2329
    21522330        return false;
    21532331    }
    21542332};
     
    28873065    return this.lastFormationName;
    28883066};
    28893067
     3068UnitAI.prototype.MoveIntoFormation = function(cmd)
     3069{
     3070    var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
     3071    if (!cmpFormation)
     3072        return;
     3073    cmpFormation.LoadFormation(cmd.name);
     3074    var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
     3075    if (!cmpPosition || !cmpPosition.IsInWorld())
     3076        return;
     3077    var pos = cmpPosition.GetPosition();
     3078   
     3079    // add new order to get in formation at the current position
     3080    this.PushOrderFront("MoveIntoFormation", { "x": pos.x, "z": pos.z, "force": true });
     3081};
     3082
    28903083/**
    28913084 * Returns the estimated distance that this unit will travel before either
    28923085 * finishing all of its orders, or reaching a non-walk target (attack, gather, etc).
     
    29093102        switch (order.type)
    29103103        {
    29113104        case "Walk":
     3105        case "MoveIntoFormation":
    29123106        case "GatherNearPosition":
    29133107            // Add the distance to the target point
    29143108            var dx = order.data.x - pos.x;
  • binaries/data/mods/public/simulation/helpers/Commands.js

     
    370370    case "formation":
    371371        var entities = FilterEntityList(cmd.entities, player, controlAllUnits);
    372372        GetFormationUnitAIs(entities, player, cmd.name).forEach(function(cmpUnitAI) {
    373             var cmpFormation = Engine.QueryInterface(cmpUnitAI.entity, IID_Formation);
    374             if (!cmpFormation)
    375                 return;
    376             cmpFormation.LoadFormation(cmd.name);
    377             cmpFormation.MoveMembersIntoFormation(true);
     373            cmpUnitAI.MoveIntoFormation(cmd);
    378374        });
    379375        break;
    380376