import * as Phaser from "phaser"; import { computeUiLayout } from "../../utils/UiLayout"; import { ENABLE_GAME_TOP_PROGRESS_DISPLAY, GAME_UI_BAR_BACKGROUND_COLOR, } from "../../DebugConfig"; import { ThemeManager } from "./ThemeManager"; import { HeartDisplay } from "./HeartDisplay"; import { CountdownDisplay } from "./CountdownDisplay"; import { ProgressDisplay } from "./ProgressDisplay"; /** 顶部UI栏的设计高度常量 */ export const GAME_TOP_UI_BAR_HEIGHT = 150; /** 顶部UI的配置选项 */ export interface GameTopUIConfig { levelText: string; initialLives: number; maxLives: number; initialTimeSeconds: number; onTimeout?: () => void; } /** * 游戏顶部UI组件。 * 包含关卡标题、生命值显示、倒计时器和进度显示。 * 布局为上下垂直结构,顺序:生命值 → 关卡标题 → 倒计时 → 进度。 */ export class GameTopUI extends Phaser.GameObjects.Container { private levelLabel!: Phaser.GameObjects.Text; private heartDisplay!: HeartDisplay; private countdownDisplay!: CountdownDisplay; private progressDisplay?: ProgressDisplay; constructor(scene: Phaser.Scene, config: GameTopUIConfig) { super(scene); scene.add.existing(this); // Ensure top UI renders above the board container this.setDepth(500); const layout = computeUiLayout(scene); const { width, height, uiScale, vScale } = layout; // 获取栏背景颜色:优先使用配置,否则使用主题颜色 const themeBarColor = ThemeManager.instance.getUiBarBackgroundColorNumber(); const barBgColor = GAME_UI_BAR_BACKGROUND_COLOR >= 0 ? GAME_UI_BAR_BACKGROUND_COLOR : themeBarColor; const barHeight = GAME_TOP_UI_BAR_HEIGHT * vScale; const bg = scene.add.rectangle( width / 2, barHeight / 2, width, barHeight, barBgColor, ); this.add(bg); // --- Vertical layout: top-to-bottom --- // All vertical sizes use vScale so total height fits within barHeight. // Horizontal sizes (spacing, widths) still use uiScale for proper proportions. const rowGap = 6 * vScale; const topPadding = 10 * vScale; let currentY = topPadding; // --- Row 1: Hearts (lives) --- const heartSpacing = 45 * uiScale; const heartsGroupWidth = config.maxLives * heartSpacing; const heartStartX = (width - heartsGroupWidth) / 2 + heartSpacing / 2; const heartDrawSize = 16 * uiScale; const heartRowHeight = 30 * vScale; const heartsY = currentY + heartRowHeight / 2; this.heartDisplay = new HeartDisplay(scene, this, { startX: heartStartX, y: heartsY, spacing: heartSpacing, drawSize: heartDrawSize, maxLives: config.maxLives, initialLives: config.initialLives, }); currentY += heartRowHeight + rowGap; // --- Row 2: Level label --- const hasLevelText = config.levelText && config.levelText.trim().length > 0; const levelFontSize = 28 * uiScale; const levelRowHeight = 30 * vScale; if (hasLevelText) { this.levelLabel = scene.add .text(width / 2, currentY + levelRowHeight / 2, config.levelText, { fontFamily: "Poppins", fontSize: `${levelFontSize}px`, color: ThemeManager.instance.getUiLevelTextColor(), fontStyle: "bold", }) .setOrigin(0.5, 0.5); this.add(this.levelLabel); currentY += levelRowHeight + rowGap; } else { this.levelLabel = scene.add .text(0, 0, "", { fontFamily: "Poppins", fontSize: `${levelFontSize}px`, color: ThemeManager.instance.getUiLevelTextColor(), fontStyle: "bold", }) .setOrigin(0.5, 0.5) .setVisible(false); this.add(this.levelLabel); // No row height added when hidden } // --- Row 3: Countdown (icon + digits) --- const timeIconSize = 36 * uiScale; const iconTextGap = 8 * uiScale; const digitWidth = 20 * uiScale; const digitHeight = 36 * uiScale; const digitSpacing = 4 * uiScale; const colonWidth = 10 * uiScale; const timerDisplayWidth = digitWidth * 4 + colonWidth + digitSpacing * 4; const timerGroupWidth = timeIconSize + iconTextGap + timerDisplayWidth; const timerRowHeight = 36 * vScale; const timerY = currentY + timerRowHeight / 2; // Center the timer group horizontally const timerGroupStartX = (width - timerGroupWidth) / 2; const timeIconX = timerGroupStartX + timeIconSize / 2; const timeIcon = scene.add.image(timeIconX, timerY, "time"); timeIcon.setScale(uiScale * 0.2).setOrigin(0.5); this.add(timeIcon); this.countdownDisplay = new CountdownDisplay(scene, this, { xLeft: timeIconX + timeIconSize / 2 + iconTextGap, y: timerY + vScale - 1, digitWidth, digitHeight, digitSpacing, colonWidth, totalSeconds: config.initialTimeSeconds, onTimeout: config.onTimeout, }); this.countdownDisplay.updateDisplay(); currentY += timerRowHeight + rowGap; // --- Row 4: Progress display (controlled by config) --- if (ENABLE_GAME_TOP_PROGRESS_DISPLAY) { const progressDigitWidth = 18 * uiScale; const progressDigitHeight = 36 * uiScale; const progressDigitSpacing = 3 * uiScale; const percentWidth = 14 * uiScale; // Approximate total width: 3 digits + percent sign + spacing const progressTotalWidth = progressDigitWidth * 3 + progressDigitSpacing * 3 + percentWidth; const progressStartX = (width - progressTotalWidth) / 2; const progressRowHeight = 36 * vScale; const progressY = currentY + progressRowHeight / 2; this.progressDisplay = new ProgressDisplay(scene, this, { xLeft: progressStartX, y: progressY + vScale, digitWidth: progressDigitWidth, digitHeight: progressDigitHeight, digitSpacing: progressDigitSpacing, percentWidth, }); // Initialize to 0% this.progressDisplay.updateProgress(0, 0); currentY += progressRowHeight + rowGap; } } // --- 委托公共API(为外部调用者保持相同接口) --- /** * 设置当前生命值。 * @param lives 生命值数量 */ public setLives(lives: number): void { this.heartDisplay.setLives(lives); } /** * 更新计时器。 * @param deltaMs 帧间隔时间(毫秒) * @param nowMs 当前时间戳(毫秒,可选) */ public updateTimer(deltaMs: number, nowMs?: number): void { this.countdownDisplay.updateTimer(deltaMs, nowMs); } /** * 为倒计时添加额外时间。 * @param seconds 要添加的秒数 */ public addTime(seconds: number): void { this.countdownDisplay.addTime(seconds); } /** 启动计时器 */ public startTimer(): void { this.countdownDisplay.startTimer(); } /** 停止计时器 */ public stopTimer(): void { this.countdownDisplay.stopTimer(); } /** 获取已经过的秒数 */ public getElapsedSeconds(): number { return this.countdownDisplay.getElapsedSeconds(); } /** * 更新进度显示。 * @param removed 已消除的路径数 * @param total 总路径数 */ public updateProgress(removed: number, total: number): void { if (this.progressDisplay) { this.progressDisplay.updateProgress(removed, total); } } }