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 <cursoragent@cursor.com>
This commit is contained in:
Денис Шкабатур
2026-02-12 18:22:40 +03:00
parent 6ba0746bb9
commit 1b2cc0cd86
4 changed files with 65 additions and 0 deletions

View File

@@ -48,6 +48,9 @@ import type { MetaState, RunState } from '../run/types';
import { query } from 'bitecs'; import { query } from 'bitecs';
import { Projectile } from '../ecs/components'; import { Projectile } from '../ecs/components';
// UI zoom compensation
import { fixToScreen } from '../ui/screen-fix';
/** Data passed from GameScene to BossArenaScene */ /** Data passed from GameScene to BossArenaScene */
interface BossArenaInitData { interface BossArenaInitData {
meta: MetaState; meta: MetaState;
@@ -608,6 +611,13 @@ export class BossArenaScene extends Phaser.Scene {
const cam = this.cameras.main; const cam = this.cameras.main;
const healthPct = this.bossState.health / this.bossState.maxHealth; 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 // Health bar
const barWidth = 300; const barWidth = 300;
const barHeight = 8; const barHeight = 8;
@@ -679,6 +689,7 @@ export class BossArenaScene extends Phaser.Scene {
victoryText.setScrollFactor(0); victoryText.setScrollFactor(0);
victoryText.setOrigin(0.5); victoryText.setOrigin(0.5);
victoryText.setDepth(200); victoryText.setDepth(200);
fixToScreen(victoryText, cam.width / 2, cam.height / 2, cam);
const methodNames: Record<string, string> = { const methodNames: Record<string, string> = {
chemical: 'Алхимическая победа (NaOH)', chemical: 'Алхимическая победа (NaOH)',

View File

@@ -59,6 +59,9 @@ import {
} from '../run/crisis'; } from '../run/crisis';
import { getEscalationEffects } from '../run/escalation'; import { getEscalationEffects } from '../run/escalation';
// UI zoom compensation
import { fixToScreen } from '../ui/screen-fix';
// Mycelium imports // Mycelium imports
import { FungalNode } from '../ecs/components'; import { FungalNode } from '../ecs/components';
import { spawnFungalNodes } from '../mycelium/nodes'; import { spawnFungalNodes } from '../mycelium/nodes';
@@ -623,6 +626,16 @@ export class GameScene extends Phaser.Scene {
} else { } else {
this.phaseText.setColor('#00ff88'); 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 */ /** Find the nearest fungal node within interaction range, or null */

31
src/ui/screen-fix.ts Normal file
View File

@@ -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);
}

View File

@@ -1,5 +1,6 @@
import Phaser from 'phaser'; import Phaser from 'phaser';
import type { TileData, TileGrid, WorldData } from './types'; import type { TileData, TileGrid, WorldData } from './types';
import { fixToScreen } from '../ui/screen-fix';
const MINIMAP_DEPTH = 100; const MINIMAP_DEPTH = 100;
const VIEWPORT_DEPTH = 101; const VIEWPORT_DEPTH = 101;
@@ -102,8 +103,17 @@ export class Minimap {
/** Update the viewport indicator rectangle — call each frame */ /** Update the viewport indicator rectangle — call each frame */
update(camera: Phaser.Cameras.Scene2D.Camera): void { 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 screenW = camera.width;
const padding = 10; const padding = 10;
fixToScreen(this.image, screenW - padding, padding, camera);
const minimapW = this.mapWidth * this.minimapScale; const minimapW = this.mapWidth * this.minimapScale;
const minimapX = screenW - padding - minimapW; const minimapX = screenW - padding - minimapW;
const minimapY = padding; const minimapY = padding;