Ticket #2034: escort-v2.2.diff

File escort-v2.2.diff, 38.4 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/cursors/action-remove-guard.png

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

    Modification de propriétés sur binaries/data/mods/public/art/textures/cursors/action-remove-guard.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;
     
    200201        }
    201202        else if (action == "move" || action == "attack-move")
    202203            return {"possible": true};
     204        else if (action == "remove-guard")
     205            return {"possible": true};
    203206        else
    204207            return {"possible": false};
    205208    }
     
    426429            if (entState.attack && targetState.hitpoints && (enemyOwned || neutralOwned))
    427430                return {"possible": Engine.GuiInterfaceCall("CanAttack", {"entity": entState.id, "target": target})};
    428431            break;
     432        case "guard":
     433            if (targetState.guard && (playerOwned || mutualAllyOwned))
     434                return {"possible": (entState.unitAI && entState.unitAI.canGuard && !(targetState.unitAI && targetState.unitAI.isGuarding))};
     435            break;
    429436        }
    430437    }
    431438    if (action == "move" || action == "attack-move")
     
    502509            else
    503510                return {"type": "none", "cursor": "action-repair-disabled", "target": undefined};
    504511            break;
     512        case ACTION_GUARD:
     513            if (getActionInfo("guard", target).possible)
     514                return {"type": "guard", "cursor": "action-guard", "target": target};
     515            else
     516                return {"type": "none", "cursor": "action-guard-disabled", "target": undefined};
     517            break;
    505518        }
    506519    }
    507520    else if (Engine.HotkeyIsPressed("session.attack") && getActionInfo("attack", target).possible)
     
    516529    {
    517530            return {"type": "attack-move", "cursor": "action-attack-move"};
    518531    }
     532    else if (Engine.HotkeyIsPressed("session.guard") && getActionInfo("guard", target).possible)
     533    {
     534        return {"type": "guard", "cursor": "action-guard", "target": target};
     535    }
     536    else if (Engine.HotkeyIsPressed("session.guard") && getActionInfo("remove-guard", target).possible)
     537    {
     538        var isGuarding = selection.some(function(ent) {
     539            var entState = GetEntityState(ent);
     540            return entState && entState.unitAI && entState.unitAI.isGuarding;
     541        });
     542        if (isGuarding)
     543            return {"type": "remove-guard", "cursor": "action-remove-guard"};
     544    }
    519545    else
    520546    {
    521547        if ((actionInfo = getActionInfo("setup-trade-route", target)).possible)
     
    10531079        recalculateStatusBarDisplay();
    10541080    }
    10551081
     1082    if (ev.hotkey == "session.highlightguarding")
     1083    {
     1084        g_ShowGuarding = (ev.type == "hotkeydown");
     1085        updateAdditionnalHighlight();
     1086    }
     1087
     1088    if (ev.hotkey == "session.highlightguarded")
     1089    {
     1090        g_ShowGuarded = (ev.type == "hotkeydown");
     1091        updateAdditionnalHighlight();
     1092    }
     1093
    10561094    // State-machine processing:
    10571095
    10581096    switch (inputState)
     
    11141152    case INPUT_PRESELECTEDACTION:
    11151153        switch (ev.type)
    11161154        {
     1155        case "mousemotion":
     1156            // Highlight the first hovered entity (if any)
     1157            var ents = Engine.PickEntitiesAtPoint(ev.x, ev.y);
     1158            if (ents.length)
     1159                g_Selection.setHighlightList([ents[0]]);
     1160            else
     1161                g_Selection.setHighlightList([]);
     1162
     1163            return false;
     1164
    11171165        case "mousebuttondown":
    11181166            if (ev.button == SDL_BUTTON_LEFT && preSelectedAction != ACTION_NONE)
    11191167            {
     
    13941442        Engine.GuiInterfaceCall("PlaySound", { "name": "order_garrison", "entity": selection[0] });
    13951443        return true;
    13961444
     1445    case "guard":
     1446        Engine.PostNetworkCommand({"type": "guard", "entities": selection, "target": action.target, "queued": queued});
     1447        Engine.GuiInterfaceCall("PlaySound", { "name": "order_guard", "entity": selection[0] });
     1448        return true;
     1449
     1450    case "remove-guard":
     1451        Engine.PostNetworkCommand({"type": "remove-guard", "entities": selection, "target": action.target, "queued": queued});
     1452        Engine.GuiInterfaceCall("PlaySound", { "name": "order_guard", "entity": selection[0] });
     1453        return true;
     1454
    13971455    case "set-rallypoint":
    13981456        var pos = undefined;
    13991457        // if there is a position set in the action then use this so that when setting a
     
    18201878                inputState = INPUT_PRESELECTEDACTION;
    18211879                preSelectedAction = ACTION_REPAIR;
    18221880                break;
     1881            case "add-guard":
     1882                inputState = INPUT_PRESELECTEDACTION;
     1883                preSelectedAction = ACTION_GUARD;
     1884                break;
     1885            case "remove-guard":
     1886                removeGuard();
     1887                break;
    18231888            case "unload-all":
    18241889                unloadAll();
    18251890                break;
     
    21062171   
    21072172}
    21082173
     2174function removeGuard()
     2175{
     2176    // Filter out all entities that are currently guarding/escorting.
     2177    var entities = g_Selection.toList().filter(function(e) {
     2178        var state = GetEntityState(e);
     2179        return (state && state.unitAI && state.unitAI.isGuarding);
     2180    });
     2181   
     2182    Engine.PostNetworkCommand({"type": "remove-guard", "entities": entities});
     2183}
     2184
    21092185function clearSelection()
    21102186{
    21112187    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)
     
    443448    if (g_ShowAllStatusBars)
    444449        recalculateStatusBarDisplay();
    445450
     451    if (g_ShowGuarding || g_ShowGuarded)
     452        updateAdditionnalHighlight();
     453
    446454    updateHero();
    447455    updateGroups();
    448456    updateDebug();
     
    665673    Engine.GuiInterfaceCall("SetStatusBars", { "entities": entities, "enabled": g_ShowAllStatusBars });
    666674}
    667675
     676// Update the additionnal list of entities to be highlighted.
     677function updateAdditionnalHighlight()
     678{
     679    var entsAdd = [];    // list of entities units to be highlighted
     680    var entsRemove = [];
     681    var highlighted = g_Selection.toList();
     682    for each (var ent in g_Selection.highlighted)
     683        highlighted.push(ent);
     684
     685    if (g_ShowGuarding)
     686    {
     687        // flag the guarding entities to add in this additionnal highlight
     688        for each (var sel in g_Selection.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 (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1)
     695                    entsAdd.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 g_Selection.selected)
     703        {
     704            var state = GetEntityState(sel);
     705            if (!state.unitAI || !state.unitAI.isGuarding)
     706                continue;
     707            var ent = state.unitAI.isGuarding;
     708            if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1)
     709                entsAdd.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 (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1 && entsRemove.indexOf(ent) == -1)
     716            entsRemove.push(ent);
     717
     718    _setHighlight(entsAdd   , HIGHLIGHTED_ALPHA, true );
     719    _setHighlight(entsRemove, 0                , false);
     720    g_AdditionnalHighlight = entsAdd;
     721}
     722
    668723// Temporarily adding this here
    669724const AMBIENT_TEMPERATE = "temperate";
    670725var currentAmbient;
  • binaries/data/mods/public/gui/session/utility_functions.js

     
    303303        });
    304304    }
    305305
     306    if (entState.unitAI && entState.unitAI.canGuard && !entState.unitAI.isGuarding)
     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    {
  • 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.StopMoving();
     1445                this.FinishOrder();
     1446            },
     1447
     1448            "leave": function () {
     1449            },
     1450
     1451            "ESCORTING": {
     1452                "enter": function () {
     1453                    // Show weapons rather than carried resources.
     1454                    this.SetGathererAnimationOverride(true);
     1455
     1456                    this.StartTimer(0, 1000);
     1457                    this.SelectAnimation("move");
     1458                    this.SetHeldPositionOnEntity(this.isGuardOf);
     1459                    return false;
     1460                },
     1461
     1462                "Timer": function(msg) {
     1463                    // Check the target is alive
     1464                    if (!this.TargetIsAlive(this.isGuardOf))
     1465                    {
     1466                        this.StopMoving();
     1467                        this.FinishOrder();
     1468                        return;
     1469                    }
     1470                    this.SetHeldPositionOnEntity(this.isGuardOf);
     1471                },
     1472
     1473                "leave": function(msg) {
     1474                    this.SetMoveSpeed(this.GetWalkSpeed());
     1475                    this.StopTimer();
     1476                },
     1477
     1478                "MoveStarted": function(msg) {
     1479                    // Adapt the speed to the one of the target if needed
     1480                    var cmpUnitMotion = Engine.QueryInterface(this.entity, IID_UnitMotion);
     1481                    if (cmpUnitMotion.IsInTargetRange(this.isGuardOf, 0, 3*this.guardRange))
     1482                    {
     1483                        var cmpUnitAI = Engine.QueryInterface(this.isGuardOf, IID_UnitAI);
     1484                        if (cmpUnitAI)
     1485                        {
     1486                            var speed = cmpUnitAI.GetWalkSpeed();
     1487                            if (speed < this.GetWalkSpeed())
     1488                                this.SetMoveSpeed(speed);
     1489                        }
     1490                    }
     1491                },
     1492
     1493                "MoveCompleted": function() {
     1494                    this.SetMoveSpeed(this.GetWalkSpeed());
     1495                    if (!this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange))
     1496                        this.SetNextState("GUARDING");
     1497                },
     1498            },
     1499
     1500            "GUARDING": {
     1501                "enter": function () {
     1502                    this.StartTimer(1000, 1000);
     1503                    this.SetHeldPositionOnEntity(this.entity);
     1504                    this.SelectAnimation("idle");
     1505                    return false;
     1506                },
     1507
     1508                "LosRangeUpdate": function(msg) {
     1509                    // Start attacking one of the newly-seen enemy (if any)
     1510                    if (this.GetStance().targetVisibleEnemies)
     1511                        this.AttackEntitiesByPreference(msg.data.added);
     1512                },
     1513
     1514                "LosGaiaRangeUpdate": function(msg) {
     1515                    // Start attacking one of the newly-seen enemy (if any)
     1516                    if (this.GetStance().targetVisibleEnemies)
     1517                        this.AttackGaiaEntitiesByPreference(msg.data.added);
     1518                },
     1519
     1520                "Timer": function(msg) {
     1521                    // Check the target is alive
     1522                    if (!this.TargetIsAlive(this.isGuardOf))
     1523                    {
     1524                        this.FinishOrder();
     1525                        return;
     1526                    }
     1527                    // then check is the target has moved
     1528                    if (this.MoveToTargetRangeExplicit(this.isGuardOf, 0, this.guardRange))
     1529                        this.SetNextState("ESCORTING");
     1530                    else
     1531                    {
     1532                        // if nothing better to do, check if the guarded needs to be healed or repaired
     1533                        var cmpHealth = Engine.QueryInterface(this.isGuardOf, IID_Health);
     1534                        if (cmpHealth && (cmpHealth.GetHitpoints() < cmpHealth.GetMaxHitpoints()))
     1535                        {
     1536                            if (this.CanHeal(this.isGuardOf))
     1537                                this.PushOrderFront("Heal", { "target": this.isGuardOf, "force": false });
     1538                            else if (this.CanRepair(this.isGuardOf) && cmpHealth.IsRepairable())
     1539                                this.PushOrderFront("Repair", { "target": this.isGuardOf, "autocontinue": false, "force": false });
     1540                        }
     1541                    }
     1542                },
     1543
     1544                "leave": function(msg) {
     1545                    this.StopTimer();
     1546                },
     1547            },
     1548        },
     1549
    13691550        "FLEEING": {
    13701551            "enter": function() {
    13711552                this.PlaySound("panic");
     
    27152896    // Queue of remembered works
    27162897    this.workOrders = [];
    27172898
     2899    this.isGuardOf = undefined;
     2900
    27182901    // For preventing increased action rate due to Stop orders or target death.
    27192902    this.lastAttacked = undefined;
    27202903    this.lastHealed = undefined;
     
    30393222        error("FinishOrder called for entity " + this.entity + " (" + template + ") when order queue is empty\n" + stack);
    30403223    }
    30413224
    3042     // Remove the order from the queue
    30433225    this.orderQueue.shift();
    30443226    this.order = this.orderQueue[0];
    30453227
     
    33913573    for each (var order in this.orderQueue)
    33923574        if (order.data && order.data.target && order.data.target == msg.entity)
    33933575            order.data.target = msg.newentity;
     3576
     3577    if (this.isGuardOf && this.isGuardOf == msg.entity)
     3578        this.isGuardOf = msg.newentity;
    33943579};
    33953580
    33963581UnitAI.prototype.OnAttacked = function(msg)
     
    33983583    UnitFsm.ProcessMessage(this, {"type": "Attacked", "data": msg});
    33993584};
    34003585
     3586UnitAI.prototype.OnGuardedAttacked = function(msg)
     3587{
     3588    UnitFsm.ProcessMessage(this, {"type": "GuardedAttacked", "data": msg.data});
     3589};
     3590
    34013591UnitAI.prototype.OnHealthChanged = function(msg)
    34023592{
    34033593    UnitFsm.ProcessMessage(this, {"type": "HealthChanged", "from": msg.from, "to": msg.to});
     
    40434233    if (force)
    40444234        return false;
    40454235
     4236    // If we are guarding/escorting, don't abandon as long as the guarded is in target range of the attacker
     4237    if (this.isGuardOf)
     4238    {
     4239        var cmpUnitAI =  Engine.QueryInterface(target, IID_UnitAI);
     4240        var cmpAttack = Engine.QueryInterface(target, IID_Attack);
     4241        if (cmpUnitAI && cmpAttack)
     4242        {
     4243            for each (var type in cmpAttack.GetAttackTypes())
     4244                if (cmpUnitAI.CheckTargetAttackRange(this.isGuardOf, IID_Attack, type))
     4245                    return false;
     4246        }
     4247    }
     4248
    40464249    // Stop if we're in hold-ground mode and it's too far from the holding point
    40474250    if (this.GetStance().respondHoldGround)
    40484251    {
     
    40774280    if (this.GetStance().respondChase)
    40784281        return true;
    40794282
     4283    // If we are guarding/escorting, chase at least as long as the guarded is in target range of the attacker
     4284    if (this.isGuardOf)
     4285    {
     4286        var cmpUnitAI =  Engine.QueryInterface(target, IID_UnitAI);
     4287        var cmpAttack = Engine.QueryInterface(target, IID_Attack);
     4288        if (cmpUnitAI && cmpAttack)
     4289        {
     4290            for each (var type in cmpAttack.GetAttackTypes())
     4291                if (cmpUnitAI.CheckTargetAttackRange(this.isGuardOf, IID_Attack, type))
     4292                    return true;
     4293        }
     4294    }
     4295
    40804296    if (force)
    40814297        return true;
    40824298
     
    41774393
    41784394        case "WalkToTarget":
    41794395        case "WalkToTargetRange": // This doesn't move to the target (just into range), but a later order will.
     4396        case "Guard":
    41804397        case "Flee":
    41814398        case "LeaveFoundation":
    41824399        case "Attack":
     
    42224439};
    42234440
    42244441/**
     4442 * Adds guard/escort order to the queue, forced by the player.
     4443 */
     4444UnitAI.prototype.Guard = function(target, queued)
     4445{
     4446    if (!this.CanGuard())
     4447    {
     4448        this.WalkToTarget(target, queued);
     4449        return;
     4450    }
     4451
     4452    // if we already had an old guard order, do nothing if the target is the same
     4453    // and the order is running, otherwise remove the previous order
     4454    if (this.isGuardOf)
     4455    {
     4456        if (this.isGuardOf == target && this.order && this.order.type == "Guard")
     4457            return;
     4458        else
     4459            this.RemoveGuard();
     4460    }
     4461
     4462    this.AddOrder("Guard", { "target": target, "force": false }, queued);
     4463};
     4464
     4465UnitAI.prototype.AddGuard = function(target)
     4466{
     4467    if (!this.CanGuard())
     4468        return false;
     4469    var cmpGuard = Engine.QueryInterface(target, IID_Guard);
     4470    if (!cmpGuard)
     4471        return false;
     4472    // Do not allow to guard a unit already guarding
     4473    var cmpUnitAI = Engine.QueryInterface(target, IID_UnitAI);
     4474    if (cmpUnitAI && cmpUnitAI.IsGuardOf())
     4475        return false;
     4476    this.isGuardOf = target;
     4477    this.guardRange = cmpGuard.GetRange(this.entity);
     4478    cmpGuard.AddGuard(this.entity);
     4479    return true;
     4480};
     4481
     4482UnitAI.prototype.RemoveGuard = function()
     4483{
     4484    if (this.isGuardOf)
     4485    {
     4486        var cmpGuard = Engine.QueryInterface(this.isGuardOf, IID_Guard);
     4487        if (cmpGuard)
     4488            cmpGuard.RemoveGuard(this.entity);
     4489        this.guardRange = undefined;
     4490        this.isGuardOf = undefined;
     4491    }
     4492
     4493    if (!this.order)
     4494        return;
     4495
     4496    if (this.order.type == "Guard")
     4497        UnitFsm.ProcessMessage(this, {"type": "RemoveGuard"});
     4498    else
     4499        for (var i = 1; i < this.orderQueue.length; ++i)
     4500            if (this.orderQueue[i].type == "Guard")
     4501                this.orderQueue.splice(i, 1);
     4502};
     4503
     4504UnitAI.prototype.IsGuardOf = function()
     4505{
     4506    return this.isGuardOf;
     4507};
     4508
     4509UnitAI.prototype.SetGuardOf = function(entity)
     4510{
     4511    // entity may be undefined
     4512    this.isGuardOf = entity;
     4513};
     4514
     4515UnitAI.prototype.CanGuard = function()
     4516{
     4517    // Formation controllers should always respond to commands
     4518    // (then the individual units can make up their own minds)
     4519    if (this.IsFormationController())
     4520        return true;
     4521
     4522    // Do not let a unit already guarded to guard. This would work in principle,
     4523    // but would clutter the gui with too much buttons to take all cases into account
     4524    var cmpGuard = Engine.QueryInterface(this.entity, IID_Guard);
     4525    if (cmpGuard && cmpGuard.GetEntities().length)
     4526        return false;
     4527
     4528    return (this.template.CanGuard == "true");
     4529};
     4530
     4531/**
    42254532 * Adds walk order to queue, forced by the player.
    42264533 */
    42274534UnitAI.prototype.Walk = function(x, z, queued)
     
    46824989    this.heldPosition = {"x": x, "z": z};
    46834990};
    46844991
     4992UnitAI.prototype.SetHeldPositionOnEntity = function(entity)
     4993{
     4994    var cmpPosition = Engine.QueryInterface(this.entity, IID_Position);
     4995    if (!cmpPosition || !cmpPosition.IsInWorld())
     4996        return;
     4997    var pos = cmpPosition.GetPosition();
     4998    this.SetHeldPosition(pos.x, pos.z);
     4999};
     5000
    46855001UnitAI.prototype.GetHeldPosition = function(pos)
    46865002{
    46875003    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

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

     
    4848    <MaxDistance>10.0</MaxDistance>
    4949    <GainMultiplier>1.0</GainMultiplier>
    5050  </Trader>
     51  <UnitAI>
     52    <CanGuard>false</CanGuard>
     53  </UnitAI>
    5154  <UnitMotion>
    5255    <WalkSpeed>10.5</WalkSpeed>
    5356  </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>