Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b01d48a88d | |||
| 7c747ac9fd | |||
| 92a8c40734 | |||
| 918e0b1e86 | |||
| 38152d9b3d | |||
| 50dd54f09e | |||
| 79a14516bb | |||
| a5747ff55c | |||
| 34c36306fe | |||
| 24e71704e5 | |||
| d96ad779b1 | |||
| fcc9fbb981 | |||
| 667d657ee1 | |||
| 23d0fa63b6 | |||
| 93045cc2d3 | |||
| 8e68d9c712 | |||
| 30fb10b66c | |||
| 0485cd30fe | |||
| e2706bcdcc | |||
| 13ae305e53 | |||
| 47ca7473ec | |||
| 7db0bfadfc | |||
| a117b12981 | |||
| ea10e6ef12 | |||
| cbf7bac239 | |||
| 24c24acf44 |
@@ -0,0 +1,40 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
## 项目名称
|
||||||
|
使用大模型辅助开发俄罗斯方块程序
|
||||||
|
|
||||||
|
## 基本开发要求
|
||||||
|
|
||||||
|
### 编程语言
|
||||||
|
- 使用 C++。
|
||||||
|
- 仅使用课程已学基础语法:数组、循环、分支、函数、结构体等。
|
||||||
|
- 不使用 `class`、继承、多态等面向对象特性。
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
- 源码主要位于 `src` 目录。
|
||||||
|
- 不要随意改动已有全局变量、函数声明和文件结构。
|
||||||
|
- 如需新增创新功能,可以新增 `.cpp` 文件。
|
||||||
|
|
||||||
|
## 构建方式
|
||||||
|
|
||||||
|
优先使用项目根目录下的构建脚本:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\build-mingw.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
## 代码质量要求
|
||||||
|
|
||||||
|
1. 每次只实现一个明确功能。
|
||||||
|
2. 每个函数必须有功能描述注释。
|
||||||
|
3. 变量命名保持和原框架一致。
|
||||||
|
4. 不随意改动已有全局变量和函数声明。
|
||||||
|
5. 生成代码后必须人工审查。
|
||||||
|
6. 每个阶段完成后必须编译运行。
|
||||||
|
7. 出现 bug 时,应记录问题、原因和修复过程。
|
||||||
|
|
||||||
|
## 开发注意事项
|
||||||
|
|
||||||
|
1. 每次补全前后都要保存版本,便于报告展示。
|
||||||
|
2. 现场汇报时,所有组员都可能被提问,不能只有一人理解代码。
|
||||||
+107
-119
@@ -1,150 +1,138 @@
|
|||||||
# Dev-C++ 运行说明
|
# Dev-C++ 运行说明
|
||||||
|
|
||||||
## 1. 适用环境
|
本项目可以尝试在 Dev-C++ 中运行,但更推荐使用 VS Code 和 `build-mingw.ps1` 脚本。Dev-C++ 对资源文件、Unicode 入口和链接库的处理更容易出现环境差异。
|
||||||
|
|
||||||
本说明适用于 Windows 下的 Dev-C++ + MinGW 环境。
|
## 1. 工程类型
|
||||||
|
|
||||||
如果 Dev-C++ 自带的是较旧版本 MinGW,也可以尝试使用,但更建议使用支持 C++17 和 `windres` 的 MinGW。
|
新建工程时请选择:
|
||||||
|
|
||||||
## 2. 当前工程结构
|
|
||||||
|
|
||||||
项目已按工程方式整理:
|
|
||||||
|
|
||||||
```text
|
|
||||||
src/
|
|
||||||
├─ include/ 头文件
|
|
||||||
├─ source/ 源文件
|
|
||||||
└─ resources/ Windows 资源脚本
|
|
||||||
|
|
||||||
assets/
|
|
||||||
├─ icons/ 图标资源
|
|
||||||
├─ images/ 图片资源
|
|
||||||
└─ audio/ 音频资源
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3. 建议的工程类型
|
|
||||||
|
|
||||||
在 Dev-C++ 中新建工程时,建议选择:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Windows Application
|
Windows Application
|
||||||
```
|
```
|
||||||
|
|
||||||
不要选控制台程序,否则窗口程序的入口和链接方式会不匹配。
|
不要选择控制台程序。
|
||||||
|
|
||||||
## 4. 需要加入工程的文件
|
## 2. 需要加入工程的源码
|
||||||
|
|
||||||
### 源文件
|
源文件:
|
||||||
|
|
||||||
把以下文件加入工程:
|
```text
|
||||||
|
src/source/stdafx.cpp
|
||||||
|
src/source/Tetris.cpp
|
||||||
|
src/source/TetrisLogic.cpp
|
||||||
|
src/source/TetrisRender.cpp
|
||||||
|
src/source/TetrisRogue.cpp
|
||||||
|
```
|
||||||
|
|
||||||
- `src/source/stdafx.cpp`
|
头文件目录:
|
||||||
- `src/source/Tetris.cpp`
|
|
||||||
- `src/source/TetrisLogic.cpp`
|
|
||||||
- `src/source/TetrisRender.cpp`
|
|
||||||
|
|
||||||
### 头文件
|
|
||||||
|
|
||||||
头文件通常不需要全部加入编译列表,但建议加入工程树便于查看:
|
|
||||||
|
|
||||||
- `src/include/stdafx.h`
|
|
||||||
- `src/include/Tetris.h`
|
|
||||||
- `src/include/targetver.h`
|
|
||||||
- `src/include/resource.h`
|
|
||||||
|
|
||||||
### 资源文件
|
|
||||||
|
|
||||||
如果 Dev-C++ 当前环境支持资源编译,再把下面文件加入工程:
|
|
||||||
|
|
||||||
- `src/resources/Tetris.rc`
|
|
||||||
|
|
||||||
## 5. 需要配置的选项
|
|
||||||
|
|
||||||
### 头文件搜索路径
|
|
||||||
|
|
||||||
把下面目录加入 include path:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
src/include
|
src/include
|
||||||
```
|
```
|
||||||
|
|
||||||
### 链接库
|
建议把以下头文件加入工程树,便于查看:
|
||||||
|
|
||||||
确保工程链接以下 Windows 库:
|
|
||||||
|
|
||||||
- `winmm`
|
|
||||||
- `gdi32`
|
|
||||||
- `user32`
|
|
||||||
|
|
||||||
### 编译标准
|
|
||||||
|
|
||||||
建议使用:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
C++17
|
src/include/stdafx.h
|
||||||
|
src/include/Tetris.h
|
||||||
|
src/include/TetrisLogicInternal.h
|
||||||
|
src/include/targetver.h
|
||||||
|
src/include/resource.h
|
||||||
```
|
```
|
||||||
|
|
||||||
### 预处理宏
|
## 3. 资源文件
|
||||||
|
|
||||||
建议定义:
|
资源文件为:
|
||||||
|
|
||||||
- `UNICODE`
|
|
||||||
- `_UNICODE`
|
|
||||||
- `_WINDOWS`
|
|
||||||
|
|
||||||
### 工程类型相关参数
|
|
||||||
|
|
||||||
如果需要手动补参数,建议与当前脚本保持一致:
|
|
||||||
|
|
||||||
- `-mwindows`
|
|
||||||
- `-municode`
|
|
||||||
|
|
||||||
## 6. 关于资源文件
|
|
||||||
|
|
||||||
这里是 Dev-C++ 环境下最可能出问题的地方。
|
|
||||||
|
|
||||||
`src/resources/Tetris.rc` 原始编码是 UTF-16,而有些 MinGW / Dev-C++ 组合下的 `windres` 不能直接编译它。
|
|
||||||
|
|
||||||
同时,资源脚本中引用的图标名是:
|
|
||||||
|
|
||||||
- `Tetris.ico`
|
|
||||||
- `small.ico`
|
|
||||||
|
|
||||||
而实际文件位于:
|
|
||||||
|
|
||||||
- `assets/icons/Tetris.ico`
|
|
||||||
- `assets/icons/small.ico`
|
|
||||||
|
|
||||||
## 7. 推荐做法
|
|
||||||
|
|
||||||
### 做法一:先不编资源文件
|
|
||||||
|
|
||||||
最省事的方式是先不要把 `Tetris.rc` 加入 Dev-C++ 工程,只编译 C++ 源文件。
|
|
||||||
|
|
||||||
这样:
|
|
||||||
|
|
||||||
- 程序主体通常可以编译运行
|
|
||||||
- 但图标、菜单、关于框资源可能缺失
|
|
||||||
|
|
||||||
### 做法二:单独处理资源文件后再加入工程
|
|
||||||
|
|
||||||
如果你希望在 Dev-C++ 中也带资源运行,建议先做这两步:
|
|
||||||
|
|
||||||
1. 将 `Tetris.rc` 另存为 UTF-8 或 ANSI
|
|
||||||
2. 把资源中的图标路径改成实际可访问路径
|
|
||||||
|
|
||||||
例如改为:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
"assets/icons/Tetris.ico"
|
src/resources/Tetris.rc
|
||||||
"assets/icons/small.ico"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
这样更容易在 Dev-C++ 中直接通过资源编译。
|
如果 Dev-C++ 能正常编译资源,可以加入该文件。
|
||||||
|
如果资源编译失败,可以先不加入资源文件,只编译 C++ 源码。这样程序主体仍可运行,但图标、菜单等资源可能不完整。
|
||||||
|
|
||||||
## 8. 运行结果
|
## 4. 编译设置
|
||||||
|
|
||||||
如果配置正确,编译后应该能得到一个 Windows 图形界面的 `exe`,并正常弹出游戏窗口。
|
建议:
|
||||||
|
|
||||||
如果只是为了开发和调试,建议优先使用本项目现成的 VS Code 配置,因为当前目录结构、构建脚本和资源处理逻辑已经和 VS Code 对齐。 Dev-C++ 更适合作为兼容运行方案。
|
- C++ 标准:`C++17`
|
||||||
|
- 工程类型:Windows 程序
|
||||||
|
- 字符集:Unicode
|
||||||
|
|
||||||
|
建议预处理宏:
|
||||||
|
|
||||||
|
```text
|
||||||
|
UNICODE
|
||||||
|
_UNICODE
|
||||||
|
_WINDOWS
|
||||||
|
```
|
||||||
|
|
||||||
|
建议编译 / 链接参数:
|
||||||
|
|
||||||
|
```text
|
||||||
|
-mwindows
|
||||||
|
-municode
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 链接库
|
||||||
|
|
||||||
|
需要链接以下库:
|
||||||
|
|
||||||
|
```text
|
||||||
|
winmm
|
||||||
|
gdi32
|
||||||
|
user32
|
||||||
|
comdlg32
|
||||||
|
ole32
|
||||||
|
gdiplus
|
||||||
|
shell32
|
||||||
|
```
|
||||||
|
|
||||||
|
如果出现 `undefined reference`,优先检查这些库是否正确加入。
|
||||||
|
|
||||||
|
## 6. 资源目录
|
||||||
|
|
||||||
|
运行时请保留:
|
||||||
|
|
||||||
|
```text
|
||||||
|
assets/icons/
|
||||||
|
assets/images/
|
||||||
|
assets/audio/
|
||||||
|
assets/video/
|
||||||
|
```
|
||||||
|
|
||||||
|
这些资源用于:
|
||||||
|
|
||||||
|
- 程序图标
|
||||||
|
- 背景图片
|
||||||
|
- 背景音乐
|
||||||
|
- 视频复活
|
||||||
|
|
||||||
|
如果工作目录设置不正确,程序可能找不到这些资源。
|
||||||
|
|
||||||
|
## 7. 常见问题
|
||||||
|
|
||||||
|
### 资源文件编译失败
|
||||||
|
|
||||||
|
Dev-C++ 的 `windres` 对资源文件编码和路径比较敏感。可以先不加入 `Tetris.rc`,或者改用项目自带脚本构建。
|
||||||
|
|
||||||
|
### 无法识别 `_tWinMain`
|
||||||
|
|
||||||
|
说明工程类型或 Unicode 参数不正确。请确认使用 Windows Application,并启用 Unicode 相关宏和 `-municode`。
|
||||||
|
|
||||||
|
### 背景、音乐或视频缺失
|
||||||
|
|
||||||
|
说明运行目录找不到 `assets/`。建议从项目根目录运行,或保持 exe 与资源目录的相对位置。
|
||||||
|
|
||||||
|
### 链接失败
|
||||||
|
|
||||||
|
检查是否加入了 `winmm`、`gdiplus`、`shell32` 等库。
|
||||||
|
|
||||||
|
## 8. 建议
|
||||||
|
|
||||||
|
Dev-C++ 适合作为备用运行方式。
|
||||||
|
如果要稳定构建、调试和课堂展示,建议优先使用:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File .\build-mingw.ps1
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,21 +1,97 @@
|
|||||||
# Tereis
|
# Tereis
|
||||||
|
|
||||||
基于 C++ 与 Windows API 实现的俄罗斯方块课程项目。
|
Tereis 是一个基于 C++、Win32 API、GDI/GDI+ 实现的桌面版俄罗斯方块课程大作业。
|
||||||
|
|
||||||
本项目使用 MinGW 进行构建,当前已完成基础窗口框架、方块逻辑、绘图显示与资源编译接入,适合作为《大学计算》程序设计大作业使用。
|
项目在经典俄罗斯方块玩法上扩展了 Rogue 模式,加入等级成长、强化选择、主动技能、特殊方块、视频复活、鼠标交互和视觉特效。程序不依赖游戏引擎,主要使用 Win32 消息循环和 GDI 绘图完成。
|
||||||
|
|
||||||
## 项目简介
|
## 功能概览
|
||||||
|
|
||||||
项目目标是实现一个可运行的桌面版俄罗斯方块程序,包含以下核心内容:
|
### 经典模式
|
||||||
|
|
||||||
- 创建 Windows 游戏窗口
|
- 标准俄罗斯方块规则
|
||||||
- 实现方块生成、移动、旋转与下落
|
- 方块生成、移动、旋转、软降、硬降
|
||||||
- 实现碰撞检测、方块固定与游戏结束判定
|
- 方块落地固定、消行、计分和死亡判定
|
||||||
- 实现消行逻辑与基础分数系统
|
- 预测落点显示
|
||||||
- 实现界面绘制与部分资源显示
|
- 暂停、重开、返回主菜单
|
||||||
- 提供 MinGW 构建脚本和 VS Code 调试配置
|
|
||||||
|
|
||||||
## 目录结构
|
### Rogue 模式
|
||||||
|
|
||||||
|
Rogue 模式是本项目的主要扩展玩法。
|
||||||
|
|
||||||
|
- 消行获得分数和 EXP
|
||||||
|
- EXP 满后进入强化选择界面
|
||||||
|
- 支持普通三选一强化
|
||||||
|
- 支持双重抉择,同屏选择两个强化
|
||||||
|
- 支持命运轮盘,同屏展示六个强化并选择两个
|
||||||
|
- 随时间提升危险等级,底部封锁区会压缩可用空间
|
||||||
|
- 支持多种强化联动和构筑方向
|
||||||
|
|
||||||
|
### 强化与技能
|
||||||
|
|
||||||
|
项目中包含多类强化效果:
|
||||||
|
|
||||||
|
- 基础成长:得分倍率、EXP 倍率、预览数量、下落速度调整
|
||||||
|
- 生存强化:最后一搏、时间缓流、稳定结构
|
||||||
|
- 主动技能:备用仓、清屏炸弹、黑洞奇点、空中换形
|
||||||
|
- 特殊方块:爆破核心、棱镜激光、十字方块、彩虹方块
|
||||||
|
- 进阶联动:连锁火花、连环炸弹、雷霆四消、雷霆棱镜
|
||||||
|
- 风险收益:高压悬赏、豪赌四消、极限玩家、赌徒契约
|
||||||
|
- 升级联动:双重抉择、命运轮盘、升级冲击波、进化冲击
|
||||||
|
|
||||||
|
具体效果可在游戏主菜单的 `帮助 -> 强化图鉴` 中查看。
|
||||||
|
|
||||||
|
### 鼠标交互
|
||||||
|
|
||||||
|
除键盘操作外,项目也支持鼠标点击:
|
||||||
|
|
||||||
|
- 主菜单项目可点击
|
||||||
|
- 帮助页项目可点击
|
||||||
|
- 升级卡片可点击选择
|
||||||
|
- 多选强化可点击标记
|
||||||
|
- 暂停和结算界面按钮可点击
|
||||||
|
- 非主菜单界面左上角有返回按钮,可点击回到主菜单
|
||||||
|
- 右下角音乐按钮可点击开关背景音乐
|
||||||
|
|
||||||
|
### 视听与资源
|
||||||
|
|
||||||
|
- 自定义图标
|
||||||
|
- 背景图片
|
||||||
|
- 背景音乐
|
||||||
|
- 消行和技能清除特效
|
||||||
|
- 死亡后可播放本地视频复活一次
|
||||||
|
|
||||||
|
## 操作说明
|
||||||
|
|
||||||
|
### 通用键盘操作
|
||||||
|
|
||||||
|
| 按键 | 功能 |
|
||||||
|
| --- | --- |
|
||||||
|
| `← / A` | 左移 |
|
||||||
|
| `→ / D` | 右移 |
|
||||||
|
| `↑ / W` | 旋转 |
|
||||||
|
| `↓ / S` | 软降 |
|
||||||
|
| `Space` | 硬降 |
|
||||||
|
| `P` | 暂停 / 继续 |
|
||||||
|
| `R` | 重开当前对局 |
|
||||||
|
| `M` | 返回主菜单 |
|
||||||
|
|
||||||
|
### Rogue 模式额外按键
|
||||||
|
|
||||||
|
| 按键 | 功能 |
|
||||||
|
| --- | --- |
|
||||||
|
| `C / Shift` | 备用仓 |
|
||||||
|
| `Z` | 黑洞奇点 |
|
||||||
|
| `X` | 清屏炸弹 |
|
||||||
|
| `V` | 空中换形 |
|
||||||
|
| 死亡后 `V` | 看视频复活一次 |
|
||||||
|
|
||||||
|
### 升级选择
|
||||||
|
|
||||||
|
- 普通升级:方向键 / WASD 切换,Enter 或 Space 确认
|
||||||
|
- 双重抉择 / 命运轮盘:Space 标记,Enter 确认已选强化
|
||||||
|
- 鼠标操作:直接点击升级卡片即可选择或标记
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Tereis/
|
Tereis/
|
||||||
@@ -24,45 +100,37 @@ Tereis/
|
|||||||
│ ├─ source/ 源文件
|
│ ├─ source/ 源文件
|
||||||
│ └─ resources/ Windows 资源脚本
|
│ └─ resources/ Windows 资源脚本
|
||||||
├─ assets/
|
├─ assets/
|
||||||
│ ├─ icons/ 图标资源
|
│ ├─ audio/ 背景音乐
|
||||||
│ ├─ images/ 图片资源
|
│ ├─ icons/ 程序图标
|
||||||
│ └─ audio/ 音频资源
|
│ ├─ images/ 背景图片
|
||||||
|
│ └─ video/ 复活视频
|
||||||
|
├─ report/ 报告相关材料
|
||||||
├─ .vscode/ VS Code 配置
|
├─ .vscode/ VS Code 配置
|
||||||
├─ .vscode-build/ 本地构建输出目录
|
├─ .vscode-build/ 本地构建输出目录
|
||||||
├─ report/ 实验报告材料与草稿
|
|
||||||
├─ build-mingw.ps1 MinGW 构建脚本
|
├─ build-mingw.ps1 MinGW 构建脚本
|
||||||
├─ list.md 项目阶段划分
|
├─ README.md 项目说明
|
||||||
├─ VSCode运行说明.md VS Code 使用说明
|
├─ VSCode运行说明.md VS Code 构建运行说明
|
||||||
└─ README.md 项目说明
|
└─ Dev-C++运行说明.md Dev-C++ 兼容运行说明
|
||||||
```
|
```
|
||||||
|
|
||||||
## 开发阶段划分
|
## 构建环境
|
||||||
|
|
||||||
整个程序按 6 个阶段拆分实现:
|
推荐环境:
|
||||||
|
|
||||||
1. 窗口创建与程序框架搭建
|
|
||||||
2. 游戏区域与方块数据结构设计
|
|
||||||
3. 方块生成、移动与旋转功能
|
|
||||||
4. 碰撞检测与方块固定逻辑
|
|
||||||
5. 消除逻辑与分数系统
|
|
||||||
6. 界面完善与创新功能扩展
|
|
||||||
|
|
||||||
详细内容见 [list.md](./list.md)。
|
|
||||||
|
|
||||||
## 构建与运行
|
|
||||||
|
|
||||||
### 环境要求
|
|
||||||
|
|
||||||
- Windows
|
- Windows
|
||||||
- MinGW
|
|
||||||
- `g++.exe`
|
|
||||||
- `gdb.exe`
|
|
||||||
- `windres.exe`
|
|
||||||
- PowerShell
|
- PowerShell
|
||||||
|
- MinGW-w64
|
||||||
|
- `g++.exe`
|
||||||
|
- `windres.exe`
|
||||||
|
- 如需调试:`gdb.exe`
|
||||||
|
|
||||||
脚本会优先使用系统 `PATH` 中的工具;如果未加入 `PATH`,也兼容 `C:\mingw64\bin\` 下的 MinGW。
|
构建脚本会优先使用系统 `PATH` 中的 MinGW。如果没有加入 `PATH`,脚本也会尝试使用:
|
||||||
|
|
||||||
### 使用脚本构建
|
```text
|
||||||
|
C:\mingw64\bin\
|
||||||
|
```
|
||||||
|
|
||||||
|
## 构建与运行
|
||||||
|
|
||||||
在项目根目录执行:
|
在项目根目录执行:
|
||||||
|
|
||||||
@@ -70,58 +138,66 @@ Tereis/
|
|||||||
powershell -NoProfile -ExecutionPolicy Bypass -File .\build-mingw.ps1
|
powershell -NoProfile -ExecutionPolicy Bypass -File .\build-mingw.ps1
|
||||||
```
|
```
|
||||||
|
|
||||||
构建完成后会生成:
|
构建成功后生成:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
.vscode-build\mingw\Tetris.exe
|
.vscode-build\mingw\Tetris.exe
|
||||||
```
|
```
|
||||||
|
|
||||||
### 构建并直接运行
|
构建后直接运行:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell -NoProfile -ExecutionPolicy Bypass -File .\build-mingw.ps1 -Run
|
powershell -NoProfile -ExecutionPolicy Bypass -File .\build-mingw.ps1 -Run
|
||||||
```
|
```
|
||||||
|
|
||||||
### 使用 VS Code
|
## 常见问题
|
||||||
|
|
||||||
项目已提供:
|
### 1. 提示 `Tetris.exe: Permission denied`
|
||||||
|
|
||||||
- 构建任务 `build Tetris MinGW`
|
说明游戏程序仍在运行,链接器无法覆盖旧文件。
|
||||||
- 运行任务 `run Tetris MinGW`
|
|
||||||
- 调试配置 `Debug Tetris MinGW`
|
|
||||||
|
|
||||||
详见 [VSCode运行说明.md](./VSCode运行说明.md)。
|
处理方式:
|
||||||
|
|
||||||
## 资源文件说明
|
- 关闭正在运行的游戏窗口
|
||||||
|
- 重新执行构建命令
|
||||||
|
|
||||||
项目包含 Windows 资源文件 `src/resources/Tetris.rc`,其中定义了图标、菜单、快捷键和关于框等内容。
|
### 2. 没有背景图、音乐或视频
|
||||||
|
|
||||||
由于原始 `Tetris.rc` 为 UTF-16 编码,当前构建脚本会在编译时临时转换资源文件编码,并将 `assets/icons/` 中的图标文件一起编译进最终程序,因此资源不再被跳过。
|
请确认运行时保留了 `assets/` 目录。项目会从资源目录读取背景、音乐和复活视频。
|
||||||
|
|
||||||
## 报告目录
|
### 3. 视频复活播放失败
|
||||||
|
|
||||||
实验报告相关材料已整理到 [report/](./report/):
|
项目会优先查找:
|
||||||
|
|
||||||
- `report.md`:报告正文草稿
|
- `assets/video/video.avi`
|
||||||
- `outline.md`:章节提纲
|
- `assets/video/video.mp4`
|
||||||
- `notes.md`:待补充内容
|
|
||||||
- `images/`:截图和流程图
|
|
||||||
- `code-snippets/`:报告中准备引用的代码
|
|
||||||
- `submission/`:最终提交版文档
|
|
||||||
|
|
||||||
## 当前状态
|
如果系统不支持对应格式,可能会播放失败。建议保留项目中已提供的视频文件。
|
||||||
|
|
||||||
当前项目已完成的工作:
|
### 4. 鼠标点击不生效
|
||||||
|
|
||||||
- 修复项目迁移后的路径配置问题
|
请确认运行的是最新构建结果。若构建时 `Tetris.exe` 被占用,实际运行的可能仍是旧版本。
|
||||||
- 补充 `.gitignore`
|
|
||||||
- 接入资源文件编译流程
|
|
||||||
- 整理项目阶段清单
|
|
||||||
- 建立实验报告目录结构
|
|
||||||
|
|
||||||
后续可以继续完善的方向:
|
## 课程展示建议
|
||||||
|
|
||||||
- 优化界面表现
|
建议按以下顺序展示:
|
||||||
- 完善分数与状态提示
|
|
||||||
- 增加创新功能
|
1. 主菜单、帮助页和鼠标点击
|
||||||
- 补充测试截图和实验分析
|
2. 经典模式基础玩法
|
||||||
|
3. Rogue 模式升级选择
|
||||||
|
4. 双重抉择或命运轮盘的多选界面
|
||||||
|
5. 主动技能:黑洞、炸弹、换形、备用仓
|
||||||
|
6. 特殊方块和消除特效
|
||||||
|
7. 死亡后视频复活
|
||||||
|
|
||||||
|
## 实现说明
|
||||||
|
|
||||||
|
本项目以过程式 C++ 写法为主,核心逻辑分布如下:
|
||||||
|
|
||||||
|
- `src/source/Tetris.cpp`:窗口、消息循环、输入和鼠标交互
|
||||||
|
- `src/source/TetrisLogic.cpp`:基础方块逻辑、消行和状态重置
|
||||||
|
- `src/source/TetrisRogue.cpp`:Rogue 模式、强化、技能和成长系统
|
||||||
|
- `src/source/TetrisRender.cpp`:界面绘制、面板、动画和特效
|
||||||
|
- `src/include/Tetris.h`:主要结构体、全局状态和函数声明
|
||||||
|
|
||||||
|
项目适合作为程序设计课程大作业展示,也便于在答辩时讲解窗口程序、游戏循环、碰撞检测、状态管理和功能扩展。
|
||||||
|
|||||||
@@ -0,0 +1,331 @@
|
|||||||
|
# Rogue 事件系统 TODO 评估
|
||||||
|
|
||||||
|
本文档用于整理后续 Rogue 随机事件方向,先做设计评估,不进入代码实现。
|
||||||
|
|
||||||
|
当前项目已经具备的基础:
|
||||||
|
|
||||||
|
- `workRegion[20][10]` 棋盘格,可直接做障碍、垃圾行、局部清除、压实等棋盘改动。
|
||||||
|
- Rogue 模式已有危险等级、底部封锁行、下落速度变化、强化池、主动技能、特殊方块、Hold、Next 预览。
|
||||||
|
- 消行结算、方块生成、计时器、渲染反馈已经集中在少数文件中,适合加一层“事件状态 + 事件调度”。
|
||||||
|
|
||||||
|
当前项目暂缺的基础:
|
||||||
|
|
||||||
|
- 没有通用随机事件调度器。
|
||||||
|
- 棋盘格目前主要用数字表示普通方块/彩虹方块,缺少石头、污染、尖刺、锁链、目标块等细分格子类型。
|
||||||
|
- 不规划 Boss/敌人系统;事件只作为 Rogue 随机事件存在。
|
||||||
|
- 没有完整的事件 UI 状态栏、倒计时提示和事件历史展示。
|
||||||
|
|
||||||
|
## 第一阶段:最适合当前项目,优先考虑
|
||||||
|
|
||||||
|
这些事件基本能复用现有棋盘、计时、方块生成、Hold/Next、消行和反馈系统,改动相对可控。
|
||||||
|
|
||||||
|
- [ ] 底部随机升起:底部生成 1-3 行障碍,带少量空洞。
|
||||||
|
- 适合度:高。
|
||||||
|
- 原因:类似垃圾行,可直接移动棋盘并填充底部。
|
||||||
|
- 注意:需要避免无预警秒杀,可加 2-3 秒提示。
|
||||||
|
|
||||||
|
- [ ] 墙体收缩:左右各封锁 1 列,持续 20 秒。
|
||||||
|
- 适合度:高。
|
||||||
|
- 原因:已有底部封锁概念,可扩展为临时左右边界。
|
||||||
|
- 注意:碰撞、落点、渲染都要读取临时边界,避免只画不挡。
|
||||||
|
|
||||||
|
- [ ] 断层错位:随机选择几行,整体左/右平移 1 格。
|
||||||
|
- 适合度:高。
|
||||||
|
- 原因:直接操作 `workRegion` 行数据。
|
||||||
|
- 注意:边缘溢出的格子如何处理需要规则化,建议溢出消失或反向空洞补位。
|
||||||
|
|
||||||
|
- [ ] 塌方:场上悬空方块向下坠落,重新压实。
|
||||||
|
- 适合度:高。
|
||||||
|
- 原因:项目已有 `ApplyBoardGravity()`,可复用或扩展。
|
||||||
|
- 注意:作为负面事件时可能反而帮助玩家,需要定位为混合事件。
|
||||||
|
|
||||||
|
- [ ] 地刺:底部若干格变成尖刺,占位但可被消行清除。
|
||||||
|
- 适合度:中高。
|
||||||
|
- 原因:本质是特殊障碍格。
|
||||||
|
- 注意:需要新增格子类型和渲染颜色,但规则简单。
|
||||||
|
|
||||||
|
- [ ] 封印列:某一列暂时不能放置方块,持续数个方块。
|
||||||
|
- 适合度:高。
|
||||||
|
- 原因:可通过碰撞检测禁止当前方块固定到该列。
|
||||||
|
- 注意:需要明确“不能经过”还是“不能落地占用”,建议先做“不能落地占用”。
|
||||||
|
|
||||||
|
- [ ] 强风:当前方块每隔 1 秒自动向左/右偏移。
|
||||||
|
- 适合度:高。
|
||||||
|
- 原因:已有左右移动和计时器。
|
||||||
|
- 注意:偏移前必须做碰撞检测;建议只作用于活动方块。
|
||||||
|
|
||||||
|
- [ ] 重力紊乱:方块下落速度周期性忽快忽慢。
|
||||||
|
- 适合度:高。
|
||||||
|
- 原因:已有 `currentFallInterval` 和 Rogue 下落速度计算。
|
||||||
|
- 注意:需要避免与狂热、时间缓流等强化互相覆盖。
|
||||||
|
|
||||||
|
- [ ] 旋转失灵:每隔一个方块,有一个方块不能旋转。
|
||||||
|
- 适合度:高。
|
||||||
|
- 原因:只需在 `Rotate()` 入口判断事件状态。
|
||||||
|
- 注意:UI 要明确提示,避免像输入失效。
|
||||||
|
|
||||||
|
- [ ] 镜像操作:左右移动反转,持续 10 秒。
|
||||||
|
- 适合度:高。
|
||||||
|
- 原因:输入分发处交换左右移动即可。
|
||||||
|
- 注意:鼠标或未来触控输入也要统一处理。
|
||||||
|
|
||||||
|
- [ ] 粘滞空气:横移延迟增加,持续 15 秒。
|
||||||
|
- 适合度:中。
|
||||||
|
- 原因:当前键盘输入看起来偏即时响应,若没有 DAS/ARR 机制,需要先补横移节流。
|
||||||
|
- 注意:实现成本比描述略高。
|
||||||
|
|
||||||
|
- [ ] 超重方块:当前方块落地后锁定时间大幅缩短。
|
||||||
|
- 适合度:中。
|
||||||
|
- 原因:若当前没有锁定延迟,需要先引入 lock delay。
|
||||||
|
- 注意:没有锁定延迟时可以降级为“触底立即固定”。
|
||||||
|
|
||||||
|
- [ ] 长条枯竭:一段时间内 I 方块出现率降低。
|
||||||
|
- 适合度:高。
|
||||||
|
- 原因:已有 Rogue 方块权重生成。
|
||||||
|
- 注意:不要完全禁用 I,避免体验过硬。
|
||||||
|
|
||||||
|
- [ ] 蛇群:接下来 6 个方块更容易出现 S/Z。
|
||||||
|
- 适合度:高。
|
||||||
|
- 原因:同样是方块生成权重调整。
|
||||||
|
|
||||||
|
- [ ] 小块雨:连续掉落若干 1x1 或 1x2 小块。
|
||||||
|
- 适合度:中高。
|
||||||
|
- 原因:可以作为特殊临时方块池。
|
||||||
|
- 注意:现有方块数组是 7 种 4x4,需要扩展临时形状或伪装为特殊类型。
|
||||||
|
|
||||||
|
- [ ] 石化块:接下来 3 个方块落地后部分格子变石头。
|
||||||
|
- 适合度:中高。
|
||||||
|
- 原因:固定阶段可替换格子类型。
|
||||||
|
- 注意:需要新增石头格规则:是否可消行、是否可被技能清除。
|
||||||
|
|
||||||
|
- [ ] 幽灵块:方块预览正常,但落下时形状随机变化一次。
|
||||||
|
- 适合度:中高。
|
||||||
|
- 原因:消费 Next 后替换当前 `type`。
|
||||||
|
- 注意:变化后要重新校验出生位置,避免直接死亡。
|
||||||
|
|
||||||
|
- [ ] 高压:30 秒内必须消除 4 行,否则加垃圾行。
|
||||||
|
- 适合度:高。
|
||||||
|
- 原因:已有总消行统计和计时器。
|
||||||
|
- 注意:要记录事件期间消行数,不用全局总数直接判断。
|
||||||
|
|
||||||
|
- [ ] 单消惩罚:单行消除会额外生成 1 行垃圾。
|
||||||
|
- 适合度:高。
|
||||||
|
- 原因:接在消行结算后处理。
|
||||||
|
- 注意:对新手很重,适合短持续或中后期。
|
||||||
|
|
||||||
|
- [ ] 禁止四消:四消不会得分,反而生成障碍,短期事件。
|
||||||
|
- 适合度:中高。
|
||||||
|
- 原因:消行奖励处可拦截。
|
||||||
|
- 注意:与现有“雷霆四消/赌命四消”强化冲突,需要定义优先级。
|
||||||
|
|
||||||
|
- [ ] Hold 冻结:暂时无法使用 Hold。
|
||||||
|
- 适合度:高。
|
||||||
|
- 原因:`HoldCurrentPiece()` 可直接判断事件状态。
|
||||||
|
|
||||||
|
- [ ] 幽灵落点失效:影子落点隐藏。
|
||||||
|
- 适合度:高。
|
||||||
|
- 原因:渲染处跳过落点绘制。
|
||||||
|
|
||||||
|
- [ ] 预览故障:Next 队列隐藏或随机显示假预览。
|
||||||
|
- 适合度:中高。
|
||||||
|
- 原因:渲染和实际队列分离即可。
|
||||||
|
- 注意:假预览要清楚是事件效果,否则会像 bug。
|
||||||
|
|
||||||
|
- [ ] 盲盒方块:下一个方块落下前不显示形状。
|
||||||
|
- 适合度:高。
|
||||||
|
- 原因:比假预览更简单,只隐藏下一块显示。
|
||||||
|
|
||||||
|
- [ ] 色彩错乱:方块颜色随机打乱,影响识别。
|
||||||
|
- 适合度:高。
|
||||||
|
- 原因:只影响渲染颜色表映射。
|
||||||
|
|
||||||
|
- [ ] 强化过热:主动技能冷却或充能需求翻倍,持续 30 秒。
|
||||||
|
- 适合度:中高。
|
||||||
|
- 原因:已有清屏炸弹、黑洞、空中换形等主动能力。
|
||||||
|
- 注意:当前更像次数/充能,不一定是冷却;描述可改为“充能需求提高”。
|
||||||
|
|
||||||
|
- [ ] 能量泄露:玩家能量条持续下降,消行可补充。
|
||||||
|
- 适合度:中。
|
||||||
|
- 原因:当前没有统一能量条,但有技能充能概念。
|
||||||
|
- 注意:除非先做能量资源,否则建议暂缓。
|
||||||
|
|
||||||
|
- [ ] 装备短路:随机一个强化暂时失效。
|
||||||
|
- 适合度:中高。
|
||||||
|
- 原因:已有强化等级字段,可加临时禁用表。
|
||||||
|
- 注意:需要避免禁用核心 UI/基础能力导致解释困难。
|
||||||
|
|
||||||
|
- [ ] 贪婪试炼:期间消行奖励翻倍,但每 10 秒加 1 行垃圾。
|
||||||
|
- 适合度:高。
|
||||||
|
- 原因:得分/经验倍率和垃圾行都能复用。
|
||||||
|
|
||||||
|
- [ ] 混乱祝福:下落速度提高,但消行奖励翻倍。
|
||||||
|
- 适合度:高。
|
||||||
|
- 原因:与当前 Rogue 风险收益强化风格一致。
|
||||||
|
|
||||||
|
- [ ] 危险长条:下一个必定是 I,但落地后生成 1 行垃圾。
|
||||||
|
- 适合度:高。
|
||||||
|
- 原因:直接改 Next 队列并挂一个落地后副作用。
|
||||||
|
|
||||||
|
- [ ] 猎杀时刻:生成目标块,清除后给奖励,失败则加障碍。
|
||||||
|
- 适合度:中高。
|
||||||
|
- 原因:需要目标格类型,但玩法清晰,展示效果好。
|
||||||
|
|
||||||
|
- [ ] 极限压缩:场地高度降低,但所有消行计为双倍。
|
||||||
|
- 适合度:中高。
|
||||||
|
- 原因:已有底部封锁和奖励计算。
|
||||||
|
- 注意:与常驻危险等级封锁叠加时要设上限。
|
||||||
|
|
||||||
|
## 第二阶段:可做,但需要先补系统能力
|
||||||
|
|
||||||
|
这些事件有价值,但依赖特殊格子、持续状态、倒计时 UI、事件优先级或更复杂的结算顺序。
|
||||||
|
|
||||||
|
- [ ] 裂缝:随机列变成危险列,若 10 秒内没消除该列附近,会生成障碍。
|
||||||
|
- 依赖:危险列标记、倒计时、附近消行判定。
|
||||||
|
|
||||||
|
- [ ] 污染区:随机 3x3 区域被污染,污染格消除后会扩散一次。
|
||||||
|
- 依赖:污染格类型、扩散结算、特殊渲染。
|
||||||
|
- 风险:规则复杂,容易和普通消行/技能清除冲突。
|
||||||
|
|
||||||
|
- [ ] 脆弱方块:当前方块每旋转一次,随机掉落一个单格碎片。
|
||||||
|
- 依赖:活动方块局部拆分或生成独立固定格。
|
||||||
|
- 风险:要定义碎片是否立即固定、是否触发消行。
|
||||||
|
|
||||||
|
- [ ] 磁力干扰:方块靠近障碍块时会被吸附,加速锁定。
|
||||||
|
- 依赖:障碍格类型、锁定延迟或特殊横移规则。
|
||||||
|
- 风险:玩家可读性较差。
|
||||||
|
|
||||||
|
- [ ] 巨型块:下一个方块变成五格或六格异形块。
|
||||||
|
- 依赖:扩展方块形状系统。
|
||||||
|
- 风险:现有 `bricks[7][4][4][4]` 固定为 7 种,需要重构或另建临时形状。
|
||||||
|
|
||||||
|
- [ ] 爆裂块:下一个方块落地后随机炸掉相邻格子,可能好也可能坏。
|
||||||
|
- 依赖:落地后局部爆破和反馈。
|
||||||
|
- 备注:已有爆破核心,可复用特效和清除函数。
|
||||||
|
|
||||||
|
- [ ] 锁链块:方块落地后被锁住,只有相邻消行才能解除。
|
||||||
|
- 依赖:锁链格类型、相邻消行判定。
|
||||||
|
- 风险:若锁住后不能正常消行,规则会很绕。
|
||||||
|
|
||||||
|
- [ ] 连击试炼:规定时间内保持连击,断连则触发惩罚。
|
||||||
|
- 依赖:连击生命周期定义。
|
||||||
|
- 备注:已有 `comboChain`,但需要确认何时断连。
|
||||||
|
|
||||||
|
- [ ] 精准清理:只有消除指定发光行才算有效消行。
|
||||||
|
- 依赖:目标行标记、奖励过滤。
|
||||||
|
- 风险:需要处理非目标行消除是否仍清棋盘。
|
||||||
|
|
||||||
|
- [ ] 过载清除:消行后不会立即消失,而是延迟 2 秒,期间仍占位。
|
||||||
|
- 依赖:延迟消行队列。
|
||||||
|
- 风险:会影响核心俄罗斯方块节奏,改动较大。
|
||||||
|
|
||||||
|
- [ ] 腐蚀行:某些行如果长期不被消除,会逐渐变成石头。
|
||||||
|
- 依赖:按行计时、石头格类型。
|
||||||
|
|
||||||
|
- [ ] 献祭规则:每消 3 行会摧毁一个随机强化效果,持续短时间。
|
||||||
|
- 依赖:强化临时禁用/降级机制。
|
||||||
|
- 风险:永久摧毁太重,建议先做短时封印。
|
||||||
|
|
||||||
|
- [ ] 迷雾:只显示当前方块附近区域。
|
||||||
|
- 依赖:渲染遮罩。
|
||||||
|
- 风险:实现不难,但视觉遮挡强,需要短时使用。
|
||||||
|
|
||||||
|
- [ ] 倒计时遮蔽:场地部分区域被 UI 遮挡,数秒后消失。
|
||||||
|
- 依赖:渲染遮罩和事件 UI。
|
||||||
|
- 风险:可能被认为是不公平遮挡。
|
||||||
|
|
||||||
|
- [ ] 假警报:显示即将生成垃圾行的预警,但部分是假的。
|
||||||
|
- 依赖:预警 UI 和真假队列。
|
||||||
|
- 风险:需要先有稳定的真实预警系统。
|
||||||
|
|
||||||
|
- [ ] 诅咒回响:最近一次选择的强化产生副作用。
|
||||||
|
- 依赖:记录最近强化、为每类强化配置副作用。
|
||||||
|
- 风险:内容量较大。
|
||||||
|
|
||||||
|
- [ ] 保险失效:复活、护盾、防死类强化暂时不可用。
|
||||||
|
- 依赖:临时禁用强化系统。
|
||||||
|
- 备注:当前已有复活/最后一搏类能力,适合在禁用系统完成后做。
|
||||||
|
|
||||||
|
- [ ] 超载窗口:所有强化效果增强,但结束后生成大量垃圾行。
|
||||||
|
- 依赖:强化倍率覆盖层。
|
||||||
|
- 风险:需要为每个强化定义“增强”含义。
|
||||||
|
|
||||||
|
- [ ] 债务事件:立刻获得奖励,但未来 60 秒内难度提高。
|
||||||
|
- 依赖:奖励发放、难度临时增益。
|
||||||
|
- 备注:适合作为事件选择,而不是无条件负面事件。
|
||||||
|
|
||||||
|
- [ ] 不稳定炸弹:给一个炸弹块,能清障碍,但倒计时结束会爆坏场地。
|
||||||
|
- 依赖:临时特殊方块、倒计时、坏爆炸规则。
|
||||||
|
|
||||||
|
- [ ] 交易事件:牺牲一个强化,换取清屏/降难度。
|
||||||
|
- 依赖:事件选择 UI、强化移除或临时禁用。
|
||||||
|
|
||||||
|
- [ ] 祝福陷阱:获得临时强力效果,结束后触发一次负面事件。
|
||||||
|
- 依赖:事件链和延迟触发。
|
||||||
|
|
||||||
|
## 第三阶段:攻击型随机事件,可选做
|
||||||
|
|
||||||
|
这些事件原本偏“敌人攻击”风格,但本项目不做 Boss 机制。若保留,只作为普通随机事件触发,不做血量、阶段、护盾和敌人 UI。
|
||||||
|
|
||||||
|
- [ ] 炮击:指定列被标记,数秒后生成障碍块。
|
||||||
|
- 适合度:中高。
|
||||||
|
- 备注:可以和裂缝共用“列预警 + 延迟生成障碍”的逻辑。
|
||||||
|
|
||||||
|
- [ ] 毒液喷洒:随机格变污染块,消行时才清除。
|
||||||
|
- 适合度:中。
|
||||||
|
- 备注:依赖污染格系统。
|
||||||
|
|
||||||
|
- [ ] 目标块入侵:场上出现几个需要消行击破的目标块。
|
||||||
|
- 适合度:中。
|
||||||
|
- 备注:可复用猎杀时刻目标块。
|
||||||
|
|
||||||
|
- [ ] 护盾阶段:不做。
|
||||||
|
- 原因:依赖 Boss 血量、阶段和伤害规则,已经超出当前项目方向。
|
||||||
|
|
||||||
|
- [ ] 反击规则:玩家每次消行,额外生成 1 行垃圾。
|
||||||
|
- 适合度:中。
|
||||||
|
- 备注:可作为短时高压事件,但要避免和“单消惩罚”重复。
|
||||||
|
|
||||||
|
- [ ] 蓄力攻击:倒计时结束后生成大量垃圾,消行可延缓倒计时。
|
||||||
|
- 适合度:中高。
|
||||||
|
- 备注:作为普通倒计时事件即可。
|
||||||
|
|
||||||
|
- [ ] 寄生核心:一个核心块出现,每隔一段时间向周围扩散障碍。
|
||||||
|
- 依赖:核心格、扩散逻辑、清除判定。
|
||||||
|
|
||||||
|
- [ ] 锁定轰炸:玩家最近放置最多的列被优先攻击。
|
||||||
|
- 依赖:记录落子列热度。
|
||||||
|
- 备注:可作为高级普通事件。
|
||||||
|
|
||||||
|
## 建议实现顺序
|
||||||
|
|
||||||
|
- [ ] 设计并新增 `RogueEventState`:当前事件、剩余时间、剩余方块数、事件参数、临时倍率、禁用标记。
|
||||||
|
- [ ] 新增事件调度入口:只在 Rogue 模式中按时间/危险等级触发,避免经典模式受影响。
|
||||||
|
- [ ] 新增事件提示 UI:事件名、剩余秒数、简短效果;先用右侧反馈面板,不做复杂界面。
|
||||||
|
- [ ] 先完成 6 个低风险事件作为 MVP:
|
||||||
|
- [ ] 底部随机升起
|
||||||
|
- [ ] 强风
|
||||||
|
- [ ] 重力紊乱
|
||||||
|
- [ ] 镜像操作
|
||||||
|
- [ ] 长条枯竭
|
||||||
|
- [ ] 高压
|
||||||
|
- [ ] 再完成 4 个能体现肉鸽取舍的混合事件:
|
||||||
|
- [ ] 贪婪试炼
|
||||||
|
- [ ] 混乱祝福
|
||||||
|
- [ ] 危险长条
|
||||||
|
- [ ] 极限压缩
|
||||||
|
- [ ] 然后补特殊格子系统:
|
||||||
|
- [ ] 石头格
|
||||||
|
- [ ] 尖刺格
|
||||||
|
- [ ] 污染格
|
||||||
|
- [ ] 目标格
|
||||||
|
- [ ] 锁链格
|
||||||
|
- [ ] 特殊格子系统稳定后,再做污染、猎杀、寄生核心、锁链块、石化块。
|
||||||
|
- [ ] 攻击型随机事件最后做;如果时间有限,只保留“炮击/蓄力攻击/锁定轰炸”三个最容易解释的事件。
|
||||||
|
|
||||||
|
## 不建议优先做的事件
|
||||||
|
|
||||||
|
- [ ] 过载清除:会改动消行核心节奏,容易引入状态错乱。
|
||||||
|
- [ ] 巨型块:需要扩展方块数据结构,投入高。
|
||||||
|
- [ ] 磁力干扰:可读性弱,调参成本高。
|
||||||
|
- [ ] 倒计时遮蔽:玩家体验可能偏负面。
|
||||||
|
- [ ] 护盾阶段:需要 Boss 血量和阶段机制,当前项目明确不做。
|
||||||
+75
-80
@@ -1,82 +1,72 @@
|
|||||||
# VS Code 运行说明
|
# VS Code 运行说明
|
||||||
|
|
||||||
## 1. 适用环境
|
本项目推荐使用 `VS Code + MinGW-w64 + PowerShell` 构建和调试。
|
||||||
|
|
||||||
本项目适用于 Windows + VS Code + MinGW 环境。
|
## 1. 环境准备
|
||||||
|
|
||||||
建议已安装:
|
需要安装:
|
||||||
|
|
||||||
- VS Code
|
- Visual Studio Code
|
||||||
- C/C++ 扩展(Microsoft)
|
- Microsoft C/C++ 扩展
|
||||||
|
- MinGW-w64
|
||||||
- PowerShell
|
- PowerShell
|
||||||
- MinGW,且可用 `g++.exe`、`gdb.exe`、`windres.exe`
|
|
||||||
|
|
||||||
脚本会优先使用系统 `PATH` 中的工具;如果未加入 `PATH`,也兼容 `C:\mingw64\bin\` 下的 MinGW。
|
MinGW 中至少需要:
|
||||||
|
|
||||||
## 2. 项目结构
|
- `g++.exe`
|
||||||
|
- `windres.exe`
|
||||||
|
- `gdb.exe`,仅调试时需要
|
||||||
|
|
||||||
当前工程目录结构如下:
|
## 2. 打开项目
|
||||||
|
|
||||||
|
请在 VS Code 中打开项目根目录,也就是包含以下文件和目录的位置:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
build-mingw.ps1
|
||||||
src/
|
src/
|
||||||
├─ include/ 头文件
|
|
||||||
├─ source/ 源文件
|
|
||||||
└─ resources/ Windows 资源脚本
|
|
||||||
|
|
||||||
assets/
|
assets/
|
||||||
├─ icons/ 图标资源
|
.vscode/
|
||||||
├─ images/ 图片资源
|
README.md
|
||||||
└─ audio/ 音频资源
|
|
||||||
```
|
```
|
||||||
|
|
||||||
其中:
|
不要只打开 `src/` 子目录,否则 VS Code 任务和调试配置无法正常工作。
|
||||||
|
|
||||||
- 头文件检索路径为 `src/include`
|
## 3. 使用 VS Code 任务构建
|
||||||
- 编译的源文件位于 `src/source`
|
|
||||||
- 资源脚本为 `src/resources/Tetris.rc`
|
|
||||||
- 图标资源为 `assets/icons/Tetris.ico` 和 `assets/icons/small.ico`
|
|
||||||
|
|
||||||
## 3. 打开方式
|
按:
|
||||||
|
|
||||||
用 VS Code 打开项目根目录,也就是包含以下文件的目录:
|
|
||||||
|
|
||||||
- `build-mingw.ps1`
|
|
||||||
- `.vscode/`
|
|
||||||
- `src/`
|
|
||||||
- `assets/`
|
|
||||||
|
|
||||||
不要只打开 `src/` 子目录,否则任务和调试配置会失效。
|
|
||||||
|
|
||||||
## 4. 构建方式
|
|
||||||
|
|
||||||
### 方法一:快捷键构建
|
|
||||||
|
|
||||||
按 `Ctrl+Shift+B`,默认会执行:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
build Tetris MinGW
|
Ctrl + Shift + B
|
||||||
```
|
```
|
||||||
|
|
||||||
它会调用:
|
默认会执行项目中的 MinGW 构建任务。
|
||||||
|
|
||||||
```powershell
|
也可以打开命令面板,选择:
|
||||||
powershell -NoProfile -ExecutionPolicy Bypass -File .\build-mingw.ps1
|
|
||||||
```
|
|
||||||
|
|
||||||
### 方法二:命令面板运行任务
|
|
||||||
|
|
||||||
在命令面板中执行:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Tasks: Run Task
|
Tasks: Run Task
|
||||||
```
|
```
|
||||||
|
|
||||||
然后选择:
|
然后运行:
|
||||||
|
|
||||||
- `build Tetris MinGW`
|
- `build Tetris MinGW`
|
||||||
- `run Tetris MinGW`
|
- `run Tetris MinGW`
|
||||||
|
|
||||||
## 5. 调试方式
|
## 4. 使用命令行构建
|
||||||
|
|
||||||
|
在 VS Code 终端中进入项目根目录,执行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File .\build-mingw.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
构建后直接运行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File .\build-mingw.ps1 -Run
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 调试
|
||||||
|
|
||||||
按 `F5`,选择:
|
按 `F5`,选择:
|
||||||
|
|
||||||
@@ -84,69 +74,74 @@ Tasks: Run Task
|
|||||||
Debug Tetris MinGW
|
Debug Tetris MinGW
|
||||||
```
|
```
|
||||||
|
|
||||||
调试配置会先执行构建任务,然后启动:
|
调试配置会先构建项目,再启动:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
.vscode-build\mingw\Tetris.exe
|
.vscode-build\mingw\Tetris.exe
|
||||||
```
|
```
|
||||||
|
|
||||||
当前工作目录为项目根目录。
|
如果提示找不到 `gdb.exe`,说明 MinGW 的调试器没有安装或没有加入 `PATH`。
|
||||||
|
|
||||||
## 6. 构建输出
|
## 6. 构建输出
|
||||||
|
|
||||||
成功构建后,输出文件位于:
|
最终程序:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
.vscode-build\mingw\Tetris.exe
|
.vscode-build\mingw\Tetris.exe
|
||||||
```
|
```
|
||||||
|
|
||||||
同时在资源编译阶段,脚本还会临时生成:
|
构建中间文件:
|
||||||
|
|
||||||
- `.vscode-build\mingw\Tetris.utf8.rc`
|
```text
|
||||||
- `.vscode-build\mingw\Tetris.res.o`
|
.vscode-build\mingw\Tetris.utf8.rc
|
||||||
|
.vscode-build\mingw\Tetris.res.o
|
||||||
|
```
|
||||||
|
|
||||||
这些都属于中间产物,不需要手动维护。
|
这些中间文件不需要手动维护。
|
||||||
|
|
||||||
## 7. 资源文件说明
|
## 7. 资源文件
|
||||||
|
|
||||||
原始 `Tetris.rc` 是 UTF-16 编码,MinGW 的 `windres` 不能直接稳定编译该文件。
|
项目运行依赖:
|
||||||
|
|
||||||
当前脚本的处理方式是:
|
```text
|
||||||
|
assets/icons/
|
||||||
|
assets/images/
|
||||||
|
assets/audio/
|
||||||
|
assets/video/
|
||||||
|
```
|
||||||
|
|
||||||
1. 读取 `src/resources/Tetris.rc`
|
如果只移动 `Tetris.exe` 而不带 `assets/`,会影响背景图、音乐和视频复活功能。
|
||||||
2. 临时转换为 UTF-8
|
|
||||||
3. 将图标路径替换为 `assets/icons/` 下的实际文件
|
|
||||||
4. 使用 `windres` 编译资源
|
|
||||||
5. 将资源对象与 C++ 源文件一起链接
|
|
||||||
|
|
||||||
因此在 VS Code 环境下,图标和菜单资源是会参与构建的。
|
|
||||||
|
|
||||||
## 8. 常见问题
|
## 8. 常见问题
|
||||||
|
|
||||||
### 找不到 `g++.exe`
|
### 找不到 `g++.exe`
|
||||||
|
|
||||||
说明 MinGW 没加入系统 `PATH`,或者未安装在 `C:\mingw64\bin\`。
|
|
||||||
处理方式:
|
处理方式:
|
||||||
|
|
||||||
- 把 MinGW 的 `bin` 目录加入 `PATH`
|
- 将 MinGW 的 `bin` 目录加入系统 `PATH`
|
||||||
- 或安装到 `C:\mingw64\bin\`
|
- 或将 MinGW 安装到 `C:\mingw64\bin\`
|
||||||
|
|
||||||
### 找不到 `gdb.exe`
|
|
||||||
|
|
||||||
说明调试器不可用。
|
|
||||||
构建通常还能继续,但 `F5` 调试会失败。
|
|
||||||
|
|
||||||
### 找不到 `windres.exe`
|
### 找不到 `windres.exe`
|
||||||
|
|
||||||
程序主体仍可能编译通过,但资源文件无法编译进最终 `exe`。
|
资源文件无法编译,图标和菜单资源可能缺失。请检查 MinGW 安装是否完整。
|
||||||
|
|
||||||
### 打开的是 `src/` 而不是项目根目录
|
### `Tetris.exe: Permission denied`
|
||||||
|
|
||||||
会导致:
|
说明程序正在运行,构建时无法覆盖旧 exe。
|
||||||
|
|
||||||
- VS Code 任务不可用
|
处理方式:
|
||||||
- 调试配置不可用
|
|
||||||
- include 路径不正确
|
|
||||||
|
|
||||||
应重新打开项目根目录。
|
1. 关闭游戏窗口
|
||||||
|
2. 重新构建
|
||||||
|
|
||||||
|
### 鼠标点击、按钮或界面不是最新版
|
||||||
|
|
||||||
|
通常是因为构建失败后仍在运行旧 exe。请确认构建命令成功完成。
|
||||||
|
|
||||||
|
## 9. 推荐运行流程
|
||||||
|
|
||||||
|
1. 打开项目根目录
|
||||||
|
2. 关闭旧的游戏窗口
|
||||||
|
3. 执行构建
|
||||||
|
4. 运行 `.vscode-build\mingw\Tetris.exe`
|
||||||
|
5. 如果要调试,按 `F5`
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
@@ -65,6 +65,7 @@ $Sources = @(
|
|||||||
(Join-Path $SourceDir "stdafx.cpp"),
|
(Join-Path $SourceDir "stdafx.cpp"),
|
||||||
(Join-Path $SourceDir "Tetris.cpp"),
|
(Join-Path $SourceDir "Tetris.cpp"),
|
||||||
(Join-Path $SourceDir "TetrisLogic.cpp"),
|
(Join-Path $SourceDir "TetrisLogic.cpp"),
|
||||||
|
(Join-Path $SourceDir "TetrisLogicInnovation.cpp"),
|
||||||
(Join-Path $SourceDir "TetrisRogue.cpp"),
|
(Join-Path $SourceDir "TetrisRogue.cpp"),
|
||||||
(Join-Path $SourceDir "TetrisRender.cpp")
|
(Join-Path $SourceDir "TetrisRender.cpp")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -140,6 +140,8 @@ struct UpgradeUiState
|
|||||||
int pendingCount;
|
int pendingCount;
|
||||||
int totalChosenCount;
|
int totalChosenCount;
|
||||||
int picksRemaining;
|
int picksRemaining;
|
||||||
|
int markedCount;
|
||||||
|
bool marked[6];
|
||||||
UpgradeOption options[6];
|
UpgradeOption options[6];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -180,6 +182,15 @@ struct ParticleEffect
|
|||||||
COLORREF color;
|
COLORREF color;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct CellFlashEffect
|
||||||
|
{
|
||||||
|
int ticks;
|
||||||
|
int totalTicks;
|
||||||
|
int x;
|
||||||
|
int y;
|
||||||
|
COLORREF color;
|
||||||
|
};
|
||||||
|
|
||||||
enum ScreenState
|
enum ScreenState
|
||||||
{
|
{
|
||||||
SCREEN_MENU = 0,
|
SCREEN_MENU = 0,
|
||||||
@@ -215,6 +226,11 @@ extern Point point;
|
|||||||
extern Point target;
|
extern Point target;
|
||||||
extern MenuState menuState;
|
extern MenuState menuState;
|
||||||
extern HelpState helpState;
|
extern HelpState helpState;
|
||||||
|
extern int helpScrollOffset;
|
||||||
|
extern int creditPageIndex;
|
||||||
|
extern int creditAnimationTicks;
|
||||||
|
extern int creditAnimationDirection;
|
||||||
|
extern int upgradeListScrollOffset;
|
||||||
extern PlayerStats classicStats;
|
extern PlayerStats classicStats;
|
||||||
extern PlayerStats rogueStats;
|
extern PlayerStats rogueStats;
|
||||||
extern UpgradeUiState upgradeUiState;
|
extern UpgradeUiState upgradeUiState;
|
||||||
@@ -222,6 +238,7 @@ extern FeedbackState feedbackState;
|
|||||||
extern ClearEffectState clearEffectState;
|
extern ClearEffectState clearEffectState;
|
||||||
extern FloatingTextEffect floatingTextEffects[8];
|
extern FloatingTextEffect floatingTextEffects[8];
|
||||||
extern ParticleEffect particleEffects[96];
|
extern ParticleEffect particleEffects[96];
|
||||||
|
extern CellFlashEffect cellFlashEffects[64];
|
||||||
extern int currentScreen;
|
extern int currentScreen;
|
||||||
extern int currentMode;
|
extern int currentMode;
|
||||||
extern int currentFallInterval;
|
extern int currentFallInterval;
|
||||||
@@ -253,8 +270,11 @@ void ReturnToMainMenu();
|
|||||||
void ReviveAfterVideo();
|
void ReviveAfterVideo();
|
||||||
void SetFeedbackMessage(const TCHAR* title, const TCHAR* detail, int ticks);
|
void SetFeedbackMessage(const TCHAR* title, const TCHAR* detail, int ticks);
|
||||||
void OpenRulesScreen();
|
void OpenRulesScreen();
|
||||||
|
void OpenCreditScreen();
|
||||||
|
void ChangeCreditPage(int direction);
|
||||||
void OpenUpgradeMenu();
|
void OpenUpgradeMenu();
|
||||||
void ConfirmUpgradeSelection();
|
void ConfirmUpgradeSelection();
|
||||||
|
void ResetUpgradeUiState();
|
||||||
void HoldCurrentPiece();
|
void HoldCurrentPiece();
|
||||||
void UseScreenBomb();
|
void UseScreenBomb();
|
||||||
void UseBlackHole();
|
void UseBlackHole();
|
||||||
@@ -262,10 +282,13 @@ void UseAirReshape();
|
|||||||
void ResetPendingRogueVisualEvents();
|
void ResetPendingRogueVisualEvents();
|
||||||
void ResetVisualEffects();
|
void ResetVisualEffects();
|
||||||
bool TickVisualEffects();
|
bool TickVisualEffects();
|
||||||
|
bool TickCreditAnimation();
|
||||||
void TriggerLineClearEffect(const int* rows, int rowCount, int linesCleared);
|
void TriggerLineClearEffect(const int* rows, int rowCount, int linesCleared);
|
||||||
void PlayPendingLineClearEffect();
|
void PlayPendingLineClearEffect();
|
||||||
void TriggerCellClearEffect(const Point* cells, int cellCount, bool strongBurst);
|
void TriggerCellClearEffect(const Point* cells, int cellCount, bool strongBurst);
|
||||||
|
void TriggerColoredCellClearEffect(const Point* cells, int cellCount, COLORREF flashColor, bool strongBurst);
|
||||||
void AwardRogueSkillClearRewards(int clearedCells, int& scoreGain, int& expGain, bool allowLevelProgress);
|
void AwardRogueSkillClearRewards(int clearedCells, int& scoreGain, int& expGain, bool allowLevelProgress);
|
||||||
|
void CheckRogueLevelProgress();
|
||||||
void ApplyBoardGravity();
|
void ApplyBoardGravity();
|
||||||
int GetRogueFallInterval();
|
int GetRogueFallInterval();
|
||||||
int GetRoguePlayableHeight();
|
int GetRoguePlayableHeight();
|
||||||
|
|||||||
@@ -4,20 +4,102 @@
|
|||||||
|
|
||||||
extern Point pendingChainBombCenter;
|
extern Point pendingChainBombCenter;
|
||||||
extern bool pendingChainBombFollowup;
|
extern bool pendingChainBombFollowup;
|
||||||
|
extern int pendingLineClearEffectTicks;
|
||||||
|
extern int pendingLineClearEffectRows[8];
|
||||||
|
extern int pendingLineClearEffectRowCount;
|
||||||
|
extern int pendingLineClearEffectLineCount;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 计算指定方块在棋盘顶部的统一生成位置。
|
||||||
|
*/
|
||||||
Point GetSpawnPoint(int brickType);
|
Point GetSpawnPoint(int brickType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 重置经典或 Rogue 模式使用的玩家统计数据。
|
||||||
|
*/
|
||||||
void ResetPlayerStats(PlayerStats& stats, bool useRogueRules);
|
void ResetPlayerStats(PlayerStats& stats, bool useRogueRules);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置界面右侧显示的即时反馈标题、内容和持续时间。
|
||||||
|
*/
|
||||||
void SetFeedbackMessage(const TCHAR* title, const TCHAR* detail, int ticks);
|
void SetFeedbackMessage(const TCHAR* title, const TCHAR* detail, int ticks);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 判断指定方块、旋转状态和位置是否可以合法放置。
|
||||||
|
*/
|
||||||
bool IsPiecePlacementValid(int pieceType, int pieceState, Point position);
|
bool IsPiecePlacementValid(int pieceType, int pieceState, Point position);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 判断棋盘格是否为彩虹特殊方块。
|
||||||
|
*/
|
||||||
bool IsRainbowBoardCell(int cellValue);
|
bool IsRainbowBoardCell(int cellValue);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 触发小型黑洞并返回被清除的固定方块数量。
|
||||||
|
*/
|
||||||
int TriggerMiniBlackHole(int maxCellsToClear);
|
int TriggerMiniBlackHole(int maxCellsToClear);
|
||||||
int TriggerRainbowRowCompletion(int minRow, int maxRow);
|
|
||||||
|
/**
|
||||||
|
* @brief 触发彩虹方块行清除与覆盖行染色效果。
|
||||||
|
*/
|
||||||
|
int TriggerRainbowColorShift(int anchorRow, int minRow, int maxRow, int& recoloredCount);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 引爆清屏炸弹并返回清除格数。
|
||||||
|
*/
|
||||||
int TriggerScreenBomb();
|
int TriggerScreenBomb();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 清除指定中心点周围的爆破范围并返回清除格数。
|
||||||
|
*/
|
||||||
int ClearExplosiveAreaAt(int centerY, int centerX);
|
int ClearExplosiveAreaAt(int centerY, int centerX);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 清除指定列并返回清除格数。
|
||||||
|
*/
|
||||||
int ClearColumnAt(int column);
|
int ClearColumnAt(int column);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 使用指定颜色特效清除指定列并返回清除格数。
|
||||||
|
*/
|
||||||
|
int ClearColumnAtWithColor(int column, COLORREF flashColor);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 清除指定行并返回清除格数。
|
||||||
|
*/
|
||||||
int ClearRowAt(int row);
|
int ClearRowAt(int row);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 尝试填补局部空洞以稳定棋盘结构。
|
||||||
|
*/
|
||||||
int TryStabilizeBoard();
|
int TryStabilizeBoard();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 为当前方块刷新 Rogue 特殊方块标记。
|
||||||
|
*/
|
||||||
void RollCurrentPieceSpecialFlags(bool allowRandomSpecials);
|
void RollCurrentPieceSpecialFlags(bool allowRandomSpecials);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 暂存消行动画,等待升级选择结束后再播放。
|
||||||
|
*/
|
||||||
|
void QueueLineClearEffect(const int* rows, int rowCount, int linesCleared);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 尝试把旋转后的方块横向偏移指定格数后放置。
|
||||||
|
*/
|
||||||
|
bool TryRotateWithOffset(int nextState, int offsetX);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 重置下一方块预览队列。
|
||||||
|
*/
|
||||||
void ResetNextQueue();
|
void ResetNextQueue();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 消费队首下一方块并补充新的预览方块。
|
||||||
|
*/
|
||||||
int ConsumeNextType();
|
int ConsumeNextType();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 结算一次标准消行带来的 Rogue 玩法效果。
|
||||||
|
*/
|
||||||
void ApplyLineClearResult(int linesCleared);
|
void ApplyLineClearResult(int linesCleared);
|
||||||
|
|||||||
+608
-26
@@ -6,8 +6,11 @@
|
|||||||
#define MAX_LOADSTRING 100
|
#define MAX_LOADSTRING 100
|
||||||
#define GAME_TIMER_ID 1
|
#define GAME_TIMER_ID 1
|
||||||
#define EFFECT_TIMER_ID 2
|
#define EFFECT_TIMER_ID 2
|
||||||
|
#define CREDIT_TIMER_ID 3
|
||||||
|
#define WM_CREDIT_TICK (WM_APP + 1)
|
||||||
#define GAME_TIMER_INTERVAL 500
|
#define GAME_TIMER_INTERVAL 500
|
||||||
#define EFFECT_TIMER_INTERVAL 33
|
#define EFFECT_TIMER_INTERVAL 16
|
||||||
|
#define CREDIT_TIMER_INTERVAL 5
|
||||||
|
|
||||||
HINSTANCE hInst;
|
HINSTANCE hInst;
|
||||||
TCHAR szTitle[MAX_LOADSTRING];
|
TCHAR szTitle[MAX_LOADSTRING];
|
||||||
@@ -16,6 +19,7 @@ bool bgmEnabled = true;
|
|||||||
|
|
||||||
static bool bgmPlaying = false;
|
static bool bgmPlaying = false;
|
||||||
static bool bgmUsingMci = false;
|
static bool bgmUsingMci = false;
|
||||||
|
static MMRESULT creditTimerHandle = 0;
|
||||||
static constexpr const wchar_t* kBgmAlias = L"TereisBgm";
|
static constexpr const wchar_t* kBgmAlias = L"TereisBgm";
|
||||||
static constexpr const wchar_t* kReviveVideoAlias = L"TereisReviveVideo";
|
static constexpr const wchar_t* kReviveVideoAlias = L"TereisReviveVideo";
|
||||||
|
|
||||||
@@ -29,6 +33,293 @@ static bool FileExists(const std::wstring& path);
|
|||||||
static void StopBackgroundMusic();
|
static void StopBackgroundMusic();
|
||||||
static void StartBackgroundMusic();
|
static void StartBackgroundMusic();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 多媒体定时器回调,用于高频率请求致谢页动画刷新。
|
||||||
|
*/
|
||||||
|
static void CALLBACK CreditTimerCallback(UINT, UINT, DWORD_PTR userData, DWORD_PTR, DWORD_PTR)
|
||||||
|
{
|
||||||
|
HWND hWnd = reinterpret_cast<HWND>(userData);
|
||||||
|
if (hWnd != nullptr)
|
||||||
|
{
|
||||||
|
PostMessage(hWnd, WM_CREDIT_TICK, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 将指定滚动偏移按步长调整,并限制在非负范围内。
|
||||||
|
*/
|
||||||
|
static void AdjustScrollOffset(int& scrollOffset, int delta)
|
||||||
|
{
|
||||||
|
scrollOffset += delta;
|
||||||
|
if (scrollOffset < 0)
|
||||||
|
{
|
||||||
|
scrollOffset = 0;
|
||||||
|
}
|
||||||
|
if (scrollOffset > 2400)
|
||||||
|
{
|
||||||
|
scrollOffset = 2400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 按当前窗口缩放返回一次滚动操作的像素距离。
|
||||||
|
*/
|
||||||
|
static int GetScrollStep(HWND hWnd, int baseStep)
|
||||||
|
{
|
||||||
|
RECT clientRect;
|
||||||
|
GetClientRect(hWnd, &clientRect);
|
||||||
|
|
||||||
|
int clientWidth = clientRect.right - clientRect.left;
|
||||||
|
int clientHeight = clientRect.bottom - clientRect.top;
|
||||||
|
int scaleX = MulDiv(clientWidth, 1000, WINDOW_CLIENT_WIDTH);
|
||||||
|
int scaleY = MulDiv(clientHeight, 1000, WINDOW_CLIENT_HEIGHT);
|
||||||
|
int scale = (scaleX < scaleY) ? scaleX : scaleY;
|
||||||
|
if (scale < 500)
|
||||||
|
{
|
||||||
|
scale = 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MulDiv(baseStep, scale, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LayoutMetrics
|
||||||
|
{
|
||||||
|
int scale;
|
||||||
|
int offsetX;
|
||||||
|
int offsetY;
|
||||||
|
int layoutWidth;
|
||||||
|
int layoutHeight;
|
||||||
|
int grid;
|
||||||
|
};
|
||||||
|
|
||||||
|
static LayoutMetrics GetLayoutMetrics(HWND hWnd)
|
||||||
|
{
|
||||||
|
RECT clientRect;
|
||||||
|
GetClientRect(hWnd, &clientRect);
|
||||||
|
|
||||||
|
int clientWidth = clientRect.right - clientRect.left;
|
||||||
|
int clientHeight = clientRect.bottom - clientRect.top;
|
||||||
|
int scaleX = MulDiv(clientWidth, 1000, WINDOW_CLIENT_WIDTH);
|
||||||
|
int scaleY = MulDiv(clientHeight, 1000, WINDOW_CLIENT_HEIGHT);
|
||||||
|
int scale = (scaleX < scaleY) ? scaleX : scaleY;
|
||||||
|
if (scale < 500)
|
||||||
|
{
|
||||||
|
scale = 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
LayoutMetrics metrics = {};
|
||||||
|
metrics.scale = scale;
|
||||||
|
metrics.layoutWidth = MulDiv(WINDOW_CLIENT_WIDTH, scale, 1000);
|
||||||
|
metrics.layoutHeight = MulDiv(WINDOW_CLIENT_HEIGHT, scale, 1000);
|
||||||
|
metrics.offsetX = (clientWidth - metrics.layoutWidth) / 2;
|
||||||
|
metrics.offsetY = 0;
|
||||||
|
metrics.grid = MulDiv(GRID, scale, 1000);
|
||||||
|
return metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int ScaleValue(const LayoutMetrics& metrics, int value)
|
||||||
|
{
|
||||||
|
return MulDiv(value, metrics.scale, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int ScaleXValue(const LayoutMetrics& metrics, int value)
|
||||||
|
{
|
||||||
|
return metrics.offsetX + MulDiv(value, metrics.scale, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int ScaleYValue(const LayoutMetrics& metrics, int value)
|
||||||
|
{
|
||||||
|
return metrics.offsetY + MulDiv(value, metrics.scale, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
static RECT GetMenuCardRect(HWND hWnd)
|
||||||
|
{
|
||||||
|
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
|
||||||
|
RECT rect =
|
||||||
|
{
|
||||||
|
ScaleXValue(metrics, 110),
|
||||||
|
ScaleYValue(metrics, 70),
|
||||||
|
ScaleXValue(metrics, WINDOW_CLIENT_WIDTH - 110),
|
||||||
|
ScaleYValue(metrics, WINDOW_CLIENT_HEIGHT - 70)
|
||||||
|
};
|
||||||
|
return rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
static RECT GetMenuOptionRect(HWND hWnd, int index)
|
||||||
|
{
|
||||||
|
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
|
||||||
|
RECT menuCard = GetMenuCardRect(hWnd);
|
||||||
|
int top = menuCard.top + ScaleValue(metrics, 140) + index * ScaleValue(metrics, 130);
|
||||||
|
RECT rect =
|
||||||
|
{
|
||||||
|
menuCard.left + ScaleValue(metrics, 36),
|
||||||
|
top,
|
||||||
|
menuCard.right - ScaleValue(metrics, 36),
|
||||||
|
top + ScaleValue(metrics, 104)
|
||||||
|
};
|
||||||
|
return rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
static RECT GetRulesCardRect(HWND hWnd)
|
||||||
|
{
|
||||||
|
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
|
||||||
|
RECT rect =
|
||||||
|
{
|
||||||
|
ScaleXValue(metrics, 76),
|
||||||
|
ScaleYValue(metrics, 54),
|
||||||
|
ScaleXValue(metrics, WINDOW_CLIENT_WIDTH - 76),
|
||||||
|
ScaleYValue(metrics, WINDOW_CLIENT_HEIGHT - 54)
|
||||||
|
};
|
||||||
|
return rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
static RECT GetHelpOptionRect(HWND hWnd, int index)
|
||||||
|
{
|
||||||
|
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
|
||||||
|
RECT rulesCard = GetRulesCardRect(hWnd);
|
||||||
|
RECT contentRect =
|
||||||
|
{
|
||||||
|
rulesCard.left + ScaleValue(metrics, 36),
|
||||||
|
rulesCard.top + ScaleValue(metrics, 126),
|
||||||
|
rulesCard.right - ScaleValue(metrics, 36),
|
||||||
|
rulesCard.bottom - ScaleValue(metrics, 86)
|
||||||
|
};
|
||||||
|
int optionHeight = ScaleValue(metrics, 100);
|
||||||
|
int optionGap = ScaleValue(metrics, 22);
|
||||||
|
int optionTop = contentRect.top + ScaleValue(metrics, 18);
|
||||||
|
RECT rect =
|
||||||
|
{
|
||||||
|
contentRect.left,
|
||||||
|
optionTop + index * (optionHeight + optionGap),
|
||||||
|
contentRect.right,
|
||||||
|
optionTop + index * (optionHeight + optionGap) + optionHeight
|
||||||
|
};
|
||||||
|
return rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
static RECT GetHelpBackHintRect(HWND hWnd)
|
||||||
|
{
|
||||||
|
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
|
||||||
|
RECT rulesCard = GetRulesCardRect(hWnd);
|
||||||
|
RECT rect =
|
||||||
|
{
|
||||||
|
rulesCard.left + ScaleValue(metrics, 36),
|
||||||
|
rulesCard.bottom - ScaleValue(metrics, 58),
|
||||||
|
rulesCard.right - ScaleValue(metrics, 36),
|
||||||
|
rulesCard.bottom - ScaleValue(metrics, 24)
|
||||||
|
};
|
||||||
|
return rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取致谢页左右切换按钮的绘制和点击区域。
|
||||||
|
*/
|
||||||
|
static RECT GetCreditArrowRect(HWND hWnd, int direction)
|
||||||
|
{
|
||||||
|
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
|
||||||
|
RECT rulesCard = GetRulesCardRect(hWnd);
|
||||||
|
int size = ScaleValue(metrics, 54);
|
||||||
|
int centerY = (rulesCard.top + rulesCard.bottom) / 2;
|
||||||
|
int left = direction < 0
|
||||||
|
? rulesCard.left + ScaleValue(metrics, 52)
|
||||||
|
: rulesCard.right - ScaleValue(metrics, 52) - size;
|
||||||
|
|
||||||
|
RECT rect =
|
||||||
|
{
|
||||||
|
left,
|
||||||
|
centerY - size / 2,
|
||||||
|
left + size,
|
||||||
|
centerY + size / 2
|
||||||
|
};
|
||||||
|
return rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
static RECT GetUpgradeOverlayRect(HWND hWnd)
|
||||||
|
{
|
||||||
|
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
|
||||||
|
RECT rect =
|
||||||
|
{
|
||||||
|
ScaleXValue(metrics, 60),
|
||||||
|
ScaleYValue(metrics, 80),
|
||||||
|
ScaleXValue(metrics, WINDOW_CLIENT_WIDTH - 60),
|
||||||
|
ScaleYValue(metrics, WINDOW_CLIENT_HEIGHT - 80)
|
||||||
|
};
|
||||||
|
return rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
static RECT GetUpgradeCardRect(HWND hWnd, int index)
|
||||||
|
{
|
||||||
|
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
|
||||||
|
RECT overlayRect = GetUpgradeOverlayRect(hWnd);
|
||||||
|
int gap = ScaleValue(metrics, 18);
|
||||||
|
int horizontalPadding = ScaleValue(metrics, 36);
|
||||||
|
int verticalTop = overlayRect.top + ScaleValue(metrics, 138);
|
||||||
|
int columnCount = upgradeUiState.optionCount <= 3 ? upgradeUiState.optionCount : 3;
|
||||||
|
if (columnCount < 1)
|
||||||
|
{
|
||||||
|
columnCount = 1;
|
||||||
|
}
|
||||||
|
int rowCount = (upgradeUiState.optionCount + columnCount - 1) / columnCount;
|
||||||
|
if (rowCount < 1)
|
||||||
|
{
|
||||||
|
rowCount = 1;
|
||||||
|
}
|
||||||
|
int cardWidth = (overlayRect.right - overlayRect.left - horizontalPadding * 2 - gap * (columnCount - 1)) / columnCount;
|
||||||
|
int availableHeight = overlayRect.bottom - verticalTop - ScaleValue(metrics, 72) - (rowCount - 1) * gap;
|
||||||
|
int cardHeight = availableHeight / rowCount;
|
||||||
|
int column = index % columnCount;
|
||||||
|
int row = index / columnCount;
|
||||||
|
int left = overlayRect.left + horizontalPadding + column * (cardWidth + gap);
|
||||||
|
int top = verticalTop + row * (cardHeight + gap);
|
||||||
|
RECT rect = { left, top, left + cardWidth, top + cardHeight };
|
||||||
|
return rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
static RECT GetGameOverlayRect(HWND hWnd)
|
||||||
|
{
|
||||||
|
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
|
||||||
|
int panelGap = ScaleValue(metrics, SIDE_PANEL_GAP);
|
||||||
|
int panelWidth = ScaleValue(metrics, SIDE_PANEL_WIDTH);
|
||||||
|
int boardLeft = ScaleXValue(metrics, WINDOW_PADDING) + panelWidth + panelGap;
|
||||||
|
int boardTop = ScaleYValue(metrics, WINDOW_PADDING);
|
||||||
|
int boardWidth = nGameWidth * metrics.grid;
|
||||||
|
RECT rect =
|
||||||
|
{
|
||||||
|
boardLeft + ScaleValue(metrics, 28),
|
||||||
|
boardTop + metrics.grid * 6 + ScaleValue(metrics, 10),
|
||||||
|
boardLeft + boardWidth - ScaleValue(metrics, 28),
|
||||||
|
boardTop + metrics.grid * 10 + ScaleValue(metrics, 30)
|
||||||
|
};
|
||||||
|
return rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
static RECT GetOverlayButtonRect(HWND hWnd, int index, int buttonCount)
|
||||||
|
{
|
||||||
|
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
|
||||||
|
RECT overlayRect = GetGameOverlayRect(hWnd);
|
||||||
|
int gap = buttonCount == 3 ? ScaleValue(metrics, 8) : ScaleValue(metrics, 18);
|
||||||
|
int sidePadding = buttonCount == 3 ? ScaleValue(metrics, 14) : ScaleValue(metrics, 34);
|
||||||
|
int width = (overlayRect.right - overlayRect.left - sidePadding * 2 - gap * (buttonCount - 1)) / buttonCount;
|
||||||
|
int height = ScaleValue(metrics, 44);
|
||||||
|
int left = overlayRect.left + sidePadding + index * (width + gap);
|
||||||
|
int top = overlayRect.top + ScaleValue(metrics, 94);
|
||||||
|
RECT rect = { left, top, left + width, top + height };
|
||||||
|
return rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
static RECT GetBackButtonRect(HWND hWnd)
|
||||||
|
{
|
||||||
|
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
|
||||||
|
RECT rect =
|
||||||
|
{
|
||||||
|
ScaleXValue(metrics, 6),
|
||||||
|
ScaleYValue(metrics, 6),
|
||||||
|
ScaleXValue(metrics, 34),
|
||||||
|
ScaleYValue(metrics, 34)
|
||||||
|
};
|
||||||
|
return rect;
|
||||||
|
}
|
||||||
|
|
||||||
static void ResetGameTimer(HWND hWnd)
|
static void ResetGameTimer(HWND hWnd)
|
||||||
{
|
{
|
||||||
KillTimer(hWnd, GAME_TIMER_ID);
|
KillTimer(hWnd, GAME_TIMER_ID);
|
||||||
@@ -170,33 +461,18 @@ static bool FileExists(const std::wstring& path)
|
|||||||
|
|
||||||
static RECT GetMusicButtonRect(HWND hWnd)
|
static RECT GetMusicButtonRect(HWND hWnd)
|
||||||
{
|
{
|
||||||
RECT clientRect;
|
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
|
||||||
GetClientRect(hWnd, &clientRect);
|
int size = ScaleValue(metrics, 28);
|
||||||
|
|
||||||
int clientWidth = clientRect.right - clientRect.left;
|
|
||||||
int clientHeight = clientRect.bottom - clientRect.top;
|
|
||||||
int scaleX = MulDiv(clientWidth, 1000, WINDOW_CLIENT_WIDTH);
|
|
||||||
int scaleY = MulDiv(clientHeight, 1000, WINDOW_CLIENT_HEIGHT);
|
|
||||||
int scale = (scaleX < scaleY) ? scaleX : scaleY;
|
|
||||||
if (scale < 500)
|
|
||||||
{
|
|
||||||
scale = 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
int layoutWidth = MulDiv(WINDOW_CLIENT_WIDTH, scale, 1000);
|
|
||||||
int offsetX = (clientWidth - layoutWidth) / 2;
|
|
||||||
int offsetY = 0;
|
|
||||||
int size = MulDiv(28, scale, 1000);
|
|
||||||
if (size < 22)
|
if (size < 22)
|
||||||
{
|
{
|
||||||
size = 22;
|
size = 22;
|
||||||
}
|
}
|
||||||
int marginRight = MulDiv(12, scale, 1000);
|
int marginRight = ScaleValue(metrics, 12);
|
||||||
if (marginRight < 6)
|
if (marginRight < 6)
|
||||||
{
|
{
|
||||||
marginRight = 6;
|
marginRight = 6;
|
||||||
}
|
}
|
||||||
int marginBottom = MulDiv(12, scale, 1000);
|
int marginBottom = ScaleValue(metrics, 12);
|
||||||
if (marginBottom < 6)
|
if (marginBottom < 6)
|
||||||
{
|
{
|
||||||
marginBottom = 6;
|
marginBottom = 6;
|
||||||
@@ -204,10 +480,10 @@ static RECT GetMusicButtonRect(HWND hWnd)
|
|||||||
|
|
||||||
RECT buttonRect =
|
RECT buttonRect =
|
||||||
{
|
{
|
||||||
offsetX + layoutWidth - marginRight - size,
|
metrics.offsetX + metrics.layoutWidth - marginRight - size,
|
||||||
offsetY + MulDiv(WINDOW_CLIENT_HEIGHT, scale, 1000) - marginBottom - size,
|
metrics.offsetY + metrics.layoutHeight - marginBottom - size,
|
||||||
offsetX + layoutWidth - marginRight,
|
metrics.offsetX + metrics.layoutWidth - marginRight,
|
||||||
offsetY + MulDiv(WINDOW_CLIENT_HEIGHT, scale, 1000) - marginBottom
|
metrics.offsetY + metrics.layoutHeight - marginBottom
|
||||||
};
|
};
|
||||||
return buttonRect;
|
return buttonRect;
|
||||||
}
|
}
|
||||||
@@ -444,11 +720,22 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|||||||
switch (message)
|
switch (message)
|
||||||
{
|
{
|
||||||
case WM_CREATE:
|
case WM_CREATE:
|
||||||
|
timeBeginPeriod(1);
|
||||||
srand((unsigned int)time(nullptr));
|
srand((unsigned int)time(nullptr));
|
||||||
ReturnToMainMenu();
|
ReturnToMainMenu();
|
||||||
StartBackgroundMusic();
|
StartBackgroundMusic();
|
||||||
ResetGameTimer(hWnd);
|
ResetGameTimer(hWnd);
|
||||||
SetTimer(hWnd, EFFECT_TIMER_ID, EFFECT_TIMER_INTERVAL, nullptr);
|
SetTimer(hWnd, EFFECT_TIMER_ID, EFFECT_TIMER_INTERVAL, nullptr);
|
||||||
|
creditTimerHandle = timeSetEvent(
|
||||||
|
CREDIT_TIMER_INTERVAL,
|
||||||
|
1,
|
||||||
|
CreditTimerCallback,
|
||||||
|
reinterpret_cast<DWORD_PTR>(hWnd),
|
||||||
|
TIME_PERIODIC | TIME_CALLBACK_FUNCTION);
|
||||||
|
if (creditTimerHandle == 0)
|
||||||
|
{
|
||||||
|
SetTimer(hWnd, CREDIT_TIMER_ID, CREDIT_TIMER_INTERVAL, nullptr);
|
||||||
|
}
|
||||||
InvalidateRect(hWnd, nullptr, FALSE);
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
break;
|
break;
|
||||||
case WM_COMMAND:
|
case WM_COMMAND:
|
||||||
@@ -468,6 +755,12 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case WM_CREDIT_TICK:
|
||||||
|
if (currentScreen == SCREEN_RULES && helpState.currentPage == 4 && TickCreditAnimation())
|
||||||
|
{
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
break;
|
||||||
case WM_TIMER:
|
case WM_TIMER:
|
||||||
if (wParam == EFFECT_TIMER_ID)
|
if (wParam == EFFECT_TIMER_ID)
|
||||||
{
|
{
|
||||||
@@ -477,6 +770,14 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (wParam == CREDIT_TIMER_ID && creditTimerHandle == 0)
|
||||||
|
{
|
||||||
|
if (currentScreen == SCREEN_RULES && helpState.currentPage == 4 && TickCreditAnimation())
|
||||||
|
{
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (wParam == GAME_TIMER_ID)
|
if (wParam == GAME_TIMER_ID)
|
||||||
{
|
{
|
||||||
@@ -608,6 +909,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|||||||
if (!gameOverFlag)
|
if (!gameOverFlag)
|
||||||
{
|
{
|
||||||
DeleteLines();
|
DeleteLines();
|
||||||
|
CheckRogueLevelProgress();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -639,8 +941,221 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (currentScreen != SCREEN_MENU && IsPointInRect(GetBackButtonRect(hWnd), mouseX, mouseY))
|
||||||
|
{
|
||||||
|
ReturnToMainMenu();
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentScreen == SCREEN_MENU)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < menuState.optionCount; i++)
|
||||||
|
{
|
||||||
|
if (!IsPointInRect(GetMenuOptionRect(hWnd, i), mouseX, mouseY))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
menuState.selectedIndex = i;
|
||||||
|
if (i == 0)
|
||||||
|
{
|
||||||
|
StartGameWithMode(MODE_CLASSIC);
|
||||||
|
}
|
||||||
|
else if (i == 1)
|
||||||
|
{
|
||||||
|
StartGameWithMode(MODE_ROGUE);
|
||||||
|
}
|
||||||
|
else if (i == 2)
|
||||||
|
{
|
||||||
|
OpenRulesScreen();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
OpenCreditScreen();
|
||||||
|
}
|
||||||
|
ResetGameTimer(hWnd);
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentScreen == SCREEN_RULES)
|
||||||
|
{
|
||||||
|
if (helpState.currentPage == 0)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < helpState.optionCount; i++)
|
||||||
|
{
|
||||||
|
if (IsPointInRect(GetHelpOptionRect(hWnd, i), mouseX, mouseY))
|
||||||
|
{
|
||||||
|
helpState.selectedIndex = i;
|
||||||
|
helpState.currentPage = i + 1;
|
||||||
|
helpScrollOffset = 0;
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (IsPointInRect(GetHelpBackHintRect(hWnd), mouseX, mouseY))
|
||||||
|
{
|
||||||
|
ReturnToMainMenu();
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (IsPointInRect(GetHelpBackHintRect(hWnd), mouseX, mouseY))
|
||||||
|
{
|
||||||
|
if (helpState.currentPage == 4)
|
||||||
|
{
|
||||||
|
ReturnToMainMenu();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
helpState.currentPage = 0;
|
||||||
|
helpScrollOffset = 0;
|
||||||
|
}
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
else if (helpState.currentPage == 4 && IsPointInRect(GetCreditArrowRect(hWnd, -1), mouseX, mouseY))
|
||||||
|
{
|
||||||
|
ChangeCreditPage(-1);
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
else if (helpState.currentPage == 4 && IsPointInRect(GetCreditArrowRect(hWnd, 1), mouseX, mouseY))
|
||||||
|
{
|
||||||
|
ChangeCreditPage(1);
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentScreen == SCREEN_UPGRADE)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < upgradeUiState.optionCount; i++)
|
||||||
|
{
|
||||||
|
if (!IsPointInRect(GetUpgradeCardRect(hWnd, i), mouseX, mouseY))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
upgradeUiState.selectedIndex = i;
|
||||||
|
if (upgradeUiState.picksRemaining > 1)
|
||||||
|
{
|
||||||
|
bool currentlyMarked = upgradeUiState.marked[i];
|
||||||
|
if (currentlyMarked)
|
||||||
|
{
|
||||||
|
upgradeUiState.marked[i] = false;
|
||||||
|
if (upgradeUiState.markedCount > 0)
|
||||||
|
{
|
||||||
|
upgradeUiState.markedCount--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (upgradeUiState.markedCount < upgradeUiState.picksRemaining)
|
||||||
|
{
|
||||||
|
upgradeUiState.marked[i] = true;
|
||||||
|
upgradeUiState.markedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (upgradeUiState.markedCount == upgradeUiState.picksRemaining)
|
||||||
|
{
|
||||||
|
ConfirmUpgradeSelection();
|
||||||
|
ResetGameTimer(hWnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ConfirmUpgradeSelection();
|
||||||
|
ResetGameTimer(hWnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentScreen == SCREEN_PLAYING && suspendFlag)
|
||||||
|
{
|
||||||
|
if (IsPointInRect(GetOverlayButtonRect(hWnd, 0, 2), mouseX, mouseY))
|
||||||
|
{
|
||||||
|
suspendFlag = false;
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (IsPointInRect(GetOverlayButtonRect(hWnd, 1, 2), mouseX, mouseY))
|
||||||
|
{
|
||||||
|
ReturnToMainMenu();
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentScreen == SCREEN_PLAYING && gameOverFlag)
|
||||||
|
{
|
||||||
|
if (reviveAvailable)
|
||||||
|
{
|
||||||
|
if (IsPointInRect(GetOverlayButtonRect(hWnd, 0, 3), mouseX, mouseY))
|
||||||
|
{
|
||||||
|
if (PlayReviveVideo(hWnd))
|
||||||
|
{
|
||||||
|
ReviveAfterVideo();
|
||||||
|
ResetGameTimer(hWnd);
|
||||||
|
}
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (IsPointInRect(GetOverlayButtonRect(hWnd, 1, 3), mouseX, mouseY))
|
||||||
|
{
|
||||||
|
StartGameWithMode(currentMode);
|
||||||
|
ResetGameTimer(hWnd);
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (IsPointInRect(GetOverlayButtonRect(hWnd, 2, 3), mouseX, mouseY))
|
||||||
|
{
|
||||||
|
ReturnToMainMenu();
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (IsPointInRect(GetOverlayButtonRect(hWnd, 0, 2), mouseX, mouseY))
|
||||||
|
{
|
||||||
|
StartGameWithMode(currentMode);
|
||||||
|
ResetGameTimer(hWnd);
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (IsPointInRect(GetOverlayButtonRect(hWnd, 1, 2), mouseX, mouseY))
|
||||||
|
{
|
||||||
|
ReturnToMainMenu();
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return DefWindowProc(hWnd, message, wParam, lParam);
|
return DefWindowProc(hWnd, message, wParam, lParam);
|
||||||
}
|
}
|
||||||
|
case WM_MOUSEWHEEL:
|
||||||
|
{
|
||||||
|
int wheelDelta = GET_WHEEL_DELTA_WPARAM(wParam);
|
||||||
|
int direction = (wheelDelta > 0) ? -1 : 1;
|
||||||
|
int scrollStep = GetScrollStep(hWnd, 64);
|
||||||
|
if (currentScreen == SCREEN_RULES && helpState.currentPage != 0)
|
||||||
|
{
|
||||||
|
AdjustScrollOffset(helpScrollOffset, direction * scrollStep);
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (currentScreen == SCREEN_PLAYING && currentMode == MODE_ROGUE)
|
||||||
|
{
|
||||||
|
AdjustScrollOffset(upgradeListScrollOffset, direction * scrollStep);
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
case WM_KEYDOWN:
|
case WM_KEYDOWN:
|
||||||
if (currentScreen == SCREEN_MENU)
|
if (currentScreen == SCREEN_MENU)
|
||||||
{
|
{
|
||||||
@@ -678,10 +1193,14 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|||||||
{
|
{
|
||||||
StartGameWithMode(MODE_ROGUE);
|
StartGameWithMode(MODE_ROGUE);
|
||||||
}
|
}
|
||||||
else
|
else if (menuState.selectedIndex == 2)
|
||||||
{
|
{
|
||||||
OpenRulesScreen();
|
OpenRulesScreen();
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
OpenCreditScreen();
|
||||||
|
}
|
||||||
ResetGameTimer(hWnd);
|
ResetGameTimer(hWnd);
|
||||||
InvalidateRect(hWnd, nullptr, FALSE);
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
break;
|
break;
|
||||||
@@ -711,6 +1230,11 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|||||||
}
|
}
|
||||||
InvalidateRect(hWnd, nullptr, FALSE);
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
}
|
}
|
||||||
|
else if (helpState.currentPage == 4)
|
||||||
|
{
|
||||||
|
ChangeCreditPage(-1);
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case VK_DOWN:
|
case VK_DOWN:
|
||||||
case VK_RIGHT:
|
case VK_RIGHT:
|
||||||
@@ -725,12 +1249,18 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|||||||
}
|
}
|
||||||
InvalidateRect(hWnd, nullptr, FALSE);
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
}
|
}
|
||||||
|
else if (helpState.currentPage == 4)
|
||||||
|
{
|
||||||
|
ChangeCreditPage(1);
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case VK_RETURN:
|
case VK_RETURN:
|
||||||
case VK_SPACE:
|
case VK_SPACE:
|
||||||
if (helpState.currentPage == 0)
|
if (helpState.currentPage == 0)
|
||||||
{
|
{
|
||||||
helpState.currentPage = helpState.selectedIndex + 1;
|
helpState.currentPage = helpState.selectedIndex + 1;
|
||||||
|
helpScrollOffset = 0;
|
||||||
InvalidateRect(hWnd, nullptr, FALSE);
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -741,9 +1271,14 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|||||||
{
|
{
|
||||||
ReturnToMainMenu();
|
ReturnToMainMenu();
|
||||||
}
|
}
|
||||||
|
else if (helpState.currentPage == 4)
|
||||||
|
{
|
||||||
|
ReturnToMainMenu();
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
helpState.currentPage = 0;
|
helpState.currentPage = 0;
|
||||||
|
helpScrollOffset = 0;
|
||||||
}
|
}
|
||||||
InvalidateRect(hWnd, nullptr, FALSE);
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
break;
|
break;
|
||||||
@@ -823,11 +1358,36 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|||||||
InvalidateRect(hWnd, nullptr, FALSE);
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
break;
|
break;
|
||||||
case VK_RETURN:
|
case VK_RETURN:
|
||||||
case VK_SPACE:
|
|
||||||
ConfirmUpgradeSelection();
|
ConfirmUpgradeSelection();
|
||||||
ResetGameTimer(hWnd);
|
ResetGameTimer(hWnd);
|
||||||
InvalidateRect(hWnd, nullptr, FALSE);
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
break;
|
break;
|
||||||
|
case VK_SPACE:
|
||||||
|
if (upgradeUiState.picksRemaining > 1 && upgradeUiState.optionCount > 0)
|
||||||
|
{
|
||||||
|
bool currentlyMarked = upgradeUiState.marked[upgradeUiState.selectedIndex];
|
||||||
|
if (currentlyMarked)
|
||||||
|
{
|
||||||
|
upgradeUiState.marked[upgradeUiState.selectedIndex] = false;
|
||||||
|
if (upgradeUiState.markedCount > 0)
|
||||||
|
{
|
||||||
|
upgradeUiState.markedCount--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (upgradeUiState.markedCount < upgradeUiState.picksRemaining)
|
||||||
|
{
|
||||||
|
upgradeUiState.marked[upgradeUiState.selectedIndex] = true;
|
||||||
|
upgradeUiState.markedCount++;
|
||||||
|
}
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ConfirmUpgradeSelection();
|
||||||
|
ResetGameTimer(hWnd);
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -883,6 +1443,14 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (currentMode == MODE_ROGUE && (wParam == 'J' || wParam == 'K'))
|
||||||
|
{
|
||||||
|
int direction = (wParam == 'J') ? 1 : -1;
|
||||||
|
AdjustScrollOffset(upgradeListScrollOffset, direction * GetScrollStep(hWnd, 52));
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
switch (wParam)
|
switch (wParam)
|
||||||
{
|
{
|
||||||
case VK_LEFT:
|
case VK_LEFT:
|
||||||
@@ -911,6 +1479,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|||||||
if (!gameOverFlag)
|
if (!gameOverFlag)
|
||||||
{
|
{
|
||||||
DeleteLines();
|
DeleteLines();
|
||||||
|
CheckRogueLevelProgress();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -924,10 +1493,13 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|||||||
if (!gameOverFlag)
|
if (!gameOverFlag)
|
||||||
{
|
{
|
||||||
DeleteLines();
|
DeleteLines();
|
||||||
|
CheckRogueLevelProgress();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'C':
|
case 'C':
|
||||||
case VK_SHIFT:
|
case VK_SHIFT:
|
||||||
|
case VK_LSHIFT:
|
||||||
|
case VK_RSHIFT:
|
||||||
HoldCurrentPiece();
|
HoldCurrentPiece();
|
||||||
break;
|
break;
|
||||||
case 'Z':
|
case 'Z':
|
||||||
@@ -985,7 +1557,17 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|||||||
case WM_DESTROY:
|
case WM_DESTROY:
|
||||||
KillTimer(hWnd, GAME_TIMER_ID);
|
KillTimer(hWnd, GAME_TIMER_ID);
|
||||||
KillTimer(hWnd, EFFECT_TIMER_ID);
|
KillTimer(hWnd, EFFECT_TIMER_ID);
|
||||||
|
if (creditTimerHandle != 0)
|
||||||
|
{
|
||||||
|
timeKillEvent(creditTimerHandle);
|
||||||
|
creditTimerHandle = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
KillTimer(hWnd, CREDIT_TIMER_ID);
|
||||||
|
}
|
||||||
StopBackgroundMusic();
|
StopBackgroundMusic();
|
||||||
|
timeEndPeriod(1);
|
||||||
PostQuitMessage(0);
|
PostQuitMessage(0);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|||||||
+103
-492
@@ -13,15 +13,21 @@ bool reviveAvailable = false;
|
|||||||
int workRegion[20][10] = { 0 };
|
int workRegion[20][10] = { 0 };
|
||||||
Point point = { 0, 0 };
|
Point point = { 0, 0 };
|
||||||
Point target = { 0, 0 };
|
Point target = { 0, 0 };
|
||||||
MenuState menuState = { 0, 2 };
|
MenuState menuState = { 0, 4 };
|
||||||
HelpState helpState = { 0, 3, 0 };
|
HelpState helpState = { 0, 3, 0 };
|
||||||
|
int helpScrollOffset = 0;
|
||||||
|
int creditPageIndex = 0;
|
||||||
|
int creditAnimationTicks = 0;
|
||||||
|
int creditAnimationDirection = 0;
|
||||||
|
int upgradeListScrollOffset = 0;
|
||||||
PlayerStats classicStats = { 0, 1, 0, 0, 0 };
|
PlayerStats classicStats = { 0, 1, 0, 0, 0 };
|
||||||
PlayerStats rogueStats = { 0, 1, 0, 30, 0, 100, 100, 0 };
|
PlayerStats rogueStats = { 0, 1, 0, 30, 0, 100, 100, 0 };
|
||||||
UpgradeUiState upgradeUiState = { 0, 0, 0, 0, {} };
|
UpgradeUiState upgradeUiState = { 0, 0, 0, 0, 0, 0, {}, {} };
|
||||||
FeedbackState feedbackState = { 0, _T(""), _T("") };
|
FeedbackState feedbackState = { 0, _T(""), _T("") };
|
||||||
ClearEffectState clearEffectState = { 0, 0, 0, {} };
|
ClearEffectState clearEffectState = { 0, 0, 0, {} };
|
||||||
FloatingTextEffect floatingTextEffects[8] = {};
|
FloatingTextEffect floatingTextEffects[8] = {};
|
||||||
ParticleEffect particleEffects[96] = {};
|
ParticleEffect particleEffects[96] = {};
|
||||||
|
CellFlashEffect cellFlashEffects[64] = {};
|
||||||
int currentScreen = SCREEN_MENU;
|
int currentScreen = SCREEN_MENU;
|
||||||
int currentMode = MODE_CLASSIC;
|
int currentMode = MODE_CLASSIC;
|
||||||
int currentFallInterval = 500;
|
int currentFallInterval = 500;
|
||||||
@@ -34,10 +40,6 @@ bool currentPieceIsCross = false;
|
|||||||
bool currentPieceIsRainbow = false;
|
bool currentPieceIsRainbow = false;
|
||||||
Point pendingChainBombCenter = { 0, 0 };
|
Point pendingChainBombCenter = { 0, 0 };
|
||||||
bool pendingChainBombFollowup = false;
|
bool pendingChainBombFollowup = false;
|
||||||
static int pendingLineClearEffectTicks = 0;
|
|
||||||
static int pendingLineClearEffectRows[8] = {};
|
|
||||||
static int pendingLineClearEffectRowCount = 0;
|
|
||||||
static int pendingLineClearEffectLineCount = 0;
|
|
||||||
|
|
||||||
int bricks[7][4][4][4] =
|
int bricks[7][4][4][4] =
|
||||||
{
|
{
|
||||||
@@ -167,369 +169,6 @@ Point GetSpawnPoint(int brickType)
|
|||||||
return spawnPoint;
|
return spawnPoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ResetPlayerStats(PlayerStats& stats, bool useRogueRules)
|
|
||||||
{
|
|
||||||
stats.score = 0;
|
|
||||||
stats.level = 1;
|
|
||||||
stats.exp = 0;
|
|
||||||
stats.requiredExp = useRogueRules ? 10 : 0;
|
|
||||||
stats.totalLinesCleared = 0;
|
|
||||||
stats.scoreMultiplierPercent = 100;
|
|
||||||
stats.expMultiplierPercent = 100;
|
|
||||||
stats.slowFallStacks = 0;
|
|
||||||
stats.comboBonusStacks = 0;
|
|
||||||
stats.comboChain = 0;
|
|
||||||
stats.previewCount = 1;
|
|
||||||
stats.lastChanceCount = 0;
|
|
||||||
stats.scoreUpgradeLevel = 0;
|
|
||||||
stats.expUpgradeLevel = 0;
|
|
||||||
stats.previewUpgradeLevel = 0;
|
|
||||||
stats.lastChanceUpgradeLevel = 0;
|
|
||||||
stats.holdUnlocked = 0;
|
|
||||||
stats.pressureReliefLevel = 0;
|
|
||||||
stats.sweeperLevel = 0;
|
|
||||||
stats.sweeperCharge = 0;
|
|
||||||
stats.explosiveLevel = 0;
|
|
||||||
stats.explosivePieceCounter = 0;
|
|
||||||
stats.chainBlastLevel = 0;
|
|
||||||
stats.chainBombLevel = 0;
|
|
||||||
stats.laserLevel = 0;
|
|
||||||
stats.thunderTetrisLevel = 0;
|
|
||||||
stats.thunderLaserLevel = 0;
|
|
||||||
stats.feverLevel = 0;
|
|
||||||
stats.rageStackLevel = 0;
|
|
||||||
stats.infiniteFeverLevel = 0;
|
|
||||||
stats.feverLineCharge = 0;
|
|
||||||
stats.feverTicks = 0;
|
|
||||||
stats.screenBombLevel = 0;
|
|
||||||
stats.screenBombCharge = 0;
|
|
||||||
stats.screenBombCount = 0;
|
|
||||||
stats.terminalClearLevel = 0;
|
|
||||||
stats.dualChoiceLevel = 0;
|
|
||||||
stats.destinyWheelLevel = 0;
|
|
||||||
stats.perfectRotateLevel = 0;
|
|
||||||
stats.timeDilationLevel = 0;
|
|
||||||
stats.timeDilationTicks = 0;
|
|
||||||
stats.highPressureLevel = 0;
|
|
||||||
stats.tetrisGambleLevel = 0;
|
|
||||||
stats.extremePlayerLevel = 0;
|
|
||||||
stats.extremeSlowTicks = 0;
|
|
||||||
stats.extremeDangerTicks = 30;
|
|
||||||
stats.extremeDangerLevel = 0;
|
|
||||||
stats.upgradeShockwaveLevel = 0;
|
|
||||||
stats.evolutionImpactLevel = 0;
|
|
||||||
stats.controlMasterLevel = 0;
|
|
||||||
stats.holdSlowTicks = 0;
|
|
||||||
stats.blockStormLevel = 0;
|
|
||||||
stats.blockStormPiecesRemaining = 0;
|
|
||||||
stats.blackHoleLevel = 0;
|
|
||||||
stats.blackHoleCharges = 0;
|
|
||||||
stats.reshapeLevel = 0;
|
|
||||||
stats.reshapeCharges = 0;
|
|
||||||
stats.rainbowPieceLevel = 0;
|
|
||||||
stats.voidCoreLevel = 0;
|
|
||||||
stats.pendingRainbowPieceCount = 0;
|
|
||||||
stats.stableStructureLevel = 0;
|
|
||||||
stats.doubleGrowthLevel = 0;
|
|
||||||
stats.gamblerLevel = 0;
|
|
||||||
stats.difficultyElapsedMs = 0;
|
|
||||||
stats.difficultyLevel = 0;
|
|
||||||
stats.lockedRows = 0;
|
|
||||||
for (int i = 0; i < 7; i++)
|
|
||||||
{
|
|
||||||
stats.pieceTuningLevels[i] = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void SetFeedbackMessage(const TCHAR* title, const TCHAR* detail, int ticks)
|
|
||||||
{
|
|
||||||
feedbackState.visibleTicks = ticks;
|
|
||||||
lstrcpyn(feedbackState.title, title, sizeof(feedbackState.title) / sizeof(TCHAR));
|
|
||||||
lstrcpyn(feedbackState.detail, detail, sizeof(feedbackState.detail) / sizeof(TCHAR));
|
|
||||||
}
|
|
||||||
|
|
||||||
void ResetVisualEffects()
|
|
||||||
{
|
|
||||||
clearEffectState.ticks = 0;
|
|
||||||
clearEffectState.totalTicks = 0;
|
|
||||||
clearEffectState.rowCount = 0;
|
|
||||||
|
|
||||||
for (int i = 0; i < 8; i++)
|
|
||||||
{
|
|
||||||
floatingTextEffects[i].ticks = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < 96; i++)
|
|
||||||
{
|
|
||||||
particleEffects[i].ticks = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool TickVisualEffects()
|
|
||||||
{
|
|
||||||
bool active = false;
|
|
||||||
|
|
||||||
if (clearEffectState.ticks > 0)
|
|
||||||
{
|
|
||||||
clearEffectState.ticks--;
|
|
||||||
active = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < 8; i++)
|
|
||||||
{
|
|
||||||
if (floatingTextEffects[i].ticks > 0)
|
|
||||||
{
|
|
||||||
floatingTextEffects[i].ticks--;
|
|
||||||
active = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < 96; i++)
|
|
||||||
{
|
|
||||||
if (particleEffects[i].ticks > 0)
|
|
||||||
{
|
|
||||||
particleEffects[i].ticks--;
|
|
||||||
active = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return active;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void AddFloatingText(int boardX, int boardY, const TCHAR* text, COLORREF color)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < 8; i++)
|
|
||||||
{
|
|
||||||
if (floatingTextEffects[i].ticks <= 0)
|
|
||||||
{
|
|
||||||
floatingTextEffects[i].ticks = 22;
|
|
||||||
floatingTextEffects[i].totalTicks = 22;
|
|
||||||
floatingTextEffects[i].boardX = boardX;
|
|
||||||
floatingTextEffects[i].boardY = boardY;
|
|
||||||
floatingTextEffects[i].color = color;
|
|
||||||
lstrcpyn(floatingTextEffects[i].text, text, sizeof(floatingTextEffects[i].text) / sizeof(TCHAR));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void AddParticle(int boardX, int boardY, int velocityX, int velocityY, int size, COLORREF color)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < 96; i++)
|
|
||||||
{
|
|
||||||
if (particleEffects[i].ticks <= 0)
|
|
||||||
{
|
|
||||||
particleEffects[i].ticks = 12 + rand() % 7;
|
|
||||||
particleEffects[i].totalTicks = particleEffects[i].ticks;
|
|
||||||
particleEffects[i].boardX = boardX;
|
|
||||||
particleEffects[i].boardY = boardY;
|
|
||||||
particleEffects[i].velocityX = velocityX;
|
|
||||||
particleEffects[i].velocityY = velocityY;
|
|
||||||
particleEffects[i].size = size;
|
|
||||||
particleEffects[i].color = color;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void AddBurstParticles(int boardX, int boardY, COLORREF baseColor, bool strongBurst)
|
|
||||||
{
|
|
||||||
int burstCount = strongBurst ? 5 : 3;
|
|
||||||
for (int i = 0; i < burstCount; i++)
|
|
||||||
{
|
|
||||||
int angleSeed = rand() % 8;
|
|
||||||
int speed = strongBurst ? (9 + rand() % 9) : (6 + rand() % 7);
|
|
||||||
int velocityX = 0;
|
|
||||||
int velocityY = 0;
|
|
||||||
|
|
||||||
switch (angleSeed)
|
|
||||||
{
|
|
||||||
case 0:
|
|
||||||
velocityX = speed;
|
|
||||||
velocityY = -rand() % 4;
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
velocityX = -speed;
|
|
||||||
velocityY = -rand() % 4;
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
velocityX = (rand() % 5) - 2;
|
|
||||||
velocityY = -speed;
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
velocityX = (rand() % 5) - 2;
|
|
||||||
velocityY = speed / 2;
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
velocityX = speed;
|
|
||||||
velocityY = -speed;
|
|
||||||
break;
|
|
||||||
case 5:
|
|
||||||
velocityX = -speed;
|
|
||||||
velocityY = -speed;
|
|
||||||
break;
|
|
||||||
case 6:
|
|
||||||
velocityX = speed;
|
|
||||||
velocityY = speed / 3;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
velocityX = -speed;
|
|
||||||
velocityY = speed / 3;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
velocityX += (rand() % 7) - 3;
|
|
||||||
velocityY += (rand() % 7) - 3;
|
|
||||||
|
|
||||||
COLORREF color = (i % 3 == 0) ? RGB(255, 248, 220) : baseColor;
|
|
||||||
AddParticle(
|
|
||||||
boardX + (rand() % 31) - 15,
|
|
||||||
boardY + (rand() % 31) - 15,
|
|
||||||
velocityX,
|
|
||||||
velocityY,
|
|
||||||
strongBurst ? (4 + rand() % 5) : (3 + rand() % 4),
|
|
||||||
color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void QueueLineClearEffect(const int* rows, int rowCount, int linesCleared)
|
|
||||||
{
|
|
||||||
if (rows == nullptr || rowCount <= 0 || linesCleared <= 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rowCount > 8)
|
|
||||||
{
|
|
||||||
rowCount = 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
pendingLineClearEffectTicks = 1;
|
|
||||||
pendingLineClearEffectRowCount = rowCount;
|
|
||||||
pendingLineClearEffectLineCount = linesCleared;
|
|
||||||
for (int i = 0; i < rowCount; i++)
|
|
||||||
{
|
|
||||||
pendingLineClearEffectRows[i] = rows[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void PlayPendingLineClearEffect()
|
|
||||||
{
|
|
||||||
if (pendingLineClearEffectTicks <= 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
pendingLineClearEffectTicks = 0;
|
|
||||||
TriggerLineClearEffect(
|
|
||||||
pendingLineClearEffectRows,
|
|
||||||
pendingLineClearEffectRowCount,
|
|
||||||
pendingLineClearEffectLineCount);
|
|
||||||
pendingLineClearEffectRowCount = 0;
|
|
||||||
pendingLineClearEffectLineCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void TriggerLineClearEffect(const int* rows, int rowCount, int linesCleared)
|
|
||||||
{
|
|
||||||
if (rows == nullptr || rowCount <= 0 || linesCleared <= 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rowCount > 8)
|
|
||||||
{
|
|
||||||
rowCount = 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
clearEffectState.ticks = 16;
|
|
||||||
clearEffectState.totalTicks = 16;
|
|
||||||
clearEffectState.rowCount = rowCount;
|
|
||||||
|
|
||||||
int rowSum = 0;
|
|
||||||
for (int i = 0; i < rowCount; i++)
|
|
||||||
{
|
|
||||||
clearEffectState.rows[i] = rows[i];
|
|
||||||
rowSum += rows[i];
|
|
||||||
for (int x = 0; x < nGameWidth; x++)
|
|
||||||
{
|
|
||||||
COLORREF particleColor = BrickColor[(x + rows[i]) % 7];
|
|
||||||
int centerX = x * 100 + 50;
|
|
||||||
int centerY = rows[i] * 100 + 50;
|
|
||||||
AddBurstParticles(centerX, centerY, particleColor, linesCleared >= 4);
|
|
||||||
|
|
||||||
if (linesCleared >= 4)
|
|
||||||
{
|
|
||||||
AddParticle(
|
|
||||||
centerX,
|
|
||||||
centerY,
|
|
||||||
((x < nGameWidth / 2) ? -1 : 1) * (16 + rand() % 12),
|
|
||||||
-16 - rand() % 10,
|
|
||||||
4 + rand() % 3,
|
|
||||||
RGB(255, 238, 120));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TCHAR text[64];
|
|
||||||
if (linesCleared >= 4)
|
|
||||||
{
|
|
||||||
_stprintf_s(text, _T("TETRIS"));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_stprintf_s(text, _T("%d LINE%s"), linesCleared, linesCleared > 1 ? _T("S") : _T(""));
|
|
||||||
}
|
|
||||||
AddFloatingText(nGameWidth * 50, (rowSum * 100 / rowCount) - 20, text, linesCleared >= 4 ? RGB(255, 232, 120) : RGB(255, 250, 252));
|
|
||||||
}
|
|
||||||
|
|
||||||
void TriggerCellClearEffect(const Point* cells, int cellCount, bool strongBurst)
|
|
||||||
{
|
|
||||||
if (cells == nullptr || cellCount <= 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < cellCount; i++)
|
|
||||||
{
|
|
||||||
if (cells[i].x < 0 || cells[i].x >= nGameWidth || cells[i].y < 0 || cells[i].y >= nGameHeight)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
COLORREF particleColor = BrickColor[(cells[i].x + cells[i].y) % 7];
|
|
||||||
AddBurstParticles(cells[i].x * 100 + 50, cells[i].y * 100 + 50, particleColor, strongBurst);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool IsPiecePlacementValid(int pieceType, int pieceState, Point position)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < 4; i++)
|
|
||||||
{
|
|
||||||
for (int j = 0; j < 4; j++)
|
|
||||||
{
|
|
||||||
if (bricks[pieceType][pieceState][i][j] == 0)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
int checkY = position.y + i;
|
|
||||||
int checkX = position.x + j;
|
|
||||||
|
|
||||||
if (checkX < 0 || checkX >= nGameWidth || checkY >= GetRoguePlayableHeight())
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checkY >= 0 && workRegion[checkY][checkX] != 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 判断当前方块是否可以继续向下移动。
|
* @brief 判断当前方块是否可以继续向下移动。
|
||||||
*
|
*
|
||||||
@@ -650,13 +289,6 @@ bool CanMoveRight()
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool TryRotateWithOffset(int nextState, int offsetX)
|
|
||||||
{
|
|
||||||
Point rotatedPoint = point;
|
|
||||||
rotatedPoint.x += offsetX;
|
|
||||||
return IsPiecePlacementValid(type, nextState, rotatedPoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief 将当前活动方块向下移动一格。
|
* @brief 将当前活动方块向下移动一格。
|
||||||
*
|
*
|
||||||
@@ -755,9 +387,10 @@ void DropDown()
|
|||||||
void Fixing()
|
void Fixing()
|
||||||
{
|
{
|
||||||
bool overflowTop = false;
|
bool overflowTop = false;
|
||||||
|
Point fixedCells[4] = {};
|
||||||
|
int fixedCellCount = 0;
|
||||||
Point explosiveCells[4] = {};
|
Point explosiveCells[4] = {};
|
||||||
int explosiveCellCount = 0;
|
int explosiveCellCount = 0;
|
||||||
int rainbowFilledCount = 0;
|
|
||||||
pendingChainBombFollowup = false;
|
pendingChainBombFollowup = false;
|
||||||
|
|
||||||
for (int i = 0; i < 4; i++)
|
for (int i = 0; i < 4; i++)
|
||||||
@@ -779,6 +412,12 @@ void Fixing()
|
|||||||
if (fixY >= 0 && fixY < GetRoguePlayableHeight() && fixX >= 0 && fixX < nGameWidth)
|
if (fixY >= 0 && fixY < GetRoguePlayableHeight() && fixX >= 0 && fixX < nGameWidth)
|
||||||
{
|
{
|
||||||
workRegion[fixY][fixX] = currentPieceIsRainbow ? 8 : bricks[type][state][i][j];
|
workRegion[fixY][fixX] = currentPieceIsRainbow ? 8 : bricks[type][state][i][j];
|
||||||
|
if (fixedCellCount < 4)
|
||||||
|
{
|
||||||
|
fixedCells[fixedCellCount].x = fixX;
|
||||||
|
fixedCells[fixedCellCount].y = fixY;
|
||||||
|
fixedCellCount++;
|
||||||
|
}
|
||||||
if (currentPieceIsExplosive && explosiveCellCount < 4)
|
if (currentPieceIsExplosive && explosiveCellCount < 4)
|
||||||
{
|
{
|
||||||
explosiveCells[explosiveCellCount].x = fixX;
|
explosiveCells[explosiveCellCount].x = fixX;
|
||||||
@@ -792,13 +431,64 @@ void Fixing()
|
|||||||
|
|
||||||
if (!overflowTop && currentPieceIsRainbow)
|
if (!overflowTop && currentPieceIsRainbow)
|
||||||
{
|
{
|
||||||
rainbowFilledCount = TriggerRainbowRowCompletion(point.y, point.y + 3);
|
int rainbowAnchorRow = point.y + 1;
|
||||||
if (rainbowFilledCount > 0)
|
if (fixedCellCount > 0)
|
||||||
{
|
{
|
||||||
TCHAR rainbowDetail[128];
|
int ySum = 0;
|
||||||
_stprintf_s(rainbowDetail, _T("彩虹能量补齐 %d 个缺口,消行机会扩大。"), rainbowFilledCount);
|
for (int i = 0; i < fixedCellCount; i++)
|
||||||
SetFeedbackMessage(_T("彩虹方块"), rainbowDetail, 10);
|
{
|
||||||
|
ySum += fixedCells[i].y;
|
||||||
}
|
}
|
||||||
|
rainbowAnchorRow = (ySum + fixedCellCount / 2) / fixedCellCount;
|
||||||
|
}
|
||||||
|
if (rainbowAnchorRow < 0)
|
||||||
|
{
|
||||||
|
rainbowAnchorRow = 0;
|
||||||
|
}
|
||||||
|
if (rainbowAnchorRow >= GetRoguePlayableHeight())
|
||||||
|
{
|
||||||
|
rainbowAnchorRow = GetRoguePlayableHeight() - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int rainbowRecoloredCount = 0;
|
||||||
|
int rainbowClearedCount = TriggerRainbowColorShift(rainbowAnchorRow, point.y, point.y + 3, rainbowRecoloredCount);
|
||||||
|
int rainbowScore = 0;
|
||||||
|
int rainbowExp = 0;
|
||||||
|
int voidClearedCount = 0;
|
||||||
|
int voidScore = 0;
|
||||||
|
int voidExp = 0;
|
||||||
|
if (currentMode == MODE_ROGUE && rainbowClearedCount > 0)
|
||||||
|
{
|
||||||
|
AwardRogueSkillClearRewards(rainbowClearedCount, rainbowScore, rainbowExp, false);
|
||||||
|
if (rogueStats.voidCoreLevel > 0)
|
||||||
|
{
|
||||||
|
voidClearedCount = TriggerMiniBlackHole(5);
|
||||||
|
AwardRogueSkillClearRewards(voidClearedCount, voidScore, voidExp, false);
|
||||||
|
}
|
||||||
|
ApplyBoardGravity();
|
||||||
|
}
|
||||||
|
|
||||||
|
TCHAR rainbowDetail[128];
|
||||||
|
if (voidClearedCount > 0)
|
||||||
|
{
|
||||||
|
_stprintf_s(
|
||||||
|
rainbowDetail,
|
||||||
|
_T("第 %d 行清 %d 格,染色 %d 格,虚空追加 %d 格"),
|
||||||
|
rainbowAnchorRow + 1,
|
||||||
|
rainbowClearedCount,
|
||||||
|
rainbowRecoloredCount,
|
||||||
|
voidClearedCount);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_stprintf_s(
|
||||||
|
rainbowDetail,
|
||||||
|
_T("第 %d 行清除主色 %d 格,覆盖行染色 %d 格。"),
|
||||||
|
rainbowAnchorRow + 1,
|
||||||
|
rainbowClearedCount,
|
||||||
|
rainbowRecoloredCount);
|
||||||
|
}
|
||||||
|
SetFeedbackMessage(_T("彩虹方块"), rainbowDetail, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (overflowTop)
|
if (overflowTop)
|
||||||
@@ -876,6 +566,15 @@ void Fixing()
|
|||||||
if (currentPieceIsLaser)
|
if (currentPieceIsLaser)
|
||||||
{
|
{
|
||||||
int laserColumn = point.x + 1;
|
int laserColumn = point.x + 1;
|
||||||
|
if (fixedCellCount > 0)
|
||||||
|
{
|
||||||
|
int xSum = 0;
|
||||||
|
for (int i = 0; i < fixedCellCount; i++)
|
||||||
|
{
|
||||||
|
xSum += fixedCells[i].x;
|
||||||
|
}
|
||||||
|
laserColumn = (xSum + fixedCellCount / 2) / fixedCellCount;
|
||||||
|
}
|
||||||
if (laserColumn < 0)
|
if (laserColumn < 0)
|
||||||
{
|
{
|
||||||
laserColumn = 0;
|
laserColumn = 0;
|
||||||
@@ -894,7 +593,7 @@ void Fixing()
|
|||||||
ApplyBoardGravity();
|
ApplyBoardGravity();
|
||||||
|
|
||||||
TCHAR laserDetail[128];
|
TCHAR laserDetail[128];
|
||||||
_stprintf_s(laserDetail, _T("激光贯穿一列,清除 %d 格 +%d 分 +%d EXP"), laserCellsCleared, laserScore, laserExp);
|
_stprintf_s(laserDetail, _T("激光贯穿第 %d 列,清除 %d 格 +%d 分 +%d EXP"), laserColumn + 1, laserCellsCleared, laserScore, laserExp);
|
||||||
SetFeedbackMessage(_T("棱镜激光"), laserDetail, 12);
|
SetFeedbackMessage(_T("棱镜激光"), laserDetail, 12);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -903,6 +602,18 @@ void Fixing()
|
|||||||
{
|
{
|
||||||
int crossRow = point.y + 1;
|
int crossRow = point.y + 1;
|
||||||
int crossColumn = point.x + 1;
|
int crossColumn = point.x + 1;
|
||||||
|
if (fixedCellCount > 0)
|
||||||
|
{
|
||||||
|
int xSum = 0;
|
||||||
|
int ySum = 0;
|
||||||
|
for (int i = 0; i < fixedCellCount; i++)
|
||||||
|
{
|
||||||
|
xSum += fixedCells[i].x;
|
||||||
|
ySum += fixedCells[i].y;
|
||||||
|
}
|
||||||
|
crossColumn = (xSum + fixedCellCount / 2) / fixedCellCount;
|
||||||
|
crossRow = (ySum + fixedCellCount / 2) / fixedCellCount;
|
||||||
|
}
|
||||||
if (crossRow < 0)
|
if (crossRow < 0)
|
||||||
{
|
{
|
||||||
crossRow = 0;
|
crossRow = 0;
|
||||||
@@ -921,7 +632,7 @@ void Fixing()
|
|||||||
}
|
}
|
||||||
|
|
||||||
int crossCellsCleared = ClearRowAt(crossRow);
|
int crossCellsCleared = ClearRowAt(crossRow);
|
||||||
int columnCellsCleared = ClearColumnAt(crossColumn);
|
int columnCellsCleared = ClearColumnAtWithColor(crossColumn, RGB(196, 255, 132));
|
||||||
if (workRegion[crossRow][crossColumn] == 0 && columnCellsCleared > 0)
|
if (workRegion[crossRow][crossColumn] == 0 && columnCellsCleared > 0)
|
||||||
{
|
{
|
||||||
// center cell may already be counted by row clear
|
// center cell may already be counted by row clear
|
||||||
@@ -936,12 +647,12 @@ void Fixing()
|
|||||||
ApplyBoardGravity();
|
ApplyBoardGravity();
|
||||||
|
|
||||||
TCHAR crossDetail[128];
|
TCHAR crossDetail[128];
|
||||||
_stprintf_s(crossDetail, _T("十字冲击清除 %d 格 +%d 分 +%d EXP"), totalCrossCleared, crossScore, crossExp);
|
_stprintf_s(crossDetail, _T("十字冲击第 %d 行 / 第 %d 列,清除 %d 格 +%d 分 +%d EXP"), crossRow + 1, crossColumn + 1, totalCrossCleared, crossScore, crossExp);
|
||||||
SetFeedbackMessage(_T("十字方块"), crossDetail, 12);
|
SetFeedbackMessage(_T("十字方块"), crossDetail, 12);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (TryStabilizeBoard() > 0)
|
if (!currentPieceIsRainbow && TryStabilizeBoard() > 0)
|
||||||
{
|
{
|
||||||
SetFeedbackMessage(_T("稳定结构"), _T("附近空洞被自动填补,阵型更加稳固。"), 10);
|
SetFeedbackMessage(_T("稳定结构"), _T("附近空洞被自动填补,阵型更加稳固。"), 10);
|
||||||
}
|
}
|
||||||
@@ -998,7 +709,6 @@ void DeleteOneLine(int number)
|
|||||||
int DeleteLines()
|
int DeleteLines()
|
||||||
{
|
{
|
||||||
int clearedLines = 0;
|
int clearedLines = 0;
|
||||||
bool clearedWithRainbow = false;
|
|
||||||
int clearedRows[8] = {};
|
int clearedRows[8] = {};
|
||||||
int clearedRowCount = 0;
|
int clearedRowCount = 0;
|
||||||
|
|
||||||
@@ -1024,14 +734,6 @@ int DeleteLines()
|
|||||||
clearedRowCount++;
|
clearedRowCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int j = 0; j < nGameWidth; j++)
|
|
||||||
{
|
|
||||||
if (IsRainbowBoardCell(workRegion[i][j]))
|
|
||||||
{
|
|
||||||
clearedWithRainbow = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DeleteOneLine(i);
|
DeleteOneLine(i);
|
||||||
clearedLines++;
|
clearedLines++;
|
||||||
i++;
|
i++;
|
||||||
@@ -1039,6 +741,11 @@ int DeleteLines()
|
|||||||
}
|
}
|
||||||
|
|
||||||
ApplyLineClearResult(clearedLines);
|
ApplyLineClearResult(clearedLines);
|
||||||
|
if (currentMode == MODE_CLASSIC && clearedLines > 0)
|
||||||
|
{
|
||||||
|
ApplyBoardGravity();
|
||||||
|
}
|
||||||
|
|
||||||
if (currentScreen == SCREEN_UPGRADE)
|
if (currentScreen == SCREEN_UPGRADE)
|
||||||
{
|
{
|
||||||
QueueLineClearEffect(clearedRows, clearedRowCount, clearedLines);
|
QueueLineClearEffect(clearedRows, clearedRowCount, clearedLines);
|
||||||
@@ -1097,22 +804,6 @@ int DeleteLines()
|
|||||||
pendingChainBombFollowup = false;
|
pendingChainBombFollowup = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentMode == MODE_ROGUE && clearedWithRainbow && rogueStats.voidCoreLevel > 0)
|
|
||||||
{
|
|
||||||
int miniBlackHoleCleared = TriggerMiniBlackHole(5);
|
|
||||||
if (miniBlackHoleCleared > 0)
|
|
||||||
{
|
|
||||||
int miniScore = 0;
|
|
||||||
int miniExp = 0;
|
|
||||||
AwardRogueSkillClearRewards(miniBlackHoleCleared, miniScore, miniExp, false);
|
|
||||||
ApplyBoardGravity();
|
|
||||||
|
|
||||||
TCHAR miniDetail[128];
|
|
||||||
_stprintf_s(miniDetail, _T("彩虹消行撕开小型黑洞,清除 %d 格 +%d 分 +%d EXP"), miniBlackHoleCleared, miniScore, miniExp);
|
|
||||||
SetFeedbackMessage(_T("虚空核心"), miniDetail, 12);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return clearedLines;
|
return clearedLines;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1167,11 +858,7 @@ void Restart()
|
|||||||
|
|
||||||
ResetPlayerStats(classicStats, false);
|
ResetPlayerStats(classicStats, false);
|
||||||
ResetPlayerStats(rogueStats, true);
|
ResetPlayerStats(rogueStats, true);
|
||||||
upgradeUiState.selectedIndex = 0;
|
ResetUpgradeUiState();
|
||||||
upgradeUiState.optionCount = 0;
|
|
||||||
upgradeUiState.pendingCount = 0;
|
|
||||||
upgradeUiState.totalChosenCount = 0;
|
|
||||||
upgradeUiState.picksRemaining = 0;
|
|
||||||
feedbackState.visibleTicks = 0;
|
feedbackState.visibleTicks = 0;
|
||||||
feedbackState.title[0] = _T('\0');
|
feedbackState.title[0] = _T('\0');
|
||||||
feedbackState.detail[0] = _T('\0');
|
feedbackState.detail[0] = _T('\0');
|
||||||
@@ -1197,79 +884,3 @@ void Restart()
|
|||||||
ComputeTarget();
|
ComputeTarget();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ReviveAfterVideo()
|
|
||||||
{
|
|
||||||
if (!gameOverFlag || !reviveAvailable)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
reviveAvailable = false;
|
|
||||||
gameOverFlag = false;
|
|
||||||
suspendFlag = false;
|
|
||||||
currentScreen = SCREEN_PLAYING;
|
|
||||||
|
|
||||||
int playableHeight = GetRoguePlayableHeight();
|
|
||||||
int rowsToClear = playableHeight / 3;
|
|
||||||
if (rowsToClear < 5)
|
|
||||||
{
|
|
||||||
rowsToClear = 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int y = 0; y < rowsToClear && y < playableHeight; y++)
|
|
||||||
{
|
|
||||||
for (int x = 0; x < nGameWidth; x++)
|
|
||||||
{
|
|
||||||
workRegion[y][x] = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type = ConsumeNextType();
|
|
||||||
nType = nextTypes[0];
|
|
||||||
state = 0;
|
|
||||||
holdUsedThisTurn = false;
|
|
||||||
RollCurrentPieceSpecialFlags(true);
|
|
||||||
point = GetSpawnPoint(type);
|
|
||||||
target = point;
|
|
||||||
ComputeTarget();
|
|
||||||
|
|
||||||
SetFeedbackMessage(_T("复活成功"), _T("已清理顶部空间,本局复活机会已用完。"), 14);
|
|
||||||
}
|
|
||||||
|
|
||||||
void StartGameWithMode(int mode)
|
|
||||||
{
|
|
||||||
currentMode = mode;
|
|
||||||
currentScreen = SCREEN_PLAYING;
|
|
||||||
Restart();
|
|
||||||
currentFallInterval = (currentMode == MODE_ROGUE) ? GetRogueFallInterval() : 500;
|
|
||||||
tScore = (currentMode == MODE_CLASSIC) ? classicStats.score : rogueStats.score;
|
|
||||||
}
|
|
||||||
|
|
||||||
void ReturnToMainMenu()
|
|
||||||
{
|
|
||||||
currentScreen = SCREEN_MENU;
|
|
||||||
suspendFlag = false;
|
|
||||||
gameOverFlag = false;
|
|
||||||
ResetVisualEffects();
|
|
||||||
ResetPendingRogueVisualEvents();
|
|
||||||
pendingLineClearEffectTicks = 0;
|
|
||||||
pendingLineClearEffectRowCount = 0;
|
|
||||||
pendingLineClearEffectLineCount = 0;
|
|
||||||
menuState.optionCount = 3;
|
|
||||||
upgradeUiState.pendingCount = 0;
|
|
||||||
upgradeUiState.picksRemaining = 0;
|
|
||||||
|
|
||||||
if (menuState.selectedIndex < 0 || menuState.selectedIndex >= menuState.optionCount)
|
|
||||||
{
|
|
||||||
menuState.selectedIndex = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void OpenRulesScreen()
|
|
||||||
{
|
|
||||||
currentScreen = SCREEN_RULES;
|
|
||||||
suspendFlag = false;
|
|
||||||
helpState.selectedIndex = 0;
|
|
||||||
helpState.optionCount = 3;
|
|
||||||
helpState.currentPage = 0;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,623 @@
|
|||||||
|
#include "stdafx.h"
|
||||||
|
#include "TetrisLogicInternal.h"
|
||||||
|
|
||||||
|
int pendingLineClearEffectTicks = 0;
|
||||||
|
int pendingLineClearEffectRows[8] = {};
|
||||||
|
int pendingLineClearEffectRowCount = 0;
|
||||||
|
int pendingLineClearEffectLineCount = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 重置经典或 Rogue 模式使用的玩家统计数据。
|
||||||
|
*/
|
||||||
|
void ResetPlayerStats(PlayerStats& stats, bool useRogueRules)
|
||||||
|
{
|
||||||
|
stats.score = 0;
|
||||||
|
stats.level = 1;
|
||||||
|
stats.exp = 0;
|
||||||
|
stats.requiredExp = useRogueRules ? 10 : 0;
|
||||||
|
stats.totalLinesCleared = 0;
|
||||||
|
stats.scoreMultiplierPercent = 100;
|
||||||
|
stats.expMultiplierPercent = 100;
|
||||||
|
stats.slowFallStacks = 0;
|
||||||
|
stats.comboBonusStacks = 0;
|
||||||
|
stats.comboChain = 0;
|
||||||
|
stats.previewCount = 1;
|
||||||
|
stats.lastChanceCount = 0;
|
||||||
|
stats.scoreUpgradeLevel = 0;
|
||||||
|
stats.expUpgradeLevel = 0;
|
||||||
|
stats.previewUpgradeLevel = 0;
|
||||||
|
stats.lastChanceUpgradeLevel = 0;
|
||||||
|
stats.holdUnlocked = 0;
|
||||||
|
stats.pressureReliefLevel = 0;
|
||||||
|
stats.sweeperLevel = 0;
|
||||||
|
stats.sweeperCharge = 0;
|
||||||
|
stats.explosiveLevel = 0;
|
||||||
|
stats.explosivePieceCounter = 0;
|
||||||
|
stats.chainBlastLevel = 0;
|
||||||
|
stats.chainBombLevel = 0;
|
||||||
|
stats.laserLevel = 0;
|
||||||
|
stats.thunderTetrisLevel = 0;
|
||||||
|
stats.thunderLaserLevel = 0;
|
||||||
|
stats.feverLevel = 0;
|
||||||
|
stats.rageStackLevel = 0;
|
||||||
|
stats.infiniteFeverLevel = 0;
|
||||||
|
stats.feverLineCharge = 0;
|
||||||
|
stats.feverTicks = 0;
|
||||||
|
stats.screenBombLevel = 0;
|
||||||
|
stats.screenBombCharge = 0;
|
||||||
|
stats.screenBombCount = 0;
|
||||||
|
stats.terminalClearLevel = 0;
|
||||||
|
stats.dualChoiceLevel = 0;
|
||||||
|
stats.destinyWheelLevel = 0;
|
||||||
|
stats.perfectRotateLevel = 0;
|
||||||
|
stats.timeDilationLevel = 0;
|
||||||
|
stats.timeDilationTicks = 0;
|
||||||
|
stats.highPressureLevel = 0;
|
||||||
|
stats.tetrisGambleLevel = 0;
|
||||||
|
stats.extremePlayerLevel = 0;
|
||||||
|
stats.extremeSlowTicks = 0;
|
||||||
|
stats.extremeDangerTicks = 30;
|
||||||
|
stats.extremeDangerLevel = 0;
|
||||||
|
stats.upgradeShockwaveLevel = 0;
|
||||||
|
stats.evolutionImpactLevel = 0;
|
||||||
|
stats.controlMasterLevel = 0;
|
||||||
|
stats.holdSlowTicks = 0;
|
||||||
|
stats.blockStormLevel = 0;
|
||||||
|
stats.blockStormPiecesRemaining = 0;
|
||||||
|
stats.blackHoleLevel = 0;
|
||||||
|
stats.blackHoleCharges = 0;
|
||||||
|
stats.reshapeLevel = 0;
|
||||||
|
stats.reshapeCharges = 0;
|
||||||
|
stats.rainbowPieceLevel = 0;
|
||||||
|
stats.voidCoreLevel = 0;
|
||||||
|
stats.pendingRainbowPieceCount = 0;
|
||||||
|
stats.stableStructureLevel = 0;
|
||||||
|
stats.doubleGrowthLevel = 0;
|
||||||
|
stats.gamblerLevel = 0;
|
||||||
|
stats.difficultyElapsedMs = 0;
|
||||||
|
stats.difficultyLevel = 0;
|
||||||
|
stats.lockedRows = 0;
|
||||||
|
for (int i = 0; i < 7; i++)
|
||||||
|
{
|
||||||
|
stats.pieceTuningLevels[i] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置界面右侧显示的即时反馈标题、内容和持续时间。
|
||||||
|
*/
|
||||||
|
void SetFeedbackMessage(const TCHAR* title, const TCHAR* detail, int ticks)
|
||||||
|
{
|
||||||
|
feedbackState.visibleTicks = ticks;
|
||||||
|
lstrcpyn(feedbackState.title, title, sizeof(feedbackState.title) / sizeof(TCHAR));
|
||||||
|
lstrcpyn(feedbackState.detail, detail, sizeof(feedbackState.detail) / sizeof(TCHAR));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 清空所有消行、浮动文字和粒子视觉效果。
|
||||||
|
*/
|
||||||
|
void ResetVisualEffects()
|
||||||
|
{
|
||||||
|
clearEffectState.ticks = 0;
|
||||||
|
clearEffectState.totalTicks = 0;
|
||||||
|
clearEffectState.rowCount = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < 8; i++)
|
||||||
|
{
|
||||||
|
floatingTextEffects[i].ticks = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < 96; i++)
|
||||||
|
{
|
||||||
|
particleEffects[i].ticks = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < 64; i++)
|
||||||
|
{
|
||||||
|
cellFlashEffects[i].ticks = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 推进视觉效果计时,并返回是否仍有动画需要刷新。
|
||||||
|
*/
|
||||||
|
bool TickVisualEffects()
|
||||||
|
{
|
||||||
|
bool active = false;
|
||||||
|
|
||||||
|
if (clearEffectState.ticks > 0)
|
||||||
|
{
|
||||||
|
clearEffectState.ticks--;
|
||||||
|
active = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < 8; i++)
|
||||||
|
{
|
||||||
|
if (floatingTextEffects[i].ticks > 0)
|
||||||
|
{
|
||||||
|
floatingTextEffects[i].ticks--;
|
||||||
|
active = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < 96; i++)
|
||||||
|
{
|
||||||
|
if (particleEffects[i].ticks > 0)
|
||||||
|
{
|
||||||
|
particleEffects[i].ticks--;
|
||||||
|
active = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < 64; i++)
|
||||||
|
{
|
||||||
|
if (cellFlashEffects[i].ticks > 0)
|
||||||
|
{
|
||||||
|
cellFlashEffects[i].ticks--;
|
||||||
|
active = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return active;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 推进致谢页左右切换动画,并返回是否需要刷新界面。
|
||||||
|
*/
|
||||||
|
bool TickCreditAnimation()
|
||||||
|
{
|
||||||
|
if (creditAnimationTicks > 0)
|
||||||
|
{
|
||||||
|
creditAnimationTicks--;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 添加一段棋盘坐标系中的浮动文字效果。
|
||||||
|
*/
|
||||||
|
static void AddFloatingText(int boardX, int boardY, const TCHAR* text, COLORREF color)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 8; i++)
|
||||||
|
{
|
||||||
|
if (floatingTextEffects[i].ticks <= 0)
|
||||||
|
{
|
||||||
|
floatingTextEffects[i].ticks = 22;
|
||||||
|
floatingTextEffects[i].totalTicks = 22;
|
||||||
|
floatingTextEffects[i].boardX = boardX;
|
||||||
|
floatingTextEffects[i].boardY = boardY;
|
||||||
|
floatingTextEffects[i].color = color;
|
||||||
|
lstrcpyn(floatingTextEffects[i].text, text, sizeof(floatingTextEffects[i].text) / sizeof(TCHAR));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 添加一个棋盘坐标系中的粒子效果。
|
||||||
|
*/
|
||||||
|
static void AddParticle(int boardX, int boardY, int velocityX, int velocityY, int size, COLORREF color)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 96; i++)
|
||||||
|
{
|
||||||
|
if (particleEffects[i].ticks <= 0)
|
||||||
|
{
|
||||||
|
particleEffects[i].ticks = 12 + rand() % 7;
|
||||||
|
particleEffects[i].totalTicks = particleEffects[i].ticks;
|
||||||
|
particleEffects[i].boardX = boardX;
|
||||||
|
particleEffects[i].boardY = boardY;
|
||||||
|
particleEffects[i].velocityX = velocityX;
|
||||||
|
particleEffects[i].velocityY = velocityY;
|
||||||
|
particleEffects[i].size = size;
|
||||||
|
particleEffects[i].color = color;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 在指定棋盘坐标周围生成一组爆裂粒子。
|
||||||
|
*/
|
||||||
|
static void AddBurstParticles(int boardX, int boardY, COLORREF baseColor, bool strongBurst)
|
||||||
|
{
|
||||||
|
int burstCount = strongBurst ? 4 : 2;
|
||||||
|
for (int i = 0; i < burstCount; i++)
|
||||||
|
{
|
||||||
|
int angleSeed = rand() % 8;
|
||||||
|
int speed = strongBurst ? (9 + rand() % 9) : (6 + rand() % 7);
|
||||||
|
int velocityX = 0;
|
||||||
|
int velocityY = 0;
|
||||||
|
|
||||||
|
switch (angleSeed)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
velocityX = speed;
|
||||||
|
velocityY = -rand() % 4;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
velocityX = -speed;
|
||||||
|
velocityY = -rand() % 4;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
velocityX = (rand() % 5) - 2;
|
||||||
|
velocityY = -speed;
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
velocityX = (rand() % 5) - 2;
|
||||||
|
velocityY = speed / 2;
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
velocityX = speed;
|
||||||
|
velocityY = -speed;
|
||||||
|
break;
|
||||||
|
case 5:
|
||||||
|
velocityX = -speed;
|
||||||
|
velocityY = -speed;
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
velocityX = speed;
|
||||||
|
velocityY = speed / 3;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
velocityX = -speed;
|
||||||
|
velocityY = speed / 3;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
velocityX += (rand() % 7) - 3;
|
||||||
|
velocityY += (rand() % 7) - 3;
|
||||||
|
|
||||||
|
COLORREF color = (i % 3 == 0) ? RGB(255, 248, 220) : baseColor;
|
||||||
|
AddParticle(
|
||||||
|
boardX + (rand() % 31) - 15,
|
||||||
|
boardY + (rand() % 31) - 15,
|
||||||
|
velocityX,
|
||||||
|
velocityY,
|
||||||
|
strongBurst ? (4 + rand() % 5) : (3 + rand() % 4),
|
||||||
|
color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 添加一个被清除格子的短时高亮效果。
|
||||||
|
*/
|
||||||
|
static void AddCellFlash(int x, int y, COLORREF color, bool strongFlash)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 64; i++)
|
||||||
|
{
|
||||||
|
if (cellFlashEffects[i].ticks <= 0)
|
||||||
|
{
|
||||||
|
cellFlashEffects[i].ticks = strongFlash ? 18 : 14;
|
||||||
|
cellFlashEffects[i].totalTicks = cellFlashEffects[i].ticks;
|
||||||
|
cellFlashEffects[i].x = x;
|
||||||
|
cellFlashEffects[i].y = y;
|
||||||
|
cellFlashEffects[i].color = color;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 暂存消行动画,等待升级选择结束后再播放。
|
||||||
|
*/
|
||||||
|
void QueueLineClearEffect(const int* rows, int rowCount, int linesCleared)
|
||||||
|
{
|
||||||
|
if (rows == nullptr || rowCount <= 0 || linesCleared <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rowCount > 8)
|
||||||
|
{
|
||||||
|
rowCount = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingLineClearEffectTicks = 1;
|
||||||
|
pendingLineClearEffectRowCount = rowCount;
|
||||||
|
pendingLineClearEffectLineCount = linesCleared;
|
||||||
|
for (int i = 0; i < rowCount; i++)
|
||||||
|
{
|
||||||
|
pendingLineClearEffectRows[i] = rows[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 播放之前暂存的消行动画。
|
||||||
|
*/
|
||||||
|
void PlayPendingLineClearEffect()
|
||||||
|
{
|
||||||
|
if (pendingLineClearEffectTicks <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingLineClearEffectTicks = 0;
|
||||||
|
TriggerLineClearEffect(
|
||||||
|
pendingLineClearEffectRows,
|
||||||
|
pendingLineClearEffectRowCount,
|
||||||
|
pendingLineClearEffectLineCount);
|
||||||
|
pendingLineClearEffectRowCount = 0;
|
||||||
|
pendingLineClearEffectLineCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 触发标准消行动画和浮动文字。
|
||||||
|
*/
|
||||||
|
void TriggerLineClearEffect(const int* rows, int rowCount, int linesCleared)
|
||||||
|
{
|
||||||
|
if (rows == nullptr || rowCount <= 0 || linesCleared <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rowCount > 8)
|
||||||
|
{
|
||||||
|
rowCount = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearEffectState.ticks = 16;
|
||||||
|
clearEffectState.totalTicks = 16;
|
||||||
|
clearEffectState.rowCount = rowCount;
|
||||||
|
|
||||||
|
int rowSum = 0;
|
||||||
|
for (int i = 0; i < rowCount; i++)
|
||||||
|
{
|
||||||
|
clearEffectState.rows[i] = rows[i];
|
||||||
|
rowSum += rows[i];
|
||||||
|
for (int x = 0; x < nGameWidth; x++)
|
||||||
|
{
|
||||||
|
COLORREF particleColor = BrickColor[(x + rows[i]) % 7];
|
||||||
|
int centerX = x * 100 + 50;
|
||||||
|
int centerY = rows[i] * 100 + 50;
|
||||||
|
AddBurstParticles(centerX, centerY, particleColor, linesCleared >= 4);
|
||||||
|
|
||||||
|
if (linesCleared >= 4)
|
||||||
|
{
|
||||||
|
AddParticle(
|
||||||
|
centerX,
|
||||||
|
centerY,
|
||||||
|
((x < nGameWidth / 2) ? -1 : 1) * (16 + rand() % 12),
|
||||||
|
-16 - rand() % 10,
|
||||||
|
4 + rand() % 3,
|
||||||
|
RGB(255, 238, 120));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TCHAR text[64];
|
||||||
|
if (linesCleared >= 4)
|
||||||
|
{
|
||||||
|
_stprintf_s(text, _T("TETRIS"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_stprintf_s(text, _T("%d LINE%s"), linesCleared, linesCleared > 1 ? _T("S") : _T(""));
|
||||||
|
}
|
||||||
|
AddFloatingText(nGameWidth * 50, (rowSum * 100 / rowCount) - 20, text, linesCleared >= 4 ? RGB(255, 232, 120) : RGB(255, 250, 252));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 为指定棋盘格集合触发清除粒子效果。
|
||||||
|
*/
|
||||||
|
void TriggerCellClearEffect(const Point* cells, int cellCount, bool strongBurst)
|
||||||
|
{
|
||||||
|
TriggerColoredCellClearEffect(cells, cellCount, RGB(255, 238, 120), strongBurst);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 为指定棋盘格集合触发带颜色区分的清除高亮和粒子效果。
|
||||||
|
*/
|
||||||
|
void TriggerColoredCellClearEffect(const Point* cells, int cellCount, COLORREF flashColor, bool strongBurst)
|
||||||
|
{
|
||||||
|
if (cells == nullptr || cellCount <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < cellCount; i++)
|
||||||
|
{
|
||||||
|
if (cells[i].x < 0 || cells[i].x >= nGameWidth || cells[i].y < 0 || cells[i].y >= nGameHeight)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
COLORREF particleColor = BrickColor[(cells[i].x + cells[i].y) % 7];
|
||||||
|
AddCellFlash(cells[i].x, cells[i].y, flashColor, strongBurst);
|
||||||
|
AddBurstParticles(cells[i].x * 100 + 50, cells[i].y * 100 + 50, particleColor, strongBurst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 判断指定方块、旋转状态和位置是否可以合法放置。
|
||||||
|
*/
|
||||||
|
bool IsPiecePlacementValid(int pieceType, int pieceState, Point position)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 4; i++)
|
||||||
|
{
|
||||||
|
for (int j = 0; j < 4; j++)
|
||||||
|
{
|
||||||
|
if (bricks[pieceType][pieceState][i][j] == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int checkY = position.y + i;
|
||||||
|
int checkX = position.x + j;
|
||||||
|
|
||||||
|
if (checkX < 0 || checkX >= nGameWidth || checkY >= GetRoguePlayableHeight())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkY >= 0 && workRegion[checkY][checkX] != 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 尝试把旋转后的方块横向偏移指定格数后放置。
|
||||||
|
*/
|
||||||
|
bool TryRotateWithOffset(int nextState, int offsetX)
|
||||||
|
{
|
||||||
|
Point rotatedPoint = point;
|
||||||
|
rotatedPoint.x += offsetX;
|
||||||
|
return IsPiecePlacementValid(type, nextState, rotatedPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 视频复活后清理顶部空间并恢复一局游戏。
|
||||||
|
*/
|
||||||
|
void ReviveAfterVideo()
|
||||||
|
{
|
||||||
|
if (!gameOverFlag || !reviveAvailable)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
reviveAvailable = false;
|
||||||
|
gameOverFlag = false;
|
||||||
|
suspendFlag = false;
|
||||||
|
currentScreen = SCREEN_PLAYING;
|
||||||
|
|
||||||
|
int playableHeight = GetRoguePlayableHeight();
|
||||||
|
int rowsToClear = playableHeight / 3;
|
||||||
|
if (rowsToClear < 5)
|
||||||
|
{
|
||||||
|
rowsToClear = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int y = 0; y < rowsToClear && y < playableHeight; y++)
|
||||||
|
{
|
||||||
|
for (int x = 0; x < nGameWidth; x++)
|
||||||
|
{
|
||||||
|
workRegion[y][x] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type = ConsumeNextType();
|
||||||
|
nType = nextTypes[0];
|
||||||
|
state = 0;
|
||||||
|
holdUsedThisTurn = false;
|
||||||
|
RollCurrentPieceSpecialFlags(true);
|
||||||
|
point = GetSpawnPoint(type);
|
||||||
|
target = point;
|
||||||
|
ComputeTarget();
|
||||||
|
|
||||||
|
SetFeedbackMessage(_T("复活成功"), _T("已清理顶部空间,本局复活机会已用完。"), 14);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 按指定模式开始新游戏。
|
||||||
|
*/
|
||||||
|
void StartGameWithMode(int mode)
|
||||||
|
{
|
||||||
|
currentMode = mode;
|
||||||
|
currentScreen = SCREEN_PLAYING;
|
||||||
|
upgradeListScrollOffset = 0;
|
||||||
|
Restart();
|
||||||
|
currentFallInterval = (currentMode == MODE_ROGUE) ? GetRogueFallInterval() : 500;
|
||||||
|
tScore = (currentMode == MODE_CLASSIC) ? classicStats.score : rogueStats.score;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 返回主菜单并清理游戏中的临时界面状态。
|
||||||
|
*/
|
||||||
|
void ReturnToMainMenu()
|
||||||
|
{
|
||||||
|
currentScreen = SCREEN_MENU;
|
||||||
|
suspendFlag = false;
|
||||||
|
gameOverFlag = false;
|
||||||
|
ResetVisualEffects();
|
||||||
|
ResetPendingRogueVisualEvents();
|
||||||
|
helpScrollOffset = 0;
|
||||||
|
creditPageIndex = 0;
|
||||||
|
creditAnimationTicks = 0;
|
||||||
|
creditAnimationDirection = 0;
|
||||||
|
upgradeListScrollOffset = 0;
|
||||||
|
pendingLineClearEffectTicks = 0;
|
||||||
|
pendingLineClearEffectRowCount = 0;
|
||||||
|
pendingLineClearEffectLineCount = 0;
|
||||||
|
menuState.optionCount = 4;
|
||||||
|
ResetUpgradeUiState();
|
||||||
|
|
||||||
|
if (menuState.selectedIndex < 0 || menuState.selectedIndex >= menuState.optionCount)
|
||||||
|
{
|
||||||
|
menuState.selectedIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 打开规则说明界面并重置说明页状态。
|
||||||
|
*/
|
||||||
|
void OpenRulesScreen()
|
||||||
|
{
|
||||||
|
currentScreen = SCREEN_RULES;
|
||||||
|
suspendFlag = false;
|
||||||
|
helpState.selectedIndex = 0;
|
||||||
|
helpState.optionCount = 3;
|
||||||
|
helpState.currentPage = 0;
|
||||||
|
helpScrollOffset = 0;
|
||||||
|
creditPageIndex = 0;
|
||||||
|
creditAnimationTicks = 0;
|
||||||
|
creditAnimationDirection = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 打开致谢界面并重置致谢页切换状态。
|
||||||
|
*/
|
||||||
|
void OpenCreditScreen()
|
||||||
|
{
|
||||||
|
currentScreen = SCREEN_RULES;
|
||||||
|
suspendFlag = false;
|
||||||
|
helpState.selectedIndex = 0;
|
||||||
|
helpState.optionCount = 3;
|
||||||
|
helpState.currentPage = 4;
|
||||||
|
helpScrollOffset = 0;
|
||||||
|
creditPageIndex = 0;
|
||||||
|
creditAnimationTicks = 0;
|
||||||
|
creditAnimationDirection = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 切换致谢页图片,并启动左右滑动动画。
|
||||||
|
*/
|
||||||
|
void ChangeCreditPage(int direction)
|
||||||
|
{
|
||||||
|
if (direction == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int oldPageIndex = creditPageIndex;
|
||||||
|
if (direction > 0)
|
||||||
|
{
|
||||||
|
creditPageIndex++;
|
||||||
|
creditAnimationDirection = 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
creditPageIndex--;
|
||||||
|
creditAnimationDirection = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (creditPageIndex < 0)
|
||||||
|
{
|
||||||
|
creditPageIndex = 1;
|
||||||
|
}
|
||||||
|
if (creditPageIndex > 1)
|
||||||
|
{
|
||||||
|
creditPageIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (creditPageIndex != oldPageIndex)
|
||||||
|
{
|
||||||
|
creditAnimationTicks = 60;
|
||||||
|
}
|
||||||
|
}
|
||||||
+697
-95
File diff suppressed because it is too large
Load Diff
+505
-74
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user