diff --git a/resources/lang/en.json b/resources/lang/en.json index 030f08916..64c491ee9 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -442,6 +442,9 @@ "none": "None", "alliances": "Alliances" }, + "replay_panel": { + "replay_speed": "Replay speed" + }, "error_modal": { "crashed": "Game crashed!", "paste_discord": "Please paste the following in your bug report in Discord:", diff --git a/src/client/InputHandler.ts b/src/client/InputHandler.ts index a4ba1eae6..96fde5beb 100644 --- a/src/client/InputHandler.ts +++ b/src/client/InputHandler.ts @@ -1,6 +1,7 @@ import { EventBus, GameEvent } from "../core/EventBus"; import { UnitView } from "../core/game/GameView"; import { UserSettings } from "../core/game/UserSettings"; +import { ReplaySpeedMultiplier } from "./utilities/ReplaySpeedMultiplier"; export class MouseUpEvent implements GameEvent { constructor( @@ -82,6 +83,10 @@ export class AttackRatioEvent implements GameEvent { constructor(public readonly attackRatio: number) {} } +export class ReplaySpeedChangeEvent implements GameEvent { + constructor(public readonly replaySpeedMultiplier: ReplaySpeedMultiplier) {} +} + export class CenterCameraEvent implements GameEvent { constructor() {} } diff --git a/src/client/LangSelector.ts b/src/client/LangSelector.ts index 27b7f9399..ad1486b77 100644 --- a/src/client/LangSelector.ts +++ b/src/client/LangSelector.ts @@ -186,6 +186,7 @@ export class LangSelector extends LitElement { "game-starting-modal", "top-bar", "player-panel", + "replay-panel", "help-modal", "username-input", "public-lobby", diff --git a/src/client/LocalServer.ts b/src/client/LocalServer.ts index e3bc08dcf..73e4961d2 100644 --- a/src/client/LocalServer.ts +++ b/src/client/LocalServer.ts @@ -1,3 +1,4 @@ +import { EventBus } from "../core/EventBus"; import { AllPlayersStats, ClientMessage, @@ -12,7 +13,9 @@ import { } from "../core/Schemas"; import { createGameRecord, decompressGameRecord, replacer } from "../core/Util"; import { LobbyConfig } from "./ClientGameRunner"; +import { ReplaySpeedChangeEvent } from "./InputHandler"; import { getPersistentID } from "./Main"; +import { defaultReplaySpeedMultiplier } from "./utilities/ReplaySpeedMultiplier"; export class LocalServer { // All turns from the game record on replay. @@ -24,6 +27,7 @@ export class LocalServer { private startedAt: number; private paused = false; + private replaySpeedMultiplier = defaultReplaySpeedMultiplier; private winner: ClientSendWinnerMessage | null = null; private allPlayersStats: AllPlayersStats = {}; @@ -38,23 +42,29 @@ export class LocalServer { private clientConnect: () => void, private clientMessage: (message: ServerMessage) => void, private isReplay: boolean, + private eventBus: EventBus, ) {} start() { this.turnCheckInterval = setInterval(() => { - if (this.turnsExecuted === this.turns.length) { - if ( - this.isReplay || - Date.now() > - this.turnStartTime + this.lobbyConfig.serverConfig.turnIntervalMs() - ) { - this.turnStartTime = Date.now(); - // End turn on the server means the client will start processing the turn. - this.endTurn(); - } + const turnIntervalMs = + this.lobbyConfig.serverConfig.turnIntervalMs() * + this.replaySpeedMultiplier; + + if ( + this.turnsExecuted === this.turns.length && + Date.now() > this.turnStartTime + turnIntervalMs + ) { + this.turnStartTime = Date.now(); + // End turn on the server means the client will start processing the turn. + this.endTurn(); } }, 5); + this.eventBus.on(ReplaySpeedChangeEvent, (event) => { + this.replaySpeedMultiplier = event.replaySpeedMultiplier; + }); + this.startedAt = Date.now(); this.clientConnect(); if (this.lobbyConfig.gameRecord) { diff --git a/src/client/Transport.ts b/src/client/Transport.ts index f208a55a1..e9124b6f5 100644 --- a/src/client/Transport.ts +++ b/src/client/Transport.ts @@ -266,6 +266,7 @@ export class Transport { onconnect, onmessage, this.lobbyConfig.gameRecord !== undefined, + this.eventBus, ); this.localServer.start(); } diff --git a/src/client/graphics/GameRenderer.ts b/src/client/graphics/GameRenderer.ts index ee3bbc4b9..8699c2741 100644 --- a/src/client/graphics/GameRenderer.ts +++ b/src/client/graphics/GameRenderer.ts @@ -21,6 +21,7 @@ import { OptionsMenu } from "./layers/OptionsMenu"; import { PlayerInfoOverlay } from "./layers/PlayerInfoOverlay"; import { PlayerPanel } from "./layers/PlayerPanel"; import { PlayerTeamLabel } from "./layers/PlayerTeamLabel"; +import { ReplayPanel } from "./layers/ReplayPanel"; import { SpawnTimer } from "./layers/SpawnTimer"; import { StructureLayer } from "./layers/StructureLayer"; import { TeamStats } from "./layers/TeamStats"; @@ -126,6 +127,13 @@ export function createRenderer( optionsMenu.eventBus = eventBus; optionsMenu.game = game; + const replayPanel = document.querySelector("replay-panel") as ReplayPanel; + if (!(replayPanel instanceof ReplayPanel)) { + console.error("ReplayPanel element not found in the DOM"); + } + replayPanel.eventBus = eventBus; + replayPanel.game = game; + const topBar = document.querySelector("top-bar") as TopBar; if (!(topBar instanceof TopBar)) { console.error("top bar not found"); @@ -215,6 +223,7 @@ export function createRenderer( playerInfo, winModel, optionsMenu, + replayPanel, teamStats, topBar, playerPanel, diff --git a/src/client/graphics/layers/OptionsMenu.ts b/src/client/graphics/layers/OptionsMenu.ts index 56e71605a..79aa4dad5 100644 --- a/src/client/graphics/layers/OptionsMenu.ts +++ b/src/client/graphics/layers/OptionsMenu.ts @@ -1,4 +1,4 @@ -import { LitElement, html } from "lit"; +import { html, LitElement } from "lit"; import { customElement, state } from "lit/decorators.js"; import { EventBus } from "../../../core/EventBus"; import { GameType } from "../../../core/game/Game"; diff --git a/src/client/graphics/layers/ReplayPanel.ts b/src/client/graphics/layers/ReplayPanel.ts new file mode 100644 index 000000000..529a52ca1 --- /dev/null +++ b/src/client/graphics/layers/ReplayPanel.ts @@ -0,0 +1,123 @@ +import { html, LitElement } from "lit"; +import { customElement, state } from "lit/decorators.js"; +import { EventBus } from "../../../core/EventBus"; +import { GameType } from "../../../core/game/Game"; +import { GameView } from "../../../core/game/GameView"; +import { ReplaySpeedChangeEvent } from "../../InputHandler"; +import { + defaultReplaySpeedMultiplier, + ReplaySpeedMultiplier, +} from "../../utilities/ReplaySpeedMultiplier"; +import { translateText } from "../../Utils"; +import { Layer } from "./Layer"; + +@customElement("replay-panel") +export class ReplayPanel extends LitElement implements Layer { + public game: GameView | undefined; + public eventBus: EventBus | undefined; + + @state() + private _replaySpeedMultiplier: number = defaultReplaySpeedMultiplier; + + @state() + private _isVisible = false; + + init() { + if (this.game?.config().gameConfig().gameType === GameType.Singleplayer) { + this.setVisible(true); + } + } + + tick() { + if (!this._isVisible && this.game?.config().isReplay()) { + this.setVisible(true); + } + + this.requestUpdate(); + } + + onReplaySpeedChange(value: ReplaySpeedMultiplier) { + this._replaySpeedMultiplier = value; + this.eventBus?.emit(new ReplaySpeedChangeEvent(value)); + } + + renderLayer(context: CanvasRenderingContext2D) { + // Render any necessary canvas elements + } + + shouldTransform(): boolean { + return false; + } + + setVisible(visible: boolean) { + this._isVisible = visible; + this.requestUpdate(); + } + + render() { + if (!this._isVisible) { + return html``; + } + + return html` +