Skip to content

Conversation

@MitchZinck
Copy link

If this PR fixes an issue, link it below. If not, delete these two lines.
Resolves #2686

Description:

  • Implemented feature for lobby creator to kick players in game.
  • Added new moderation option for lobby creator, with a kick player option if they aren't the creator, a bot, and exist in game.
  • Includes a confirm kick option, and keeps track of kicked players so that the kick option changes to "Already Kicked" if the kicked player panel is opened again on the kicked player.

Screenshot order:

  1. Open player panel
  2. Click on moderation
  3. Click on kick player and confirm kick
  4. Player is kicked, open same player panel again and observe change in kick status
  5. Receiving player kick message
Screenshot 2026-01-20 at 12 33 55 PM Screenshot 2026-01-20 at 11 58 58 AM Screenshot 2026-01-20 at 12 31 46 PM Screenshot 2026-01-20 at 11 57 58 AM Screenshot 2026-01-20 at 11 57 39 AM

Please complete the following:

  • I have added screenshots for all UI updates
  • I process any text displayed to the user through translateText() and I've added it to the en.json file
  • I have added relevant tests to the test directory
  • 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:

mitchfz

@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 20, 2026

Walkthrough

Adds lobby-creator moderation: client UI and modal to kick players, client state to track kicked players, server-side kick-reason propagation, new translations, tests for the kick flow, and a minor error-message translation tweak.

Changes

Cohort / File(s) Summary
Translations
resources/lang/en.json
Add player_panel keys: moderation, kick, kicked, kick_confirm; add top-level kick_reason entries: duplicate_session, lobby_creator.
Client — Player panel
src/client/graphics/layers/PlayerPanel.ts
Add moderationTarget state and kickedPlayerIDs set; add open/close/handle-kicked handlers; render moderation action button and modal; reset moderation state on show/send/hide.
Client — Moderation modal
src/client/graphics/layers/PlayerModerationModal.ts
New PlayerModerationModal LitElement with props eventBus,myPlayer,target,open,alreadyKicked; keyboard/backdrop handling, canKick gate, emits SendKickPlayerIntentEvent and local "kicked" event, renders confirmation and Kick button.
Server — Kick logic
src/server/GameServer.ts
Add KICK_REASON_* constants; extend kickClient(clientID, reasonKey = ...); propagate reasonKey in logs, error payloads, WebSocket close reason; mark kicked clients; update call sites to pass reason.
Client — Error translation
src/client/ClientGameRunner.ts
Prefer translated error text when building displayed error in showErrorModal.
Tests
tests/client/graphics/layers/PlayerPanelKick.test.ts
New test suite covering moderation UI rendering, modal open/close, confirm/cancel flows, event emission, and kicked-state behavior.
Misc / Manifest
package.json
Small manifest edit.

Sequence Diagram(s)

sequenceDiagram
    participant User as "Lobby Creator"
    participant Panel as "PlayerPanel"
    participant Modal as "PlayerModerationModal"
    participant EventBus as "EventBus"
    participant Server as "GameServer"
    participant Target as "Target Client"

    User->>Panel: open player actions / click Kick
    Panel->>Modal: openModeration(target)
    Modal->>User: show confirmation dialog
    User->>Modal: confirm kick
    Modal->>Panel: dispatch local "kicked" event (targetID)
    Modal->>EventBus: emit SendKickPlayerIntentEvent(targetID)
    EventBus->>Server: deliver kick intent (targetID)
    Server->>Server: kickClient(targetID, "lobby_creator")
    Server->>Target: send error (reasonKey) and close WebSocket
    Panel->>Panel: add targetID to kickedPlayerIDs and hide UI
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • Add handling of kick reasons #1654 — Both change GameServer.kickClient to accept and propagate a kick-reason parameter and update call sites/logging to send reason-specific messages; this PR implements that propagation.

Poem

A tiny shield, a modal bright,
A tap confirms, the socket tight,
A reason sent, a player flown,
Translations added, the logs now shown,
Lobby kept tidy, order known.

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: Kick player in game' accurately summarizes the main change - implementing the ability for lobby creators to kick players during gameplay.
Description check ✅ Passed The description is clearly related to the changeset, providing implementation details, addressing the PR objectives, and demonstrating the feature with screenshots.
Linked Issues check ✅ Passed The PR implementation meets all primary coding requirements from issue #2686: lobby creator can kick players in-game, kick button appears in player info panel, restrictions applied (not self/bots/absent), confirmation provided, and kicked state tracked.
Out of Scope Changes check ✅ Passed All changes are in-scope: translation entries for UI strings, moderation modal component, player panel integration, server-side kick logic with reason codes, and test coverage all directly support the kick-player feature.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Warning

