Index: binaries/data/mods/public/gui/session/input.js =================================================================== --- binaries/data/mods/public/gui/session/input.js +++ binaries/data/mods/public/gui/session/input.js @@ -18,6 +18,7 @@ const ACTION_GUARD = 3; const ACTION_PATROL = 4; const ACTION_OCCUPY_TURRET = 5; +const ACTION_CALLTOARMS = 6; var preSelectedAction = ACTION_NONE; const INPUT_NORMAL = 0; @@ -246,7 +247,6 @@ // Decide between the following ordered actions, // if two actions are possible, the first one is taken // thus the most specific should appear first. - if (preSelectedAction != ACTION_NONE) { for (let action of g_UnitActionsSortedKeys) Index: binaries/data/mods/public/gui/session/unit_actions.js =================================================================== --- binaries/data/mods/public/gui/session/unit_actions.js +++ binaries/data/mods/public/gui/session/unit_actions.js @@ -245,6 +245,37 @@ }, "specificness": 10, }, + "call-to-arms": { + "execute": function(target, action, selection, queued) + { + let targetClasses; + if (Engine.HotkeyIsPressed("session.attackmoveUnit")) + targetClasses = { "attack": ["Unit"] }; + else + targetClasses = { "attack": ["Unit", "Structure"] }; + Engine.PostNetworkCommand({ + "type": "call-to-arms", + "entities": selection, + "target": target, + "targetClasses": targetClasses, + "queued": false, + "allowCapture": false + }); + + return true; + }, + "preSelectedActionCheck": function(target, selection) + { + if (preSelectedAction != ACTION_CALLTOARMS || !getActionInfo("attack-move", target, selection).possible) + return false; + return { + "type": "call-to-arms", + "cursor": "action-attack", + "target": target + }; + }, + "specificness": 50, + }, "patrol": { @@ -1423,6 +1454,36 @@ "allowedPlayers": ["Player"] }, + "call-to-arms": { + "getInfo": function(entStates) + { + if(entStates.every(entState => { + const classes = entState.identity.classes; + if (classes.includes("FemaleCitizen") || classes.includes("Structure") || classes.includes("Trader")) + return true; + if (classes.includes("Ship") && !classes.includes("Warship")) + return true; + if (classes.includes("Support") && classes.includes("Elephant")) + return true; + return false; + })) + { + return false; + } + return { + "tooltip": colorizeHotkey("%(hotkey)s" + " ", "session.callToArms") + + translate("Send the selected units on attack move to the specified location after dropping resources."), + "icon": "call-to-arms.png", + "enabled": true + }; + }, + "execute": function(entStates) + { + inputState = INPUT_PRESELECTEDACTION; + preSelectedAction = ACTION_CALLTOARMS; + }, + "allowedPlayers": ["Player"] + }, "garrison": { "getInfo": function(entStates) { Index: binaries/data/mods/public/simulation/components/UnitAI.js =================================================================== --- binaries/data/mods/public/simulation/components/UnitAI.js +++ binaries/data/mods/public/simulation/components/UnitAI.js @@ -4052,7 +4052,7 @@ UnitAI.prototype.UpdateWorkOrders = function(type) { - var isWorkType = type => type == "Gather" || type == "Trade" || type == "Repair" || type == "ReturnResource"; + const isWorkType = t => t == "Gather" || t == "Trade" || t == "Repair"; if (isWorkType(type)) { this.workOrders = []; @@ -5365,6 +5365,26 @@ this.AddOrder("Stop", { "force": true }, queued, pushFront); }; +/** + * The unit will drop all resources at the next dropsite. If this unit is no gatherer or + * no dropsite is available, it will do nothing. + * + * @return {boolean} Whether this unit could drop resources. + */ +UnitAI.prototype.DropAtNearestDropSite = function(queued) +{ + let nearby; + const cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); + if (cmpResourceGatherer) + nearby = this.FindNearestDropsite(cmpResourceGatherer.GetMainCarryingType()); + if (nearby) + { + this.ReturnResource(nearby, queued); + return true; + } + return false; +}; + /** * Adds walk-to-target order to queue, this only occurs in response * to a player order, and so is forced. Index: binaries/data/mods/public/simulation/helpers/Commands.js =================================================================== --- binaries/data/mods/public/simulation/helpers/Commands.js +++ binaries/data/mods/public/simulation/helpers/Commands.js @@ -279,6 +279,40 @@ } }, + "call-to-arms": function(player, cmd, data) + { + const templateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); + // Filter, which units to move. + // Soldiers, Healers, Heros, Siege Engines, Warships are moved + // Female citizens, Traders, Fishingboats and other units with no attack stay. + const ents = data.entities.filter(ent => { + if (Engine.QueryInterface(ent, IID_Trader)) + return false; + if (Engine.QueryInterface(ent, IID_Heal)) + return true; + const template = templateManager.GetTemplate(templateManager.GetCurrentTemplateName(ent)); + const identity = Engine.QueryInterface(ent, IID_Identity); + const classes = identity.GetClassesList(); + // Unconditionally move heroes + if (classes.includes("Hero") || classes.includes("Champion")) + return true; + else if (classes.includes("FemaleCitizen")|| (classes.includes("Elephant") && + classes.includes("Support")) || (classes.includes("Ship") && !classes.includes("Warship"))) + return false; + const attack = Engine.QueryInterface(ent, IID_Attack); + // Move unit, if it can attack + if (attack && attack.GetAttackTypes().length > 0) + return true; + return false; + }); + GetFormationUnitAIs(ents, player).forEach(cmpUnitAI => { + if (!cmpUnitAI.DropAtNearestDropSite(cmd.queued)) + cmpUnitAI.Stop(cmd.queued); + const target = cmd.target; + cmpUnitAI.WalkAndFight(target.x, target.z, cmd.targetClasses, cmd.allowCapture, true); + }); + }, + "remove-guard": function(player, cmd, data) { for (let ent of data.entities)