From 7bf6bfd4b995990cffe1dc836f4b0168bef3f8dd Mon Sep 17 00:00:00 2001
From: Abdallah Bahrawi <140177728+abdallahbahrawi1@users.noreply.github.com>
Date: Wed, 18 Feb 2026 01:12:56 +0200
Subject: [PATCH] =?UTF-8?q?=F0=9F=94=B4=20Major=20SAM=20Targeting=20Fix=20?=
=?UTF-8?q?(#3223)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Description:
Bug (before): In multi-nuke scenarios (e.g., 6 incoming nukes), SAMs,
when stacked, could behave like they only engage one nuke at a time,
letting other nukes slip through and wipe the SAM stack. This was most
noticeable when nukes entered from certain angles/entry sides (from what
I’ve noticed, it happens more often in the 2 o’clock to 6 o’clock
direction).
What was fixed:
1- Removed permanent "unreachable" caching: When a nuke was out of SAM
range on first evaluation, it was cached as null (permanently
unreachable) and never re-evaluated. Since nukes move each tick, they
could later enter interception range but would be ignored. The fix
removes this permanent cache, so nukes are re-evaluated every tick.
2 - Moved targetedBySAM filter into the targeting system: The
targetedBySAM() check was at the launch decision, if the
highest-priority nuke was already claimed by another SAM, the launcher
skipped firing entirely instead of falling back to the next best target.
The fix moves the filter inside getSingleTarget() so claimed nukes are
excluded before ranking.
3 - Cleared targetedBySAM flag on SAM missile abort: When a SAM missile
aborted (for example target became allied), the targetedBySAM flag was
never cleared. This permanently prevented all other SAMs from targeting
that nuke. The fix calls setTargetedBySAM(false) on abort so the nuke
becomes available for re-targeting.
**Videos:**
I uploaded a before/after clip showing SAM performance intercepting 6
nukes, before the fix, nukes could break through, but after the fix,
SAMs consistently intercept as expected.
Before:
https://github.com/user-attachments/assets/d5a85354-f35c-4aca-82f8-902f5966312b
after:
https://github.com/user-attachments/assets/54074c09-fbdf-44d5-a88c-b1d54b20fee2
- Deployed for further testing
## Please complete the following:
- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
## Please put your Discord username so you can be contacted if a bug or
regression is found:
abodcraft1
---------
Co-authored-by: Ryan <7389646+ryanbarlow97@users.noreply.github.com>
---
src/core/execution/SAMLauncherExecution.ts | 13 +++++++------
src/core/execution/SAMMissileExecution.ts | 4 ++++
2 files changed, 11 insertions(+), 6 deletions(-)
diff --git a/src/core/execution/SAMLauncherExecution.ts b/src/core/execution/SAMLauncherExecution.ts
index c723df0d8..d7d55aafa 100644
--- a/src/core/execution/SAMLauncherExecution.ts
+++ b/src/core/execution/SAMLauncherExecution.ts
@@ -26,8 +26,8 @@ type InterceptionTile = {
*/
class SAMTargetingSystem {
// Interception tiles are computed a single time, but it may not be reachable yet.
- // Store the result so it can be intercepted at the proper time, rather than recomputing each ticks
- // Null interception tile means there are no interception tiles in range. Store it to
+ // Store the result so it can be intercepted at the proper time, rather than recomputing each tick.
+ // Null interception tile means there are no interception tiles in range. Store it to avoid recomputing.
private readonly precomputedNukes: Map =
new Map();
private readonly missileSpeed: number;
@@ -91,7 +91,8 @@ class SAMTargetingSystem {
return (
isUnit(unit) &&
unit.owner() !== this.sam.owner() &&
- !this.sam.owner().isFriendly(unit.owner())
+ !this.sam.owner().isFriendly(unit.owner()) &&
+ !unit.targetedBySAM()
);
},
);
@@ -105,7 +106,7 @@ class SAMTargetingSystem {
const cached = this.precomputedNukes.get(nukeId);
if (cached !== undefined) {
if (cached === null) {
- // Known unreachable, skip.
+ // Already computed as unreachable, skip
continue;
}
if (cached.tick === ticks) {
@@ -259,8 +260,8 @@ export class SAMLauncherExecution implements Execution {
}
}
- const isSingleTarget = target && !target.unit.targetedBySAM();
- if (isSingleTarget || mirvWarheadTargets.length > 0) {
+ // target is already filtered to exclude nukes targeted by other SAMs
+ if (target || mirvWarheadTargets.length > 0) {
this.sam.launch();
const type =
mirvWarheadTargets.length > 0
diff --git a/src/core/execution/SAMMissileExecution.ts b/src/core/execution/SAMMissileExecution.ts
index 7fb4346ce..657166a80 100644
--- a/src/core/execution/SAMMissileExecution.ts
+++ b/src/core/execution/SAMMissileExecution.ts
@@ -50,6 +50,10 @@ export class SAMMissileExecution implements Execution {
this.target.owner() === this.SAMMissile.owner() ||
!nukesWhitelist.includes(this.target.type())
) {
+ // Clear the flag so other SAMs can re-target this nuke
+ if (this.target.isActive()) {
+ this.target.setTargetedBySAM(false);
+ }
this.SAMMissile.delete(false);
this.active = false;
return;