diff --git a/resources/lang/en.json b/resources/lang/en.json
index 414222d57c..59a75cff39 100644
--- a/resources/lang/en.json
+++ b/resources/lang/en.json
@@ -694,7 +694,9 @@
"send_alliance": "Send Alliance",
"send_troops": "Send Troops",
"send_gold": "Send Gold",
- "emotes": "Emojis"
+ "emotes": "Emojis",
+ "kick": "Kick Player",
+ "kick_confirm": "Are you sure you want to kick {name}?"
},
"send_troops_modal": {
"title_with_name": "Send Troops to {name}",
diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts
index 27c2f01cbd..092583aed5 100644
--- a/src/client/ClientGameRunner.ts
+++ b/src/client/ClientGameRunner.ts
@@ -191,6 +191,7 @@ async function createClientGame(
lobbyConfig.clientID,
lobbyConfig.gameStartInfo.gameID,
lobbyConfig.gameStartInfo.players,
+ lobbyConfig.gameStartInfo.lobbyCreatorID,
);
const canvas = createCanvas();
diff --git a/src/client/graphics/layers/PlayerPanel.ts b/src/client/graphics/layers/PlayerPanel.ts
index 560c1fe227..bad07c19a1 100644
--- a/src/client/graphics/layers/PlayerPanel.ts
+++ b/src/client/graphics/layers/PlayerPanel.ts
@@ -2,6 +2,7 @@ import { html, LitElement } from "lit";
import { customElement, state } from "lit/decorators.js";
import allianceIcon from "../../../../resources/images/AllianceIconWhite.svg";
import chatIcon from "../../../../resources/images/ChatIconWhite.svg";
+import disabledIcon from "../../../../resources/images/DisabledIcon.svg";
import donateGoldIcon from "../../../../resources/images/DonateGoldIconWhite.svg";
import donateTroopIcon from "../../../../resources/images/DonateTroopIconWhite.svg";
import emojiIcon from "../../../../resources/images/EmojiIconWhite.svg";
@@ -31,6 +32,7 @@ import {
SendEmbargoAllIntentEvent,
SendEmbargoIntentEvent,
SendEmojiIntentEvent,
+ SendKickPlayerIntentEvent,
SendTargetPlayerIntentEvent,
} from "../../Transport";
import {
@@ -274,6 +276,24 @@ export class PlayerPanel extends LitElement implements Layer {
this.hide();
}
+ private handleKickClick(e: Event, other: PlayerView) {
+ e.stopPropagation();
+ if (
+ !window.confirm(
+ translateText("player_panel.kick_confirm", { name: other.name() }),
+ )
+ ) {
+ return;
+ }
+ const targetClientID = other.clientID();
+ if (!targetClientID) {
+ console.warn("Cannot kick player without clientID");
+ return;
+ }
+ this.eventBus.emit(new SendKickPlayerIntentEvent(targetClientID));
+ this.hide();
+ }
+
private identityChipProps(type: PlayerType) {
switch (type) {
case PlayerType.Nation:
@@ -618,6 +638,7 @@ export class PlayerPanel extends LitElement implements Layer {
const canBreakAlliance = this.actions?.interaction?.canBreakAlliance;
const canTarget = this.actions?.interaction?.canTarget;
const canEmbargo = this.actions?.interaction?.canEmbargo;
+ const canKick = this.actions?.interaction?.canKick;
return html`
@@ -718,6 +739,16 @@ export class PlayerPanel extends LitElement implements Layer {
type: "indigo",
})
: ""}
+ ${canKick && !this.g.isLobbyCreator(other)
+ ? actionButton({
+ onClick: (e: MouseEvent) => this.handleKickClick(e, other),
+ icon: disabledIcon,
+ iconAlt: "Kick",
+ title: translateText("player_panel.kick"),
+ label: translateText("player_panel.kick"),
+ type: "red",
+ })
+ : ""}
${other === my
diff --git a/src/core/GameRunner.ts b/src/core/GameRunner.ts
index 92eba215b7..16ea3eaaeb 100644
--- a/src/core/GameRunner.ts
+++ b/src/core/GameRunner.ts
@@ -78,6 +78,7 @@ export async function createGameRunner(
game,
new Executor(game, gameStart.gameID, clientID),
callBack,
+ gameStart.lobbyCreatorID,
);
gr.init();
return gr;
@@ -94,6 +95,7 @@ export class GameRunner {
public game: Game,
private execManager: Executor,
private callBack: (gu: GameUpdateViewData | ErrorUpdate) => void,
+ private lobbyCreatorID: ClientID | undefined,
) {}
init() {
@@ -208,6 +210,9 @@ export class GameRunner {
canDonateGold: player.canDonateGold(other),
canDonateTroops: player.canDonateTroops(other),
canEmbargo: !player.hasEmbargoAgainst(other),
+ canKick:
+ this.lobbyCreatorID === player.clientID() &&
+ player.clientID() !== other.clientID(),
};
const alliance = player.allianceWith(other as Player);
if (alliance) {
diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts
index 27ceb7c053..3013dbede5 100644
--- a/src/core/Schemas.ts
+++ b/src/core/Schemas.ts
@@ -447,6 +447,7 @@ export const GameStartInfoSchema = z.object({
lobbyCreatedAt: z.number(),
config: GameConfigSchema,
players: PlayerSchema.array(),
+ lobbyCreatorID: ID.optional(),
});
export const WinnerSchema = z
diff --git a/src/core/game/Game.ts b/src/core/game/Game.ts
index 4a33d9b804..a7acad9395 100644
--- a/src/core/game/Game.ts
+++ b/src/core/game/Game.ts
@@ -803,6 +803,7 @@ export interface PlayerInteraction {
canDonateGold: boolean;
canDonateTroops: boolean;
canEmbargo: boolean;
+ canKick?: boolean;
allianceExpiresAt?: Tick;
}
diff --git a/src/core/game/GameView.ts b/src/core/game/GameView.ts
index 04bf79d525..6ac3020c05 100644
--- a/src/core/game/GameView.ts
+++ b/src/core/game/GameView.ts
@@ -605,6 +605,7 @@ export class GameView implements GameMap {
private _myClientID: ClientID,
private _gameID: GameID,
private humans: Player[],
+ private _lobbyCreatorID: ClientID | undefined,
) {
this._map = this._mapData.gameMap;
this.lastUpdate = null;
@@ -620,6 +621,10 @@ export class GameView implements GameMap {
}
}
+ isLobbyCreator(player: PlayerView): boolean {
+ return player.clientID() === this._lobbyCreatorID;
+ }
+
isOnEdgeOfMap(ref: TileRef): boolean {
return this._map.isOnEdgeOfMap(ref);
}
diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts
index 428275f3c0..4019fdff07 100644
--- a/src/server/GameServer.ts
+++ b/src/server/GameServer.ts
@@ -497,6 +497,7 @@ export class GameServer {
cosmetics: c.cosmetics,
isLobbyCreator: this.lobbyCreatorID === c.clientID,
})),
+ lobbyCreatorID: this.lobbyCreatorID,
});
if (!result.success) {
const error = z.prettifyError(result.error);