Files
DevelopingTom bd820425ba SAMs should target only nukes aimed at nearby targets (#1038)
## 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
2025-06-04 10:18:51 -07:00

165 lines
5.1 KiB
TypeScript

import { NukeExecution } from "../src/core/execution/NukeExecution";
import { SAMLauncherExecution } from "../src/core/execution/SAMLauncherExecution";
import { SpawnExecution } from "../src/core/execution/SpawnExecution";
import {
Game,
Player,
PlayerInfo,
PlayerType,
UnitType,
} from "../src/core/game/Game";
import { setup } from "./util/Setup";
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("BigPlains", { infiniteGold: true, instantBuild: true });
const defender_info = new PlayerInfo(
"us",
"defender_id",
PlayerType.Human,
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",
PlayerType.Human,
null,
"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)),
);
while (game.inSpawnPhase()) {
game.executeNextTick();
}
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);
});
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), {
targetTile: game.ref(2, 1),
});
executeTicks(game, 3);
expect(attacker.units(UnitType.AtomBomb)).toHaveLength(0);
});
test("sam should only get one nuke at a time", 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(2, 1), {
targetTile: game.ref(2, 1),
});
attacker.buildUnit(UnitType.AtomBomb, game.ref(1, 2), {
targetTile: game.ref(1, 2),
});
expect(attacker.units(UnitType.AtomBomb)).toHaveLength(2);
executeTicks(game, 3);
expect(attacker.units(UnitType.AtomBomb)).toHaveLength(1);
});
test("sam should cooldown as long as configured", async () => {
const sam = defender.buildUnit(UnitType.SAMLauncher, game.ref(1, 1), {});
game.addExecution(new SAMLauncherExecution(defender.id(), null, sam));
expect(sam.isInCooldown()).toBeFalsy();
const nuke = attacker.buildUnit(UnitType.AtomBomb, game.ref(1, 2), {
targetTile: game.ref(1, 2),
});
executeTicks(game, 3);
expect(nuke.isActive()).toBeFalsy();
for (let i = 0; i < game.config().SAMCooldown() - 3; i++) {
game.executeNextTick();
expect(sam.isInCooldown()).toBeTruthy();
}
executeTicks(game, 2);
expect(sam.isInCooldown()).toBeFalsy();
});
test("two sams should not target twice same nuke", async () => {
const sam1 = defender.buildUnit(UnitType.SAMLauncher, game.ref(1, 1), {
cooldownDuration: 10,
});
game.addExecution(new SAMLauncherExecution(defender.id(), null, sam1));
const sam2 = defender.buildUnit(UnitType.SAMLauncher, game.ref(1, 2), {});
game.addExecution(new SAMLauncherExecution(defender.id(), null, sam2));
const nuke = attacker.buildUnit(UnitType.AtomBomb, game.ref(2, 2), {
targetTile: game.ref(2, 2),
});
executeTicks(game, 3);
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();
});
});