Index: binaries/data/mods/public/gui/session/selection.js =================================================================== --- binaries/data/mods/public/gui/session/selection.js +++ binaries/data/mods/public/gui/session/selection.js @@ -48,6 +48,7 @@ { this.groups = {}; this.ents = {}; + this.selectedUniqueTemplates = []; }; EntityGroups.prototype.add = function(ents) @@ -208,6 +209,23 @@ }; /** + * Get a list of the unique templates names. + * @return - An array of template names which occur one or more times in the selection. + */ +EntitySelection.prototype.getUniqueTemplateNames = function() +{ + let templateNames = []; + + for (let ent in this.selected) + { + let entState = GetEntityState(+ent); + if (entState && templateNames.indexOf(entState.template) == -1) + templateNames.push(entState.template); + } + return templateNames; +}; + +/** * Update the selection to take care of changes (like units that have been killed) */ EntitySelection.prototype.update = function() @@ -354,6 +372,7 @@ _setStatusBars(this.toList(), false); _setMotionOverlay(this.toList(), false); this.selected = {}; + this.selectedUniqueTemplates = []; this.groups.reset(); this.onChange(); }; @@ -445,6 +464,7 @@ g_canMoveIntoFormation = {}; g_allBuildableEntities = undefined; g_allTrainableEntities = undefined; + g_Selection.selectedUniqueTemplates = g_Selection.getUniqueTemplateNames(); } /** Index: binaries/data/mods/public/gui/session/selection_panels.js =================================================================== --- binaries/data/mods/public/gui/session/selection_panels.js +++ binaries/data/mods/public/gui/session/selection_panels.js @@ -921,6 +921,45 @@ } }; +g_SelectionPanels.PrefAttack = { + "getMaxNumberOfItems": function() + { + return 5; + }, + "getItems": function(entStates) + { + if (entStates.some(state => !state.unitAI || !hasClass(state, "Unit") || !state.attack)) + return []; + + let items = ["Default"]; + for (let template of g_Selection.selectedUniqueTemplates) + items = items.concat(Object.keys(GetTemplateData(template).attack).filter( + type => items.indexOf(type) == -1 && type != "Slaughter" + )); + + // No point in showing only "Default" and one extra attack type. + return items.length > 2 ? items : ""; + }, + "setupButton": function(data) + { + let entIds = data.unitEntStates.map(state => state.id); + data.button.onPress = function() { performPrefAttack(entIds, data.item); }; + + data.button.tooltip = getPrefAttackDisplay(data.item).name + "\n" + + "[font=\"sans-13\"]" + getPrefAttackDisplay(data.item).tooltip + "[/font]"; + + data.guiSelection.hidden = !Engine.GuiInterfaceCall("IsPrefAttackSelected", { + "ents": entIds, + "prefAttack": data.item + }); + data.icon.sprite = "stretched:session/icons/attacks/" + data.item.toLowerCase() + ".png"; + data.button.enabled = controlsPlayer(data.player); + + setPanelObjectPosition(data.button, data.i, data.rowLength); + return true; + } +}; + g_SelectionPanels.Training = { "getMaxNumberOfItems": function() { @@ -1162,7 +1201,8 @@ "Garrison", // More important than Formation, as you want to see the garrisoned units in ships "Alert", "Formation", - "Stance", // Normal together with formation + "Stance", // Normal together with formation and preferred attack. + "PrefAttack", // Normally together with formation and stance. // RIGHT PANE "Gate", // Must always be shown on gates Index: binaries/data/mods/public/gui/session/selection_panels_helpers.js =================================================================== --- binaries/data/mods/public/gui/session/selection_panels_helpers.js +++ binaries/data/mods/public/gui/session/selection_panels_helpers.js @@ -87,6 +87,19 @@ } } +function getPrefAttackDisplay(name) +{ + let text = {}; + text.name = sprintf(translateWithContext("attack type", "%(name)s"), { + "name": name + }); + text.tooltip = name == "Default" ? translate("Let the unit decide.") : + sprintf(translate("Prefer %(name)s attack."), { + "name": translateWithContext("attack type", name) + }); + return text; +} + /** * Format entity count/limit message for the tooltip */ @@ -290,6 +303,18 @@ }); } +function performPrefAttack(entities, prefAttackType) +{ + if (!entities) + return; + + Engine.PostNetworkCommand({ + "type": "prefAttackType", + "entities": entities, + "name": prefAttackType + }); +} + function lockGate(lock) { Engine.PostNetworkCommand({ Index: binaries/data/mods/public/gui/session/selection_panels_left/attack_panel.xml =================================================================== --- /dev/null +++ binaries/data/mods/public/gui/session/selection_panels_left/attack_panel.xml @@ -0,0 +1,16 @@ + + + + + + Index: binaries/data/mods/public/gui/session/session.xml =================================================================== --- binaries/data/mods/public/gui/session/session.xml +++ binaries/data/mods/public/gui/session/session.xml @@ -120,7 +120,7 @@ { + let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); + return cmpUnitAI && cmpUnitAI.GetPrefAttack() == data.prefAttack; + }); +}; + GuiInterface.prototype.GetAllBuildableEntities = function(player, cmd) { let buildableEnts = []; @@ -1934,6 +1942,7 @@ "IsFormationSelected": 1, "GetFormationInfoFromTemplate": 1, "IsStanceSelected": 1, + "IsPrefAttackSelected": 1, "UpdateDisplayedPlayerColors": 1, "SetSelectionHighlight": 1, 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 @@ -3212,6 +3212,7 @@ this.lastHealed = undefined; this.SetStance(this.template.DefaultStance); + this.prefAttack = "Default"; }; UnitAI.prototype.IsTurret = function() @@ -4561,9 +4562,15 @@ UnitAI.prototype.GetBestAttackAgainst = function(target, allowCapture) { - var cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); + let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); if (!cmpAttack) return undefined; + + // First try to use a preferred attack type. + let prefAttack = this.GetPrefAttack(); + if (prefAttack != "Default" && cmpAttack.CanAttack(target, [prefAttack])) + return prefAttack; + return cmpAttack.GetBestAttackAgainst(target, allowCapture); }; @@ -5632,6 +5639,30 @@ return this.stance; }; +UnitAI.prototype.SetPrefAttack = function(attackType) +{ + if (this.GetSelectableAttacks().indexOf(attackType) != -1) + { + this.prefAttack = attackType; + Engine.PostMessage(this.entity, MT_UnitPrefAttackChanged, { "to": this.prefAttack }); + + // Reset the range queries, since the range depends on attack type. + this.SetupRangeQueries(); + } +}; + +UnitAI.prototype.GetSelectableAttacks = function() +{ + let cmpAttack = Engine.QueryInterface(this.entity, IID_Attack); + if (cmpAttack) + return cmpAttack.GetAttackTypes().concat(["Default"]); +}; + +UnitAI.prototype.GetPrefAttack = function() +{ + return this.prefAttack; +}; + /* * Make the unit walk at its normal pace. */ Index: binaries/data/mods/public/simulation/components/interfaces/UnitAI.js =================================================================== --- binaries/data/mods/public/simulation/components/interfaces/UnitAI.js +++ binaries/data/mods/public/simulation/components/interfaces/UnitAI.js @@ -38,3 +38,10 @@ * sent from UnitAI whenever a pickup is aborted. */ Engine.RegisterMessageType("PickupCanceled"); + +/** + * Message of the form { "to": string } + * where "to" value is a UnitAI preferred attack type, + * sent from UnitAI whenever the unit's pref. attack changes. + */ +Engine.RegisterMessageType("UnitPrefAttackChanged"); 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 @@ -627,6 +627,16 @@ } }, + "prefAttackType": function(player, cmd, data) + { + for (let ent of data.entities) + { + let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI); + if (cmpUnitAI) + cmpUnitAI.SetPrefAttack(cmd.name); + } + }, + "lock-gate": function(player, cmd, data) { for (let ent of data.entities)