From 1b2cc0cd86594b419b002254148bdf8f20facf42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B5=D0=BD=D0=B8=D1=81=20=D0=A8=D0=BA=D0=B0=D0=B1?= =?UTF-8?q?=D0=B0=D1=82=D1=83=D1=80?= Date: Thu, 12 Feb 2026 18:22:40 +0300 Subject: [PATCH] fix: HUD elements no longer shift when zooming in/out setScrollFactor(0) prevents camera scroll but NOT zoom displacement. Added fixToScreen() utility that compensates object positions and scale each frame based on current camera zoom. Applied to all scrollFactor(0) UI elements in GameScene, Minimap, and BossArenaScene. Co-authored-by: Cursor --- src/scenes/BossArenaScene.ts | 11 +++++++++++ src/scenes/GameScene.ts | 13 +++++++++++++ src/ui/screen-fix.ts | 31 +++++++++++++++++++++++++++++++ src/world/minimap.ts | 10 ++++++++++ 4 files changed, 65 insertions(+) create mode 100644 src/ui/screen-fix.ts diff --git a/src/scenes/BossArenaScene.ts b/src/scenes/BossArenaScene.ts index 5358e07..34db17f 100644 --- a/src/scenes/BossArenaScene.ts +++ b/src/scenes/BossArenaScene.ts @@ -48,6 +48,9 @@ import type { MetaState, RunState } from '../run/types'; import { query } from 'bitecs'; import { Projectile } from '../ecs/components'; +// UI zoom compensation +import { fixToScreen } from '../ui/screen-fix'; + /** Data passed from GameScene to BossArenaScene */ interface BossArenaInitData { meta: MetaState; @@ -608,6 +611,13 @@ export class BossArenaScene extends Phaser.Scene { const cam = this.cameras.main; const healthPct = this.bossState.health / this.bossState.maxHealth; + // Fix UI positions for camera zoom. + // Graphics at (0,0) — local draw coordinates map 1:1 to screen pixels. + fixToScreen(this.bossHealthBar, 0, 0, cam); + fixToScreen(this.bossHealthText, cam.width / 2, 20, cam); + fixToScreen(this.phaseText, cam.width / 2, 55, cam); + fixToScreen(this.feedbackText, cam.width / 2, cam.height - 60, cam); + // Health bar const barWidth = 300; const barHeight = 8; @@ -679,6 +689,7 @@ export class BossArenaScene extends Phaser.Scene { victoryText.setScrollFactor(0); victoryText.setOrigin(0.5); victoryText.setDepth(200); + fixToScreen(victoryText, cam.width / 2, cam.height / 2, cam); const methodNames: Record = { chemical: 'Алхимическая победа (NaOH)', diff --git a/src/scenes/GameScene.ts b/src/scenes/GameScene.ts index 4f65de2..cbc46d9 100644 --- a/src/scenes/GameScene.ts +++ b/src/scenes/GameScene.ts @@ -59,6 +59,9 @@ import { } from '../run/crisis'; import { getEscalationEffects } from '../run/escalation'; +// UI zoom compensation +import { fixToScreen } from '../ui/screen-fix'; + // Mycelium imports import { FungalNode } from '../ecs/components'; import { spawnFungalNodes } from '../mycelium/nodes'; @@ -623,6 +626,16 @@ export class GameScene extends Phaser.Scene { } else { this.phaseText.setColor('#00ff88'); } + + // Fix UI element positions for current camera zoom. + // setScrollFactor(0) prevents scroll but NOT zoom displacement. + const cam = this.cameras.main; + fixToScreen(this.statsText, 10, 30, cam); + fixToScreen(this.interactionText, cam.width / 2, cam.height - 40, cam); + fixToScreen(this.phaseText, cam.width / 2, 12, cam); + fixToScreen(this.memoryFlashText, cam.width / 2, cam.height / 2, cam); + fixToScreen(this.crisisOverlay, cam.width / 2, cam.height / 2, cam); + fixToScreen(this.mycosisOverlay, cam.width / 2, cam.height / 2, cam); } /** Find the nearest fungal node within interaction range, or null */ diff --git a/src/ui/screen-fix.ts b/src/ui/screen-fix.ts new file mode 100644 index 0000000..d1d4e84 --- /dev/null +++ b/src/ui/screen-fix.ts @@ -0,0 +1,31 @@ +import type Phaser from 'phaser'; + +/** + * Position a scrollFactor(0) game object at fixed screen coordinates, + * compensating for camera zoom. + * + * Phaser's camera zoom scales around the viewport center, which displaces + * scrollFactor(0) objects: + * screenPos = (objPos − center) × zoom + center + * + * This function solves for the object position that maps to the desired + * screen pixel, and counter-scales the object so it appears at 1× size: + * objPos = (desiredScreen − center) / zoom + center + * scale = baseScale / zoom + * + * Call every frame for each UI element that uses setScrollFactor(0). + */ +export function fixToScreen( + obj: { x: number; y: number; setScale(x: number, y?: number): unknown }, + screenX: number, + screenY: number, + camera: Phaser.Cameras.Scene2D.Camera, + baseScale = 1, +): void { + const zoom = camera.zoom; + const cx = camera.width * 0.5; + const cy = camera.height * 0.5; + obj.x = (screenX - cx) / zoom + cx; + obj.y = (screenY - cy) / zoom + cy; + obj.setScale(baseScale / zoom); +} diff --git a/src/world/minimap.ts b/src/world/minimap.ts index 9e7ec03..c17dbaf 100644 --- a/src/world/minimap.ts +++ b/src/world/minimap.ts @@ -1,5 +1,6 @@ import Phaser from 'phaser'; import type { TileData, TileGrid, WorldData } from './types'; +import { fixToScreen } from '../ui/screen-fix'; const MINIMAP_DEPTH = 100; const VIEWPORT_DEPTH = 101; @@ -102,8 +103,17 @@ export class Minimap { /** Update the viewport indicator rectangle — call each frame */ update(camera: Phaser.Cameras.Scene2D.Camera): void { + // Compensate for camera zoom on all minimap elements. + // Graphics objects (border, viewport) use (0,0) as origin so their + // local draw coordinates map 1:1 to screen pixels after compensation. + fixToScreen(this.border, 0, 0, camera); + fixToScreen(this.viewport, 0, 0, camera); + + // Image has origin(1,0): position = top-right corner const screenW = camera.width; const padding = 10; + fixToScreen(this.image, screenW - padding, padding, camera); + const minimapW = this.mapWidth * this.minimapScale; const minimapX = screenW - padding - minimapW; const minimapY = padding;