Fix nation spawnkilling 🔧 (#3222)

## 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>
This commit is contained in:
FloPinguin
2026-02-17 01:19:36 +01:00
committed by GitHub
parent 18f52c01bb
commit 86e51ab790
10 changed files with 111 additions and 29 deletions
+1
View File
@@ -58,6 +58,7 @@ export interface NukeMagnitude {
export interface Config {
spawnImmunityDuration(): Tick;
nationSpawnImmunityDuration(): Tick;
hasExtendedSpawnImmunity(): boolean;
serverConfig(): ServerConfig;
gameConfig(): GameConfig;
+3
View File
@@ -168,6 +168,9 @@ export class DefaultConfig implements Config {
this._gameConfig.spawnImmunityDuration ?? DEFAULT_SPAWN_IMMUNITY_TICKS
);
}
nationSpawnImmunityDuration(): Tick {
return DEFAULT_SPAWN_IMMUNITY_TICKS;
}
hasExtendedSpawnImmunity(): boolean {
return this.spawnImmunityDuration() > DEFAULT_SPAWN_IMMUNITY_TICKS;
}
+21 -22
View File
@@ -94,33 +94,14 @@ export class NationExecution implements Execution {
this.warshipBehavior.trackShipsAndRetaliate();
}
if (ticks % this.attackRate !== this.attackTick) {
// Call handleStructures twice between regular attack ticks (at 1/3 and 2/3 of the interval)
// Otherwise it is possible that we earn more gold than we can spend
// The alternative is placing multiple structures in handleStructures, but that causes problems
if (
this.behaviorsInitialized &&
this.player !== null &&
this.player.isAlive()
) {
const offset = ticks % this.attackRate;
const oneThird =
(this.attackTick + Math.floor(this.attackRate / 3)) % this.attackRate;
const twoThirds =
(this.attackTick + Math.floor((this.attackRate * 2) / 3)) %
this.attackRate;
if (offset === oneThird || offset === twoThirds) {
this.structureBehavior.handleStructures();
}
}
return;
}
if (this.player === null) {
return;
}
if (this.mg.inSpawnPhase()) {
if (ticks % this.attackRate !== this.attackTick) {
return;
}
// Place nations without a spawn cell (Dynamically created for HumansVsNations) randomly by SpawnExecution
if (this.nation.spawnCell === undefined) {
this.mg.addExecution(
@@ -155,6 +136,24 @@ export class NationExecution implements Execution {
return;
}
if (ticks % this.attackRate !== this.attackTick) {
// Call handleStructures twice between regular attack ticks (at 1/3 and 2/3 of the interval)
// Otherwise it is possible that we earn more gold than we can spend
// The alternative is placing multiple structures in handleStructures, but that causes problems
if (this.player.isAlive()) {
const offset = ticks % this.attackRate;
const oneThird =
(this.attackTick + Math.floor(this.attackRate / 3)) % this.attackRate;
const twoThirds =
(this.attackTick + Math.floor((this.attackRate * 2) / 3)) %
this.attackRate;
if (offset === oneThird || offset === twoThirds) {
this.structureBehavior.handleStructures();
}
}
return;
}
this.emojiBehavior.maybeSendCasualEmoji();
this.updateRelationsFromEmbargos();
this.allianceBehavior.handleAllianceRequests();
+1
View File
@@ -759,6 +759,7 @@ export interface Game extends GameMap {
// Immunity timer
isSpawnImmunityActive(): boolean;
isNationSpawnImmunityActive(): boolean;
// Game State
ticks(): Tick;
+8
View File
@@ -727,6 +727,14 @@ export class GameImpl implements Game {
);
}
public isNationSpawnImmunityActive(): boolean {
return (
this.config().numSpawnPhaseTurns() +
this.config().nationSpawnImmunityDuration() >
this.ticks()
);
}
sendEmojiUpdate(msg: EmojiMessage): void {
this.addUpdate({
type: GameUpdateType.Emoji,
+7
View File
@@ -824,6 +824,13 @@ export class GameView implements GameMap {
this.ticks()
);
}
isNationSpawnImmunityActive(): boolean {
return (
this._config.numSpawnPhaseTurns() +
this._config.nationSpawnImmunityDuration() >
this.ticks()
);
}
config(): Config {
return this._config;
}
+12 -5
View File
@@ -1261,18 +1261,25 @@ export class PlayerImpl implements Player {
}
public isImmune(): boolean {
return this.type() === PlayerType.Human && this.mg.isSpawnImmunityActive();
if (this.type() === PlayerType.Human) {
return this.mg.isSpawnImmunityActive();
}
if (this.type() === PlayerType.Nation) {
return this.mg.isNationSpawnImmunityActive();
}
return false;
}
public canAttackPlayer(
player: Player,
treatAFKFriendly: boolean = false,
): boolean {
if (this.type() === PlayerType.Human) {
return !player.isImmune() && !this.isFriendly(player, treatAFKFriendly);
if (this.type() === PlayerType.Bot) {
// Bots are not affected by immunity
return !this.isFriendly(player, treatAFKFriendly);
}
// Only humans are affected by immunity, bots and nations should be able to attack freely
return !this.isFriendly(player, treatAFKFriendly);
// Humans and Nations respect immunity
return !player.isImmune() && !this.isFriendly(player, treatAFKFriendly);
}
public canAttack(tile: TileRef): boolean {