mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-28 01:35:36 +00:00
Added pause functionality for private multiplayer games (#2657)
If this PR fixes an issue, link it below. If not, delete these two lines. Resolves #2491 ## Description: Adds pause/unpause functionality for private multiplayer games. Only the lobby creator can pause the game, and all players see a pause overlay when the game is paused. **Key features:** - Lobby creator sees pause/play button in control panel (alongside existing singleplayer/replay controls) - Server validates that only lobby creator can toggle pause - All players see "Game paused by Lobby Creator" overlay when paused - Game state freezes (no turn execution) while paused - Unpause resumes normal gameplay **Implementation details:** - Server-side pause state (`isPaused`) prevents turn execution during pause - Each client receives `isLobbyCreator` flag in `GameStartInfo` to show/hide pause button - Added `TogglePauseIntent` that broadcasts to all clients via `NoOpExecution` - New `PauseOverlay` component (shows in single player also) ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] 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: furo18 <img width="1459" height="861" alt="Screenshot 2025-12-20 at 15 16 33" src="https://github.com/user-attachments/assets/f5a3222f-f54b-473c-b0f6-104ce4c1e7a8" />
This commit is contained in:
@@ -10,7 +10,7 @@ import { GameType } from "../../../core/game/Game";
|
||||
import { GameUpdateType } from "../../../core/game/GameUpdates";
|
||||
import { GameView } from "../../../core/game/GameView";
|
||||
import { crazyGamesSDK } from "../../CrazyGamesSDK";
|
||||
import { PauseGameEvent } from "../../Transport";
|
||||
import { PauseGameIntentEvent } from "../../Transport";
|
||||
import { translateText } from "../../Utils";
|
||||
import { Layer } from "./Layer";
|
||||
import { ShowReplayPanelEvent } from "./ReplayPanel";
|
||||
@@ -37,6 +37,7 @@ export class GameRightSidebar extends LitElement implements Layer {
|
||||
private timer: number = 0;
|
||||
|
||||
private hasWinner = false;
|
||||
private isLobbyCreator = false;
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
@@ -48,6 +49,7 @@ export class GameRightSidebar extends LitElement implements Layer {
|
||||
this.game.config().isReplay();
|
||||
this._isVisible = true;
|
||||
this.game.inSpawnPhase();
|
||||
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
@@ -57,6 +59,13 @@ export class GameRightSidebar extends LitElement implements Layer {
|
||||
if (updates) {
|
||||
this.hasWinner = this.hasWinner || updates[GameUpdateType.Win].length > 0;
|
||||
}
|
||||
|
||||
// Check if the player is the lobby creator
|
||||
if (!this.isLobbyCreator && this.game.myPlayer()?.isLobbyCreator()) {
|
||||
this.isLobbyCreator = true;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
const maxTimerValue = this.game.config().gameConfig().maxTimerValue;
|
||||
if (maxTimerValue !== undefined) {
|
||||
if (this.game.inSpawnPhase()) {
|
||||
@@ -96,7 +105,7 @@ export class GameRightSidebar extends LitElement implements Layer {
|
||||
|
||||
private onPauseButtonClick() {
|
||||
this.isPaused = !this.isPaused;
|
||||
this.eventBus.emit(new PauseGameEvent(this.isPaused));
|
||||
this.eventBus.emit(new PauseGameIntentEvent(this.isPaused));
|
||||
}
|
||||
|
||||
private onExitButtonClick() {
|
||||
@@ -153,25 +162,35 @@ export class GameRightSidebar extends LitElement implements Layer {
|
||||
}
|
||||
|
||||
maybeRenderReplayButtons() {
|
||||
if (this._isSinglePlayer || this.game?.config()?.isReplay()) {
|
||||
return html` <div class="cursor-pointer" @click=${this.toggleReplayPanel}>
|
||||
<img
|
||||
src=${FastForwardIconSolid}
|
||||
alt="replay"
|
||||
width="20"
|
||||
height="20"
|
||||
/>
|
||||
</div>
|
||||
<div class="cursor-pointer" @click=${this.onPauseButtonClick}>
|
||||
<img
|
||||
src=${this.isPaused ? playIcon : pauseIcon}
|
||||
alt="play/pause"
|
||||
width="20"
|
||||
height="20"
|
||||
/>
|
||||
</div>`;
|
||||
} else {
|
||||
return html``;
|
||||
}
|
||||
const isReplayOrSingleplayer =
|
||||
this._isSinglePlayer || this.game?.config()?.isReplay();
|
||||
const showPauseButton = isReplayOrSingleplayer || this.isLobbyCreator;
|
||||
|
||||
return html`
|
||||
${isReplayOrSingleplayer
|
||||
? html`
|
||||
<div class="cursor-pointer" @click=${this.toggleReplayPanel}>
|
||||
<img
|
||||
src=${FastForwardIconSolid}
|
||||
alt="replay"
|
||||
width="20"
|
||||
height="20"
|
||||
/>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
${showPauseButton
|
||||
? html`
|
||||
<div class="cursor-pointer" @click=${this.onPauseButtonClick}>
|
||||
<img
|
||||
src=${this.isPaused ? playIcon : pauseIcon}
|
||||
alt="play/pause"
|
||||
width="20"
|
||||
height="20"
|
||||
/>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { GameType } from "../../../core/game/Game";
|
||||
import { GameUpdateType } from "../../../core/game/GameUpdates";
|
||||
import { GameView } from "../../../core/game/GameView";
|
||||
import { translateText } from "../../Utils";
|
||||
import { Layer } from "./Layer";
|
||||
@@ -11,6 +13,9 @@ export class HeadsUpMessage extends LitElement implements Layer {
|
||||
@state()
|
||||
private isVisible = false;
|
||||
|
||||
@state()
|
||||
private isPaused = false;
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
@@ -21,10 +26,27 @@ export class HeadsUpMessage extends LitElement implements Layer {
|
||||
}
|
||||
|
||||
tick() {
|
||||
if (!this.game.inSpawnPhase()) {
|
||||
this.isVisible = false;
|
||||
this.requestUpdate();
|
||||
const updates = this.game.updatesSinceLastTick();
|
||||
if (updates && updates[GameUpdateType.GamePaused].length > 0) {
|
||||
const pauseUpdate = updates[GameUpdateType.GamePaused][0];
|
||||
this.isPaused = pauseUpdate.paused;
|
||||
}
|
||||
|
||||
this.isVisible = this.game.inSpawnPhase() || this.isPaused;
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private getMessage(): string {
|
||||
if (this.isPaused) {
|
||||
if (this.game.config().gameConfig().gameType === GameType.Singleplayer) {
|
||||
return translateText("pause.singleplayer_game_paused");
|
||||
} else {
|
||||
return translateText("pause.multiplayer_game_paused");
|
||||
}
|
||||
}
|
||||
return this.game.config().isRandomSpawn()
|
||||
? translateText("heads_up_message.random_spawn")
|
||||
: translateText("heads_up_message.choose_spawn");
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -32,17 +54,17 @@ export class HeadsUpMessage extends LitElement implements Layer {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const message = this.getMessage();
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="flex items-center relative
|
||||
w-full justify-evenly h-8 lg:h-10 md:top-[70px] left-0 lg:left-4
|
||||
bg-opacity-60 bg-gray-900 rounded-md lg:rounded-lg
|
||||
w-full justify-evenly h-8 lg:h-10 md:top-[70px] left-0 lg:left-4
|
||||
bg-opacity-60 bg-gray-900 rounded-md lg:rounded-lg
|
||||
backdrop-blur-md text-white text-md lg:text-xl p-1 lg:p-2"
|
||||
@contextmenu=${(e: MouseEvent) => e.preventDefault()}
|
||||
>
|
||||
${this.game.config().isRandomSpawn()
|
||||
? translateText("heads_up_message.random_spawn")
|
||||
: translateText("heads_up_message.choose_spawn")}
|
||||
${message}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import musicIcon from "../../../../resources/images/music.svg";
|
||||
import { EventBus } from "../../../core/EventBus";
|
||||
import { UserSettings } from "../../../core/game/UserSettings";
|
||||
import { AlternateViewEvent, RefreshGraphicsEvent } from "../../InputHandler";
|
||||
import { PauseGameEvent } from "../../Transport";
|
||||
import { PauseGameIntentEvent } from "../../Transport";
|
||||
import { translateText } from "../../Utils";
|
||||
import SoundManager from "../../sound/SoundManager";
|
||||
import { Layer } from "./Layer";
|
||||
@@ -108,7 +108,7 @@ export class SettingsModal extends LitElement implements Layer {
|
||||
|
||||
private pauseGame(pause: boolean) {
|
||||
if (this.shouldPause && !this.wasPausedWhenOpened)
|
||||
this.eventBus.emit(new PauseGameEvent(pause));
|
||||
this.eventBus.emit(new PauseGameIntentEvent(pause));
|
||||
}
|
||||
|
||||
private onTerrainButtonClick() {
|
||||
|
||||
Reference in New Issue
Block a user