From 1776ae4f359c34938a6f799a1ca1e08eb9bfe92a Mon Sep 17 00:00:00 2001 From: Evan Date: Wed, 29 Apr 2026 22:29:41 -0600 Subject: [PATCH] go to player on spawn start (#3802) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description: Some new players were having trouble finding themselves on game start * Emits a GoToPlayerEvent (zoom=8) on the first turn after the spawn phase, using a hasGoneToPlayer flag to ensure it only fires once per session * Adds a zoom parameter to GoToPlayerEvent so callers can specify a target zoom level * Adds smooth zoom animation to TransformHandler — the camera now eases to the target scale alongside the existing position easing, with screen-center correction to avoid visual jumping on mobile (where canvas and map dimensions differ) * Moves GoToPlayerEvent, GoToPositionEvent, and GoToUnitEvent out of Leaderboard.ts into TransformHandler.ts, where they logically belong ## 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: evan --- src/client/ClientGameRunner.ts | 15 ++++- src/client/graphics/TransformHandler.ts | 61 ++++++++++++++++---- src/client/graphics/layers/AttacksDisplay.ts | 6 +- src/client/graphics/layers/EventsDisplay.ts | 2 +- src/client/graphics/layers/Leaderboard.ts | 20 +------ 5 files changed, 70 insertions(+), 34 deletions(-) diff --git a/src/client/ClientGameRunner.ts b/src/client/ClientGameRunner.ts index 8d55d0cd8..7fd16a306 100644 --- a/src/client/ClientGameRunner.ts +++ b/src/client/ClientGameRunner.ts @@ -53,7 +53,7 @@ import { } from "./Transport"; import { createCanvas } from "./Utils"; import { createRenderer, GameRenderer } from "./graphics/GameRenderer"; -import { GoToPlayerEvent } from "./graphics/layers/Leaderboard"; +import { GoToPlayerEvent } from "./graphics/TransformHandler"; import { SoundManager } from "./sound/SoundManager"; export interface LobbyConfig { @@ -441,6 +441,8 @@ export class ClientGameRunner { console.log("Connected to game server!"); this.transport.rejoinGame(this.turnsSeen); }; + + let hasGoneToPlayer = false; const onmessage = (message: ServerMessage) => { this.lastMessageTime = Date.now(); if (message.type === "start") { @@ -472,7 +474,7 @@ export class ClientGameRunner { return; } - this.eventBus.emit(new GoToPlayerEvent(myPlayer)); + this.eventBus.emit(new GoToPlayerEvent(myPlayer, 10)); }; goToPlayer(); @@ -519,6 +521,15 @@ export class ClientGameRunner { ); } if (message.type === "turn") { + if ( + !this.gameView.inSpawnPhase() && + !hasGoneToPlayer && + this.gameView.myPlayer() + ) { + hasGoneToPlayer = true; + this.eventBus.emit(new GoToPlayerEvent(this.gameView.myPlayer()!, 8)); + } + // Track when we receive the turn to calculate delay const now = Date.now(); if (this.lastTickReceiveTime > 0) { diff --git a/src/client/graphics/TransformHandler.ts b/src/client/graphics/TransformHandler.ts index 6b6159694..90966525c 100644 --- a/src/client/graphics/TransformHandler.ts +++ b/src/client/graphics/TransformHandler.ts @@ -1,12 +1,25 @@ -import { EventBus } from "../../core/EventBus"; +import { EventBus, GameEvent } from "../../core/EventBus"; import { Cell } from "../../core/game/Game"; -import { GameView } from "../../core/game/GameView"; +import { GameView, PlayerView, UnitView } from "../../core/game/GameView"; import { CenterCameraEvent, DragEvent, ZoomEvent } from "../InputHandler"; -import { - GoToPlayerEvent, - GoToPositionEvent, - GoToUnitEvent, -} from "./layers/Leaderboard"; + +export class GoToPlayerEvent implements GameEvent { + constructor( + public player: PlayerView, + public zoom?: number, + ) {} +} + +export class GoToPositionEvent implements GameEvent { + constructor( + public x: number, + public y: number, + ) {} +} + +export class GoToUnitEvent implements GameEvent { + constructor(public unit: UnitView) {} +} export const GOTO_INTERVAL_MS = 16; export const CAMERA_MAX_SPEED = 15; @@ -20,6 +33,7 @@ export class TransformHandler { private lastGoToCallTime: number | null = null; private target: Cell | null; + private targetScale: number | null = null; private intervalID: NodeJS.Timeout | null = null; private changed = false; @@ -183,6 +197,7 @@ export class TransformHandler { return; } this.target = new Cell(nameLocation.x, nameLocation.y); + this.targetScale = event.zoom ?? null; this.intervalID = setInterval(() => this.goTo(), GOTO_INTERVAL_MS); } @@ -214,10 +229,12 @@ export class TransformHandler { if (this.target === null) throw new Error("null target"); - if ( - Math.abs(this.target.x - screenX) + Math.abs(this.target.y - screenY) < - 2 - ) { + const positionClose = + Math.abs(this.target.x - screenX) + Math.abs(this.target.y - screenY) < 2; + const scaleClose = + this.targetScale === null || + Math.abs(this.scale - this.targetScale) < 0.01; + if (positionClose && scaleClose) { this.clearTarget(); return; } @@ -242,6 +259,27 @@ export class TransformHandler { -CAMERA_MAX_SPEED, ); + if (this.targetScale !== null) { + const oldScale = this.scale; + const zoomSmoothing = 0.7; + const zoomR = 1 - Math.pow(zoomSmoothing, dt / 1000); + const diff = this.targetScale - this.scale; + const smoothStep = diff * zoomR; + const minStep = + Math.sign(diff) * Math.min(Math.abs(diff), (6.0 * dt) / 1000); + this.scale += + Math.abs(smoothStep) >= Math.abs(minStep) ? smoothStep : minStep; + // Keep screen center pinned as scale changes: (canvasSize - mapSize) / (2 * scale) + // shifts the apparent center when canvas != map dimensions (always on mobile). + const { width: canvasWidth, height: canvasHeight } = this.boundingRect(); + this.offsetX += + (canvasWidth - this.game.width()) * + (1 / (2 * oldScale) - 1 / (2 * this.scale)); + this.offsetY += + (canvasHeight - this.game.height()) * + (1 / (2 * oldScale) - 1 / (2 * this.scale)); + } + this.changed = true; } @@ -321,6 +359,7 @@ export class TransformHandler { this.intervalID = null; } this.target = null; + this.targetScale = null; } override(x: number = 0, y: number = 0, s: number = 1) { diff --git a/src/client/graphics/layers/AttacksDisplay.ts b/src/client/graphics/layers/AttacksDisplay.ts index d745b6bd6..5cadfc192 100644 --- a/src/client/graphics/layers/AttacksDisplay.ts +++ b/src/client/graphics/layers/AttacksDisplay.ts @@ -16,13 +16,13 @@ import { } from "../../Transport"; import { renderTroops, translateText } from "../../Utils"; import { getColoredSprite } from "../SpriteLoader"; -import { UIState } from "../UIState"; -import { Layer } from "./Layer"; import { GoToPlayerEvent, GoToPositionEvent, GoToUnitEvent, -} from "./Leaderboard"; +} from "../TransformHandler"; +import { UIState } from "../UIState"; +import { Layer } from "./Layer"; const soldierIcon = assetUrl("images/SoldierIcon.svg"); const swordIcon = assetUrl("images/SwordIcon.svg"); diff --git a/src/client/graphics/layers/EventsDisplay.ts b/src/client/graphics/layers/EventsDisplay.ts index bd4b02adb..cb6eb6211 100644 --- a/src/client/graphics/layers/EventsDisplay.ts +++ b/src/client/graphics/layers/EventsDisplay.ts @@ -34,7 +34,7 @@ import { Layer } from "./Layer"; import { GameView, PlayerView, UnitView } from "../../../core/game/GameView"; import { onlyImages } from "../../../core/Util"; import { renderNumber } from "../../Utils"; -import { GoToPlayerEvent, GoToUnitEvent } from "./Leaderboard"; +import { GoToPlayerEvent, GoToUnitEvent } from "../TransformHandler"; import { PlaySoundEffectEvent } from "../../sound/Sounds"; import { getMessageTypeClasses, translateText } from "../../Utils"; diff --git a/src/client/graphics/layers/Leaderboard.ts b/src/client/graphics/layers/Leaderboard.ts index fa4e495bc..3100e88d9 100644 --- a/src/client/graphics/layers/Leaderboard.ts +++ b/src/client/graphics/layers/Leaderboard.ts @@ -2,9 +2,10 @@ import { LitElement, html } from "lit"; import { customElement, property, state } from "lit/decorators.js"; import { repeat } from "lit/directives/repeat.js"; import { renderTroops, translateText } from "../../../client/Utils"; -import { EventBus, GameEvent } from "../../../core/EventBus"; -import { GameView, PlayerView, UnitView } from "../../../core/game/GameView"; +import { EventBus } from "../../../core/EventBus"; +import { GameView, PlayerView } from "../../../core/game/GameView"; import { formatPercentage, renderNumber } from "../../Utils"; +import { GoToPlayerEvent } from "../TransformHandler"; import { Layer } from "./Layer"; interface Entry { @@ -18,21 +19,6 @@ interface Entry { player: PlayerView; } -export class GoToPlayerEvent implements GameEvent { - constructor(public player: PlayerView) {} -} - -export class GoToPositionEvent implements GameEvent { - constructor( - public x: number, - public y: number, - ) {} -} - -export class GoToUnitEvent implements GameEvent { - constructor(public unit: UnitView) {} -} - @customElement("leader-board") export class Leaderboard extends LitElement implements Layer { public game: GameView | null = null;