From bcdc2126c66c38cf1588ebd089db0a3dfcbc9435 Mon Sep 17 00:00:00 2001 From: Evan Date: Sat, 2 May 2026 09:05:10 -0600 Subject: [PATCH] bugfix: SAM was only reloading when not in cooldown (#3817) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description: reloadMissile() was inside the isInCooldown() block. For level-2+ SAMs, isInCooldown() returns queue.length === level, so after firing one of two missiles (queue.length = 1 < level = 2) the SAM is not in cooldown — meaning expired timers were never cleaned up. Stale queue entries caused subsequent shots to be treated as still-cooling even after the cooldown elapsed. SAM execution now mirrors the missile silo execution ## 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: evan --- src/core/execution/SAMLauncherExecution.ts | 27 +++++++++---------- .../executions/SAMLauncherExecution.test.ts | 18 +++++++++++++ 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/src/core/execution/SAMLauncherExecution.ts b/src/core/execution/SAMLauncherExecution.ts index 21a3e26de..c27328f45 100644 --- a/src/core/execution/SAMLauncherExecution.ts +++ b/src/core/execution/SAMLauncherExecution.ts @@ -247,20 +247,6 @@ export class SAMLauncherExecution implements Execution { return; } - if (this.sam.isInCooldown()) { - const frontTime = this.sam.missileTimerQueue()[0]; - if (frontTime === undefined) { - return; - } - const cooldown = - this.mg.config().SAMCooldown() - (this.mg.ticks() - frontTime); - - if (cooldown <= 0) { - this.sam.reloadMissile(); - } - return; - } - if (!this.sam.isActive()) { this.active = false; return; @@ -270,6 +256,19 @@ export class SAMLauncherExecution implements Execution { this.player = this.sam.owner(); } + const frontTime = this.sam.missileTimerQueue()[0]; + if (frontTime !== undefined) { + const cooldown = + this.mg.config().SAMCooldown() - (this.mg.ticks() - frontTime); + + if (cooldown <= 0) { + this.sam.reloadMissile(); + } + } + if (this.sam.isInCooldown()) { + return; + } + this.pseudoRandom ??= new PseudoRandom(this.sam.id()); const mirvWarheadTargets = this.mg.nearbyUnits( diff --git a/tests/core/executions/SAMLauncherExecution.test.ts b/tests/core/executions/SAMLauncherExecution.test.ts index 6916761d1..a1459c27d 100644 --- a/tests/core/executions/SAMLauncherExecution.test.ts +++ b/tests/core/executions/SAMLauncherExecution.test.ts @@ -254,4 +254,22 @@ describe("SAM", () => { expect(defender.units(UnitType.SAMLauncher)[0].level()).toEqual(2); }); + + test("SAM should reload expired missile timers even when not in cooldown", async () => { + // Upgrading to level 2 pushes a timer for the new slot. queue.length(1) < level(2) + // so isInCooldown() is false, but the expired timer still needs to be cleaned up. + const sam = defender.buildUnit(UnitType.SAMLauncher, game.ref(1, 1), {}); + sam.increaseLevel(); + expect(sam.level()).toBe(2); + + game.addExecution(new SAMLauncherExecution(defender, null, sam)); + + expect(sam.missileTimerQueue()).toHaveLength(1); + expect(sam.isInCooldown()).toBeFalsy(); + + // Wait for the timer to expire — reload must fire even though isInCooldown() is false + executeTicks(game, game.config().SAMCooldown() + 1); + + expect(sam.missileTimerQueue()).toHaveLength(0); + }); });