import * as Phaser from "phaser"; import { GAME_TOP_UI_BAR_HEIGHT } from "./GameTopUI"; import { ThemeManager } from "./ThemeManager"; import type { ParsedLevel } from "../../levels/LevelTypes"; import { BOARD_MARGIN_RATIO, BOARD_PAN_MARGIN_RATIO, BOARD_SCALE_FACTOR, CELL_SPACING_MULTIPLIER, GameConfig, } from "../../GameConfig"; import { GAME_CANVAS_BACKGROUND_COLOR } from "../../DebugConfig"; /** * 描述 BoardLayout 从 Game 场景中所需内容的最小接口。 */ export interface BoardLayoutGameRef extends Phaser.Scene { level: ParsedLevel | null; cellSize: number; cellWidth: number; cellHeight: number; visualSize: number; boardWidth: number; boardHeight: number; boardContainer: Phaser.GameObjects.Container; boardGraphics: Phaser.GameObjects.Graphics; hintOverlayGraphics: Phaser.GameObjects.Graphics | undefined; levelBackgroundImage: Phaser.GameObjects.Image | null; zoom: number; minZoom: number; boardOffsetX: number; boardOffsetY: number; removedPaths: Set; topUI: any; // GameTopUI | null backgroundRect: Phaser.GameObjects.Rectangle | undefined; // BoardLayout 需要的 Game 方法 renderGridPaths(level: ParsedLevel, clampedProgress: number): void; renderHintOverlay(): void; } export class BoardLayout { private scene: BoardLayoutGameRef; constructor(scene: BoardLayoutGameRef) { this.scene = scene; } // ── 构建棋盘 ── /** * 根据关卡数据和屏幕尺寸构建棋盘布局。 * 计算格子大小、棋盘尺寸,并渲染路径。 * @param level 关卡数据 * @param drawProgress 绘制进度(0~1) */ buildBoard(level: ParsedLevel, drawProgress: number = 1): void { const s = this.scene; const { width, height } = s.scale; const isLandscape = width > height; const marginRatio = BOARD_MARGIN_RATIO; const usableWidth = width * (1 - 2 * marginRatio); // 计算可用高度:减去顶部和底部UI高度 const BASE_HEIGHT = 1334; const scaleY = height / BASE_HEIGHT; let topUiHeight: number; let bottomUiHeight: number; if (isLandscape) { // 横屏模式:使用较小的固定像素高度 topUiHeight = 55; bottomUiHeight = 45; } else { // 竖屏模式:使用缩放逻辑 topUiHeight = GAME_TOP_UI_BAR_HEIGHT * scaleY; bottomUiHeight = GameConfig.GAME_BOTTOM_UI_BAR_HEIGHT * scaleY; } const availableHeight = height - topUiHeight - bottomUiHeight; const usableHeight = availableHeight * (1 - 2 * marginRatio); // 应用棋盘缩放系数 const scaleFactor = BOARD_SCALE_FACTOR; const scaledUsableWidth = usableWidth * scaleFactor; const scaledUsableHeight = usableHeight * scaleFactor; // 计算原始格子大小(用于箭头和线条渲染) let baseSize: number; if (isLandscape) { const maxBaseSizeFromHeight = Math.floor( scaledUsableHeight / (CELL_SPACING_MULTIPLIER * level.rows), ); const maxBaseSizeFromWidth = Math.floor( scaledUsableWidth / (CELL_SPACING_MULTIPLIER * level.cols), ); baseSize = Math.max( 4, Math.min(maxBaseSizeFromHeight, maxBaseSizeFromWidth), ); const maxCellFromHeight = Math.floor(scaledUsableHeight / level.rows); const maxCellFromWidth = Math.floor(scaledUsableWidth / level.cols); const squareCellSize = Math.min(maxCellFromHeight, maxCellFromWidth); s.cellWidth = squareCellSize; s.cellHeight = squareCellSize; s.visualSize = Math.floor(squareCellSize / CELL_SPACING_MULTIPLIER); s.cellSize = Math.floor(baseSize * CELL_SPACING_MULTIPLIER); s.boardWidth = s.cellWidth * level.cols; s.boardHeight = s.cellHeight * level.rows; } else { // 竖屏:正方形格子 baseSize = Math.max( 4, Math.floor( Math.min( scaledUsableWidth / level.cols, scaledUsableHeight / level.rows, ), ), ); s.visualSize = baseSize; s.cellSize = Math.floor(baseSize * CELL_SPACING_MULTIPLIER); s.cellWidth = s.cellSize; s.cellHeight = s.cellSize; s.boardWidth = s.cellSize * level.cols; s.boardHeight = s.cellSize * level.rows; } this.positionBoardContainer(); s.boardGraphics.clear(); s.hintOverlayGraphics?.clear(); // 处理背景图片 this.handleLevelBackground(level); const clampedProgress = Phaser.Math.Clamp(drawProgress, 0, 1); s.renderGridPaths(level, clampedProgress); } // ── 处理关卡背景 ── /** * 处理关卡背景图片的加载和显示。 * 如果关卡有背景图配置,则加载并显示;否则清除已有背景。 * @param level 关卡数据 */ handleLevelBackground(level: ParsedLevel): void { const s = this.scene; // 清除之前的背景图片 if (s.levelBackgroundImage) { s.levelBackgroundImage.destroy(); s.levelBackgroundImage = null; } // 如果关卡有背景图配置,加载并显示 if (level.background && level.background.imageKey) { const { imageKey, dataUrl, opacity = 0.5 } = level.background; console.log(`[Game] 处理背景图片: ${imageKey}`); if (!s.textures.exists(imageKey)) { if (dataUrl) { console.log(`[Game] 从 data URL 创建背景纹理: ${imageKey}`); this.createTextureFromDataUrl(imageKey, dataUrl); return; // 异步纹理创建,完成后会创建图片 } else { console.warn(`[Game] 未找到背景纹理且无 data URL: ${imageKey}`); return; } } s.levelBackgroundImage = s.add.image( s.boardWidth / 2, s.boardHeight / 2, imageKey, ); s.levelBackgroundImage.setOrigin(0.5, 0.5); s.levelBackgroundImage.setAlpha(opacity); s.levelBackgroundImage.setDepth(3000); s.boardContainer.addAt(s.levelBackgroundImage, 0); this.resizeLevelBackgroundImage(); } } // ── 从 DataUrl 创建纹理 ── /** * 从 data URL 异步创建纹理。 * @param textureKey 纹理键名 * @param dataUrl 图片的 data URL */ private createTextureFromDataUrl(textureKey: string, dataUrl: string): void { const s = this.scene; const img = new Image(); img.onload = () => { if (!s.scene || !s.textures) { console.warn(`[Game] 纹理创建前场景已被销毁: ${textureKey}`); return; } if (s.textures.exists(textureKey)) { s.textures.remove(textureKey); } const texture = s.textures.createCanvas( textureKey, img.width, img.height, ); if (!texture) { console.error(`[Game] 创建 canvas 纹理失败: ${textureKey}`); return; } const context = texture.getContext(); if (!context) { console.error(`[Game] 获取纹理 canvas 上下文失败: ${textureKey}`); return; } context.drawImage(img, 0, 0); texture.refresh(); console.log(`[Game] 从 data URL 创建纹理成功: ${textureKey}`); this.loadBackgroundImageAfterTextureCreation(textureKey); }; img.onerror = () => { console.error(`[Game] 从 data URL 加载图片失败: ${textureKey}`); }; img.src = dataUrl; } // ── 纹理创建后加载背景图片 ── /** * 纹理异步创建完成后,加载并显示背景图片。 * @param textureKey 纹理键名 */ private loadBackgroundImageAfterTextureCreation(textureKey: string): void { const s = this.scene; if ( s.level && s.level.background && s.level.background.imageKey === textureKey ) { const { opacity = 0.5 } = s.level.background; if (s.levelBackgroundImage) { s.levelBackgroundImage.destroy(); } s.levelBackgroundImage = s.add.image( s.boardWidth / 2, s.boardHeight / 2, textureKey, ); s.levelBackgroundImage.setOrigin(0.5, 0.5); s.levelBackgroundImage.setAlpha(opacity); s.levelBackgroundImage.setDepth(3000); s.boardContainer.addAt(s.levelBackgroundImage, 0); this.resizeLevelBackgroundImage(); console.log(`[Game] 纹理创建后背景图片加载成功: ${textureKey}`); } } // ── 调整关卡背景图片尺寸 ── /** 根据棋盘尺寸调整背景图片的缩放和位置。 */ resizeLevelBackgroundImage(): void { const s = this.scene; if (!s.levelBackgroundImage) return; const scaleX = s.boardWidth / s.levelBackgroundImage.width; const scaleY = s.boardHeight / s.levelBackgroundImage.height; s.levelBackgroundImage.setScale(scaleX, scaleY); s.levelBackgroundImage.setPosition(s.boardWidth / 2, s.boardHeight / 2); s.levelBackgroundImage.setOrigin(0.5, 0.5); if ( s.boardContainer && s.levelBackgroundImage.parentContainer !== s.boardContainer ) { s.boardContainer.addAt(s.levelBackgroundImage, 0); } } // ── 重新布局棋盘 ── /** 重新布局整个棋盘,包括背景颜色、棋盘构建和提示覆盖层。 */ relayoutBoard(): void { const s = this.scene; if (!s.level) return; // 重置背景颜色和相机大小 const themeManager = ThemeManager.instance; const bgColor = themeManager.getBackgroundColorNumber(); const finalBgColor = GAME_CANVAS_BACKGROUND_COLOR >= 0 ? GAME_CANVAS_BACKGROUND_COLOR : bgColor; s.cameras.main.setBackgroundColor(finalBgColor); // 更新背景矩形 const { width, height } = s.scale; if (s.backgroundRect) { s.backgroundRect.setPosition(width / 2, height / 2); s.backgroundRect.setSize(width, height); s.backgroundRect.setFillStyle(finalBgColor); } this.buildBoard(s.level); s.renderHintOverlay(); // 更新进度百分比 if (s.topUI) { const totalPaths = s.level.paths.length; s.topUI.updateProgress(s.removedPaths.size, totalPaths); } } // ── 定位棋盘容器 ── /** 根据缩放和偏移量定位棋盘容器,并约束拖拽范围。 */ positionBoardContainer(): void { const s = this.scene; if (!s.boardContainer) return; const width = s.scale.width; const height = s.scale.height; const isLandscape = width > height; const scaledWidth = s.boardWidth * s.zoom; const scaledHeight = s.boardHeight * s.zoom; s.boardContainer.setScale(s.zoom); // 计算顶部和底部UI高度(必须与 buildBoard 一致) const BASE_HEIGHT = 1334; const scaleY = height / BASE_HEIGHT; let topOffset: number; let bottomOffset: number; if (isLandscape) { topOffset = 55; bottomOffset = 45; } else { topOffset = GAME_TOP_UI_BAR_HEIGHT * scaleY; bottomOffset = GameConfig.GAME_BOTTOM_UI_BAR_HEIGHT * scaleY; } const availableHeight = height - topOffset - bottomOffset; const baseX = (width - scaledWidth) / 2; // Ensure the board never overlaps the top UI bar const rawBaseY = topOffset + (availableHeight - scaledHeight) / 2; const baseY = Math.max(topOffset, rawBaseY); let x = baseX + s.boardOffsetX; let y = baseY + s.boardOffsetY; // 约束拖拽范围 const marginX = width * BOARD_PAN_MARGIN_RATIO; const marginY = availableHeight * BOARD_PAN_MARGIN_RATIO; // 水平方向 if (scaledWidth + 2 * marginX <= width) { x = baseX; s.boardOffsetX = 0; } else { const minX = width - marginX - scaledWidth; const maxX = marginX; if (minX > maxX) { x = baseX; s.boardOffsetX = 0; } else if (x < minX) { x = minX; s.boardOffsetX = x - baseX; } else if (x > maxX) { x = maxX; s.boardOffsetX = x - baseX; } } // 垂直方向 if (scaledHeight + 2 * marginY <= availableHeight) { y = baseY; s.boardOffsetY = 0; } else { const minY = height - marginY - scaledHeight; const maxY = topOffset + marginY; if (minY > maxY) { y = baseY; s.boardOffsetY = 0; } else if (y < minY) { y = minY; s.boardOffsetY = y - baseY; } else if (y > maxY) { y = maxY; s.boardOffsetY = y - baseY; } } s.boardContainer.x = x; s.boardContainer.y = y; } }