/** * UIScene — HUD overlay on top of GameScene * * Renders health bar, quick slots, inventory weight. * Reads shared game state from Phaser registry. */ import Phaser from 'phaser'; import { SLOT_COUNT } from '../player/quickslots'; import { ElementRegistry } from '../chemistry/elements'; import { CompoundRegistry } from '../chemistry/compounds'; const SLOT_SIZE = 44; const SLOT_GAP = 4; const HEALTH_BAR_WIDTH = 160; const HEALTH_BAR_HEIGHT = 14; /** Get color for a chemical item as number (for rendering) */ function getItemDisplayColor(itemId: string): number { const el = ElementRegistry.getBySymbol(itemId); if (el) return parseInt(el.color.replace('#', ''), 16); const comp = CompoundRegistry.getById(itemId); if (comp) return parseInt(comp.color.replace('#', ''), 16); return 0xffffff; } /** Get display name for a chemical item */ function getItemDisplayName(itemId: string): string { const el = ElementRegistry.getBySymbol(itemId); if (el) return el.symbol; const comp = CompoundRegistry.getById(itemId); if (comp) return comp.formula; return itemId; } export class UIScene extends Phaser.Scene { // Health private healthBarBg!: Phaser.GameObjects.Rectangle; private healthBarFill!: Phaser.GameObjects.Rectangle; private healthText!: Phaser.GameObjects.Text; // Quick slots private slotBoxes: Phaser.GameObjects.Rectangle[] = []; private slotTexts: Phaser.GameObjects.Text[] = []; private slotNumTexts: Phaser.GameObjects.Text[] = []; private slotCountTexts: Phaser.GameObjects.Text[] = []; // Inventory private invText!: Phaser.GameObjects.Text; // Controls hint private controlsText!: Phaser.GameObjects.Text; // Cycle info (top-left, below health) private cycleText!: Phaser.GameObjects.Text; // Run phase (top-left, below cycle) private phaseText!: Phaser.GameObjects.Text; constructor() { super({ key: 'UIScene' }); } create(): void { const w = this.cameras.main.width; const h = this.cameras.main.height; // === Health bar (top-left) === const hpX = 12; const hpY = 12; this.healthBarBg = this.add.rectangle( hpX + HEALTH_BAR_WIDTH / 2, hpY + HEALTH_BAR_HEIGHT / 2, HEALTH_BAR_WIDTH, HEALTH_BAR_HEIGHT, 0x330000, ); this.healthBarBg.setStrokeStyle(1, 0x660000); this.healthBarFill = this.add.rectangle( hpX + HEALTH_BAR_WIDTH / 2, hpY + HEALTH_BAR_HEIGHT / 2, HEALTH_BAR_WIDTH, HEALTH_BAR_HEIGHT, 0x00cc44, ); this.healthText = this.add.text( hpX + HEALTH_BAR_WIDTH + 8, hpY, '', { fontSize: '12px', color: '#00ff88', fontFamily: 'monospace', }, ); // === Quick slots (bottom-center) === const totalW = SLOT_COUNT * SLOT_SIZE + (SLOT_COUNT - 1) * SLOT_GAP; const startX = (w - totalW) / 2; const startY = h - SLOT_SIZE - 12; for (let i = 0; i < SLOT_COUNT; i++) { const x = startX + i * (SLOT_SIZE + SLOT_GAP); const cx = x + SLOT_SIZE / 2; const cy = startY + SLOT_SIZE / 2; // Slot background const box = this.add.rectangle(cx, cy, SLOT_SIZE, SLOT_SIZE, 0x111111, 0.85); box.setStrokeStyle(2, 0x444444); this.slotBoxes.push(box); // Slot number (top-left corner) const numText = this.add.text(x + 3, startY + 2, `${i + 1}`, { fontSize: '10px', color: '#555555', fontFamily: 'monospace', }); this.slotNumTexts.push(numText); // Item name (center) const itemText = this.add.text(cx, cy - 2, '', { fontSize: '14px', color: '#ffffff', fontFamily: 'monospace', fontStyle: 'bold', }); itemText.setOrigin(0.5); this.slotTexts.push(itemText); // Count (bottom-right) const countText = this.add.text(x + SLOT_SIZE - 3, startY + SLOT_SIZE - 3, '', { fontSize: '10px', color: '#aaaaaa', fontFamily: 'monospace', }); countText.setOrigin(1, 1); this.slotCountTexts.push(countText); } // === Inventory info (bottom-right) === this.invText = this.add.text(w - 12, h - 14, '', { fontSize: '11px', color: '#888888', fontFamily: 'monospace', backgroundColor: '#000000aa', padding: { x: 4, y: 2 }, }); this.invText.setOrigin(1, 1); // === Cycle info (top-left, below health bar) === this.cycleText = this.add.text(12, 32, '', { fontSize: '10px', color: '#446644', fontFamily: 'monospace', }); // === Run phase (below cycle) === this.phaseText = this.add.text(12, 46, '', { fontSize: '10px', color: '#557755', fontFamily: 'monospace', }); // === Controls hint (top-right) === this.controlsText = this.add.text(w - 12, 12, 'WASD move | E collect | F throw | 1-4 slots | scroll zoom', { fontSize: '10px', color: '#444444', fontFamily: 'monospace', }); this.controlsText.setOrigin(1, 0); } update(): void { // === Read shared state from registry === const health = (this.registry.get('health') as number) ?? 100; const healthMax = (this.registry.get('healthMax') as number) ?? 100; const slots = (this.registry.get('quickSlots') as (string | null)[]) ?? []; const activeSlot = (this.registry.get('activeSlot') as number) ?? 0; const invWeight = (this.registry.get('invWeight') as number) ?? 0; const invMaxWeight = (this.registry.get('invMaxWeight') as number) ?? 500; const invSlots = (this.registry.get('invSlots') as number) ?? 0; const invCounts = (this.registry.get('invCounts') as Map) ?? new Map(); // === Update health bar === const ratio = healthMax > 0 ? health / healthMax : 0; const fillWidth = HEALTH_BAR_WIDTH * ratio; this.healthBarFill.width = fillWidth; this.healthBarFill.x = this.healthBarBg.x - (HEALTH_BAR_WIDTH - fillWidth) / 2; // Color: green → yellow → red const hpColor = ratio > 0.5 ? 0x00cc44 : ratio > 0.25 ? 0xccaa00 : 0xcc2200; this.healthBarFill.setFillStyle(hpColor); this.healthText.setText(`${Math.round(health)}/${healthMax}`); // === Update quick slots === for (let i = 0; i < SLOT_COUNT; i++) { const itemId = i < slots.length ? slots[i] : null; const isActive = i === activeSlot; // Border color this.slotBoxes[i].setStrokeStyle(2, isActive ? 0x00ff88 : 0x444444); this.slotBoxes[i].setFillStyle(isActive ? 0x1a2a1a : 0x111111, 0.85); if (itemId) { const displayName = getItemDisplayName(itemId); const displayColor = getItemDisplayColor(itemId); this.slotTexts[i].setText(displayName); this.slotTexts[i].setColor(`#${displayColor.toString(16).padStart(6, '0')}`); const count = invCounts.get(itemId) ?? 0; this.slotCountTexts[i].setText(count > 0 ? `${count}` : '0'); this.slotCountTexts[i].setColor(count > 0 ? '#aaaaaa' : '#660000'); } else { this.slotTexts[i].setText(''); this.slotCountTexts[i].setText(''); } } // === Update inventory === this.invText.setText(`${Math.round(invWeight)}/${invMaxWeight} AMU | ${invSlots} items`); // === Update cycle info === const cycleNumber = (this.registry.get('cycleNumber') as number) ?? 1; const runInCycle = (this.registry.get('runInCycle') as number) ?? 1; const cycleTheme = (this.registry.get('cycleThemeRu') as string) ?? ''; this.cycleText.setText(`Цикл ${cycleNumber}: ${cycleTheme} | Ран ${runInCycle}/7`); // === Update run phase === const runPhaseRu = (this.registry.get('runPhaseRu') as string) ?? ''; this.phaseText.setText(runPhaseRu); } }