Files
synthesis/src/scenes/UIScene.ts
Денис Шкабатур d9213b6be0 feat: CradleScene + UIScene + GameScene cycle integration
Scene integration for the Great Cycle system:
- CradleScene: shows "Великий Цикл N: Тема | Ран X/7", narrative quote
- UIScene: cycle info bar and run phase display below health
- GameScene: world trace spawning (ruins/markers from past runs),
  trace glow rendering, death position recording, cycle info to registry,
  biomeId + worldSeed passed to RunState
- Scene flow: Fractal → RenewalScene (on 7th run) → Cradle

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 18:51:19 +03:00

227 lines
7.5 KiB
TypeScript

/**
* 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<string, number>) ?? 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);
}
}