Tools execution failed with the following error:

Failed to run tools: 13 INTERNAL: Received RST_STREAM with code 2 (Internal server error)


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@MitchZinck MitchZinck marked this pull request as ready for review January 20, 2026 17:41
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/client/graphics/layers/PlayerPanel.ts`:
- Around line 324-326: renderModeration currently displays the "Kick" button
when clientID !== null but handleKickClick uses a falsy check (const
targetClientID = other.clientID(); if (!targetClientID) return;) which allows
empty string/undefined to show a non-functional button; make the checks
consistent by changing handleKickClick to the same existence test used in
renderModeration (e.g., explicitly compare to null/undefined the same way
renderModeration does) or update renderModeration to use the same falsy check as
handleKickClick so the Kick button is only rendered when other.clientID() passes
the identical truthiness test; update both occurrences referenced
(handleKickClick and the other similar block) to use the same check.

In `@src/server/GameServer.ts`:
- Around line 777-820: kickClient currently only adds to kickedClients when the
client is present in activeClients, so a user who disconnected can still rejoin
after a moderation kick; update kickClient to also add the target clientID to
this.kickedClients when the client is not found (the else branch) — or at
minimum when reason indicates a moderation/lobby kick (reason !==
"duplicate_session") — so that kickedClients always blocks rejoin even if the
WebSocket is already gone; reference the kickClient method, this.activeClients,
and this.kickedClients when making the change.

@MitchZinck MitchZinck changed the title 2686 - Kick player in game feat: Kick player in game Jan 20, 2026
@ryanbarlow97
Copy link
Contributor

can you make it so a nation takes over? not sure how hard/possible it would be

coderabbitai[bot]
coderabbitai bot previously approved these changes Jan 20, 2026
@ryanbarlow97 ryanbarlow97 added this to the v30 milestone Jan 20, 2026
@MitchZinck
Copy link
Author

can you make it so a nation takes over? not sure how hard/possible it would be

I'm not sure if that is a wanted feature. I'm sure it's possible!

public kickClient(clientID: ClientID): void {
public kickClient(
clientID: ClientID,
reason: KickClientReason = "duplicate_session",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead can this be a string, and the server just passes a translation key

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/server/GameServer.ts (1)

333-363: Validate kick targets are in this game before blocking rejoin. Right now a lobby creator can send any clientID; since kickClient records it even if not active, this can block unknown IDs from ever joining. Please verify the target exists in allClients (or activeClients) before calling kickClient.

🔧 Suggested fix
               case "kick_player": {
                 // Check if the authenticated client is the lobby creator
                 if (client.clientID !== this.lobbyCreatorID) {
                   this.log.warn(`Only lobby creator can kick players`, {
                     clientID: client.clientID,
                     creatorID: this.lobbyCreatorID,
                     target: clientMsg.intent.target,
                     gameID: this.id,
                   });
                   return;
                 }

                 // Don't allow lobby creator to kick themselves
                 if (client.clientID === clientMsg.intent.target) {
                   this.log.warn(`Cannot kick yourself`, {
                     clientID: client.clientID,
                   });
                   return;
                 }
+
+                const targetClient = this.allClients.get(clientMsg.intent.target);
+                if (!targetClient) {
+                  this.log.warn(`Cannot kick client not in game`, {
+                    clientID: client.clientID,
+                    target: clientMsg.intent.target,
+                    gameID: this.id,
+                  });
+                  return;
+                }

                 // Log and execute the kick
                 this.log.info(`Lobby creator initiated kick of player`, {
                   creatorID: client.clientID,
                   target: clientMsg.intent.target,
                   gameID: this.id,
                   kickMethod: "websocket",
                 });

                 this.kickClient(clientMsg.intent.target, KICK_REASON_LOBBY_CREATOR);
                 return;
               }
🤖 Fix all issues with AI agents
In `@src/client/graphics/layers/PlayerModerationModal.ts`:
- Around line 53-74: The handler handleKickClick currently proceeds to
confirmation and dispatches a local "kicked" event even if this.eventBus is
null; update handleKickClick to guard for a missing eventBus early (before the
confirm dialog) and return if this.eventBus is falsy to avoid locally marking a
player kicked without sending the SendKickPlayerIntentEvent; ensure
alreadyKicked and dispatchEvent("kicked")/closeModal are only reached after
confirming and successfully emitting the eventBus event.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/server/GameServer.ts`:
- Around line 785-823: kickClient currently adds clientID to kickedClients
before verifying membership, which lets callers mark arbitrary IDs kicked;
change the flow in kickClient so you first check whether the target exists in
this.allClients (or this.activeClients) and only then add to this.kickedClients
and proceed with sending the error/close and removing from activeClients; if the
client is disconnected but present in this.allClients still add to kickedClients
(to block rejoin) but do not attempt to send/close a ws; preserve the existing
log messages (use this.allClients / this.activeClients and client.ws/send/close
references) and avoid adding to kickedClients on unknown IDs.

