From e9e6ddb8a7e71ae166b81d1867f48c718c3fb2a2 Mon Sep 17 00:00:00 2001 From: leiyuwei <249304969@qq.com> Date: Tue, 16 Jun 2026 12:12:39 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84:=20=E5=90=88=E6=88=90?= =?UTF-8?q?=E5=A4=A7=E4=BD=9C=E6=88=98=E6=94=B9=E4=B8=BA=E6=8B=96=E6=8B=BD?= =?UTF-8?q?=E5=90=88=E6=88=90=E6=9C=BA=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 全部代码重写,改为拖拽合成(drag-and-drop merge) - 拖拽方块到相邻同等级方块上完成合成 - 拖到空格移动,拖到无效位置弹回 - Cocos Creator 3.8.8 全面适配 - 移除 GameManager.ts → 改为 GameController.ts Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 6 +- TUTORIAL.md | 345 ------------------------ assets/Scripts/Block.ts | 190 +++++++------ assets/Scripts/BlockConfig.ts | 68 +++-- assets/Scripts/GameBoard.ts | 383 +++++++++++++------------- assets/Scripts/GameController.ts | 283 +++++++++++++++++++ assets/Scripts/GameManager.ts | 290 -------------------- index.html | 448 ------------------------------- package.json | 8 +- settings/v2/packages.json | 3 +- tsconfig.json | 4 +- 11 files changed, 594 insertions(+), 1434 deletions(-) delete mode 100644 TUTORIAL.md create mode 100644 assets/Scripts/GameController.ts delete mode 100644 assets/Scripts/GameManager.ts delete mode 100644 index.html diff --git a/.gitignore b/.gitignore index 50136ca..78a1670 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,10 @@ -# Cocos Creator project +# Cocos Creator .DS_Store -*.local.json library/ local/ temp/ build/ node_modules/ *.log - -# Claude Code +*.local.json .claude/ diff --git a/TUTORIAL.md b/TUTORIAL.md deleted file mode 100644 index 2652869..0000000 --- a/TUTORIAL.md +++ /dev/null @@ -1,345 +0,0 @@ -# 合成大作战 - Cocos Creator 3.8.8 教程 - -## 目录 - -1. [环境准备](#1-环境准备) -2. [打开项目](#2-打开项目) -3. [创建场景(首次必需)](#3-创建场景首次必需) -4. [运行游戏](#4-运行游戏) -5. [游戏玩法说明](#5-游戏玩法说明) -6. [项目结构](#6-项目结构) -7. [代码开发流程](#7-代码开发流程) -8. [发布游戏](#8-发布游戏) -9. [常见问题](#9-常见问题) - ---- - -## 1. 环境准备 - -### 下载安装 Cocos Creator 3.8.8 - -从 [Cocos 官网](https://www.cocos.com/creator-download) 下载 **Cocos Creator 3.8.8** 版本并安装。 - -> 注意:需要 3.8.x 系列版本,推荐 3.8.8。 - -安装完成后,启动 Cocos Creator,你会看到 Dashboard(仪表盘)界面。 - ---- - -## 2. 打开项目 - -### 方式一:通过 Dashboard - -1. 打开 Cocos Creator -2. 在 Dashboard 中点击 **打开其他项目** -3. 浏览到本项目的根目录 `G:\code\cocos-hehe` -4. 点击 **打开** - -### 方式二:通过菜单 - -1. 在 Cocos Creator 中点击 **项目 → 打开项目**(或快捷键 Ctrl+O) -2. 选择本项目的根目录 -3. 点击打开 - -### 等待导入 - -首次打开时,编辑器会自动: -- 扫描 `assets/` 目录下的所有资源 -- 为每个资源生成 `.meta` 文件(UUID 映射) -- 编译 TypeScript 脚本 -- 这个过程可能需要 10-30 秒,请耐心等待 - -导入完成后,你会在 **资源管理器** (Assets) 面板中看到以下结构: - -``` -assets -└── Scripts - ├── BlockConfig.ts # 方块配置(颜色、等级、尺寸) - ├── Block.ts # 方块组件(渲染、动画) - ├── GameBoard.ts # 棋盘组件(网格、合并逻辑) - └── GameManager.ts # 主控制器(UI、输入、游戏流程) -``` - ---- - -## 3. 创建场景(首次必需) - -项目代码中**不包含场景文件**(因为场景格式是版本特定的),你需要手动创建一个: - -### 步骤 - -1. 在 **资源管理器** 中右键点击 `assets` 文件夹 -2. 选择 **创建 → Scene**(或 Ctrl+N) -3. 将场景命名为 `Game`(双击可重命名) -4. 双击打开场景 `Game` - -### 设置场景 - -1. 在 **层级管理器** (Hierarchy) 中,你会看到一个 `Scene` 节点 -2. 在 `Scene` 节点上右键,选择 **创建 → UI 组件 → Canvas** - - 此时会创建一个 `Canvas` 节点(包含 Canvas 和 UITransform 组件) -3. 点击选中 `Canvas` 节点 -4. 在 **属性检查器** (Inspector) 中找到 Canvas 组件 -5. 将 **设计分辨率 (Design Resolution)** 设为: - - **Width**: 580 - - **Height**: 960 -6. **Fit Height**: 勾选 ✅ - -### 附加脚本 - -1. 在 **资源管理器** 中找到 `Scripts/GameManager.ts` -2. 拖拽到 `Canvas` 节点上(或者在 Canvas 的 Inspector 中点击 **添加组件 → 自定义脚本 → GameManager**) -3. 确认 GameManager 组件已出现在 Canvas 的 Inspector 中 - -### 保存场景 - -按 **Ctrl+S** 保存场景。 - ---- - -## 4. 运行游戏 - -### 在编辑器中预览 - -点击编辑器顶部的 **播放 ▶** 按钮(或按 Ctrl+P) - -游戏将在 **Game 预览窗口** 中启动。 - -### 模拟手机设备 - -1. 在 Game 窗口顶部的下拉菜单中,选择一个移动设备分辨率 - - 推荐: **iPhone 6/7/8 (375×667)** 或 **Galaxy S5 (360×640)** -2. 点击播放按钮 - -### 浏览器中预览 - -编辑器预览默认使用内置浏览器窗口。你也可以: -1. 点击 **播放 → 在浏览器中预览** -2. 选择 Chrome 或其他浏览器 - ---- - -## 5. 游戏玩法说明 - -### 核心规则 - -| 项目 | 说明 | -|------|------| -| 棋盘 | 8列 × 12行 | -| 方块等级 | 1~8级,共8种颜色 | -| 合成规则 | 两个**相邻同等级**方块合成,等级+1 | -| 方块生成 | 每3秒在随机空格生成一个新方块 | -| 游戏结束 | 棋盘满时游戏结束 | - -### 等级与颜色对照 - -| 等级 | 颜色 | 名称 | -|:----:|:----:|:----:| -| 1 | 浅绿 `#A8E6CF` | 草 | -| 2 | 深绿 `#4CAF50` | 木 | -| 3 | 蓝色 `#2196F3` | 水 | -| 4 | 紫色 `#9C27B0` | 风 | -| 5 | 橙色 `#FF9800` | 火 | -| 6 | 红色 `#F44336` | 雷 | -| 7 | 粉色 `#E91E63` | 光 | -| 8 | 金色 `#FFD700` | 神 | - -### 操作方法 - -| 操作 | 说明 | -|------|------| -| **点击方块** | 选中方块(白色高亮边框) | -| **点击相邻同等级方块** | 合成升级 | -| **点击空白格** | 移动选中方块到该位置 | -| **点击其他方块** | 切换选中目标 | - -### 计分规则 - -- 合成出 **Lv.N** 的方块 = N × 10 分 -- 例如:合成出 Lv.5(火)= 50 分 -- 链式合成:一次移动可能触发多次合成,分数累加 - -### 策略技巧 - -1. **优先合成高等级方块**:高等级方块得分更高 -2. **清理低级方块**:低级太多会快速填满棋盘 -3. **规划移动路径**:利用空格作为临时存放区 -4. **链式合成**:一次移动触发多次合并是关键得分手段 - ---- - -## 6. 项目结构 - -``` -cocos-hehe/ -├── assets/ # 游戏资源目录(编辑器主要工作区) -│ └── Scripts/ -│ ├── BlockConfig.ts # 方块颜色与等级配置 -│ ├── Block.ts # 方块可视化组件 -│ ├── GameBoard.ts # 棋盘网格管理 -│ └── GameManager.ts # 游戏主控制器 -├── settings/ # 编辑器设置(自动生成) -├── index.html # 独立可玩 HTML 版(无编辑器也可运行) -├── package.json # 项目描述文件 -├── tsconfig.json # TypeScript 编译配置 -└── TUTORIAL.md # 本教程 -``` - -### 脚本职责 - -| 文件 | 主要职责 | 关键方法 | -|------|---------|---------| -| `BlockConfig.ts` | 配置常量、工具函数 | `getBlockConfig()`, `getRandomSpawnLevel()` | -| `Block.ts` | 方块外观、动画 | `init()`, `upgradeTo()`, `setSelected()` | -| `GameBoard.ts` | 网格管理、合并逻辑 | `spawnRandom()`, `tryMerge()`, `triggerMergeCheck()` | -| `GameManager.ts` | 游戏流程、UI、输入 | `onTouchEnd()`, `startGame()`, `gameOver()` | - -### 数据流 - -``` -GameManager (主控) - ├── GameBoard (网格 + 合并) - │ └── Block[] (方块实例) - ├── Label (UI) - └── Node (GameOver 遮罩) -``` - ---- - -## 7. 代码开发流程 - -### 修改代码后 - -1. 保存 `.ts` 文件 -2. **Ctrl+S** 保存 -3. Cocos Creator 会自动检测并重新编译 -4. 按 **Ctrl+P** 重新预览 - -> 如果编译报错,请在 **控制台** (Console) 面板查看错误信息 - -### 添加新功能示例:增加方块品类 - -在 `BlockConfig.ts` 的 `BLOCK_CONFIGS` 数组中添加: - -```typescript -{ level: 9, color: '#00BCD4', name: '龙' }, -{ level: 10, color: '#FF4081', name: '圣' }, -``` - -同时修改 `MERGE_MAX_LEVEL` 逻辑(如果有上限校验)。 - -### 调整游戏难度 - -在 `BlockConfig.ts` 中修改: - -```typescript -export const SPAWN_INTERVAL = 3.0; // 改成 2.0 更快,4.0 更慢 -``` - -修改 `getRandomSpawnLevel()` 中的概率分布: - -```typescript -export function getRandomSpawnLevel(): number { - const roll = Math.random(); - if (roll < 0.30) return 1; // Lv1 概率降低 - if (roll < 0.60) return 2; // Lv2 增加 - if (roll < 0.80) return 3; - if (roll < 0.93) return 4; - return 5; -} -``` - ---- - -## 8. 发布游戏 - -### 发布为 Web 版(HTML5) - -1. 点击 **项目 → 构建发布** -2. 平台选择 **Web 移动端** 或 **Web 桌面端** -3. 设置输出路径(如 `build/web-mobile`) -4. 点击 **构建** -5. 构建完成后,打开 `build/web-mobile/index.html` 即可游玩 - -### 发布为 Android 版本 - -1. 安装 Android Studio 并配置 NDK/SDK -2. 在 Cocos Creator 中点击 **项目 → 构建发布** -3. 平台选择 **Android** -4. 配置包名(如 `com.lywdev.mergegame`) -5. 点击 **构建 → 编译** -6. 生成的 APK 在 `build/android/` 目录 - -### 发布为 iOS 版本 - -1. 需要 macOS 和 Xcode -2. 构建时选择 **iOS** 平台 -3. 用 Xcode 打开生成的项目 -4. 配置签名后发布到 App Store - -### 发布为微信小游戏 - -1. 安装微信开发者工具 -2. 在 Cocos Creator 中 **项目 → 构建发布 → 微信小游戏** -3. 填入 AppID -4. 构建后,用微信开发者工具打开生成目录 -5. 上传代码 - ---- - -## 9. 常见问题 - -### Q: 编辑器报 "Cannot find module 'cc'" - -**原因**: TypeScript 语言服务未正确配置。 - -**解决**: -1. 关闭 Cocos Creator -2. 删除项目根目录的 `temp/` 和 `library/` 文件夹 -3. 重新打开 Cocos Creator - -### Q: 方块显示为白色方块 - -**原因**: Sprite 组件缺少纹理(spriteFrame)。 - -**解决**: Cocos Creator 3.8+ 中 `Sprite` 不设置 `spriteFrame` 会使用内置白色纹理。如果没有自动显示颜色,可以在 `Block.ts` 的 `createVisuals()` 中添加: - -```typescript -import { SpriteFrame, Texture2D } from 'cc'; -// ... -const sprite = bgNode.addComponent(Sprite); -sprite.type = Sprite.Type.SIMPLE; -sprite.color = ...; // 你的颜色 -``` - -### Q: 预览时点击无效 - -**原因**: 场景中可能缺少 UITransform 组件。 - -**解决**: -1. 选中 Canvas 节点 -2. 检查 Inspector 中是否有 **UITransform** 组件 -3. 如果没有,点击 **添加组件 → UI → UITransform** - -### Q: 背景颜色不显示 - -**原因**: Sprite 没有纹理时某些版本不渲染。 - -**解决**: 确保所有 Sprite 组件都正确设置了 `Type = SIMPLE` 并赋予了 `color` 值。 - -### Q: 如何导出并在手机上测试? - -1. 点击 **项目 → 构建发布** -2. 选择 **Web 移动端** -3. 构建后,将 `build/web-mobile` 目录部署到本地服务器(如用 `npm serve` 或 `python -m http.server`) -4. 在同一 WiFi 下的手机浏览器中访问 - -### Q: 编辑器中显示 "Failed to load scene" - -**原因**: 场景文件损坏或版本不兼容。 - -**解决**: 删除旧的场景文件(在 `assets/` 中),按 [第3节](#3-创建场景首次必需) 重新创建场景。 - ---- - -> 如有其他问题,请参考 [Cocos Creator 官方文档](https://docs.cocos.com/creator/3.8/manual/zh/) diff --git a/assets/Scripts/Block.ts b/assets/Scripts/Block.ts index d8c64a2..26e9474 100644 --- a/assets/Scripts/Block.ts +++ b/assets/Scripts/Block.ts @@ -1,120 +1,116 @@ /** - * 方块组件 - 渲染方块外观、处理选中/升级动画 - * Cocos Creator 3.8.8 + * 方块组件 — 视觉、动画、拖拽状态 */ import { _decorator, Component, Node, Sprite, Color, UITransform, Label, tween, v3 } from 'cc'; -import { getBlockConfig, CELL_SIZE } from './BlockConfig'; +import { CELL, conf } from './BlockConfig'; -const { ccclass, property } = _decorator; +const { ccclass } = _decorator; @ccclass('Block') export class Block extends Component { - // ---- 公开属性 ---- - public row: number = -1; - public col: number = -1; - public level: number = 1; - public isMerging: boolean = false; + // ---- 网格位置 ---- + public row = -1; + public col = -1; + public level = 1; + public isMerging = false; - // ---- 内部引用 ---- - private _bgSprite: Sprite | null = null; - private _levelLabel: Label | null = null; + // ---- 内部 ---- + private _sprite!: Sprite; + private _label!: Label; - // ============================================================ - // 生命周期 - // ============================================================ + // ============================================================ + // 生命周期 + // ============================================================ - /** 组件添加到节点时调用,创建视觉元素(此时 level=1 默认值) */ - onLoad() { - this.createVisuals(); - } + onLoad() { + this.node.addComponent(UITransform).setContentSize(CELL, CELL); - // ============================================================ - // 公共方法 - // ============================================================ + // 方块底色 + const bg = new Node('bg'); + this._sprite = bg.addComponent(Sprite); + this._sprite.type = Sprite.Type.SIMPLE; + bg.addComponent(UITransform).setContentSize(CELL - 4, CELL - 4); + this.node.addChild(bg); - /** 初始化方块的行列和等级 */ - public init(row: number, col: number, level: number) { - this.row = row; - this.col = col; - this.level = level; - this.isMerging = false; - this.refreshAllVisuals(); - } + // 等级文字 + const lb = new Node('lb'); + this._label = lb.addComponent(Label); + this._label.fontSize = 24; + this._label.lineHeight = 28; + this._label.color = Color.WHITE.clone(); + lb.addComponent(UITransform).setContentSize(CELL - 4, CELL - 4); + this.node.addChild(lb); - /** 设置/清除选中高亮 */ - public setSelected(selected: boolean) { - if (!this._bgSprite) return; - const cfg = getBlockConfig(this.level); - const c = this.hexToColor(cfg.color); - if (selected) c.a = 160; // 选中时半透明 - this._bgSprite.color = c; - } + this.applyVisuals(); + } - /** 升级到新等级(合并后调用) */ - public upgradeTo(newLevel: number) { - this.level = newLevel; - this.isMerging = false; - this.refreshAllVisuals(); + // ============================================================ + // 公开方法 + // ============================================================ - // 弹性放大动画 - this.node.setScale(v3(1.3, 1.3, 1)); - tween(this.node) - .to(0.3, { scale: v3(1, 1, 1) }, { easing: 'elasticOut' }) - .start(); - } + /** 设置网格位置和等级 */ + init(row: number, col: number, level: number) { + this.row = row; + this.col = col; + this.level = level; + this.isMerging = false; + this.applyVisuals(); + this.playSpawnAnim(); + } - // ============================================================ - // 内部方法 - // ============================================================ + /** 合并升级 */ + upgradeTo(lv: number) { + this.level = lv; + this.isMerging = false; + this.applyVisuals(); + this.playMergeAnim(); + } - /** 初始化时创建一次视觉元素 */ - private createVisuals() { - // 方块底色 - const bgNode = new Node('Bg'); - this._bgSprite = bgNode.addComponent(Sprite); - this._bgSprite.type = Sprite.Type.SIMPLE; - bgNode.addComponent(UITransform).setContentSize(CELL_SIZE - 4, CELL_SIZE - 4); - this.node.addChild(bgNode); + /** 高亮(拖拽选中) */ + setHighlight(on: boolean) { + const c = this._color(); + if (on) c.a = 160; + this._sprite.color = c; + } - // 等级文字 - const labelNode = new Node('LvLabel'); - this._levelLabel = labelNode.addComponent(Label); - this._levelLabel.fontSize = 24; - this._levelLabel.lineHeight = 28; - this._levelLabel.color = Color.WHITE.clone(); - labelNode.addComponent(UITransform).setContentSize(CELL_SIZE - 4, CELL_SIZE - 4); - this.node.addChild(labelNode); + // ============================================================ + // 动画 + // ============================================================ - // 节点碰撞尺寸 - this.node.addComponent(UITransform).setContentSize(CELL_SIZE, CELL_SIZE); + private playSpawnAnim() { + this.node.setScale(v3(0, 0, 1)); + tween(this.node).to(0.2, { scale: v3(1, 1, 1) }, { easing: 'backOut' }).start(); + } - // 入场缩放动画 - this.node.setScale(v3(0, 0, 1)); - tween(this.node) - .to(0.2, { scale: v3(1, 1, 1) }, { easing: 'backOut' }) - .start(); - } + private playMergeAnim() { + this.node.setScale(v3(1.4, 1.4, 1)); + tween(this.node).to(0.35, { scale: v3(1, 1, 1) }, { easing: 'elasticOut' }).start(); + } - /** 根据当前等级刷新所有视觉 */ - private refreshAllVisuals() { - const cfg = getBlockConfig(this.level); - if (this._bgSprite) { - this._bgSprite.color = this.hexToColor(cfg.color); - } - if (this._levelLabel) { - this._levelLabel.string = `${this.level}`; - // 高等级文字稍大 - this._levelLabel.fontSize = 24 + Math.min(this.level * 2, 10); - } - } + /** 弹回动画(拖拽到无效位置) */ + playSnapBack(from: { x: number; y: number }, to: { x: number; y: number }) { + this.node.setPosition(from.x, from.y, 0); + tween(this.node).to(0.18, { position: v3(to.x, to.y, 0) }, { easing: 'sineOut' }).start(); + } - /** 十六进制字符串 → Color */ - private hexToColor(hex: string): Color { - return new Color( - parseInt(hex.slice(1, 3), 16), - parseInt(hex.slice(3, 5), 16), - parseInt(hex.slice(5, 7), 16), - 255 - ); - } + // ============================================================ + // 内部 + // ============================================================ + + private applyVisuals() { + const c = this._color(); + this._sprite.color = c; + this._label.string = `${this.level}`; + this._label.fontSize = 24 + Math.min(this.level * 2, 10); + } + + private _color(): Color { + const hex = conf(this.level).color; + return new Color( + parseInt(hex.slice(1, 3), 16), + parseInt(hex.slice(3, 5), 16), + parseInt(hex.slice(5, 7), 16), + 255, + ); + } } diff --git a/assets/Scripts/BlockConfig.ts b/assets/Scripts/BlockConfig.ts index a3ab045..2bf6eed 100644 --- a/assets/Scripts/BlockConfig.ts +++ b/assets/Scripts/BlockConfig.ts @@ -1,52 +1,48 @@ /** - * 方块配置 - 8 个等级,不同颜色 - * Cocos Creator 3.8.8 + * 方块与棋盘配置 */ export interface LevelConfig { - level: number; - color: string; // 十六进制颜色 - name: string; // 等级名称 + level: number; + color: string; + name: string; } -export const BLOCK_CONFIGS: LevelConfig[] = [ - { level: 1, color: '#A8E6CF', name: '草' }, - { level: 2, color: '#4CAF50', name: '木' }, - { level: 3, color: '#2196F3', name: '水' }, - { level: 4, color: '#9C27B0', name: '风' }, - { level: 5, color: '#FF9800', name: '火' }, - { level: 6, color: '#F44336', name: '雷' }, - { level: 7, color: '#E91E63', name: '光' }, - { level: 8, color: '#FFD700', name: '神' }, +/** 8 个等级 — 颜色由浅到深,名称由低到高 */ +export const LEVELS: LevelConfig[] = [ + { level: 1, color: '#A8E6CF', name: '草' }, + { level: 2, color: '#4CAF50', name: '木' }, + { level: 3, color: '#2196F3', name: '水' }, + { level: 4, color: '#9C27B0', name: '风' }, + { level: 5, color: '#FF9800', name: '火' }, + { level: 6, color: '#F44336', name: '雷' }, + { level: 7, color: '#E91E63', name: '光' }, + { level: 8, color: '#FFD700', name: '神' }, ]; -// 棋盘尺寸 +/** 棋盘尺寸 */ export const ROWS = 12; export const COLS = 8; -export const CELL_SIZE = 64; -export const BOARD_PADDING = 8; +export const CELL = 64; // 格子像素 +export const PAD = 6; // 格子间距 +export const MAX_LEVEL = 8; // 最高等级 -// 自动生成间隔(秒) +/** 自动生成间隔(秒) */ export const SPAWN_INTERVAL = 3.0; -// 合并分数基数 -export const MERGE_SCORE = 10; +/** 合并基础分 */ +export const MERGE_BASE_SCORE = 10; -/** - * 获取指定等级的配置 - */ -export function getBlockConfig(level: number): LevelConfig { - const idx = Math.min(Math.max(level - 1, 0), BLOCK_CONFIGS.length - 1); - return BLOCK_CONFIGS[idx]; +/** 获取等级配置 */ +export function conf(level: number): LevelConfig { + return LEVELS[Math.min(Math.max(level - 1, 0), LEVELS.length - 1)]; } -/** - * 随机生成一个初始等级(低等级概率高) - */ -export function getRandomSpawnLevel(): number { - const roll = Math.random(); - if (roll < 0.45) return 1; - if (roll < 0.75) return 2; - if (roll < 0.90) return 3; - if (roll < 0.97) return 4; - return 5; +/** 随机生成初始等级(低等级高概率) */ +export function randLevel(): number { + const r = Math.random(); + if (r < 0.45) return 1; + if (r < 0.75) return 2; + if (r < 0.90) return 3; + if (r < 0.97) return 4; + return 5; } diff --git a/assets/Scripts/GameBoard.ts b/assets/Scripts/GameBoard.ts index 2ea180a..edfbd83 100644 --- a/assets/Scripts/GameBoard.ts +++ b/assets/Scripts/GameBoard.ts @@ -1,231 +1,208 @@ /** - * 棋盘组件 - 管理 8x12 方格的生成、移动、合并 - * Cocos Creator 3.8.8 + * 棋盘 — 8×12 网格管理、生成、移动、合并 */ import { _decorator, Component, Node, UITransform, Color, Sprite, Vec3, v3 } from 'cc'; -import { ROWS, COLS, CELL_SIZE, BOARD_PADDING, MERGE_SCORE } from './BlockConfig'; +import { ROWS, COLS, CELL, PAD, MAX_LEVEL, MERGE_BASE_SCORE } from './BlockConfig'; import { Block } from './Block'; const { ccclass } = _decorator; @ccclass('GameBoard') export class GameBoard extends Component { - // 二维网格引用 - private grid: (Block | null)[][] = []; + private grid: (Block | null)[][] = []; + private originX = 0; + private originY = 0; - // 棋盘几何参数 - private startX = 0; - private startY = 0; + /** 合并得分回调 */ + onScore: ((pts: number) => void) | null = null; - // 外部回调 - public onScoreUpdate: ((pts: number) => void) | null = null; + // ============================================================ + // 初始化 + // ============================================================ - // ============================================================ - // 生命周期 - // ============================================================ + onLoad() { + const w = COLS * (CELL + PAD) + PAD; + const h = ROWS * (CELL + PAD) + PAD; + this.originX = -w / 2 + PAD; + this.originY = -h / 2 + PAD; - onLoad() { - const boardW = COLS * (CELL_SIZE + BOARD_PADDING) + BOARD_PADDING; - const boardH = ROWS * (CELL_SIZE + BOARD_PADDING) + BOARD_PADDING; - this.startX = -boardW / 2 + BOARD_PADDING; - this.startY = -boardH / 2 + BOARD_PADDING; + this.initGrid(); + this.drawBg(w, h); + } - this.buildGrid(); - this.drawBackground(boardW, boardH); + private initGrid() { + this.grid = []; + for (let r = 0; r < ROWS; r++) { + this.grid[r] = []; + for (let c = 0; c < COLS; c++) { + this.grid[r][c] = null; + this.drawCell(r, c); + } } + } - // ============================================================ - // 棋盘初始化 - // ============================================================ + /** 每个格子的半透明底纹 */ + private drawCell(r: number, c: number) { + const n = new Node('cell'); + const s = n.addComponent(Sprite); + s.type = Sprite.Type.SIMPLE; + s.color = new Color(60, 60, 60, 80); + n.addComponent(UITransform).setContentSize(CELL, CELL); + n.setPosition(this.toWorld(r, c)); + this.node.addChild(n); + } - /** 创建空网格 + 格子背景 */ - private buildGrid() { - this.grid = []; - for (let r = 0; r < ROWS; r++) { - this.grid[r] = []; - for (let c = 0; c < COLS; c++) { - this.grid[r][c] = null; - this.createCellBackdrop(r, c); - } - } + /** 棋盘背景面板 */ + private drawBg(w: number, h: number) { + const bg = new Node('bg'); + const s = bg.addComponent(Sprite); + s.type = Sprite.Type.SIMPLE; + s.color = new Color(28, 28, 40, 220); + bg.addComponent(UITransform).setContentSize(w + 16, h + 16); + bg.setPosition(0, 0, -1); + this.node.addChild(bg); + } + + // ============================================================ + // 坐标 + // ============================================================ + + toWorld(r: number, c: number): Vec3 { + return v3( + this.originX + c * (CELL + PAD) + CELL / 2, + this.originY + r * (CELL + PAD) + CELL / 2, + 0, + ); + } + + /** 世界坐标 → 网格坐标,超出范围返回 null */ + toGrid(x: number, y: number): { r: number; c: number } | null { + const c = Math.round((x - this.originX) / (CELL + PAD)); + const r = Math.round((y - this.originY) / (CELL + PAD)); + if (r < 0 || r >= ROWS || c < 0 || c >= COLS) return null; + return { r, c }; + } + + // ============================================================ + // 查询 + // ============================================================ + + isEmpty(r: number, c: number) { + return r >= 0 && r < ROWS && c >= 0 && c < COLS && !this.grid[r][c]; + } + + get(r: number, c: number): Block | null { + if (r < 0 || r >= ROWS || c < 0 || c >= COLS) return null; + return this.grid[r][c]; + } + + /** 四方向邻居 */ + neighbors(r: number, c: number): Block[] { + const out: Block[] = []; + for (const [dr, dc] of [[-1, 0], [1, 0], [0, -1], [0, 1]]) { + const b = this.get(r + dr, c + dc); + if (b) out.push(b); } + return out; + } - /** 一个格子的半透明背景 */ - private createCellBackdrop(row: number, col: number) { - const n = new Node('Cell'); - const s = n.addComponent(Sprite); - s.type = Sprite.Type.SIMPLE; - s.color = new Color(60, 60, 60, 80); - n.addComponent(UITransform).setContentSize(CELL_SIZE, CELL_SIZE); - n.setPosition(this.gridToWorld(row, col)); - this.node.addChild(n); + // ============================================================ + // 方块操作 + // ============================================================ + + /** 在随机空位生成新方块 */ + spawn(level: number): Block | null { + const empty: { r: number; c: number }[] = []; + for (let r = 0; r < ROWS; r++) + for (let c = 0; c < COLS; c++) + if (this.isEmpty(r, c)) empty.push({ r, c }); + + if (!empty.length) return null; + const g = empty[Math.floor(Math.random() * empty.length)]; + return this._create(g.r, g.c, level); + } + + /** 在指定位置创建方块 */ + _create(r: number, c: number, level: number): Block { + const n = new Node('block'); + const b = n.addComponent(Block); + b.init(r, c, level); + n.setPosition(this.toWorld(r, c)); + this.node.addChild(n); + this.grid[r][c] = b; + return b; + } + + /** 移动方块到新位置(目标必须为空) */ + move(block: Block, nr: number, nc: number): boolean { + if (!this.isEmpty(nr, nc)) return false; + this.grid[block.row][block.col] = null; + block.row = nr; + block.col = nc; + this.grid[nr][nc] = block; + block.node.setPosition(this.toWorld(nr, nc)); + return true; + } + + /** 移除方块 */ + remove(block: Block) { + this.grid[block.row][block.col] = null; + block.node.destroy(); + } + + // ============================================================ + // 合并 + // ============================================================ + + /** 尝试合并 a 和 b(必须相邻同等级) */ + tryMerge(a: Block, b: Block): boolean { + if (a.level !== b.level || a.isMerging || b.isMerging) return false; + if (Math.abs(a.row - b.row) + Math.abs(a.col - b.col) !== 1) return false; + this._doMerge(a, b); + return true; + } + + /** 触发某个方块周围的合并检测 */ + checkMerge(block: Block) { + if (block.isMerging) return; + for (const nb of this.neighbors(block.row, block.col)) { + if (nb.level === block.level && !nb.isMerging) { + this._doMerge(block, nb); + return; + } } + } - /** 整个棋盘的深色背景 */ - private drawBackground(w: number, h: number) { - const bg = new Node('BoardBg'); - const s = bg.addComponent(Sprite); - s.type = Sprite.Type.SIMPLE; - s.color = new Color(30, 30, 40, 200); - bg.addComponent(UITransform).setContentSize(w + 16, h + 16); - bg.setPosition(v3(0, 0, -1)); - this.node.addChild(bg); - } + private _doMerge(a: Block, b: Block) { + a.isMerging = true; + b.isMerging = true; - // ============================================================ - // 坐标转换 - // ============================================================ + const lv = Math.min(a.level + 1, MAX_LEVEL); + this.remove(b); + a.upgradeTo(lv); - public gridToWorld(row: number, col: number): Vec3 { - return v3( - this.startX + col * (CELL_SIZE + BOARD_PADDING) + CELL_SIZE / 2, - this.startY + row * (CELL_SIZE + BOARD_PADDING) + CELL_SIZE / 2, - 0 - ); - } + if (this.onScore) this.onScore(lv * MERGE_BASE_SCORE); - public worldToGrid(wx: number, wy: number): { row: number; col: number } | null { - const col = Math.round((wx - this.startX) / (CELL_SIZE + BOARD_PADDING)); - const row = Math.round((wy - this.startY) / (CELL_SIZE + BOARD_PADDING)); - if (row < 0 || row >= ROWS || col < 0 || col >= COLS) return null; - return { row, col }; - } + // 链式合并 + this.scheduleOnce(() => this.checkMerge(a), 0.25); + } - // ============================================================ - // 网格查询 - // ============================================================ + // ============================================================ + // 全局 + // ============================================================ - public isEmpty(row: number, col: number): boolean { - return row >= 0 && row < ROWS && col >= 0 && col < COLS && this.grid[row][col] === null; - } + isFull(): boolean { + for (let r = 0; r < ROWS; r++) + for (let c = 0; c < COLS; c++) + if (!this.grid[r][c]) return false; + return true; + } - public getBlock(row: number, col: number): Block | null { - if (row < 0 || row >= ROWS || col < 0 || col >= COLS) return null; - return this.grid[row][col]; - } - - /** 获取四方向的相邻方块 */ - public getAdjacent(row: number, col: number): Block[] { - const result: Block[] = []; - for (const [dr, dc] of [[-1, 0], [1, 0], [0, -1], [0, 1]]) { - const b = this.getBlock(row + dr, col + dc); - if (b) result.push(b); - } - return result; - } - - // ============================================================ - // 方块操作 - // ============================================================ - - /** 在随机空格子上生成一个方块 */ - public spawnRandom(level: number): Block | null { - const empty: { r: number; c: number }[] = []; - for (let r = 0; r < ROWS; r++) { - for (let c = 0; c < COLS; c++) { - if (this.isEmpty(r, c)) empty.push({ r, c }); - } - } - if (empty.length === 0) return null; - const cell = empty[Math.floor(Math.random() * empty.length)]; - return this._createBlockAt(cell.r, cell.c, level); - } - - /** 创建方块节点并放入网格 */ - private _createBlockAt(row: number, col: number, level: number): Block { - const node = new Node('Block'); - const block = node.addComponent(Block); - block.init(row, col, level); - node.setPosition(this.gridToWorld(row, col)); - this.node.addChild(node); - this.grid[row][col] = block; - return block; - } - - /** 移动方块到新位置(空格) */ - public moveBlock(block: Block, newRow: number, newCol: number): boolean { - if (!this.isEmpty(newRow, newCol)) return false; - this.grid[block.row][block.col] = null; - block.row = newRow; - block.col = newCol; - this.grid[newRow][newCol] = block; - block.node.setPosition(this.gridToWorld(newRow, newCol)); - return true; - } - - /** 从网格移除并销毁 */ - public removeBlock(block: Block) { - this.grid[block.row][block.col] = null; - block.node.destroy(); - } - - // ============================================================ - // 合并逻辑 - // ============================================================ - - /** 尝试合并两个相邻同等级方块 */ - public tryMerge(a: Block, b: Block): boolean { - if (a.level !== b.level || a.isMerging || b.isMerging) return false; - if (!this._isAdjacent(a, b)) return false; - this._doMerge(a, b); - return true; - } - - /** 触发一个方块周围的合并检测 */ - public triggerMergeCheck(block: Block) { - if (block.isMerging) return; - for (const other of this.getAdjacent(block.row, block.col)) { - if (other.level === block.level && !other.isMerging) { - this._doMerge(block, other); - return; - } - } - } - - private _isAdjacent(a: Block, b: Block): boolean { - const dr = Math.abs(a.row - b.row); - const dc = Math.abs(a.col - b.col); - return (dr === 1 && dc === 0) || (dr === 0 && dc === 1); - } - - private _doMerge(a: Block, b: Block) { - a.isMerging = true; - b.isMerging = true; - - const newLevel = Math.min(a.level + 1, 8); - this.removeBlock(b); - a.upgradeTo(newLevel); - - const pts = newLevel * MERGE_SCORE; - if (this.onScoreUpdate) this.onScoreUpdate(pts); - - // 链式合并(延迟检测) - this.scheduleOnce(() => this.triggerMergeCheck(a), 0.25); - } - - // ============================================================ - // 全局 - // ============================================================ - - public isBoardFull(): boolean { - for (let r = 0; r < ROWS; r++) { - for (let c = 0; c < COLS; c++) { - if (this.isEmpty(r, c)) return false; - } - } - return true; - } - - /** 清除所有方块 */ - public clearAll() { - for (let r = 0; r < ROWS; r++) { - for (let c = 0; c < COLS; c++) { - const b = this.grid[r][c]; - if (b) { - b.node.destroy(); - this.grid[r][c] = null; - } - } - } - } + clearAll() { + for (let r = 0; r < ROWS; r++) + for (let c = 0; c < COLS; c++) { + const b = this.grid[r][c]; + if (b) { b.node.destroy(); this.grid[r][c] = null; } + } + } } diff --git a/assets/Scripts/GameController.ts b/assets/Scripts/GameController.ts new file mode 100644 index 0000000..c03df95 --- /dev/null +++ b/assets/Scripts/GameController.ts @@ -0,0 +1,283 @@ +/** + * 游戏主控 — UI、游戏流程、拖拽输入 + * 拖拽机制:按下拾取 → 拖拽跟随 → 释放判定合并/移动/弹回 + */ +import { + _decorator, Component, Node, Label, Color, Sprite, UITransform, Vec3, v3, + EventTouch, +} from 'cc'; +import { GameBoard } from './GameBoard'; +import { Block } from './Block'; +import { ROWS, CELL, PAD, SPAWN_INTERVAL, randLevel } from './BlockConfig'; + +const { ccclass } = _decorator; + +// 普通枚举(不被 isolatedModules 限制) +enum GState { Idle, Playing, Over } + +@ccclass('GameController') +export class GameController extends Component { + private board!: GameBoard; + private boardNode!: Node; + private state = GState.Idle; + private score = 0; + private spawnTimer = 0; + + // ---- 拖拽 ---- + private dragging: Block | null = null; + private dragHome = v3(); // 拖拽前的位置(board 空间) + private dragOff = v3(); // 手指到方块中心偏移 + + // ---- UI ---- + private scoreLabel!: Label; + private overlay!: Node; + private finalScore!: Label; + + // ============================================================ + // 生命周期 + // ============================================================ + + onLoad() { + this.buildUI(); + this.buildBoard(); + } + + start() { + this.startGame(); + } + + update(dt: number) { + if (this.state !== GState.Playing) return; + this.spawnTimer += dt; + if (this.spawnTimer >= SPAWN_INTERVAL) { + this.spawnTimer = 0; + this.trySpawn(); + } + } + + // ============================================================ + // UI + // ============================================================ + + private buildUI() { + // 背景 + this.addSprite('bg', new Color(22, 22, 34, 255), 600, 1200, v3(0, 0, -3)); + + const bh = ROWS * (CELL + PAD); // 棋盘像素高度 + const barY = Math.floor(bh / 2) + 68; // 顶栏 Y + + // 顶栏背景 + this.addSprite('bar', new Color(38, 38, 52, 230), 480, 52, v3(0, barY, 0)); + + // 标题 + this.addLabel('title', '合成大作战', 18, new Color(180, 180, 190, 255), v3(0, barY + 42, 0)); + + // 得分 + const sn = this.addLabel('score', '得分: 0', 30, Color.WHITE, v3(0, barY, 0)); + this.scoreLabel = sn.getComponent(Label)!; + + // ---- 游戏结束 ---- + this.overlay = this.addSprite('overlay', new Color(0, 0, 0, 180), 600, 1200, v3(0, 0, 10)); + this.overlay.active = false; + + this.addLabel('go', '游戏结束', 48, new Color(255, 200, 50, 255), v3(0, 70, 11), this.overlay); + + const fn = this.addLabel('final', '最终得分: 0', 32, Color.WHITE, v3(0, 0, 11), this.overlay); + this.finalScore = fn.getComponent(Label)!; + + // 重新开始按钮 + const btn = this.addSprite('restart', new Color(66, 165, 77, 255), 200, 56, v3(0, -90, 11), this.overlay); + this.addLabel('bl', '重新开始', 26, Color.WHITE, v3(0, 0, 12), btn); + btn.on(Node.EventType.TOUCH_END, () => this.onRestart()); + } + + // ============================================================ + // 棋盘 + // ============================================================ + + private buildBoard() { + this.boardNode = new Node('board'); + this.board = this.boardNode.addComponent(GameBoard); + this.board.onScore = (p) => this.addScore(p); + this.node.addChild(this.boardNode); + + // 拖拽事件注册在根节点 (Canvas),确保全屏响应 + this.node.on(Node.EventType.TOUCH_START, this.onTouchStart, this); + this.node.on(Node.EventType.TOUCH_MOVE, this.onTouchMove, this); + this.node.on(Node.EventType.TOUCH_END, this.onTouchEnd, this); + this.node.on(Node.EventType.TOUCH_CANCEL, this.onTouchCancel, this); + } + + // ============================================================ + // 游戏流程 + // ============================================================ + + private startGame() { + this.score = 0; + this.spawnTimer = 0; + this.dragging = null; + this.state = GState.Playing; + this.scoreLabel.string = '得分: 0'; + this.overlay.active = false; + for (let i = 0; i < 3; i++) this.board.spawn(randLevel()); + } + + private trySpawn() { + if (!this.board.spawn(randLevel()) && this.board.isFull()) this.gameOver(); + } + + private addScore(p: number) { + this.score += p; + this.scoreLabel.string = `得分: ${this.score}`; + } + + private gameOver() { + this.state = GState.Over; + this.overlay.active = true; + this.finalScore.string = `最终得分: ${this.score}`; + } + + private onRestart() { + this.board.clearAll(); + this.startGame(); + } + + // ============================================================ + // 拖拽事件 + // ============================================================ + + /** UI 坐标 → Canvas 本地坐标 */ + private toLocal(t: EventTouch): Vec3 { + const p = t.getUILocation(); + const tr = this.node.getComponent(UITransform)!; + return tr.convertToNodeSpaceAR(v3(p.x, p.y, 0)); + } + + private onTouchStart(evt: EventTouch) { + if (this.state !== GState.Playing) return; + const local = this.toLocal(evt); + const g = this.board.toGrid(local.x, local.y); + if (!g) return; + + const block = this.board.get(g.r, g.c); + if (!block || block.isMerging) return; + + // 拾取 + this.dragging = block; + this.dragHome = block.node.getPosition().clone(); + this.dragOff = v3(local.x - this.dragHome.x, local.y - this.dragHome.y, 0); + + // 临时挂到 Canvas 根节点,确保渲染在最上层 + block.node.parent = this.node; + block.node.setPosition(this.dragHome.x, this.dragHome.y, 0); + block.setHighlight(true); + } + + private onTouchMove(evt: EventTouch) { + if (!this.dragging) return; + const local = this.toLocal(evt); + this.dragging.node.setPosition( + local.x - this.dragOff.x, + local.y - this.dragOff.y, + 0, + ); + } + + private onTouchEnd(_evt: EventTouch) { + if (!this.dragging) return; + this.resolveDrop(); + } + + private onTouchCancel() { + if (!this.dragging) return; + this.resolveDrop(); + } + + // ============================================================ + // 释放判定 + // ============================================================ + + private resolveDrop() { + const block = this.dragging!; + const pos = block.node.getPosition(); + + // 挂回棋盘节点(坐标系相同,位置不变) + block.node.parent = this.boardNode; + + const g = this.board.toGrid(pos.x, pos.y); + let ok = false; + + if (g) { + const { r, c } = g; + const target = this.board.get(r, c); + + if (target && target !== block && !target.isMerging) { + // ---- 目标是个方块 ---- + if (this.isAdj(block, target) && block.level === target.level) { + // 相邻同等级 → 合并 + ok = this.board.tryMerge(block, target); + } + } else if (!target && (r !== block.row || c !== block.col)) { + // ---- 目标为空位 → 移动 ---- + ok = this.board.move(block, r, c); + if (ok) { + // 移动后检查合并 + this.scheduleOnce(() => { + if (block.isValid) this.board.checkMerge(block); + }, 0.1); + } + } + } + + if (!ok) { + // 弹回 + block.playSnapBack( + { x: pos.x, y: pos.y }, + { x: this.dragHome.x, y: this.dragHome.y }, + ); + } + + block.setHighlight(false); + this.dragging = null; + } + + /** 两方块是否相邻(四方向之一) */ + private isAdj(a: Block, b: Block): boolean { + return Math.abs(a.row - b.row) + Math.abs(a.col - b.col) === 1; + } + + // ============================================================ + // 工具 + // ============================================================ + + private addSprite( + name: string, color: Color, w: number, h: number, + pos: Vec3, parent?: Node, + ): Node { + const n = new Node(name); + const s = n.addComponent(Sprite); + s.type = Sprite.Type.SIMPLE; + s.color = color; + n.addComponent(UITransform).setContentSize(w, h); + n.setPosition(pos); + (parent ?? this.node).addChild(n); + return n; + } + + private addLabel( + name: string, text: string, size: number, color: Color, + pos: Vec3, parent?: Node, + ): Node { + const n = new Node(name); + const l = n.addComponent(Label); + l.string = text; + l.fontSize = size; + l.color = color; + const ut = n.addComponent(UITransform); + // 估算宽高让点击区域适用 + ut.setContentSize(text.length * (size * 0.6) + 20, size + 10); + n.setPosition(pos); + (parent ?? this.node).addChild(n); + return n; + } +} diff --git a/assets/Scripts/GameManager.ts b/assets/Scripts/GameManager.ts deleted file mode 100644 index 903631a..0000000 --- a/assets/Scripts/GameManager.ts +++ /dev/null @@ -1,290 +0,0 @@ -/** - * 游戏主控制器 - UI、计分、输入、游戏流程 - * Cocos Creator 3.8.8 - */ -import { _decorator, Component, Node, Label, Color, Sprite, UITransform, Vec3, v3, EventTouch } from 'cc'; -import { GameBoard } from './GameBoard'; -import { Block } from './Block'; -import { ROWS, COLS, CELL_SIZE, BOARD_PADDING, SPAWN_INTERVAL, getRandomSpawnLevel } from './BlockConfig'; - -const { ccclass, property } = _decorator; - -const enum GameState { IDLE, PLAYING, GAME_OVER } - -@ccclass('GameManager') -export class GameManager extends Component { - // Cocos Creator 属性面板可见(可在此处拖拽引用) - @property({ displayName: '生成间隔(秒)', min: 1, max: 10 }) - private spawnInterval: number = SPAWN_INTERVAL; - - // ---- 内部状态 ---- - private board: GameBoard | null = null; - private boardNode: Node | null = null; - private score = 0; - private spawnTimer = 0; - private selected: Block | null = null; - private state: GameState = GameState.IDLE; - - // ---- UI 节点 ---- - private scoreLabel: Label | null = null; - private gameOverNode: Node | null = null; - private finalScoreLabel: Label | null = null; - - // ============================================================ - // 生命周期 - // ============================================================ - - onLoad() { - this.buildUI(); - this.buildBoard(); - } - - start() { - this.startGame(); - } - - update(dt: number) { - if (this.state !== GameState.PLAYING) return; - this.spawnTimer += dt; - if (this.spawnTimer >= this.spawnInterval) { - this.spawnTimer = 0; - this.spawnBlock(); - } - } - - // ============================================================ - // UI 构建 - // ============================================================ - - private buildUI() { - const root = this.node; - const rh = 960; // Canvas 设计高度 - - // ---- 背景 ---- - { - const bg = new Node('Bg'); - const s = bg.addComponent(Sprite); - s.type = Sprite.Type.SIMPLE; - s.color = new Color(25, 25, 35, 255); - bg.addComponent(UITransform).setContentSize(600, 1200); - bg.setPosition(0, 0, -2); - root.addChild(bg); - } - - const boardH = ROWS * (CELL_SIZE + BOARD_PADDING); - const topY = boardH / 2 + 60; // 棋盘中点之上的偏移 - - // ---- 记分板底 ---- - { - const sb = new Node('ScoreBg'); - const s = sb.addComponent(Sprite); - s.type = Sprite.Type.SIMPLE; - s.color = new Color(40, 40, 50, 220); - sb.addComponent(UITransform).setContentSize(480, 60); - sb.setPosition(0, topY, 0); - root.addChild(sb); - } - - // ---- 标题 ---- - { - const t = new Node('Title'); - const l = t.addComponent(Label); - l.string = '合成大作战'; - l.fontSize = 20; - l.color = new Color(200, 200, 200, 255); - t.addComponent(UITransform).setContentSize(160, 30); - t.setPosition(0, topY + 45, 0); - root.addChild(t); - } - - // ---- 分数 ---- - { - const sn = new Node('ScoreLabel'); - this.scoreLabel = sn.addComponent(Label); - this.scoreLabel.string = '得分: 0'; - this.scoreLabel.fontSize = 28; - this.scoreLabel.color = Color.WHITE.clone(); - sn.addComponent(UITransform).setContentSize(300, 40); - sn.setPosition(0, topY, 0); - root.addChild(sn); - } - - // ---- 游戏结束遮罩 ---- - { - const go = new Node('GameOver'); - const s = go.addComponent(Sprite); - s.type = Sprite.Type.SIMPLE; - s.color = new Color(0, 0, 0, 180); - go.addComponent(UITransform).setContentSize(600, 1200); - go.setPosition(0, 0, 10); - go.active = false; - root.addChild(go); - this.gameOverNode = go; - - // "游戏结束" - { - const n = new Node('GoLabel'); - const l = n.addComponent(Label); - l.string = '游戏结束'; - l.fontSize = 48; - l.color = new Color(255, 200, 50, 255); - n.addComponent(UITransform).setContentSize(300, 80); - n.setPosition(0, 80, 11); - go.addChild(n); - } - - // 最终得分 - { - const n = new Node('FinalScore'); - this.finalScoreLabel = n.addComponent(Label); - this.finalScoreLabel.string = '最终得分: 0'; - this.finalScoreLabel.fontSize = 32; - this.finalScoreLabel.color = Color.WHITE.clone(); - n.addComponent(UITransform).setContentSize(300, 50); - n.setPosition(0, 10, 11); - go.addChild(n); - } - - // 重新开始按钮 - { - const btn = new Node('RestartBtn'); - const s = btn.addComponent(Sprite); - s.type = Sprite.Type.SIMPLE; - s.color = new Color(76, 175, 80, 255); - btn.addComponent(UITransform).setContentSize(200, 60); - btn.setPosition(0, -80, 11); - const ln = new Node('BtnLabel'); - const l = ln.addComponent(Label); - l.string = '重新开始'; - l.fontSize = 28; - l.color = Color.WHITE.clone(); - ln.addComponent(UITransform).setContentSize(200, 40); - ln.setPosition(0, 0, 12); - btn.addChild(ln); - btn.on(Node.EventType.TOUCH_END, () => this.restartGame()); - go.addChild(btn); - } - } - } - - // ============================================================ - // 棋盘 - // ============================================================ - - private buildBoard() { - this.boardNode = new Node('Board'); - this.board = this.boardNode.addComponent(GameBoard); - this.board.onScoreUpdate = (pts) => this.addScore(pts); - this.node.addChild(this.boardNode); - } - - // ============================================================ - // 游戏流程 - // ============================================================ - - private startGame() { - this.score = 0; - this.spawnTimer = 0; - this.selected = null; - this.state = GameState.PLAYING; - - if (this.scoreLabel) this.scoreLabel.string = '得分: 0'; - if (this.gameOverNode) this.gameOverNode.active = false; - - // 初始生成 3 个 - if (this.board) { - for (let i = 0; i < 3; i++) { - this.board.spawnRandom(getRandomSpawnLevel()); - } - } - - // 注册触摸 - this.node.on(Node.EventType.TOUCH_END, this.onTouchEnd, this); - } - - private spawnBlock() { - if (!this.board) return; - if (!this.board.spawnRandom(getRandomSpawnLevel()) && this.board.isBoardFull()) { - this.gameOver(); - } - } - - private addScore(pts: number) { - this.score += pts; - if (this.scoreLabel) this.scoreLabel.string = `得分: ${this.score}`; - } - - private gameOver() { - this.state = GameState.GAME_OVER; - if (this.gameOverNode) this.gameOverNode.active = true; - if (this.finalScoreLabel) this.finalScoreLabel.string = `最终得分: ${this.score}`; - } - - private restartGame() { - if (this.board) this.board.clearAll(); - this.startGame(); - } - - // ============================================================ - // 输入处理 - // ============================================================ - - private onTouchEnd(evt: EventTouch) { - if (this.state !== GameState.PLAYING || !this.board) return; - - // 屏幕坐标 → Canvas 节点空间坐标 - const uiPos = evt.getUILocation(); - const uiTransform = this.node.getComponent(UITransform); - if (!uiTransform) return; - const local = uiTransform.convertToNodeSpaceAR(v3(uiPos.x, uiPos.y, 0)); - - const g = this.board.worldToGrid(local.x, local.y); - if (!g) { this.clearSelected(); return; } - - const { row, col } = g; - const tapped = this.board.getBlock(row, col); - - if (tapped) { - // 点击了方块 - if (this.selected) { - const sel = this.selected; - if (this.isAdj(sel, tapped) && sel.level === tapped.level) { - this.board.tryMerge(sel, tapped); - this.clearSelected(); - } else { - this.clearSelected(); - tapped.setSelected(true); - this.selected = tapped; - } - } else { - tapped.setSelected(true); - this.selected = tapped; - } - } else if (this.selected && this.board.isEmpty(row, col)) { - // 点击空格 → 移动选中方块 - if (this.board.moveBlock(this.selected, row, col)) { - const block = this.selected; - this.clearSelected(); - this.scheduleOnce(() => { - if (block.isValid) this.board!.triggerMergeCheck(block); - }, 0.15); - } else { - this.clearSelected(); - } - } else { - this.clearSelected(); - } - } - - private isAdj(a: Block, b: Block): boolean { - return (Math.abs(a.row - b.row) === 1 && a.col === b.col) || - (Math.abs(a.col - b.col) === 1 && a.row === b.row); - } - - private clearSelected() { - if (this.selected) { - this.selected.setSelected(false); - this.selected = null; - } - } -} diff --git a/index.html b/index.html deleted file mode 100644 index 7a1abb1..0000000 --- a/index.html +++ /dev/null @@ -1,448 +0,0 @@ - - - - - - 合成大作战 - - - -
- -
- - - diff --git a/package.json b/package.json index 40b8410..d9655ab 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,6 @@ { "name": "merge-game", "version": "1.0.0", - "description": "合成大作战 - Cocos Creator 3.8.8 合成小游戏", - "author": "lywdev", - "scripts": { - "build": "cocos build", - "run": "cocos run" - } + "description": "合成大作战 - Cocos Creator 3.8.8 拖拽合成小游戏", + "author": "lywdev" } diff --git a/settings/v2/packages.json b/settings/v2/packages.json index 2efcc11..8103732 100644 --- a/settings/v2/packages.json +++ b/settings/v2/packages.json @@ -2,6 +2,5 @@ "name": "合成大作战", "platform": "mobile", "version": "3.8.8", - "description": "8x12 棋盘合成小游戏", - "author": "lywdev" + "description": "8x12 棋盘拖拽合成小游戏" } diff --git a/tsconfig.json b/tsconfig.json index 985a453..ee94df5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,5 @@ "sourceMap": true, "types": [] }, - "include": [ - "assets/**/*" - ] + "include": ["assets/**/*"] }