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(); + }); });