Files
slide_block/assets/scripts/GameManager.ts
1970-01-01 08:27:52 +08:00

1387 lines
38 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string> = 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<void>((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<string>();
const oldPositions = new Set<string>();
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. FisherYates 随机打乱
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) {
// 石头 不处理
}
}
}
}
// FisherYates 洗牌
shuffleArray<T>(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<number>();
// 收集当前所有存在的方块类型
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)
);
}
}