import * as Phaser from "phaser"; import { GAME_TOP_UI_BAR_HEIGHT } from "./GameTopUI"; import { computeUiLayout } from "../../utils/UiLayout"; import { ENABLE_ZOOM_GESTURE_GUIDE, AUTO_USE_HINT_ON_START, } from "../../DebugConfig"; /** * 描述 TutorialManager 从 Game 场景中所需内容的最小接口。 * 避免模块级别的循环导入。 */ export interface TutorialGameRef extends Phaser.Scene { level: any; // ParsedLevel | null boardContainer: Phaser.GameObjects.Container | null; visualSize: number; isLevelOne: boolean; isLevelTwo: boolean; removedPaths: Set; animatingPaths: Set; cellCenter(col: number, row: number): { x: number; y: number }; renderHintOverlay(): void; findRemovablePathIndex(): number | null; isCellOccupiedByAnyPath( col: number, row: number, ignorePath: number, ): boolean; } /** * 教程管理器。 * 负责管理第一关教程引导、提示高亮、第二关缩放手势引导等。 */ export class TutorialManager { private scene: TutorialGameRef; // ── 提示高亮状态 ── hintHighlightTween: Phaser.Tweens.Tween | null = null; hintTargetIndex: number | null = null; hintPulseScale = 1; hintFingerContainer: Phaser.GameObjects.Container | null = null; hintFingerTween: Phaser.Tweens.Tween | null = null; hintFingerMoveTween: Phaser.Tweens.Tween | null = null; // ── 第一关教程状态 ── isLevelOneTutorialActive = false; levelOneTutorialExpectedPathIndex: number | null = null; // ── 第二关缩放引导状态 ── isLevelTwoZoomGuideActive = false; hasLevelTwoZoomGuideShown = false; private levelTwoZoomGuideBoardContainer: Phaser.GameObjects.Container | null = null; private levelTwoZoomGuideTextContainer: Phaser.GameObjects.Container | null = null; constructor(scene: TutorialGameRef) { this.scene = scene; } // ── 生命周期 ────────────────────────────────────────────── /** * 重置所有教程状态。在每关开始时调用。 */ resetState(): void { this.clearHintHighlight(); this.hideLevelTwoZoomGuide(); this.isLevelOneTutorialActive = false; this.levelOneTutorialExpectedPathIndex = null; this.hasLevelTwoZoomGuideShown = false; this.isLevelTwoZoomGuideActive = false; } // ── 提示高亮 ───────────────────────────────────────── /** 清除当前的提示高亮效果及手指动画。 */ clearHintHighlight(): void { if (this.hintHighlightTween) { this.hintHighlightTween.stop(); this.hintHighlightTween = null; } if (this.hintFingerTween) { this.hintFingerTween.stop(); this.hintFingerTween = null; } if (this.hintFingerMoveTween) { this.hintFingerMoveTween.stop(); this.hintFingerMoveTween = null; } if (this.hintFingerContainer) { this.hintFingerContainer.destroy(); this.hintFingerContainer = null; } if (this.hintTargetIndex !== null) { this.hintTargetIndex = null; this.hintPulseScale = 1; this.scene.renderHintOverlay(); } } /** * 显示指定路径的提示高亮效果,包括手指图标和脉冲动画。 * @param pathIndex 需要高亮的路径索引 */ showHintHighlight(pathIndex: number): void { const scene = this.scene; if (!scene.level) return; // 清除旧的提示效果(如果有) this.clearHintHighlight(); const isTutorial = this.isLevelOneTutorialActive; this.hintTargetIndex = pathIndex; this.hintPulseScale = isTutorial ? 1 : 1.25; scene.renderHintOverlay(); const level = scene.level; let hintDirection: "left" | "right" | "up" | "down" = "right"; let headCenter: { x: number; y: number } | null = null; const path = level.paths[pathIndex]!; const headCell = path.head; headCenter = scene.cellCenter(headCell.x, headCell.y); hintDirection = path.direction; if (!headCenter) return; // 所有提示(包括教程和道具提示)都显示手指+光环; // 但只有非教程提示会对箭头/路径进行脉冲缩放。 if (scene.boardContainer) { const fingerContainer = scene.add.container(headCenter.x, headCenter.y); scene.boardContainer.add(fingerContainer); scene.boardContainer.bringToTop(fingerContainer); this.hintFingerContainer = fingerContainer; const finger = scene.add.image(0, 0, "figer"); finger.setOrigin(0, 0); // 使用 visualSize 而非 cellSize 来放大手指显示 const fingerMaxSize = scene.visualSize * 5; const fingerBaseScale = Math.min( fingerMaxSize / (finger.width || 1), fingerMaxSize / (finger.height || 1), ); let fingerScale = fingerBaseScale; if (isTutorial) { // 教程模式下手指缩小 1/3 fingerScale *= 2 / 3; } finger.setScale(fingerScale); const moveOffsetX = scene.visualSize * 0.4; const moveOffsetY = scene.visualSize * 0.4; let fingerRotation = 0; if (hintDirection === "down") { fingerRotation = Math.PI; } finger.setRotation(fingerRotation); const fingerOffsetX = moveOffsetX; finger.setFlipX(false); finger.setPosition(fingerOffsetX, moveOffsetY); // 仅添加手指,不添加黄色圆圈 fingerContainer.add([finger]); // Google 渠道不显示手指移动动画 const isGoogleChannel = (window as any).PlayableSDK?.getCurChannel?.() === "google"; if (!isGoogleChannel) { this.hintFingerMoveTween = scene.tweens.add({ targets: finger, x: { from: fingerOffsetX, to: 0 }, y: { from: moveOffsetY, to: 0 }, duration: 650, ease: "Sine.easeInOut", yoyo: true, repeat: -1, }); } else { // Google 渠道:手指静止在目标位置 finger.setPosition(0, 0); } } // 非教程模式下添加脉冲缩放动画 if (!isTutorial) { this.hintHighlightTween = scene.tweens.add({ targets: this, hintPulseScale: { from: 1.25, to: 0.9 }, duration: 450, ease: "Sine.easeInOut", yoyo: true, repeat: -1, onUpdate: () => { scene.renderHintOverlay(); }, }); } } // ── 第一关教程 ───────────────────────────────────── /** 启动第一关教程引导,显示第一个可消除路径的提示。 */ startLevelOneTutorial(): void { const scene = this.scene; if (!scene.level || !scene.isLevelOne) { return; } this.isLevelOneTutorialActive = true; const nextIndex = this.findNextLevelOneTutorialPathIndex(); if (nextIndex === null) { this.isLevelOneTutorialActive = false; this.levelOneTutorialExpectedPathIndex = null; return; } this.levelOneTutorialExpectedPathIndex = nextIndex; this.showHintHighlight(nextIndex); } /** 推进第一关教程到下一个可消除路径。 */ advanceLevelOneTutorial(): void { const scene = this.scene; if (!scene.level || !scene.isLevelOne) { return; } const nextIndex = this.findNextLevelOneTutorialPathIndex(); if (nextIndex === null) { this.isLevelOneTutorialActive = false; this.levelOneTutorialExpectedPathIndex = null; this.clearHintHighlight(); return; } this.levelOneTutorialExpectedPathIndex = nextIndex; this.showHintHighlight(nextIndex); } /** * 查找下一个可消除的路径索引(用于教程引导)。 * @returns 下一个可消除的路径索引,未找到返回 null */ private findNextLevelOneTutorialPathIndex(): number | null { return this.scene.findRemovablePathIndex(); } // ── 开始时自动使用提示 ──────────────────────────────────── /** 关卡开始时自动显示一个可消除路径的提示高亮。 */ autoUseHintOnStart(): void { const scene = this.scene; if (!scene.level) return; console.log( "[提示] autoUseHintOnStart 调用, levelIndex =", scene.level.index, ); const targetIndex = scene.findRemovablePathIndex(); console.log("[提示] 自动提示 - findRemovablePathIndex 结果 =", targetIndex); if (targetIndex === null) { console.log("[提示] 自动提示 - 未找到可消除的路径"); return; } // 直接显示提示高亮,不调用服务器API this.showHintHighlight(targetIndex); console.log("[提示] 自动提示已显示,路径索引:", targetIndex); } // ── 第二关缩放手势引导 ────────────────────────────────── /** 显示第二关的缩放手势引导(双指缩放提示文字)。 */ showLevelTwoZoomGuide(): void { const scene = this.scene; // 检查配置中是否启用了手势引导 if (!ENABLE_ZOOM_GESTURE_GUIDE) { return; } if (!scene.level || !scene.boardContainer) { return; } console.log("hasLevelTwoZoomGuideShown", this.hasLevelTwoZoomGuideShown); if (!scene.isLevelTwo || this.hasLevelTwoZoomGuideShown) { return; } this.isLevelTwoZoomGuideActive = true; this.hasLevelTwoZoomGuideShown = true; const boardGuide = scene.add.container(0, 0); scene.boardContainer.add(boardGuide); scene.boardContainer.bringToTop(boardGuide); this.levelTwoZoomGuideBoardContainer = boardGuide; const layout = computeUiLayout(scene); const { width, uiScale, vScale } = layout; const textY = GAME_TOP_UI_BAR_HEIGHT * vScale + 55 * vScale; const textContainer = scene.add.container(width / 2, textY); textContainer.setScrollFactor(0); textContainer.setDepth(1500); const message = "双指捏合可以缩放棋盘。"; const text = scene.add.text(0, 0, message, { fontFamily: "Poppins", fontSize: `${34 * uiScale}px`, color: "#ffffff", fontStyle: "bold", align: "center", }); text.setOrigin(0.5, 0.5); const paddingX = 18 * uiScale; const paddingY = 8 * vScale; const maxBgWidth = width * 0.9; const maxTextWidth = Math.max(10, maxBgWidth - paddingX * 2); if (text.displayWidth > maxTextWidth) { text.setWordWrapWidth(maxTextWidth); text.setAlign("center"); } const bgWidth = Math.min(maxBgWidth, text.displayWidth + paddingX * 2); const bgHeight = text.displayHeight + paddingY * 2; const bg = scene.add.rectangle(0, 0, bgWidth, bgHeight, 0xff7a3c, 0.96); textContainer.add([bg, text]); this.levelTwoZoomGuideTextContainer = textContainer; } /** 隐藏第二关缩放手势引导,销毁相关容器。 */ hideLevelTwoZoomGuide(): void { if ( !this.isLevelTwoZoomGuideActive && !this.levelTwoZoomGuideBoardContainer && !this.levelTwoZoomGuideTextContainer ) { return; } this.isLevelTwoZoomGuideActive = false; if (this.levelTwoZoomGuideBoardContainer) { this.levelTwoZoomGuideBoardContainer.destroy(); this.levelTwoZoomGuideBoardContainer = null; } if (this.levelTwoZoomGuideTextContainer) { this.levelTwoZoomGuideTextContainer.destroy(); this.levelTwoZoomGuideTextContainer = null; } } }