mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 23:01:54 +00:00
86e51ab790
## Description: As far as I can remember, in v28 the spawn immunity applied to both humans and nations. With the configurable spawn immunity (added for v29) the spawn immunity no longer applies to nations... Because its called PVP immunity now. So right now it's possible to spawnkill nations. This is a big problem for the 5M gold modifier games... And you can "cheat" in singleplayer. This PR changes two things: - Nations always have 5 seconds spawn immunity now, no matter whats configured for the PVP immunity - Nations attack TerraNullius earlier (Otherwise the easy nations would sometimes do their first attack after the 5 seconds are over, spawnkills would still be possible) ## 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: FloPinguin --------- Co-authored-by: Ryan <7389646+ryanbarlow97@users.noreply.github.com>
482 lines
15 KiB
TypeScript
482 lines
15 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,
|
|
),
|
|
);
|
|
|
|
while (game.inSpawnPhase()) {
|
|
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.orderBoatRetreat();
|
|
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));
|
|
|
|
while (game.inSpawnPhase()) {
|
|
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));
|
|
|
|
// 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("Attack immunity", () => {
|
|
beforeEach(async () => {
|
|
game = await setup("ocean_and_land", {
|
|
infiniteGold: true,
|
|
instantBuild: true,
|
|
infiniteTroops: true,
|
|
});
|
|
|
|
(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));
|
|
|
|
while (game.inSpawnPhase()) {
|
|
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);
|
|
});
|
|
});
|