Index: binaries/data/config/default.cfg =================================================================== --- binaries/data/config/default.cfg +++ binaries/data/config/default.cfg @@ -353,6 +353,7 @@ toggledefaultformation = "" ; Switch between null default formation and the last default formation used (defaults to "box") flare = K ; Modifier to send a flare to your allies flareactivate = "" ; Modifier to activate the mode to send a flare to your allies +calltoarms = "" ; Modifier to call the selected units to the arms. ; Overlays showstatusbars = Tab ; Toggle display of status bars devcommands.toggle = "Alt+D" ; Toggle developer commands panel Index: binaries/data/mods/public/gui/hotkeys/spec/ingame.json =================================================================== --- binaries/data/mods/public/gui/hotkeys/spec/ingame.json +++ binaries/data/mods/public/gui/hotkeys/spec/ingame.json @@ -174,6 +174,10 @@ "session.flareactivate": { "name": "Flare (toggle)", "desc": "Set the cursor to Flare. The hotkey can be released." + }, + "session.calltoarms": { + "name": "Call to arms", + "desc": "Send the selected units on attack move to the specified location after dropping resources." } } } 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; 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 @@ -246,6 +246,47 @@ "specificness": 10, }, + "call-to-arms": { + "execute": function(target, action, selection, queued, pushFront) + { + 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": queued, + "pushFront": pushFront, + "allowCapture": true + }); + return true; + }, + "getActionInfo": function(entState, targetState) + { + return { "possible": !!entState.unitAI }; + }, + "actionCheck": function(target, selection) + { + const actionInfo = getActionInfo("call-to-arms", target, selection); + return actionInfo.possible && { + "type": "call-to-arms", + "cursor": "action-attack", + "target": target, + "firstAbleEntity": actionInfo.entity + }; + }, + "preSelectedActionCheck": function(target, selection) + { + return preSelectedAction == ACTION_CALLTOARMS && + this.actionCheck(target, selection); + }, + "specificness": 50, + }, + "patrol": { "execute": function(target, action, selection, queued, pushFront) @@ -1423,6 +1464,31 @@ "allowedPlayers": ["Player"] }, + "call-to-arms": { + "getInfo": function(entStates) + { + if (entStates.every(entState => { + const classes = entState.identity.classes; + return !MatchesClassList(classes, ["Soldier", "Warship", "Siege", "Healer"]); + })) + { + 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 @@ -4057,7 +4057,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" || t == "ReturnResource"; if (isWorkType(type)) { this.workOrders = []; @@ -5370,6 +5370,24 @@ this.AddOrder("Stop", { "force": true }, queued, pushFront); }; +/** + * The unit will drop all resources at the closest 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, pushFront) +{ + const cmpResourceGatherer = Engine.QueryInterface(this.entity, IID_ResourceGatherer); + if (!cmpResourceGatherer) + return false; + const nearby = this.FindNearestDropsite(cmpResourceGatherer.GetMainCarryingType()); + if (!nearby) + return false; + this.ReturnResource(nearby, queued, pushFront); + return true; +}; + /** * 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,30 @@ } }, + "call-to-arms": function(player, cmd, data) + { + const templateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager); + const unitsToMove = data.entities.filter(ent => { + const cmpIdentity = Engine.QueryInterface(ent, IID_Identity); + return MatchesClassList(cmpIdentity.GetClassesList(), ["Soldier", "Warship", "Siege", "Healer"]); + }); + GetFormationUnitAIs(unitsToMove, player).forEach(cmpUnitAI => { + const target = cmd.target; + if (cmd.pushFront) + { + cmpUnitAI.WalkAndFight(target.x, target.z, cmd.targetClasses, cmd.allowCapture, true, cmd.pushFront); + if (!cmpUnitAI.DropAtNearestDropSite(cmd.queued, cmd.pushFront)) + cmpUnitAI.Stop(cmd.queued); + } + else + { + if (!cmpUnitAI.DropAtNearestDropSite(cmd.queued, cmd.pushFront)) + cmpUnitAI.Stop(cmd.queued); + cmpUnitAI.WalkAndFight(target.x, target.z, cmd.targetClasses, cmd.allowCapture, true, cmd.pushFront); + } + }); + }, + "remove-guard": function(player, cmd, data) { for (let ent of data.entities)