From cff708c0b47ef2c039358fb75a2c98f010f3fa3d Mon Sep 17 00:00:00 2001 From: DevelopingTom Date: Wed, 4 Jun 2025 19:18:51 +0200 Subject: [PATCH] SAMs should target only nukes aimed at nearby targets (#1038) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description: Currently, SAMs target any nuke within range. This can be frustrating when a random SAM from another player intercepts your nuke, especially since nukes follow a curved trajectory, leaving little room to adjust their path. This change modifies how SAMs intercept nukes: they will now only target those whose impact points are near the SAM. The “target range” is still generous, allowing players to defend against Hydrogen bombs, while preventing random SAMs to intercept your valued nukes. In this example, the target was the opponent missile silo: https://github.com/user-attachments/assets/0d8be2ac-e04d-44a4-a67e-54836cce8899 ## 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 - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors ## Please put your Discord username so you can be contacted if a bug or regression is found: IngloriousTom --- src/core/execution/SAMLauncherExecution.ts | 17 ++++++- tests/SAM.test.ts | 55 ++++++++++++++++++++-- 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/src/core/execution/SAMLauncherExecution.ts b/src/core/execution/SAMLauncherExecution.ts index 09b5a1d8a..d27737cc0 100644 --- a/src/core/execution/SAMLauncherExecution.ts +++ b/src/core/execution/SAMLauncherExecution.ts @@ -17,6 +17,7 @@ export class SAMLauncherExecution implements Execution { private active: boolean = true; private searchRangeRadius = 80; + private targetRangeRadius = 120; // Nuke's target should be in this range to be focusable // As MIRV go very fast we have to detect them very early but we only // shoot the one targeting very close (MIRVWarheadProtectionRadius) private MIRVWarheadSearchRadius = 400; @@ -44,6 +45,18 @@ export class SAMLauncherExecution implements Execution { this.player = mg.player(this.ownerId); } + private nukeTargetInRange(nuke: Unit) { + const targetTile = nuke.targetTile(); + if (this.sam === null || targetTile === undefined) { + return false; + } + const targetRangeSquared = this.targetRangeRadius * this.targetRangeRadius; + return ( + this.mg.euclideanDistSquared(this.sam.tile(), targetTile) < + targetRangeSquared + ); + } + private getSingleTarget(): Unit | null { if (this.sam === null) return null; const nukes = this.mg @@ -53,7 +66,9 @@ export class SAMLauncherExecution implements Execution { ]) .filter( ({ unit }) => - unit.owner() !== this.player && !this.player.isFriendly(unit.owner()), + unit.owner() !== this.player && + !this.player.isFriendly(unit.owner()) && + this.nukeTargetInRange(unit), ); return ( diff --git a/tests/SAM.test.ts b/tests/SAM.test.ts index d8eb2d325..fc463f960 100644 --- a/tests/SAM.test.ts +++ b/tests/SAM.test.ts @@ -1,3 +1,4 @@ +import { NukeExecution } from "../src/core/execution/NukeExecution"; import { SAMLauncherExecution } from "../src/core/execution/SAMLauncherExecution"; import { SpawnExecution } from "../src/core/execution/SpawnExecution"; import { @@ -13,10 +14,11 @@ import { constructionExecution, executeTicks } from "./util/utils"; let game: Game; let attacker: Player; let defender: Player; +let far_defender: Player; describe("SAM", () => { beforeEach(async () => { - game = await setup("Plains", { infiniteGold: true, instantBuild: true }); + game = await setup("BigPlains", { infiniteGold: true, instantBuild: true }); const defender_info = new PlayerInfo( "us", "defender_id", @@ -24,6 +26,13 @@ describe("SAM", () => { null, "defender_id", ); + const far_defender_info = new PlayerInfo( + "us", + "far_defender_id", + PlayerType.Human, + null, + "far_defender_id", + ); const attacker_info = new PlayerInfo( "fr", "attacker_id", @@ -32,10 +41,15 @@ describe("SAM", () => { "attacker_id", ); game.addPlayer(defender_info); + game.addPlayer(far_defender_info); game.addPlayer(attacker_info); game.addExecution( new SpawnExecution(game.player(defender_info.id).info(), game.ref(1, 1)), + new SpawnExecution( + game.player(far_defender_info.id).info(), + game.ref(199, 1), + ), new SpawnExecution(game.player(attacker_info.id).info(), game.ref(7, 7)), ); @@ -43,8 +57,9 @@ describe("SAM", () => { game.executeNextTick(); } - defender = game.player("defender_id"); attacker = game.player("attacker_id"); + defender = game.player("defender_id"); + far_defender = game.player("far_defender_id"); constructionExecution(game, attacker.id(), 7, 7, UnitType.MissileSilo); }); @@ -52,7 +67,9 @@ describe("SAM", () => { test("one sam should take down one nuke", async () => { const sam = defender.buildUnit(UnitType.SAMLauncher, game.ref(1, 1), {}); game.addExecution(new SAMLauncherExecution(defender.id(), null, sam)); - attacker.buildUnit(UnitType.AtomBomb, game.ref(1, 1), {}); + attacker.buildUnit(UnitType.AtomBomb, game.ref(1, 1), { + targetTile: game.ref(2, 1), + }); executeTicks(game, 3); @@ -112,4 +129,36 @@ describe("SAM", () => { expect(nuke.isActive()).toBeFalsy(); expect([sam1, sam2].filter((s) => s.isInCooldown())).toHaveLength(1); }); + + test("SAMs should target only nukes aimed at nearby targets", async () => { + const targetDistance = 199; + // Close SAM: should not intercept anything + const sam1 = defender.buildUnit(UnitType.SAMLauncher, game.ref(1, 1), {}); + game.addExecution(new SAMLauncherExecution(defender.id(), null, sam1)); + + // Far SAM: Should intercept the nuke. Use the far_defender so the SAM can be built + const sam2 = far_defender.buildUnit( + UnitType.SAMLauncher, + game.ref(targetDistance, 1), + {}, + ); + game.addExecution(new SAMLauncherExecution(far_defender.id(), null, sam2)); + + const nukeExecution = new NukeExecution( + UnitType.AtomBomb, + attacker.id(), + game.ref(targetDistance, 1), + null, + ); + game.addExecution(nukeExecution); + // Long distance nuke: compute the proper number of ticks + const ticksToExecute = Math.ceil( + targetDistance / game.config().defaultNukeSpeed(), + ); + executeTicks(game, ticksToExecute); + + expect(nukeExecution.isActive()).toBeFalsy(); + expect(sam1.isInCooldown()).toBeFalsy(); + expect(sam2.isInCooldown()).toBeTruthy(); + }); });