Index: ps/trunk/binaries/data/config/default.cfg =================================================================== --- ps/trunk/binaries/data/config/default.cfg +++ ps/trunk/binaries/data/config/default.cfg @@ -284,6 +284,7 @@ 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" Index: ps/trunk/binaries/data/mods/public/gui/hotkeys/spec/selection.json =================================================================== --- ps/trunk/binaries/data/mods/public/gui/hotkeys/spec/selection.json +++ ps/trunk/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." Index: ps/trunk/binaries/data/mods/public/gui/session/input.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/input.js +++ ps/trunk/binaries/data/mods/public/gui/session/input.js @@ -1170,7 +1170,7 @@ )); if (unit) { - g_Selection.removeList([unit]); + g_Selection.removeList([unit], true); return [unit]; } return null; Index: ps/trunk/binaries/data/mods/public/gui/session/selection.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/selection.js +++ ps/trunk/binaries/data/mods/public/gui/session/selection.js @@ -284,9 +284,9 @@ 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 @@ 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 @@ 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); @@ -462,6 +467,28 @@ } /** + * 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 */ Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js +++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js @@ -281,6 +281,12 @@ "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 @@ "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); Index: ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js +++ ps/trunk/binaries/data/mods/public/simulation/components/tests/test_GuiInterface.js @@ -10,6 +10,7 @@ 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");