@MitchZinck
Copy link
Author

Screenshot 2026-01-21 at 2 56 34 PM

New modal, shows up after clicking the "Moderation" button

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/server/GameServer.ts (1)

334-365: Add target existence validation before calling kickClient.

The intent handler doesn't validate that the target client exists in activeClients before passing it to kickClient. Although kickClient itself checks allClients and logs a warning if not found, validation should happen at the API boundary in the intent handler. This matches the input validation principle: validate all client-provided data (the target ID) before processing.

                // Validate target exists in active clients
                const targetClient = this.activeClients.find(
                  (c) => c.clientID === clientMsg.intent.target,
                );
                if (!targetClient) {
                  this.log.warn(`Cannot kick: target not in active clients`, {
                    creatorID: client.clientID,
                    target: clientMsg.intent.target,
                    gameID: this.id,
                  });
                  return;
                }

                this.kickClient(
                  clientMsg.intent.target,
                  KICK_REASON_LOBBY_CREATOR,
                );
🧹 Nitpick comments (1)
src/server/GameServer.ts (1)

34-35: Type the kick reason with a union instead of string.

This is a small TS safety win and matches the “typed unions” guideline.

♻️ Suggested change
-const KICK_REASON_DUPLICATE_SESSION = "kick_reason.duplicate_session";
-const KICK_REASON_LOBBY_CREATOR = "kick_reason.lobby_creator";
+const KICK_REASON_DUPLICATE_SESSION = "kick_reason.duplicate_session" as const;
+const KICK_REASON_LOBBY_CREATOR = "kick_reason.lobby_creator" as const;
+type KickReasonKey =
+  | typeof KICK_REASON_DUPLICATE_SESSION
+  | typeof KICK_REASON_LOBBY_CREATOR;

 public kickClient(
   clientID: ClientID,
-  reasonKey: string = KICK_REASON_DUPLICATE_SESSION,
+  reasonKey: KickReasonKey = KICK_REASON_DUPLICATE_SESSION,
 ): void {

Also applies to: 785-788

coderabbitai[bot]
coderabbitai bot previously approved these changes Jan 22, 2026
other.type() === PlayerType.Human &&
!!other.clientID();

if (!canKick && !alreadyKicked) return html``;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we should the moderation panel regardless? and then the moderation panel can have the kick button grayed out if we already kicked the player? Otherwise people maybe get confused why moderation button dissapeared.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, good catch

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/client/graphics/layers/PlayerPanel.ts`:
- Around line 446-469: In PlayerPanel.renderModeration, don't render the
moderation button unless the local player is allowed to kick; add an early
return when canKick is false (and/or when alreadyKicked is true if you want to
hide after kicking) so the button only shows for
creator-only/human/not-self/in-game cases, and remove or repurpose the unused
locals to satisfy ESLint (use canKick and alreadyKicked in the guard or delete
the unused variable). Ensure you update the function renderModeration to return
nothing/empty markup when the guard fails so the moderation UI is not rendered.

@MitchZinck MitchZinck force-pushed the feature/2686-kick-player-in-game branch from cd3b1cc to 0d3fc2b Compare January 23, 2026 14:35
@MitchZinck MitchZinck force-pushed the feature/2686-kick-player-in-game branch from 0d3fc2b to 4ea751a Compare January 23, 2026 14:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow Lobby Creator to kick players in game

4 participants