diff --git a/index.html b/index.html
index a3105a2a9..2ab3f22a6 100644
--- a/index.html
+++ b/index.html
@@ -451,6 +451,7 @@
+
diff --git a/resources/lang/debug.json b/resources/lang/debug.json
index 924720ce3..1e7210a03 100644
--- a/resources/lang/debug.json
+++ b/resources/lang/debug.json
@@ -145,6 +145,7 @@
"options_title": "host_modal.options_title",
"bots": "host_modal.bots",
"bots_disabled": "host_modal.bots_disabled",
+ "player_immunity_duration": "host_modal.player_immunity_duration",
"disable_nations": "host_modal.disable_nations",
"instant_build": "host_modal.instant_build",
"random_spawn": "host_modal.random_spawn",
diff --git a/resources/lang/en.json b/resources/lang/en.json
index a4f363404..0e1e5fd53 100644
--- a/resources/lang/en.json
+++ b/resources/lang/en.json
@@ -307,6 +307,7 @@
"options_title": "Options",
"bots": "Bots: ",
"bots_disabled": "Disabled",
+ "player_immunity_duration": "PVP immunity duration (minutes)",
"nations": "Nations: ",
"disable_nations": "Disable Nations",
"max_timer": "Game length (minutes)",
diff --git a/src/client/HostLobbyModal.ts b/src/client/HostLobbyModal.ts
index d730a820a..1853d5c28 100644
--- a/src/client/HostLobbyModal.ts
+++ b/src/client/HostLobbyModal.ts
@@ -45,6 +45,8 @@ export class HostLobbyModal extends LitElement {
@state() private gameMode: GameMode = GameMode.FFA;
@state() private teamCount: TeamCountConfig = 2;
@state() private bots: number = 400;
+ @state() private spawnImmunity: boolean = false;
+ @state() private spawnImmunityDurationMinutes: number | undefined = undefined;
@state() private infiniteGold: boolean = false;
@state() private donateGold: boolean = false;
@state() private infiniteTroops: boolean = false;
@@ -514,7 +516,7 @@ export class HostLobbyModal extends LitElement {
id="end-timer-value"
min="0"
max="120"
- .value=${String(this.maxTimerValue ?? "")}
+ .value=${String(this.maxTimerValue ?? 0)}
style="width: 60px; color: black; text-align: right; border-radius: 8px;"
@input=${this.handleMaxTimerValueChanges}
@keydown=${this.handleMaxTimerValueKeyDown}
@@ -524,6 +526,47 @@ export class HostLobbyModal extends LitElement {
${translateText("host_modal.max_timer")}
+
+
+
@@ -691,6 +734,23 @@ export class HostLobbyModal extends LitElement {
this.putGameConfig();
}
+ private handleSpawnImmunityDurationKeyDown(e: KeyboardEvent) {
+ if (["-", "+", "e", "E"].includes(e.key)) {
+ e.preventDefault();
+ }
+ }
+
+ private handleSpawnImmunityDurationInput(e: Event) {
+ const input = e.target as HTMLInputElement;
+ input.value = input.value.replace(/[eE+-]/g, "");
+ const value = parseInt(input.value, 10);
+ if (Number.isNaN(value) || value < 0 || value > 120) {
+ return;
+ }
+ this.spawnImmunityDurationMinutes = value;
+ this.putGameConfig();
+ }
+
private handleRandomSpawnChange(e: Event) {
this.randomSpawn = Boolean((e.target as HTMLInputElement).checked);
this.putGameConfig();
@@ -757,6 +817,9 @@ export class HostLobbyModal extends LitElement {
}
private async putGameConfig() {
+ const spawnImmunityTicks = this.spawnImmunityDurationMinutes
+ ? this.spawnImmunityDurationMinutes * 60 * 10
+ : 0;
this.dispatchEvent(
new CustomEvent("update-game-config", {
detail: {
@@ -775,6 +838,9 @@ export class HostLobbyModal extends LitElement {
randomSpawn: this.randomSpawn,
gameMode: this.gameMode,
disabledUnits: this.disabledUnits,
+ spawnImmunityDuration: this.spawnImmunity
+ ? spawnImmunityTicks
+ : undefined,
playerTeams: this.teamCount,
...(this.gameMode === GameMode.Team &&
this.teamCount === HumansVsNations
diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts
index ffb642ab7..dd4cf64b4 100644
--- a/src/client/graphics/GameRenderer.ts
+++ b/src/client/graphics/GameRenderer.ts
@@ -18,6 +18,7 @@ import { FxLayer } from "./layers/FxLayer";
import { GameLeftSidebar } from "./layers/GameLeftSidebar";
import { GameRightSidebar } from "./layers/GameRightSidebar";
import { HeadsUpMessage } from "./layers/HeadsUpMessage";
+import { ImmunityTimer } from "./layers/ImmunityTimer";
import { Layer } from "./layers/Layer";
import { Leaderboard } from "./layers/Leaderboard";
import { MainRadialMenu } from "./layers/MainRadialMenu";
@@ -234,6 +235,14 @@ export function createRenderer(
spawnTimer.game = game;
spawnTimer.transformHandler = transformHandler;
+ const immunityTimer = document.querySelector(
+ "immunity-timer",
+ ) as ImmunityTimer;
+ if (!(immunityTimer instanceof ImmunityTimer)) {
+ console.error("immunity timer not found");
+ }
+ immunityTimer.game = game;
+
// When updating these layers please be mindful of the order.
// Try to group layers by the return value of shouldTransform.
// Not grouping the layers may cause excessive calls to context.save() and context.restore().
@@ -262,6 +271,7 @@ export function createRenderer(
playerPanel,
),
spawnTimer,
+ immunityTimer,
leaderboard,
gameLeftSidebar,
unitDisplay,
diff --git a/src/client/graphics/layers/ImmunityTimer.ts b/src/client/graphics/layers/ImmunityTimer.ts
new file mode 100644
index 000000000..702a08b38
--- /dev/null
+++ b/src/client/graphics/layers/ImmunityTimer.ts
@@ -0,0 +1,93 @@
+import { LitElement, html } from "lit";
+import { customElement } from "lit/decorators.js";
+import { GameMode } from "../../../core/game/Game";
+import { GameView } from "../../../core/game/GameView";
+import { Layer } from "./Layer";
+
+@customElement("immunity-timer")
+export class ImmunityTimer extends LitElement implements Layer {
+ public game: GameView;
+
+ private isVisible = false;
+ private isActive = false;
+ private progressRatio = 0;
+
+ createRenderRoot() {
+ this.style.position = "fixed";
+ this.style.top = "0";
+ this.style.left = "0";
+ this.style.width = "100%";
+ this.style.height = "7px";
+ this.style.zIndex = "1000";
+ this.style.pointerEvents = "none";
+ return this;
+ }
+
+ init() {
+ this.isVisible = true;
+ }
+
+ tick() {
+ if (!this.game || !this.isVisible) {
+ return;
+ }
+
+ const showTeamOwnershipBar =
+ this.game.config().gameConfig().gameMode === GameMode.Team &&
+ !this.game.inSpawnPhase();
+
+ this.style.top = showTeamOwnershipBar ? "7px" : "0px";
+
+ const immunityDuration = this.game.config().spawnImmunityDuration();
+ const spawnPhaseTurns = this.game.config().numSpawnPhaseTurns();
+
+ if (immunityDuration <= 5 * 10 || this.game.inSpawnPhase()) {
+ this.setInactive();
+ return;
+ }
+
+ const immunityEnd = spawnPhaseTurns + immunityDuration;
+ const ticks = this.game.ticks();
+
+ if (ticks >= immunityEnd || ticks < spawnPhaseTurns) {
+ this.setInactive();
+ return;
+ }
+
+ const elapsedTicks = Math.max(0, ticks - spawnPhaseTurns);
+ this.progressRatio = Math.min(
+ 1,
+ Math.max(0, elapsedTicks / immunityDuration),
+ );
+ this.isActive = true;
+ this.requestUpdate();
+ }
+
+ private setInactive() {
+ if (this.isActive) {
+ this.isActive = false;
+ this.requestUpdate();
+ }
+ }
+
+ shouldTransform(): boolean {
+ return false;
+ }
+
+ render() {
+ if (!this.isVisible || !this.isActive) {
+ return html``;
+ }
+
+ const widthPercent = this.progressRatio * 100;
+
+ return html`
+
+ `;
+ }
+}
diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts
index b9bc2e7a8..fc520af80 100644
--- a/src/core/Schemas.ts
+++ b/src/core/Schemas.ts
@@ -179,6 +179,7 @@ export const GameConfigSchema = z.object({
randomSpawn: z.boolean(),
maxPlayers: z.number().optional(),
maxTimerValue: z.number().int().min(1).max(120).optional(),
+ spawnImmunityDuration: z.number().int().min(0).optional(), // In ticks
disabledUnits: z.enum(UnitType).array().optional(),
playerTeams: TeamCountConfigSchema.optional(),
});
diff --git a/src/core/configuration/DefaultConfig.ts b/src/core/configuration/DefaultConfig.ts
index dca944492..64621e0a9 100644
--- a/src/core/configuration/DefaultConfig.ts
+++ b/src/core/configuration/DefaultConfig.ts
@@ -246,7 +246,7 @@ export class DefaultConfig implements Config {
return 30 * 10; // 30 seconds
}
spawnImmunityDuration(): Tick {
- return 5 * 10;
+ return this._gameConfig.spawnImmunityDuration ?? 5 * 10; // default to 5 seconds
}
gameConfig(): GameConfig {
diff --git a/src/core/execution/AttackExecution.ts b/src/core/execution/AttackExecution.ts
index d123bd159..3adf24f75 100644
--- a/src/core/execution/AttackExecution.ts
+++ b/src/core/execution/AttackExecution.ts
@@ -92,16 +92,9 @@ export class AttackExecution implements Execution {
}
}
- if (this.target.isPlayer()) {
- if (
- this.mg.config().numSpawnPhaseTurns() +
- this.mg.config().spawnImmunityDuration() >
- this.mg.ticks()
- ) {
- console.warn("cannot attack player during immunity phase");
- this.active = false;
- return;
- }
+ if (this.target.isPlayer() && !this._owner.canAttackPlayer(this.target)) {
+ this.active = false;
+ return;
}
this.startTroops ??= this.mg
diff --git a/src/core/execution/TransportShipExecution.ts b/src/core/execution/TransportShipExecution.ts
index 6e37e066d..761058f32 100644
--- a/src/core/execution/TransportShipExecution.ts
+++ b/src/core/execution/TransportShipExecution.ts
@@ -93,6 +93,10 @@ export class TransportShipExecution implements Execution {
} else {
this.target = mg.player(this.targetID);
}
+ if (this.target.isPlayer() && !this.attacker.canAttackPlayer(this.target)) {
+ this.active = false;
+ return;
+ }
this.startTroops ??= this.mg
.config()
diff --git a/src/core/execution/WarshipExecution.ts b/src/core/execution/WarshipExecution.ts
index 1ddb064cb..aa2f7522f 100644
--- a/src/core/execution/WarshipExecution.ts
+++ b/src/core/execution/WarshipExecution.ts
@@ -89,7 +89,7 @@ export class WarshipExecution implements Execution {
if (
unit.owner() === this.warship.owner() ||
unit === this.warship ||
- unit.owner().isFriendly(this.warship.owner(), true) ||
+ !this.warship.owner().canAttackPlayer(unit.owner(), true) ||
this.alreadySentShell.has(unit)
) {
continue;
diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts
index c38b00300..ec54e4a51 100644
--- a/src/core/game/Game.ts
+++ b/src/core/game/Game.ts
@@ -659,6 +659,8 @@ export interface Player {
// Attacking.
canAttack(tile: TileRef): boolean;
+ canAttackPlayer(player: Player, treatAFKFriendly?: boolean): boolean;
+ isImmune(): boolean;
createAttack(
target: Player | TerraNullius,
@@ -713,6 +715,9 @@ export interface Game extends GameMap {
alliances(): MutableAlliance[];
expireAlliance(alliance: Alliance): void;
+ // Immunity timer
+ isSpawnImmunityActive(): boolean;
+
// Game State
ticks(): Tick;
inSpawnPhase(): boolean;
diff --git a/src/core/game/GameImpl.ts b/src/core/game/GameImpl.ts
index ee0a7783a..465eeaa25 100644
--- a/src/core/game/GameImpl.ts
+++ b/src/core/game/GameImpl.ts
@@ -677,6 +677,14 @@ export class GameImpl implements Game {
});
}
+ public isSpawnImmunityActive(): boolean {
+ return (
+ this.config().numSpawnPhaseTurns() +
+ this.config().spawnImmunityDuration() >=
+ this.ticks()
+ );
+ }
+
sendEmojiUpdate(msg: EmojiMessage): void {
this.addUpdate({
type: GameUpdateType.Emoji,
diff --git a/src/core/game/PlayerImpl.ts b/src/core/game/PlayerImpl.ts
index 7b1a8210c..09c02c5a7 100644
--- a/src/core/game/PlayerImpl.ts
+++ b/src/core/game/PlayerImpl.ts
@@ -1010,6 +1010,9 @@ export class PlayerImpl implements Player {
}
nukeSpawn(tile: TileRef): TileRef | false {
+ if (this.mg.isSpawnImmunityActive()) {
+ return false;
+ }
const owner = this.mg.owner(tile);
if (owner.isPlayer()) {
if (this.isOnSameTeam(owner)) {
@@ -1200,31 +1203,36 @@ export class PlayerImpl implements Player {
return this._incomingAttacks;
}
+ public isImmune(): boolean {
+ return this.type() === PlayerType.Human && this.mg.isSpawnImmunityActive();
+ }
+
+ public canAttackPlayer(
+ player: Player,
+ treatAFKFriendly: boolean = false,
+ ): boolean {
+ if (this.type() === PlayerType.Human) {
+ return !player.isImmune() && !this.isFriendly(player, treatAFKFriendly);
+ }
+ // Only humans are affected by immunity, bots and nations should be able to attack freely
+ return !this.isFriendly(player, treatAFKFriendly);
+ }
+
public canAttack(tile: TileRef): boolean {
- if (
- this.mg.hasOwner(tile) &&
- this.mg.config().numSpawnPhaseTurns() +
- this.mg.config().spawnImmunityDuration() >
- this.mg.ticks()
- ) {
+ const owner = this.mg.owner(tile);
+ if (owner === this) {
return false;
}
- if (this.mg.owner(tile) === this) {
+ if (owner.isPlayer() && !this.canAttackPlayer(owner)) {
return false;
}
- const other = this.mg.owner(tile);
- if (other.isPlayer()) {
- if (this.isFriendly(other)) {
- return false;
- }
- }
if (!this.mg.isLand(tile)) {
return false;
}
if (this.mg.hasOwner(tile)) {
- return this.sharesBorderWith(other);
+ return this.sharesBorderWith(owner);
} else {
for (const t of this.mg.bfs(
tile,
diff --git a/src/core/game/TransportShipUtils.ts b/src/core/game/TransportShipUtils.ts
index a0af53526..4d60f6bed 100644
--- a/src/core/game/TransportShipUtils.ts
+++ b/src/core/game/TransportShipUtils.ts
@@ -23,7 +23,7 @@ export function canBuildTransportShip(
if (other === player) {
return false;
}
- if (other.isPlayer() && player.isFriendly(other)) {
+ if (other.isPlayer() && !player.canAttackPlayer(other)) {
return false;
}
diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts
index 3aa8d53b6..068253920 100644
--- a/src/server/GameServer.ts
+++ b/src/server/GameServer.ts
@@ -121,6 +121,9 @@ export class GameServer {
if (gameConfig.randomSpawn !== undefined) {
this.gameConfig.randomSpawn = gameConfig.randomSpawn;
}
+ if (gameConfig.spawnImmunityDuration !== undefined) {
+ this.gameConfig.spawnImmunityDuration = gameConfig.spawnImmunityDuration;
+ }
if (gameConfig.gameMode !== undefined) {
this.gameConfig.gameMode = gameConfig.gameMode;
}
diff --git a/src/server/MapPlaylist.ts b/src/server/MapPlaylist.ts
index 7811a7c51..6efdde53d 100644
--- a/src/server/MapPlaylist.ts
+++ b/src/server/MapPlaylist.ts
@@ -110,6 +110,7 @@ export class MapPlaylist {
gameMode: mode,
playerTeams,
bots: 400,
+ spawnImmunityDuration: 5 * 10,
disabledUnits: [],
} satisfies GameConfig;
}
diff --git a/tests/Attack.test.ts b/tests/Attack.test.ts
index 5715565a0..75e536f08 100644
--- a/tests/Attack.test.ts
+++ b/tests/Attack.test.ts
@@ -27,6 +27,13 @@ function sendBoat(target: TileRef, source: TileRef, troops: number) {
);
}
+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", {
@@ -185,7 +192,7 @@ describe("Attack race condition with alliance requests", () => {
}
});
- it("should not mark attacker as traitor when alliance is formed after attack starts", async () => {
+ 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();
@@ -229,7 +236,7 @@ describe("Attack race condition with alliance requests", () => {
expect(playerB.outgoingAttacks()).toHaveLength(0);
});
- it("should prevent player from attacking allied player", async () => {
+ 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) {
@@ -261,7 +268,7 @@ describe("Attack race condition with alliance requests", () => {
expect(playerB.incomingAttacks()).toHaveLength(0);
});
- test("should cancel alliance requests if the recipient attacks", async () => {
+ 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();
@@ -285,7 +292,7 @@ describe("Attack race condition with alliance requests", () => {
expect(playerB.incomingAllianceRequests()).toHaveLength(0);
});
- test("should cancel the proper alliance request among many", async () => {
+ test("Should cancel the proper alliance request among many", async () => {
// Add a new player to have more alliance requests
const playerCInfo = new PlayerInfo(
"playerB",
@@ -324,3 +331,148 @@ describe("Attack race condition with alliance requests", () => {
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(0, 11));
+
+ 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 - 1; i++) {
+ game.executeNextTick();
+ }
+ // Player A attacks Player B
+ game.addExecution(new AttackExecution(null, playerA, playerB.id(), null));
+ game.executeNextTick(); // ticks === immunityPhaseTicks here
+ // Attack is not possible during immunity
+ expect(playerA.outgoingAttacks()).toHaveLength(0);
+
+ // Retry after the immunity is over
+ game.executeNextTick(); // ticks === immunityPhaseTicks + 1
+ 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,
+ playerB.id(),
+ game.ref(15, 8),
+ 10,
+ game.ref(10, 5),
+ ),
+ );
+ 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,
+ playerB.id(),
+ game.ref(15, 8),
+ 10,
+ game.ref(7, 0),
+ ),
+ );
+ game.executeNextTick();
+ expect(playerA.units(UnitType.TransportShip)).toHaveLength(1);
+ });
+
+ test("Should be able to attack nations during immunity phase", async () => {
+ const nationId = "nation_id";
+ const nation = new PlayerInfo("nation", PlayerType.Nation, null, nationId);
+ game.addPlayer(nation);
+ // Player A attacks the nation
+ 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);
+ });
+});
diff --git a/tests/util/TestConfig.ts b/tests/util/TestConfig.ts
index 57d50558a..9e1085bc7 100644
--- a/tests/util/TestConfig.ts
+++ b/tests/util/TestConfig.ts
@@ -12,6 +12,7 @@ import { TileRef } from "../../src/core/game/GameMap";
export class TestConfig extends DefaultConfig {
private _proximityBonusPortsNb: number = 0;
private _defaultNukeSpeed: number = 4;
+ private _spawnImmunityDuration: number = 0;
radiusPortSpawn(): number {
return 1;
@@ -54,8 +55,12 @@ export class TestConfig extends DefaultConfig {
return 20;
}
+ setSpawnImmunityDuration(duration: Tick) {
+ this._spawnImmunityDuration = duration;
+ }
+
spawnImmunityDuration(): Tick {
- return 0;
+ return this._spawnImmunityDuration;
}
attackLogic(