first commit

This commit is contained in:
zzc
1970-01-01 08:27:52 +08:00
commit 3c80593c3d
135 changed files with 19822 additions and 0 deletions

View File

@@ -0,0 +1,139 @@
import { _decorator, Component, Node, AudioSource, AudioClip, director } from 'cc';
const { ccclass, property } = _decorator;
// 背景音乐类型
export enum BGMType {
Home,
Level,
Boss,
}
// 音效类型
export enum SFXType {
Remove,
Success,
Fail,
Item,
Victory,
Magic,
Shuffle,
Hint
}
@ccclass('AudioManager')
export class AudioManager extends Component {
private static _instance: AudioManager | null = null;
public static get instance(): AudioManager {
if (!this._instance) {
const node = new Node('AudioManager');
this._instance = node.addComponent(AudioManager);
director.addPersistRootNode(node);
}
return this._instance;
}
// AudioSources
private bgmSource!: AudioSource;
private sfxSource!: AudioSource;
// 开关
public bgmEnabled: boolean = true;
public sfxEnabled: boolean = true;
// 音乐 / 音效 Map
private bgmClips: Map<BGMType, AudioClip> = new Map();
private sfxClips: Map<SFXType, AudioClip> = new Map();
private currentBGM: BGMType | null = null;
onLoad() {
// 初始化单例
AudioManager._instance = this;
// 跨场景不销毁
// director.addPersistRootNode(this.node);
// 读取本地开关
this.bgmEnabled = localStorage.getItem('bgmEnabled') !== '0';
this.sfxEnabled = localStorage.getItem('sfxEnabled') !== '0';
// 自动挂载或创建 AudioSource
const sources = this.node.getComponents(AudioSource);
if (sources.length >= 2) {
this.bgmSource = sources[0];
this.sfxSource = sources[1];
} else {
this.bgmSource = this.node.addComponent(AudioSource);
this.bgmSource.loop = true;
this.sfxSource = this.node.addComponent(AudioSource);
}
}
/** 注册 BGM */
public registerBGM(type: BGMType, clip: AudioClip) {
this.bgmClips.set(type, clip);
}
/** 注册 SFX */
public registerSFX(type: SFXType, clip: AudioClip) {
this.sfxClips.set(type, clip);
}
/** 播放背景音乐 */
public playBGM(type: BGMType) {
if (!this.bgmEnabled) return;
// if (this.currentBGM === type) return;
const clip = this.bgmClips.get(type);
if (!clip) return;
this.currentBGM = type;
this.bgmSource.clip = clip;
this.bgmSource.loop = true;
this.bgmSource.play();
}
/** 播放音效 */
public playSFX(type: SFXType) {
if (!this.sfxEnabled) return;
const clip = this.sfxClips.get(type);
if (!clip) return;
this.sfxSource.playOneShot(clip, 1.0);
}
/** 设置 BGM 开关 */
public setBGMEnabled(enabled: boolean) {
this.bgmEnabled = enabled;
localStorage.setItem('bgmEnabled', enabled ? '1' : '0');
if (!this.bgmSource) return;
if (enabled) {
// 开启播放当前 BGM
if (this.currentBGM !== null || this.currentBGM !== undefined) {
const clip = this.bgmClips.get(this.currentBGM);
if (clip) {
this.bgmSource.clip = clip;
this.bgmSource.play();
}
}
} else {
// 关闭停止播放
this.bgmSource.stop();
}
}
/** 设置 SFX 开关 */
public setSFXEnabled(enabled: boolean) {
this.sfxEnabled = enabled;
localStorage.setItem('sfxEnabled', enabled ? '1' : '0');
// 关闭音效后playSFX 会直接返回,不会播放
}
/** 设置 SFX 开关 */
public setCurBgm(type: number) {
this.currentBGM = type;
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "b07ed8a6-f1ba-46a1-8edd-9b424894ec9f",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,48 @@
import { _decorator, Component, Node, Toggle } from 'cc';
import { AudioManager } from './AudioManager';
const { ccclass, property } = _decorator;
@ccclass('AudioSettingsUI')
export class AudioSettingsUI extends Component {
@property(Node)
panel: Node = null!; // 设置面板节点
@property(Toggle)
bgmToggle: Toggle = null!;
@property(Toggle)
sfxToggle: Toggle = null!;
onLoad() {
const am = AudioManager.instance;
// 初始化 Toggle 状态
this.bgmToggle.isChecked = am.bgmEnabled;
this.sfxToggle.isChecked = am.sfxEnabled;
// Toggle 监听
this.bgmToggle.node.on('toggle', this.onBgmToggleChanged, this);
this.sfxToggle.node.on('toggle', this.onSfxToggleChanged, this);
// 面板默认隐藏
this.panel.active = false;
}
/** 打开设置面板 */
openPanel() {
this.panel.active = true;
}
/** 关闭设置面板 */
closePanel() {
this.panel.active = false;
}
private onBgmToggleChanged(toggle: Toggle) {
AudioManager.instance.setBGMEnabled(toggle.isChecked);
}
private onSfxToggleChanged(toggle: Toggle) {
AudioManager.instance.setSFXEnabled(toggle.isChecked);
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "e6a5f56b-bc7d-48ad-82cf-23ab8d7da791",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,50 @@
export type BoardShapeName = "rectangle" | "L_shape" | "T_shape" | "cross_shape" | "diamond" | "custom";
export const BoardShapes: Record<BoardShapeName, number[][]> = {
rectangle: [
[1,1,1,1,1,1],
[1,1,1,1,1,1],
[1,1,1,1,1,1],
[1,1,1,1,1,1],
[1,1,1,1,1,1],
],
L_shape: [
[1,1,1,0,0,0],
[1,1,1,0,0,0],
[1,1,1,1,1,1],
[1,1,1,1,1,1],
],
T_shape: [
[1,1,1,1,1,1,1],
[0,0,1,1,1,0,0],
[0,0,1,1,1,0,0],
[0,0,1,1,1,0,0],
],
cross_shape: [
[0,0,1,1,0,0],
[0,0,1,1,0,0],
[1,1,1,1,1,1],
[0,0,1,1,0,0],
[0,0,1,1,0,0],
],
diamond: [
[0,0,0,1,0,0,0],
[0,0,1,1,1,0,0],
[0,1,1,1,1,1,0],
[1,1,1,1,1,1,1],
[0,1,1,1,1,1,0],
[0,0,1,1,1,0,0],
[0,0,0,1,0,0,0],
[0,0,0,1,0,0,0],
],
custom: [
[0,0,0,0,-1,0,0],
[0,0,0,0,-1,0,0],
[0,0,0,0,-1,0,0],
[0,0,0,0,1,1,1],
[0,0,0,0,1,0,0],
[0,0,0,0,0,0,0],
[0,0,0,0,0,0,0],
[0,0,0,0,0,0,0],
]
};

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "5ff0aad1-47ce-41a6-9dfd-25e0d722b769",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,195 @@
import { _decorator, Component, Node, Label, sys } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('ButtonBadge')
export class ButtonBadge extends Component {
@property(Node)
hint_badge: Node | null = null;
@property(Label)
hint_countLabel: Label | null = null;
@property(Node)
hint_adIcon: Node | null = null;
@property(Node)
shuffle_badge: Node | null = null;
@property(Label)
shuffle_countLabel: Label | null = null;
@property(Node)
shuffle_adIcon: Node | null = null;
@property(Node)
magic_badge: Node | null = null;
@property(Label)
magic_countLabel: Label | null = null;
@property(Node)
magic_adIcon: Node | null = null;
private _hint_count: number = 0;
private _hint_storageKey: string = "hint_button_usage_count";
private _shuffle_count: number = 0;
private _shuffle_storageKey: string = "shuffle_button_usage_count";
private _magic_count: number = 0;
private _magic_storageKey: string = "magic_button_usage_count";
onLoad() {
// 从本地缓存读取
const hintSaved = sys.localStorage.getItem(this._hint_storageKey);
const shuffleSaved = sys.localStorage.getItem(this._shuffle_storageKey);
const magicSaved = sys.localStorage.getItem(this._magic_storageKey);
this._hint_count = hintSaved ? parseInt(hintSaved) : 3; // 默认初始 3 次
this._shuffle_count = shuffleSaved ? parseInt(shuffleSaved) : 3; // 默认初始 3 次
this._magic_count = magicSaved ? parseInt(magicSaved) : 3; // 默认初始 3 次
console.log('load', this._shuffle_count, this._magic_count)
this.updateHintView();
this.updateShuffleView();
this.updateMagicView();
}
private updateHintView() {
if (!this.hint_badge) return;
if (this._hint_count > 0) {
this.hint_badge.active = true;
if (this.hint_countLabel) {
this.hint_countLabel.string = this._hint_count.toString();
this.hint_countLabel.node.active = true;
}
if (this.hint_adIcon) this.hint_adIcon.active = false;
} else {
this.hint_badge.active = true;
if (this.hint_countLabel) this.hint_countLabel.node.active = false;
if (this.hint_adIcon) this.hint_adIcon.active = true;
}
}
private updateShuffleView() {
if (!this.shuffle_badge) return;
if (this._shuffle_count > 0) {
this.shuffle_badge.active = true;
if (this.shuffle_countLabel) {
this.shuffle_countLabel.string = this._shuffle_count.toString();
this.shuffle_countLabel.node.active = true;
}
if (this.shuffle_adIcon) this.shuffle_adIcon.active = false;
} else {
this.shuffle_badge.active = true;
if (this.shuffle_countLabel) this.shuffle_countLabel.node.active = false;
if (this.shuffle_adIcon) this.shuffle_adIcon.active = true;
}
}
private updateMagicView() {
if (!this.magic_badge) return;
if (this._magic_count > 0) {
this.magic_badge.active = true;
if (this.magic_countLabel) {
this.magic_countLabel.string = this._magic_count.toString();
this.magic_countLabel.node.active = true;
}
if (this.magic_adIcon) this.magic_adIcon.active = false;
} else {
this.magic_badge.active = true;
if (this.magic_countLabel) this.magic_countLabel.node.active = false;
if (this.magic_adIcon) this.magic_adIcon.active = true;
}
}
private saveHint() {
sys.localStorage.setItem(this._hint_storageKey, this._hint_count.toString());
}
private saveShuffle() {
sys.localStorage.setItem(this._shuffle_storageKey, this._shuffle_count.toString());
}
private saveMagic() {
sys.localStorage.setItem(this._magic_storageKey, this._magic_count.toString());
}
public useHintOnce(): boolean {
if (this._hint_count > 0) {
this._hint_count--;
this.saveHint();
this.updateHintView();
return true; // 成功使用
} else {
// this.showAdAndReward();
return false; // 没次数,去看广告
}
}
public useShuffleOnce(): boolean {
if (this._shuffle_count > 0) {
this._shuffle_count--;
this.saveShuffle();
this.updateShuffleView();
return true; // 成功使用
} else {
// this.showAdAndReward();
return false; // 没次数,去看广告
}
}
public useMagicOnce(): boolean {
if (this._magic_count > 0) {
this._magic_count--;
this.saveMagic();
this.updateMagicView();
return true; // 成功使用
} else {
// this.showAdAndReward();
return false; // 没次数,去看广告
}
}
public getHintCount() {
return this._hint_count;
}
public getShuffleCount() {
return this._shuffle_count;
}
public getMagicCount() {
return this._magic_count;
}
public showAdAndReward(type) {
// ⚠️ 这里接入广告 SDK
console.log("显示广告...");
// 假设广告结束回调
if(type === 'hint') this.onAdHintComplete();
if(type === 'shuffle') this.onAdShuffleComplete();
if(type === 'magic') this.onAdMagicComplete();
}
private onAdHintComplete() {
this._hint_count += 3;
this.saveHint();
this.updateHintView();
}
private onAdShuffleComplete() {
this._shuffle_count += 3;
this.saveShuffle();
this.updateShuffleView();
}
private onAdMagicComplete() {
this._magic_count += 3;
this.saveMagic();
this.updateMagicView();
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "718c3d80-1392-4a19-9a8c-31e59d8b5627",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,232 @@
import { Node, Vec3, tween } from "cc";
export interface DragCell {
r: number;
c: number;
type: number;
node: Node;
}
export interface DragGroup {
cells: DragCell[];
dirR: number;
dirC: number;
maxSteps: number;
}
export class DragManager {
private board: number[][];
private blockNodes: (Node | null)[][];
private rows: number;
private cols: number;
private blockSize: number;
constructor(
board: number[][],
blockNodes: (Node | null)[][],
rows: number,
cols: number,
blockSize: number,
) {
this.board = board;
this.blockNodes = blockNodes;
this.rows = rows;
this.cols = cols;
this.blockSize = blockSize;
}
/** 查找拖动组(从点击格子 + 方向出发) */
findDragGroup(
r: number,
c: number,
dirR: number,
dirC: number,
): { r: number; c: number }[] {
if (this.board[r][c] < 0) return [];
const group: { r: number; c: number }[] = [];
// 包含起点
group.push({ r, c });
if (dirR === 0 && dirC === 0) {
// 无方向,返回只有自己
return group;
}
if (dirR !== 0) {
// 垂直方向,向前方向查找
// 前面方向就是 dirR 方向(正负决定上或下)
let rr = r + dirR;
while (rr >= 0 && rr < this.rows) {
if (this.board[rr][c] >= 0) {
if (dirR > 0) {
// 向下push后面
group.push({ r: rr, c });
} else {
// 向上unshift前面
group.unshift({ r: rr, c });
}
rr += dirR;
} else {
break;
}
}
} else if (dirC !== 0) {
// 水平方向,向前方向查找
// 前面方向就是 dirC 方向(正负决定左或右)
let cc = c + dirC;
while (cc >= 0 && cc < this.cols) {
if (this.board[r][cc] >= 0) {
if (dirC > 0) {
// 向右push后面
group.push({ r, c: cc });
} else {
// 向左unshift前面
group.unshift({ r, c: cc });
}
cc += dirC;
} else {
break;
}
}
}
return group;
}
/** 计算最多能移动多少格 */
countEmptyStepsGroup(
group: { r: number; c: number }[],
dirR: number,
dirC: number,
): number {
if (group.length === 0) return 0;
// 按滑动方向,找整组前方连续空格最小值
if (dirR !== 0) {
// 纵向滑动,找组中最顶部或最底部行号
const rows = group.map((g) => g.r);
const col = group[0].c;
const boundaryRow = dirR === 1 ? Math.max(...rows) : Math.min(...rows);
let count = 0;
let checkRow = boundaryRow + dirR;
while (
checkRow >= 0 &&
checkRow < this.rows &&
this.board[checkRow][col] === -1
) {
count++;
checkRow += dirR;
}
return count;
} else {
// 横向滑动,找组中最左或最右列号
const cols = group.map((g) => g.c);
const row = group[0].r;
const boundaryCol = dirC === 1 ? Math.max(...cols) : Math.min(...cols);
let count = 0;
let checkCol = boundaryCol + dirC;
while (
checkCol >= 0 &&
checkCol < this.cols &&
this.board[row][checkCol] === -1
) {
count++;
checkCol += dirC;
}
return count;
}
}
/** 执行拖动中的偏移 */
public applyDragOffset(
draggingGroup,
maxSteps: number,
dirR: number,
dirC: number,
dy: number,
dx: number,
) {
// 计算偏移量滑动方向相反时置0
let offset = dirR !== 0 ? dy : dx;
if (
(dirR === -1 && offset <= 0) ||
(dirR === 1 && offset >= 0) ||
(dirC === 1 && offset <= 0) ||
(dirC === -1 && offset >= 0)
)
offset = 0;
const maxOffset = maxSteps * this.blockSize;
if (Math.abs(offset) > maxOffset) offset = maxOffset * Math.sign(offset);
// 整组方块移动
draggingGroup.forEach(({ r, c }) => {
const block = this.blockNodes[r][c];
if (!block) return;
const startPos = this.getPos(r, c);
if (dirR !== 0) {
block.setPosition(startPos.x, startPos.y + offset, 0);
} else {
block.setPosition(startPos.x + offset, startPos.y, 0);
}
});
}
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);
}
/** 松手:确认移动 or 回弹 */
public async settleDrag(
blocksInfo: DragCell[],
reset: boolean,
steps?: number,
slidingDir?,
) {
if (reset) {
await this.resetGroup(blocksInfo);
} else {
await this.applyMove(blocksInfo, steps, slidingDir);
}
}
/** 执行动画移动 */
private async applyMove(blocksInfo: DragCell[], steps: number, slidingDir) {
blocksInfo.forEach(({ r, c, type, node }) => {
const tr = r + slidingDir.dirR * steps;
const tc = c + slidingDir.dirC * steps;
this.board[tr][tc] = type;
this.blockNodes[tr][tc] = node;
if (node) node.setPosition(this.getPos(tr, tc));
});
}
/** 回弹动画 */
private async resetGroup(blocksInfo) {
const promises = blocksInfo.map(({ r, c, type, node }) => {
const targetPos = this.getPos(r, c);
this.board[r][c] = type;
this.blockNodes[r][c] = node;
return new Promise<void>((resolve) => {
tween(node)
.to(0.15, { position: targetPos })
.call(() => resolve()) // ✅ 修复
.start();
});
});
await Promise.all(promises);
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "6426afbd-e807-450a-b364-9b404873d8b3",
"files": [],
"subMetas": {},
"userData": {}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "f5caabc4-7ab4-4966-ad71-81c18beb874c",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,75 @@
import { _decorator, Component, Node, tween, Vec3, Sprite, UITransform } from "cc";
const { ccclass, property } = _decorator;
@ccclass("HintManager")
export class HintManager extends Component {
@property(Node)
handNode: Node | null = null; // 编辑器里拖一只手的 sprite 节点
private runningTween: any = null;
// 传入 getHint 的结果
showHint(hint: any, getPos: (r: number, c: number) => Vec3) {
if (!this.handNode) return;
// 清理之前的动画
if (this.runningTween) {
this.runningTween.stop();
this.runningTween = null;
}
if (!hint.from || !hint.dir || hint.steps == null) return;
const { from, dir, steps } = hint;
const dr = dir.dr;
const dc = dir.dc;
// 生成路径格子(起点 + dir * step
const pathCells: { r: number; c: number }[] = [];
for (let s = 0; s <= steps; s++) {
pathCells.push({ r: from.r + dr * s, c: from.c + dc * s });
}
// 转换成 UI 坐标
const positions: Vec3[] = pathCells.map(cell => getPos(cell.r, cell.c));
const startPos = positions[0];
this.handNode.active = true;
this.handNode.setSiblingIndex(this.handNode.parent.children.length - 1);
this.handNode.setPosition(startPos);
// 构建单向滑动动画(格子之间平均时间)
const durationPerCell = 1 / steps;
let tw = tween(this.handNode);
for (let i = 1; i < positions.length; i++) {
tw = tw.to(durationPerCell, { position: positions[i] });
}
// 到终点瞬间回起点
tw = tw.call(() => this.handNode.setPosition(startPos));
// 重复 3 次
this.runningTween = tween(this.handNode)
.repeat(3, tw)
.call(() => {
this.handNode.active = false;
this.runningTween = null;
})
.start();
}
cancelHint() {
if (this.runningTween) {
try {
this.runningTween.stop();
} catch (e) {
console.warn("停止手势动画失败", e);
}
this.runningTween = null;
}
if (this.handNode) {
this.handNode.active = false;
}
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "1fafa285-6dac-4a29-8a97-fbb3d3389996",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,267 @@
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
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "b69764f9-e98c-4d95-afa7-0b2649089cb5",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,131 @@
import { sys } from "cc";
import { LevelConfig } from "./LevelBuilder";
import { BoardShapeName } from "./BoardShape";
export class LevelManager {
private static _instance: LevelManager;
public static get instance() {
if (!this._instance) this._instance = new LevelManager();
return this._instance;
}
private levelIndex: number = 0;
private unlockedLevels: number = 1; // 已解锁关卡数
private STORAGE_KEY = "MY_GAME_PROGRESS";
private TUTORIAL_KEY = "TUTORIAL_COMPLETED";
// 预设关卡配置表(可以继续扩展) //rectangle L_shape T_shape cross_shape
private levels: LevelConfig[] = [
{ currentShape: "rectangle", blockSize: 100, tileTypes: 5 },
{ currentShape: "L_shape", blockSize: 90, tileTypes: 4 },
{ currentShape: "T_shape", blockSize: 80, tileTypes: 3 },
{ currentShape: "cross_shape", blockSize: 70, tileTypes: 3 },
{ currentShape: "diamond", blockSize: 80, tileTypes: 4 },
];
constructor() {
this.loadProgress();
}
public isTutorialCompleted(): boolean {
return sys.localStorage.getItem(this.TUTORIAL_KEY) === "true";
}
public setTutorialCompleted() {
sys.localStorage.setItem(this.TUTORIAL_KEY, "true");
}
public getTutorialLevel(): LevelConfig {
return {
isTutorial: true,
tileTypes: 2,
blockSize: 120,
fixedBoard: [
[0, 0, -1],
[1, -1, 1]
]
};
}
public getCurrentLevel(): LevelConfig {
if (!this.isTutorialCompleted()) {
return this.getTutorialLevel();
}
if (this.levelIndex < this.levels.length) {
return this.levels[this.levelIndex];
}
console.log('随机关卡')
return this.generateRandomLevel(this.levelIndex + 1);
}
public nextLevel(): LevelConfig | null {
if (!this.isTutorialCompleted()) {
this.setTutorialCompleted();
this.levelIndex = 0;
this.saveProgress();
return this.getCurrentLevel();
}
this.levelIndex++;
this.saveProgress();
return this.getCurrentLevel();
}
public reset() {
this.levelIndex = 0;
this.unlockedLevels = 1;
this.saveProgress();
// 重置时是否重置教学?通常不用,除非显式清除数据
// sys.localStorage.removeItem(this.TUTORIAL_KEY);
}
/** 本地存储进度 */
private saveProgress() {
const data = {
levelIndex: this.levelIndex,
unlockedLevels: this.unlockedLevels,
};
sys.localStorage.setItem(this.STORAGE_KEY, JSON.stringify(data));
}
/** 读取本地存储 */
private loadProgress() {
const json = sys.localStorage.getItem(this.STORAGE_KEY);
if (json) {
try {
const data = JSON.parse(json);
this.levelIndex = data.levelIndex ?? 0;
this.unlockedLevels = data.unlockedLevels ?? 1;
// 如果是老玩家已解锁超过1关跳过教学
if (this.unlockedLevels > 1 && !this.isTutorialCompleted()) {
this.setTutorialCompleted();
}
} catch (e) {
console.warn("进度解析失败, 重置进度");
this.reset();
}
}
}
private generateRandomLevel(id: number): LevelConfig {
const allShapes: BoardShapeName[] = ["rectangle", "L_shape", "T_shape", "cross_shape", "diamond"];
// 随机选一个形状
const shape = allShapes[Math.floor(Math.random() * allShapes.length)];
// 随机决定方块种类 (6~12)
const tileTypes = 5;
// const tileTypes = 6 + Math.floor(Math.random() * 7);
// 随机大小 (70~100)
const blockSize = 70 + Math.floor(Math.random() * 31);
return {
currentShape: shape,
tileTypes,
blockSize,
};
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "f6594edb-0e1e-41bc-b818-0c9b51a816ff",
"files": [],
"subMetas": {},
"userData": {}
}

64
assets/scripts/Loading.ts Normal file
View File

@@ -0,0 +1,64 @@
import { _decorator, Component, ProgressBar, Label, assetManager, director, SpriteFrame, Prefab } from 'cc';
import { ResourceManager } from './ResourceManager';
const { ccclass, property } = _decorator;
@ccclass('Loading')
export class Loading extends Component {
@property(ProgressBar)
progressBar: ProgressBar | null = null;
@property(Label)
progressLabel: Label | null = null;
@property
nextScene: string = "StartScene";
onLoad() {
// if (typeof wx !== 'undefined' && wx.hideLoading) {
// wx.hideLoading();
// }
this.startLoading();
}
startLoading() {
assetManager.loadBundle("resources", (err, bundle) => {
if (err) {
console.error("加载 resources 失败:", err);
return;
}
console.log(11111111111,2222)
// 加载整个 resources 下的内容
bundle.loadDir(".", (finished, total, item) => {
let ratio = finished / total;
console.log(11111111111,ratio)
this.updateProgress(ratio);
}, (err, assets) => {
if (err) {
console.error("资源加载失败:", err);
return;
}
// 缓存资源
assets.forEach(asset => {
ResourceManager.instance.set(asset.name, asset);
asset.addRef(); // 增加引用计数,避免被自动释放
});
// 进入主场景
director.loadScene(this.nextScene);
});
});
}
updateProgress(ratio: number) {
console.log(111111, ratio)
if (this.progressBar) {
this.progressBar.progress = ratio;
}
if (this.progressLabel) {
this.progressLabel.string = `${Math.floor(ratio * 100)}%`;
}
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "5eac7988-bb99-4c65-a76c-37013eb0eac7",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,45 @@
import { _decorator, Asset } from 'cc';
const { ccclass } = _decorator;
@ccclass('ResourceManager')
export class ResourceManager {
private static _instance: ResourceManager;
private cache: Map<string, Asset> = new Map();
static get instance(): ResourceManager {
if (!this._instance) {
this._instance = new ResourceManager();
}
return this._instance;
}
/** 存资源 */
set(key: string, asset: Asset) {
this.cache.set(key, asset);
}
/** 取资源 */
get<T extends Asset>(key: string): T | undefined {
return this.cache.get(key) as T | undefined;
}
/** 是否存在 */
has(key: string): boolean {
return this.cache.has(key);
}
/** 释放某个资源 */
release(key: string) {
const asset = this.cache.get(key);
if (asset) {
asset.decRef(); // 减少引用计数
this.cache.delete(key);
}
}
/** 释放全部资源 */
releaseAll() {
this.cache.forEach(asset => asset.decRef());
this.cache.clear();
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "57226c7a-7652-40f6-a947-d9fc032231b0",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,160 @@
import {
_decorator,
Component,
Node,
tween,
UIOpacity,
Vec3,
director,
Button,
AudioClip,
AudioSource,
Toggle,
} from "cc";
import { AudioManager, BGMType, SFXType } from "./AudioManager";
const { ccclass, property } = _decorator;
@ccclass("StartScene")
export class StartScene extends Component {
@property(Node)
background: Node = null;
@property(Node)
logo: Node = null;
@property(Node)
startButton: Node = null;
@property(Node)
setting_node: Node = null;
@property(Node)
setting_tab_node: Node = null;
@property(AudioClip)
bg_audio: AudioClip = null;
@property(Toggle)
bgmToggle: Toggle = null!;
@property(Toggle)
sfxToggle: Toggle = null!;
onLoad() {
const am = AudioManager.instance;
// 注册首页 BGM 和音效
am.registerBGM(BGMType.Home, this.bg_audio);
// 初始化 Toggle 状态
this.bgmToggle.isChecked = am.bgmEnabled;
this.sfxToggle.isChecked = am.sfxEnabled;
// am.registerSFX(SFXType.Item, this.startClickClip);
am.setCurBgm(BGMType.Home);
am.playBGM(BGMType.Home);
// 监听切换事件
this.bgmToggle.node.on("toggle", this.onBgmToggleChanged, this);
this.sfxToggle.node.on("toggle", this.onSfxToggleChanged, this);
}
start() {
// 初始化透明度
const bgOpacity = this.background.addComponent(UIOpacity);
bgOpacity.opacity = 0;
const logoOpacity = this.logo.addComponent(UIOpacity);
logoOpacity.opacity = 0;
this.logo.setScale(new Vec3(0.5, 0.5, 1));
const btnOpacity = this.startButton.addComponent(UIOpacity);
btnOpacity.opacity = 0;
// 背景渐入
tween(bgOpacity).to(1, { opacity: 255 }).start();
// Logo 缩放 + 渐入
tween(logoOpacity).delay(0.5).to(1, { opacity: 255 }).start();
tween(this.logo)
.delay(0.5)
.to(1, { scale: new Vec3(1.1, 1.1, 1) })
.to(0.3, { scale: new Vec3(1, 1, 1) })
.start();
// 按钮呼吸动画
tween(btnOpacity)
.delay(1.2)
.to(0.8, { opacity: 255 })
.call(() => {
this.playButtonBreathing();
})
.start();
// 按钮点击事件
// this.startButton.on(Button.EventType.CLICK, this.onStartGame, this);
}
onBgmToggleChanged(toggle: Toggle) {
AudioManager.instance.setBGMEnabled(toggle.isChecked);
}
onSfxToggleChanged(toggle: Toggle) {
AudioManager.instance.setSFXEnabled(toggle.isChecked);
}
playButtonBreathing() {
tween(this.startButton)
.repeatForever(
tween()
.to(0.6, { scale: new Vec3(1.1, 1.1, 1) })
.to(0.6, { scale: new Vec3(1, 1, 1) })
)
.start();
}
onStartGame() {
// 按钮点击反馈动画
tween(this.startButton)
.to(0.1, { scale: new Vec3(0.9, 0.9, 1) })
.to(0.1, { scale: new Vec3(1, 1, 1) })
.call(() => {
director.loadScene("s1"); // 这里替换为你的第一关场景名
})
.start();
}
onShowSettingTab() {
this.setting_tab_node.active = true;
}
onHideSettingTab() {
this.setting_tab_node.active = false;
}
onSwitchMusic(toggle) {
// this.audioMgr.bgmSource = this.node.getComponent(AudioSource);
if (toggle.isChecked) {
// this.audioMgr.setBGMEnabled(true);
// this.audioMgr.playBGM(this.bg_audio);
// this.playBgAudio();
} else {
// this.audioMgr.setBGMEnabled(false);
// this.notPlayBgAudio();
// this.audioMgr.playBGM(this.bg_audio);
}
}
playBgAudio() {
const Audio = this.node.getComponent(AudioSource);
Audio.clip = this.bg_audio;
Audio.play();
}
notPlayBgAudio() {
const Audio = this.node.getComponent(AudioSource);
Audio.clip = this.bg_audio;
Audio.stop();
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "0c27000f-a20c-4b07-9ac1-5c05df71c90e",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "c85a2ba2-7cde-4fdc-ae16-3f2718db5c2c",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,35 @@
// ScreenAdapter.ts
import { _decorator, Component, view, ResolutionPolicy } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('ScreenAdapter')
export class ScreenAdapter extends Component {
@property
designWidth: number = 1080; // 默认设计宽度
@property
designHeight: number = 1920; // 默认设计高度
onLoad() {
this.applyAdapter();
}
/**
* 应用屏幕适配
*/
applyAdapter() {
// const frameSize = view.getFrameSize();
// const aspect = frameSize.width / frameSize.height;
// const designAspect = this.designWidth / this.designHeight;
// if (aspect > designAspect) {
// // 屏幕更宽 → 固定高度,保证上下完整
// view.setDesignResolutionSize(this.designWidth, this.designHeight, ResolutionPolicy.FIXED_HEIGHT);
// console.log('[ScreenAdapter] Use FIXED_HEIGHT, aspect =', aspect.toFixed(2));
// } else {
// // 屏幕更窄 → 固定宽度,保证左右完整
// view.setDesignResolutionSize(this.designWidth, this.designHeight, ResolutionPolicy.FIXED_WIDTH);
// console.log('[ScreenAdapter] Use FIXED_WIDTH, aspect =', aspect.toFixed(2));
// }
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "043cfab9-7936-40a6-ab63-145e9193c71a",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -0,0 +1,13 @@
import { resources, SpriteFrame } from "cc";
export const loadSpriteFrames = (dir: string): Promise<SpriteFrame[]> => {
return new Promise((resolve, reject) => {
resources.loadDir(dir, SpriteFrame, (err, frames) => {
if (err) {
reject(err);
return;
}
resolve(frames);
});
});
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "a2baa6eb-8db8-4998-9719-ae146445a3e2",
"files": [],
"subMetas": {},
"userData": {}
}