mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 12:00:44 +00:00
6c8ce958b2
## Description: ### Problem PVP immunity (the extended spawn immunity setting) was incorrectly preventing AI nations from attacking human players. The intent of PVP immunity is to protect human-vs-human combat only, but nations were subject to the same restriction. ### Root Cause In `canAttackPlayer()`, only `PlayerType.Bot` was exempt from checking target immunity. Nations fell through to the same path as humans, so when a nation tried to attack an immune human, `player.isImmune()` returned true and the attack was blocked. ### Fix Changed the immunity bypass condition from `this.type() === PlayerType.Bot` to `this.type() !== PlayerType.Human`. Now only human attackers check target immunity. Both bots and nations bypass it (they only check alliance status). This does not affect nation spawn immunity (`nationSpawnImmunityDuration`), which is a separate mechanism that protects newly spawned nations from all attackers and continues to work independently. ## 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 ## Please put your Discord username so you can be contacted if a bug or regression is found: FloPinguin
624 lines
20 KiB
TypeScript
624 lines
20 KiB
TypeScript
import { AttackExecution } from "../src/core/execution/AttackExecution";
|
|
import { SpawnExecution } from "../src/core/execution/SpawnExecution";
|
|
import { TransportShipExecution } from "../src/core/execution/TransportShipExecution";
|
|
import {
|
|
Game,
|
|
Player,
|
|
PlayerInfo,
|
|
PlayerType,
|
|
UnitType,
|
|
} from "../src/core/game/Game";
|
|
import { TileRef } from "../src/core/game/GameMap";
|
|
import { GameID } from "../src/core/Schemas";
|
|
import { setup } from "./util/Setup";
|
|
import { TestConfig } from "./util/TestConfig";
|
|
import { constructionExecution } from "./util/utils";
|
|
|
|
let game: Game;
|
|
const gameID: GameID = "game_id";
|
|
let attacker: Player;
|
|
let defender: Player;
|
|
let defenderSpawn: TileRef;
|
|
let attackerSpawn: TileRef;
|
|
|
|
function sendBoat(target: TileRef, troops: number) {
|
|
game.addExecution(new TransportShipExecution(defender, target, troops));
|
|
}
|
|
|
|
const immunityPhaseTicks = 10;
|
|
function waitForImmunityToEnd() {
|
|
for (let i = 0; i < immunityPhaseTicks + 1; i++) {
|
|
game.executeNextTick();
|
|
}
|
|
}
|
|
|
|
describe("Attack", () => {
|
|
beforeEach(async () => {
|
|
game = await setup("ocean_and_land", {
|
|
infiniteGold: true,
|
|
instantBuild: true,
|
|
infiniteTroops: true,
|
|
});
|
|
const attackerInfo = new PlayerInfo(
|
|
"attacker dude",
|
|
PlayerType.Human,
|
|
null,
|
|
"attacker_id",
|
|
);
|
|
game.addPlayer(attackerInfo);
|
|
const defenderInfo = new PlayerInfo(
|
|
"defender dude",
|
|
PlayerType.Human,
|
|
null,
|
|
"defender_id",
|
|
);
|
|
game.addPlayer(defenderInfo);
|
|
|
|
defenderSpawn = game.ref(0, 15);
|
|
attackerSpawn = game.ref(0, 10);
|
|
|
|
game.addExecution(
|
|
new SpawnExecution(
|
|
gameID,
|
|
game.player(attackerInfo.id).info(),
|
|
attackerSpawn,
|
|
),
|
|
new SpawnExecution(
|
|
gameID,
|
|
game.player(defenderInfo.id).info(),
|
|
defenderSpawn,
|
|
),
|
|
);
|
|
game.executeNextTick();
|
|
game.executeNextTick();
|
|
|
|
attacker = game.player(attackerInfo.id);
|
|
defender = game.player(defenderInfo.id);
|
|
|
|
game.addExecution(
|
|
new AttackExecution(100, defender, game.terraNullius().id()),
|
|
);
|
|
game.executeNextTick();
|
|
while (defender.outgoingAttacks().length > 0) {
|
|
game.executeNextTick();
|
|
}
|
|
|
|
(game.config() as TestConfig).setDefaultNukeSpeed(50);
|
|
});
|
|
|
|
test("Nuke reduce attacking troop counts", async () => {
|
|
// Not building exactly spawn to it's better protected from attacks (but still
|
|
// on defender territory)
|
|
constructionExecution(game, defender, 1, 1, UnitType.MissileSilo);
|
|
expect(defender.units(UnitType.MissileSilo)).toHaveLength(1);
|
|
game.addExecution(new AttackExecution(100, attacker, defender.id()));
|
|
constructionExecution(game, defender, 0, 15, UnitType.AtomBomb, 3);
|
|
const nuke = defender.units(UnitType.AtomBomb)[0];
|
|
expect(nuke.isActive()).toBe(true);
|
|
|
|
expect(attacker.outgoingAttacks()).toHaveLength(1);
|
|
expect(attacker.outgoingAttacks()[0].troops()).toBe(98);
|
|
|
|
// Make the nuke go kaboom
|
|
game.executeNextTick();
|
|
expect(nuke.isActive()).toBe(false);
|
|
expect(attacker.outgoingAttacks()[0].troops()).not.toBe(97);
|
|
expect(attacker.outgoingAttacks()[0].troops()).toBeLessThan(90);
|
|
});
|
|
|
|
test("Nuke reduce attacking boat troop count", async () => {
|
|
constructionExecution(game, defender, 1, 1, UnitType.MissileSilo);
|
|
expect(defender.units(UnitType.MissileSilo)).toHaveLength(1);
|
|
|
|
sendBoat(game.ref(15, 8), 100);
|
|
|
|
constructionExecution(game, defender, 0, 15, UnitType.AtomBomb, 3);
|
|
const nuke = defender.units(UnitType.AtomBomb)[0];
|
|
expect(nuke.isActive()).toBe(true);
|
|
|
|
const ship = defender.units(UnitType.TransportShip)[0];
|
|
expect(ship.troops()).toBe(100);
|
|
|
|
game.executeNextTick();
|
|
|
|
expect(nuke.isActive()).toBe(false);
|
|
expect(defender.units(UnitType.TransportShip)[0].troops()).toBeLessThan(90);
|
|
});
|
|
|
|
test("Boat penalty on retreat Transport Ship arrival", async () => {
|
|
const player_start_troops = defender.troops();
|
|
const boat_troops = player_start_troops * 0.5;
|
|
|
|
sendBoat(game.ref(15, 8), boat_troops);
|
|
|
|
game.executeNextTick();
|
|
|
|
const ship = defender.units(UnitType.TransportShip)[0];
|
|
expect(ship.troops()).toBe(boat_troops);
|
|
expect(ship.isActive()).toBe(true);
|
|
|
|
ship.updateTransportShipState({ isRetreating: true });
|
|
game.executeNextTick();
|
|
|
|
expect(ship.isActive()).toBe(false);
|
|
expect(boat_troops).toBeLessThan(defender.troops());
|
|
expect(defender.troops()).toBeLessThan(player_start_troops);
|
|
});
|
|
});
|
|
|
|
let playerA: Player;
|
|
let playerB: Player;
|
|
|
|
function addPlayerToGame(
|
|
playerInfo: PlayerInfo,
|
|
game: Game,
|
|
tile: TileRef,
|
|
): Player {
|
|
game.addPlayer(playerInfo);
|
|
game.addExecution(new SpawnExecution(gameID, playerInfo, tile));
|
|
return game.player(playerInfo.id);
|
|
}
|
|
|
|
describe("Attack race condition with alliance requests", () => {
|
|
beforeEach(async () => {
|
|
game = await setup("ocean_and_land", {
|
|
infiniteGold: true,
|
|
instantBuild: true,
|
|
infiniteTroops: true,
|
|
});
|
|
|
|
const playerAInfo = new PlayerInfo(
|
|
"playerA",
|
|
PlayerType.Human,
|
|
null,
|
|
"playerA_id",
|
|
);
|
|
playerA = addPlayerToGame(playerAInfo, game, game.ref(0, 10));
|
|
|
|
const playerBInfo = new PlayerInfo(
|
|
"playerB",
|
|
PlayerType.Human,
|
|
null,
|
|
"playerB_id",
|
|
);
|
|
playerB = addPlayerToGame(playerBInfo, game, game.ref(0, 11));
|
|
game.executeNextTick();
|
|
game.executeNextTick();
|
|
});
|
|
|
|
it("Should not mark attacker as traitor when alliance is formed after attack starts", async () => {
|
|
// Player A sends alliance request to Player B
|
|
const allianceRequest = playerA.createAllianceRequest(playerB);
|
|
expect(allianceRequest).not.toBeNull();
|
|
|
|
// Player A attacks Player B
|
|
const attackExecution = new AttackExecution(
|
|
null,
|
|
playerA,
|
|
playerB.id(),
|
|
null,
|
|
);
|
|
game.addExecution(attackExecution);
|
|
|
|
// Player B counter-attacks Player A
|
|
const counterAttackExecution = new AttackExecution(
|
|
null,
|
|
playerB,
|
|
playerA.id(),
|
|
null,
|
|
);
|
|
|
|
// Player B accepts the alliance request
|
|
if (allianceRequest) {
|
|
allianceRequest.accept();
|
|
}
|
|
|
|
game.addExecution(counterAttackExecution);
|
|
|
|
// Execute a few ticks to process the attacks
|
|
for (let i = 0; i < 5; i++) {
|
|
game.executeNextTick();
|
|
}
|
|
|
|
expect(playerA.isAlive()).toBe(true);
|
|
expect(playerB.isAlive()).toBe(true);
|
|
|
|
// Player A should not be marked as traitor because the alliance was formed after the attack started
|
|
expect(playerA.isTraitor()).toBe(false);
|
|
|
|
expect(playerA.isAlliedWith(playerB)).toBe(true);
|
|
expect(playerB.isAlliedWith(playerA)).toBe(true);
|
|
// The attacks should have retreated due to the alliance being formed
|
|
expect(playerA.outgoingAttacks()).toHaveLength(0);
|
|
expect(playerB.outgoingAttacks()).toHaveLength(0);
|
|
});
|
|
|
|
it("Should prevent player from attacking allied player", async () => {
|
|
// Create an alliance between Player A and Player B
|
|
const allianceRequest = playerA.createAllianceRequest(playerB);
|
|
if (allianceRequest) {
|
|
allianceRequest.accept();
|
|
}
|
|
|
|
// Verify alliance exists
|
|
expect(playerA.isAlliedWith(playerB)).toBe(true);
|
|
expect(playerB.isAlliedWith(playerA)).toBe(true);
|
|
|
|
// Player A tries to attack Player B (should be blocked)
|
|
const attackExecution = new AttackExecution(
|
|
null,
|
|
playerA,
|
|
playerB.id(),
|
|
null,
|
|
);
|
|
game.addExecution(attackExecution);
|
|
|
|
// Execute a few ticks to process the attack
|
|
for (let i = 0; i < 10; i++) {
|
|
game.executeNextTick();
|
|
}
|
|
|
|
// No ongoing attacks should exist for either side
|
|
expect(playerA.outgoingAttacks()).toHaveLength(0);
|
|
expect(playerB.outgoingAttacks()).toHaveLength(0);
|
|
expect(playerA.incomingAttacks()).toHaveLength(0);
|
|
expect(playerB.incomingAttacks()).toHaveLength(0);
|
|
});
|
|
|
|
test("Should cancel alliance requests if the recipient attacks", async () => {
|
|
// Player A sends alliance request to Player B
|
|
const allianceRequest = playerA.createAllianceRequest(playerB);
|
|
expect(allianceRequest).not.toBeNull();
|
|
expect(playerB.incomingAllianceRequests()).toHaveLength(1);
|
|
|
|
// Player B attacks Player A
|
|
const attackExecution = new AttackExecution(
|
|
null,
|
|
playerB,
|
|
playerA.id(),
|
|
null,
|
|
);
|
|
game.addExecution(attackExecution);
|
|
|
|
// Execute a few ticks to process the attacks
|
|
for (let i = 0; i < 5; i++) {
|
|
game.executeNextTick();
|
|
}
|
|
// Alliance request should be denied since player B attacked
|
|
expect(playerA.outgoingAllianceRequests()).toHaveLength(0);
|
|
expect(playerB.incomingAllianceRequests()).toHaveLength(0);
|
|
});
|
|
|
|
test("Should cancel the proper alliance request among many", async () => {
|
|
// Add a new player to have more alliance requests
|
|
const playerCInfo = new PlayerInfo(
|
|
"playerB",
|
|
PlayerType.Human,
|
|
null,
|
|
"playerB_id",
|
|
);
|
|
const playerC = addPlayerToGame(playerCInfo, game, game.ref(10, 10));
|
|
game.executeNextTick();
|
|
game.executeNextTick();
|
|
|
|
// Player A sends alliance request to Player B
|
|
const allianceRequestAtoB = playerA.createAllianceRequest(playerB);
|
|
expect(allianceRequestAtoB).not.toBeNull();
|
|
|
|
// Player C also sends alliance request to Player B
|
|
const allianceRequestCtoB = playerC.createAllianceRequest(playerB);
|
|
expect(allianceRequestCtoB).not.toBeNull();
|
|
|
|
expect(playerB.incomingAllianceRequests()).toHaveLength(2);
|
|
|
|
// Player B attacks Player A
|
|
const attackExecution = new AttackExecution(
|
|
null,
|
|
playerB,
|
|
playerA.id(),
|
|
null,
|
|
);
|
|
game.addExecution(attackExecution);
|
|
|
|
// Execute a few ticks to process the attacks
|
|
for (let i = 0; i < 5; i++) {
|
|
game.executeNextTick();
|
|
}
|
|
// Alliance request A->B should be denied since player B attacked
|
|
expect(playerA.outgoingAllianceRequests()).toHaveLength(0);
|
|
// However C->B should remain
|
|
expect(playerB.incomingAllianceRequests()).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
describe("Transport ship alliance rejection", () => {
|
|
beforeEach(async () => {
|
|
game = await setup("ocean_and_land", {
|
|
infiniteGold: true,
|
|
instantBuild: true,
|
|
infiniteTroops: true,
|
|
});
|
|
|
|
const playerAInfo = new PlayerInfo(
|
|
"playerA",
|
|
PlayerType.Human,
|
|
null,
|
|
"playerA_id",
|
|
);
|
|
// close to the water to send boats
|
|
playerA = addPlayerToGame(playerAInfo, game, game.ref(7, 0));
|
|
|
|
const playerBInfo = new PlayerInfo(
|
|
"playerB",
|
|
PlayerType.Human,
|
|
null,
|
|
"playerB_id",
|
|
);
|
|
playerB = addPlayerToGame(playerBInfo, game, game.ref(7, 15));
|
|
game.executeNextTick();
|
|
game.executeNextTick();
|
|
});
|
|
|
|
test("Should cancel alliance requests if the recipient sends a transport ship", async () => {
|
|
// Player A sends alliance request to Player B
|
|
const allianceRequest = playerA.createAllianceRequest(playerB);
|
|
expect(allianceRequest).not.toBeNull();
|
|
expect(playerB.incomingAllianceRequests()).toHaveLength(1);
|
|
|
|
// Player B sends a transport ship toward Player A's territory
|
|
game.addExecution(new TransportShipExecution(playerB, game.ref(7, 0), 0));
|
|
|
|
// Execute a tick to process the transport ship launch
|
|
game.executeNextTick();
|
|
|
|
// Alliance request should be rejected since player B sent a naval invasion
|
|
expect(playerA.outgoingAllianceRequests()).toHaveLength(0);
|
|
expect(playerB.incomingAllianceRequests()).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe("Attack immunity", () => {
|
|
beforeEach(async () => {
|
|
game = await setup(
|
|
"ocean_and_land",
|
|
{
|
|
infiniteGold: true,
|
|
instantBuild: true,
|
|
infiniteTroops: true,
|
|
},
|
|
[],
|
|
undefined,
|
|
undefined,
|
|
false,
|
|
);
|
|
|
|
(game.config() as TestConfig).setSpawnImmunityDuration(immunityPhaseTicks);
|
|
|
|
const playerAInfo = new PlayerInfo(
|
|
"playerA",
|
|
PlayerType.Human,
|
|
null,
|
|
"playerA_id",
|
|
);
|
|
// close to the water to send boats
|
|
playerA = addPlayerToGame(playerAInfo, game, game.ref(7, 0));
|
|
|
|
const playerBInfo = new PlayerInfo(
|
|
"playerB",
|
|
PlayerType.Human,
|
|
null,
|
|
"playerB_id",
|
|
);
|
|
playerB = addPlayerToGame(playerBInfo, game, game.ref(7, 15));
|
|
game.executeNextTick();
|
|
game.executeNextTick();
|
|
});
|
|
|
|
test("Should not be able to attack during immunity phase", async () => {
|
|
// Player A attacks Player B
|
|
const attackExecution = new AttackExecution(
|
|
null,
|
|
playerA,
|
|
playerB.id(),
|
|
null,
|
|
);
|
|
game.addExecution(attackExecution);
|
|
game.executeNextTick();
|
|
expect(playerA.outgoingAttacks()).toHaveLength(0);
|
|
});
|
|
|
|
test("Should be able to attack after immunity phase", async () => {
|
|
waitForImmunityToEnd();
|
|
// Player A attacks Player B
|
|
const attackExecution = new AttackExecution(
|
|
null,
|
|
playerA,
|
|
playerB.id(),
|
|
null,
|
|
);
|
|
game.addExecution(attackExecution);
|
|
game.executeNextTick();
|
|
expect(playerA.outgoingAttacks()).toHaveLength(1);
|
|
});
|
|
|
|
test("Ensure a player can't attack during all the immunity phase", async () => {
|
|
// Execute a few ticks but stop right before the immunity phase is over
|
|
for (let i = 0; i < immunityPhaseTicks - 2; i++) {
|
|
game.executeNextTick();
|
|
}
|
|
// Player A attacks Player B
|
|
game.addExecution(new AttackExecution(null, playerA, playerB.id(), null));
|
|
game.executeNextTick(); // ticks === immunityPhaseTicks - 1 here
|
|
// Attack is not possible during immunity
|
|
expect(playerA.outgoingAttacks()).toHaveLength(0);
|
|
|
|
// Retry after the immunity is over
|
|
game.executeNextTick(); // ticks === immunityPhaseTicks
|
|
game.addExecution(new AttackExecution(null, playerA, playerB.id(), null));
|
|
game.executeNextTick();
|
|
// Attack is now possible right after
|
|
expect(playerA.outgoingAttacks()).toHaveLength(1);
|
|
});
|
|
|
|
test("Should not be able to send a boat during immunity phase", async () => {
|
|
// Player A sends a boat targeting Player B
|
|
game.addExecution(new TransportShipExecution(playerA, game.ref(7, 15), 10));
|
|
game.executeNextTick();
|
|
expect(playerA.units(UnitType.TransportShip)).toHaveLength(0);
|
|
});
|
|
|
|
test("Should be able to send a boat after immunity phase", async () => {
|
|
waitForImmunityToEnd();
|
|
// Player A sends a boat targeting Player B
|
|
game.addExecution(new TransportShipExecution(playerA, game.ref(7, 15), 10));
|
|
game.executeNextTick();
|
|
expect(playerA.units(UnitType.TransportShip)).toHaveLength(1);
|
|
});
|
|
|
|
test("Should not be able to attack nations during nation immunity phase", async () => {
|
|
(game.config() as TestConfig).setNationSpawnImmunityDuration(
|
|
immunityPhaseTicks,
|
|
);
|
|
const nationId = "nation_id";
|
|
const nation = new PlayerInfo("nation", PlayerType.Nation, null, nationId);
|
|
game.addPlayer(nation);
|
|
// Player A attacks the nation during nation immunity
|
|
const attackExecution = new AttackExecution(null, playerA, nationId, null);
|
|
game.addExecution(attackExecution);
|
|
game.executeNextTick();
|
|
expect(playerA.outgoingAttacks()).toHaveLength(0);
|
|
});
|
|
|
|
test("Should be able to attack nations after nation immunity phase", async () => {
|
|
(game.config() as TestConfig).setNationSpawnImmunityDuration(
|
|
immunityPhaseTicks,
|
|
);
|
|
const nationId = "nation_id";
|
|
const nation = new PlayerInfo("nation", PlayerType.Nation, null, nationId);
|
|
game.addPlayer(nation);
|
|
waitForImmunityToEnd();
|
|
// Player A attacks the nation after immunity
|
|
const attackExecution = new AttackExecution(null, playerA, nationId, null);
|
|
game.addExecution(attackExecution);
|
|
game.executeNextTick();
|
|
expect(playerA.outgoingAttacks()).toHaveLength(1);
|
|
});
|
|
|
|
test("Should be able to attack bots during immunity phase", async () => {
|
|
const botId = "bot_id";
|
|
const bot = new PlayerInfo("bot", PlayerType.Bot, null, botId);
|
|
game.addPlayer(bot);
|
|
// Player A attacks the bot
|
|
const attackExecution = new AttackExecution(null, playerA, botId, null);
|
|
game.addExecution(attackExecution);
|
|
game.executeNextTick();
|
|
expect(playerA.outgoingAttacks()).toHaveLength(1);
|
|
});
|
|
|
|
test("Can't send nuke during immunity phase", async () => {
|
|
constructionExecution(game, playerA, 7, 0, UnitType.MissileSilo);
|
|
expect(playerA.units(UnitType.MissileSilo)).toHaveLength(1);
|
|
// Player A sends a bomb to player B
|
|
constructionExecution(game, playerA, 0, 11, UnitType.AtomBomb, 3);
|
|
expect(playerA.units(UnitType.AtomBomb)).toHaveLength(0);
|
|
// Now wait for immunity to end
|
|
waitForImmunityToEnd();
|
|
// And send the exact same order
|
|
constructionExecution(game, playerA, 0, 11, UnitType.AtomBomb, 3);
|
|
expect(playerA.units(UnitType.AtomBomb)).toHaveLength(1);
|
|
});
|
|
|
|
test("Should abort TransportShipExecution when target is the attacker itself", async () => {
|
|
// Wait for spawn immunity to end to ensure it doesn't prematurely abort the execution
|
|
waitForImmunityToEnd();
|
|
|
|
// playerA tries to send a transport ship targeting one of playerA's own tiles (spawn tile at 7, 0)
|
|
const selfTarget = game.ref(7, 0);
|
|
const exec = new TransportShipExecution(playerA, selfTarget, 10);
|
|
game.addExecution(exec);
|
|
game.executeNextTick();
|
|
|
|
// Verify it aborted immediately: active is false, and no transport ship unit spawned
|
|
expect(exec.isActive()).toBe(false);
|
|
expect(playerA.units(UnitType.TransportShip)).toHaveLength(0);
|
|
});
|
|
|
|
test("Nation can attack human during PVP immunity", async () => {
|
|
const nationInfo = new PlayerInfo(
|
|
"nation",
|
|
PlayerType.Nation,
|
|
null,
|
|
"nation_id",
|
|
);
|
|
const nation = addPlayerToGame(nationInfo, game, game.ref(15, 0));
|
|
game.executeNextTick();
|
|
game.executeNextTick();
|
|
|
|
// Nation attacks playerA during PVP immunity - should succeed
|
|
game.addExecution(new AttackExecution(null, nation, "playerA_id", null));
|
|
game.executeNextTick();
|
|
expect(nation.outgoingAttacks()).toHaveLength(1);
|
|
});
|
|
|
|
test("Bot can attack human during PVP immunity", async () => {
|
|
const botInfo = new PlayerInfo("bot", PlayerType.Bot, null, "bot_id");
|
|
const bot = addPlayerToGame(botInfo, game, game.ref(15, 0));
|
|
game.executeNextTick();
|
|
game.executeNextTick();
|
|
|
|
// Bot attacks playerA during PVP immunity - should succeed
|
|
game.addExecution(new AttackExecution(null, bot, "playerA_id", null));
|
|
game.executeNextTick();
|
|
expect(bot.outgoingAttacks()).toHaveLength(1);
|
|
});
|
|
|
|
test("Nation can attack nation during PVP immunity", async () => {
|
|
const nationAInfo = new PlayerInfo(
|
|
"nationA",
|
|
PlayerType.Nation,
|
|
null,
|
|
"nationA_id",
|
|
);
|
|
const nationA = addPlayerToGame(nationAInfo, game, game.ref(15, 0));
|
|
|
|
const nationBInfo = new PlayerInfo(
|
|
"nationB",
|
|
PlayerType.Nation,
|
|
null,
|
|
"nationB_id",
|
|
);
|
|
addPlayerToGame(nationBInfo, game, game.ref(15, 15));
|
|
game.executeNextTick();
|
|
game.executeNextTick();
|
|
|
|
// Nation A attacks Nation B during PVP immunity - should succeed
|
|
game.addExecution(new AttackExecution(null, nationA, "nationB_id", null));
|
|
game.executeNextTick();
|
|
expect(nationA.outgoingAttacks()).toHaveLength(1);
|
|
});
|
|
|
|
test("Nation cannot attack allied human during PVP immunity", async () => {
|
|
const nationInfo = new PlayerInfo(
|
|
"nation",
|
|
PlayerType.Nation,
|
|
null,
|
|
"nation_id",
|
|
);
|
|
const nation = addPlayerToGame(nationInfo, game, game.ref(15, 0));
|
|
game.executeNextTick();
|
|
game.executeNextTick();
|
|
|
|
// Create alliance between nation and playerA
|
|
const allianceRequest = nation.createAllianceRequest(playerA);
|
|
if (allianceRequest) {
|
|
allianceRequest.accept();
|
|
}
|
|
expect(nation.isAlliedWith(playerA)).toBe(true);
|
|
|
|
// Nation tries to attack allied playerA during immunity - should be blocked by friendliness
|
|
game.addExecution(new AttackExecution(null, nation, "playerA_id", null));
|
|
game.executeNextTick();
|
|
expect(nation.outgoingAttacks()).toHaveLength(0);
|
|
});
|
|
});
|