Compare commits
44 Commits
rogue
..
7fe0244a99
| Author | SHA1 | Date | |
|---|---|---|---|
| 7fe0244a99 | |||
| be95bd25e1 | |||
| b98d2c9d59 | |||
| a331162349 | |||
| 58ab400949 | |||
| 1c000c3c21 | |||
| 0840a807b5 | |||
| 2f435f5ca6 | |||
| 45d9e988df | |||
| aa9e2f3ddc | |||
| 971d8be0dc | |||
| 9341ac9a05 | |||
| c77b877b8b | |||
| da741d1e56 | |||
| 647038b27a | |||
| 00729fbe17 | |||
| 2c04796010 | |||
| f3065c5fe7 | |||
| 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 |
@@ -1,5 +1,6 @@
|
|||||||
# Build outputs
|
# Build outputs
|
||||||
/.vscode-build/
|
/.vscode-build/
|
||||||
|
/.worktrees/
|
||||||
/build/
|
/build/
|
||||||
/bin/
|
/bin/
|
||||||
/obj/
|
/obj/
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
## 项目名称
|
||||||
|
使用大模型辅助开发俄罗斯方块程序
|
||||||
|
|
||||||
|
## 基本开发要求
|
||||||
|
|
||||||
|
### 编程语言
|
||||||
|
- 使用 C++。
|
||||||
|
- 仅使用课程已学基础语法:数组、循环、分支、函数、结构体等。
|
||||||
|
- 不使用 `class`、继承、多态等面向对象特性。
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
- 源码主要位于 `src` 目录。
|
||||||
|
- 不要随意改动已有全局变量、函数声明和文件结构。
|
||||||
|
- 如需新增创新功能,可以新增 `.cpp` 文件。
|
||||||
|
|
||||||
|
## 构建方式
|
||||||
|
|
||||||
|
优先使用项目根目录下的构建脚本:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\build-mingw.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
## 代码质量要求
|
||||||
|
|
||||||
|
1. 每次只实现一个明确功能。
|
||||||
|
2. 每个函数必须有功能描述注释。
|
||||||
|
3. 变量命名保持和原框架一致。
|
||||||
|
4. 不随意改动已有全局变量和函数声明。
|
||||||
|
5. 生成代码后必须人工审查。
|
||||||
|
6. 每个阶段完成后必须编译运行。
|
||||||
|
7. 出现 bug 时,应记录问题、原因和修复过程。
|
||||||
|
|
||||||
|
## 开发注意事项
|
||||||
|
|
||||||
|
1. 每次补全前后都要保存版本,便于报告展示。
|
||||||
|
2. 现场汇报时,所有组员都可能被提问,不能只有一人理解代码。
|
||||||
-150
@@ -1,150 +0,0 @@
|
|||||||
# Dev-C++ 运行说明
|
|
||||||
|
|
||||||
## 1. 适用环境
|
|
||||||
|
|
||||||
本说明适用于 Windows 下的 Dev-C++ + MinGW 环境。
|
|
||||||
|
|
||||||
如果 Dev-C++ 自带的是较旧版本 MinGW,也可以尝试使用,但更建议使用支持 C++17 和 `windres` 的 MinGW。
|
|
||||||
|
|
||||||
## 2. 当前工程结构
|
|
||||||
|
|
||||||
项目已按工程方式整理:
|
|
||||||
|
|
||||||
```text
|
|
||||||
src/
|
|
||||||
├─ include/ 头文件
|
|
||||||
├─ source/ 源文件
|
|
||||||
└─ resources/ Windows 资源脚本
|
|
||||||
|
|
||||||
assets/
|
|
||||||
├─ icons/ 图标资源
|
|
||||||
├─ images/ 图片资源
|
|
||||||
└─ audio/ 音频资源
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3. 建议的工程类型
|
|
||||||
|
|
||||||
在 Dev-C++ 中新建工程时,建议选择:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Windows Application
|
|
||||||
```
|
|
||||||
|
|
||||||
不要选控制台程序,否则窗口程序的入口和链接方式会不匹配。
|
|
||||||
|
|
||||||
## 4. 需要加入工程的文件
|
|
||||||
|
|
||||||
### 源文件
|
|
||||||
|
|
||||||
把以下文件加入工程:
|
|
||||||
|
|
||||||
- `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
|
|
||||||
src/include
|
|
||||||
```
|
|
||||||
|
|
||||||
### 链接库
|
|
||||||
|
|
||||||
确保工程链接以下 Windows 库:
|
|
||||||
|
|
||||||
- `winmm`
|
|
||||||
- `gdi32`
|
|
||||||
- `user32`
|
|
||||||
|
|
||||||
### 编译标准
|
|
||||||
|
|
||||||
建议使用:
|
|
||||||
|
|
||||||
```text
|
|
||||||
C++17
|
|
||||||
```
|
|
||||||
|
|
||||||
### 预处理宏
|
|
||||||
|
|
||||||
建议定义:
|
|
||||||
|
|
||||||
- `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
|
|
||||||
"assets/icons/Tetris.ico"
|
|
||||||
"assets/icons/small.ico"
|
|
||||||
```
|
|
||||||
|
|
||||||
这样更容易在 Dev-C++ 中直接通过资源编译。
|
|
||||||
|
|
||||||
## 8. 运行结果
|
|
||||||
|
|
||||||
如果配置正确,编译后应该能得到一个 Windows 图形界面的 `exe`,并正常弹出游戏窗口。
|
|
||||||
|
|
||||||
如果只是为了开发和调试,建议优先使用本项目现成的 VS Code 配置,因为当前目录结构、构建脚本和资源处理逻辑已经和 VS Code 对齐。 Dev-C++ 更适合作为兼容运行方案。
|
|
||||||
|
|
||||||
@@ -1,68 +1,250 @@
|
|||||||
# Tereis
|
# Tereis
|
||||||
|
|
||||||
基于 C++ 与 Windows API 实现的俄罗斯方块课程项目。
|
Tereis 是一个基于 C++、Win32 API、GDI/GDI+ 实现的桌面版俄罗斯方块课程大作业。
|
||||||
|
|
||||||
本项目使用 MinGW 进行构建,当前已完成基础窗口框架、方块逻辑、绘图显示与资源编译接入,适合作为《大学计算》程序设计大作业使用。
|
项目在经典俄罗斯方块玩法上扩展了 Rogue 模式,加入等级成长、强化选择、主动技能、特殊方块、视频复活、鼠标交互和视觉特效。程序不依赖游戏引擎,主要使用 Win32 消息循环和 GDI 绘图完成。
|
||||||
|
|
||||||
## 项目简介
|
## 快速运行
|
||||||
|
|
||||||
项目目标是实现一个可运行的桌面版俄罗斯方块程序,包含以下核心内容:
|
推荐在 Windows + PowerShell + MinGW-w64 环境下运行。
|
||||||
|
|
||||||
- 创建 Windows 游戏窗口
|
1. 确认 `g++.exe` 和 `windres.exe` 已加入 `PATH`,或安装在 `C:\mingw64\bin\`。
|
||||||
- 实现方块生成、移动、旋转与下落
|
2. 在项目根目录执行构建并运行:
|
||||||
- 实现碰撞检测、方块固定与游戏结束判定
|
|
||||||
- 实现消行逻辑与基础分数系统
|
|
||||||
- 实现界面绘制与部分资源显示
|
|
||||||
- 提供 MinGW 构建脚本和 VS Code 调试配置
|
|
||||||
|
|
||||||
## 目录结构
|
```powershell
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File .\build-mingw.ps1 -Run
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 如只需构建,不启动程序:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File .\build-mingw.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
构建产物位于:
|
||||||
|
|
||||||
|
```text
|
||||||
|
.vscode-build\mingw\Tetris.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
运行时请从项目根目录启动程序,确保 `assets/` 目录可被读取,否则背景图、音乐和复活视频可能无法加载。
|
||||||
|
|
||||||
|
## 功能概览
|
||||||
|
|
||||||
|
### 经典模式
|
||||||
|
|
||||||
|
- 标准俄罗斯方块规则
|
||||||
|
- 方块生成、移动、旋转、软降、硬降
|
||||||
|
- 方块落地固定、消行、计分和死亡判定
|
||||||
|
- 预测落点显示
|
||||||
|
- 暂停、重开、返回主菜单
|
||||||
|
|
||||||
|
### 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 确认已选强化
|
||||||
|
- 鼠标操作:直接点击升级卡片即可选择或标记
|
||||||
|
|
||||||
|
## 运行说明
|
||||||
|
|
||||||
|
### 方式一:PowerShell 一键运行
|
||||||
|
|
||||||
|
在项目根目录打开 PowerShell,执行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File .\build-mingw.ps1 -Run
|
||||||
|
```
|
||||||
|
|
||||||
|
该命令会先编译项目,编译成功后自动启动游戏窗口。
|
||||||
|
|
||||||
|
### 方式二:先构建再运行
|
||||||
|
|
||||||
|
先在项目根目录执行构建:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File .\build-mingw.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
构建成功后运行生成的程序:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\.vscode-build\mingw\Tetris.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方式三:VS Code 运行和调试
|
||||||
|
|
||||||
|
项目已经配置好 VS Code 任务:
|
||||||
|
|
||||||
|
- 按 `Ctrl + Shift + B` 执行默认构建任务 `build Tetris MinGW`
|
||||||
|
- 在任务列表中运行 `run Tetris MinGW` 可构建并启动游戏
|
||||||
|
- 在“运行和调试”中选择 `Debug Tetris MinGW` 可启动调试
|
||||||
|
|
||||||
|
调试需要系统能找到 `gdb.exe`。如果无法调试,请确认 MinGW 的 `bin` 目录已经加入 `PATH`。
|
||||||
|
|
||||||
|
### 方式四:Visual Studio 中运行
|
||||||
|
|
||||||
|
本项目没有提供 Visual Studio 的 `.sln` 或 `.vcxproj` 工程文件,推荐在 Visual Studio 中打开项目文件夹,然后通过终端调用已有构建脚本运行。
|
||||||
|
|
||||||
|
操作步骤:
|
||||||
|
|
||||||
|
1. 打开 Visual Studio。
|
||||||
|
2. 选择 `文件 -> 打开 -> 文件夹`,打开项目根目录 `Tereis`。
|
||||||
|
3. 打开 Visual Studio 内置终端,或在项目根目录单独打开 PowerShell。
|
||||||
|
4. 确认 MinGW-w64 已安装,并且 `g++.exe`、`windres.exe` 可以被系统找到。
|
||||||
|
5. 在终端中执行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File .\build-mingw.ps1 -Run
|
||||||
|
```
|
||||||
|
|
||||||
|
如果只想编译,不立即运行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File .\build-mingw.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
编译成功后,程序位置为:
|
||||||
|
|
||||||
|
```text
|
||||||
|
.vscode-build\mingw\Tetris.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
也可以在 Visual Studio 的终端中运行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\.vscode-build\mingw\Tetris.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
注意:不要直接把 `src` 目录中的单个 `.cpp` 文件当作独立程序运行。本项目由多个源文件、资源文件和 `assets/` 资源目录共同组成,必须通过项目根目录下的 `build-mingw.ps1` 构建。
|
||||||
|
|
||||||
|
### 运行注意事项
|
||||||
|
|
||||||
|
- 推荐始终从项目根目录启动程序。
|
||||||
|
- 不建议直接双击 `.vscode-build\mingw\Tetris.exe`,因为工作目录可能不正确,导致 `assets/` 资源加载失败。
|
||||||
|
- 如果重新构建时提示 `Tetris.exe: Permission denied`,请先关闭正在运行的游戏窗口。
|
||||||
|
- 程序使用 Win32 桌面窗口运行,不会显示控制台窗口。
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Tereis/
|
Tereis/
|
||||||
├─ src/
|
├─ src/
|
||||||
│ ├─ include/ 头文件
|
│ ├─ include/ 头文件
|
||||||
│ ├─ source/ 源文件
|
│ ├─ source/ 源文件
|
||||||
│ └─ resources/ Windows 资源脚本
|
│ │ ├─ Tetris.cpp 程序入口、窗口和消息框架
|
||||||
|
│ │ ├─ TetrisLogic.cpp 基础俄罗斯方块逻辑框架
|
||||||
|
│ │ ├─ TetrisRender.cpp 基础绘制框架
|
||||||
|
│ │ ├─ common/ 资源路径、文件检查等通用工具
|
||||||
|
│ │ ├─ app/ 媒体播放、布局命中、输入和定时器处理
|
||||||
|
│ │ ├─ extensions/ 框架外通用扩展、界面状态和视觉效果
|
||||||
|
│ │ ├─ logic/ 特殊方块落地效果等逻辑扩展
|
||||||
|
│ │ ├─ render/ 图片加载等渲染内部支持
|
||||||
|
│ │ └─ rogue/ Rogue 模式、强化和技能系统
|
||||||
|
│ └─ resources/ Windows 资源脚本
|
||||||
├─ assets/
|
├─ assets/
|
||||||
│ ├─ icons/ 图标资源
|
│ ├─ audio/ 背景音乐
|
||||||
│ ├─ images/ 图片资源
|
│ ├─ icons/ 程序图标
|
||||||
│ └─ audio/ 音频资源
|
│ ├─ images/ 背景图片
|
||||||
├─ .vscode/ VS Code 配置
|
│ └─ video/ 复活视频
|
||||||
├─ .vscode-build/ 本地构建输出目录
|
├─ report/ 报告相关材料
|
||||||
├─ report/ 实验报告材料与草稿
|
├─ .vscode/ VS Code 配置
|
||||||
├─ build-mingw.ps1 MinGW 构建脚本
|
├─ .vscode-build/ 本地构建输出目录
|
||||||
├─ list.md 项目阶段划分
|
├─ build-mingw.ps1 MinGW 构建脚本
|
||||||
├─ VSCode运行说明.md VS Code 使用说明
|
├─ README.md 项目说明
|
||||||
└─ README.md 项目说明
|
└─ AGENTS.md 项目协作和代码生成约束
|
||||||
```
|
```
|
||||||
|
|
||||||
## 开发阶段划分
|
## 构建环境
|
||||||
|
|
||||||
整个程序按 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\
|
||||||
|
```
|
||||||
|
|
||||||
|
构建脚本会递归收集 `src/source` 下的 `.cpp` 文件。新增功能代码可以放入功能目录,不需要手动维护固定源码列表。
|
||||||
|
|
||||||
|
## 构建与运行
|
||||||
|
|
||||||
在项目根目录执行:
|
在项目根目录执行:
|
||||||
|
|
||||||
@@ -70,58 +252,86 @@ 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
|
也可以直接运行已生成的程序:
|
||||||
|
|
||||||
项目已提供:
|
```powershell
|
||||||
|
.\.vscode-build\mingw\Tetris.exe
|
||||||
|
```
|
||||||
|
|
||||||
- 构建任务 `build Tetris MinGW`
|
如果使用 VS Code:
|
||||||
- 运行任务 `run Tetris MinGW`
|
|
||||||
- 调试配置 `Debug Tetris MinGW`
|
|
||||||
|
|
||||||
详见 [VSCode运行说明.md](./VSCode运行说明.md)。
|
- `Ctrl + Shift + B` 执行默认构建任务 `build Tetris MinGW`
|
||||||
|
- 运行任务 `run Tetris MinGW` 可构建并启动游戏
|
||||||
|
- 调试配置 `Debug Tetris MinGW` 会先构建,再使用 `gdb.exe` 启动调试
|
||||||
|
|
||||||
## 资源文件说明
|
注意:直接双击 `.vscode-build\mingw\Tetris.exe` 时,当前工作目录可能不是项目根目录,资源文件可能无法正常读取。推荐从项目根目录通过脚本或 VS Code 任务启动。
|
||||||
|
|
||||||
项目包含 Windows 资源文件 `src/resources/Tetris.rc`,其中定义了图标、菜单、快捷键和关于框等内容。
|
## 常见问题
|
||||||
|
|
||||||
由于原始 `Tetris.rc` 为 UTF-16 编码,当前构建脚本会在编译时临时转换资源文件编码,并将 `assets/icons/` 中的图标文件一起编译进最终程序,因此资源不再被跳过。
|
### 1. 提示 `Tetris.exe: Permission denied`
|
||||||
|
|
||||||
## 报告目录
|
说明游戏程序仍在运行,链接器无法覆盖旧文件。
|
||||||
|
|
||||||
实验报告相关材料已整理到 [report/](./report/):
|
处理方式:
|
||||||
|
|
||||||
- `report.md`:报告正文草稿
|
- 关闭正在运行的游戏窗口
|
||||||
- `outline.md`:章节提纲
|
- 重新执行构建命令
|
||||||
- `notes.md`:待补充内容
|
|
||||||
- `images/`:截图和流程图
|
|
||||||
- `code-snippets/`:报告中准备引用的代码
|
|
||||||
- `submission/`:最终提交版文档
|
|
||||||
|
|
||||||
## 当前状态
|
### 2. 没有背景图、音乐或视频
|
||||||
|
|
||||||
当前项目已完成的工作:
|
请确认运行时保留了 `assets/` 目录。项目会从资源目录读取背景、音乐和复活视频。
|
||||||
|
|
||||||
- 修复项目迁移后的路径配置问题
|
### 3. 视频复活播放失败
|
||||||
- 补充 `.gitignore`
|
|
||||||
- 接入资源文件编译流程
|
|
||||||
- 整理项目阶段清单
|
|
||||||
- 建立实验报告目录结构
|
|
||||||
|
|
||||||
后续可以继续完善的方向:
|
项目会优先查找:
|
||||||
|
|
||||||
- 优化界面表现
|
- `assets/video/video.avi`
|
||||||
- 完善分数与状态提示
|
- `assets/video/video.mp4`
|
||||||
- 增加创新功能
|
|
||||||
- 补充测试截图和实验分析
|
如果系统不支持对应格式,可能会播放失败。建议保留项目中已提供的视频文件。
|
||||||
|
|
||||||
|
### 4. 鼠标点击不生效
|
||||||
|
|
||||||
|
请确认运行的是最新构建结果。若构建时 `Tetris.exe` 被占用,实际运行的可能仍是旧版本。
|
||||||
|
|
||||||
|
## 课程展示建议
|
||||||
|
|
||||||
|
建议按以下顺序展示:
|
||||||
|
|
||||||
|
1. 主菜单、帮助页和鼠标点击
|
||||||
|
2. 经典模式基础玩法
|
||||||
|
3. Rogue 模式升级选择
|
||||||
|
4. 双重抉择或命运轮盘的多选界面
|
||||||
|
5. 主动技能:黑洞、炸弹、换形、备用仓
|
||||||
|
6. 特殊方块和消除特效
|
||||||
|
7. 死亡后视频复活
|
||||||
|
|
||||||
|
## 实现说明
|
||||||
|
|
||||||
|
本项目以过程式 C++ 写法为主,核心逻辑分布如下:
|
||||||
|
|
||||||
|
- `src/source/Tetris.cpp`:Win32 程序入口、窗口创建和消息分发主干
|
||||||
|
- `src/source/TetrisLogic.cpp`:基础方块逻辑、消行和状态重置
|
||||||
|
- `src/source/TetrisRender.cpp`:界面绘制、面板、动画和特效
|
||||||
|
- `src/source/common/TetrisAssets.cpp`:资源路径拼接和文件存在判断
|
||||||
|
- `src/source/app/`:背景音乐、复活视频、窗口布局命中、鼠标键盘和定时器处理
|
||||||
|
- `src/source/logic/TetrisPieceEffects.cpp`:彩虹、爆破、激光、十字和稳定结构等落地效果
|
||||||
|
- `src/source/extensions/TetrisGameExtensions.cpp`:框架外通用状态切换、复活、说明页、视觉效果等扩展支持
|
||||||
|
- `src/source/render/TetrisRenderAssets.cpp`:背景图、致谢页图片等 GDI+ 图片资源加载
|
||||||
|
- `src/source/rogue/TetrisRogue.cpp`:Rogue 模式、强化、技能和成长系统
|
||||||
|
- `src/include/Tetris.h`:主要结构体、全局状态和函数声明
|
||||||
|
- `src/include/TetrisAppInternal.h`、`src/include/TetrisRenderInternal.h`、`src/include/TetrisAssets.h`:窗口层、渲染层和资源工具的内部声明
|
||||||
|
|
||||||
|
项目适合作为程序设计课程大作业展示,也便于在答辩时讲解窗口程序、游戏循环、碰撞检测、状态管理和功能扩展。
|
||||||
|
|||||||
-152
@@ -1,152 +0,0 @@
|
|||||||
# VS Code 运行说明
|
|
||||||
|
|
||||||
## 1. 适用环境
|
|
||||||
|
|
||||||
本项目适用于 Windows + VS Code + MinGW 环境。
|
|
||||||
|
|
||||||
建议已安装:
|
|
||||||
|
|
||||||
- VS Code
|
|
||||||
- C/C++ 扩展(Microsoft)
|
|
||||||
- PowerShell
|
|
||||||
- MinGW,且可用 `g++.exe`、`gdb.exe`、`windres.exe`
|
|
||||||
|
|
||||||
脚本会优先使用系统 `PATH` 中的工具;如果未加入 `PATH`,也兼容 `C:\mingw64\bin\` 下的 MinGW。
|
|
||||||
|
|
||||||
## 2. 项目结构
|
|
||||||
|
|
||||||
当前工程目录结构如下:
|
|
||||||
|
|
||||||
```text
|
|
||||||
src/
|
|
||||||
├─ include/ 头文件
|
|
||||||
├─ source/ 源文件
|
|
||||||
└─ resources/ Windows 资源脚本
|
|
||||||
|
|
||||||
assets/
|
|
||||||
├─ icons/ 图标资源
|
|
||||||
├─ images/ 图片资源
|
|
||||||
└─ audio/ 音频资源
|
|
||||||
```
|
|
||||||
|
|
||||||
其中:
|
|
||||||
|
|
||||||
- 头文件检索路径为 `src/include`
|
|
||||||
- 编译的源文件位于 `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
|
|
||||||
build Tetris MinGW
|
|
||||||
```
|
|
||||||
|
|
||||||
它会调用:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
powershell -NoProfile -ExecutionPolicy Bypass -File .\build-mingw.ps1
|
|
||||||
```
|
|
||||||
|
|
||||||
### 方法二:命令面板运行任务
|
|
||||||
|
|
||||||
在命令面板中执行:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Tasks: Run Task
|
|
||||||
```
|
|
||||||
|
|
||||||
然后选择:
|
|
||||||
|
|
||||||
- `build Tetris MinGW`
|
|
||||||
- `run Tetris MinGW`
|
|
||||||
|
|
||||||
## 5. 调试方式
|
|
||||||
|
|
||||||
按 `F5`,选择:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Debug Tetris MinGW
|
|
||||||
```
|
|
||||||
|
|
||||||
调试配置会先执行构建任务,然后启动:
|
|
||||||
|
|
||||||
```text
|
|
||||||
.vscode-build\mingw\Tetris.exe
|
|
||||||
```
|
|
||||||
|
|
||||||
当前工作目录为项目根目录。
|
|
||||||
|
|
||||||
## 6. 构建输出
|
|
||||||
|
|
||||||
成功构建后,输出文件位于:
|
|
||||||
|
|
||||||
```text
|
|
||||||
.vscode-build\mingw\Tetris.exe
|
|
||||||
```
|
|
||||||
|
|
||||||
同时在资源编译阶段,脚本还会临时生成:
|
|
||||||
|
|
||||||
- `.vscode-build\mingw\Tetris.utf8.rc`
|
|
||||||
- `.vscode-build\mingw\Tetris.res.o`
|
|
||||||
|
|
||||||
这些都属于中间产物,不需要手动维护。
|
|
||||||
|
|
||||||
## 7. 资源文件说明
|
|
||||||
|
|
||||||
原始 `Tetris.rc` 是 UTF-16 编码,MinGW 的 `windres` 不能直接稳定编译该文件。
|
|
||||||
|
|
||||||
当前脚本的处理方式是:
|
|
||||||
|
|
||||||
1. 读取 `src/resources/Tetris.rc`
|
|
||||||
2. 临时转换为 UTF-8
|
|
||||||
3. 将图标路径替换为 `assets/icons/` 下的实际文件
|
|
||||||
4. 使用 `windres` 编译资源
|
|
||||||
5. 将资源对象与 C++ 源文件一起链接
|
|
||||||
|
|
||||||
因此在 VS Code 环境下,图标和菜单资源是会参与构建的。
|
|
||||||
|
|
||||||
## 8. 常见问题
|
|
||||||
|
|
||||||
### 找不到 `g++.exe`
|
|
||||||
|
|
||||||
说明 MinGW 没加入系统 `PATH`,或者未安装在 `C:\mingw64\bin\`。
|
|
||||||
处理方式:
|
|
||||||
|
|
||||||
- 把 MinGW 的 `bin` 目录加入 `PATH`
|
|
||||||
- 或安装到 `C:\mingw64\bin\`
|
|
||||||
|
|
||||||
### 找不到 `gdb.exe`
|
|
||||||
|
|
||||||
说明调试器不可用。
|
|
||||||
构建通常还能继续,但 `F5` 调试会失败。
|
|
||||||
|
|
||||||
### 找不到 `windres.exe`
|
|
||||||
|
|
||||||
程序主体仍可能编译通过,但资源文件无法编译进最终 `exe`。
|
|
||||||
|
|
||||||
### 打开的是 `src/` 而不是项目根目录
|
|
||||||
|
|
||||||
会导致:
|
|
||||||
|
|
||||||
- VS Code 任务不可用
|
|
||||||
- 调试配置不可用
|
|
||||||
- include 路径不正确
|
|
||||||
|
|
||||||
应重新打开项目根目录。
|
|
||||||
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 722 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 194 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
+3
-7
@@ -61,13 +61,9 @@ foreach ($Candidate in $WindresCandidates) {
|
|||||||
|
|
||||||
New-Item -ItemType Directory -Force -Path $BuildDir | Out-Null
|
New-Item -ItemType Directory -Force -Path $BuildDir | Out-Null
|
||||||
|
|
||||||
$Sources = @(
|
$Sources = Get-ChildItem -Path $SourceDir -Recurse -Filter "*.cpp" |
|
||||||
(Join-Path $SourceDir "stdafx.cpp"),
|
Sort-Object FullName |
|
||||||
(Join-Path $SourceDir "Tetris.cpp"),
|
Select-Object -ExpandProperty FullName
|
||||||
(Join-Path $SourceDir "TetrisLogic.cpp"),
|
|
||||||
(Join-Path $SourceDir "TetrisRogue.cpp"),
|
|
||||||
(Join-Path $SourceDir "TetrisRender.cpp")
|
|
||||||
)
|
|
||||||
|
|
||||||
$LinkInputs = @()
|
$LinkInputs = @()
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file Tetris.h
|
||||||
|
* @brief 定义俄罗斯方块项目的全局常量、结构体、枚举、全局状态和公开函数接口。
|
||||||
|
*/
|
||||||
|
|
||||||
#include "resource.h"
|
#include "resource.h"
|
||||||
#include "stdafx.h"
|
#include "stdafx.h"
|
||||||
#include <mmsystem.h>
|
#include <mmsystem.h>
|
||||||
@@ -140,6 +145,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 +187,25 @@ struct ParticleEffect
|
|||||||
COLORREF color;
|
COLORREF color;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct CellFlashEffect
|
||||||
|
{
|
||||||
|
int ticks;
|
||||||
|
int totalTicks;
|
||||||
|
int x;
|
||||||
|
int y;
|
||||||
|
COLORREF color;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct GravityFallEffect
|
||||||
|
{
|
||||||
|
int ticks;
|
||||||
|
int totalTicks;
|
||||||
|
int x;
|
||||||
|
int fromY;
|
||||||
|
int toY;
|
||||||
|
int cellValue;
|
||||||
|
};
|
||||||
|
|
||||||
enum ScreenState
|
enum ScreenState
|
||||||
{
|
{
|
||||||
SCREEN_MENU = 0,
|
SCREEN_MENU = 0,
|
||||||
@@ -210,11 +236,17 @@ extern bool suspendFlag;
|
|||||||
extern bool targetFlag;
|
extern bool targetFlag;
|
||||||
extern bool bgmEnabled;
|
extern bool bgmEnabled;
|
||||||
extern bool reviveAvailable;
|
extern bool reviveAvailable;
|
||||||
|
extern bool rogueDemoMode;
|
||||||
extern int workRegion[20][10];
|
extern int workRegion[20][10];
|
||||||
extern Point point;
|
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 +254,8 @@ 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 GravityFallEffect gravityFallEffects[80];
|
||||||
extern int currentScreen;
|
extern int currentScreen;
|
||||||
extern int currentMode;
|
extern int currentMode;
|
||||||
extern int currentFallInterval;
|
extern int currentFallInterval;
|
||||||
@@ -235,42 +269,329 @@ extern bool currentPieceIsRainbow;
|
|||||||
extern int bricks[7][4][4][4];
|
extern int bricks[7][4][4][4];
|
||||||
extern COLORREF BrickColor[7];
|
extern COLORREF BrickColor[7];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 判断当前活动方块是否还能向下移动一格。
|
||||||
|
* @return 可以下落返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
bool CanMoveDown();
|
bool CanMoveDown();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 判断当前活动方块是否还能向左移动一格。
|
||||||
|
* @return 可以左移返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
bool CanMoveLeft();
|
bool CanMoveLeft();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 判断当前活动方块是否还能向右移动一格。
|
||||||
|
* @return 可以右移返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
bool CanMoveRight();
|
bool CanMoveRight();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 将当前活动方块向下移动一格。
|
||||||
|
*/
|
||||||
void MoveDown();
|
void MoveDown();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 将当前活动方块向左移动一格。
|
||||||
|
*/
|
||||||
void MoveLeft();
|
void MoveLeft();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 将当前活动方块向右移动一格。
|
||||||
|
*/
|
||||||
void MoveRight();
|
void MoveRight();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 尝试旋转当前活动方块,Rogue 完美旋转会额外尝试左右偏移。
|
||||||
|
*/
|
||||||
void Rotate();
|
void Rotate();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 将当前活动方块直接下落到预测落点。
|
||||||
|
*/
|
||||||
void DropDown();
|
void DropDown();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 将当前活动方块固定到棋盘并生成下一块。
|
||||||
|
*/
|
||||||
void Fixing();
|
void Fixing();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 删除指定行并让上方棋盘整体下落。
|
||||||
|
* @param number 要删除的棋盘行号。
|
||||||
|
*/
|
||||||
void DeleteOneLine(int number);
|
void DeleteOneLine(int number);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 扫描棋盘、删除所有满行并触发消行结算。
|
||||||
|
* @return 本次删除的行数。
|
||||||
|
*/
|
||||||
int DeleteLines();
|
int DeleteLines();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 计算当前活动方块的预测落点。
|
||||||
|
*/
|
||||||
void ComputeTarget();
|
void ComputeTarget();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 重置棋盘、方块、统计和视觉状态,开始一局新游戏。
|
||||||
|
*/
|
||||||
void Restart();
|
void Restart();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 按指定模式开始新游戏。
|
||||||
|
* @param mode 游戏模式,取值来自 GameMode。
|
||||||
|
*/
|
||||||
void StartGameWithMode(int mode);
|
void StartGameWithMode(int mode);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 返回主菜单并清理临时玩法与界面状态。
|
||||||
|
*/
|
||||||
void ReturnToMainMenu();
|
void ReturnToMainMenu();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 复活视频播放成功后恢复游戏并清理顶部空间。
|
||||||
|
*/
|
||||||
void ReviveAfterVideo();
|
void ReviveAfterVideo();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 从帮助页进入 Rogue 技能演示的第一项。
|
||||||
|
*/
|
||||||
|
void StartRogueSkillDemo();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 从帮助页进入指定 Rogue 技能演示。
|
||||||
|
* @param demoIndex 技能演示序号。
|
||||||
|
*/
|
||||||
|
void StartRogueSkillDemoAt(int demoIndex);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 重新开始当前 Rogue 技能演示场景。
|
||||||
|
*/
|
||||||
|
void RestartCurrentRogueSkillDemo();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 判断当前是否处于 Rogue 技能演示模式。
|
||||||
|
* @return 演示模式中返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
|
bool IsRogueSkillDemoMode();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 推进 Rogue 技能演示计时。
|
||||||
|
* @return 演示模式正在运行返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
|
bool TickRogueSkillDemo();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 切换到下一项 Rogue 技能演示。
|
||||||
|
*/
|
||||||
|
void AdvanceRogueSkillDemo();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取 Rogue 技能演示条目数量。
|
||||||
|
* @return 可选择的演示条目总数。
|
||||||
|
*/
|
||||||
|
int GetRogueSkillDemoCount();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取指定 Rogue 技能演示名称。
|
||||||
|
* @param demoIndex 技能演示序号。
|
||||||
|
* @return 名称字符串,越界时返回空字符串。
|
||||||
|
*/
|
||||||
|
const TCHAR* GetRogueSkillDemoName(int demoIndex);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取指定 Rogue 技能演示说明。
|
||||||
|
* @param demoIndex 技能演示序号。
|
||||||
|
* @return 说明字符串,越界时返回空字符串。
|
||||||
|
*/
|
||||||
|
const TCHAR* GetRogueSkillDemoDetail(int demoIndex);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取当前 Rogue 技能演示名称。
|
||||||
|
* @return 当前名称,非演示模式返回空字符串。
|
||||||
|
*/
|
||||||
|
const TCHAR* GetCurrentRogueSkillDemoName();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置右侧战斗日志反馈信息。
|
||||||
|
* @param title 反馈标题。
|
||||||
|
* @param detail 反馈详情。
|
||||||
|
* @param ticks 保持显示的游戏计时次数。
|
||||||
|
*/
|
||||||
void SetFeedbackMessage(const TCHAR* title, const TCHAR* detail, int ticks);
|
void SetFeedbackMessage(const TCHAR* title, const TCHAR* detail, int ticks);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 打开帮助首页。
|
||||||
|
*/
|
||||||
void OpenRulesScreen();
|
void OpenRulesScreen();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 打开 Rogue 技能演示选择页。
|
||||||
|
*/
|
||||||
|
void OpenSkillDemoScreen();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 打开致谢页。
|
||||||
|
*/
|
||||||
|
void OpenCreditScreen();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 切换致谢页图片。
|
||||||
|
* @param direction 小于 0 向前切换,大于 0 向后切换。
|
||||||
|
*/
|
||||||
|
void ChangeCreditPage(int direction);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 打开 Rogue 升级选择界面。
|
||||||
|
*/
|
||||||
void OpenUpgradeMenu();
|
void OpenUpgradeMenu();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 确认当前升级选择并恢复游戏流程。
|
||||||
|
*/
|
||||||
void ConfirmUpgradeSelection();
|
void ConfirmUpgradeSelection();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 重置升级选择界面状态。
|
||||||
|
*/
|
||||||
|
void ResetUpgradeUiState();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 使用或解锁后处理 Hold 备用仓逻辑。
|
||||||
|
*/
|
||||||
void HoldCurrentPiece();
|
void HoldCurrentPiece();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 使用清屏炸弹主动技能。
|
||||||
|
*/
|
||||||
void UseScreenBomb();
|
void UseScreenBomb();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 使用黑洞主动技能。
|
||||||
|
*/
|
||||||
void UseBlackHole();
|
void UseBlackHole();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 使用空中换形主动技能。
|
||||||
|
*/
|
||||||
void UseAirReshape();
|
void UseAirReshape();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 重置 Rogue 待播放视觉事件。
|
||||||
|
*/
|
||||||
void ResetPendingRogueVisualEvents();
|
void ResetPendingRogueVisualEvents();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 清空所有视觉效果状态。
|
||||||
|
*/
|
||||||
void ResetVisualEffects();
|
void ResetVisualEffects();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 推进视觉效果动画。
|
||||||
|
* @return 仍有动画需要刷新返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
bool TickVisualEffects();
|
bool TickVisualEffects();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 推进致谢页切换动画。
|
||||||
|
* @return 需要刷新界面返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
|
bool TickCreditAnimation();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 触发标准消行动画。
|
||||||
|
* @param rows 被消除的行号数组。
|
||||||
|
* @param rowCount 行号数量。
|
||||||
|
* @param linesCleared 实际消除行数。
|
||||||
|
*/
|
||||||
void TriggerLineClearEffect(const int* rows, int rowCount, int linesCleared);
|
void TriggerLineClearEffect(const int* rows, int rowCount, int linesCleared);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 播放之前因升级界面暂存的消行动画。
|
||||||
|
*/
|
||||||
void PlayPendingLineClearEffect();
|
void PlayPendingLineClearEffect();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 触发指定棋盘格的默认清除特效。
|
||||||
|
* @param cells 被清除格子数组。
|
||||||
|
* @param cellCount 格子数量。
|
||||||
|
* @param strongBurst 是否使用更强的爆裂粒子。
|
||||||
|
*/
|
||||||
void TriggerCellClearEffect(const Point* cells, int cellCount, bool strongBurst);
|
void TriggerCellClearEffect(const Point* cells, int cellCount, bool strongBurst);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 触发指定棋盘格的自定义颜色清除特效。
|
||||||
|
* @param cells 被清除格子数组。
|
||||||
|
* @param cellCount 格子数量。
|
||||||
|
* @param flashColor 高亮颜色。
|
||||||
|
* @param strongBurst 是否使用更强的爆裂粒子。
|
||||||
|
*/
|
||||||
|
void TriggerColoredCellClearEffect(const Point* cells, int cellCount, COLORREF flashColor, bool strongBurst);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 记录一个固定方块受重力下落的轨迹。
|
||||||
|
* @param x 棋盘列号。
|
||||||
|
* @param fromY 下落起始行号。
|
||||||
|
* @param toY 下落目标行号。
|
||||||
|
* @param cellValue 方块格子数值。
|
||||||
|
*/
|
||||||
|
void TriggerGravityFallEffect(int x, int fromY, int toY, int cellValue);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 为 Rogue 主动或特殊技能清除格子发放奖励。
|
||||||
|
* @param clearedCells 清除格子数。
|
||||||
|
* @param scoreGain 返回本次得分增量。
|
||||||
|
* @param expGain 返回本次经验增量。
|
||||||
|
* @param allowLevelProgress 是否允许本次奖励触发升级流程。
|
||||||
|
*/
|
||||||
void AwardRogueSkillClearRewards(int clearedCells, int& scoreGain, int& expGain, bool allowLevelProgress);
|
void AwardRogueSkillClearRewards(int clearedCells, int& scoreGain, int& expGain, bool allowLevelProgress);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 检查 Rogue 经验是否达到升级条件。
|
||||||
|
*/
|
||||||
|
void CheckRogueLevelProgress();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 对棋盘固定方块应用重力下落。
|
||||||
|
*/
|
||||||
void ApplyBoardGravity();
|
void ApplyBoardGravity();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 计算当前 Rogue 模式下落间隔。
|
||||||
|
* @return 下落计时器间隔,单位毫秒。
|
||||||
|
*/
|
||||||
int GetRogueFallInterval();
|
int GetRogueFallInterval();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取 Rogue 当前可操作棋盘高度。
|
||||||
|
* @return 未被底部封锁占用的行数。
|
||||||
|
*/
|
||||||
int GetRoguePlayableHeight();
|
int GetRoguePlayableHeight();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取 Rogue 难度系统当前封锁的底部行数。
|
||||||
|
* @return 封锁行数。
|
||||||
|
*/
|
||||||
int GetRogueLockedRows();
|
int GetRogueLockedRows();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 按经过时间推进 Rogue 难度。
|
||||||
|
* @param elapsedMs 本次推进的时间,单位毫秒。
|
||||||
|
*/
|
||||||
void AdvanceRogueDifficulty(int elapsedMs);
|
void AdvanceRogueDifficulty(int elapsedMs);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取进化强化的合成路线文本。
|
||||||
|
* @param upgradeId 强化编号。
|
||||||
|
* @return 路线文本;普通强化返回空或空指针。
|
||||||
|
*/
|
||||||
const TCHAR* GetUpgradeSynthesisPath(int upgradeId);
|
const TCHAR* GetUpgradeSynthesisPath(int upgradeId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 绘制当前窗口中的完整游戏界面。
|
||||||
|
* @param hdc 目标绘图设备上下文。
|
||||||
|
* @param hWnd 当前窗口句柄,用于读取客户区大小。
|
||||||
|
*/
|
||||||
void TDrawScreen(HDC hdc, HWND hWnd);
|
void TDrawScreen(HDC hdc, HWND hWnd);
|
||||||
|
|||||||
@@ -0,0 +1,227 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file TetrisAppInternal.h
|
||||||
|
* @brief 声明窗口布局、输入、计时器和媒体播放等应用层内部接口。
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "Tetris.h"
|
||||||
|
|
||||||
|
constexpr int GAME_TIMER_ID = 1;
|
||||||
|
constexpr int EFFECT_TIMER_ID = 2;
|
||||||
|
constexpr int CREDIT_TIMER_ID = 3;
|
||||||
|
constexpr int WM_CREDIT_TICK = WM_APP + 1;
|
||||||
|
constexpr int GAME_TIMER_INTERVAL = 500;
|
||||||
|
constexpr int EFFECT_TIMER_INTERVAL = 16;
|
||||||
|
constexpr int CREDIT_TIMER_INTERVAL = 5;
|
||||||
|
|
||||||
|
struct LayoutMetrics
|
||||||
|
{
|
||||||
|
int scale;
|
||||||
|
int offsetX;
|
||||||
|
int offsetY;
|
||||||
|
int layoutWidth;
|
||||||
|
int layoutHeight;
|
||||||
|
int grid;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 根据当前窗口大小计算整体界面缩放与偏移。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @return 布局缩放、偏移和网格尺寸。
|
||||||
|
*/
|
||||||
|
LayoutMetrics GetLayoutMetrics(HWND hWnd);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 按当前布局比例缩放一个尺寸值。
|
||||||
|
* @param metrics 当前布局参数。
|
||||||
|
* @param value 设计稿尺寸值。
|
||||||
|
* @return 缩放后的像素尺寸。
|
||||||
|
*/
|
||||||
|
int ScaleValue(const LayoutMetrics& metrics, int value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 按当前布局比例缩放横坐标并叠加窗口偏移。
|
||||||
|
* @param metrics 当前布局参数。
|
||||||
|
* @param value 设计稿横坐标。
|
||||||
|
* @return 实际窗口横坐标。
|
||||||
|
*/
|
||||||
|
int ScaleXValue(const LayoutMetrics& metrics, int value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 按当前布局比例缩放纵坐标并叠加窗口偏移。
|
||||||
|
* @param metrics 当前布局参数。
|
||||||
|
* @param value 设计稿纵坐标。
|
||||||
|
* @return 实际窗口纵坐标。
|
||||||
|
*/
|
||||||
|
int ScaleYValue(const LayoutMetrics& metrics, int value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取主菜单选项的点击区域。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @param index 菜单选项序号。
|
||||||
|
* @return 选项在窗口中的矩形区域。
|
||||||
|
*/
|
||||||
|
RECT GetMenuOptionRect(HWND hWnd, int index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取帮助页选项的点击区域。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @param index 帮助首页选项序号。
|
||||||
|
* @return 选项在窗口中的矩形区域。
|
||||||
|
*/
|
||||||
|
RECT GetHelpOptionRect(HWND hWnd, int index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取技能演示列表项的点击区域。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @param index 技能演示条目序号。
|
||||||
|
* @return 条目在窗口中的矩形区域。
|
||||||
|
*/
|
||||||
|
RECT GetHelpSkillDemoItemRect(HWND hWnd, int index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取帮助页底部返回提示的点击区域。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @return 返回提示在窗口中的矩形区域。
|
||||||
|
*/
|
||||||
|
RECT GetHelpBackHintRect(HWND hWnd);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取致谢页左右切换按钮的点击区域。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @param direction 小于 0 表示左箭头,大于 0 表示右箭头。
|
||||||
|
* @return 切换按钮在窗口中的矩形区域。
|
||||||
|
*/
|
||||||
|
RECT GetCreditArrowRect(HWND hWnd, int direction);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取升级选择卡片的点击区域。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @param index 强化卡片序号。
|
||||||
|
* @return 卡片在窗口中的矩形区域。
|
||||||
|
*/
|
||||||
|
RECT GetUpgradeCardRect(HWND hWnd, int index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取暂停或结束覆盖层按钮的点击区域。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @param index 按钮序号。
|
||||||
|
* @param buttonCount 覆盖层当前按钮总数。
|
||||||
|
* @return 按钮在窗口中的矩形区域。
|
||||||
|
*/
|
||||||
|
RECT GetOverlayButtonRect(HWND hWnd, int index, int buttonCount);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取左上角返回按钮的点击区域。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @return 返回按钮在窗口中的矩形区域。
|
||||||
|
*/
|
||||||
|
RECT GetBackButtonRect(HWND hWnd);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取右下角音乐按钮的点击区域。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @return 音乐按钮在窗口中的矩形区域。
|
||||||
|
*/
|
||||||
|
RECT GetMusicButtonRect(HWND hWnd);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 判断点坐标是否落在矩形内部。
|
||||||
|
* @param rect 待判断矩形。
|
||||||
|
* @param x 点的横坐标。
|
||||||
|
* @param y 点的纵坐标。
|
||||||
|
* @return 点在矩形内返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
|
bool IsPointInRect(const RECT& rect, int x, int y);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 将滚动偏移按步长调整并限制在有效范围内。
|
||||||
|
* @param scrollOffset 需要修改的滚动偏移。
|
||||||
|
* @param delta 本次滚动增量。
|
||||||
|
*/
|
||||||
|
void AdjustScrollOffset(int& scrollOffset, int delta);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取适配当前窗口缩放的一次滚动步长。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @param baseStep 设计稿中的基础滚动步长。
|
||||||
|
* @return 缩放后的滚动步长。
|
||||||
|
*/
|
||||||
|
int GetScrollStep(HWND hWnd, int baseStep);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 重置主下落定时器。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
*/
|
||||||
|
void ResetGameTimer(HWND hWnd);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 启动游戏、特效和致谢页动画定时器。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
*/
|
||||||
|
void StartAppTimers(HWND hWnd);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 停止游戏、特效和致谢页动画定时器。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
*/
|
||||||
|
void StopAppTimers(HWND hWnd);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 处理致谢页高频动画刷新消息。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
*/
|
||||||
|
void HandleCreditTick(HWND hWnd);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 处理窗口定时器消息。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @param timerId 触发的定时器编号。
|
||||||
|
*/
|
||||||
|
void HandleTimerMessage(HWND hWnd, WPARAM timerId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 启动背景音乐。
|
||||||
|
*/
|
||||||
|
void StartBackgroundMusic();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 停止背景音乐。
|
||||||
|
*/
|
||||||
|
void StopBackgroundMusic();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 切换背景音乐开关并刷新窗口。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
*/
|
||||||
|
void ToggleBackgroundMusic(HWND hWnd);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 播放复活视频,播放成功返回 true。
|
||||||
|
* @param hWnd 当前窗口句柄,用于 MCI 播放和父窗口绑定。
|
||||||
|
* @return 播放成功返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
|
bool PlayReviveVideo(HWND hWnd);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 处理鼠标左键释放事件,返回是否已处理。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @param lParam 鼠标消息坐标参数。
|
||||||
|
* @return 事件已被界面逻辑消费返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
|
bool HandleMouseClick(HWND hWnd, LPARAM lParam);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 处理鼠标滚轮事件。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @param wParam 鼠标滚轮消息参数。
|
||||||
|
*/
|
||||||
|
void HandleMouseWheel(HWND hWnd, WPARAM wParam);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 处理键盘按键事件。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @param wParam 按键虚拟键码。
|
||||||
|
*/
|
||||||
|
void HandleKeyDown(HWND hWnd, WPARAM wParam);
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file TetrisAssets.h
|
||||||
|
* @brief 声明资源路径拼接和文件存在性检查工具函数。
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "stdafx.h"
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 根据程序所在目录拼出项目资源文件的绝对路径。
|
||||||
|
* @param relativePath 相对于项目根目录的资源路径。
|
||||||
|
* @return 规范化后的绝对路径;解析失败时返回拼接路径。
|
||||||
|
*/
|
||||||
|
std::wstring BuildAssetPath(const wchar_t* relativePath);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 根据当前工作目录拼出项目资源文件的绝对路径。
|
||||||
|
* @param relativePath 相对于当前工作目录的资源路径。
|
||||||
|
* @return 规范化后的绝对路径;解析失败时返回拼接路径。
|
||||||
|
*/
|
||||||
|
std::wstring BuildWorkingDirAssetPath(const wchar_t* relativePath);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 判断指定路径是否存在且不是目录。
|
||||||
|
* @param path 待检查的文件路径。
|
||||||
|
* @return 文件存在且不是目录返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
|
bool FileExists(const std::wstring& path);
|
||||||
@@ -1,23 +1,177 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file TetrisLogicInternal.h
|
||||||
|
* @brief 声明棋盘逻辑、Rogue 结算和特殊方块效果使用的内部接口。
|
||||||
|
*/
|
||||||
|
|
||||||
#include "Tetris.h"
|
#include "Tetris.h"
|
||||||
|
|
||||||
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 计算指定方块在棋盘顶部的统一生成位置。
|
||||||
|
* @param brickType 方块类型编号。
|
||||||
|
* @return 生成坐标,可能位于可视区域上方。
|
||||||
|
*/
|
||||||
Point GetSpawnPoint(int brickType);
|
Point GetSpawnPoint(int brickType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 重置经典或 Rogue 模式使用的玩家统计数据。
|
||||||
|
* @param stats 需要重置的统计结构。
|
||||||
|
* @param useRogueRules 是否按 Rogue 模式设置初始经验需求。
|
||||||
|
*/
|
||||||
void ResetPlayerStats(PlayerStats& stats, bool useRogueRules);
|
void ResetPlayerStats(PlayerStats& stats, bool useRogueRules);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 设置界面右侧显示的即时反馈标题、内容和持续时间。
|
||||||
|
* @param title 反馈标题。
|
||||||
|
* @param detail 反馈详情。
|
||||||
|
* @param ticks 显示持续的游戏计时次数。
|
||||||
|
*/
|
||||||
void SetFeedbackMessage(const TCHAR* title, const TCHAR* detail, int ticks);
|
void SetFeedbackMessage(const TCHAR* title, const TCHAR* detail, int ticks);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 判断指定方块、旋转状态和位置是否可以合法放置。
|
||||||
|
* @param pieceType 方块类型编号。
|
||||||
|
* @param pieceState 方块旋转状态。
|
||||||
|
* @param position 待检测的左上角坐标。
|
||||||
|
* @return 可以放置返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
bool IsPiecePlacementValid(int pieceType, int pieceState, Point position);
|
bool IsPiecePlacementValid(int pieceType, int pieceState, Point position);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 判断棋盘格是否为彩虹特殊方块。
|
||||||
|
* @param cellValue 棋盘格存储值。
|
||||||
|
* @return 彩虹方块返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
bool IsRainbowBoardCell(int cellValue);
|
bool IsRainbowBoardCell(int cellValue);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 触发小型黑洞并返回被清除的固定方块数量。
|
||||||
|
* @param maxCellsToClear 最多清除的格子数。
|
||||||
|
* @return 实际清除格子数。
|
||||||
|
*/
|
||||||
int TriggerMiniBlackHole(int maxCellsToClear);
|
int TriggerMiniBlackHole(int maxCellsToClear);
|
||||||
int TriggerRainbowRowCompletion(int minRow, int maxRow);
|
|
||||||
|
/**
|
||||||
|
* @brief 触发彩虹方块行清除与覆盖行染色效果。
|
||||||
|
* @param anchorRow 作为主色判断的中心行。
|
||||||
|
* @param minRow 允许染色范围的最小行。
|
||||||
|
* @param maxRow 允许染色范围的最大行。
|
||||||
|
* @param recoloredCount 返回被染色的格子数。
|
||||||
|
* @return 被清除的主色格子数。
|
||||||
|
*/
|
||||||
|
int TriggerRainbowColorShift(int anchorRow, int minRow, int maxRow, int& recoloredCount);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 引爆清屏炸弹并返回清除格数。
|
||||||
|
* @return 实际清除格子数。
|
||||||
|
*/
|
||||||
int TriggerScreenBomb();
|
int TriggerScreenBomb();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 清除指定中心点周围的爆破范围并返回清除格数。
|
||||||
|
* @param centerY 爆破中心行。
|
||||||
|
* @param centerX 爆破中心列。
|
||||||
|
* @return 实际清除格子数。
|
||||||
|
*/
|
||||||
int ClearExplosiveAreaAt(int centerY, int centerX);
|
int ClearExplosiveAreaAt(int centerY, int centerX);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 清除指定列并返回清除格数。
|
||||||
|
* @param column 目标列号。
|
||||||
|
* @return 实际清除格子数。
|
||||||
|
*/
|
||||||
int ClearColumnAt(int column);
|
int ClearColumnAt(int column);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 使用指定颜色特效清除指定列并返回清除格数。
|
||||||
|
* @param column 目标列号。
|
||||||
|
* @param flashColor 清除高亮颜色。
|
||||||
|
* @return 实际清除格子数。
|
||||||
|
*/
|
||||||
|
int ClearColumnAtWithColor(int column, COLORREF flashColor);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 清除指定行并返回清除格数。
|
||||||
|
* @param row 目标行号。
|
||||||
|
* @return 实际清除格子数。
|
||||||
|
*/
|
||||||
int ClearRowAt(int row);
|
int ClearRowAt(int row);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 尝试填补局部空洞以稳定棋盘结构。
|
||||||
|
* @return 实际填补格子数。
|
||||||
|
*/
|
||||||
int TryStabilizeBoard();
|
int TryStabilizeBoard();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 为当前方块刷新 Rogue 特殊方块标记。
|
||||||
|
* @param allowRandomSpecials 是否允许按强化概率随机生成特殊方块。
|
||||||
|
*/
|
||||||
void RollCurrentPieceSpecialFlags(bool allowRandomSpecials);
|
void RollCurrentPieceSpecialFlags(bool allowRandomSpecials);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 暂存消行动画,等待升级选择结束后再播放。
|
||||||
|
* @param rows 被消除的行号数组。
|
||||||
|
* @param rowCount 行号数量。
|
||||||
|
* @param linesCleared 实际消除行数。
|
||||||
|
*/
|
||||||
|
void QueueLineClearEffect(const int* rows, int rowCount, int linesCleared);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 记录固定方块受重力下落的轨迹,用于播放纵向残影特效。
|
||||||
|
* @param x 棋盘列号。
|
||||||
|
* @param fromY 起始行号。
|
||||||
|
* @param toY 目标行号。
|
||||||
|
* @param cellValue 方块格子值。
|
||||||
|
*/
|
||||||
|
void TriggerGravityFallEffect(int x, int fromY, int toY, int cellValue);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 尝试把旋转后的方块横向偏移指定格数后放置。
|
||||||
|
* @param nextState 旋转后的状态编号。
|
||||||
|
* @param offsetX 横向试探偏移。
|
||||||
|
* @return 偏移后可以放置返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
|
bool TryRotateWithOffset(int nextState, int offsetX);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 重置下一方块预览队列。
|
||||||
|
*/
|
||||||
void ResetNextQueue();
|
void ResetNextQueue();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 消费队首下一方块并补充新的预览方块。
|
||||||
|
* @return 新的当前方块类型编号。
|
||||||
|
*/
|
||||||
int ConsumeNextType();
|
int ConsumeNextType();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 结算一次标准消行带来的 Rogue 玩法效果。
|
||||||
|
* @param linesCleared 本次标准消行数量。
|
||||||
|
*/
|
||||||
void ApplyLineClearResult(int linesCleared);
|
void ApplyLineClearResult(int linesCleared);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 结算彩虹方块固定后的染色和清除效果。
|
||||||
|
* @param overflowTop 固定时是否已经越过顶部。
|
||||||
|
* @param fixedCells 当前方块写入棋盘的格子数组。
|
||||||
|
* @param fixedCellCount 写入棋盘的格子数量。
|
||||||
|
*/
|
||||||
|
void ApplyRainbowLandingEffect(bool overflowTop, const Point* fixedCells, int fixedCellCount);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 结算爆破、激光、十字和稳定结构等特殊落地效果。
|
||||||
|
* @param fixedCells 当前方块写入棋盘的格子数组。
|
||||||
|
* @param fixedCellCount 写入棋盘的格子数量。
|
||||||
|
* @param explosiveCells 爆破方块写入棋盘的格子数组。
|
||||||
|
* @param explosiveCellCount 爆破格子数量。
|
||||||
|
*/
|
||||||
|
void ApplySpecialLandingEffects(const Point* fixedCells, int fixedCellCount, const Point* explosiveCells, int explosiveCellCount);
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file TetrisRenderInternal.h
|
||||||
|
* @brief 声明渲染模块内部使用的 GDI+ 图片加载接口。
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "Tetris.h"
|
||||||
|
#include <objidl.h>
|
||||||
|
#include <gdiplus.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 加载并缓存主背景图片。
|
||||||
|
* @return 成功时返回缓存位图指针,失败时返回 nullptr。
|
||||||
|
*/
|
||||||
|
Gdiplus::Bitmap* LoadBackgroundImage();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 按序号加载并缓存致谢页图片。
|
||||||
|
* @param index 致谢页图片序号。
|
||||||
|
* @return 成功时返回缓存位图指针,失败或越界时返回 nullptr。
|
||||||
|
*/
|
||||||
|
Gdiplus::Bitmap* LoadCreditImage(int index);
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file resource.h
|
||||||
|
* @brief 定义菜单、图标、对话框和命令等 Windows 资源编号。
|
||||||
|
*/
|
||||||
|
|
||||||
//{{NO_DEPENDENCIES}}
|
//{{NO_DEPENDENCIES}}
|
||||||
// Microsoft Visual C++ 生成的包含文件。
|
// Microsoft Visual C++ 生成的包含文件。
|
||||||
// 供 Tetris.rc 使用
|
// 供 Tetris.rc 使用
|
||||||
@@ -12,6 +17,7 @@
|
|||||||
#define IDD_ABOUTBOX 103
|
#define IDD_ABOUTBOX 103
|
||||||
#define IDM_ABOUT 104
|
#define IDM_ABOUT 104
|
||||||
#define IDM_EXIT 105
|
#define IDM_EXIT 105
|
||||||
|
#define IDM_SKILL_DEMO 106
|
||||||
#define IDI_TETRIS 107
|
#define IDI_TETRIS 107
|
||||||
#define IDI_SMALL 108
|
#define IDI_SMALL 108
|
||||||
#define IDC_TETRIS 109
|
#define IDC_TETRIS 109
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// stdafx.h : 标准系统包含文件的包含文件,
|
/**
|
||||||
// 或是经常使用但不常更改的
|
* @file stdafx.h
|
||||||
// 特定于项目的包含文件
|
* @brief 集中包含 Windows、C 运行时和项目常用基础头文件。
|
||||||
//
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file targetver.h
|
||||||
|
* @brief 设置 Windows SDK 目标平台版本,供 Win32 头文件选择可用 API。
|
||||||
|
*/
|
||||||
|
|
||||||
// 包括 SDKDDKVer.h 将定义可用的最高版本的 Windows 平台。
|
// 包括 SDKDDKVer.h 将定义可用的最高版本的 Windows 平台。
|
||||||
|
|
||||||
// 如果要为以前的 Windows 平台生成应用程序,请包括 WinSDKVer.h,并将
|
// 如果要为以前的 Windows 平台生成应用程序,请包括 WinSDKVer.h,并将
|
||||||
|
|||||||
Binary file not shown.
+101
-805
File diff suppressed because it is too large
Load Diff
+232
-720
File diff suppressed because it is too large
Load Diff
+909
-204
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,969 @@
|
|||||||
|
#include "stdafx.h"
|
||||||
|
/**
|
||||||
|
* @file TetrisInput.cpp
|
||||||
|
* @brief 实现鼠标和键盘输入处理,负责菜单、帮助、升级界面和游戏操作分发。
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "TetrisAppInternal.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 打开当前菜单选中的页面或开始对应模式。
|
||||||
|
* @param hWnd 当前窗口句柄,用于重置计时器和触发重绘。
|
||||||
|
*/
|
||||||
|
static void ActivateMenuSelection(HWND hWnd)
|
||||||
|
{
|
||||||
|
if (menuState.selectedIndex == 0)
|
||||||
|
{
|
||||||
|
StartGameWithMode(MODE_CLASSIC);
|
||||||
|
}
|
||||||
|
else if (menuState.selectedIndex == 1)
|
||||||
|
{
|
||||||
|
StartGameWithMode(MODE_ROGUE);
|
||||||
|
}
|
||||||
|
else if (menuState.selectedIndex == 2)
|
||||||
|
{
|
||||||
|
OpenRulesScreen();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
OpenCreditScreen();
|
||||||
|
}
|
||||||
|
ResetGameTimer(hWnd);
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 处理返回按钮的统一点击行为。
|
||||||
|
* @param hWnd 当前窗口句柄,用于触发重绘。
|
||||||
|
*/
|
||||||
|
static void HandleBackButtonClick(HWND hWnd)
|
||||||
|
{
|
||||||
|
if (currentScreen == SCREEN_PLAYING && IsRogueSkillDemoMode())
|
||||||
|
{
|
||||||
|
OpenSkillDemoScreen();
|
||||||
|
}
|
||||||
|
else if (currentScreen == SCREEN_RULES && helpState.currentPage != 0)
|
||||||
|
{
|
||||||
|
helpState.selectedIndex = (helpState.currentPage == 5) ? 3 : helpState.currentPage - 1;
|
||||||
|
helpState.currentPage = 0;
|
||||||
|
helpScrollOffset = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ReturnToMainMenu();
|
||||||
|
}
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 处理主菜单点击。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @param mouseX 鼠标横坐标。
|
||||||
|
* @param mouseY 鼠标纵坐标。
|
||||||
|
* @return 当前界面是菜单时返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
|
static bool HandleMenuClick(HWND hWnd, int mouseX, int mouseY)
|
||||||
|
{
|
||||||
|
if (currentScreen != SCREEN_MENU)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < menuState.optionCount; i++)
|
||||||
|
{
|
||||||
|
if (!IsPointInRect(GetMenuOptionRect(hWnd, i), mouseX, mouseY))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
menuState.selectedIndex = i;
|
||||||
|
ActivateMenuSelection(hWnd);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 处理规则、帮助、致谢和技能演示页点击。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @param mouseX 鼠标横坐标。
|
||||||
|
* @param mouseY 鼠标纵坐标。
|
||||||
|
* @return 当前界面是帮助页时返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
|
static bool HandleRulesClick(HWND hWnd, int mouseX, int mouseY)
|
||||||
|
{
|
||||||
|
if (currentScreen != SCREEN_RULES)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (helpState.currentPage == 0)
|
||||||
|
{
|
||||||
|
// 帮助首页的四个入口分别进入介绍、操作、图鉴和技能演示页。
|
||||||
|
for (int i = 0; i < helpState.optionCount; i++)
|
||||||
|
{
|
||||||
|
if (IsPointInRect(GetHelpOptionRect(hWnd, i), mouseX, mouseY))
|
||||||
|
{
|
||||||
|
helpState.selectedIndex = i;
|
||||||
|
if (i == 3)
|
||||||
|
{
|
||||||
|
helpState.currentPage = 5;
|
||||||
|
helpState.selectedIndex = 0;
|
||||||
|
helpScrollOffset = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
helpState.currentPage = i + 1;
|
||||||
|
helpScrollOffset = 0;
|
||||||
|
}
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (IsPointInRect(GetHelpBackHintRect(hWnd), mouseX, mouseY))
|
||||||
|
{
|
||||||
|
ReturnToMainMenu();
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (helpState.currentPage == 5)
|
||||||
|
{
|
||||||
|
if (IsPointInRect(GetHelpBackHintRect(hWnd), mouseX, mouseY))
|
||||||
|
{
|
||||||
|
helpState.currentPage = 0;
|
||||||
|
helpState.selectedIndex = 3;
|
||||||
|
helpScrollOffset = 0;
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 技能演示页的列表项直接启动对应预设棋盘。
|
||||||
|
int demoCount = GetRogueSkillDemoCount();
|
||||||
|
for (int i = 0; i < demoCount; i++)
|
||||||
|
{
|
||||||
|
if (IsPointInRect(GetHelpSkillDemoItemRect(hWnd, i), mouseX, mouseY))
|
||||||
|
{
|
||||||
|
helpState.selectedIndex = i;
|
||||||
|
StartRogueSkillDemoAt(i);
|
||||||
|
ResetGameTimer(hWnd);
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsPointInRect(GetHelpBackHintRect(hWnd), mouseX, mouseY))
|
||||||
|
{
|
||||||
|
helpState.selectedIndex = (helpState.currentPage == 5) ? 3 : helpState.currentPage - 1;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 处理升级选择界面点击。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @param mouseX 鼠标横坐标。
|
||||||
|
* @param mouseY 鼠标纵坐标。
|
||||||
|
* @return 当前界面是升级选择时返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
|
static bool HandleUpgradeClick(HWND hWnd, int mouseX, int mouseY)
|
||||||
|
{
|
||||||
|
if (currentScreen != SCREEN_UPGRADE)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 处理暂停和结束覆盖层点击。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @param mouseX 鼠标横坐标。
|
||||||
|
* @param mouseY 鼠标纵坐标。
|
||||||
|
* @return 点击命中覆盖层按钮返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
|
static bool HandleOverlayClick(HWND hWnd, int mouseX, int mouseY)
|
||||||
|
{
|
||||||
|
if (currentScreen == SCREEN_PLAYING && suspendFlag)
|
||||||
|
{
|
||||||
|
if (IsPointInRect(GetOverlayButtonRect(hWnd, 0, 2), mouseX, mouseY))
|
||||||
|
{
|
||||||
|
suspendFlag = false;
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (IsPointInRect(GetOverlayButtonRect(hWnd, 1, 2), mouseX, mouseY))
|
||||||
|
{
|
||||||
|
ReturnToMainMenu();
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (IsPointInRect(GetOverlayButtonRect(hWnd, 1, 3), mouseX, mouseY))
|
||||||
|
{
|
||||||
|
StartGameWithMode(currentMode);
|
||||||
|
ResetGameTimer(hWnd);
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (IsPointInRect(GetOverlayButtonRect(hWnd, 2, 3), mouseX, mouseY))
|
||||||
|
{
|
||||||
|
ReturnToMainMenu();
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (IsPointInRect(GetOverlayButtonRect(hWnd, 0, 2), mouseX, mouseY))
|
||||||
|
{
|
||||||
|
StartGameWithMode(currentMode);
|
||||||
|
ResetGameTimer(hWnd);
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (IsPointInRect(GetOverlayButtonRect(hWnd, 1, 2), mouseX, mouseY))
|
||||||
|
{
|
||||||
|
ReturnToMainMenu();
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 处理鼠标左键释放事件,返回是否已处理。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @param lParam 鼠标消息坐标参数。
|
||||||
|
* @return 事件已被界面逻辑消费返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
|
bool HandleMouseClick(HWND hWnd, LPARAM lParam)
|
||||||
|
{
|
||||||
|
int mouseX = static_cast<short>(LOWORD(lParam));
|
||||||
|
int mouseY = static_cast<short>(HIWORD(lParam));
|
||||||
|
if (IsPointInRect(GetMusicButtonRect(hWnd), mouseX, mouseY))
|
||||||
|
{
|
||||||
|
ToggleBackgroundMusic(hWnd);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentScreen != SCREEN_MENU && IsPointInRect(GetBackButtonRect(hWnd), mouseX, mouseY))
|
||||||
|
{
|
||||||
|
HandleBackButtonClick(hWnd);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (HandleMenuClick(hWnd, mouseX, mouseY) ||
|
||||||
|
HandleRulesClick(hWnd, mouseX, mouseY) ||
|
||||||
|
HandleUpgradeClick(hWnd, mouseX, mouseY) ||
|
||||||
|
HandleOverlayClick(hWnd, mouseX, mouseY))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 处理鼠标滚轮事件。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @param wParam 鼠标滚轮消息参数。
|
||||||
|
*/
|
||||||
|
void HandleMouseWheel(HWND hWnd, WPARAM wParam)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentScreen == SCREEN_PLAYING && currentMode == MODE_ROGUE)
|
||||||
|
{
|
||||||
|
AdjustScrollOffset(upgradeListScrollOffset, direction * scrollStep);
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 处理主菜单键盘导航。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @param key 按键虚拟键码。
|
||||||
|
* @return 当前界面是菜单时返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
|
static bool HandleMenuKey(HWND hWnd, WPARAM key)
|
||||||
|
{
|
||||||
|
if (currentScreen != SCREEN_MENU)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (key)
|
||||||
|
{
|
||||||
|
case VK_UP:
|
||||||
|
case VK_LEFT:
|
||||||
|
case 'W':
|
||||||
|
case 'A':
|
||||||
|
menuState.selectedIndex--;
|
||||||
|
if (menuState.selectedIndex < 0)
|
||||||
|
{
|
||||||
|
menuState.selectedIndex = menuState.optionCount - 1;
|
||||||
|
}
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
break;
|
||||||
|
case VK_DOWN:
|
||||||
|
case VK_RIGHT:
|
||||||
|
case 'S':
|
||||||
|
case 'D':
|
||||||
|
menuState.selectedIndex++;
|
||||||
|
if (menuState.selectedIndex >= menuState.optionCount)
|
||||||
|
{
|
||||||
|
menuState.selectedIndex = 0;
|
||||||
|
}
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
break;
|
||||||
|
case VK_RETURN:
|
||||||
|
case VK_SPACE:
|
||||||
|
ActivateMenuSelection(hWnd);
|
||||||
|
break;
|
||||||
|
case VK_ESCAPE:
|
||||||
|
DestroyWindow(hWnd);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 在帮助首页选项之间循环移动。
|
||||||
|
* @param direction 负数向前,正数向后。
|
||||||
|
*/
|
||||||
|
static void MoveHelpHomeSelection(int direction)
|
||||||
|
{
|
||||||
|
helpState.selectedIndex += direction;
|
||||||
|
if (helpState.selectedIndex < 0)
|
||||||
|
{
|
||||||
|
helpState.selectedIndex = helpState.optionCount - 1;
|
||||||
|
}
|
||||||
|
if (helpState.selectedIndex >= helpState.optionCount)
|
||||||
|
{
|
||||||
|
helpState.selectedIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 在技能演示列表中移动选中项,并同步滚动偏移。
|
||||||
|
* @param direction 负数向上,正数向下。
|
||||||
|
*/
|
||||||
|
static void MoveSkillDemoSelection(int direction)
|
||||||
|
{
|
||||||
|
helpState.selectedIndex += direction;
|
||||||
|
if (helpState.selectedIndex < 0)
|
||||||
|
{
|
||||||
|
helpState.selectedIndex = GetRogueSkillDemoCount() - 1;
|
||||||
|
}
|
||||||
|
if (helpState.selectedIndex >= GetRogueSkillDemoCount())
|
||||||
|
{
|
||||||
|
helpState.selectedIndex = 0;
|
||||||
|
helpScrollOffset = 0;
|
||||||
|
}
|
||||||
|
else if (direction < 0 && helpState.selectedIndex * 68 < helpScrollOffset)
|
||||||
|
{
|
||||||
|
helpScrollOffset = helpState.selectedIndex * 68;
|
||||||
|
}
|
||||||
|
else if (direction > 0 && helpState.selectedIndex * 68 > helpScrollOffset + 360)
|
||||||
|
{
|
||||||
|
helpScrollOffset = helpState.selectedIndex * 68 - 360;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 打开帮助首页当前选中的子页面。
|
||||||
|
*/
|
||||||
|
static void ActivateHelpSelection()
|
||||||
|
{
|
||||||
|
if (helpState.selectedIndex == 3)
|
||||||
|
{
|
||||||
|
helpState.currentPage = 5;
|
||||||
|
helpState.selectedIndex = 0;
|
||||||
|
helpScrollOffset = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
helpState.currentPage = helpState.selectedIndex + 1;
|
||||||
|
helpScrollOffset = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 从帮助子页返回帮助首页或主菜单。
|
||||||
|
*/
|
||||||
|
static void LeaveRulesPage()
|
||||||
|
{
|
||||||
|
int previousPage = helpState.currentPage;
|
||||||
|
if (helpState.currentPage == 0)
|
||||||
|
{
|
||||||
|
ReturnToMainMenu();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
helpState.currentPage = 0;
|
||||||
|
if (previousPage == 4 || previousPage == 5)
|
||||||
|
{
|
||||||
|
helpState.selectedIndex = 3;
|
||||||
|
}
|
||||||
|
helpScrollOffset = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 处理帮助和致谢页键盘导航。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @param key 按键虚拟键码。
|
||||||
|
* @return 当前界面是帮助页时返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
|
static bool HandleRulesKey(HWND hWnd, WPARAM key)
|
||||||
|
{
|
||||||
|
if (currentScreen != SCREEN_RULES)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (key)
|
||||||
|
{
|
||||||
|
case VK_UP:
|
||||||
|
case VK_LEFT:
|
||||||
|
case 'W':
|
||||||
|
case 'A':
|
||||||
|
if (helpState.currentPage == 0)
|
||||||
|
{
|
||||||
|
MoveHelpHomeSelection(-1);
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
else if (helpState.currentPage == 4)
|
||||||
|
{
|
||||||
|
ChangeCreditPage(-1);
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
else if (helpState.currentPage == 5)
|
||||||
|
{
|
||||||
|
MoveSkillDemoSelection(-1);
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case VK_DOWN:
|
||||||
|
case VK_RIGHT:
|
||||||
|
case 'S':
|
||||||
|
case 'D':
|
||||||
|
if (helpState.currentPage == 0)
|
||||||
|
{
|
||||||
|
MoveHelpHomeSelection(1);
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
else if (helpState.currentPage == 4)
|
||||||
|
{
|
||||||
|
ChangeCreditPage(1);
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
else if (helpState.currentPage == 5)
|
||||||
|
{
|
||||||
|
MoveSkillDemoSelection(1);
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case VK_RETURN:
|
||||||
|
case VK_SPACE:
|
||||||
|
if (helpState.currentPage == 0)
|
||||||
|
{
|
||||||
|
ActivateHelpSelection();
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
else if (helpState.currentPage == 5)
|
||||||
|
{
|
||||||
|
StartRogueSkillDemoAt(helpState.selectedIndex);
|
||||||
|
ResetGameTimer(hWnd);
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case VK_ESCAPE:
|
||||||
|
case VK_BACK:
|
||||||
|
case 'M':
|
||||||
|
LeaveRulesPage();
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 计算升级卡片网格列数。
|
||||||
|
* @return 当前升级界面使用的列数,至少为 1。
|
||||||
|
*/
|
||||||
|
static int GetUpgradeColumnCount()
|
||||||
|
{
|
||||||
|
int upgradeColumnCount = upgradeUiState.optionCount <= 3 ? upgradeUiState.optionCount : 3;
|
||||||
|
if (upgradeColumnCount < 1)
|
||||||
|
{
|
||||||
|
upgradeColumnCount = 1;
|
||||||
|
}
|
||||||
|
return upgradeColumnCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 在升级卡片网格中横向移动选中项。
|
||||||
|
* @param direction 负数向左,正数向右。
|
||||||
|
*/
|
||||||
|
static void MoveUpgradeSelectionHorizontal(int direction)
|
||||||
|
{
|
||||||
|
if (upgradeUiState.optionCount <= 1)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int upgradeColumnCount = GetUpgradeColumnCount();
|
||||||
|
int rowStart = upgradeUiState.selectedIndex - (upgradeUiState.selectedIndex % upgradeColumnCount);
|
||||||
|
int rowEnd = rowStart + upgradeColumnCount - 1;
|
||||||
|
if (rowEnd >= upgradeUiState.optionCount)
|
||||||
|
{
|
||||||
|
rowEnd = upgradeUiState.optionCount - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (direction < 0)
|
||||||
|
{
|
||||||
|
upgradeUiState.selectedIndex = (upgradeUiState.selectedIndex > rowStart) ? upgradeUiState.selectedIndex - 1 : rowEnd;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
upgradeUiState.selectedIndex = (upgradeUiState.selectedIndex < rowEnd) ? upgradeUiState.selectedIndex + 1 : rowStart;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 在升级卡片网格中纵向移动选中项。
|
||||||
|
* @param direction 负数向上,正数向下。
|
||||||
|
*/
|
||||||
|
static void MoveUpgradeSelectionVertical(int direction)
|
||||||
|
{
|
||||||
|
int upgradeColumnCount = GetUpgradeColumnCount();
|
||||||
|
if (direction < 0 && upgradeUiState.selectedIndex >= upgradeColumnCount)
|
||||||
|
{
|
||||||
|
upgradeUiState.selectedIndex -= upgradeColumnCount;
|
||||||
|
}
|
||||||
|
else if (direction > 0 && upgradeUiState.selectedIndex + upgradeColumnCount < upgradeUiState.optionCount)
|
||||||
|
{
|
||||||
|
upgradeUiState.selectedIndex += upgradeColumnCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 切换多选升级中的当前卡片标记状态。
|
||||||
|
*/
|
||||||
|
static void ToggleUpgradeMarkedSelection()
|
||||||
|
{
|
||||||
|
if (upgradeUiState.picksRemaining <= 1 || upgradeUiState.optionCount <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 处理升级选择界面键盘导航。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @param key 按键虚拟键码。
|
||||||
|
* @return 当前界面是升级选择时返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
|
static bool HandleUpgradeKey(HWND hWnd, WPARAM key)
|
||||||
|
{
|
||||||
|
if (currentScreen != SCREEN_UPGRADE)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (key)
|
||||||
|
{
|
||||||
|
case VK_LEFT:
|
||||||
|
case 'A':
|
||||||
|
MoveUpgradeSelectionHorizontal(-1);
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
break;
|
||||||
|
case VK_RIGHT:
|
||||||
|
case 'D':
|
||||||
|
MoveUpgradeSelectionHorizontal(1);
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
break;
|
||||||
|
case VK_UP:
|
||||||
|
case 'W':
|
||||||
|
MoveUpgradeSelectionVertical(-1);
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
break;
|
||||||
|
case VK_DOWN:
|
||||||
|
case 'S':
|
||||||
|
MoveUpgradeSelectionVertical(1);
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
break;
|
||||||
|
case VK_RETURN:
|
||||||
|
ConfirmUpgradeSelection();
|
||||||
|
ResetGameTimer(hWnd);
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
break;
|
||||||
|
case VK_SPACE:
|
||||||
|
if (upgradeUiState.picksRemaining > 1 && upgradeUiState.optionCount > 0)
|
||||||
|
{
|
||||||
|
ToggleUpgradeMarkedSelection();
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ConfirmUpgradeSelection();
|
||||||
|
ResetGameTimer(hWnd);
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'M':
|
||||||
|
ReturnToMainMenu();
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 处理 Rogue 技能演示模式的专用按键。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @param key 按键虚拟键码。
|
||||||
|
* @return 已处理返回 true。
|
||||||
|
*/
|
||||||
|
static bool HandleDemoPlayingKey(HWND hWnd, WPARAM key)
|
||||||
|
{
|
||||||
|
if (!IsRogueSkillDemoMode())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key == 'N')
|
||||||
|
{
|
||||||
|
AdvanceRogueSkillDemo();
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (key == 'R')
|
||||||
|
{
|
||||||
|
RestartCurrentRogueSkillDemo();
|
||||||
|
ResetGameTimer(hWnd);
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (key == VK_ESCAPE || key == VK_BACK || key == 'M')
|
||||||
|
{
|
||||||
|
OpenSkillDemoScreen();
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 处理正常战局控制键,如菜单、重开、暂停、目标提示和复活。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @param key 按键虚拟键码。
|
||||||
|
* @return 已处理返回 true。
|
||||||
|
*/
|
||||||
|
static bool HandleBattleControlKey(HWND hWnd, WPARAM key)
|
||||||
|
{
|
||||||
|
if (!IsRogueSkillDemoMode() && key == 'M')
|
||||||
|
{
|
||||||
|
ReturnToMainMenu();
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsRogueSkillDemoMode() && key == 'R')
|
||||||
|
{
|
||||||
|
StartGameWithMode(currentMode);
|
||||||
|
ResetGameTimer(hWnd);
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsRogueSkillDemoMode() && key == 'P')
|
||||||
|
{
|
||||||
|
suspendFlag = !suspendFlag;
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key == 'G')
|
||||||
|
{
|
||||||
|
targetFlag = !targetFlag;
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gameOverFlag && reviveAvailable && key == 'V')
|
||||||
|
{
|
||||||
|
if (PlayReviveVideo(hWnd))
|
||||||
|
{
|
||||||
|
ReviveAfterVideo();
|
||||||
|
ResetGameTimer(hWnd);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SetFeedbackMessage(_T("视频播放失败"), _T("无法打开复活视频,复活机会未消耗。"), 14);
|
||||||
|
}
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 处理 Rogue 侧栏滚动和主动技能按键。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @param key 按键虚拟键码。
|
||||||
|
* @return 已处理返回 true。
|
||||||
|
*/
|
||||||
|
static bool HandleRogueSkillKey(HWND hWnd, WPARAM key)
|
||||||
|
{
|
||||||
|
if (currentMode == MODE_ROGUE && (key == 'J' || key == 'K'))
|
||||||
|
{
|
||||||
|
int direction = (key == 'J') ? 1 : -1;
|
||||||
|
AdjustScrollOffset(upgradeListScrollOffset, direction * GetScrollStep(hWnd, 52));
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (key)
|
||||||
|
{
|
||||||
|
case 'C':
|
||||||
|
case VK_SHIFT:
|
||||||
|
case VK_LSHIFT:
|
||||||
|
case VK_RSHIFT:
|
||||||
|
HoldCurrentPiece();
|
||||||
|
return true;
|
||||||
|
case 'Z':
|
||||||
|
UseBlackHole();
|
||||||
|
return true;
|
||||||
|
case 'X':
|
||||||
|
UseScreenBomb();
|
||||||
|
return true;
|
||||||
|
case 'V':
|
||||||
|
UseAirReshape();
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 固定当前方块后执行消行和 Rogue 升级检查。
|
||||||
|
*/
|
||||||
|
static void FixPieceAndResolveLines()
|
||||||
|
{
|
||||||
|
Fixing();
|
||||||
|
if (!gameOverFlag)
|
||||||
|
{
|
||||||
|
DeleteLines();
|
||||||
|
CheckRogueLevelProgress();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 处理移动、旋转、软降和硬降等方块操作键。
|
||||||
|
* @param key 按键虚拟键码。
|
||||||
|
* @return 已处理返回 true。
|
||||||
|
*/
|
||||||
|
static bool HandlePieceMovementKey(WPARAM key)
|
||||||
|
{
|
||||||
|
switch (key)
|
||||||
|
{
|
||||||
|
case VK_LEFT:
|
||||||
|
case 'A':
|
||||||
|
if (CanMoveLeft())
|
||||||
|
{
|
||||||
|
MoveLeft();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
case VK_RIGHT:
|
||||||
|
case 'D':
|
||||||
|
if (CanMoveRight())
|
||||||
|
{
|
||||||
|
MoveRight();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
case VK_DOWN:
|
||||||
|
case 'S':
|
||||||
|
if (CanMoveDown())
|
||||||
|
{
|
||||||
|
MoveDown();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
FixPieceAndResolveLines();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
case VK_UP:
|
||||||
|
case 'W':
|
||||||
|
Rotate();
|
||||||
|
return true;
|
||||||
|
case VK_SPACE:
|
||||||
|
DropDown();
|
||||||
|
FixPieceAndResolveLines();
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 处理游戏过程中的按键。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @param key 按键虚拟键码。
|
||||||
|
*/
|
||||||
|
static void HandlePlayingKey(HWND hWnd, WPARAM key)
|
||||||
|
{
|
||||||
|
if (HandleDemoPlayingKey(hWnd, key) || HandleBattleControlKey(hWnd, key))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gameOverFlag || suspendFlag)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 正常游玩按键先改变方块或触发技能,再统一刷新预测落点和界面。
|
||||||
|
if (!HandlePieceMovementKey(key))
|
||||||
|
{
|
||||||
|
HandleRogueSkillKey(hWnd, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!gameOverFlag)
|
||||||
|
{
|
||||||
|
ComputeTarget();
|
||||||
|
}
|
||||||
|
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 处理键盘按键事件。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @param wParam 按键虚拟键码。
|
||||||
|
*/
|
||||||
|
void HandleKeyDown(HWND hWnd, WPARAM wParam)
|
||||||
|
{
|
||||||
|
if (HandleMenuKey(hWnd, wParam) ||
|
||||||
|
HandleRulesKey(hWnd, wParam) ||
|
||||||
|
HandleUpgradeKey(hWnd, wParam))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HandlePlayingKey(hWnd, wParam);
|
||||||
|
}
|
||||||
@@ -0,0 +1,422 @@
|
|||||||
|
#include "stdafx.h"
|
||||||
|
/**
|
||||||
|
* @file TetrisLayout.cpp
|
||||||
|
* @brief 实现窗口缩放布局和各类按钮、卡片、列表项的点击区域计算。
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "TetrisAppInternal.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 将指定滚动偏移按步长调整,并限制在非负范围内。
|
||||||
|
* @param scrollOffset 需要修改的滚动偏移。
|
||||||
|
* @param delta 本次滚动增量。
|
||||||
|
*/
|
||||||
|
void AdjustScrollOffset(int& scrollOffset, int delta)
|
||||||
|
{
|
||||||
|
scrollOffset += delta;
|
||||||
|
if (scrollOffset < 0)
|
||||||
|
{
|
||||||
|
scrollOffset = 0;
|
||||||
|
}
|
||||||
|
if (scrollOffset > 2400)
|
||||||
|
{
|
||||||
|
scrollOffset = 2400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 按当前窗口缩放返回一次滚动操作的像素距离。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @param baseStep 设计稿中的基础滚动步长。
|
||||||
|
* @return 缩放后的滚动步长。
|
||||||
|
*/
|
||||||
|
int GetScrollStep(HWND hWnd, int baseStep)
|
||||||
|
{
|
||||||
|
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
|
||||||
|
return MulDiv(baseStep, metrics.scale, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 根据当前窗口大小计算整体界面缩放与偏移。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @return 布局缩放、偏移和网格尺寸。
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 按当前布局比例缩放一个尺寸值。
|
||||||
|
* @param metrics 当前布局参数。
|
||||||
|
* @param value 设计稿尺寸值。
|
||||||
|
* @return 缩放后的像素尺寸。
|
||||||
|
*/
|
||||||
|
int ScaleValue(const LayoutMetrics& metrics, int value)
|
||||||
|
{
|
||||||
|
return MulDiv(value, metrics.scale, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 按当前布局比例缩放横坐标并叠加窗口偏移。
|
||||||
|
* @param metrics 当前布局参数。
|
||||||
|
* @param value 设计稿横坐标。
|
||||||
|
* @return 实际窗口横坐标。
|
||||||
|
*/
|
||||||
|
int ScaleXValue(const LayoutMetrics& metrics, int value)
|
||||||
|
{
|
||||||
|
return metrics.offsetX + MulDiv(value, metrics.scale, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 按当前布局比例缩放纵坐标并叠加窗口偏移。
|
||||||
|
* @param metrics 当前布局参数。
|
||||||
|
* @param value 设计稿纵坐标。
|
||||||
|
* @return 实际窗口纵坐标。
|
||||||
|
*/
|
||||||
|
int ScaleYValue(const LayoutMetrics& metrics, int value)
|
||||||
|
{
|
||||||
|
return metrics.offsetY + MulDiv(value, metrics.scale, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取主菜单中央卡片区域。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @return 菜单卡片在窗口中的矩形区域。
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取主菜单选项的点击区域。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @param index 菜单选项序号。
|
||||||
|
* @return 选项在窗口中的矩形区域。
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取帮助页卡片区域。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @return 帮助卡片在窗口中的矩形区域。
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取帮助页首页选项的点击区域。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @param index 帮助选项序号。
|
||||||
|
* @return 选项在窗口中的矩形区域。
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取技能演示列表项的点击区域。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @param index 技能演示条目序号。
|
||||||
|
* @return 条目在窗口中的矩形区域。
|
||||||
|
*/
|
||||||
|
RECT GetHelpSkillDemoItemRect(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 itemHeight = ScaleValue(metrics, 58);
|
||||||
|
int itemGap = ScaleValue(metrics, 10);
|
||||||
|
int itemTop = contentRect.top + ScaleValue(metrics, 8) - helpScrollOffset;
|
||||||
|
RECT rect =
|
||||||
|
{
|
||||||
|
contentRect.left,
|
||||||
|
itemTop + index * (itemHeight + itemGap),
|
||||||
|
contentRect.right,
|
||||||
|
itemTop + index * (itemHeight + itemGap) + itemHeight
|
||||||
|
};
|
||||||
|
return rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取帮助页底部返回提示的点击区域。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @return 返回提示在窗口中的矩形区域。
|
||||||
|
*/
|
||||||
|
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 获取致谢页左右箭头按钮区域。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @param direction 小于 0 为左箭头,大于 0 为右箭头。
|
||||||
|
* @return 箭头按钮在窗口中的矩形区域。
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取升级选择覆盖层区域。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @return 覆盖层在窗口中的矩形区域。
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取升级选择卡片的点击区域。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @param index 强化卡片序号。
|
||||||
|
* @return 卡片在窗口中的矩形区域。
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取暂停或结束提示覆盖层区域。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @return 覆盖层在窗口中的矩形区域。
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取暂停或结束覆盖层按钮的点击区域。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @param index 按钮序号。
|
||||||
|
* @param buttonCount 当前覆盖层按钮总数。
|
||||||
|
* @return 按钮在窗口中的矩形区域。
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取左上角返回按钮的点击区域。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @return 返回按钮在窗口中的矩形区域。
|
||||||
|
*/
|
||||||
|
RECT GetBackButtonRect(HWND hWnd)
|
||||||
|
{
|
||||||
|
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
|
||||||
|
RECT rect =
|
||||||
|
{
|
||||||
|
ScaleXValue(metrics, 6),
|
||||||
|
ScaleYValue(metrics, 6),
|
||||||
|
ScaleXValue(metrics, 34),
|
||||||
|
ScaleYValue(metrics, 34)
|
||||||
|
};
|
||||||
|
return rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取右下角音乐按钮的点击区域。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @return 音乐按钮在窗口中的矩形区域。
|
||||||
|
*/
|
||||||
|
RECT GetMusicButtonRect(HWND hWnd)
|
||||||
|
{
|
||||||
|
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
|
||||||
|
int size = ScaleValue(metrics, 28);
|
||||||
|
if (size < 22)
|
||||||
|
{
|
||||||
|
size = 22;
|
||||||
|
}
|
||||||
|
int marginRight = ScaleValue(metrics, 12);
|
||||||
|
if (marginRight < 6)
|
||||||
|
{
|
||||||
|
marginRight = 6;
|
||||||
|
}
|
||||||
|
int marginBottom = ScaleValue(metrics, 12);
|
||||||
|
if (marginBottom < 6)
|
||||||
|
{
|
||||||
|
marginBottom = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
RECT buttonRect =
|
||||||
|
{
|
||||||
|
metrics.offsetX + metrics.layoutWidth - marginRight - size,
|
||||||
|
metrics.offsetY + metrics.layoutHeight - marginBottom - size,
|
||||||
|
metrics.offsetX + metrics.layoutWidth - marginRight,
|
||||||
|
metrics.offsetY + metrics.layoutHeight - marginBottom
|
||||||
|
};
|
||||||
|
return buttonRect;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 判断点坐标是否落在矩形内部。
|
||||||
|
* @param rect 待判断矩形。
|
||||||
|
* @param x 点的横坐标。
|
||||||
|
* @param y 点的纵坐标。
|
||||||
|
* @return 点在矩形内返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
|
bool IsPointInRect(const RECT& rect, int x, int y)
|
||||||
|
{
|
||||||
|
return x >= rect.left && x < rect.right && y >= rect.top && y < rect.bottom;
|
||||||
|
}
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
#include "stdafx.h"
|
||||||
|
/**
|
||||||
|
* @file TetrisMedia.cpp
|
||||||
|
* @brief 实现背景音乐开关和复活视频播放逻辑。
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "Tetris.h"
|
||||||
|
#include "TetrisAppInternal.h"
|
||||||
|
#include "TetrisAssets.h"
|
||||||
|
#include <shellapi.h>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
static bool bgmPlaying = false;
|
||||||
|
static bool bgmUsingMci = false;
|
||||||
|
static constexpr const wchar_t* kBgmAlias = L"TereisBgm";
|
||||||
|
static constexpr const wchar_t* kReviveVideoAlias = L"TereisReviveVideo";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 尝试通过 MCI 循环播放指定音乐文件。
|
||||||
|
* @param path 音频文件路径。
|
||||||
|
* @param forceMpegVideo 是否强制按 mpegvideo 类型打开。
|
||||||
|
* @return 播放成功返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
|
static bool TryPlayMciLoop(const std::wstring& path, bool forceMpegVideo)
|
||||||
|
{
|
||||||
|
if (!FileExists(path))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mciSendStringW((std::wstring(L"close ") + kBgmAlias).c_str(), nullptr, 0, nullptr);
|
||||||
|
|
||||||
|
// MCI 对部分 OGG/视频容器识别不稳定,调用方会按不同类型尝试。
|
||||||
|
std::wstring openCommand = L"open \"" + path + L"\" ";
|
||||||
|
if (forceMpegVideo)
|
||||||
|
{
|
||||||
|
openCommand += L"type mpegvideo ";
|
||||||
|
}
|
||||||
|
openCommand += L"alias ";
|
||||||
|
openCommand += kBgmAlias;
|
||||||
|
|
||||||
|
if (mciSendStringW(openCommand.c_str(), nullptr, 0, nullptr) != 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring playCommand = std::wstring(L"play ") + kBgmAlias + L" repeat";
|
||||||
|
if (mciSendStringW(playCommand.c_str(), nullptr, 0, nullptr) != 0)
|
||||||
|
{
|
||||||
|
mciSendStringW((std::wstring(L"close ") + kBgmAlias).c_str(), nullptr, 0, nullptr);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bgmPlaying = true;
|
||||||
|
bgmUsingMci = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 停止背景音乐并释放当前使用的播放设备。
|
||||||
|
*/
|
||||||
|
void StopBackgroundMusic()
|
||||||
|
{
|
||||||
|
if (bgmUsingMci)
|
||||||
|
{
|
||||||
|
mciSendStringW((std::wstring(L"stop ") + kBgmAlias).c_str(), nullptr, 0, nullptr);
|
||||||
|
mciSendStringW((std::wstring(L"close ") + kBgmAlias).c_str(), nullptr, 0, nullptr);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PlaySoundW(nullptr, nullptr, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
bgmPlaying = false;
|
||||||
|
bgmUsingMci = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 按资源优先级查找并启动背景音乐。
|
||||||
|
*/
|
||||||
|
void StartBackgroundMusic()
|
||||||
|
{
|
||||||
|
if (!bgmEnabled || bgmPlaying)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wchar_t* bgmWavRelativePath = L"assets\\audio\\bgm.wav";
|
||||||
|
const std::wstring bgmWavCandidates[] =
|
||||||
|
{
|
||||||
|
BuildAssetPath(bgmWavRelativePath),
|
||||||
|
BuildWorkingDirAssetPath(bgmWavRelativePath)
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const std::wstring& candidate : bgmWavCandidates)
|
||||||
|
{
|
||||||
|
if (FileExists(candidate) &&
|
||||||
|
PlaySoundW(candidate.c_str(), nullptr, SND_FILENAME | SND_ASYNC | SND_LOOP))
|
||||||
|
{
|
||||||
|
bgmPlaying = true;
|
||||||
|
bgmUsingMci = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const wchar_t* oggRelativePath = L"assets\\audio\\bgm.ogg";
|
||||||
|
const std::wstring oggCandidates[] =
|
||||||
|
{
|
||||||
|
BuildAssetPath(oggRelativePath),
|
||||||
|
BuildWorkingDirAssetPath(oggRelativePath)
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const std::wstring& candidate : oggCandidates)
|
||||||
|
{
|
||||||
|
if (TryPlayMciLoop(candidate, false) || TryPlayMciLoop(candidate, true))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const wchar_t* fallbackWavRelativePath = L"assets\\audio\\background.wav";
|
||||||
|
const std::wstring fallbackWavCandidates[] =
|
||||||
|
{
|
||||||
|
BuildAssetPath(fallbackWavRelativePath),
|
||||||
|
BuildWorkingDirAssetPath(fallbackWavRelativePath)
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const std::wstring& candidate : fallbackWavCandidates)
|
||||||
|
{
|
||||||
|
if (FileExists(candidate) &&
|
||||||
|
PlaySoundW(candidate.c_str(), nullptr, SND_FILENAME | SND_ASYNC | SND_LOOP))
|
||||||
|
{
|
||||||
|
bgmPlaying = true;
|
||||||
|
bgmUsingMci = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bgmEnabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 切换背景音乐开关并刷新窗口。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
*/
|
||||||
|
void ToggleBackgroundMusic(HWND hWnd)
|
||||||
|
{
|
||||||
|
bgmEnabled = !bgmEnabled;
|
||||||
|
if (bgmEnabled)
|
||||||
|
{
|
||||||
|
StartBackgroundMusic();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
StopBackgroundMusic();
|
||||||
|
}
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 播放复活视频,先尝试 MCI,全屏播放失败时退回系统默认播放器。
|
||||||
|
* @param hWnd 当前窗口句柄,用作 MCI 父窗口和 ShellExecute 父窗口。
|
||||||
|
* @return 播放成功返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
|
bool PlayReviveVideo(HWND hWnd)
|
||||||
|
{
|
||||||
|
std::wstring videoPath = BuildAssetPath(L"assets\\video\\video.avi");
|
||||||
|
if (!FileExists(videoPath))
|
||||||
|
{
|
||||||
|
videoPath = BuildWorkingDirAssetPath(L"assets\\video\\video.avi");
|
||||||
|
}
|
||||||
|
if (!FileExists(videoPath))
|
||||||
|
{
|
||||||
|
videoPath = BuildAssetPath(L"assets\\video\\video.mp4");
|
||||||
|
}
|
||||||
|
if (!FileExists(videoPath))
|
||||||
|
{
|
||||||
|
videoPath = BuildWorkingDirAssetPath(L"assets\\video\\video.mp4");
|
||||||
|
}
|
||||||
|
if (!FileExists(videoPath))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool shouldResumeBgm = bgmEnabled;
|
||||||
|
if (bgmPlaying)
|
||||||
|
{
|
||||||
|
StopBackgroundMusic();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先用 MCI 全屏同步播放;失败时再交给系统默认播放器。
|
||||||
|
bool played = false;
|
||||||
|
for (int attempt = 0; attempt < 2 && !played; attempt++)
|
||||||
|
{
|
||||||
|
bool forceMpegVideo = attempt == 0;
|
||||||
|
mciSendStringW((std::wstring(L"close ") + kReviveVideoAlias).c_str(), nullptr, 0, nullptr);
|
||||||
|
|
||||||
|
std::wstring openCommand = L"open \"" + videoPath + L"\" ";
|
||||||
|
if (forceMpegVideo)
|
||||||
|
{
|
||||||
|
openCommand += L"type mpegvideo ";
|
||||||
|
}
|
||||||
|
openCommand += L"alias ";
|
||||||
|
openCommand += kReviveVideoAlias;
|
||||||
|
|
||||||
|
if (mciSendStringW(openCommand.c_str(), nullptr, 0, hWnd) == 0)
|
||||||
|
{
|
||||||
|
std::wstring playCommand = std::wstring(L"play ") + kReviveVideoAlias + L" fullscreen wait";
|
||||||
|
MCIERROR playResult = mciSendStringW(playCommand.c_str(), nullptr, 0, hWnd);
|
||||||
|
mciSendStringW((std::wstring(L"close ") + kReviveVideoAlias).c_str(), nullptr, 0, nullptr);
|
||||||
|
played = playResult == 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!played)
|
||||||
|
{
|
||||||
|
SHELLEXECUTEINFOW shellInfo = {};
|
||||||
|
shellInfo.cbSize = sizeof(shellInfo);
|
||||||
|
shellInfo.fMask = SEE_MASK_NOCLOSEPROCESS;
|
||||||
|
shellInfo.hwnd = hWnd;
|
||||||
|
shellInfo.lpVerb = L"open";
|
||||||
|
shellInfo.lpFile = videoPath.c_str();
|
||||||
|
shellInfo.nShow = SW_SHOWNORMAL;
|
||||||
|
|
||||||
|
if (ShellExecuteExW(&shellInfo))
|
||||||
|
{
|
||||||
|
if (shellInfo.hProcess != nullptr)
|
||||||
|
{
|
||||||
|
WaitForSingleObject(shellInfo.hProcess, INFINITE);
|
||||||
|
CloseHandle(shellInfo.hProcess);
|
||||||
|
}
|
||||||
|
played = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldResumeBgm)
|
||||||
|
{
|
||||||
|
StartBackgroundMusic();
|
||||||
|
}
|
||||||
|
|
||||||
|
return played;
|
||||||
|
}
|
||||||
@@ -0,0 +1,324 @@
|
|||||||
|
#include "stdafx.h"
|
||||||
|
/**
|
||||||
|
* @file TetrisTimers.cpp
|
||||||
|
* @brief 实现游戏下落、视觉特效、致谢动画和 Rogue 限时状态的定时推进。
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "TetrisAppInternal.h"
|
||||||
|
|
||||||
|
static MMRESULT creditTimerHandle = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 多媒体定时器回调,用于高频率请求致谢页动画刷新。
|
||||||
|
* @param userData 创建定时器时传入的窗口句柄。
|
||||||
|
*/
|
||||||
|
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 重置主下落定时器。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
*/
|
||||||
|
void ResetGameTimer(HWND hWnd)
|
||||||
|
{
|
||||||
|
KillTimer(hWnd, GAME_TIMER_ID);
|
||||||
|
SetTimer(hWnd, GAME_TIMER_ID, currentFallInterval > 0 ? currentFallInterval : GAME_TIMER_INTERVAL, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 启动游戏、特效和致谢页动画定时器。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
*/
|
||||||
|
void StartAppTimers(HWND hWnd)
|
||||||
|
{
|
||||||
|
ResetGameTimer(hWnd);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 停止游戏、特效和致谢页动画定时器。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
*/
|
||||||
|
void StopAppTimers(HWND hWnd)
|
||||||
|
{
|
||||||
|
KillTimer(hWnd, GAME_TIMER_ID);
|
||||||
|
KillTimer(hWnd, EFFECT_TIMER_ID);
|
||||||
|
if (creditTimerHandle != 0)
|
||||||
|
{
|
||||||
|
timeKillEvent(creditTimerHandle);
|
||||||
|
creditTimerHandle = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
KillTimer(hWnd, CREDIT_TIMER_ID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 处理致谢页高频动画刷新消息。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
*/
|
||||||
|
void HandleCreditTick(HWND hWnd)
|
||||||
|
{
|
||||||
|
if (currentScreen == SCREEN_RULES && helpState.currentPage == 4 && TickCreditAnimation())
|
||||||
|
{
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 推进 Rogue 限时状态并按需要重置下落定时器。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @return 任意状态变化需要刷新界面时返回 true。
|
||||||
|
*/
|
||||||
|
static bool TickRogueTimedStates(HWND hWnd)
|
||||||
|
{
|
||||||
|
bool shouldRefresh = false;
|
||||||
|
|
||||||
|
if (currentMode == MODE_ROGUE && !IsRogueSkillDemoMode() && rogueStats.feverTicks > 0)
|
||||||
|
{
|
||||||
|
rogueStats.feverTicks--;
|
||||||
|
currentFallInterval = GetRogueFallInterval();
|
||||||
|
ResetGameTimer(hWnd);
|
||||||
|
shouldRefresh = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentMode == MODE_ROGUE &&
|
||||||
|
!IsRogueSkillDemoMode() &&
|
||||||
|
rogueStats.timeDilationTicks > 0 &&
|
||||||
|
currentScreen == SCREEN_PLAYING &&
|
||||||
|
!suspendFlag &&
|
||||||
|
!gameOverFlag)
|
||||||
|
{
|
||||||
|
rogueStats.timeDilationTicks--;
|
||||||
|
currentFallInterval = GetRogueFallInterval();
|
||||||
|
ResetGameTimer(hWnd);
|
||||||
|
shouldRefresh = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentMode == MODE_ROGUE && !IsRogueSkillDemoMode() && rogueStats.extremeSlowTicks > 0)
|
||||||
|
{
|
||||||
|
rogueStats.extremeSlowTicks--;
|
||||||
|
currentFallInterval = GetRogueFallInterval();
|
||||||
|
ResetGameTimer(hWnd);
|
||||||
|
shouldRefresh = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentMode == MODE_ROGUE && !IsRogueSkillDemoMode() && rogueStats.holdSlowTicks > 0)
|
||||||
|
{
|
||||||
|
rogueStats.holdSlowTicks--;
|
||||||
|
currentFallInterval = GetRogueFallInterval();
|
||||||
|
ResetGameTimer(hWnd);
|
||||||
|
shouldRefresh = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return shouldRefresh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 检查极限玩家的危险等级计时。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @return 危险等级变化时返回 true。
|
||||||
|
*/
|
||||||
|
static bool TickExtremeDanger(HWND hWnd)
|
||||||
|
{
|
||||||
|
if (currentMode != MODE_ROGUE ||
|
||||||
|
IsRogueSkillDemoMode() ||
|
||||||
|
rogueStats.extremePlayerLevel <= 0 ||
|
||||||
|
currentScreen != SCREEN_PLAYING ||
|
||||||
|
suspendFlag ||
|
||||||
|
gameOverFlag)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rogueStats.extremeDangerTicks > 0)
|
||||||
|
{
|
||||||
|
rogueStats.extremeDangerTicks--;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
rogueStats.extremeDangerTicks = 30;
|
||||||
|
if (rogueStats.extremeDangerLevel < 5)
|
||||||
|
{
|
||||||
|
rogueStats.extremeDangerLevel++;
|
||||||
|
}
|
||||||
|
currentFallInterval = GetRogueFallInterval();
|
||||||
|
ResetGameTimer(hWnd);
|
||||||
|
SetFeedbackMessage(
|
||||||
|
_T("极限压力升高"),
|
||||||
|
_T("30 秒内没有完成四消,危险等级提升,下落速度进一步加快。"),
|
||||||
|
10);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 检查高堆叠触发的时间缓流。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @return 成功触发时间缓流时返回 true。
|
||||||
|
*/
|
||||||
|
static bool TryStartTimeDilation(HWND hWnd)
|
||||||
|
{
|
||||||
|
if (currentMode != MODE_ROGUE ||
|
||||||
|
IsRogueSkillDemoMode() ||
|
||||||
|
rogueStats.timeDilationLevel <= 0 ||
|
||||||
|
rogueStats.timeDilationTicks > 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int occupiedHeight = 0;
|
||||||
|
int playableHeight = GetRoguePlayableHeight();
|
||||||
|
for (int y = 0; y < playableHeight; y++)
|
||||||
|
{
|
||||||
|
bool hasCell = false;
|
||||||
|
for (int x = 0; x < nGameWidth; x++)
|
||||||
|
{
|
||||||
|
if (workRegion[y][x] != 0)
|
||||||
|
{
|
||||||
|
hasCell = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hasCell)
|
||||||
|
{
|
||||||
|
occupiedHeight = playableHeight - y;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (occupiedHeight <= 15)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
rogueStats.timeDilationTicks = 8;
|
||||||
|
currentFallInterval = GetRogueFallInterval();
|
||||||
|
ResetGameTimer(hWnd);
|
||||||
|
SetFeedbackMessage(
|
||||||
|
_T("时间缓流"),
|
||||||
|
_T("堆叠高度超过 15 行,接下来 8 秒下落速度降低 30%。"),
|
||||||
|
10);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 推进一次自动下落逻辑。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @return 游戏状态推进后需要刷新界面返回 true。
|
||||||
|
*/
|
||||||
|
static bool TickGameFall(HWND hWnd)
|
||||||
|
{
|
||||||
|
if (currentScreen != SCREEN_PLAYING || suspendFlag || gameOverFlag)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentMode == MODE_ROGUE && !IsRogueSkillDemoMode())
|
||||||
|
{
|
||||||
|
int previousFallInterval = currentFallInterval;
|
||||||
|
AdvanceRogueDifficulty(currentFallInterval > 0 ? currentFallInterval : GAME_TIMER_INTERVAL);
|
||||||
|
if (currentFallInterval != previousFallInterval)
|
||||||
|
{
|
||||||
|
ResetGameTimer(hWnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TryStartTimeDilation(hWnd);
|
||||||
|
|
||||||
|
if (CanMoveDown())
|
||||||
|
{
|
||||||
|
MoveDown();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Fixing();
|
||||||
|
if (!gameOverFlag)
|
||||||
|
{
|
||||||
|
DeleteLines();
|
||||||
|
CheckRogueLevelProgress();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!gameOverFlag)
|
||||||
|
{
|
||||||
|
ComputeTarget();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 处理窗口定时器消息。
|
||||||
|
* @param hWnd 当前窗口句柄。
|
||||||
|
* @param timerId 触发的定时器编号。
|
||||||
|
*/
|
||||||
|
void HandleTimerMessage(HWND hWnd, WPARAM timerId)
|
||||||
|
{
|
||||||
|
if (timerId == EFFECT_TIMER_ID)
|
||||||
|
{
|
||||||
|
if (TickVisualEffects())
|
||||||
|
{
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timerId == CREDIT_TIMER_ID && creditTimerHandle == 0)
|
||||||
|
{
|
||||||
|
HandleCreditTick(hWnd);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timerId != GAME_TIMER_ID)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool shouldRefresh = false;
|
||||||
|
if (feedbackState.visibleTicks > 0)
|
||||||
|
{
|
||||||
|
feedbackState.visibleTicks--;
|
||||||
|
shouldRefresh = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsRogueSkillDemoMode() && TickRogueSkillDemo())
|
||||||
|
{
|
||||||
|
shouldRefresh = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TickRogueTimedStates(hWnd))
|
||||||
|
{
|
||||||
|
shouldRefresh = true;
|
||||||
|
}
|
||||||
|
if (TickExtremeDanger(hWnd))
|
||||||
|
{
|
||||||
|
shouldRefresh = true;
|
||||||
|
}
|
||||||
|
if (TickGameFall(hWnd))
|
||||||
|
{
|
||||||
|
shouldRefresh = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRefresh)
|
||||||
|
{
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
#include "stdafx.h"
|
||||||
|
/**
|
||||||
|
* @file TetrisAssets.cpp
|
||||||
|
* @brief 实现资源路径解析和文件存在性检查,支持构建目录或项目根目录运行。
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "TetrisAssets.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 根据程序所在目录拼出项目资源文件的绝对路径。
|
||||||
|
*
|
||||||
|
* 构建脚本会把可执行文件放到构建目录,因此这里先回到项目根目录,
|
||||||
|
* 再拼接 assets 下的图片、音频或视频路径。
|
||||||
|
*
|
||||||
|
* @param relativePath 相对于项目根目录的资源路径。
|
||||||
|
* @return 规范化后的绝对路径;解析失败时返回拼接路径。
|
||||||
|
*/
|
||||||
|
std::wstring BuildAssetPath(const wchar_t* relativePath)
|
||||||
|
{
|
||||||
|
wchar_t modulePath[MAX_PATH] = {};
|
||||||
|
GetModuleFileNameW(nullptr, modulePath, MAX_PATH);
|
||||||
|
|
||||||
|
std::wstring basePath(modulePath);
|
||||||
|
size_t lastSlash = basePath.find_last_of(L"\\/");
|
||||||
|
if (lastSlash != std::wstring::npos)
|
||||||
|
{
|
||||||
|
basePath.resize(lastSlash);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 可执行文件位于构建目录,向上两级回到项目根目录。
|
||||||
|
std::wstring projectRelative = basePath + L"\\..\\..\\" + relativePath;
|
||||||
|
wchar_t fullPath[MAX_PATH] = {};
|
||||||
|
DWORD result = GetFullPathNameW(projectRelative.c_str(), MAX_PATH, fullPath, nullptr);
|
||||||
|
if (result > 0 && result < MAX_PATH)
|
||||||
|
{
|
||||||
|
return fullPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
return projectRelative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 根据当前工作目录拼出项目资源文件的绝对路径。
|
||||||
|
*
|
||||||
|
* 这个路径用于从 IDE 或命令行直接以项目根目录运行时查找资源。
|
||||||
|
*
|
||||||
|
* @param relativePath 相对于当前工作目录的资源路径。
|
||||||
|
* @return 规范化后的绝对路径;解析失败时返回拼接路径。
|
||||||
|
*/
|
||||||
|
std::wstring BuildWorkingDirAssetPath(const wchar_t* relativePath)
|
||||||
|
{
|
||||||
|
wchar_t currentDirectory[MAX_PATH] = {};
|
||||||
|
DWORD length = GetCurrentDirectoryW(MAX_PATH, currentDirectory);
|
||||||
|
if (length == 0 || length >= MAX_PATH)
|
||||||
|
{
|
||||||
|
return L"";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring candidate = std::wstring(currentDirectory) + L"\\" + relativePath;
|
||||||
|
wchar_t fullPath[MAX_PATH] = {};
|
||||||
|
DWORD result = GetFullPathNameW(candidate.c_str(), MAX_PATH, fullPath, nullptr);
|
||||||
|
if (result > 0 && result < MAX_PATH)
|
||||||
|
{
|
||||||
|
return fullPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 判断指定路径是否存在且不是目录。
|
||||||
|
* @param path 待检查的文件路径。
|
||||||
|
* @return 文件存在且不是目录返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
|
bool FileExists(const std::wstring& path)
|
||||||
|
{
|
||||||
|
DWORD attributes = GetFileAttributesW(path.c_str());
|
||||||
|
return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) == 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,760 @@
|
|||||||
|
#include "stdafx.h"
|
||||||
|
/**
|
||||||
|
* @file TetrisGameExtensions.cpp
|
||||||
|
* @brief 实现玩家统计、视觉特效、模式切换、复活和帮助/致谢页面状态管理。
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "TetrisLogicInternal.h"
|
||||||
|
|
||||||
|
int pendingLineClearEffectTicks = 0;
|
||||||
|
int pendingLineClearEffectRows[8] = {};
|
||||||
|
int pendingLineClearEffectRowCount = 0;
|
||||||
|
int pendingLineClearEffectLineCount = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 重置经典或 Rogue 模式使用的玩家统计数据。
|
||||||
|
* @param stats 需要重置的统计结构。
|
||||||
|
* @param useRogueRules 是否按 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 设置界面右侧显示的即时反馈标题、内容和持续时间。
|
||||||
|
* @param title 反馈标题。
|
||||||
|
* @param detail 反馈详情。
|
||||||
|
* @param ticks 显示持续的游戏计时次数。
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < 80; i++)
|
||||||
|
{
|
||||||
|
gravityFallEffects[i].ticks = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 推进视觉效果计时,并返回是否仍有动画需要刷新。
|
||||||
|
* @return 仍有动画需要刷新返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < 80; i++)
|
||||||
|
{
|
||||||
|
if (gravityFallEffects[i].ticks > 0)
|
||||||
|
{
|
||||||
|
gravityFallEffects[i].ticks--;
|
||||||
|
active = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return active;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 推进致谢页左右切换动画,并返回是否需要刷新界面。
|
||||||
|
* @return 需要刷新界面返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
|
bool TickCreditAnimation()
|
||||||
|
{
|
||||||
|
if (creditAnimationTicks > 0)
|
||||||
|
{
|
||||||
|
creditAnimationTicks--;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 添加一段棋盘坐标系中的浮动文字效果。
|
||||||
|
* @param boardX 棋盘内部横坐标,使用 100 为一格的坐标系。
|
||||||
|
* @param boardY 棋盘内部纵坐标,使用 100 为一格的坐标系。
|
||||||
|
* @param text 浮动文字内容。
|
||||||
|
* @param color 文字颜色。
|
||||||
|
*/
|
||||||
|
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 添加一个棋盘坐标系中的粒子效果。
|
||||||
|
* @param boardX 粒子起始横坐标。
|
||||||
|
* @param boardY 粒子起始纵坐标。
|
||||||
|
* @param velocityX 横向速度。
|
||||||
|
* @param velocityY 纵向速度。
|
||||||
|
* @param size 粒子尺寸。
|
||||||
|
* @param color 粒子颜色。
|
||||||
|
*/
|
||||||
|
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 在指定棋盘坐标周围生成一组爆裂粒子。
|
||||||
|
* @param boardX 爆裂中心横坐标。
|
||||||
|
* @param boardY 爆裂中心纵坐标。
|
||||||
|
* @param baseColor 主粒子颜色。
|
||||||
|
* @param strongBurst 是否使用更强的粒子数量和速度。
|
||||||
|
*/
|
||||||
|
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 添加一个被清除格子的短时高亮效果。
|
||||||
|
* @param x 棋盘列号。
|
||||||
|
* @param y 棋盘行号。
|
||||||
|
* @param color 高亮颜色。
|
||||||
|
* @param strongFlash 是否使用更长的强高亮。
|
||||||
|
*/
|
||||||
|
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 暂存消行动画,等待升级选择结束后再播放。
|
||||||
|
* @param rows 被消除的行号数组。
|
||||||
|
* @param rowCount 行号数量。
|
||||||
|
* @param linesCleared 实际消除行数。
|
||||||
|
*/
|
||||||
|
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 触发标准消行动画和浮动文字。
|
||||||
|
* @param rows 被消除的行号数组。
|
||||||
|
* @param rowCount 行号数量。
|
||||||
|
* @param linesCleared 实际消除行数。
|
||||||
|
*/
|
||||||
|
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 为指定棋盘格集合触发清除粒子效果。
|
||||||
|
* @param cells 被清除格子数组。
|
||||||
|
* @param cellCount 格子数量。
|
||||||
|
* @param strongBurst 是否使用更强的粒子爆裂效果。
|
||||||
|
*/
|
||||||
|
void TriggerCellClearEffect(const Point* cells, int cellCount, bool strongBurst)
|
||||||
|
{
|
||||||
|
TriggerColoredCellClearEffect(cells, cellCount, RGB(255, 238, 120), strongBurst);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 为指定棋盘格集合触发带颜色区分的清除高亮和粒子效果。
|
||||||
|
* @param cells 被清除格子数组。
|
||||||
|
* @param cellCount 格子数量。
|
||||||
|
* @param flashColor 高亮颜色。
|
||||||
|
* @param strongBurst 是否使用更强的粒子爆裂效果。
|
||||||
|
*/
|
||||||
|
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 为一个受重力下落的固定方块记录纵向残影和落点粒子。
|
||||||
|
* @param x 棋盘列号。
|
||||||
|
* @param fromY 起始行号。
|
||||||
|
* @param toY 目标行号。
|
||||||
|
* @param cellValue 方块格子值。
|
||||||
|
*/
|
||||||
|
void TriggerGravityFallEffect(int x, int fromY, int toY, int cellValue)
|
||||||
|
{
|
||||||
|
if (x < 0 || x >= nGameWidth || fromY < 0 || fromY >= nGameHeight ||
|
||||||
|
toY < 0 || toY >= nGameHeight || toY <= fromY || cellValue == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int effectIndex = -1;
|
||||||
|
for (int i = 0; i < 80; i++)
|
||||||
|
{
|
||||||
|
if (gravityFallEffects[i].ticks <= 0)
|
||||||
|
{
|
||||||
|
effectIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (effectIndex < 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int totalTicks = 12 + (toY - fromY) * 2;
|
||||||
|
if (totalTicks > 26)
|
||||||
|
{
|
||||||
|
totalTicks = 26;
|
||||||
|
}
|
||||||
|
|
||||||
|
gravityFallEffects[effectIndex].ticks = totalTicks;
|
||||||
|
gravityFallEffects[effectIndex].totalTicks = totalTicks;
|
||||||
|
gravityFallEffects[effectIndex].x = x;
|
||||||
|
gravityFallEffects[effectIndex].fromY = fromY;
|
||||||
|
gravityFallEffects[effectIndex].toY = toY;
|
||||||
|
gravityFallEffects[effectIndex].cellValue = cellValue;
|
||||||
|
|
||||||
|
COLORREF particleColor = BrickColor[(cellValue - 1) % 7];
|
||||||
|
AddParticle(x * 100 + 50, toY * 100 + 18, -2 - rand() % 5, -12 - rand() % 7, 4, particleColor);
|
||||||
|
AddParticle(x * 100 + 50, toY * 100 + 18, 2 + rand() % 5, -12 - rand() % 7, 4, particleColor);
|
||||||
|
AddCellFlash(x, toY, RGB(210, 245, 255), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 判断指定方块、旋转状态和位置是否可以合法放置。
|
||||||
|
* @param pieceType 方块类型编号。
|
||||||
|
* @param pieceState 方块旋转状态。
|
||||||
|
* @param position 待检测的左上角坐标。
|
||||||
|
* @return 可以放置返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
|
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 尝试把旋转后的方块横向偏移指定格数后放置。
|
||||||
|
* @param nextState 旋转后的状态编号。
|
||||||
|
* @param offsetX 横向试探偏移。
|
||||||
|
* @return 偏移后可以放置返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
|
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 按指定模式开始新游戏。
|
||||||
|
* @param mode 游戏模式,取值来自 GameMode。
|
||||||
|
*/
|
||||||
|
void StartGameWithMode(int mode)
|
||||||
|
{
|
||||||
|
rogueDemoMode = false;
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
rogueDemoMode = false;
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
rogueDemoMode = false;
|
||||||
|
currentScreen = SCREEN_RULES;
|
||||||
|
suspendFlag = false;
|
||||||
|
helpState.selectedIndex = 0;
|
||||||
|
helpState.optionCount = 4;
|
||||||
|
helpState.currentPage = 0;
|
||||||
|
helpScrollOffset = 0;
|
||||||
|
creditPageIndex = 0;
|
||||||
|
creditAnimationTicks = 0;
|
||||||
|
creditAnimationDirection = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 打开 Rogue 技能演示选择页并重置帮助页状态。
|
||||||
|
*/
|
||||||
|
void OpenSkillDemoScreen()
|
||||||
|
{
|
||||||
|
rogueDemoMode = false;
|
||||||
|
currentScreen = SCREEN_RULES;
|
||||||
|
suspendFlag = false;
|
||||||
|
helpState.selectedIndex = 0;
|
||||||
|
helpState.optionCount = 4;
|
||||||
|
helpState.currentPage = 5;
|
||||||
|
helpScrollOffset = 0;
|
||||||
|
creditPageIndex = 0;
|
||||||
|
creditAnimationTicks = 0;
|
||||||
|
creditAnimationDirection = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 打开致谢界面并重置致谢页切换状态。
|
||||||
|
*/
|
||||||
|
void OpenCreditScreen()
|
||||||
|
{
|
||||||
|
rogueDemoMode = false;
|
||||||
|
currentScreen = SCREEN_RULES;
|
||||||
|
suspendFlag = false;
|
||||||
|
helpState.selectedIndex = 0;
|
||||||
|
helpState.optionCount = 4;
|
||||||
|
helpState.currentPage = 4;
|
||||||
|
helpScrollOffset = 0;
|
||||||
|
creditPageIndex = 0;
|
||||||
|
creditAnimationTicks = 0;
|
||||||
|
creditAnimationDirection = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 切换致谢页图片,并启动左右滑动动画。
|
||||||
|
* @param direction 小于 0 向前切换,大于 0 向后切换。
|
||||||
|
*/
|
||||||
|
void ChangeCreditPage(int direction)
|
||||||
|
{
|
||||||
|
constexpr int creditPageCount = 5;
|
||||||
|
if (direction == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int oldPageIndex = creditPageIndex;
|
||||||
|
if (direction > 0)
|
||||||
|
{
|
||||||
|
creditPageIndex++;
|
||||||
|
creditAnimationDirection = 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
creditPageIndex--;
|
||||||
|
creditAnimationDirection = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (creditPageIndex < 0)
|
||||||
|
{
|
||||||
|
creditPageIndex = creditPageCount - 1;
|
||||||
|
}
|
||||||
|
if (creditPageIndex >= creditPageCount)
|
||||||
|
{
|
||||||
|
creditPageIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (creditPageIndex != oldPageIndex)
|
||||||
|
{
|
||||||
|
creditAnimationTicks = 60;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
#include "stdafx.h"
|
||||||
|
/**
|
||||||
|
* @file TetrisPieceEffects.cpp
|
||||||
|
* @brief 实现彩虹、爆破、激光、十字和稳定结构等特殊方块落地效果。
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "TetrisLogicInternal.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 结算彩虹方块固定后的染色和清除效果。
|
||||||
|
* @param overflowTop 固定时是否已经越过棋盘顶部。
|
||||||
|
* @param fixedCells 当前方块写入棋盘的格子数组。
|
||||||
|
* @param fixedCellCount 写入棋盘的格子数量。
|
||||||
|
*/
|
||||||
|
void ApplyRainbowLandingEffect(bool overflowTop, const Point* fixedCells, int fixedCellCount)
|
||||||
|
{
|
||||||
|
if (overflowTop || !currentPieceIsRainbow)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 优先使用实际固定格子的平均行作为主色行,避免旋转形状偏移导致判定不自然。
|
||||||
|
int rainbowAnchorRow = point.y + 1;
|
||||||
|
if (fixedCellCount > 0)
|
||||||
|
{
|
||||||
|
int ySum = 0;
|
||||||
|
for (int i = 0; i < fixedCellCount; i++)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 结算爆破方块的范围清除效果。
|
||||||
|
* @param explosiveCells 爆破方块写入棋盘的格子数组。
|
||||||
|
* @param explosiveCellCount 爆破格子数量。
|
||||||
|
*/
|
||||||
|
static void ApplyExplosiveLandingEffect(const Point* explosiveCells, int explosiveCellCount)
|
||||||
|
{
|
||||||
|
if (!currentPieceIsExplosive)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int explosiveCellsCleared = 0;
|
||||||
|
for (int i = 0; i < explosiveCellCount; i++)
|
||||||
|
{
|
||||||
|
explosiveCellsCleared += ClearExplosiveAreaAt(explosiveCells[i].y, explosiveCells[i].x);
|
||||||
|
}
|
||||||
|
|
||||||
|
int explosiveScoreGain = 0;
|
||||||
|
int explosiveExpGain = 0;
|
||||||
|
if (currentMode == MODE_ROGUE && explosiveCellsCleared > 0)
|
||||||
|
{
|
||||||
|
AwardRogueSkillClearRewards(explosiveCellsCleared, explosiveScoreGain, explosiveExpGain, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
TCHAR explosiveDetail[128];
|
||||||
|
_stprintf_s(
|
||||||
|
explosiveDetail,
|
||||||
|
_T("爆破清除 %d 格 +%d 分 +%d EXP"),
|
||||||
|
explosiveCellsCleared,
|
||||||
|
explosiveScoreGain,
|
||||||
|
explosiveExpGain);
|
||||||
|
SetFeedbackMessage(_T("爆破核心"), explosiveDetail, 12);
|
||||||
|
|
||||||
|
// 连环炸弹需要等标准消行判断完成后,再决定是否追加一次小爆炸。
|
||||||
|
if (rogueStats.chainBombLevel > 0 && explosiveCellCount > 0)
|
||||||
|
{
|
||||||
|
pendingChainBombCenter = explosiveCells[0];
|
||||||
|
pendingChainBombFollowup = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 结算激光方块的整列清除效果。
|
||||||
|
* @param fixedCells 当前方块写入棋盘的格子数组。
|
||||||
|
* @param fixedCellCount 写入棋盘的格子数量。
|
||||||
|
*/
|
||||||
|
static void ApplyLaserLandingEffect(const Point* fixedCells, int fixedCellCount)
|
||||||
|
{
|
||||||
|
if (!currentPieceIsLaser)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
laserColumn = 0;
|
||||||
|
}
|
||||||
|
if (laserColumn >= nGameWidth)
|
||||||
|
{
|
||||||
|
laserColumn = nGameWidth - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int laserCellsCleared = ClearColumnAt(laserColumn);
|
||||||
|
if (currentMode == MODE_ROGUE && laserCellsCleared > 0)
|
||||||
|
{
|
||||||
|
int laserScore = 0;
|
||||||
|
int laserExp = 0;
|
||||||
|
AwardRogueSkillClearRewards(laserCellsCleared, laserScore, laserExp, false);
|
||||||
|
|
||||||
|
TCHAR laserDetail[128];
|
||||||
|
_stprintf_s(laserDetail, _T("激光贯穿第 %d 列,清除 %d 格 +%d 分 +%d EXP"), laserColumn + 1, laserCellsCleared, laserScore, laserExp);
|
||||||
|
SetFeedbackMessage(_T("棱镜激光"), laserDetail, 12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 结算十字方块的整行整列清除效果。
|
||||||
|
* @param fixedCells 当前方块写入棋盘的格子数组。
|
||||||
|
* @param fixedCellCount 写入棋盘的格子数量。
|
||||||
|
*/
|
||||||
|
static void ApplyCrossLandingEffect(const Point* fixedCells, int fixedCellCount)
|
||||||
|
{
|
||||||
|
if (!currentPieceIsCross)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int crossRow = point.y + 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)
|
||||||
|
{
|
||||||
|
crossRow = 0;
|
||||||
|
}
|
||||||
|
if (crossRow >= GetRoguePlayableHeight())
|
||||||
|
{
|
||||||
|
crossRow = GetRoguePlayableHeight() - 1;
|
||||||
|
}
|
||||||
|
if (crossColumn < 0)
|
||||||
|
{
|
||||||
|
crossColumn = 0;
|
||||||
|
}
|
||||||
|
if (crossColumn >= nGameWidth)
|
||||||
|
{
|
||||||
|
crossColumn = nGameWidth - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int crossCellsCleared = ClearRowAt(crossRow);
|
||||||
|
int columnCellsCleared = ClearColumnAtWithColor(crossColumn, RGB(196, 255, 132));
|
||||||
|
if (workRegion[crossRow][crossColumn] == 0 && columnCellsCleared > 0)
|
||||||
|
{
|
||||||
|
// 中心格可能已经在行清除时被计数,这里保持原有结算方式。
|
||||||
|
}
|
||||||
|
int totalCrossCleared = crossCellsCleared + columnCellsCleared;
|
||||||
|
|
||||||
|
if (currentMode == MODE_ROGUE && totalCrossCleared > 0)
|
||||||
|
{
|
||||||
|
int crossScore = 0;
|
||||||
|
int crossExp = 0;
|
||||||
|
AwardRogueSkillClearRewards(totalCrossCleared, crossScore, crossExp, false);
|
||||||
|
|
||||||
|
TCHAR crossDetail[128];
|
||||||
|
_stprintf_s(crossDetail, _T("十字冲击第 %d 行 / 第 %d 列,清除 %d 格 +%d 分 +%d EXP"), crossRow + 1, crossColumn + 1, totalCrossCleared, crossScore, crossExp);
|
||||||
|
SetFeedbackMessage(_T("十字方块"), crossDetail, 12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 结算非彩虹方块触发的稳定结构效果。
|
||||||
|
*/
|
||||||
|
static void ApplyStableStructureEffect()
|
||||||
|
{
|
||||||
|
if (!currentPieceIsRainbow && TryStabilizeBoard() > 0)
|
||||||
|
{
|
||||||
|
SetFeedbackMessage(_T("稳定结构"), _T("附近空洞被自动填补,阵型更加稳固。"), 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 结算爆破、激光、十字和稳定结构等特殊落地效果。
|
||||||
|
* @param fixedCells 当前方块写入棋盘的格子数组。
|
||||||
|
* @param fixedCellCount 写入棋盘的格子数量。
|
||||||
|
* @param explosiveCells 爆破方块写入棋盘的格子数组。
|
||||||
|
* @param explosiveCellCount 爆破格子数量。
|
||||||
|
*/
|
||||||
|
void ApplySpecialLandingEffects(const Point* fixedCells, int fixedCellCount, const Point* explosiveCells, int explosiveCellCount)
|
||||||
|
{
|
||||||
|
ApplyExplosiveLandingEffect(explosiveCells, explosiveCellCount);
|
||||||
|
ApplyLaserLandingEffect(fixedCells, fixedCellCount);
|
||||||
|
ApplyCrossLandingEffect(fixedCells, fixedCellCount);
|
||||||
|
ApplyStableStructureEffect();
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
#include "stdafx.h"
|
||||||
|
/**
|
||||||
|
* @file TetrisRenderAssets.cpp
|
||||||
|
* @brief 实现 GDI+ 初始化以及背景图、致谢页图片的加载与缓存。
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "TetrisRenderInternal.h"
|
||||||
|
#include "TetrisAssets.h"
|
||||||
|
#include <objidl.h>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#pragma comment(lib, "gdiplus.lib")
|
||||||
|
|
||||||
|
using namespace Gdiplus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 尝试从指定路径加载 GDI+ 位图。
|
||||||
|
* @param path 图片文件路径。
|
||||||
|
* @return 加载成功返回位图指针,失败返回 nullptr。
|
||||||
|
*/
|
||||||
|
static Bitmap* TryLoadBitmap(const std::wstring& path)
|
||||||
|
{
|
||||||
|
if (path.empty() || !FileExists(path))
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
Bitmap* loadedImage = Bitmap::FromFile(path.c_str(), FALSE);
|
||||||
|
if (loadedImage != nullptr && loadedImage->GetLastStatus() == Ok)
|
||||||
|
{
|
||||||
|
return loadedImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete loadedImage;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 确保 GDI+ 已初始化,返回初始化是否成功。
|
||||||
|
* @return GDI+ 可用返回 true,否则返回 false。
|
||||||
|
*/
|
||||||
|
static bool EnsureGdiplusStarted()
|
||||||
|
{
|
||||||
|
static ULONG_PTR gdiplusToken = 0;
|
||||||
|
static bool attempted = false;
|
||||||
|
static bool started = false;
|
||||||
|
|
||||||
|
if (!attempted)
|
||||||
|
{
|
||||||
|
// GDI+ 只需要初始化一次,静态标记避免重复启动。
|
||||||
|
attempted = true;
|
||||||
|
GdiplusStartupInput startupInput;
|
||||||
|
started = GdiplusStartup(&gdiplusToken, &startupInput, nullptr) == Ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
return started;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 加载并缓存主背景图片。
|
||||||
|
* @return 成功时返回缓存位图指针,失败时返回 nullptr。
|
||||||
|
*/
|
||||||
|
Bitmap* LoadBackgroundImage()
|
||||||
|
{
|
||||||
|
static Bitmap* backgroundImage = nullptr;
|
||||||
|
static bool attempted = false;
|
||||||
|
|
||||||
|
if (!attempted)
|
||||||
|
{
|
||||||
|
attempted = true;
|
||||||
|
|
||||||
|
if (EnsureGdiplusStarted())
|
||||||
|
{
|
||||||
|
const std::wstring candidates[] =
|
||||||
|
{
|
||||||
|
BuildAssetPath(L"assets\\images\\background.png"),
|
||||||
|
BuildWorkingDirAssetPath(L"assets\\images\\background.png"),
|
||||||
|
BuildAssetPath(L"assets\\images\\background.bmp"),
|
||||||
|
BuildWorkingDirAssetPath(L"assets\\images\\background.bmp")
|
||||||
|
};
|
||||||
|
|
||||||
|
// 同时支持构建目录运行和项目根目录运行两种启动方式。
|
||||||
|
for (const std::wstring& candidate : candidates)
|
||||||
|
{
|
||||||
|
backgroundImage = TryLoadBitmap(candidate);
|
||||||
|
if (backgroundImage != nullptr)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return backgroundImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 按序号加载并缓存致谢页图片。
|
||||||
|
* @param index 致谢页图片序号。
|
||||||
|
* @return 成功时返回缓存位图指针,失败或越界时返回 nullptr。
|
||||||
|
*/
|
||||||
|
Bitmap* LoadCreditImage(int index)
|
||||||
|
{
|
||||||
|
constexpr int creditPageCount = 5;
|
||||||
|
static Bitmap* creditImages[creditPageCount] = {};
|
||||||
|
static bool attempted[creditPageCount] = {};
|
||||||
|
|
||||||
|
if (index < 0 || index >= creditPageCount)
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!attempted[index])
|
||||||
|
{
|
||||||
|
attempted[index] = true;
|
||||||
|
|
||||||
|
if (EnsureGdiplusStarted())
|
||||||
|
{
|
||||||
|
const wchar_t* imageNames[creditPageCount] =
|
||||||
|
{
|
||||||
|
L"assets\\images\\qls.jpg",
|
||||||
|
L"assets\\images\\wyk.jpg",
|
||||||
|
L"assets\\images\\swj.jpg",
|
||||||
|
L"assets\\images\\qhy.jpg",
|
||||||
|
L"assets\\images\\syc.jpg"
|
||||||
|
};
|
||||||
|
const std::wstring creditExtraCandidates[] =
|
||||||
|
{
|
||||||
|
BuildAssetPath(imageNames[index]),
|
||||||
|
BuildWorkingDirAssetPath(imageNames[index]),
|
||||||
|
BuildAssetPath(L"assets\\images\\qhy.png"),
|
||||||
|
BuildWorkingDirAssetPath(L"assets\\images\\qhy.png"),
|
||||||
|
BuildAssetPath(L"assets\\images\\qhy.jpeg"),
|
||||||
|
BuildWorkingDirAssetPath(L"assets\\images\\qhy.jpeg"),
|
||||||
|
BuildAssetPath(L"assets\\images\\qhy.bmp"),
|
||||||
|
BuildWorkingDirAssetPath(L"assets\\images\\qhy.bmp")
|
||||||
|
};
|
||||||
|
int candidateCount = (index == 3) ? 8 : 2;
|
||||||
|
|
||||||
|
// 第四张致谢图历史上有多种扩展名,这里保留兼容查找。
|
||||||
|
for (int i = 0; i < candidateCount; i++)
|
||||||
|
{
|
||||||
|
creditImages[index] = TryLoadBitmap(creditExtraCandidates[i]);
|
||||||
|
if (creditImages[index] != nullptr)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return creditImages[index];
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
|||||||
// stdafx.cpp : 只包括标准包含文件的源文件
|
/**
|
||||||
// Tetris.pch 将作为预编译头
|
* @file stdafx.cpp
|
||||||
// stdafx.obj 将包含预编译类型信息
|
* @brief 预编译头源文件,只包含 stdafx.h 以生成共享编译信息。
|
||||||
|
*/
|
||||||
|
|
||||||
#include "stdafx.h"
|
#include "stdafx.h"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user