Ticket #2034: escort-v2.diff

File escort-v2.diff, 39.1 KB (added by mimo, 10 years ago)
  • binaries/data/config/default.cfg

     
    272272hotkey.session.attackmove = Ctrl            ; Modifier to attackmove when clicking on a point
    273273hotkey.session.garrison = Ctrl              ; Modifier to garrison when clicking on building
    274274hotkey.session.autorallypoint = Ctrl        ; Modifier to set the rally point on the building itself
     275hotkey.session.guard = "G"                  ; Modifier to escort/guard when clicking on unit/building
    275276hotkey.session.queue = Shift                ; Modifier to queue unit orders instead of replacing
    276277hotkey.session.batchtrain = Shift           ; Modifier to train units in batches
    277278hotkey.session.massbarter = Shift           ; Modifier to barter bunch of resources
     
    300301hotkey.menu.toggle = "F10"                   ; Toggle in-game menu
    301302hotkey.timeelapsedcounter.toggle = "F12"     ; Toggle time elapsed counter
    302303hotkey.session.showstatusbars = Tab          ; Toggle display of status bars
     304hotkey.session.highlightguarding = PgDn      ; Toggle highlight of guarding units
     305hotkey.session.highlightguarded = PgUp       ; Toggle highlight of guarded units
    303306
    304307; > HOTKEYS ONLY
    305308hotkey.chat = Return                        ; Toggle chat window
  • binaries/data/mods/public/art/textures/cursors/action-guard-disabled.png

    Impossible d'afficher : fichier considéré comme binaire.
    svn:mime-type = application/octet-stream
  • binaries/data/mods/public/art/textures/cursors/action-guard-disabled.txt

    Modification de propriétés sur binaries/data/mods/public/art/textures/cursors/action-guard-disabled.png
    ___________________________________________________________________
    Added: svn:mime-type
    ## -0,0 +1 ##
    +application/octet-stream
    \ No newline at end of property
     
     11 1
  • binaries/data/mods/public/art/textures/ui/session/icons/add-guard.png

    Impossible d'afficher : fichier considéré comme binaire.
    svn:mime-type = application/octet-stream
  • binaries/data/mods/public/art/textures/ui/session/icons/remove-guard.png

    Modification de propriétés sur binaries/data/mods/public/art/textures/ui/session/icons/add-guard.png
    ___________________________________________________________________
    Added: svn:mime-type
    ## -0,0 +1 ##
    +application/octet-stream
    \ No newline at end of property
    Impossible d'afficher : fichier considéré comme binaire.
    svn:mime-type = application/octet-stream
  • binaries/data/mods/public/gui/session/input.js

    Modification de propriétés sur binaries/data/mods/public/art/textures/ui/session/icons/remove-guard.png
    ___________________________________________________________________
    Added: svn:mime-type
    ## -0,0 +1 ##
    +application/octet-stream
    \ No newline at end of property
     
    1515const ACTION_NONE = 0;
    1616const ACTION_GARRISON = 1;
    1717const ACTION_REPAIR = 2;
     18const ACTION_GUARD = 3;
    1819var preSelectedAction = ACTION_NONE;
    1920
    2021const INPUT_NORMAL = 0;
     
    426427            if (entState.attack && targetState.hitpoints && (enemyOwned || neutralOwned))
    427428                return {"possible": Engine.GuiInterfaceCall("CanAttack", {"entity": entState.id, "target": target})};
    428429            break;
     430        case "guard":
     431            if (targetState.guard && (playerOwned || mutualAllyOwned))
     432                return {"possible": (entState.unitAI && entState.unitAI.canGuard)};
     433            break;
    429434        }
    430435    }
    431436    if (action == "move" || action == "attack-move")
     
    502507            else
    503508                return {"type": "none", "cursor": "action-repair-disabled", "target": undefined};
    504509            break;
     510        case ACTION_GUARD:
     511            if (getActionInfo("guard", target).possible)
     512                return {"type": "guard", "cursor": "action-guard", "target": target};
     513            else
     514                return {"type": "none", "cursor": "action-guard-disabled", "target": undefined};
     515            break;
    505516        }
    506517    }
    507518    else if (Engine.HotkeyIsPressed("session.attack") && getActionInfo("attack", target).possible)
     
    516527    {
    517528            return {"type": "attack-move", "cursor": "action-attack-move"};
    518529    }
     530    else if (Engine.HotkeyIsPressed("session.guard") && getActionInfo("guard", target).possible)
     531    {
     532        return {"type": "guard", "cursor": "action-guard", "target": target};
     533    }
    519534    else
    520535    {
    521536        if ((actionInfo = getActionInfo("setup-trade-route", target)).possible)
     
    10531068        recalculateStatusBarDisplay();
    10541069    }
    10551070
     1071    if (ev.hotkey == "session.highlightguarding" && g_ShowGuarding != (ev.type == "hotkeydown"))
     1072    {
     1073        g_ShowGuarding = (ev.type == "hotkeydown");
     1074        updateHighlightSelectionGuards();
     1075    }
     1076
     1077    if (ev.hotkey == "session.highlightguarded" && g_ShowGuarded != (ev.type == "hotkeydown"))
     1078    {
     1079        g_ShowGuarded = (ev.type == "hotkeydown");
     1080        updateHighlightSelectionGuards();
     1081    }
     1082
    10561083    // State-machine processing:
    10571084
    10581085    switch (inputState)
     
    13941421        Engine.GuiInterfaceCall("PlaySound", { "name": "order_garrison", "entity": selection[0] });
    13951422        return true;
    13961423
     1424    case "guard":
     1425        Engine.PostNetworkCommand({"type": "guard", "entities": selection, "target": action.target, "queued": queued});
     1426        Engine.GuiInterfaceCall("PlaySound", { "name": "order_guard", "entity": selection[0] });
     1427        return true;
     1428
    13971429    case "set-rallypoint":
    13981430        var pos = undefined;
    13991431        // if there is a position set in the action then use this so that when setting a
     
    18201852                inputState = INPUT_PRESELECTEDACTION;
    18211853                preSelectedAction = ACTION_REPAIR;
    18221854                break;
     1855            case "add-guard":
     1856                inputState = INPUT_PRESELECTEDACTION;
     1857                preSelectedAction = ACTION_GUARD;
     1858                break;
     1859            case "remove-guard":
     1860                removeGuard();
     1861                break;
    18231862            case "unload-all":
    18241863                unloadAll();
    18251864                break;
     
    21062145   
    21072146}
    21082147
     2148function removeGuard()
     2149{
     2150    // Filter out all entities that are currently guarding/escorting.
     2151    var entities = g_Selection.toList().filter(function(e) {
     2152        var state = GetEntityState(e);
     2153        return (state && state.unitAI && state.unitAI.isGuarding);
     2154    });
     2155   
     2156    Engine.PostNetworkCommand({"type": "remove-guard", "entities": entities});
     2157}
     2158
    21092159function clearSelection()
    21102160{
    21112161    if(inputState==INPUT_BUILDING_PLACEMENT || inputState==INPUT_BUILDING_WALL_PATHING)
  • binaries/data/mods/public/gui/session/session.js

     
    4646const DEFAULT_POPULATION_COLOR = "white";
    4747const POPULATION_ALERT_COLOR = "orange";
    4848
     49// List of additionnal entities to highlight
     50var g_ShowGuarding = false;
     51var g_ShowGuarded = false;
     52var g_AdditionnalHighlight = [];
     53
    4954function GetSimState()
    5055{
    5156    if (!g_SimState)
     
    331336
    332337        // Display rally points for selected buildings
    333338        Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": g_Selection.toList() });
     339
     340        // Update the highlight of guards if needed
     341        if (g_ShowGuarding || g_ShowGuarded)
     342            updateHighlightSelectionGuards();
    334343    }
    335344
    336345    // Run timers
     
    665674    Engine.GuiInterfaceCall("SetStatusBars", { "entities": entities, "enabled": g_ShowAllStatusBars });
    666675}
    667676
     677// Update the additionnal list of entities to be highlighted.
     678function updateHighlightSelectionGuards()
     679{
     680    var guarding = [];  // guarded units will be highlighted with the player color
     681    var guarded  = [];  // guarding units will be highlighted in black
     682    var remove = [];
     683    var selected = g_Selection.toList();
     684
     685    if (g_ShowGuarding)
     686    {
     687        // flag the guarding entities to add in this additionnal highlight
     688        for each (var sel in selected)
     689        {
     690            var state = GetEntityState(sel);
     691            if (!state.guard || !state.guard.entities.length)
     692                continue;
     693            for each (var ent in state.guard.entities)
     694                if (selected.indexOf(ent) == -1 && guarding.indexOf(ent) == -1)
     695                    guarding.push(ent);
     696        }
     697    }
     698
     699    if (g_ShowGuarded)
     700    {
     701        // flag the guarded entities to add in this additionnal highlight
     702        for each (var sel in selected)
     703        {
     704            var state = GetEntityState(sel);
     705            if (!state.unitAI || !state.unitAI.isGuarding)
     706                continue;
     707            var ent = state.unitAI.isGuarding;
     708            if (selected.indexOf(ent) == -1 && guarding.indexOf(ent) == -1 && guarded.indexOf(ent) == -1)
     709                guarded.push(ent);
     710        }
     711    }
     712
     713    // flag the entities to remove (from the previously added) from this additionnal highlight
     714    for each (var ent in g_AdditionnalHighlight)
     715        if (selected.indexOf(ent) == -1 && guarding.indexOf(ent) == -1 && guarded.indexOf(ent) == -1 && remove.indexOf(ent) == -1)
     716            remove.push(ent);
     717
     718    Engine.GuiInterfaceCall("SetAdditionnalHighlight", { "entitiesBlack": guarding, "entitiesColor": guarded, "entitiesRemove": remove });
     719    g_AdditionnalHighlight = guarding.concat(guarded);
     720}
     721
    668722// Temporarily adding this here
    669723const AMBIENT_TEMPERATE = "temperate";
    670724var currentAmbient;
  • binaries/data/mods/public/gui/session/utility_functions.js

     
    303303        });
    304304    }
    305305
     306    if (entState.unitAI && !entState.unitAI.isGuarding && entState.unitAI.canGuard)
     307    {
     308        commands.push({
     309            "name": "add-guard",
     310            "tooltip": "Guard",
     311            "icon": "add-guard.png"
     312        });
     313    }
     314
     315    if (entState.unitAI && entState.unitAI.isGuarding)
     316    {
     317        commands.push({
     318            "name": "remove-guard",
     319            "tooltip": "Remove guard",
     320            "icon": "remove-guard.png"
     321        });
     322    }
     323
    306324    return commands;
    307325}
    308326
  • binaries/data/mods/public/simulation/components/Guard.js

     
     1function Guard() {}
     2
     3Guard.prototype.Schema =
     4    "<empty/>";
     5
     6Guard.prototype.Init = function()
     7{
     8    this.entities = [];
     9};
     10
     11Guard.prototype.GetRange = function(entity)
     12{
     13    var range = 8;
     14    var cmpFootprint = Engine.QueryInterface(entity, IID_Footprint);
     15    if (cmpFootprint)
     16    {
     17        var shape = cmpFootprint.GetShape();
     18        if (shape.type == "square")
     19            range += Math.sqrt(shape.depth*shape.depth + shape.width*shape.width)*2/3;
     20        else if (shape.type == "circle")
     21            range += shape.radius*3/2;
     22    }
     23    return range;
     24};
     25
     26Guard.prototype.GetEntities = function()
     27{
     28    return this.entities.slice();
     29};
     30
     31Guard.prototype.SetEntities = function(entities)
     32{
     33    this.entities = entities;
     34};
     35
     36Guard.prototype.AddGuard = function(ent)
     37{
     38    if (this.entities.indexOf(ent) != -1)
     39        return;
     40    this.entities.push(ent);
     41};
     42
     43Guard.prototype.RemoveGuard = function(ent)
     44{
     45    var index = this.entities.indexOf(ent);
     46    if (index != -1)
     47        this.entities.splice(index, 1);
     48};
     49
     50Guard.prototype.OnAttacked = function(msg)
     51{
     52    for each (var ent in this.entities)
     53        Engine.PostMessage(ent, MT_GuardedAttacked, { "guarded": this.entity, "data": msg });
     54};
     55
     56/**
     57 * Update list of guarding/escorting entities if one gets renamed (e.g. by promotion)
     58 */
     59Guard.prototype.OnGlobalEntityRenamed = function(msg)
     60{
     61    var entityIndex = this.entities.indexOf(msg.entity);
     62    if (entityIndex != -1)
     63        this.entities[entityIndex] = msg.newentity;
     64};
     65
     66/**
     67 * If an entity is captured, or about to be killed (so its owner
     68 * changes to '-1'), update the guards list
     69 */
     70Guard.prototype.OnGlobalOwnershipChanged = function(msg)
     71{
     72    // the ownership change may be on the guarded
     73    if (this.entity == msg.entity)
     74    {
     75        var entities = this.GetEntities();
     76        for each (var entity in entities)
     77        {
     78            if (msg.to == -1 || !IsOwnedByMutualAllyOfEntity(this.entity, entity))
     79            {
     80                var cmpUnitAI = Engine.QueryInterface(entity, IID_UnitAI);
     81                if (cmpUnitAI && cmpUnitAI.IsGuardOf() && cmpUnitAI.IsGuardOf() == this.entity)
     82                    cmpUnitAI.RemoveGuard();
     83                else
     84                    this.RemoveGuard(entity);
     85            }
     86        }
     87        this.entities = entities;
     88        return;
     89    }
     90
     91    // or on some of its guards
     92    if (this.entities.indexOf(msg.entity) != -1)
     93    {
     94        if (msg.to == -1)
     95            this.RemoveGuard(msg.entity);
     96        else if(!IsOwnedByMutualAllyOfEntity(this.entity, msg.entity))
     97        {
     98            var cmpUnitAI = Engine.QueryInterface(msg.entity, IID_UnitAI);
     99            if (cmpUnitAI)
     100                cmpUnitAI.RemoveGuard();
     101            else
     102                this.RemoveGuard(msg.entity);
     103        }
     104    }
     105};
     106
     107Engine.RegisterComponentType(IID_Guard, "Guard", Guard);
  • binaries/data/mods/public/simulation/components/GuiInterface.js

     
    350350            "state": cmpUnitAI.GetCurrentState(),
    351351            "orders": cmpUnitAI.GetOrders(),
    352352            "hasWorkOrders": cmpUnitAI.HasWorkOrders(),
     353            "canGuard": cmpUnitAI.CanGuard(),
     354            "isGuarding": cmpUnitAI.IsGuardOf(),
    353355        };
    354356        // Add some information needed for ungarrisoning
    355357        if (cmpUnitAI.isGarrisoned && ret.player !== undefined)
    356358            ret.template = "p" + ret.player + "&" + ret.template;
    357359    }
    358360
     361    var cmpGuard = Engine.QueryInterface(ent, IID_Guard);
     362    if (cmpGuard)
     363    {
     364        ret.guard = {
     365            "entities": cmpGuard.GetEntities(),
     366        };
     367    }
     368   
    359369    var cmpGate = Engine.QueryInterface(ent, IID_Gate);
    360370    if (cmpGate)
    361371    {
     
    783793    }
    784794};
    785795
     796GuiInterface.prototype.SetAdditionnalHighlight = function(player, cmd)
     797{
     798    var cmpPlayerMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
     799    var playerColours = {}; // cache of owner -> colour map
     800
     801    // remove the previous highlight when no more needed
     802    for each (var ent in cmd.entitiesRemove)
     803    {
     804        var cmpSelectable = Engine.QueryInterface(ent, IID_Selectable);
     805        if (!cmpSelectable)
     806            continue;
     807
     808        // Find the entity's owner's colour:
     809        var owner = -1;
     810        var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
     811        if (cmpOwnership)
     812            owner = cmpOwnership.GetOwner();
     813
     814        var colour = playerColours[owner];
     815        if (!colour)
     816        {
     817            var cmpPlayer = Engine.QueryInterface(cmpPlayerMan.GetPlayerByID(owner), IID_Player);
     818            colour = cmpPlayer.GetColour();
     819            playerColours[owner] = colour;
     820        }
     821
     822        cmpSelectable.SetSelectionHighlight({"r":colour.r, "g":colour.g, "b":colour.b, "a":0}, false);
     823    }
     824
     825    // highlight the required units with player color
     826    for each (var ent in cmd.entitiesColor)
     827    {
     828        var cmpSelectable = Engine.QueryInterface(ent, IID_Selectable);
     829        if (!cmpSelectable)
     830            continue;
     831
     832        // Find the entity's owner's colour:
     833        var owner = -1;
     834        var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
     835        if (cmpOwnership)
     836            owner = cmpOwnership.GetOwner();
     837
     838        // Only units from allies should be highligted
     839        var cmpPlayer = Engine.QueryInterface(cmpPlayerMan.GetPlayerByID(owner), IID_Player);
     840        if (!cmpPlayer || !cmpPlayer.IsMutualAlly(player))
     841            continue;
     842
     843        var colour = playerColours[owner];
     844        if (!colour)
     845        {
     846            colour = cmpPlayer.GetColour();
     847            playerColours[owner] = colour;
     848        }
     849
     850        cmpSelectable.SetSelectionHighlight({"r":colour.r, "g":colour.g, "b":colour.b, "a":0.70}, true);
     851    }
     852
     853    // highlight the required units in black
     854    for each (var ent in cmd.entitiesBlack)
     855    {
     856        var cmpSelectable = Engine.QueryInterface(ent, IID_Selectable);
     857        if (!cmpSelectable)
     858            continue;
     859
     860        // Find the entity's owner's colour:
     861        var owner = -1;
     862        var cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
     863        if (cmpOwnership)
     864            owner = cmpOwnership.GetOwner();
     865
     866        // Only units from allies should be highligted
     867        var cmpPlayer = Engine.QueryInterface(cmpPlayerMan.GetPlayerByID(owner), IID_Player);
     868        if (!cmpPlayer || !cmpPlayer.IsMutualAlly(player))
     869            continue;
     870
     871        cmpSelectable.SetSelectionHighlight({"r": 0, "g": 0, "b": 0, "a":0.70}, true);
     872    }
     873};
     874
    786875GuiInterface.prototype.SetStatusBars = function(player, cmd)
    787876{
    788877    for each (var ent in cmd.entities)
     
    18011890    "IsStanceSelected": 1,
    18021891
    18031892    "SetSelectionHighlight": 1,
     1893    "SetAdditionnalHighlight": 1,
    18041894    "SetStatusBars": 1,
    18051895    "GetPlayerEntities": 1,
    18061896    "DisplayRallyPoint": 1,
     
    18121902    "GetTradingRouteGain": 1,
    18131903    "GetTradingDetails": 1,
    18141904    "CanAttack": 1,
     1905    "CanGuard": 1,
    18151906    "GetBatchTime": 1,
    18161907
    18171908    "SetPathfinderDebugOverlay": 1,
  • binaries/data/mods/public/simulation/components/Pack.js

     
    162162            if (cmpUnitAI.GetStanceName())
    163163                cmpNewUnitAI.SwitchToStance(cmpUnitAI.GetStanceName());
    164164            cmpNewUnitAI.AddOrders(cmpUnitAI.GetOrders());
     165            cmpNewUnitAI.SetGuardOf(cmpUnitAI.IsGuardOf());
    165166        }
    166167
     168        // Maintain the list of guards
     169        var cmpGuard = Engine.QueryInterface(this.entity, IID_Guard);
     170        var cmpNewGuard = Engine.QueryInterface(newEntity, IID_Guard);
     171        if (cmpGuard && cmpNewGuard)
     172            cmpNewGuard.SetEntities(cmpGuard.GetEntities());
     173
    167174        Engine.BroadcastMessage(MT_EntityRenamed, { entity: this.entity, newentity: newEntity });
    168175
    169176        // Play notification sound
  • binaries/data/mods/public/simulation/components/Promotion.js

     
    8080    cmpPromotedUnitAI.AddOrders(orders);
    8181    var workOrders = cmpCurrentUnitAI.GetWorkOrders();
    8282    cmpPromotedUnitAI.SetWorkOrders(workOrders);
     83    cmpPromotedUnitAI.SetGuardOf(cmpCurrentUnitAI.IsGuardOf());
    8384
     85    var cmpCurrentUnitGuard = Engine.QueryInterface(this.entity, IID_Guard);
     86    var cmpPromotedUnitGuard = Engine.QueryInterface(promotedUnitEntity, IID_Guard);
     87    if (cmpCurrentUnitGuard && cmpPromotedUnitGuard)
     88        cmpPromotedUnitGuard.SetEntities(cmpCurrentUnitGuard.GetEntities());
     89
    8490    Engine.BroadcastMessage(MT_EntityRenamed, { entity: this.entity, newentity: promotedUnitEntity });
    8591
    8692    // Destroy current entity
  • binaries/data/mods/public/simulation/components/UnitAI.js

     
    1818    "<element name='FleeDistance'>" +
    1919        "<ref name='positiveDecimal'/>" +
    2020    "</element>" +
     21    "<element name='CanGuard'>" +
     22        "<data type='boolean'/>" +
     23    "</element>" +
    2124    "<optional>" +
    2225        "<interleave>" +
    2326            "<element name='NaturalBehaviour' a:help='Behaviour of the unit in the absence of player commands (intended for animals)'>" +
     
    164167        // ignore
    165168    },
    166169
     170    "GuardedAttacked": function(msg) {
     171        // ignore
     172    },
     173
    167174    // Formation handlers:
    168175
    169176    "FormationLeave": function(msg) {
     
    354361        }
    355362    },
    356363
     364    "Order.Guard": function(msg) {
     365        if (!this.AddGuard(this.order.data.target))
     366        {
     367            this.FinishOrder();
     368            return;
     369        }
     370
     371        if (this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange))
     372            this.SetNextState("INDIVIDUAL.GUARD.ESCORTING");
     373        else
     374            this.SetNextState("INDIVIDUAL.GUARD.GUARDING");
     375    },
     376
    357377    "Order.Flee": function(msg) {
    358         // We use the distance between the enities to account for ranged attacks
     378        // We use the distance between the entities to account for ranged attacks
    359379        var distance = DistanceBetweenEntities(this.entity, this.order.data.target) + (+this.template.FleeDistance);
    360380        var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
    361381        if (cmpUnitMotion.MoveToTargetRange(this.order.data.target, distance, -1))
     
    740760                this.FinishOrder();
    741761        },
    742762
     763        "Order.Guard": function(msg) {
     764            var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
     765            cmpFormation.CallMemberFunction("Guard", [msg.data.target, false]);
     766            cmpFormation.Disband();
     767        },
     768
    743769        "Order.Stop": function(msg) {
    744770            var cmpFormation = Engine.QueryInterface(this.entity, IID_Formation);
    745771            cmpFormation.CallMemberFunction("Stop", [false]);
     
    12591285            }
    12601286        },
    12611287
     1288        "GuardedAttacked": function(msg) {
     1289            // do nothing if we have a forced order in queue before the guard order
     1290            for (var i = 0; i < this.orderQueue.length; ++i)
     1291            {
     1292                if (this.orderQueue[i].type == "Guard")
     1293                    break;
     1294                if (this.orderQueue[i].data && this.orderQueue[i].data.force)
     1295                    return;
     1296            }
     1297            // if we already are targeting another unit still alive, finish with it first
     1298            if (this.order && this.order.type == "WalkAndFight")
     1299                if (this.order.data.target != msg.data.attacker && this.TargetIsAlive(msg.data.attacker))
     1300                    return;
     1301
     1302            var cmpIdentity = Engine.QueryInterface(this.entity, IID_Identity);
     1303            var cmpHealth = Engine.QueryInterface(this.isGuardOf, IID_Health);
     1304            if (cmpIdentity && cmpIdentity.HasClass("Support") &&
     1305                cmpHealth && cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints())
     1306            {
     1307                if (this.CanHeal(this.isGuardOf))
     1308                    this.PushOrderFront("Heal", { "target": this.isGuardOf, "force": false });
     1309                else if (this.CanRepair(this.isGuardOf) && cmpHealth.IsRepairable())
     1310                    this.PushOrderFront("Repair", { "target": this.isGuardOf, "autocontinue": false, "force": false });
     1311                return;
     1312            }
     1313
     1314            // target the unit
     1315            var cmpPosition = Engine.QueryInterface(msg.data.attacker, IID_Position);
     1316            if (!cmpPosition || !cmpPosition.IsInWorld())
     1317                return;
     1318            var pos = cmpPosition.GetPosition();
     1319            this.PushOrderFront("WalkAndFight", { "x": pos.x, "z": pos.z, "target": msg.data.attacker, "force": false });
     1320            // if we already had a WalkAndFight, keep only the most recent one in case the target has moved
     1321            if (this.orderQueue[1] && this.orderQueue[1].type == "WalkAndFight")
     1322                this.orderQueue.splice(1, 1);
     1323        },
     1324
    12621325        "IDLE": {
    12631326            "enter": function() {
    12641327                // Switch back to idle animation to guarantee we won't
    12651328                // get stuck with an incorrect animation
    12661329                this.SelectAnimation("idle");
    12671330
     1331                // If the unit is guarding/escorting, go back to its duty
     1332                if (this.isGuardOf)
     1333                {
     1334                    this.Guard(this.isGuardOf, false);
     1335                    return true;
     1336                }
     1337
    12681338                // The GUI and AI want to know when a unit is idle, but we don't
    12691339                // want to send frequent spurious messages if the unit's only
    12701340                // idle for an instant and will quickly go off and do something else.
     
    13661436            },
    13671437        },
    13681438
     1439        "GUARD": {
     1440            "enter": function () {
     1441            },
     1442
     1443            "RemoveGuard": function() {
     1444                this.FinishOrder();
     1445            },
     1446
     1447            "leave": function () {
     1448            },
     1449
     1450            "ESCORTING": {
     1451                "enter": function () {
     1452                    // Show weapons rather than carried resources.
     1453                    this.SetGathererAnimationOverride(true);
     1454
     1455                    this.StartTimer(0, 1000);
     1456                    this.SelectAnimation("move");
     1457                    this.SetHeldPositionOnEntity(this.isGuardOf);
     1458                    return false;
     1459                },
     1460
     1461                "Timer": function(msg) {
     1462                    // Check the target is alive
     1463                    if (!this.TargetIsAlive(this.isGuardOf))
     1464                    {
     1465                        this.StopMoving();
     1466                        this.FinishOrder();
     1467                        return;
     1468                    }
     1469                    this.SetHeldPositionOnEntity(this.isGuardOf);
     1470                },
     1471
     1472                "leave": function(msg) {
     1473                    this.SetMoveSpeed(this.GetWalkSpeed());
     1474                    this.StopTimer();
     1475                },
     1476
     1477                "MoveStarted": function(msg) {
     1478                    // Adapt the speed to the one of the target if needed
     1479                    var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
     1480                    if (cmpUnitMotion.IsInTargetRange(this.isGuardOf, 0, 3*this.guardRange))
     1481                    {
     1482                        var cmpUnitAI = Engine.QueryInterface(this.isGuardOf, IID_UnitAI);
     1483                        if (cmpUnitAI)
     1484                        {
     1485                            var speed = cmpUnitAI.GetWalkSpeed();
     1486                            if (speed < this.GetWalkSpeed())
     1487                                this.SetMoveSpeed(speed);
     1488                        }
     1489                    }
     1490                },
     1491
     1492                "MoveCompleted": function() {
     1493                    this.SetMoveSpeed(this.GetWalkSpeed());
     1494                    if (!this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange))
     1495                        this.SetNextState("GUARDING");
     1496                },
     1497            },
     1498
     1499            "GUARDING": {
     1500                "enter": function () {
     1501                    this.StartTimer(1000, 1000);
     1502                    this.SetHeldPositionOnEntity(this.entity);
     1503                    this.SelectAnimation("idle");
     1504                    return false;
     1505                },
     1506
     1507                "LosRangeUpdate": function(msg) {
     1508                    // Start attacking one of the newly-seen enemy (if any)
     1509                    if (this.GetStance().targetVisibleEnemies)
     1510                        this.AttackEntitiesByPreference(msg.data.added);
     1511                },
     1512
     1513                "LosGaiaRangeUpdate": function(msg) {
     1514                    // Start attacking one of the newly-seen enemy (if any)
     1515                    if (this.GetStance().targetVisibleEnemies)
     1516                        this.AttackGaiaEntitiesByPreference(msg.data.added);
     1517                },
     1518
     1519                "Timer": function(msg) {
     1520                    // Check the target is alive
     1521                    if (!this.TargetIsAlive(this.isGuardOf))
     1522                    {
     1523                        this.FinishOrder();
     1524                        return;
     1525                    }
     1526                    // then check is the target has moved
     1527                    if (this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange))
     1528                        this.SetNextState("ESCORTING");
     1529                    else
     1530                    {
     1531                        // if nothing better to do, check if the guarded needs to be healed or repaired
     1532                        var cmpHealth = Engine.QueryInterface(this.isGuardOf, IID_Health);
     1533                        if (cmpHealth && (cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints()))
     1534                        {
     1535                            if (this.CanHeal(this.isGuardOf))
     1536                                this.PushOrderFront("Heal", { "target": this.isGuardOf, "force": false });
     1537                            else if (this.CanRepair(this.isGuardOf) && cmpHealth.IsRepairable())
     1538                                this.PushOrderFront("Repair", { "target": this.isGuardOf, "autocontinue": false, "force": false });
     1539                        }
     1540                    }
     1541                },
     1542
     1543                "leave": function(msg) {
     1544                    this.StopTimer();
     1545                },
     1546            },
     1547        },
     1548
    13691549        "FLEEING": {
    13701550            "enter": function() {
    13711551                this.PlaySound("panic");
     
    22702450                    return false;
    22712451                },
    22722452
     2453                "LosRangeUpdate": function(msg) {
     2454                },
     2455
     2456                "LosGaiaRangeUpdate": function(msg) {
     2457                },
     2458
    22732459                "leave": function() {
    22742460                    var cmpFoundation = Engine.QueryInterface(this.repairTarget, IID_Foundation);
    22752461                    if (cmpFoundation)
     
    27152901    // Queue of remembered works
    27162902    this.workOrders = [];
    27172903
     2904    this.isGuardOf = undefined;
     2905
    27182906    // For preventing increased action rate due to Stop orders or target death.
    27192907    this.lastAttacked = undefined;
    27202908    this.lastHealed = undefined;
     
    30393227        error("FinishOrder called for entity " + this.entity + " (" + template + ") when order queue is empty\n" + stack);
    30403228    }
    30413229
    3042     // Remove the order from the queue
    30433230    this.orderQueue.shift();
    30443231    this.order = this.orderQueue[0];
    30453232
     
    33913578    for each (var order in this.orderQueue)
    33923579        if (order.data && order.data.target && order.data.target == msg.entity)
    33933580            order.data.target = msg.newentity;
     3581
     3582    if (this.isGuardOf && this.isGuardOf == msg.entity)
     3583        this.isGuardOf = msg.newentity;
    33943584};
    33953585
    33963586UnitAI.prototype.OnAttacked = function(msg)
     
    33983588    UnitFsm.ProcessMessage(this, {"type": "Attacked", "data": msg});
    33993589};
    34003590
     3591UnitAI.prototype.OnGuardedAttacked = function(msg)
     3592{
     3593    UnitFsm.ProcessMessage(this, {"type": "GuardedAttacked", "data": msg.data});
     3594};
     3595
    34013596UnitAI.prototype.OnHealthChanged = function(msg)
    34023597{
    34033598    UnitFsm.ProcessMessage(this, {"type": "HealthChanged", "from": msg.from, "to": msg.to});
     
    40434238    if (force)
    40444239        return false;
    40454240
     4241    // If we are guarding/escorting, don't abandon as long as the guarded is in target range of the attacker
     4242    if (this.isGuardOf)
     4243    {
     4244        var cmpUnitAI =  Engine.QueryInterface(target, IID_UnitAI);
     4245        var cmpAttack = Engine.QueryInterface(target, IID_Attack);
     4246        if (cmpUnitAI && cmpAttack)
     4247        {
     4248            for each (var type in cmpAttack.GetAttackTypes())
     4249                if (cmpUnitAI.CheckTargetAttackRange(this.isGuardOf, IID_Attack, type))
     4250                    return false;
     4251        }
     4252    }
     4253
    40464254    // Stop if we're in hold-ground mode and it's too far from the holding point
    40474255    if (this.GetStance().respondHoldGround)
    40484256    {
     
    40774285    if (this.GetStance().respondChase)
    40784286        return true;
    40794287
     4288    // If we are guarding/escorting, chase at least as long as the guarded is in target range of the attacker
     4289    if (this.isGuardOf)
     4290    {
     4291        var cmpUnitAI =  Engine.QueryInterface(target, IID_UnitAI);
     4292        var cmpAttack = Engine.QueryInterface(target, IID_Attack);
     4293        if (cmpUnitAI && cmpAttack)
     4294        {
     4295            for each (var type in cmpAttack.GetAttackTypes())
     4296                if (cmpUnitAI.CheckTargetAttackRange(this.isGuardOf, IID_Attack, type))
     4297                    return true;
     4298        }
     4299    }
     4300
    40804301    if (force)
    40814302        return true;
    40824303
     
    41774398
    41784399        case "WalkToTarget":
    41794400        case "WalkToTargetRange": // This doesn't move to the target (just into range), but a later order will.
     4401        case "Guard":
    41804402        case "Flee":
    41814403        case "LeaveFoundation":
    41824404        case "Attack":
     
    42224444};
    42234445
    42244446/**
     4447 * Adds guard/escort order to the queue, forced by the player.
     4448 */
     4449UnitAI.prototype.Guard = function(target, queued)
     4450{
     4451    if (!this.CanGuard())
     4452    {
     4453        this.WalkToTarget(target, queued);
     4454        return;
     4455    }
     4456
     4457    // if we already had an old guard order, do nothing if the target is the same
     4458    // and the order is running, otherwise remove the previous order
     4459    if (this.isGuardOf)
     4460    {
     4461        if (this.isGuardOf == target && this.order && this.order.type == "Guard")
     4462            return;
     4463        else
     4464            this.RemoveGuard();
     4465    }
     4466
     4467    this.AddOrder("Guard", { "target": target, "force": false }, queued);
     4468};
     4469
     4470UnitAI.prototype.AddGuard = function(target)
     4471{
     4472    var cmpGuard = Engine.QueryInterface(target, IID_Guard);
     4473    if (!cmpGuard || !this.CanGuard())
     4474        return false;
     4475    this.isGuardOf = target;
     4476    this.guardRange = cmpGuard.GetRange(this.entity);
     4477    cmpGuard.AddGuard(this.entity);
     4478    return true;
     4479};
     4480
     4481UnitAI.prototype.RemoveGuard = function()
     4482{
     4483    if (this.isGuardOf)
     4484    {
     4485        var cmpGuard = Engine.QueryInterface(this.isGuardOf, IID_Guard);
     4486        if (cmpGuard)
     4487            cmpGuard.RemoveGuard(this.entity);
     4488        this.guardRange = undefined;
     4489        this.isGuardOf = undefined;
     4490    }
     4491
     4492    if (!this.order)
     4493        return;
     4494
     4495    if (this.order.type == "Guard")
     4496        UnitFsm.ProcessMessage(this, {"type": "RemoveGuard"});
     4497    else
     4498        for (var i = 1; i < this.orderQueue.length; ++i)
     4499            if (this.orderQueue[i].type == "Guard")
     4500                this.orderQueue.splice(i, 1);
     4501};
     4502
     4503UnitAI.prototype.IsGuardOf = function()
     4504{
     4505    return this.isGuardOf;
     4506};
     4507
     4508UnitAI.prototype.SetGuardOf = function(entity)
     4509{
     4510    // entity may be undefined
     4511    this.isGuardOf = entity;
     4512};
     4513
     4514UnitAI.prototype.CanGuard = function()
     4515{
     4516    // Formation controllers should always respond to commands
     4517    // (then the individual units can make up their own minds)
     4518    if (this.IsFormationController())
     4519        return true;
     4520
     4521    // Do not let a unit already guarded to guard. This would work in principle,
     4522    // but would clutter the gui with too much buttons to take all cases into account
     4523    var cmpGuard = Engine.QueryInterface(this.entity, IID_Guard);
     4524    if (cmpGuard && cmpGuard.GetEntities().length)
     4525        return false;
     4526
     4527    return (this.template.CanGuard == "true");
     4528};
     4529
     4530/**
    42254531 * Adds walk order to queue, forced by the player.
    42264532 */
    42274533UnitAI.prototype.Walk = function(x, z, queued)
     
    46824988    this.heldPosition = {"x": x, "z": z};
    46834989};
    46844990
     4991UnitAI.prototype.SetHeldPositionOnEntity = function(entity)
     4992{
     4993    var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
     4994    if (!cmpPosition || !cmpPosition.IsInWorld())
     4995        return;
     4996    var pos = cmpPosition.GetPosition();
     4997    this.SetHeldPosition(pos.x, pos.z);
     4998};
     4999
    46855000UnitAI.prototype.GetHeldPosition = function(pos)
    46865001{
    46875002    return this.heldPosition;
  • binaries/data/mods/public/simulation/components/interfaces/Guard.js

     
     1Engine.RegisterInterface("Guard");
     2
     3// Message of the form { "guarded": entity, "data": msg },
     4// sent whenever a guarded/escorted unit is attacked
     5Engine.RegisterMessageType("GuardedAttacked");
  • binaries/data/mods/public/simulation/helpers/Commands.js

     
    180180        }
    181181        break;
    182182
     183    case "remove-guard":
     184        for each (var ent in entities)
     185        {
     186            var cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
     187            if(cmpUnitAI)
     188                cmpUnitAI.RemoveGuard();
     189        }
     190        break;
     191
    183192    case "train":
    184193        // Check entity limits
    185194        var cmpTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
     
    328337        }
    329338        break;
    330339
     340    case "guard":
     341        // Verify that the target can be controlled by the player or is mutualAlly
     342        if (CanControlUnitOrIsAlly(cmd.target, player, controlAllUnits))
     343        {
     344            GetFormationUnitAIs(entities, player).forEach(function(cmpUnitAI) {
     345                cmpUnitAI.Guard(cmd.target, cmd.queued);
     346            });
     347        }
     348        else if (g_DebugCommands)
     349        {
     350            warn("Invalid command: guard/escort target cannot be controlled by player "+player+": "+uneval(cmd));
     351        }
     352        break;
     353
    331354    case "stop":
    332355        GetFormationUnitAIs(entities, player).forEach(function(cmpUnitAI) {
    333356            cmpUnitAI.Stop(cmd.queued);
  • binaries/data/mods/public/simulation/templates/special/formation.xml

     
    1717    <DefaultStance>aggressive</DefaultStance>
    1818    <FleeDistance>12.0</FleeDistance>
    1919    <FormationController>true</FormationController>
     20    <CanGuard>true</CanGuard>
    2021  </UnitAI>
    2122  <UnitMotion>
    2223    <FormationController>true</FormationController>
  • binaries/data/mods/public/simulation/templates/template_structure.xml

     
    1515    <GarrisonArrowMultiplier>0</GarrisonArrowMultiplier>
    1616    <GarrisonArrowClasses>Ranged Infantry</GarrisonArrowClasses>
    1717  </BuildingAI>
     18  <Guard/>
    1819  <BuildRestrictions>
    1920    <PlacementType>land</PlacementType>
    2021    <Territory>own</Territory>
  • binaries/data/mods/public/simulation/templates/template_unit.xml

     
    102102    <DefaultStance>aggressive</DefaultStance>
    103103    <FleeDistance>12.0</FleeDistance>
    104104    <FormationController>false</FormationController>
     105    <CanGuard>true</CanGuard>
    105106  </UnitAI>
     107  <Guard/>
    106108  <UnitMotion>
    107109    <FormationController>false</FormationController>
    108110    <WalkSpeed>7.5</WalkSpeed>
  • binaries/data/mods/public/simulation/templates/template_unit_fauna.xml

     
    2222  </Minimap>
    2323  <ResourceGatherer disable=""/>
    2424  <UnitAI>
     25    <CanGuard>false</CanGuard>
    2526    <RoamDistance>8.0</RoamDistance>
    2627    <FleeDistance>24.0</FleeDistance>
    2728    <RoamTimeMin>2000</RoamTimeMin>
     
    2930    <FeedTimeMin>15000</FeedTimeMin>
    3031    <FeedTimeMax>60000</FeedTimeMax>
    3132  </UnitAI>
     33  <Guard disable=""/>
    3234  <UnitMotion>
    3335    <WalkSpeed>5.0</WalkSpeed>
    3436    <Run>
  • binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_fishing.xml

     
    5050      <order_attack>actor/ship/boat_move.xml</order_attack>
    5151    </SoundGroups>
    5252  </Sound>
     53  <UnitAI>
     54    <CanGuard>false</CanGuard>
     55  </UnitAI>
    5356  <UnitMotion>
    5457    <WalkSpeed>8.5</WalkSpeed>
    5558  </UnitMotion>
  • binaries/data/mods/public/simulation/templates/template_unit_mechanical_ship_merchant.xml

     
    4747    <MaxDistance>10.0</MaxDistance>
    4848    <GainMultiplier>1.0</GainMultiplier>
    4949  </Trader>
     50  <UnitAI>
     51    <CanGuard>false</CanGuard>
     52  </UnitAI>
    5053  <UnitMotion>
    5154    <WalkSpeed>10.5</WalkSpeed>
    5255  </UnitMotion>
  • binaries/data/mods/public/simulation/templates/template_unit_mechanical_siege.xml

     
    1313    <GenericName>Siege</GenericName>
    1414    <RequiredTechnology>phase_city</RequiredTechnology>
    1515  </Identity>
     16  <UnitAI>
     17    <CanGuard>false</CanGuard>
     18  </UnitAI>
    1619  <Position>
    1720    <Anchor>pitch-roll</Anchor>
    1821  </Position>
  • binaries/data/mods/public/simulation/templates/template_unit_support_trader.xml

     
    4040    <MaxDistance>2.0</MaxDistance>
    4141    <GainMultiplier>1.0</GainMultiplier>
    4242  </Trader>
     43  <UnitAI>
     44    <CanGuard>false</CanGuard>
     45  </UnitAI>
    4346  <UnitMotion>
    4447    <WalkSpeed>8.0</WalkSpeed>
    4548  </UnitMotion>