mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-27 07:04:36 +00:00
50bd075b1c
## Description: ### Problem When a host toggled off certain settings (game length, PVP immunity, starting gold, gold multiplier, disable alliances) in the host lobby modal, joiners still saw the old values. The settings appeared "stuck" once enabled. ### Root Cause `putGameConfig()` sent `undefined` for disabled settings, but `JSON.stringify` strips `undefined` properties entirely. The server's `!== undefined` guard never fired, so the old value was never cleared. ### Fix - **HostLobbyModal**: Send `null` instead of `undefined` when these settings are toggled off (`null` survives JSON serialization) - **Schemas**: Add `.nullable()` to the five affected fields (`maxTimerValue`, `spawnImmunityDuration`, `goldMultiplier`, `startingGold`, `disableAlliances`) - **GameServer**: Use `?? undefined` (nullish coalescing) to convert incoming `null` back to `undefined` when storing on the config Other settings are unaffected. Booleans like `infiniteGold` always send `true`/`false`, and fields like `bots`/`gameMap` always have a concrete value.. ## 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: FloPinguin --------- Co-authored-by: Evan <evanpelle@gmail.com>
247 lines
7.2 KiB
TypeScript
247 lines
7.2 KiB
TypeScript
import { html, LitElement } from "lit";
|
|
import { customElement, state } from "lit/decorators.js";
|
|
import { assetUrl } from "../../../core/AssetUrls";
|
|
import { EventBus } from "../../../core/EventBus";
|
|
import { GameType } from "../../../core/game/Game";
|
|
import { GameView } from "../../../core/game/GameView";
|
|
import { crazyGamesSDK } from "../../CrazyGamesSDK";
|
|
import { TogglePauseIntentEvent } from "../../InputHandler";
|
|
import { PauseGameIntentEvent, SendWinnerEvent } from "../../Transport";
|
|
import { translateText } from "../../Utils";
|
|
import { ImmunityBarVisibleEvent } from "./ImmunityTimer";
|
|
import { Layer } from "./Layer";
|
|
import { ShowReplayPanelEvent } from "./ReplayPanel";
|
|
import { ShowSettingsModalEvent } from "./SettingsModal";
|
|
import { SpawnBarVisibleEvent } from "./SpawnTimer";
|
|
const exitIcon = assetUrl("images/ExitIconWhite.svg");
|
|
const FastForwardIconSolid = assetUrl("images/FastForwardIconSolidWhite.svg");
|
|
const pauseIcon = assetUrl("images/PauseIconWhite.svg");
|
|
const playIcon = assetUrl("images/PlayIconWhite.svg");
|
|
const settingsIcon = assetUrl("images/SettingIconWhite.svg");
|
|
|
|
@customElement("game-right-sidebar")
|
|
export class GameRightSidebar extends LitElement implements Layer {
|
|
public game: GameView;
|
|
public eventBus: EventBus;
|
|
|
|
@state()
|
|
private _isSinglePlayer: boolean = false;
|
|
|
|
@state()
|
|
private _isReplayVisible: boolean = false;
|
|
|
|
@state()
|
|
private _isVisible: boolean = true;
|
|
|
|
@state()
|
|
private isPaused: boolean = false;
|
|
|
|
@state()
|
|
private timer: number = 0;
|
|
|
|
private hasWinner = false;
|
|
private isLobbyCreator = false;
|
|
private spawnBarVisible = false;
|
|
private immunityBarVisible = false;
|
|
|
|
createRenderRoot() {
|
|
return this;
|
|
}
|
|
|
|
init() {
|
|
this._isSinglePlayer =
|
|
this.game?.config()?.gameConfig()?.gameType === GameType.Singleplayer ||
|
|
this.game.config().isReplay();
|
|
this._isVisible = true;
|
|
this.game.inSpawnPhase();
|
|
|
|
this.eventBus.on(SpawnBarVisibleEvent, (e) => {
|
|
this.spawnBarVisible = e.visible;
|
|
this.updateParentOffset();
|
|
});
|
|
this.eventBus.on(ImmunityBarVisibleEvent, (e) => {
|
|
this.immunityBarVisible = e.visible;
|
|
this.updateParentOffset();
|
|
});
|
|
|
|
this.eventBus.on(SendWinnerEvent, () => {
|
|
this.hasWinner = true;
|
|
this.requestUpdate();
|
|
});
|
|
|
|
this.eventBus.on(TogglePauseIntentEvent, () => {
|
|
const isReplayOrSingleplayer =
|
|
this._isSinglePlayer || this.game?.config()?.isReplay();
|
|
if (isReplayOrSingleplayer || this.isLobbyCreator) {
|
|
this.onPauseButtonClick();
|
|
}
|
|
});
|
|
|
|
this.requestUpdate();
|
|
}
|
|
|
|
getTickIntervalMs() {
|
|
return 250;
|
|
}
|
|
|
|
tick() {
|
|
// Timer logic
|
|
// 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;
|
|
const spawnPhaseTurns = this.game.config().numSpawnPhaseTurns();
|
|
const ticks = this.game.ticks();
|
|
const gameTicks = Math.max(0, ticks - spawnPhaseTurns);
|
|
const elapsedSeconds = Math.floor(gameTicks / 10); // 10 ticks per second
|
|
|
|
if (this.game.inSpawnPhase()) {
|
|
this.timer =
|
|
maxTimerValue !== null && maxTimerValue !== undefined
|
|
? maxTimerValue * 60
|
|
: 0;
|
|
return;
|
|
}
|
|
|
|
if (this.hasWinner) {
|
|
return;
|
|
}
|
|
|
|
if (maxTimerValue !== null && maxTimerValue !== undefined) {
|
|
this.timer = Math.max(0, maxTimerValue * 60 - elapsedSeconds);
|
|
} else {
|
|
this.timer = elapsedSeconds;
|
|
}
|
|
}
|
|
|
|
private updateParentOffset(): void {
|
|
const offset =
|
|
(this.spawnBarVisible ? 7 : 0) + (this.immunityBarVisible ? 7 : 0);
|
|
const parent = this.parentElement as HTMLElement;
|
|
if (parent) {
|
|
parent.style.marginTop = `${offset}px`;
|
|
}
|
|
}
|
|
|
|
private secondsToHms = (d: number): string => {
|
|
const pad = (n: number) => (n < 10 ? `0${n}` : n);
|
|
|
|
const h = Math.floor(d / 3600);
|
|
const m = Math.floor((d % 3600) / 60);
|
|
const s = Math.floor((d % 3600) % 60);
|
|
|
|
if (h !== 0) {
|
|
return `${pad(h)}:${pad(m)}:${pad(s)}`;
|
|
} else {
|
|
return `${pad(m)}:${pad(s)}`;
|
|
}
|
|
};
|
|
|
|
private toggleReplayPanel(): void {
|
|
this._isReplayVisible = !this._isReplayVisible;
|
|
this.eventBus.emit(
|
|
new ShowReplayPanelEvent(this._isReplayVisible, this._isSinglePlayer),
|
|
);
|
|
}
|
|
|
|
private onPauseButtonClick() {
|
|
this.isPaused = !this.isPaused;
|
|
if (this.isPaused) {
|
|
crazyGamesSDK.gameplayStop();
|
|
} else {
|
|
crazyGamesSDK.gameplayStart();
|
|
}
|
|
this.eventBus.emit(new PauseGameIntentEvent(this.isPaused));
|
|
}
|
|
|
|
private async onExitButtonClick() {
|
|
const isAlive = this.game.myPlayer()?.isAlive();
|
|
if (isAlive) {
|
|
const isConfirmed = confirm(
|
|
translateText("help_modal.exit_confirmation"),
|
|
);
|
|
if (!isConfirmed) return;
|
|
}
|
|
await crazyGamesSDK.requestMidgameAd();
|
|
await crazyGamesSDK.gameplayStop();
|
|
// redirect to the home page
|
|
window.location.href = "/";
|
|
}
|
|
|
|
private onSettingsButtonClick() {
|
|
this.eventBus.emit(
|
|
new ShowSettingsModalEvent(true, this._isSinglePlayer, this.isPaused),
|
|
);
|
|
}
|
|
|
|
render() {
|
|
if (this.game === undefined) return html``;
|
|
|
|
const timerColor =
|
|
this.game.config().gameConfig().maxTimerValue !== undefined &&
|
|
this.game.config().gameConfig().maxTimerValue !== null &&
|
|
this.timer < 60
|
|
? "text-red-400"
|
|
: "";
|
|
|
|
return html`
|
|
<aside
|
|
class=${`w-fit flex flex-row items-center gap-3 py-2 px-3 bg-gray-800/92 backdrop-blur-sm shadow-xs min-[1200px]:rounded-lg rounded-bl-lg transition-transform duration-300 ease-out transform text-white ${
|
|
this._isVisible ? "translate-x-0" : "translate-x-full"
|
|
}`}
|
|
@contextmenu=${(e: Event) => e.preventDefault()}
|
|
>
|
|
<!-- In-game time -->
|
|
<div class=${timerColor}>${this.secondsToHms(this.timer)}</div>
|
|
|
|
<!-- Buttons -->
|
|
${this.maybeRenderReplayButtons()}
|
|
|
|
<div class="cursor-pointer" @click=${this.onSettingsButtonClick}>
|
|
<img src=${settingsIcon} alt="settings" width="20" height="20" />
|
|
</div>
|
|
|
|
<div class="cursor-pointer" @click=${this.onExitButtonClick}>
|
|
<img src=${exitIcon} alt="exit" width="20" height="20" />
|
|
</div>
|
|
</aside>
|
|
`;
|
|
}
|
|
|
|
maybeRenderReplayButtons() {
|
|
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>
|
|
`
|
|
: ""}
|
|
`;
|
|
}
|
|
}
|