"Catching up..." HeadsUpMessage 🏃‍♀️ (#3194)

## Description:

After an internet problem or page reload the game catches up, replaying
the ticks.

But especially new players might be confused what is happening. The game
runs fast???
And you can't easily tell when its finished catching up. You need to
spot when it stops running faster than usual.

So add a HeadsUpMessage to tell people what is happening.


https://github.com/user-attachments/assets/6fcdd85f-c58e-4549-89d0-5ba51df39339

## 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: iamlewis <lewismmmm@gmail.com>
This commit is contained in:
FloPinguin
2026-02-16 20:08:11 +01:00
committed by GitHub
parent 086a7e9000
commit d0bb3a016e
6 changed files with 37 additions and 5 deletions
+2 -1
View File
@@ -849,7 +849,8 @@
"random_spawn": "Random spawn is enabled. Selecting starting location for you...",
"singleplayer_game_paused": "Game paused",
"multiplayer_game_paused": "Game paused by Lobby Creator",
"pvp_immunity_active": "PVP immunity active for {seconds}s"
"pvp_immunity_active": "PVP immunity active for {seconds}s",
"catching_up": "Catching up..."
},
"territory_patterns": {
"title": "Skins",
+25 -1
View File
@@ -19,6 +19,12 @@ export class HeadsUpMessage extends LitElement implements Layer {
@state()
private isImmunityActive = false;
@state()
private isCatchingUp = false;
private catchingUpTicks = 0;
private static readonly CATCHING_UP_SHOW_THRESHOLD = 10;
@state()
private toastMessage: string | import("lit").TemplateResult | null = null;
@state()
@@ -92,12 +98,30 @@ export class HeadsUpMessage extends LitElement implements Layer {
this.game.isSpawnImmunityActive() &&
ticksSinceSpawnEnd < showImmunityHudDuration;
const currentlyCatchingUp =
!this.game.config().isReplay() && this.game.isCatchingUp();
if (currentlyCatchingUp) {
this.catchingUpTicks++;
} else {
this.catchingUpTicks = 0;
}
this.isCatchingUp =
this.catchingUpTicks >= HeadsUpMessage.CATCHING_UP_SHOW_THRESHOLD;
this.isVisible =
this.game.inSpawnPhase() || this.isPaused || this.isImmunityActive;
this.game.inSpawnPhase() ||
this.isPaused ||
this.isImmunityActive ||
this.isCatchingUp;
this.requestUpdate();
}
private getMessage(): string {
if (this.isCatchingUp) {
return translateText("heads_up_message.catching_up");
}
if (this.isPaused) {
if (this.game.config().gameConfig().gameType === GameType.Singleplayer) {
return translateText("heads_up_message.singleplayer_game_paused");
+2 -1
View File
@@ -118,7 +118,7 @@ export class GameRunner {
this.turns.push(turn);
}
public executeNextTick(): boolean {
public executeNextTick(pendingTurns?: number): boolean {
if (this.isExecuting) {
return false;
}
@@ -182,6 +182,7 @@ export class GameRunner {
updates: updates,
playerNameViewData: this.playerViewData,
tickExecutionDuration: tickExecutionDuration,
pendingTurns: pendingTurns ?? 0,
});
this.isExecuting = false;
return true;
+1
View File
@@ -20,6 +20,7 @@ export interface GameUpdateViewData {
packedTileUpdates: BigUint64Array;
playerNameViewData: Record<string, NameViewData>;
tickExecutionDuration?: number;
pendingTurns?: number;
}
export interface ErrorUpdate {
+4
View File
@@ -637,6 +637,10 @@ export class GameView implements GameMap {
return this.lastUpdate?.updates ?? null;
}
public isCatchingUp(): boolean {
return (this.lastUpdate?.pendingTurns ?? 0) > 1;
}
public update(gu: GameUpdateViewData) {
this.toDelete.forEach((id) => this._units.delete(id));
this.toDelete.clear();
+3 -2
View File
@@ -42,9 +42,10 @@ ctx.addEventListener("message", async (e: MessageEvent<MainThreadMessage>) => {
if (!gr) {
break;
}
const ticksToRun = Math.min(gr.pendingTurns(), MAX_TICKS_PER_HEARTBEAT);
const pendingTurns = gr.pendingTurns();
const ticksToRun = Math.min(pendingTurns, MAX_TICKS_PER_HEARTBEAT);
for (let i = 0; i < ticksToRun; i++) {
if (!gr.executeNextTick()) {
if (!gr.executeNextTick(gr.pendingTurns())) {
break;
}
}