diff --git a/binaries/data/config/default.cfg b/binaries/data/config/default.cfg index c4f9f23af9..25ef27668d 100644 --- a/binaries/data/config/default.cfg +++ b/binaries/data/config/default.cfg @@ -284,6 +284,7 @@ idleworker = Period, NumDecimal ; Select next idle worker idlewarrior = Slash, NumDivide ; Select next idle warrior idleunit = BackSlash ; Select next idle unit offscreen = Alt ; Include offscreen units in selection +singleselection = "" ; Select only one entity of a formation. [hotkey.selection.group.add] 0 = "Shift+0", "Shift+Num0" 1 = "Shift+1", "Shift+Num1" diff --git a/binaries/data/mods/public/gui/hotkeys/spec/selection.json b/binaries/data/mods/public/gui/hotkeys/spec/selection.json index 21696ff7cf..1674934c3c 100644 --- a/binaries/data/mods/public/gui/hotkeys/spec/selection.json +++ b/binaries/data/mods/public/gui/hotkeys/spec/selection.json @@ -55,6 +55,10 @@ "name": "Include offscreen", "desc": "Include offscreen units in selection." }, + "selection.singleselection": { + "name": "Single selection", + "desc": "Select only one entity of a formation." + }, "selection.group.save.0": { "name": "Set Control Group 0", "desc": "Save current selection as Control Group 0." diff --git a/binaries/data/mods/public/gui/session/input.js b/binaries/data/mods/public/gui/session/input.js index 9a3ab5ac67..13e5e34d6b 100644 --- a/binaries/data/mods/public/gui/session/input.js +++ b/binaries/data/mods/public/gui/session/input.js @@ -1170,7 +1170,7 @@ function popOneFromSelection(action) )); if (unit) { - g_Selection.removeList([unit]); + g_Selection.removeList([unit], true); return [unit]; } return null; diff --git a/binaries/data/mods/public/gui/session/selection.js b/binaries/data/mods/public/gui/session/selection.js index 12c1e15377..7725b544f2 100644 --- a/binaries/data/mods/public/gui/session/selection.js +++ b/binaries/data/mods/public/gui/session/selection.js @@ -284,9 +284,9 @@ EntitySelection.prototype.addList = function(ents, quiet, force = false) if (firstEntState && firstEntState.player != g_ViewedPlayer && !force) return; - let added = []; + const added = []; - for (const ent of ents) + for (const ent of this.addFormationMembers(ents)) { if (this.selected.size >= g_MaxSelectionSize) break; @@ -324,11 +324,15 @@ EntitySelection.prototype.addList = function(ents, quiet, force = false) this.onChange(); }; -EntitySelection.prototype.removeList = function(ents) +/** + * @param {number[]} ents - The entities to remove. + * @param {boolean} dontAddFormationMembers - If true we need to exclude adding formation members. + */ +EntitySelection.prototype.removeList = function(ents, dontAddFormationMembers = false) { - var removed = []; + const removed = []; - for (let ent of ents) + for (const ent of dontAddFormationMembers ? ents : this.addFormationMembers(ents)) if (this.selected.has(ent)) { this.groups.removeEnt(ent); @@ -407,9 +411,10 @@ EntitySelection.prototype.filter = function(condition) return result; }; -EntitySelection.prototype.setHighlightList = function(ents) +EntitySelection.prototype.setHighlightList = function(entities) { const highlighted = new Set(); + const ents = this.addFormationMembers(entities); for (const ent of ents) highlighted.add(ent); @@ -461,6 +466,28 @@ EntitySelection.prototype.selectAndMoveTo = function(entityID) Engine.CameraMoveTo(entState.position.x, entState.position.z); } +/** + * Adds the formation members of a selected entities to the selection. + * @param {number[]} entities - The entity IDs of selected entities. + * @return {number[]} - Some more entity IDs if part of a formation was selected. + */ +EntitySelection.prototype.addFormationMembers = function(entities) +{ + if (!entities.length || Engine.HotkeyIsPressed("selection.singleselection")) + return entities; + + const result = new Set(entities); + for (const entity of entities) + { + const entState = GetEntityState(+entity); + if (entState?.unitAI?.formation) + for (const member of GetEntityState(+entState.unitAI.formation).formation.members) + result.add(member); + } + + return result; +}; + /** * Cache some quantities which depends only on selection */ diff --git a/binaries/data/mods/public/simulation/components/GuiInterface.js b/binaries/data/mods/public/simulation/components/GuiInterface.js index 489d99321d..f6adf74015 100644 --- a/binaries/data/mods/public/simulation/components/GuiInterface.js +++ b/binaries/data/mods/public/simulation/components/GuiInterface.js @@ -281,6 +281,12 @@ GuiInterface.prototype.GetEntityState = function(player, ent) "controllable": cmpIdentity.IsControllable() }; + const cmpFormation = Engine.QueryInterface(ent, IID_Formation); + if (cmpFormation) + ret.formation = { + "members": cmpFormation.GetMembers() + }; + let cmpPosition = Engine.QueryInterface(ent, IID_Position); if (cmpPosition && cmpPosition.IsInWorld()) ret.position = cmpPosition.GetPosition(); @@ -416,7 +422,8 @@ GuiInterface.prototype.GetEntityState = function(player, ent) "isGuarding": cmpUnitAI.IsGuardOf(), "canPatrol": cmpUnitAI.CanPatrol(), "selectableStances": cmpUnitAI.GetSelectableStances(), - "isIdle": cmpUnitAI.IsIdle() + "isIdle": cmpUnitAI.IsIdle(), + "formation": cmpUnitAI.GetFormationController() }; let cmpGuard = Engine.QueryInterface(ent, IID_Guard); diff --git a/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js b/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js index 21297c21fe..c78efc05d6 100644 --- a/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js +++ b/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js @@ -10,6 +10,7 @@ Engine.LoadComponentScript("interfaces/Resistance.js"); Engine.LoadComponentScript("interfaces/DeathDamage.js"); Engine.LoadComponentScript("interfaces/EndGameManager.js"); Engine.LoadComponentScript("interfaces/EntityLimits.js"); +Engine.LoadComponentScript("interfaces/Formation.js"); Engine.LoadComponentScript("interfaces/Foundation.js"); Engine.LoadComponentScript("interfaces/Garrisonable.js"); Engine.LoadComponentScript("interfaces/GarrisonHolder.js");