import { _decorator, Component, Node, Prefab, instantiate, Sprite, SpriteFrame, Vec2, Vec3, input, Input, EventTouch, UITransform, Size, Color, Graphics, Tween, tween, AudioClip, AudioSource, UIOpacity, resources, } from "cc"; import { AudioManager, BGMType, SFXType } from "./AudioManager"; import { ButtonBadge } from "./ButtonBadge"; import { DragManager } from "./DragManager"; import { HintManager } from "./HintManager"; import { LevelBuilder } from "./LevelBuilder"; import { LevelManager } from "./LevelManager"; import { loadSpriteFrames } from "./utils/loadResources"; const { ccclass, property } = _decorator; @ccclass("GameManager") export class GameManager extends Component { @property(Prefab) blockPrefab: Prefab = null!; @property(Node) lineLayer: Node = null; @property(Node) Function_Node: Node = null; @property(AudioClip) remove_audio: AudioClip = null; @property(AudioClip) levle_audio: AudioClip = null; @property(AudioClip) victory_sfx_audio: AudioClip = null; @property(AudioClip) magic_sfx_audio: AudioClip = null; @property(AudioClip) hint_sfx_audio: AudioClip = null; @property(AudioClip) shuffle_sfx_audio: AudioClip = null; @property(HintManager) hintManager: HintManager | null = null; @property rows: number = 5; @property cols: number = 6; @property blockSize: number = 128; @property tileTypes: number = 5; @property level: number = 1; // 当前关卡 private toolBadge: ButtonBadge | null = null; private board: number[][] = []; private blockNodes: (Node | null)[][] = []; private spriteFrames: SpriteFrame[] = []; private draggingBlock: Node | null = null; private dragStartPos: Vec2 = new Vec2(); private dragStartBlockPos: Vec3 = new Vec3(); private dragRow = -1; private dragCol = -1; private slidingDir: { dirR: number; dirC: number } | null = null; private threshold: number = 10; // 触发滑动阈值 private hintPair: { from: { r?: number; c?: number }; to: { r?: number; c?: number }; } = { from: {}, to: {} }; // 新增成员变量,标记可消除的方块集合 private markedBlocks: Set = new Set(); private firstClicked: { r: number; c: number } | null = null; // 组合移动 private draggingGroup: { r: number; c: number }[] = []; private dragManager: DragManager | null = null; private levelBuilder: LevelBuilder; @property currentShape: string = "rectangle"; private currentShapeNode: Node = null!; private isTutorial: boolean = false; private tutorialStep: number = 0; private handNode: Node = null; async onLoad() { AudioManager.instance.registerBGM(BGMType.Level, this.levle_audio); AudioManager.instance.registerSFX(SFXType.Remove, this.remove_audio); AudioManager.instance.registerSFX(SFXType.Victory, this.victory_sfx_audio); AudioManager.instance.registerSFX(SFXType.Magic, this.magic_sfx_audio); AudioManager.instance.registerSFX(SFXType.Hint, this.hint_sfx_audio); AudioManager.instance.registerSFX(SFXType.Shuffle, this.shuffle_sfx_audio); AudioManager.instance.setCurBgm(BGMType.Level); AudioManager.instance.playBGM(BGMType.Level); this.toolBadge = this.Function_Node.getComponent(ButtonBadge); const frames = await loadSpriteFrames("textures"); this.spriteFrames = frames; this.currentShapeNode = new Node("BoardRoot"); this.currentShapeNode.addComponent(UITransform); this.node.addChild(this.currentShapeNode); // 开始关卡 this.startLevel(LevelManager.instance.getCurrentLevel()); this.bindGlobalTouch(); } private startLevel(config) { this.isTutorial = !!config.isTutorial; this.tutorialStep = 1; this.currentShapeNode.removeAllChildren(); if (this.handNode && this.handNode !== this.hintManager?.handNode) { this.handNode.destroy(); } this.handNode = null; this.levelBuilder = new LevelBuilder( this.currentShapeNode, this.blockPrefab, this.spriteFrames, config, ); this.levelBuilder.buildBoard(); // 赋值给 GameManager this.board = this.levelBuilder.board; this.blockNodes = this.levelBuilder.blockNodes; this.rows = this.levelBuilder.rows; this.cols = this.levelBuilder.cols; this.blockSize = config.blockSize; this.playEnterAnimation(); this.dragManager = new DragManager( this.board, this.blockNodes, this.rows, this.cols, this.blockSize, ); console.log("关卡生成完成", this.board, this.blockNodes); if (this.isTutorial) { this.initTutorial(); } } private initTutorial() { // 尝试获取 HintManager if (!this.hintManager) { this.hintManager = this.getComponent(HintManager); } // 尝试获取 Hand 节点 if (this.hintManager && this.hintManager.handNode) { this.handNode = this.hintManager.handNode; } else { // 尝试在场景中查找 Hand const foundHand = this.node.getChildByName("Hand") || this.node.scene.getChildByName("Hand"); if (foundHand) { this.handNode = foundHand; } else { console.warn( "HintManager.handNode is not set, trying to load dynamically...", ); // 动态加载手势资源作为后备方案 resources.load( "ui/hand/spriteFrame", SpriteFrame, (err, spriteFrame) => { if (err) { console.error("动态加载手势失败", err); return; } const node = new Node("Hand"); const sprite = node.addComponent(Sprite); sprite.spriteFrame = spriteFrame; this.node.addChild(node); this.handNode = node; node.layer = this.node.layer; this.updateTutorialStep(); }, ); return; // 等待加载完成 } } if (this.handNode) { this.updateTutorialStep(); } else { console.error("无法找到手势节点 (Hand Node),教学引导可能无法显示"); } } private updateTutorialStep() { if (!this.handNode) return; this.handNode.active = true; this.handNode.setSiblingIndex(999); // Ensure on top Tween.stopAllByTarget(this.handNode); // Clean existing tweens // Fixed board: // [0, 0, -1] // [1, -1, 1] // Step 1: Click (0,0) // Step 2: Click (0,1) // Step 3: Drag (1,0) -> (1,1) let targetPos: Vec3; let targetPos2: Vec3; switch (this.tutorialStep) { case 1: // Point at (0,0) targetPos = this.getPos(0, 0); this.handNode.setPosition(targetPos.x + 30, targetPos.y - 30, 0); tween(this.handNode) .repeatForever( tween() .to(0.5, { scale: new Vec3(1.2, 1.2, 1) }) .to(0.5, { scale: new Vec3(1, 1, 1) }), ) .start(); break; case 2: // Point at (0,1) targetPos = this.getPos(0, 1); this.handNode.setPosition(targetPos.x + 30, targetPos.y - 30, 0); tween(this.handNode) .repeatForever( tween() .to(0.5, { scale: new Vec3(1.2, 1.2, 1) }) .to(0.5, { scale: new Vec3(1, 1, 1) }), ) .start(); break; case 3: // Drag (1,0) -> (1,1) targetPos = this.getPos(1, 0); targetPos2 = this.getPos(1, 1); this.handNode.setPosition(targetPos); tween(this.handNode) .repeatForever( tween() .to(1.0, { position: targetPos2 }) .delay(0.2) .set({ position: targetPos }), ) .start(); break; default: this.handNode.active = false; break; } } /** * resetState */ public resetState() { this.firstClicked = null; this.markedBlocks.clear(); } /** 播放进入关卡时的淡入动画 */ private playEnterAnimation() { for (let r = 0; r < this.levelBuilder.blockNodes.length; r++) { for (let c = 0; c < this.levelBuilder.blockNodes[r].length; c++) { const node = this.levelBuilder.blockNodes[r][c]; if (node && node.isValid) { node.setScale(0, 0, 0); tween(node) .to(0.3, { scale: new Vec3(1, 1, 1) }) .start(); } } } } public async onLevelComplete() { this.resetState(); const oldNodes: Node[] = []; for (let r = 0; r < this.levelBuilder.blockNodes.length; r++) { for (let c = 0; c < this.levelBuilder.blockNodes[r].length; c++) { const node = this.levelBuilder.blockNodes[r][c]; if (node && node.isValid) oldNodes.push(node); } } console.log(3333, oldNodes); // 等待旧棋盘缩小消失 await Promise.all( oldNodes.map( (n) => new Promise((resolve) => { tween(n) .to(0.3, { scale: new Vec3(0, 0, 0) }) .call(() => { n.destroy(); resolve(); }) .start(); }), ), ); // 进入下一关 const next = LevelManager.instance.nextLevel(); if (next) { console.log("恭喜,下一关"); AudioManager.instance.playSFX(SFXType.Victory); this.startLevel(next); } else { console.log("恭喜,全部关卡通关!"); // 这里可以加个结算UI 或者重新开始 LevelManager.instance.reset(); this.startLevel(LevelManager.instance.getCurrentLevel()); } } start() {} getPos(row: number, col: number): Vec3 { const x = col * this.blockSize - (this.cols * this.blockSize) / 2 + this.blockSize / 2; const y = (this.rows * this.blockSize) / 2 - row * this.blockSize - this.blockSize / 2; return new Vec3(x, y, 0); } setBlockBgColor(block: Node, color: Color) { if (block) { const bgNode = block.getChildByName("Bg"); const bgSprite = bgNode.getComponent(Sprite)!; bgSprite.color = color; } } bindGlobalTouch() { input.on(Input.EventType.TOUCH_START, (event: EventTouch) => this.handleTouchStart(event), ); input.on(Input.EventType.TOUCH_MOVE, (event: EventTouch) => this.handleTouchMove(event), ); input.on(Input.EventType.TOUCH_END, () => this.handleTouchEnd()); input.on(Input.EventType.TOUCH_CANCEL, (event: EventTouch) => this.handleTouchCancel(), ); } // 开始滑动 handleTouchStart(event) { this.hintManager.cancelHint(); this.resetHintPair(); this.clearMarkVisuals(); const pos = event.getUILocation(); const res = this.findBlockAtPosition(pos); if (!res) return; const { block, r, c } = res; if (this.isTutorial) { if (this.tutorialStep === 1 && (r !== 0 || c !== 0)) return; if (this.tutorialStep === 2 && (r !== 0 || c !== 1)) return; if (this.tutorialStep === 3 && (r !== 1 || c !== 0)) return; if (this.tutorialStep > 3) return; } this.draggingBlock = block; this.dragRow = r; this.dragCol = c; this.dragStartPos = pos.clone(); this.dragStartBlockPos = block.getPosition().clone(); this.slidingDir = null; this.draggingGroup = []; // 初始化空数组 this.setBlockBgColor(this.draggingBlock, new Color(255, 100, 100)); } // 拖动中 handleTouchMove(event) { if (!this.draggingBlock) return; if (this.isTutorial && this.tutorialStep < 3) return; const pos = event.getUILocation(); const dx = pos.x - this.dragStartPos.x; const dy = pos.y - this.dragStartPos.y; // 没锁定滑动方向前 if (!this.slidingDir) { if (Math.abs(dx) < this.threshold && Math.abs(dy) < this.threshold) return; // 根据滑动方向锁定行或列滑动 if (Math.abs(dx) > Math.abs(dy)) { this.slidingDir = { dirR: 0, dirC: dx > 0 ? 1 : -1 }; } else { this.slidingDir = { dirR: dy > 0 ? -1 : 1, dirC: 0 }; } } this.draggingGroup = this.dragManager.findDragGroup( this.dragRow, this.dragCol, this.slidingDir.dirR, this.slidingDir.dirC, ); // 检查整组前方空格是否足够滑动 const maxSteps = this.dragManager.countEmptyStepsGroup( this.draggingGroup, this.slidingDir.dirR, this.slidingDir.dirC, ); if (maxSteps === 0) { this.slidingDir = null; this.draggingGroup = []; return; } this.dragManager.applyDragOffset( this.draggingGroup, maxSteps, this.slidingDir.dirR, this.slidingDir.dirC, dy, dx, ); } // 滑动结束 async handleTouchEnd() { if (!this.draggingBlock) return; if (!this.slidingDir) { // 没滑动,单击消除逻辑复用 this.onBlockClick(this.dragRow, this.dragCol); // 复位单个方块位置 this.draggingBlock.setPosition(this.getPos(this.dragRow, this.dragCol)); } else { // 计算滑动步数 let steps = 0; const firstBlockPos = this.getPos(this.dragRow, this.dragCol); const curPos = this.draggingBlock.getPosition(); if (this.slidingDir.dirR !== 0) { const delta = curPos.y - firstBlockPos.y; steps = Math.round(delta / this.blockSize) * this.slidingDir.dirR * -1; } else { const delta = curPos.x - firstBlockPos.x; steps = Math.round(delta / this.blockSize) * this.slidingDir.dirC; } const maxSteps = this.dragManager.countEmptyStepsGroup( this.draggingGroup, this.slidingDir.dirR, this.slidingDir.dirC, ); if (steps > maxSteps) steps = maxSteps; if (steps < 0) steps = 0; if (steps === 0) { // 无滑动,复位所有方块位置 this.draggingGroup.forEach(({ r, c }) => { const block = this.blockNodes[r][c]; if (block) block.setPosition(this.getPos(r, c)); }); } else { // 有滑动的情况 const type = this.board[this.dragRow][this.dragCol]; // 先缓存所有方块类型,拖动多块时保持准确 const blocksInfo = this.draggingGroup.map(({ r, c }) => ({ r, c, type: this.board[r][c], node: this.blockNodes[r][c], })); // 先把原位置置空 blocksInfo.forEach(({ r, c }) => { this.board[r][c] = -1; this.blockNodes[r][c] = null; }); // 按拖动方向排序,避免位置覆盖冲突 const sortedBlocks = [...blocksInfo].sort((a, b) => { if (this.slidingDir.dirR !== 0) { return this.slidingDir.dirR === 1 ? b.r - a.r : a.r - b.r; } else { return this.slidingDir.dirC === 1 ? b.c - a.c : a.c - b.c; } }); // 清理状态 this.firstClicked = null; this.markedBlocks.clear(); // 只针对发起拖动的方块(起点) const startTr = this.dragRow + this.slidingDir.dirR * steps; const startTc = this.dragCol + this.slidingDir.dirC * steps; const startType = type; const neighbors = this.findAdjacentSameType( startTr, startTc, startType, blocksInfo, this.slidingDir, ); if (neighbors.length === 0) { this.dragManager.settleDrag(blocksInfo, true); this.setBlockBgColor(this.draggingBlock, new Color(255, 255, 255)); } else { // 移动到新位置,更新board和blockNodes this.dragManager.settleDrag( sortedBlocks, false, steps, this.slidingDir, ); if (neighbors.length === 1) { this.playRemoveAudio(); // 唯一邻居,连线并消除起点和邻居 const onlyNeighbor = neighbors[0]; this.setBlockBgColor( this.blockNodes[onlyNeighbor.nr][onlyNeighbor.nc], new Color(255, 100, 100), ); this.drawLinkLine( this.blockNodes[startTr][startTc], this.blockNodes[onlyNeighbor.nr][onlyNeighbor.nc], ); this.scheduleOnce(() => { this.removeBlock(startTr, startTc); this.removeBlock(onlyNeighbor.nr, onlyNeighbor.nc); const empty = this.isBoardEmpty(); if (empty) { this.onLevelComplete(); console.log("下一关"); } else { const result = this.hasAvailableMove(); console.log(`${result ? "还有可移动的了" : "没有可移动的了"}`); } }, 0.5); } else { // 多邻居,标记高亮起点和邻居 this.markedBlocks.clear(); // this.markedBlocks.add(`${startTr},${startTc}`); this.setBlockBgColor( this.blockNodes[startTr][startTc], new Color(255, 100, 100), ); neighbors.forEach(({ nr, nc }) => this.markedBlocks.add(`${nr},${nc}`), ); this.updateMarkVisuals(); this.firstClicked = { r: startTr, c: startTc }; } } } } this.setBlockBgColor(this.draggingBlock, new Color(255, 255, 255)); this.draggingBlock = null; this.draggingGroup = []; this.slidingDir = null; } // 取消滑动 handleTouchCancel() { if (!this.draggingBlock) return; // 复位所有拖动方块位置 this.setBlockBgColor(this.draggingBlock, new Color(255, 255, 255)); if (this.draggingGroup.length > 0) { this.draggingGroup.forEach(({ r, c }) => { const block = this.blockNodes[r][c]; if (block) block.setPosition(this.getPos(r, c)); }); } else { this.draggingBlock.setPosition(this.getPos(this.dragRow, this.dragCol)); } this.draggingBlock = null; this.draggingGroup = []; this.slidingDir = null; } findBlockAtPosition(pos: Vec2): { block: Node; r: number; c: number } | null { const uiTransform = this.currentShapeNode.getComponent(UITransform) || this.currentShapeNode.addComponent(UITransform); const localPos = uiTransform.convertToNodeSpaceAR( new Vec3(pos.x, pos.y, 0), ); const c = Math.floor( (localPos.x + (this.cols * this.blockSize) / 2) / this.blockSize, ); const r = Math.floor( ((this.rows * this.blockSize) / 2 - localPos.y) / this.blockSize, ); if (r >= 0 && r < this.rows && c >= 0 && c < this.cols) { const block = this.blockNodes[r][c]; if (block) { return { block, r, c }; } } return null; } countEmptySteps(r: number, c: number, dirR: number, dirC: number): number { let count = 0; let nr = r + dirR; let nc = c + dirC; while (nr >= 0 && nr < this.rows && nc >= 0 && nc < this.cols) { if (this.board[nr][nc] === -1) count++; else break; nr += dirR; nc += dirC; } return count; } // 点击方块处理 zzc onBlockClick(r: number, c: number) { if (this.isTutorial) { if (this.tutorialStep === 1 && r === 0 && c === 0) { this.tutorialStep = 2; this.updateTutorialStep(); } else if (this.tutorialStep === 2 && r === 0 && c === 1) { this.tutorialStep = 3; this.updateTutorialStep(); } } const key = `${r},${c}`; const type = this.board[r][c]; if (type < 0) return; // 空格不处理 if (!this.firstClicked) { // 第一次点击 this.firstClicked = { r, c }; this.markedBlocks.clear(); // 四个方向找邻居 const dirs = [ { dr: -1, dc: 0 }, // 上 { dr: 1, dc: 0 }, // 下 { dr: 0, dc: -1 }, // 左 { dr: 0, dc: 1 }, // 右 ]; let validNeighbors: { nr: number; nc: number }[] = []; dirs.forEach(({ dr, dc }) => { const neighbor = this.findFirstNeighbor(r, c, dr, dc); if (neighbor && this.board[neighbor.nr][neighbor.nc] === type) { validNeighbors.push(neighbor); this.markedBlocks.add(`${neighbor.nr},${neighbor.nc}`); } }); if (validNeighbors.length === 1) { // 唯一邻居 → 直接消除 const onlyNeighbor = validNeighbors[0]; this.setBlockBgColor( this.blockNodes[onlyNeighbor.nr][onlyNeighbor.nc], new Color(255, 100, 100), ); this.drawLinkLine( this.blockNodes[r][c], this.blockNodes[onlyNeighbor.nr][onlyNeighbor.nc], ); this.playRemoveAudio(); this.scheduleOnce(() => { this.removeBlock(r, c); this.removeBlock(onlyNeighbor.nr, onlyNeighbor.nc); this.firstClicked = null; this.markedBlocks.clear(); const empty = this.isBoardEmpty(); if (empty) { this.onLevelComplete(); console.log("下一关"); } else { const result = this.hasAvailableMove(); console.log(`${result ? "还有可移动的了" : "没有可移动的了"}`); } }, 0.5); return; } // 否则高亮标记 this.updateMarkVisuals(); return; } // 第二次点击 if (this.markedBlocks.has(key)) { this.playRemoveAudio(); this.drawLinkLine( this.blockNodes[this.firstClicked.r][this.firstClicked.c], this.blockNodes[r][c], ); const targets = [this.firstClicked, { r, c }]; this.scheduleOnce(() => { targets.forEach(({ r, c }) => this.removeBlock(r, c)); }, 0.5); this.clearMarkVisuals(); this.markedBlocks.clear(); this.firstClicked = null; const empty = this.isBoardEmpty(); if (empty) { this.onLevelComplete(); console.log("下一关"); } else { const result = this.hasAvailableMove(); console.log(`${result ? "还有可移动的了" : "没有可移动的了"}`); } } else { // 重新选择 this.clearMarkVisuals(); this.markedBlocks.clear(); this.firstClicked = null; this.onBlockClick(r, c); } } findFirstNeighbor(r: number, c: number, dr: number, dc: number) { let nr = r + dr; let nc = c + dc; while (nr >= 0 && nr < this.rows && nc >= 0 && nc < this.cols) { if (this.board[nr][nc] >= 0) { return { nr, nc }; // 找到第一个非空 } nr += dr; nc += dc; } return null; // 这个方向没有邻居 } removeBlock(r: number, c: number) { this.board[r][c] = -1; if (this.blockNodes[r][c]) { this.blockNodes[r][c].destroy(); this.blockNodes[r][c] = null; } } // 找当前方块上下左右相邻且同类型的方块(单格,不连线) findAdjacentSameType( r: number, c: number, type: number, dragGroup: { r: number; c: number }[] = [], slidingDir: { dirR: number; dirC: number } | null = null, ): { nr: number; nc: number }[] { const dirs = [ { dr: -1, dc: 0 }, // 上 { dr: 1, dc: 0 }, // 下 { dr: 0, dc: -1 }, // 左 { dr: 0, dc: 1 }, // 右 ]; const neighbors: { nr: number; nc: number }[] = []; dirs.forEach(({ dr, dc }) => { const neighbor = this.findFirstNeighbor(r, c, dr, dc); if (neighbor && this.board[neighbor.nr][neighbor.nc] === type) { if ( dr === slidingDir.dirR && dc === slidingDir.dirC && dragGroup.length > 1 ) { } else { neighbors.push(neighbor); this.markedBlocks.add(`${neighbor.nr},${neighbor.nc}`); } } }); return neighbors; } updateMarkVisuals() { this.markedBlocks.forEach((key) => { const [r, c] = key.split(",").map(Number); this.setBlockBgColor(this.blockNodes[r][c], new Color(255, 255, 0)); }); } hasSameNeighbor(row: number, col: number, type: number): boolean { const dirs = [ { dr: -1, dc: 0 }, // 上 { dr: 1, dc: 0 }, // 下 { dr: 0, dc: -1 }, // 左 { dr: 0, dc: 1 }, // 右 ]; for (const { dr, dc } of dirs) { const nr = row + dr; const nc = col + dc; if (nr >= 0 && nr < this.rows && nc >= 0 && nc < this.cols) { if (this.board[nr][nc] === type) { return true; } } } return false; } clearMarkVisuals() { this.markedBlocks.forEach((key) => { const [r, c] = key.split(",").map(Number); this.setBlockBgColor(this.blockNodes[r][c], new Color(255, 255, 255)); }); } // 消除所有标记的方块,并清理状态 clearMarkedBlocks() { if (this.markedBlocks.size === 0) return; for (const key of this.markedBlocks) { const [r, c] = key.split(",").map(Number); this.board[r][c] = -1; const block = this.blockNodes[r][c]; if (block) { block.active = false; this.blockNodes[r][c] = null; } } this.markedBlocks.clear(); // this.selectedBlock = null; this.clearMarkVisuals(); } drawLinkLine(blockA: Node, blockB: Node) { const g = this.lineLayer.getComponent(Graphics); g.clear(); // 获取世界坐标 const posAWorld = blockA.worldPosition; const posBWorld = blockB.worldPosition; // 转换到 lineLayer 本地坐标 const posA = this.lineLayer .getComponent(UITransform) .convertToNodeSpaceAR(posAWorld); const posB = this.lineLayer .getComponent(UITransform) .convertToNodeSpaceAR(posBWorld); // 绘制线 g.lineWidth = 5; g.strokeColor = Color.YELLOW; g.moveTo(posA.x, posA.y); g.lineTo(posB.x, posB.y); g.stroke(); // 1 秒后清除 this.scheduleOnce(() => { g.clear(); }, 0.5); } isBoardEmpty(): boolean { for (let r = 0; r < this.rows; r++) { for (let c = 0; c < this.cols; c++) { if (this.board[r][c] >= 0) return false; } } return true; } hasAvailableMove(): boolean { return this.findFirstEliminablePair() !== null; } // 辅助函数:模拟 group 移动 step 步后,是否有可消除邻居 getNeighborsAfterMove( r: number, c: number, step: number, dragGroup: { r: number; c: number }[] = [], slidingDir: { dr: number; dc: number } | null = null, ): { nr: number; nc: number }[] { const type = this.board[r][c]; // 预计算移动后的位置和原位置 const newPositions = new Set(); const oldPositions = new Set(); dragGroup.forEach(({ r: gr, c: gc }) => { const tr = gr + slidingDir.dr * step; const tc = gc + slidingDir.dc * step; newPositions.add(`${tr},${tc}`); oldPositions.add(`${gr},${gc}`); }); const dirs = [ { dr: -1, dc: 0 }, // 上 { dr: 1, dc: 0 }, // 下 { dr: 0, dc: -1 }, // 左 { dr: 0, dc: 1 }, // 右 ]; const neighbors: { nr: number; nc: number }[] = []; // 目标位置(也就是当前检查的方块移动后的位置) const tr = r + slidingDir.dr * step; const tc = c + slidingDir.dc * step; dirs.forEach(({ dr, dc }) => { let nr = tr + dr; let nc = tc + dc; while (nr >= 0 && nr < this.rows && nc >= 0 && nc < this.cols) { const key = `${nr},${nc}`; let cellType = -1; if (newPositions.has(key)) { // 是移动过来的方块,找到它的原始位置获取类型 const or = nr - slidingDir.dr * step; const oc = nc - slidingDir.dc * step; cellType = this.board[or][oc]; } else if (oldPositions.has(key)) { // 是原来的位置,但现在空了(且没被新方块占据) cellType = -1; } else { // 静态方块 cellType = this.board[nr][nc]; } if (cellType >= 0) { // 找到第一个非空 if (cellType === type) { // 排除自身移动方向上的内部连接(通常不需要消除) if ( dr === slidingDir.dr && dc === slidingDir.dc && dragGroup.length > 1 ) { // ignore } else { neighbors.push({ nr, nc }); } } break; } else if (cellType === -2) { // 石头,阻挡 break; } nr += dr; nc += dc; } }); return neighbors; } /** 四方向常量 */ private static readonly DIRS = [ { dr: -1, dc: 0 }, // 上 { dr: 1, dc: 0 }, // 下 { dr: 0, dc: -1 }, // 左 { dr: 0, dc: 1 }, // 右 ]; /** * 核心:找到第一对“可以消除”的方块 * 规则覆盖: * - 直接相邻; * - 单格滑动(穿过空格); * - 多块组(连续段)从两端向外滑动(两端之外第一非空如果同色就可消) * 返回 null 表示不存在 */ private findFirstEliminablePair(): { from: { r: number; c: number }; to: { r: number; c: number }; } | null { for (let r = 0; r < this.rows; r++) { for (let c = 0; c < this.cols; c++) { const type = this.board[r][c]; if (type < 0) continue; // 1️⃣ 单格邻居提示 const hint = this.checkSingleNeighbor(r, c); if (hint) return hint; // 2️⃣ 滑动提示 const hint2 = this.checkMoveNeighbor(r, c); if (hint2) return hint2; } } return null; } /** * 提示函数 * 返回可操作消除的一组方块坐标 [{r, c}, ...] * 如果没有可消除组,返回空数组 [] */ getHint(): { from: { r: number; c: number }; to: { r: number; c: number }; steps?: number; dir?: { dr: number; dc: number }; } | null { return this.findFirstEliminablePair(); } // 单格邻居检测 checkSingleNeighbor(r: number, c: number) { const type = this.board[r][c]; for (const { dr, dc } of GameManager.DIRS) { const nr = r + dr; const nc = c + dc; if (nr < 0 || nr >= this.rows || nc < 0 || nc >= this.cols) continue; if (this.board[nr][nc] === type) { return { from: { r, c }, to: { r: nr, c: nc } }; } } return null; } // 滑动提示 checkMoveNeighbor(r: number, c: number) { for (const { dr, dc } of GameManager.DIRS) { const group = this.dragManager.findDragGroup(r, c, dr, dc); const maxSteps = this.dragManager.countEmptyStepsGroup(group, dr, dc); if (maxSteps === 0) continue; for (let step = 1; step <= maxSteps; step++) { const neighbors = this.getNeighborsAfterMove(r, c, step, group, { dr, dc, }); if (neighbors.length > 0) { const { nr, nc } = neighbors[0]; return { from: { r, c }, to: { r: nr, c: nc }, steps: step, dir: { dr, dc }, }; } } } return null; } highlightHint() { const count = this.toolBadge.getHintCount(); if (count <= 0) { console.log("看广告"); this.toolBadge.showAdAndReward("hint"); return; } const hint = this.getHint(); if (hint) { AudioManager.instance.playSFX(SFXType.Hint); if (hint?.steps) { this.hintManager.showHint(hint, this.getPos.bind(this)); } this.toolBadge.useHintOnce(); this.hintPair.from = hint.from; this.hintPair.to = hint.to; this.setBlockBgColor( this.blockNodes[hint.from.r][hint.from.c], new Color(255, 100, 100), ); this.setBlockBgColor( this.blockNodes[hint.to.r][hint.to.c], new Color(255, 100, 100), ); } else { console.log("没有可消除的组合"); } // if (hintBlocks.length === 0) return; // hintBlocks.forEach(({ nr, nc }) => { // const node = this.blockNodes[nr][nc]; // if (!node) return; // // 闪烁动画:颜色在原色和高亮色之间切换 // const originalColor = node.getComponent(Sprite)!.color.clone(); // const highlightColor = new Color(255, 255, 0); // 黄色高亮 // tween(node.getComponent(Sprite)!) // .to(0.3, { color: highlightColor }) // .to(0.3, { color: originalColor }) // .to(0.3, { color: highlightColor }) // .to(0.3, { color: originalColor }) // .start(); // }); } playRemoveAudio() { AudioManager.instance.playSFX(SFXType.Remove); } shuffleBoard(): void { const count = this.toolBadge.getShuffleCount(); console.log("打乱次数", count); if (count <= 0) { console.log("看广告"); this.toolBadge.showAdAndReward("shuffle"); return; } // 1. 收集剩余方块类型 const blocks: number[] = []; for (let r = 0; r < this.rows; r++) { for (let c = 0; c < this.cols; c++) { if (this.board[r][c] >= 0) { blocks.push(this.board[r][c]); } } } // 2. Fisher–Yates 随机打乱 for (let i = blocks.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [blocks[i], blocks[j]] = [blocks[j], blocks[i]]; } // 3. 重新写入 this.board let idx = 0; for (let r = 0; r < this.rows; r++) { for (let c = 0; c < this.cols; c++) { if (this.board[r][c] >= 0) { this.board[r][c] = blocks[idx++]; } } } AudioManager.instance.playSFX(SFXType.Shuffle); // 4. 更新渲染状态 blockNodes this.updateBlockNodes(); // 5. 确保有解,没有就重新 shuffle this.toolBadge.useShuffleOnce(); if (!this.getHint()) { this.shuffleBoard(); // 递归重新打乱直到有解 } } /** * 遍历 board 更新所有 blockNodes */ updateBlockNodes(): void { for (let r = 0; r < this.rows; r++) { for (let c = 0; c < this.cols; c++) { const type = this.board[r][c]; if (type >= 0) { let node = this.blockNodes[r][c]; if (!node) { // 如果之前是空格,新建节点 // node = this.createBlockNode(type, r, c); // this.blockNodes[r][c] = node; // this.board[r][c] = type; const block = this.levelBuilder.createBlockNode(type, r, c); this.blockNodes[r][c] = block; } else { // 更新已有节点的类型显示 this.levelBuilder.setBlockType(node, type); this.levelBuilder.setBlockPosition(node, r, c); } } else if (type === -1) { // 空格 → 清理节点 if (this.blockNodes[r][c]) { this.levelBuilder.destroyBlockNode(this.blockNodes[r][c]); this.blockNodes[r][c] = null; } } else if (type === -2) { // 石头 不处理 } } } } // Fisher–Yates 洗牌 shuffleArray(arr: T[]): T[] { for (let i = arr.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [arr[i], arr[j]] = [arr[j], arr[i]]; } return arr; } // 检查棋盘是否有解 hasSolution(board: number[][]): boolean { // 临时替换 this.board const backup = this.board; this.board = board; const hint = this.findFirstEliminablePair(); this.board = backup; // 恢复 return !!hint; } public eliminateRandomType(): void { const count = this.toolBadge.getMagicCount(); if (count <= 0) { console.log("看广告"); this.toolBadge.showAdAndReward("magic"); return; } const types = new Set(); // 收集当前所有存在的方块类型 for (let r = 0; r < this.rows; r++) { for (let c = 0; c < this.cols; c++) { const val = this.board[r][c]; if (val >= 0) { types.add(val); } } } if (types.size === 0) { console.warn("没有可消除的方块了"); this.onLevelComplete(); return; } // 转成数组,随机挑一个 const typeArray = Array.from(types); const randType = typeArray[Math.floor(Math.random() * typeArray.length)]; console.log("随机消除类型:", randType); this.eliminateType(randType); } public eliminateType(type: number): void { this.toolBadge.useMagicOnce(); AudioManager.instance.playSFX(SFXType.Magic); for (let r = 0; r < this.rows; r++) { for (let c = 0; c < this.cols; c++) { if (this.board[r][c] === type) { this.board[r][c] = -1; const node = this.blockNodes[r][c]; if (node) { this.playEliminateAnimation(node, () => { this.destroyBlockNode(node); this.blockNodes[r][c] = null; }); } } } } const empty = this.isBoardEmpty(); if (empty) { setTimeout(() => { this.onLevelComplete(); console.log("下一关"); }, 1000); } } private playEliminateAnimation(node: Node, onFinish: () => void) { // 确保有透明组件 let opacityComp = node.getComponent(UIOpacity); if (!opacityComp) { opacityComp = node.addComponent(UIOpacity); } opacityComp.opacity = 255; // 动画:缩放 → 轻微旋转 → 弹缩 → 渐隐消失 tween(node) .parallel( tween().to(0.15, { scale: new Vec3(1.2, 1.2, 1) }), // 先放大一点 tween().to(0.15, { angle: 30 }), // 顺时针轻微旋转 ) .parallel( tween().to( 0.25, { scale: new Vec3(0.8, 0.8, 1) }, { easing: "quadOut" }, ), // 缩小回弹 tween().to(0.25, { angle: -30 }), // 逆时针旋转 ) .parallel( tween().to(0.2, { scale: new Vec3(0, 0, 0) }), // 最终缩小 tween(opacityComp).to(0.2, { opacity: 0 }), // 渐隐 ) .call(() => { onFinish(); }) .start(); } public destroyBlockNode(node: Node): void { if (node && node.isValid) { node.removeFromParent(); node.destroy(); } } public resetHintPair() { if ( this.isNumber(this.hintPair?.from?.r) && this.isNumber(this.hintPair?.from?.c) ) { this.setBlockBgColor( this.blockNodes[this.hintPair?.from?.r][this.hintPair?.from?.c], new Color(255, 255, 255), ); } if ( this.isNumber(this.hintPair?.to?.r) && this.isNumber(this.hintPair?.to?.c) ) { this.setBlockBgColor( this.blockNodes[this.hintPair.to.r][this.hintPair.to.c], new Color(255, 255, 255), ); } this.hintPair = { from: {}, to: {} }; } public isNumber(value) { return ( value !== null && value !== undefined && typeof value === "number" && !isNaN(value) ); } }