From b2ba37e0abbab5ba70d3c839320087a570976e1e Mon Sep 17 00:00:00 2001
From: Achim Marius <67611764+plazmaezio@users.noreply.github.com>
Date: Fri, 16 Jan 2026 06:09:01 +0200
Subject: [PATCH] Destroy incoming nukes when alliance is created (#2716)
Resolves #2484
## Description:
- When an alliance is created between two players, any incoming nukes
between them are destroyed mid-air.
This prevents the traitor debuff from being applied on impact, even if
the nukes were launched before the alliance was formed.
- If a player has launched nukes at multiple nations, only the nukes
targeting the newly allied nation are destroyed.
This is what the players will see after the alliance is created (in case
they have launched nukes at each other):
## 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:
assessin.
---
resources/lang/en.json | 2 +
.../alliance/AllianceRequestReplyExecution.ts | 66 ++++++-
tests/AllianceAcceptNukes.test.ts | 163 ++++++++++++++++++
3 files changed, 230 insertions(+), 1 deletion(-)
create mode 100644 tests/AllianceAcceptNukes.test.ts
diff --git a/resources/lang/en.json b/resources/lang/en.json
index c18d7be1a..da453ae7b 100644
--- a/resources/lang/en.json
+++ b/resources/lang/en.json
@@ -699,6 +699,8 @@
"alliance_request_status": "{name} {status} your alliance request",
"alliance_accepted": "accepted",
"alliance_rejected": "rejected",
+ "alliance_nukes_destroyed_outgoing": "{count, plural, one {# nuke launched toward {name} was destroyed due to the alliance} other {# nukes launched toward {name} were destroyed due to the alliance}}",
+ "alliance_nukes_destroyed_incoming": "{count, plural, one {# nuke launched by {name} was destroyed due to the alliance} other {# nukes launched by {name} were destroyed due to the alliance}}",
"duration_second": "1 second",
"betrayal_description": "You broke your alliance with {name}, making you a TRAITOR ({malusPercent}% defense debuff for {durationText})",
"duration_seconds_plural": "{seconds} seconds",
diff --git a/src/core/execution/alliance/AllianceRequestReplyExecution.ts b/src/core/execution/alliance/AllianceRequestReplyExecution.ts
index b1a79fc44..35ec2ef69 100644
--- a/src/core/execution/alliance/AllianceRequestReplyExecution.ts
+++ b/src/core/execution/alliance/AllianceRequestReplyExecution.ts
@@ -1,4 +1,11 @@
-import { Execution, Game, Player, PlayerID } from "../../game/Game";
+import {
+ Execution,
+ Game,
+ MessageType,
+ Player,
+ PlayerID,
+ UnitType,
+} from "../../game/Game";
export class AllianceRequestReplyExecution implements Execution {
private active = true;
@@ -10,6 +17,57 @@ export class AllianceRequestReplyExecution implements Execution {
private accept: boolean,
) {}
+ private cancelNukesBetweenAlliedPlayers(
+ mg: Game,
+ p1: Player,
+ p2: Player,
+ ): void {
+ const neutralized = new Map();
+
+ const players = [p1, p2];
+
+ for (const launcher of players) {
+ for (const unit of launcher.units(
+ UnitType.AtomBomb,
+ UnitType.HydrogenBomb,
+ )) {
+ if (!unit.isActive() || unit.reachedTarget()) continue;
+
+ const targetTile = unit.targetTile();
+ if (!targetTile) continue;
+
+ const targetOwner = mg.owner(targetTile);
+ if (!targetOwner.isPlayer()) continue;
+
+ const other = launcher === p1 ? p2 : p1;
+ if (targetOwner !== other) continue;
+
+ unit.delete(false);
+ neutralized.set(launcher, (neutralized.get(launcher) ?? 0) + 1);
+ }
+ }
+
+ for (const [launcher, count] of neutralized) {
+ const other = launcher === p1 ? p2 : p1;
+
+ mg.displayMessage(
+ "events_display.alliance_nukes_destroyed_outgoing",
+ MessageType.ALLIANCE_ACCEPTED,
+ launcher.id(),
+ undefined,
+ { name: other.displayName(), count },
+ );
+
+ mg.displayMessage(
+ "events_display.alliance_nukes_destroyed_incoming",
+ MessageType.ALLIANCE_ACCEPTED,
+ other.id(),
+ undefined,
+ { name: launcher.displayName(), count },
+ );
+ }
+ }
+
init(mg: Game, ticks: number): void {
if (!mg.hasPlayer(this.requestorID)) {
console.warn(
@@ -33,6 +91,12 @@ export class AllianceRequestReplyExecution implements Execution {
request.accept();
this.requestor.updateRelation(this.recipient, 100);
this.recipient.updateRelation(this.requestor, 100);
+
+ this.cancelNukesBetweenAlliedPlayers(
+ mg,
+ this.requestor,
+ this.recipient,
+ );
} else {
request.reject();
}
diff --git a/tests/AllianceAcceptNukes.test.ts b/tests/AllianceAcceptNukes.test.ts
new file mode 100644
index 000000000..d3da89172
--- /dev/null
+++ b/tests/AllianceAcceptNukes.test.ts
@@ -0,0 +1,163 @@
+import { AllianceRequestReplyExecution } from "src/core/execution/alliance/AllianceRequestReplyExecution";
+import { GameUpdateType } from "src/core/game/GameUpdates";
+import { NukeExecution } from "../src/core/execution/NukeExecution";
+import {
+ Game,
+ Player,
+ PlayerInfo,
+ PlayerType,
+ UnitType,
+} from "../src/core/game/Game";
+import { setup } from "./util/Setup";
+import { TestConfig } from "./util/TestConfig";
+
+let game: Game;
+let player1: Player;
+let player2: Player;
+let player3: Player;
+
+describe("Alliance acceptance immediately destroys in-flight nukes", () => {
+ beforeEach(async () => {
+ game = await setup(
+ "plains",
+ {
+ infiniteGold: true,
+ instantBuild: true,
+ infiniteTroops: true,
+ },
+ [
+ new PlayerInfo("player1", PlayerType.Human, "c1", "p1"),
+ new PlayerInfo("player2", PlayerType.Human, "c2", "p2"),
+ new PlayerInfo("player3", PlayerType.Human, "c3", "p3"),
+ ],
+ );
+
+ (game.config() as TestConfig).nukeAllianceBreakThreshold = () => 0;
+
+ while (game.inSpawnPhase()) {
+ game.executeNextTick();
+ }
+
+ player1 = game.player("p1");
+ player2 = game.player("p2");
+ player3 = game.player("p3");
+
+ player1.conquer(game.ref(0, 0));
+ player2.conquer(game.ref(5, 5));
+ player3.conquer(game.ref(10, 10));
+
+ player1.buildUnit(UnitType.MissileSilo, game.ref(0, 0), {});
+ });
+
+ test("accepting alliance destroys in-flight nukes between the newly allied players", () => {
+ game.addExecution(
+ new NukeExecution(
+ UnitType.AtomBomb,
+ player1,
+ game.ref(5, 5),
+ game.ref(0, 0),
+ -1,
+ 5,
+ ),
+ );
+
+ game.executeNextTick(); // init
+ game.executeNextTick(); // spawn nuke
+
+ expect(game.units(UnitType.AtomBomb)).toHaveLength(1);
+
+ expect(player2.isAlliedWith(player1)).toBe(false);
+ expect(player1.isFriendly(player2)).toBe(false);
+
+ player2.createAllianceRequest(player1);
+ game.addExecution(
+ new AllianceRequestReplyExecution(player2.id(), player1, true),
+ );
+
+ game.executeNextTick();
+
+ expect(player2.isAlliedWith(player1)).toBe(true);
+ expect(player1.isFriendly(player2)).toBe(true);
+
+ expect(game.units(UnitType.AtomBomb)).toHaveLength(0);
+ });
+
+ test("accepting alliance destroys only nukes between allied players", () => {
+ player1.buildUnit(UnitType.MissileSilo, game.ref(0, 0), {});
+
+ game.addExecution(
+ new NukeExecution(UnitType.AtomBomb, player1, game.ref(5, 5), null),
+ );
+ game.addExecution(
+ new NukeExecution(UnitType.AtomBomb, player1, game.ref(10, 10), null),
+ );
+
+ game.executeNextTick(); // init
+ game.executeNextTick(); // spawn nukes
+
+ expect(game.units(UnitType.AtomBomb)).toHaveLength(2);
+
+ expect(player2.isAlliedWith(player1)).toBe(false);
+ expect(player1.isFriendly(player2)).toBe(false);
+
+ player1.createAllianceRequest(player2);
+ game.addExecution(
+ new AllianceRequestReplyExecution(player1.id(), player2, true),
+ );
+
+ game.executeNextTick();
+
+ expect(player2.isAlliedWith(player1)).toBe(true);
+ expect(player1.isFriendly(player2)).toBe(true);
+
+ expect(game.units(UnitType.AtomBomb)).toHaveLength(1);
+
+ // Ensure remaining nuke targets player3
+ const remainingNuke = game.units(UnitType.AtomBomb)[0];
+ expect(remainingNuke.targetTile()).toBe(game.ref(10, 10));
+ });
+
+ test("accepting alliance displays a nuke-cancellation display message", () => {
+ game.addExecution(
+ new NukeExecution(
+ UnitType.AtomBomb,
+ player1,
+ game.ref(5, 5),
+ game.ref(0, 0),
+ -1,
+ 5,
+ ),
+ );
+
+ game.executeNextTick(); // init
+ game.executeNextTick(); // spawn nuke
+
+ expect(game.units(UnitType.AtomBomb)).toHaveLength(1);
+
+ expect(player2.isAlliedWith(player1)).toBe(false);
+ expect(player1.isFriendly(player2)).toBe(false);
+
+ player2.createAllianceRequest(player1);
+ game.addExecution(
+ new AllianceRequestReplyExecution(player2.id(), player1, true),
+ );
+
+ const updates = game.executeNextTick();
+
+ expect(player2.isAlliedWith(player1)).toBe(true);
+ expect(player1.isFriendly(player2)).toBe(true);
+
+ expect(game.units(UnitType.AtomBomb)).toHaveLength(0);
+
+ const messages =
+ updates[GameUpdateType.DisplayEvent]?.map((e) => e.message) ?? [];
+
+ expect(
+ messages.some(
+ (m) =>
+ m === "events_display.alliance_nukes_destroyed_outgoing" ||
+ m === "events_display.alliance_nukes_destroyed_incoming",
+ ),
+ ).toBe(true);
+ });
+});