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);