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 @@ -245,6 +245,44 @@ }, "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; + }, + "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 + }; + }, + "preSelectedActionCheck": function(target, selection) + { + return preSelectedAction == ACTION_CALLTOARMS && + this.actionCheck(target, selection); + }, + "specificness": 50, + }, "patrol": { @@ -1423,6 +1461,33 @@ "allowedPlayers": ["Player"] }, + "call-to-arms": { + "getInfo": function(entStates) + { + if(entStates.every(entState => { + const classes = entState.identity.classes; + // Exclude ruins + if(classes.length == 0) + return true; + return MatchesClassList(classes, ["Support+Elephant", "Ship+!Warship", "FemaleCitizen", "Structure", "Trader", "Relic"]); + })) + { + 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" || t == "ReturnResource"; if (isWorkType(type)) { this.workOrders = []; @@ -5365,6 +5365,25 @@ 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) +{ + + 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); + 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,31 @@ } }, + "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; + const identity = Engine.QueryInterface(ent, IID_Identity); + const classes = identity.GetClassesList(); + if (MatchesClassList(classes, ["Hero", "Champion", "Healer"])) + return true; + if(MatchesClassList(classes, ["FemaleCitizen", "Elephant+Support", "Ship+!Warship"])) + return false; + return Engine.QueryInterface(ent, IID_Attack) != undefined; + }); + 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)