diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts
index 5089b6be2..baf317045 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 560c1fe22..087ecd099 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,12 @@ export class PlayerPanel extends LitElement implements Layer {
this.hide();
}
+ private handleKickClick(e: Event, other: PlayerView) {
+ e.stopPropagation();
+ this.eventBus.emit(new SendKickPlayerIntentEvent(other.clientID()!));
+ this.hide();
+ }
+
private identityChipProps(type: PlayerType) {
switch (type) {
case PlayerType.Nation:
@@ -618,6 +626,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 +727,16 @@ export class PlayerPanel extends LitElement implements Layer {
type: "indigo",
})
: ""}
+ ${canKick
+ ? 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 ed8c8cd7b..9c6af48ec 100644
--- a/src/core/GameRunner.ts
+++ b/src/core/GameRunner.ts
@@ -77,6 +77,7 @@ export async function createGameRunner(
game,
new Executor(game, gameStart.gameID, clientID),
callBack,
+ gameStart.lobbyCreatorID,
);
gr.init();
return gr;
@@ -93,6 +94,7 @@ export class GameRunner {
public game: Game,
private execManager: Executor,
private callBack: (gu: GameUpdateViewData | ErrorUpdate) => void,
+ private lobbyCreatorID: ClientID | undefined,
) {}
init() {
@@ -207,6 +209,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 9370d6a47..958b55256 100644
--- a/src/core/Schemas.ts
+++ b/src/core/Schemas.ts
@@ -437,6 +437,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 9c5ef95ff..33e8d8160 100644
--- a/src/core/game/Game.ts
+++ b/src/core/game/Game.ts
@@ -796,6 +796,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 a35416364..2afef7aac 100644
--- a/src/core/game/GameView.ts
+++ b/src/core/game/GameView.ts
@@ -601,6 +601,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;
@@ -616,6 +617,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 0f26df368..f87be4153 100644
--- a/src/server/GameServer.ts
+++ b/src/server/GameServer.ts
@@ -462,6 +462,7 @@ export class GameServer {
clientID: c.clientID,
cosmetics: c.cosmetics,
})),
+ lobbyCreatorID: this.lobbyCreatorID,
});
if (!result.success) {
const error = z.prettifyError(result.error);