first commit
This commit is contained in:
139
assets/scripts/AudioManager.ts
Normal file
139
assets/scripts/AudioManager.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
9
assets/scripts/AudioManager.ts.meta
Normal file
9
assets/scripts/AudioManager.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "b07ed8a6-f1ba-46a1-8edd-9b424894ec9f",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
48
assets/scripts/AudioSettingsUI.ts
Normal file
48
assets/scripts/AudioSettingsUI.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
9
assets/scripts/AudioSettingsUI.ts.meta
Normal file
9
assets/scripts/AudioSettingsUI.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "e6a5f56b-bc7d-48ad-82cf-23ab8d7da791",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
50
assets/scripts/BoardShape.ts
Normal file
50
assets/scripts/BoardShape.ts
Normal 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],
|
||||
]
|
||||
};
|
||||
9
assets/scripts/BoardShape.ts.meta
Normal file
9
assets/scripts/BoardShape.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "5ff0aad1-47ce-41a6-9dfd-25e0d722b769",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
195
assets/scripts/ButtonBadge.ts
Normal file
195
assets/scripts/ButtonBadge.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
9
assets/scripts/ButtonBadge.ts.meta
Normal file
9
assets/scripts/ButtonBadge.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "718c3d80-1392-4a19-9a8c-31e59d8b5627",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
232
assets/scripts/DragManager.ts
Normal file
232
assets/scripts/DragManager.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
9
assets/scripts/DragManager.ts.meta
Normal file
9
assets/scripts/DragManager.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "6426afbd-e807-450a-b364-9b404873d8b3",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
1386
assets/scripts/GameManager.ts
Normal file
1386
assets/scripts/GameManager.ts
Normal file
File diff suppressed because it is too large
Load Diff
9
assets/scripts/GameManager.ts.meta
Normal file
9
assets/scripts/GameManager.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "f5caabc4-7ab4-4966-ad71-81c18beb874c",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
75
assets/scripts/HintManager.ts
Normal file
75
assets/scripts/HintManager.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
9
assets/scripts/HintManager.ts.meta
Normal file
9
assets/scripts/HintManager.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "1fafa285-6dac-4a29-8a97-fbb3d3389996",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
267
assets/scripts/LevelBuilder.ts
Normal file
267
assets/scripts/LevelBuilder.ts
Normal 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
|
||||
}
|
||||
}
|
||||
9
assets/scripts/LevelBuilder.ts.meta
Normal file
9
assets/scripts/LevelBuilder.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "b69764f9-e98c-4d95-afa7-0b2649089cb5",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
131
assets/scripts/LevelManager.ts
Normal file
131
assets/scripts/LevelManager.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
9
assets/scripts/LevelManager.ts.meta
Normal file
9
assets/scripts/LevelManager.ts.meta
Normal 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
64
assets/scripts/Loading.ts
Normal 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)}%`;
|
||||
}
|
||||
}
|
||||
}
|
||||
9
assets/scripts/Loading.ts.meta
Normal file
9
assets/scripts/Loading.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "5eac7988-bb99-4c65-a76c-37013eb0eac7",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
45
assets/scripts/ResourceManager.ts
Normal file
45
assets/scripts/ResourceManager.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
9
assets/scripts/ResourceManager.ts.meta
Normal file
9
assets/scripts/ResourceManager.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "57226c7a-7652-40f6-a947-d9fc032231b0",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
160
assets/scripts/StartScene.ts
Normal file
160
assets/scripts/StartScene.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
9
assets/scripts/StartScene.ts.meta
Normal file
9
assets/scripts/StartScene.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "0c27000f-a20c-4b07-9ac1-5c05df71c90e",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
9
assets/scripts/utils.meta
Normal file
9
assets/scripts/utils.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "c85a2ba2-7cde-4fdc-ae16-3f2718db5c2c",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
35
assets/scripts/utils/ScreenAdapter.ts
Normal file
35
assets/scripts/utils/ScreenAdapter.ts
Normal 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));
|
||||
// }
|
||||
}
|
||||
}
|
||||
9
assets/scripts/utils/ScreenAdapter.ts.meta
Normal file
9
assets/scripts/utils/ScreenAdapter.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "043cfab9-7936-40a6-ab63-145e9193c71a",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
13
assets/scripts/utils/loadResources.ts
Normal file
13
assets/scripts/utils/loadResources.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
9
assets/scripts/utils/loadResources.ts.meta
Normal file
9
assets/scripts/utils/loadResources.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "a2baa6eb-8db8-4998-9719-ae146445a3e2",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
||||
Reference in New Issue
Block a user