Files

1387 lines
38 KiB
TypeScript
Raw Permalink Normal View History

1970-01-01 08:27:52 +08:00
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)
);
}
}