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;