Feat: Game Speed + Pause keybinds (#3397)

## Description:

Can't tell you how many times I've been playing solo, I try to go change
the speed from `Max` to `x1` and before I've opened the speed controls
and clicked on one the AI completely wipes me. But not to worry, we now
have a pause and speed toggle up/down keybinds!


https://github.com/user-attachments/assets/48692c27-888f-40fb-837a-45e26f262441

Keybinds were added to "Menu Shortcuts" submenu:

<img width="1750" height="1099" alt="image"
src="https://github.com/user-attachments/assets/8c4500d5-f43e-4a1c-9940-04db75bf18cf"
/>

Tested on Solo, custom match, and public lobbies. Pause intent works
correctly on solo and private match (only if you are host), and neither
feature works in public matches, as expected.



## 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:

bijx
This commit is contained in:
bijx
2026-03-16 19:16:59 -04:00
committed by GitHub
parent c76ffd3641
commit f356f09f81
7 changed files with 136 additions and 2 deletions
+8
View File
@@ -97,6 +97,8 @@
"action_build": "Open build menu",
"action_emote": "Open emote menu",
"action_center": "Center camera on player",
"action_pause_game": "Pause / Resume game",
"action_game_speed": "Game speed down / up (single player)",
"action_zoom": "Zoom out/in",
"action_move_camera": "Move camera",
"action_ratio_change": "Decrease/Increase attack ratio",
@@ -571,6 +573,12 @@
"build_menu_modifier_desc": "Hold this key while clicking to open the build menu.",
"emoji_menu_modifier": "Emoji Menu Modifier",
"emoji_menu_modifier_desc": "Hold this key while clicking to open the emoji menu.",
"pause_game": "Pause",
"pause_game_desc": "Pause or resume the game (single player and custom games for host).",
"game_speed_up": "Game Speed Up",
"game_speed_up_desc": "Cycle to next game speed (0.5, 1, 2, max). Single player only.",
"game_speed_down": "Game Speed Down",
"game_speed_down_desc": "Cycle to previous game speed. Single player only.",
"attack_ratio_controls": "Attack Ratio Controls",
"attack_ratio_up": "Increase Attack Ratio",
"attack_ratio_up_desc": "Increase attack ratio by {amount}%",
+24
View File
@@ -58,6 +58,9 @@ export class HelpModal extends BaseModal {
modifierKey: isMac ? "MetaLeft" : "ControlLeft",
altKey: "AltLeft",
resetGfx: "KeyR",
pauseGame: "KeyP",
gameSpeedUp: "Period",
gameSpeedDown: "Comma",
...saved,
};
}
@@ -81,6 +84,8 @@ export class HelpModal extends BaseModal {
ArrowDown: "↓",
ArrowLeft: "←",
ArrowRight: "→",
Period: ">",
Comma: "<",
};
if (specialLabels[code]) return specialLabels[code];
@@ -372,6 +377,25 @@ export class HelpModal extends BaseModal {
${translateText("help_modal.action_center")}
</td>
</tr>
<tr class="hover:bg-white/5 transition-colors">
<td class="py-3 pl-4 border-b border-white/5">
${this.renderKey(keybinds.pauseGame)}
</td>
<td class="py-3 border-b border-white/5 text-white/70">
${translateText("help_modal.action_pause_game")}
</td>
</tr>
<tr class="hover:bg-white/5 transition-colors">
<td class="py-3 pl-4 border-b border-white/5">
<div class="flex flex-wrap gap-2">
${this.renderKey(keybinds.gameSpeedDown)}
${this.renderKey(keybinds.gameSpeedUp)}
</div>
</td>
<td class="py-3 border-b border-white/5 text-white/70">
${translateText("help_modal.action_game_speed")}
</td>
</tr>
<tr class="hover:bg-white/5 transition-colors">
<td class="py-3 pl-4 border-b border-white/5">
<div class="flex flex-wrap gap-2">
+22 -1
View File
@@ -123,6 +123,12 @@ export class ReplaySpeedChangeEvent implements GameEvent {
constructor(public readonly replaySpeedMultiplier: ReplaySpeedMultiplier) {}
}
export class TogglePauseIntentEvent implements GameEvent {}
export class GameSpeedUpIntentEvent implements GameEvent {}
export class GameSpeedDownIntentEvent implements GameEvent {}
export class CenterCameraEvent implements GameEvent {
constructor() {}
}
@@ -236,6 +242,9 @@ export class InputHandler {
buildAtomBomb: "Digit8",
buildHydrogenBomb: "Digit9",
buildMIRV: "Digit0",
pauseGame: "KeyP",
gameSpeedUp: "Period",
gameSpeedDown: "Comma",
...saved,
};
@@ -433,8 +442,20 @@ export class InputHandler {
this.eventBus.emit(new SwapRocketDirectionEvent(nextDirection));
}
if (!e.repeat && e.code === this.keybinds.pauseGame) {
e.preventDefault();
this.eventBus.emit(new TogglePauseIntentEvent());
}
if (!e.repeat && e.code === this.keybinds.gameSpeedUp) {
e.preventDefault();
this.eventBus.emit(new GameSpeedUpIntentEvent());
}
if (!e.repeat && e.code === this.keybinds.gameSpeedDown) {
e.preventDefault();
this.eventBus.emit(new GameSpeedDownIntentEvent());
}
// Shift-D to toggle performance overlay
console.log(e.code, e.shiftKey, e.ctrlKey, e.altKey, e.metaKey);
if (e.code === "KeyD" && e.shiftKey) {
e.preventDefault();
console.log("TogglePerformanceOverlayEvent");
+33 -1
View File
@@ -20,12 +20,24 @@ import {
} from "../core/Util";
import { getPersistentID } from "./Auth";
import { LobbyConfig } from "./ClientGameRunner";
import { ReplaySpeedChangeEvent } from "./InputHandler";
import {
GameSpeedDownIntentEvent,
GameSpeedUpIntentEvent,
ReplaySpeedChangeEvent,
} from "./InputHandler";
import {
defaultReplaySpeedMultiplier,
ReplaySpeedMultiplier,
} from "./utilities/ReplaySpeedMultiplier";
// Order: 0.5, 1, 2, max (same as ReplayPanel)
const SPEED_ORDER: ReplaySpeedMultiplier[] = [
ReplaySpeedMultiplier.slow,
ReplaySpeedMultiplier.normal,
ReplaySpeedMultiplier.fast,
ReplaySpeedMultiplier.fastest,
];
// build a small backlog so MAX can catch up.
const MAX_REPLAY_BACKLOG_TURNS = 60;
@@ -94,6 +106,26 @@ export class LocalServer {
this.replaySpeedMultiplier = event.replaySpeedMultiplier;
});
if (!this.isReplay) {
this.eventBus.on(GameSpeedUpIntentEvent, () => {
const idx = SPEED_ORDER.indexOf(this.replaySpeedMultiplier);
if (idx < 0 || idx >= SPEED_ORDER.length - 1) return;
this.replaySpeedMultiplier = SPEED_ORDER[idx + 1];
this.eventBus.emit(
new ReplaySpeedChangeEvent(this.replaySpeedMultiplier),
);
});
this.eventBus.on(GameSpeedDownIntentEvent, () => {
const idx = SPEED_ORDER.indexOf(this.replaySpeedMultiplier);
if (idx <= 0) return;
this.replaySpeedMultiplier = SPEED_ORDER[idx - 1];
this.eventBus.emit(
new ReplaySpeedChangeEvent(this.replaySpeedMultiplier),
);
});
}
this.startedAt = Date.now();
this.clientConnect();
if (this.lobbyConfig.gameRecord) {
+33
View File
@@ -47,6 +47,9 @@ const DefaultKeybinds: Record<string, string> = {
moveRight: "KeyD",
modifierKey: isMac ? "MetaLeft" : "ControlLeft",
altKey: "AltLeft",
pauseGame: "KeyP",
gameSpeedUp: "Period",
gameSpeedDown: "Comma",
};
@customElement("user-setting")
@@ -634,6 +637,36 @@ export class UserSettingModal extends BaseModal {
@change=${this.handleKeybindChange}
></setting-keybind>
<setting-keybind
action="pauseGame"
label=${translateText("user_setting.pause_game")}
description=${translateText("user_setting.pause_game_desc")}
.defaultKey=${DefaultKeybinds.pauseGame}
.value=${this.getKeyValue("pauseGame")}
.display=${this.getKeyChar("pauseGame")}
@change=${this.handleKeybindChange}
></setting-keybind>
<setting-keybind
action="gameSpeedUp"
label=${translateText("user_setting.game_speed_up")}
description=${translateText("user_setting.game_speed_up_desc")}
.defaultKey=${DefaultKeybinds.gameSpeedUp}
.value=${this.getKeyValue("gameSpeedUp")}
.display=${this.getKeyChar("gameSpeedUp")}
@change=${this.handleKeybindChange}
></setting-keybind>
<setting-keybind
action="gameSpeedDown"
label=${translateText("user_setting.game_speed_down")}
description=${translateText("user_setting.game_speed_down_desc")}
.defaultKey=${DefaultKeybinds.gameSpeedDown}
.value=${this.getKeyValue("gameSpeedDown")}
.display=${this.getKeyChar("gameSpeedDown")}
@change=${this.handleKeybindChange}
></setting-keybind>
<h2
class="text-blue-200 text-xl font-bold mt-8 mb-3 border-b border-white/10 pb-2"
>
@@ -4,6 +4,7 @@ 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";
@@ -67,6 +68,14 @@ export class GameRightSidebar extends LitElement implements Layer {
this.requestUpdate();
});
this.eventBus.on(TogglePauseIntentEvent, () => {
const isReplayOrSingleplayer =
this._isSinglePlayer || this.game?.config()?.isReplay();
if (isReplayOrSingleplayer || this.isLobbyCreator) {
this.onPauseButtonClick();
}
});
this.requestUpdate();
}
@@ -41,6 +41,13 @@ export class ReplayPanel extends LitElement implements Layer {
this.visible = event.visible;
this.isSingleplayer = event.isSingleplayer;
});
this.eventBus.on(
ReplaySpeedChangeEvent,
(event: ReplaySpeedChangeEvent) => {
this._replaySpeedMultiplier = event.replaySpeedMultiplier;
this.requestUpdate();
},
);
}
}