268 lines
7.2 KiB
TypeScript
268 lines
7.2 KiB
TypeScript
|
|
import {
|
|||
|
|
Node,
|
|||
|
|
Prefab,
|
|||
|
|
instantiate,
|
|||
|
|
Sprite,
|
|||
|
|
SpriteFrame,
|
|||
|
|
UITransform,
|
|||
|
|
Vec3,
|
|||
|
|
} from "cc";
|
|||
|
|
import { BoardShapes, BoardShapeName } from "./BoardShape";
|
|||
|
|
|
|||
|
|
export interface LevelConfig {
|
|||
|
|
rows?: number;
|
|||
|
|
cols?: number;
|
|||
|
|
blockSize?: number;
|
|||
|
|
tileTypes?: number;
|
|||
|
|
currentShape?: BoardShapeName;
|
|||
|
|
isTutorial?: boolean;
|
|||
|
|
fixedBoard?: number[][];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export class LevelBuilder {
|
|||
|
|
public board: number[][] = [];
|
|||
|
|
public blockNodes: (Node | null)[][] = [];
|
|||
|
|
public rows: number = 0;
|
|||
|
|
public cols: number = 0;
|
|||
|
|
|
|||
|
|
private node: Node;
|
|||
|
|
private blockPrefab: Prefab;
|
|||
|
|
private spriteFrames: SpriteFrame[] = [];
|
|||
|
|
private blockSize: number = 100;
|
|||
|
|
private tileTypes: number = 5;
|
|||
|
|
|
|||
|
|
public currentShape: BoardShapeName = "rectangle";
|
|||
|
|
private fixedBoard: number[][] | null = null;
|
|||
|
|
|
|||
|
|
constructor(
|
|||
|
|
node: Node,
|
|||
|
|
blockPrefab: Prefab,
|
|||
|
|
spriteFrames: SpriteFrame[],
|
|||
|
|
config?: LevelConfig,
|
|||
|
|
) {
|
|||
|
|
this.node = node;
|
|||
|
|
this.blockPrefab = blockPrefab;
|
|||
|
|
this.spriteFrames = spriteFrames;
|
|||
|
|
|
|||
|
|
if (config) {
|
|||
|
|
this.blockSize = config.blockSize ?? this.blockSize;
|
|||
|
|
this.tileTypes = config.tileTypes ?? this.tileTypes;
|
|||
|
|
if (config.currentShape) this.currentShape = config.currentShape;
|
|||
|
|
if (config.fixedBoard) this.fixedBoard = config.fixedBoard;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public buildBoard(): void {
|
|||
|
|
if (this.fixedBoard) {
|
|||
|
|
this.buildFixedBoard();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const shapeTemplate = BoardShapes[this.currentShape];
|
|||
|
|
if (!shapeTemplate) throw new Error(`Shape "${this.currentShape}" 未定义`);
|
|||
|
|
|
|||
|
|
const maxCols = Math.max(...shapeTemplate.map((row) => row.length));
|
|||
|
|
const rows = shapeTemplate.length;
|
|||
|
|
|
|||
|
|
// 自动补齐矩形
|
|||
|
|
const fixedTemplate: number[][] = [];
|
|||
|
|
for (let r = 0; r < rows; r++) {
|
|||
|
|
fixedTemplate[r] = [];
|
|||
|
|
for (let c = 0; c < maxCols; c++) {
|
|||
|
|
fixedTemplate[r][c] = shapeTemplate[r][c] ?? 0;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 收集可用格子
|
|||
|
|
const validCells = [];
|
|||
|
|
for (let r = 0; r < rows; r++) {
|
|||
|
|
for (let c = 0; c < maxCols; c++) {
|
|||
|
|
if (fixedTemplate[r][c] === 1) validCells.push({ r, c });
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (validCells.length % 2 !== 0) throw new Error("有效格子数量必须为偶数");
|
|||
|
|
|
|||
|
|
// 随机生成配对
|
|||
|
|
let tiles: number[] = [];
|
|||
|
|
for (let i = 0; i < validCells.length / 2; i++) {
|
|||
|
|
const id = i % this.tileTypes;
|
|||
|
|
tiles.push(id, id);
|
|||
|
|
}
|
|||
|
|
for (let i = tiles.length - 1; i > 0; i--) {
|
|||
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|||
|
|
[tiles[i], tiles[j]] = [tiles[j], tiles[i]];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let tileIndex = 0;
|
|||
|
|
this.board = [];
|
|||
|
|
this.blockNodes = [];
|
|||
|
|
this.rows = rows;
|
|||
|
|
this.cols = maxCols;
|
|||
|
|
|
|||
|
|
for (let r = 0; r < rows; r++) {
|
|||
|
|
this.board[r] = [];
|
|||
|
|
this.blockNodes[r] = [];
|
|||
|
|
for (let c = 0; c < maxCols; c++) {
|
|||
|
|
if (fixedTemplate[r][c] === 1) {
|
|||
|
|
const tileId = tiles[tileIndex++];
|
|||
|
|
this.board[r][c] = tileId;
|
|||
|
|
const block = this.createNormalBlock(tileId);
|
|||
|
|
block.setPosition(this.getPos(r, c));
|
|||
|
|
this.node.addChild(block);
|
|||
|
|
this.blockNodes[r][c] = block;
|
|||
|
|
} else if (fixedTemplate[r][c] === -1) {
|
|||
|
|
this.board[r][c] = -1;
|
|||
|
|
this.blockNodes[r][c] = null;
|
|||
|
|
} else {
|
|||
|
|
this.board[r][c] = -2;
|
|||
|
|
const stone = this.createStoneBlock();
|
|||
|
|
stone.setPosition(this.getPos(r, c));
|
|||
|
|
this.node.addChild(stone);
|
|||
|
|
this.blockNodes[r][c] = stone;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private buildFixedBoard() {
|
|||
|
|
this.rows = this.fixedBoard.length;
|
|||
|
|
this.cols = this.fixedBoard[0].length;
|
|||
|
|
this.board = [];
|
|||
|
|
this.blockNodes = [];
|
|||
|
|
|
|||
|
|
for (let r = 0; r < this.rows; r++) {
|
|||
|
|
this.board[r] = [];
|
|||
|
|
this.blockNodes[r] = [];
|
|||
|
|
for (let c = 0; c < this.cols; c++) {
|
|||
|
|
const val = this.fixedBoard[r][c];
|
|||
|
|
this.board[r][c] = val;
|
|||
|
|
|
|||
|
|
if (val >= 0) {
|
|||
|
|
const block = this.createNormalBlock(val);
|
|||
|
|
block.setPosition(this.getPos(r, c));
|
|||
|
|
this.node.addChild(block);
|
|||
|
|
this.blockNodes[r][c] = block;
|
|||
|
|
} else if (val === -1) {
|
|||
|
|
this.blockNodes[r][c] = null;
|
|||
|
|
} else if (val === -2) {
|
|||
|
|
const stone = this.createStoneBlock();
|
|||
|
|
stone.setPosition(this.getPos(r, c));
|
|||
|
|
this.node.addChild(stone);
|
|||
|
|
this.blockNodes[r][c] = stone;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private createNormalBlock(tileId: number) {
|
|||
|
|
const block = instantiate(this.blockPrefab);
|
|||
|
|
const bgNode = block.getChildByName("Bg");
|
|||
|
|
const bgUI =
|
|||
|
|
bgNode.getComponent(UITransform) ?? bgNode.addComponent(UITransform);
|
|||
|
|
bgUI.setContentSize(this.blockSize, this.blockSize);
|
|||
|
|
|
|||
|
|
const iconNode = block.getChildByName("Icon");
|
|||
|
|
const iconUI =
|
|||
|
|
iconNode.getComponent(UITransform) ?? iconNode.addComponent(UITransform);
|
|||
|
|
const iconUI_sprite = iconNode.getComponent(Sprite)!;
|
|||
|
|
iconUI_sprite.spriteFrame = this.spriteFrames[tileId];
|
|||
|
|
iconUI.setContentSize(this.blockSize * 0.7, this.blockSize * 0.7);
|
|||
|
|
iconNode.setPosition(0, 0, 0);
|
|||
|
|
|
|||
|
|
return block;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private createStoneBlock() {
|
|||
|
|
const stone = instantiate(this.blockPrefab);
|
|||
|
|
const bgNode = stone.getChildByName("Bg");
|
|||
|
|
const bgUI =
|
|||
|
|
bgNode.getComponent(UITransform) ?? bgNode.addComponent(UITransform);
|
|||
|
|
bgUI.setContentSize(this.blockSize, this.blockSize);
|
|||
|
|
|
|||
|
|
const iconNode = stone.getChildByName("Icon");
|
|||
|
|
iconNode.active = false;
|
|||
|
|
|
|||
|
|
return stone;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private 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);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 新建 UI 节点,并设置类型和位置
|
|||
|
|
*/
|
|||
|
|
public createBlockNode(type: number, r: number, c: number): Node {
|
|||
|
|
let block: Node;
|
|||
|
|
|
|||
|
|
if (type >= 0) {
|
|||
|
|
// 普通方块
|
|||
|
|
block = this.createNormalBlock(type);
|
|||
|
|
} else if (type === -2) {
|
|||
|
|
// 石头障碍
|
|||
|
|
block = this.createStoneBlock();
|
|||
|
|
} else {
|
|||
|
|
// 空格不需要节点
|
|||
|
|
return null!;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
block.setPosition(this.getPos(r, c));
|
|||
|
|
this.node.addChild(block);
|
|||
|
|
|
|||
|
|
return block;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 更新节点的贴图/颜色/数字等显示
|
|||
|
|
*/
|
|||
|
|
public setBlockType(node: Node, type: number): void {
|
|||
|
|
if (!node) return;
|
|||
|
|
|
|||
|
|
const iconNode = node.getChildByName("Icon");
|
|||
|
|
if (!iconNode) return;
|
|||
|
|
|
|||
|
|
if (type >= 0) {
|
|||
|
|
// 普通方块
|
|||
|
|
iconNode.active = true;
|
|||
|
|
const iconUI =
|
|||
|
|
iconNode.getComponent(UITransform) ??
|
|||
|
|
iconNode.addComponent(UITransform);
|
|||
|
|
const iconUI_sprite = iconNode.getComponent(Sprite)!;
|
|||
|
|
iconUI_sprite.spriteFrame = this.spriteFrames[type];
|
|||
|
|
iconUI.setContentSize(this.blockSize * 0.7, this.blockSize * 0.7);
|
|||
|
|
iconNode.setPosition(0, 0, 0);
|
|||
|
|
} else if (type === -2) {
|
|||
|
|
// 石头
|
|||
|
|
iconNode.active = false;
|
|||
|
|
} else {
|
|||
|
|
// 空格
|
|||
|
|
iconNode.active = false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 更新节点的坐标位置(UI 上)
|
|||
|
|
*/
|
|||
|
|
public setBlockPosition(node: Node, r: number, c: number): void {
|
|||
|
|
if (!node) return;
|
|||
|
|
node.setPosition(this.getPos(r, c));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 销毁节点或隐藏
|
|||
|
|
*/
|
|||
|
|
public destroyBlockNode(node: Node): void {
|
|||
|
|
if (!node) return;
|
|||
|
|
node.destroy(); // 如果想复用可改为 node.active = false
|
|||
|
|
}
|
|||
|
|
}
|