diff --git a/CREDITS.md b/CREDITS.md
index 731ccc454c..435efb019c 100644
--- a/CREDITS.md
+++ b/CREDITS.md
@@ -888,3 +888,5 @@ This page lists all the individual contributions to the project by their author.
- **Chang_zhi**:
- Interop export interface for accessing scenario local/global variables
- Add `ClampToScreen` tag for `BannerType` to control whether banner position is clamped to the visible area
+- **Igor Kolchinskii (leosnake2208)**:
+ - Type selection by double/triple-click
diff --git a/Phobos.vcxproj b/Phobos.vcxproj
index cea5e31f02..815b17d939 100644
--- a/Phobos.vcxproj
+++ b/Phobos.vcxproj
@@ -263,6 +263,7 @@
+
diff --git a/docs/User-Interface.md b/docs/User-Interface.md
index 26fc2b8418..52b81006dd 100644
--- a/docs/User-Interface.md
+++ b/docs/User-Interface.md
@@ -450,6 +450,17 @@ BuildingTypeSelectable=false ; boolean
Due to technical limitations, this feature is forcibly disabled without Ares.
```
+### Type selection by multi-click
+
+- Double-clicking a unit selects every unit of the same selection group currently on screen; triple-clicking selects every unit of that group across the whole map. This complements the vanilla type-select hotkey (hold `T` and click) with the double/triple-click gesture common to modern RTS games. Only your own selectable units are affected, and the selection group is the same one used by the type-select hotkey (the `[TechnoType] -> GroupAs` tag, falling back to the type's ID).
+- Enable it with `TypeSelectByMultiClick=true`.
+
+In `RA2MD.ini`:
+```ini
+[Phobos]
+TypeSelectByMultiClick=false ; boolean
+```
+
### Visual effects toggling
- It is possible to toggle certain light flash effects off. These light flash effects include:
diff --git a/docs/Whats-New.md b/docs/Whats-New.md
index 1befcdb9ae..5df6aa0381 100644
--- a/docs/Whats-New.md
+++ b/docs/Whats-New.md
@@ -150,6 +150,7 @@ ShowWeedsCounter=true ; boolean
ToolTipDescriptions=true ; boolean
ToolTipBlur=false ; boolean
SaveGameOnScenarioStart=true ; boolean
+TypeSelectByMultiClick=false ; boolean
HideLightFlashEffects=false ; boolean
HideLaserTrailEffects=false ; boolean
HideShakeEffects=false ; boolean
@@ -386,6 +387,7 @@ HideShakeEffects=false ; boolean
:open:
#### New:
+- [Type selection by double/triple-click](User-Interface.md#type-selection-by-multi-click) (by leosnake2208)
- [Allow using waypoints, area guard and attack move with aircraft](Fixed-or-Improved-Logics.md#extended-aircraft-missions) (by CrimRecya)
- [Enhanced Straight trajectory](New-or-Enhanced-Logics.md#straight-trajectory) (by CrimRecya)
- [Enable building production queue](User-Interface.md#building-production-queue) (by CrimRecya)
diff --git a/src/Misc/MultiClickTypeSelect.cpp b/src/Misc/MultiClickTypeSelect.cpp
new file mode 100644
index 0000000000..1bd5ed001d
--- /dev/null
+++ b/src/Misc/MultiClickTypeSelect.cpp
@@ -0,0 +1,111 @@
+#include
+
+#include
+#include
+#include
+#include
+
+// Multi-click type selection.
+//
+// Vanilla Yuri's Revenge only offers "type select" through the type-select hotkey (hold T,
+// then click/drag) - there is no double-click equivalent; a double-click in the tactical
+// area falls through to the message handler's default case. This adds the modern-RTS gesture:
+// * double-click a unit -> select every unit of the same selection group on screen;
+// * triple-click a unit -> select every unit of the same selection group across the map.
+//
+// "Selection group" reuses Phobos' existing concept (TechnoTypeExt::GetSelectionGroupID /
+// HasSelectionGroupID, i.e. the [TechnoType]GroupAs tag with the type ID as fallback), so it
+// stays consistent with the hotkey-driven type select in Selection.cpp.
+//
+// Gated behind [Phobos]TypeSelectByMultiClick (default off).
+
+namespace MultiClickTypeSelect
+{
+ // Clicks within GetDoubleClickTime() and this many screen pixels of the previous one
+ // count as part of the same streak.
+ static constexpr int PositionTolerance = 4;
+
+ static DWORD LastClickTick = 0;
+ static POINT LastClickPos = { -9999, -9999 };
+ static int ClickStreak = 0;
+
+ // Mirrors ExtSelection::ObjectClass_IsSelectable (Selection.cpp): an own, alive, currently
+ // selectable object.
+ static bool IsOwnSelectable(TechnoClass* pTechno)
+ {
+ const auto pOwner = pTechno->GetOwningHouse();
+ return pOwner && pOwner->IsControlledByCurrentPlayer()
+ && pTechno->CanBeSelected() && pTechno->CanBeSelectedNow()
+ && !pTechno->InLimbo;
+ }
+
+ // Add to the current selection every own, selectable mobile unit sharing the just-clicked
+ // unit's selection group. onScreenOnly limits it to units drawn in the tactical viewport.
+ static void SelectSameGroup(bool onScreenOnly)
+ {
+ if (ObjectClass::CurrentObjects.Count < 1)
+ return;
+
+ // The single click that preceded this streak left the clicked unit as the selection.
+ const auto pClicked = abstract_cast(ObjectClass::CurrentObjects.GetItem(0));
+ if (!pClicked)
+ return; // only mobile units drive type select
+
+ const char* groupID = TechnoTypeExt::GetSelectionGroupID(pClicked->GetTechnoType());
+
+ for (auto const pTechno : TechnoClass::Array)
+ {
+ const auto pFoot = abstract_cast(pTechno);
+
+ if (!pFoot || pFoot->IsSelected || !IsOwnSelectable(pFoot))
+ continue;
+
+ if (!TechnoTypeExt::HasSelectionGroupID(pFoot->GetTechnoType(), groupID))
+ continue;
+
+ if (onScreenOnly && !TacticalClass::Instance->CoordsToClient(pFoot->GetCoords()).second)
+ continue;
+
+ pFoot->Select();
+ }
+ }
+
+ // Update the click streak from the current cursor time/position and act on it.
+ static void HandleClick()
+ {
+ POINT pos { 0, 0 };
+ GetCursorPos(&pos);
+ const DWORD now = GetTickCount();
+
+ const int dx = pos.x - LastClickPos.x;
+ const int dy = pos.y - LastClickPos.y;
+ const bool sameSpot = dx >= -PositionTolerance && dx <= PositionTolerance
+ && dy >= -PositionTolerance && dy <= PositionTolerance;
+
+ if (now - LastClickTick <= GetDoubleClickTime() && sameSpot)
+ ClickStreak = ClickStreak < 3 ? ClickStreak + 1 : 3;
+ else
+ ClickStreak = 1;
+
+ LastClickTick = now;
+ LastClickPos = pos;
+
+ if (ClickStreak == 2)
+ SelectSameGroup(true); // same group, on screen
+ else if (ClickStreak == 3)
+ SelectSameGroup(false); // same group, whole map
+ }
+}
+
+// Tactical LBUTTONUP handler, just after the click's own selection has been applied (call to
+// 0x4AB9B0) and before the drag flag is cleared. This point is only reached by a genuine
+// single click - a completed rubber-band selection returns earlier - so the preceding click
+// has already made the clicked unit the current selection. Stolen bytes:
+// mov byte ptr [esi+0x555A], bl (absolute operand, safe to relocate).
+DEFINE_HOOK(0x693290, TacticalClass_LButtonUp_MultiClickTypeSelect, 0x6)
+{
+ if (Phobos::Config::TypeSelectByMultiClick)
+ MultiClickTypeSelect::HandleClick();
+
+ return 0;
+}
diff --git a/src/Phobos.INI.cpp b/src/Phobos.INI.cpp
index ac9f7649b2..04dd08a16e 100644
--- a/src/Phobos.INI.cpp
+++ b/src/Phobos.INI.cpp
@@ -50,6 +50,7 @@ bool Phobos::Config::ToolTipBlur = false;
bool Phobos::Config::PrioritySelectionFiltering = true;
bool Phobos::Config::PriorityDeployFiltering = true;
bool Phobos::Config::TypeSelectUseIFVMode = true;
+bool Phobos::Config::TypeSelectByMultiClick = false;
bool Phobos::Config::DevelopmentCommands = true;
bool Phobos::Config::SuperWeaponSidebarCommands = false;
bool Phobos::Config::ShowPlanningPath = false;
@@ -95,6 +96,7 @@ DEFINE_HOOK(0x5FACDF, OptionsClass_LoadSettings_LoadPhobosSettings, 0x5)
Phobos::Config::PrioritySelectionFiltering = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "PrioritySelectionFiltering", true);
Phobos::Config::PriorityDeployFiltering = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "PriorityDeployFiltering", true);
Phobos::Config::TypeSelectUseIFVMode = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "TypeSelectUseIFVMode", true);
+ Phobos::Config::TypeSelectByMultiClick = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "TypeSelectByMultiClick", false);
Phobos::Config::ShowPlacementPreview = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "ShowPlacementPreview", true);
Phobos::Config::MessageApplyHoverState = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "MessageApplyHoverState", false);
Phobos::Config::MessageDisplayInCenter = CCINIClass::INI_RA2MD.ReadBool(phobosSection, "MessageDisplayInCenter", false);
diff --git a/src/Phobos.h b/src/Phobos.h
index 6bdb4073ec..b4b50d23b4 100644
--- a/src/Phobos.h
+++ b/src/Phobos.h
@@ -85,6 +85,7 @@ class Phobos
static bool PrioritySelectionFiltering;
static bool PriorityDeployFiltering;
static bool TypeSelectUseIFVMode;
+ static bool TypeSelectByMultiClick;
static bool DevelopmentCommands;
static bool SuperWeaponSidebarCommands;
static bool ArtImageSwap;