1387 lines
38 KiB
TypeScript
1387 lines
38 KiB
TypeScript
|
|
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. 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<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)
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|