From 2fc07ebe526b9722f020b879ba3b18c066cb72cc Mon Sep 17 00:00:00 2001 From: Templarfreak Date: Sat, 29 Aug 2026 01:17:53 -0400 Subject: [PATCH 1/6] Fix for SWs with same Action= shadowing each other SWs that have the same Action type shadow each other due to pulling which superweapon the player is targeting via `From_Action`, which only returns the first entry that matches the action. This adds new player state that tracks what the last SW the player started targeting and uses that instead. --- code/display.cpp | 12 +++++++++++- code/house.cpp | 4 +++- code/house.h | 5 +++++ code/sidebar.cpp | 8 ++++++-- 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/code/display.cpp b/code/display.cpp index 373ac538..a3943d2e 100644 --- a/code/display.cpp +++ b/code/display.cpp @@ -136,6 +136,7 @@ #include "smudtype.h" #include "sidebar.h" #include "suprtype.h" +#include "super.h" #include "surface.h" #include "tactical.h" #include "tag.h" @@ -1831,6 +1832,7 @@ void DisplayClass::Mouse_Right_Release(Point2D const & point) } else { if (IsTargettingMode != SUPER_NONE) { IsTargettingMode = SUPER_NONE; + PlayerPtr->TargetingSW = NULL; } else { if (IsWaypointMode) { Waypoint_Mode_Control(0); @@ -2427,10 +2429,18 @@ void DisplayClass::Mouse_Left_Release(Coord const & coord, Cell const & cell, Ob } } - SuperWeaponTypeClass *stype = SuperWeaponTypeClass::From_Action(action); + SuperWeaponTypeClass *stype; + if (PlayerPtr->TargetingSW != NULL) { + stype = PlayerPtr->TargetingSW->Class; + } else { + stype = SuperWeaponTypeClass::From_Action(action); + } + if (stype != NULL) { OutList.push_back(EventClass(PlayerPtr->HeapID, EventClass::SPECIAL_PLACE, stype->HeapID, cell)); } + + PlayerPtr->TargetingSW = NULL; } IsTentative = false; diff --git a/code/house.cpp b/code/house.cpp index 9f259989..aff4be9a 100644 --- a/code/house.cpp +++ b/code/house.cpp @@ -355,7 +355,8 @@ HouseClass::HouseClass(HouseTypeClass const * type) : EnemyArmorForcePrediction(0.33f), EnemyAirForcePrediction(0.33f), EnemyInfantryForcePrediction(0.34f), - PowerSurplus(0) + PowerSurplus(0), + TargetingSW(NULL) { int index; @@ -6491,6 +6492,7 @@ void HouseClass::Serialize(SaveStreamClass & stream) stream.Serialize(EnemyAirForcePrediction); stream.Serialize(EnemyInfantryForcePrediction); stream.Serialize(PowerSurplus); + stream.Serialize(TargetingSW); } diff --git a/code/house.h b/code/house.h index 604f5f20..8e78e31b 100644 --- a/code/house.h +++ b/code/house.h @@ -416,6 +416,11 @@ class HouseClass : public AbstractClass */ DynamicVectorClass SuperWeapon; + /* + ** Superweapon class currently in targeting mode for house. + */ + SuperClass * TargetingSW; + /* ** This is a record of the last building that was built. For buildings that ** were built as a part of scenario creation, it will be the last one diff --git a/code/sidebar.cpp b/code/sidebar.cpp index 4ca14117..2eaaaf9f 100644 --- a/code/sidebar.cpp +++ b/code/sidebar.cpp @@ -2146,6 +2146,7 @@ int SidebarClass::StripClass::SelectClass::Action(unsigned flags, KeyNumType & k */ if (flags & RIGHTPRESS) { Map.IsTargettingMode = SUPER_NONE; + PlayerPtr->TargetingSW = NULL; } /* ** A left mouse press signal "activate". If our weapon type is @@ -2154,16 +2155,19 @@ int SidebarClass::StripClass::SelectClass::Action(unsigned flags, KeyNumType & k if (flags & LEFTPRESS) { if ((unsigned)spc < (unsigned)PlayerPtr->SuperWeapon.Count()) { - if (PlayerPtr->SuperWeapon[spc]->Can_Place()) { - if (PlayerPtr->SuperWeapon[spc]->Class->Action == ACTION_NONE) { + SuperClass* curr_sw = PlayerPtr->SuperWeapon[spc]; + if (curr_sw->Can_Place()) { + if (curr_sw->Class->Action == ACTION_NONE) { OutList.push_back(EventClass(PlayerPtr->HeapID, EventClass::SPECIAL_PLACE, PlayerPtr->SuperWeapon[spc]->Class->HeapID, Cell(0, 0))); } else { + PlayerPtr->TargetingSW = curr_sw; Map.IsTargettingMode = spc; Unselect_All(); Speak(VOX_SELECT_TARGET); } } else { PlayerPtr->SuperWeapon[spc]->Impatient_Click(); + PlayerPtr->TargetingSW = NULL; } } } From 15063c8c486ae641f3a614128c9b195c94a4d092 Mon Sep 17 00:00:00 2001 From: Templarfreak Date: Sat, 29 Aug 2026 22:22:12 -0400 Subject: [PATCH 2/6] ow-power, SW being removed, etc Adds clearing TargetingSW in places where IsTargetingMode is already being cleared. --- code/house.cpp | 3 +++ code/init.cpp | 1 + code/super.cpp | 5 +++++ 3 files changed, 9 insertions(+) diff --git a/code/house.cpp b/code/house.cpp index aff4be9a..56b299c2 100644 --- a/code/house.cpp +++ b/code/house.cpp @@ -8728,6 +8728,7 @@ void HouseClass::Update_Present_Super_Weapons(void) if (PlayerPtr != NULL && Fetch_ID() == PlayerPtr->Fetch_ID()) { if (Map.IsTargettingMode == s) { Map.IsTargettingMode = SUPER_NONE; + PlayerPtr->TargetingSW = NULL; } Map.Column[1].Flag_To_Redraw(); } @@ -8738,6 +8739,7 @@ void HouseClass::Update_Present_Super_Weapons(void) if (PlayerPtr != NULL && Fetch_ID() == PlayerPtr->Fetch_ID()) { if (s == Map.IsTargettingMode) { Map.IsTargettingMode = SUPER_NONE; + PlayerPtr->TargetingSW = NULL; } Map.Column[1].Flag_To_Redraw(); } @@ -8748,6 +8750,7 @@ void HouseClass::Update_Present_Super_Weapons(void) if (Fetch_ID() == PlayerPtr->Fetch_ID()) { if (Map.IsTargettingMode == s) { Map.IsTargettingMode = SUPER_NONE; + PlayerPtr->TargetingSW = NULL; } Map.Column[1].Flag_To_Redraw(); } diff --git a/code/init.cpp b/code/init.cpp index ed66b0c6..7e04014d 100644 --- a/code/init.cpp +++ b/code/init.cpp @@ -4927,6 +4927,7 @@ class ManualPlaceCommandClass : public CommandClass // Drop any superweapon cursor, so that placing the building does not return to it. Map.IsTargettingMode = SUPER_NONE; + PlayerPtr->TargetingSW = NULL; PlayerPtr->Manual_Place(builder, (BuildingClass *)pending); } diff --git a/code/super.cpp b/code/super.cpp index 2c204708..227eb6fd 100644 --- a/code/super.cpp +++ b/code/super.cpp @@ -609,6 +609,7 @@ void SuperClass::Place(Cell const & cell, bool player) Drop_Pods(cell); if (player) { Map.IsTargettingMode = SUPER_NONE; + PlayerPtr->TargetingSW = NULL; } House->IsRecalcNeeded = true; break; @@ -619,6 +620,7 @@ void SuperClass::Place(Cell const & cell, bool player) if (coord != COORD_NONE) { if (player) { Map.IsTargettingMode = SUPER_NONE; + PlayerPtr->TargetingSW = NULL; } House->IsRecalcNeeded = true; new IonBlastClass(coord); @@ -656,6 +658,7 @@ void SuperClass::Place(Cell const & cell, bool player) } if (player) { Map.IsTargettingMode = SUPER_NONE; + PlayerPtr->TargetingSW = NULL; } House->IsRecalcNeeded = true; } @@ -698,6 +701,7 @@ void SuperClass::Place(Cell const & cell, bool player) } if (player) { Map.IsTargettingMode = SUPER_NONE; + PlayerPtr->TargetingSW = NULL; } House->IsRecalcNeeded = true; } else { @@ -726,6 +730,7 @@ void SuperClass::Place(Cell const & cell, bool player) } if (player) { Map.IsTargettingMode = SUPER_NONE; + PlayerPtr->TargetingSW = NULL; } House->IsRecalcNeeded = true; } From 00515e544078f55024150f8b7747a97c3823968e Mon Sep 17 00:00:00 2001 From: Templarfreak Date: Sun, 30 Aug 2026 00:02:14 -0400 Subject: [PATCH 3/6] Create fix-sw-shadowing.md --- manual/changes/fix-sw-shadowing.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 manual/changes/fix-sw-shadowing.md diff --git a/manual/changes/fix-sw-shadowing.md b/manual/changes/fix-sw-shadowing.md new file mode 100644 index 00000000..bc0c28e9 --- /dev/null +++ b/manual/changes/fix-sw-shadowing.md @@ -0,0 +1,20 @@ +--- +title: Keep superweapons that share an Action from replacing each other +category: fix +release: 0.2.0 +targets: +- type: system + id: superweapons + effect: changed +credit: [Templarfreak] +--- + +Placing a superweapon on the map now fires the one that was actually armed, even when +another superweapon defines the same Action. Clicking a target cell previously resolved +back to whichever superweapon type first matched that Action, so two superweapons sharing +one Action value could not be fired independently: placing either one always discharged +the same type and left the other's charge untouched. + +The game now remembers the specific superweapon that was armed from the sidebar and fires +that one directly, falling back to the old Action lookup only when nothing was armed +through the sidebar. From 38813ccd2f3c556e58dc61c6543669782ffb5527 Mon Sep 17 00:00:00 2001 From: Templarfreak Date: Wed, 2 Sep 2026 03:23:40 -0400 Subject: [PATCH 4/6] Use IsTargettingMode Instead Much simpler much easier approach, should be easier to work with in the future. --- code/display.cpp | 18 +++++------------- code/house.cpp | 7 +------ code/house.h | 5 ----- code/init.cpp | 1 - code/sidebar.cpp | 8 ++------ code/super.cpp | 5 ----- 6 files changed, 8 insertions(+), 36 deletions(-) diff --git a/code/display.cpp b/code/display.cpp index a3943d2e..af3c467b 100644 --- a/code/display.cpp +++ b/code/display.cpp @@ -136,7 +136,6 @@ #include "smudtype.h" #include "sidebar.h" #include "suprtype.h" -#include "super.h" #include "surface.h" #include "tactical.h" #include "tag.h" @@ -1832,7 +1831,6 @@ void DisplayClass::Mouse_Right_Release(Point2D const & point) } else { if (IsTargettingMode != SUPER_NONE) { IsTargettingMode = SUPER_NONE; - PlayerPtr->TargetingSW = NULL; } else { if (IsWaypointMode) { Waypoint_Mode_Control(0); @@ -2429,18 +2427,12 @@ void DisplayClass::Mouse_Left_Release(Coord const & coord, Cell const & cell, Ob } } - SuperWeaponTypeClass *stype; - if (PlayerPtr->TargetingSW != NULL) { - stype = PlayerPtr->TargetingSW->Class; - } else { - stype = SuperWeaponTypeClass::From_Action(action); - } - - if (stype != NULL) { - OutList.push_back(EventClass(PlayerPtr->HeapID, EventClass::SPECIAL_PLACE, stype->HeapID, cell)); + if (IsTargettingMode != SUPER_NONE) { + SuperWeaponTypeClass *stype = SuperWeaponTypes[IsTargettingMode]; + if (stype != NULL) { + OutList.push_back(EventClass(PlayerPtr->HeapID, EventClass::SPECIAL_PLACE, stype->HeapID, cell)); + } } - - PlayerPtr->TargetingSW = NULL; } IsTentative = false; diff --git a/code/house.cpp b/code/house.cpp index 56b299c2..9f259989 100644 --- a/code/house.cpp +++ b/code/house.cpp @@ -355,8 +355,7 @@ HouseClass::HouseClass(HouseTypeClass const * type) : EnemyArmorForcePrediction(0.33f), EnemyAirForcePrediction(0.33f), EnemyInfantryForcePrediction(0.34f), - PowerSurplus(0), - TargetingSW(NULL) + PowerSurplus(0) { int index; @@ -6492,7 +6491,6 @@ void HouseClass::Serialize(SaveStreamClass & stream) stream.Serialize(EnemyAirForcePrediction); stream.Serialize(EnemyInfantryForcePrediction); stream.Serialize(PowerSurplus); - stream.Serialize(TargetingSW); } @@ -8728,7 +8726,6 @@ void HouseClass::Update_Present_Super_Weapons(void) if (PlayerPtr != NULL && Fetch_ID() == PlayerPtr->Fetch_ID()) { if (Map.IsTargettingMode == s) { Map.IsTargettingMode = SUPER_NONE; - PlayerPtr->TargetingSW = NULL; } Map.Column[1].Flag_To_Redraw(); } @@ -8739,7 +8736,6 @@ void HouseClass::Update_Present_Super_Weapons(void) if (PlayerPtr != NULL && Fetch_ID() == PlayerPtr->Fetch_ID()) { if (s == Map.IsTargettingMode) { Map.IsTargettingMode = SUPER_NONE; - PlayerPtr->TargetingSW = NULL; } Map.Column[1].Flag_To_Redraw(); } @@ -8750,7 +8746,6 @@ void HouseClass::Update_Present_Super_Weapons(void) if (Fetch_ID() == PlayerPtr->Fetch_ID()) { if (Map.IsTargettingMode == s) { Map.IsTargettingMode = SUPER_NONE; - PlayerPtr->TargetingSW = NULL; } Map.Column[1].Flag_To_Redraw(); } diff --git a/code/house.h b/code/house.h index 8e78e31b..604f5f20 100644 --- a/code/house.h +++ b/code/house.h @@ -416,11 +416,6 @@ class HouseClass : public AbstractClass */ DynamicVectorClass SuperWeapon; - /* - ** Superweapon class currently in targeting mode for house. - */ - SuperClass * TargetingSW; - /* ** This is a record of the last building that was built. For buildings that ** were built as a part of scenario creation, it will be the last one diff --git a/code/init.cpp b/code/init.cpp index 7e04014d..ed66b0c6 100644 --- a/code/init.cpp +++ b/code/init.cpp @@ -4927,7 +4927,6 @@ class ManualPlaceCommandClass : public CommandClass // Drop any superweapon cursor, so that placing the building does not return to it. Map.IsTargettingMode = SUPER_NONE; - PlayerPtr->TargetingSW = NULL; PlayerPtr->Manual_Place(builder, (BuildingClass *)pending); } diff --git a/code/sidebar.cpp b/code/sidebar.cpp index 2eaaaf9f..4ca14117 100644 --- a/code/sidebar.cpp +++ b/code/sidebar.cpp @@ -2146,7 +2146,6 @@ int SidebarClass::StripClass::SelectClass::Action(unsigned flags, KeyNumType & k */ if (flags & RIGHTPRESS) { Map.IsTargettingMode = SUPER_NONE; - PlayerPtr->TargetingSW = NULL; } /* ** A left mouse press signal "activate". If our weapon type is @@ -2155,19 +2154,16 @@ int SidebarClass::StripClass::SelectClass::Action(unsigned flags, KeyNumType & k if (flags & LEFTPRESS) { if ((unsigned)spc < (unsigned)PlayerPtr->SuperWeapon.Count()) { - SuperClass* curr_sw = PlayerPtr->SuperWeapon[spc]; - if (curr_sw->Can_Place()) { - if (curr_sw->Class->Action == ACTION_NONE) { + if (PlayerPtr->SuperWeapon[spc]->Can_Place()) { + if (PlayerPtr->SuperWeapon[spc]->Class->Action == ACTION_NONE) { OutList.push_back(EventClass(PlayerPtr->HeapID, EventClass::SPECIAL_PLACE, PlayerPtr->SuperWeapon[spc]->Class->HeapID, Cell(0, 0))); } else { - PlayerPtr->TargetingSW = curr_sw; Map.IsTargettingMode = spc; Unselect_All(); Speak(VOX_SELECT_TARGET); } } else { PlayerPtr->SuperWeapon[spc]->Impatient_Click(); - PlayerPtr->TargetingSW = NULL; } } } diff --git a/code/super.cpp b/code/super.cpp index 227eb6fd..2c204708 100644 --- a/code/super.cpp +++ b/code/super.cpp @@ -609,7 +609,6 @@ void SuperClass::Place(Cell const & cell, bool player) Drop_Pods(cell); if (player) { Map.IsTargettingMode = SUPER_NONE; - PlayerPtr->TargetingSW = NULL; } House->IsRecalcNeeded = true; break; @@ -620,7 +619,6 @@ void SuperClass::Place(Cell const & cell, bool player) if (coord != COORD_NONE) { if (player) { Map.IsTargettingMode = SUPER_NONE; - PlayerPtr->TargetingSW = NULL; } House->IsRecalcNeeded = true; new IonBlastClass(coord); @@ -658,7 +656,6 @@ void SuperClass::Place(Cell const & cell, bool player) } if (player) { Map.IsTargettingMode = SUPER_NONE; - PlayerPtr->TargetingSW = NULL; } House->IsRecalcNeeded = true; } @@ -701,7 +698,6 @@ void SuperClass::Place(Cell const & cell, bool player) } if (player) { Map.IsTargettingMode = SUPER_NONE; - PlayerPtr->TargetingSW = NULL; } House->IsRecalcNeeded = true; } else { @@ -730,7 +726,6 @@ void SuperClass::Place(Cell const & cell, bool player) } if (player) { Map.IsTargettingMode = SUPER_NONE; - PlayerPtr->TargetingSW = NULL; } House->IsRecalcNeeded = true; } From bf367031f8fb15cc82098cb5d1e34488ac2e829b Mon Sep 17 00:00:00 2001 From: Templarfreak Date: Wed, 2 Sep 2026 11:50:59 -0400 Subject: [PATCH 5/6] Update fix-sw-shadowing.md still implied old fix --- manual/changes/fix-sw-shadowing.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/manual/changes/fix-sw-shadowing.md b/manual/changes/fix-sw-shadowing.md index bc0c28e9..aba700a3 100644 --- a/manual/changes/fix-sw-shadowing.md +++ b/manual/changes/fix-sw-shadowing.md @@ -14,7 +14,3 @@ another superweapon defines the same Action. Clicking a target cell previously r back to whichever superweapon type first matched that Action, so two superweapons sharing one Action value could not be fired independently: placing either one always discharged the same type and left the other's charge untouched. - -The game now remembers the specific superweapon that was armed from the sidebar and fires -that one directly, falling back to the old Action lookup only when nothing was armed -through the sidebar. From bd355ab1d298501c4b14162f1b8b856bc6b16d6a Mon Sep 17 00:00:00 2001 From: Templarfreak Date: Thu, 3 Sep 2026 16:33:49 -0400 Subject: [PATCH 6/6] EMP SW fix + documentation changes --- code/display.cpp | 2 +- manual/content/enums/cursor-action.md | 2 +- manual/content/keys/action--superweapontype.md | 4 ---- manual/content/systems/superweapons.md | 2 -- 4 files changed, 2 insertions(+), 8 deletions(-) diff --git a/code/display.cpp b/code/display.cpp index af3c467b..f020cd4c 100644 --- a/code/display.cpp +++ b/code/display.cpp @@ -2429,7 +2429,7 @@ void DisplayClass::Mouse_Left_Release(Coord const & coord, Cell const & cell, Ob if (IsTargettingMode != SUPER_NONE) { SuperWeaponTypeClass *stype = SuperWeaponTypes[IsTargettingMode]; - if (stype != NULL) { + if (stype != NULL && stype->Action == action) { OutList.push_back(EventClass(PlayerPtr->HeapID, EventClass::SPECIAL_PLACE, stype->HeapID, cell)); } } diff --git a/manual/content/enums/cursor-action.md b/manual/content/enums/cursor-action.md index 9be5e4ca..567cbf8a 100644 --- a/manual/content/enums/cursor-action.md +++ b/manual/content/enums/cursor-action.md @@ -66,6 +66,6 @@ values: These are the actions the engine picks between as the cursor crosses the map — what the mouse shape offers and what a left click will then do. They are not the trigger actions a map fires from its tags, which are a separate list. -One rules setting takes a name from here: a SuperWeaponType's [`Action`](/keys/action/). While the map is in that weapon's targeting mode the name becomes the action under the cursor, which is what picks the mouse shape, and it is also the key the click is matched on afterward: a click whose action is neither `None` nor `Select` is compared against every superweapon's `Action` in turn, and the first weapon carrying that action has a firing order queued for the cell. Two weapons naming the same action therefore leave the later one unreachable, and a weapon left at `None` neither takes the cursor nor can be fired by a click at all — which is where an unrecognized name lands it, since the parser reads anything it does not know as `None`. +One rules setting takes a name from here: a SuperWeaponType's [`Action`](/keys/action/). While the map is in that weapon's targeting mode the name becomes the action under the cursor, which is what picks the mouse shape. A weapon left at `None` neither takes the cursor nor can be fired by a click at all — which is where an unrecognized name lands it, since the parser reads anything it does not know as `None`. Seven entries carry historical `DontUse` tokens even though their engine constants still name older actions. Nothing assigns or tests those seven, so what survives of them is the spelling: they are distinct names a superweapon can claim, and nothing more. diff --git a/manual/content/keys/action--superweapontype.md b/manual/content/keys/action--superweapontype.md index 21fc9c1a..22fbc96c 100644 --- a/manual/content/keys/action--superweapontype.md +++ b/manual/content/keys/action--superweapontype.md @@ -15,7 +15,3 @@ The values that carry a superweapon cursor are `Nuke`, `IonCannon`, `DropPod`, ` :::caution[An unrecognized value reads as `None`] A misspelling is not rejected. It resolves to `None`, and the weapon then fires from the cameo click at cell 0,0 with no targeting step and no choice of where the effect lands. ::: - -:::caution[The map click finds the weapon by this value alone] -Every left click that resolves to an action searches the declared superweapons for the first one carrying that `Action=`, whether or not targeting mode was armed. Two sections sharing a value therefore always fire the earlier of the two, and a value that ordinary orders also produce — an attack, a move — discharges a charged weapon on the next such order. -::: diff --git a/manual/content/systems/superweapons.md b/manual/content/systems/superweapons.md index 641d9ffd..93528122 100644 --- a/manual/content/systems/superweapons.md +++ b/manual/content/systems/superweapons.md @@ -195,8 +195,6 @@ Clicking a cameo that can be fired takes one of two paths, decided by [`Action=` While targeting mode is armed, the cursor over the map reports the weapon's `Action=` in place of the ordinary one, and releasing the left button fires the weapon at the cell under the pointer. A right click on the cameo or on the map cancels targeting, and band selection is suppressed while it is armed. The minimap does not accept superweapon actions, so a shot cannot be aimed there. -Every left click that resolves to an action searches the declared list for the first section whose `Action=` matches, whether or not targeting mode was armed and without asking which weapon armed it. Two sections sharing one `Action=` therefore always fire the earlier of the two, and an `Action=` that ordinary orders also produce discharges a charged weapon on the next such order. - :::caution[A misspelled `Action=` becomes `None`] An `Action=` value the engine does not recognize is not rejected; it reads as `None`. The weapon then takes the immediate-fire path: clicking the charged cameo discharges it on the spot at cell 0,0, with no targeting step and no opportunity to choose where the effect lands. :::