46 Commits

Author SHA1 Message Date
Qi-huanye d5f6cea2ed 进一步补充详细注释 2026-05-01 16:27:27 +08:00
Qi-huanye 84017ae6b7 再次整理文件结构 2026-05-01 16:03:34 +08:00
Qi-huanye 7fe0244a99 文件调整 2026-04-29 15:56:31 +08:00
Qi-huanye be95bd25e1 清屏炸弹不可多选 2026-04-29 15:45:02 +08:00
Qi-huanye b98d2c9d59 调整致谢页并清理worktree 2026-04-29 15:27:06 +08:00
Qi-huanye a331162349 进一步拆分与注释补强 2026-04-29 15:21:14 +08:00
Qi-huanye 58ab400949 调整照片 2026-04-29 15:05:37 +08:00
Qi-huanye 1c000c3c21 补强注释 2026-04-28 23:18:51 +08:00
Qi-huanye 0840a807b5 项目架构重构,代码整理 2026-04-28 22:44:31 +08:00
Qi-huanye 2f435f5ca6 优化帮助页逻辑 2026-04-28 21:11:50 +08:00
Qi-huanye 45d9e988df 去除重力 2026-04-28 20:57:57 +08:00
Qi-huanye aa9e2f3ddc 补充调整演示模式 2026-04-28 20:41:15 +08:00
Qi-huanye 971d8be0dc 下落调整 2026-04-28 20:31:41 +08:00
Qi-huanye 9341ac9a05 演示调整 2026-04-28 20:18:04 +08:00
wyk c77b877b8b 增加技能演示选项 2026-04-28 18:28:46 +08:00
Qi-huanye da741d1e56 加入帮助页轮播框架未完善(实则一坨) 2026-04-28 15:03:51 +08:00
Qi-huanye 647038b27a 致谢页添加 2026-04-28 14:34:26 +08:00
Qi-huanye 00729fbe17 清屏炸弹可以重复选择 2026-04-28 14:22:36 +08:00
Qi-huanye 2c04796010 修复强化界面m不能返回 2026-04-27 23:16:54 +08:00
Qi-huanye f3065c5fe7 空中换形可重复选择 2026-04-27 23:14:50 +08:00
Qi-huanye b01d48a88d 添加致谢页 2026-04-27 16:46:23 +08:00
Qi-huanye 7c747ac9fd TODO 2026-04-27 00:12:21 +08:00
Qi-huanye 92a8c40734 黑洞可以多选 2026-04-26 23:55:17 +08:00
Qi-huanye 918e0b1e86 修复存在保留强化 2026-04-26 23:52:46 +08:00
Qi-huanye 38152d9b3d 将底线清道夫添加上限级别 2026-04-26 19:45:41 +08:00
Qi-huanye 50dd54f09e 落实彩虹方块稀有度修改 2026-04-26 19:36:05 +08:00
Qi-huanye 79a14516bb 调整强化文字大小 2026-04-26 19:32:13 +08:00
Qi-huanye a5747ff55c 优化选择强化逻辑 2026-04-26 19:30:32 +08:00
Qi-huanye 34c36306fe 调整赌徒强化 2026-04-26 18:58:41 +08:00
Qi-huanye 24e71704e5 降低彩虹方块的稀有度 2026-04-26 18:47:16 +08:00
Qi-huanye d96ad779b1 调整文字遮挡 2026-04-26 18:34:35 +08:00
Qi-huanye fcc9fbb981 完善彩色方块特效 2026-04-26 18:31:56 +08:00
Qi-huanye 667d657ee1 增强彩虹方块 重新设计效果 2026-04-26 18:29:58 +08:00
Qi-huanye 23d0fa63b6 修改特殊方块消行逻辑 以及增强彩虹方块 2026-04-26 18:17:45 +08:00
Qi-huanye 93045cc2d3 修改彩虹方块效果 2026-04-26 18:02:20 +08:00
Qi-huanye 8e68d9c712 突出特殊方块删除 2026-04-26 17:59:24 +08:00
Qi-huanye 30fb10b66c 完善彩虹方块逻辑 游戏帮助描述 2026-04-26 17:24:41 +08:00
Qi-huanye 0485cd30fe 补充经典模式重力 2026-04-26 17:14:51 +08:00
Qi-huanye e2706bcdcc 代码结构整理 2026-04-26 17:13:14 +08:00
Qi-huanye 13ae305e53 调整ui 强化和诅咒显示 2026-04-26 16:49:17 +08:00
Qi-huanye 47ca7473ec 删除无用文件 整理README等文件 2026-04-26 16:06:04 +08:00
Qi-huanye 7db0bfadfc 添加鼠标点击功能 2026-04-26 16:02:52 +08:00
Qi-huanye a117b12981 添加完善说明 2026-04-26 15:20:50 +08:00
Qi-huanye ea10e6ef12 粒子调整 2026-04-26 15:12:35 +08:00
Qi-huanye cbf7bac239 升级多选优化 2026-04-26 14:57:21 +08:00
Qi-huanye 24c24acf44 修复exp 2026-04-26 14:50:15 +08:00
35 changed files with 9497 additions and 4344 deletions
+1
View File
@@ -1,5 +1,6 @@
# Build outputs
/.vscode-build/
/.worktrees/
/build/
/bin/
/obj/
+40
View File
@@ -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
View File
@@ -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++ 更适合作为兼容运行方案。
+286 -76
View File
@@ -1,68 +1,250 @@
# Tereis
基于 C++ 与 Windows API 实现的俄罗斯方块课程项目
Tereis 是一个基于 C++、Win32 API、GDI/GDI+ 实现的桌面版俄罗斯方块课程大作业
项目使用 MinGW 进行构建,当前已完成基础窗口框架、方块逻辑、绘图显示与资源编译接入,适合作为《大学计算》程序设计大作业使用
项目在经典俄罗斯方块玩法上扩展了 Rogue 模式,加入等级成长、强化选择、主动技能、特殊方块、视频复活、鼠标交互和视觉特效。程序不依赖游戏引擎,主要使用 Win32 消息循环和 GDI 绘图完成
## 项目简介
## 快速运行
项目目标是实现一个可运行的桌面版俄罗斯方块程序,包含以下核心内容:
推荐在 Windows + PowerShell + MinGW-w64 环境下运行。
- 创建 Windows 游戏窗口
- 实现方块生成、移动、旋转与下落
- 实现碰撞检测、方块固定与游戏结束判定
- 实现消行逻辑与基础分数系统
- 实现界面绘制与部分资源显示
- 提供 MinGW 构建脚本和 VS Code 调试配置
1. 确认 `g++.exe``windres.exe` 已加入 `PATH`,或安装在 `C:\mingw64\bin\`
2. 在项目根目录执行构建并运行:
## 目录结构
```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
Tereis/
├─ src/
│ ├─ include/ 头文件
│ ├─ source/ 源文件
└─ resources/ Windows 资源脚本
│ ├─ include/ 头文件
│ ├─ source/ 源文件
│ ├─ Tetris.cpp 程序入口、窗口和消息框架
│ │ ├─ TetrisLogic.cpp 基础俄罗斯方块逻辑框架
│ │ ├─ TetrisRender.cpp 基础绘制框架
│ │ ├─ common/ 资源路径、文件检查等通用工具
│ │ ├─ app/ 媒体播放、布局命中、输入和定时器处理
│ │ ├─ extensions/ 框架外通用扩展、界面状态和视觉效果
│ │ ├─ logic/ 特殊方块落地效果等逻辑扩展
│ │ ├─ render/ 图片加载等渲染内部支持
│ │ └─ rogue/ Rogue 模式、强化和技能系统
│ └─ resources/ Windows 资源脚本
├─ assets/
│ ├─ icons/ 图标资源
│ ├─ images/ 图片资源
audio/ 音频资源
├─ .vscode/ VS Code 配置
├─ .vscode-build/ 本地构建输出目录
├─ report/ 实验报告材料与草稿
├─ build-mingw.ps1 MinGW 构建脚本
├─ list.md 项目阶段划分
├─ VSCode运行说明.md VS Code 使用说明
└─ README.md 项目说明
│ ├─ audio/ 背景音乐
│ ├─ icons/ 程序图标
images/ 背景图片
│ └─ video/ 复活视频
├─ report/ 报告相关材料
├─ .vscode/ VS Code 配置
├─ .vscode-build/ 本地构建输出目录
├─ build-mingw.ps1 MinGW 构建脚本
├─ README.md 项目说明
└─ AGENTS.md 项目协作和代码生成约束
```
## 开发阶段划分
## 构建环境
整个程序按 6 个阶段拆分实现
1. 窗口创建与程序框架搭建
2. 游戏区域与方块数据结构设计
3. 方块生成、移动与旋转功能
4. 碰撞检测与方块固定逻辑
5. 消除逻辑与分数系统
6. 界面完善与创新功能扩展
详细内容见 [list.md](./list.md)。
## 构建与运行
### 环境要求
推荐环境
- Windows
- MinGW
- `g++.exe`
- `gdb.exe`
- `windres.exe`
- 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
```
构建完成后会生成:
构建成功后生成:
```text
.vscode-build\mingw\Tetris.exe
```
### 构建直接运行
构建直接运行
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\build-mingw.ps1 -Run
```
### 使用 VS Code
也可以直接运行已生成的程序:
项目已提供:
```powershell
.\.vscode-build\mingw\Tetris.exe
```
- 构建任务 `build Tetris MinGW`
- 运行任务 `run Tetris MinGW`
- 调试配置 `Debug Tetris MinGW`
如果使用 VS Code
详见 [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/` 目录。项目会从资源目录读取背景、音乐和复活视频。
- 修复项目迁移后的路径配置问题
- 补充 `.gitignore`
- 接入资源文件编译流程
- 整理项目阶段清单
- 建立实验报告目录结构
### 3. 视频复活播放失败
后续可以继续完善的方向
项目会优先查找
- 优化界面表现
- 完善分数与状态提示
- 增加创新功能
- 补充测试截图和实验分析
- `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
View File
@@ -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
View File
@@ -61,13 +61,9 @@ foreach ($Candidate in $WindresCandidates) {
New-Item -ItemType Directory -Force -Path $BuildDir | Out-Null
$Sources = @(
(Join-Path $SourceDir "stdafx.cpp"),
(Join-Path $SourceDir "Tetris.cpp"),
(Join-Path $SourceDir "TetrisLogic.cpp"),
(Join-Path $SourceDir "TetrisRogue.cpp"),
(Join-Path $SourceDir "TetrisRender.cpp")
)
$Sources = Get-ChildItem -Path $SourceDir -Recurse -Filter "*.cpp" |
Sort-Object FullName |
Select-Object -ExpandProperty FullName
$LinkInputs = @()
+380
View File
@@ -1,11 +1,17 @@
#pragma once
/**
* @file Tetris.h
* @brief 定义俄罗斯方块项目的全局常量、结构体、枚举、全局状态和公开函数接口。
*/
#include "resource.h"
#include "stdafx.h"
#include <mmsystem.h>
#pragma comment(lib, "winmm.lib")
// 棋盘和窗口基础尺寸,渲染层会按当前窗口大小统一缩放这些设计稿尺寸。
constexpr int GRID = 40;
constexpr int nGameWidth = 10;
constexpr int nGameHeight = 20;
@@ -19,18 +25,27 @@ constexpr int WINDOW_CLIENT_WIDTH = WINDOW_PADDING * 2 + nGameWidth * GRID + SID
constexpr int BOARD_CLIENT_HEIGHT = WINDOW_PADDING * 2 + nGameHeight * GRID + 20;
constexpr int WINDOW_CLIENT_HEIGHT = (BOARD_CLIENT_HEIGHT > SIDE_PANEL_HEIGHT) ? BOARD_CLIENT_HEIGHT : SIDE_PANEL_HEIGHT;
/**
* @brief 棋盘坐标点,x 表示列号,y 表示行号。
*/
struct Point
{
int x;
int y;
};
/**
* @brief 主菜单导航状态。
*/
struct MenuState
{
int selectedIndex;
int optionCount;
};
/**
* @brief 帮助、规则、致谢和技能演示页面共享的导航状态。
*/
struct HelpState
{
int selectedIndex;
@@ -38,6 +53,12 @@ struct HelpState
int currentPage;
};
/**
* @brief 记录经典模式和 Rogue 模式的分数、等级、强化与临时状态。
*
* 课程要求不使用 class,因此所有与玩家成长有关的数据都集中放在结构体字段中,
* 由逻辑层函数按过程式方式读取和修改。
*/
struct PlayerStats
{
int score;
@@ -110,6 +131,9 @@ struct PlayerStats
int pieceTuningLevels[7];
};
/**
* @brief 升级界面中已经生成并显示给玩家的一个候选强化。
*/
struct UpgradeOption
{
int id;
@@ -122,6 +146,9 @@ struct UpgradeOption
const TCHAR* description;
};
/**
* @brief 强化池中的基础配置项,用于生成升级界面候选。
*/
struct UpgradeEntry
{
int id;
@@ -133,6 +160,9 @@ struct UpgradeEntry
const TCHAR* description;
};
/**
* @brief Rogue 升级选择界面的临时 UI 状态。
*/
struct UpgradeUiState
{
int selectedIndex;
@@ -140,9 +170,14 @@ struct UpgradeUiState
int pendingCount;
int totalChosenCount;
int picksRemaining;
int markedCount;
bool marked[6];
UpgradeOption options[6];
};
/**
* @brief 右侧战斗日志或提示条的显示状态。
*/
struct FeedbackState
{
int visibleTicks;
@@ -150,6 +185,9 @@ struct FeedbackState
TCHAR detail[128];
};
/**
* @brief 标准消行动画状态。
*/
struct ClearEffectState
{
int ticks;
@@ -158,6 +196,9 @@ struct ClearEffectState
int rows[8];
};
/**
* @brief 棋盘上浮动文字特效的单个实例。
*/
struct FloatingTextEffect
{
int ticks;
@@ -168,6 +209,9 @@ struct FloatingTextEffect
COLORREF color;
};
/**
* @brief 棋盘粒子特效的单个实例。
*/
struct ParticleEffect
{
int ticks;
@@ -180,6 +224,34 @@ struct ParticleEffect
COLORREF color;
};
/**
* @brief 被清除格子的短时闪烁高亮状态。
*/
struct CellFlashEffect
{
int ticks;
int totalTicks;
int x;
int y;
COLORREF color;
};
/**
* @brief 固定方块受重力下落时的残影轨迹状态。
*/
struct GravityFallEffect
{
int ticks;
int totalTicks;
int x;
int fromY;
int toY;
int cellValue;
};
/**
* @brief 当前应用所在的大界面。
*/
enum ScreenState
{
SCREEN_MENU = 0,
@@ -188,12 +260,18 @@ enum ScreenState
SCREEN_RULES = 3
};
/**
* @brief 当前游戏玩法模式。
*/
enum GameMode
{
MODE_CLASSIC = 0,
MODE_ROGUE = 1
};
/**
* @brief 强化候选的稀有度,用于渲染不同颜色和排序说明。
*/
enum UpgradeRarity
{
UPGRADE_RARITY_COMMON = 0,
@@ -201,6 +279,7 @@ enum UpgradeRarity
UPGRADE_RARITY_RARE = 2
};
// 以下全局状态沿用老师框架的过程式组织方式,各模块通过函数集中维护这些变量。
extern int nType;
extern int type;
extern int state;
@@ -210,11 +289,17 @@ extern bool suspendFlag;
extern bool targetFlag;
extern bool bgmEnabled;
extern bool reviveAvailable;
extern bool rogueDemoMode;
extern int workRegion[20][10];
extern Point point;
extern Point target;
extern MenuState menuState;
extern HelpState helpState;
extern int helpScrollOffset;
extern int creditPageIndex;
extern int creditAnimationTicks;
extern int creditAnimationDirection;
extern int upgradeListScrollOffset;
extern PlayerStats classicStats;
extern PlayerStats rogueStats;
extern UpgradeUiState upgradeUiState;
@@ -222,6 +307,8 @@ extern FeedbackState feedbackState;
extern ClearEffectState clearEffectState;
extern FloatingTextEffect floatingTextEffects[8];
extern ParticleEffect particleEffects[96];
extern CellFlashEffect cellFlashEffects[64];
extern GravityFallEffect gravityFallEffects[80];
extern int currentScreen;
extern int currentMode;
extern int currentFallInterval;
@@ -235,42 +322,335 @@ extern bool currentPieceIsRainbow;
extern int bricks[7][4][4][4];
extern COLORREF BrickColor[7];
/**
* @brief 判断当前活动方块是否还能向下移动一格。
* @return 可以下落返回 true,否则返回 false。
*/
bool CanMoveDown();
/**
* @brief 判断当前活动方块是否还能向左移动一格。
* @return 可以左移返回 true,否则返回 false。
*/
bool CanMoveLeft();
/**
* @brief 判断当前活动方块是否还能向右移动一格。
* @return 可以右移返回 true,否则返回 false。
*/
bool CanMoveRight();
/**
* @brief 将当前活动方块向下移动一格。
*/
void MoveDown();
/**
* @brief 将当前活动方块向左移动一格。
*/
void MoveLeft();
/**
* @brief 将当前活动方块向右移动一格。
*/
void MoveRight();
/**
* @brief 尝试旋转当前活动方块,Rogue 完美旋转会额外尝试左右偏移。
*/
void Rotate();
/**
* @brief 将当前活动方块直接下落到预测落点。
*/
void DropDown();
/**
* @brief 将当前活动方块固定到棋盘并生成下一块。
*/
void Fixing();
/**
* @brief 删除指定行并让上方棋盘整体下落。
* @param number 要删除的棋盘行号。
*/
void DeleteOneLine(int number);
/**
* @brief 扫描棋盘、删除所有满行并触发消行结算。
* @return 本次删除的行数。
*/
int DeleteLines();
/**
* @brief 判断当前游戏是否已经结束。
* @return 游戏结束返回 true,否则返回 false。
*/
bool GameOver();
/**
* @brief 计算当前活动方块的预测落点。
*/
void ComputeTarget();
/**
* @brief 重置棋盘、方块、统计和视觉状态,开始一局新游戏。
*/
void Restart();
/**
* @brief 按指定模式开始新游戏。
* @param mode 游戏模式,取值来自 GameMode。
*/
void StartGameWithMode(int mode);
/**
* @brief 返回主菜单并清理临时玩法与界面状态。
*/
void ReturnToMainMenu();
/**
* @brief 复活视频播放成功后恢复游戏并清理顶部空间。
*/
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);
/**
* @brief 打开帮助首页。
*/
void OpenRulesScreen();
/**
* @brief 打开 Rogue 技能演示选择页。
*/
void OpenSkillDemoScreen();
/**
* @brief 打开致谢页。
*/
void OpenCreditScreen();
/**
* @brief 切换致谢页图片。
* @param direction 小于 0 向前切换,大于 0 向后切换。
*/
void ChangeCreditPage(int direction);
/**
* @brief 打开 Rogue 升级选择界面。
*/
void OpenUpgradeMenu();
/**
* @brief 确认当前升级选择并恢复游戏流程。
*/
void ConfirmUpgradeSelection();
/**
* @brief 重置升级选择界面状态。
*/
void ResetUpgradeUiState();
/**
* @brief 使用或解锁后处理 Hold 备用仓逻辑。
*/
void HoldCurrentPiece();
/**
* @brief 使用清屏炸弹主动技能。
*/
void UseScreenBomb();
/**
* @brief 使用黑洞主动技能。
*/
void UseBlackHole();
/**
* @brief 使用空中换形主动技能。
*/
void UseAirReshape();
/**
* @brief 重置 Rogue 待播放视觉事件。
*/
void ResetPendingRogueVisualEvents();
/**
* @brief 清空所有视觉效果状态。
*/
void ResetVisualEffects();
/**
* @brief 推进视觉效果动画。
* @return 仍有动画需要刷新返回 true,否则返回 false。
*/
bool TickVisualEffects();
/**
* @brief 推进致谢页切换动画。
* @return 需要刷新界面返回 true,否则返回 false。
*/
bool TickCreditAnimation();
/**
* @brief 触发标准消行动画。
* @param rows 被消除的行号数组。
* @param rowCount 行号数量。
* @param linesCleared 实际消除行数。
*/
void TriggerLineClearEffect(const int* rows, int rowCount, int linesCleared);
/**
* @brief 播放之前因升级界面暂存的消行动画。
*/
void PlayPendingLineClearEffect();
/**
* @brief 触发指定棋盘格的默认清除特效。
* @param cells 被清除格子数组。
* @param cellCount 格子数量。
* @param 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);
/**
* @brief 检查 Rogue 经验是否达到升级条件。
*/
void CheckRogueLevelProgress();
/**
* @brief 对棋盘固定方块应用重力下落。
*/
void ApplyBoardGravity();
/**
* @brief 计算当前 Rogue 模式下落间隔。
* @return 下落计时器间隔,单位毫秒。
*/
int GetRogueFallInterval();
/**
* @brief 获取 Rogue 当前可操作棋盘高度。
* @return 未被底部封锁占用的行数。
*/
int GetRoguePlayableHeight();
/**
* @brief 获取 Rogue 难度系统当前封锁的底部行数。
* @return 封锁行数。
*/
int GetRogueLockedRows();
/**
* @brief 按经过时间推进 Rogue 难度。
* @param elapsedMs 本次推进的时间,单位毫秒。
*/
void AdvanceRogueDifficulty(int elapsedMs);
/**
* @brief 获取进化强化的合成路线文本。
* @param upgradeId 强化编号。
* @return 路线文本;普通强化返回空或空指针。
*/
const TCHAR* GetUpgradeSynthesisPath(int upgradeId);
/**
* @brief 绘制当前窗口中的完整游戏界面。
* @param hdc 目标绘图设备上下文。
* @param hWnd 当前窗口句柄,用于读取客户区大小。
*/
void TDrawScreen(HDC hdc, HWND hWnd);
+233
View File
@@ -0,0 +1,233 @@
#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;
/**
* @brief 当前窗口缩放后的布局参数。
*
* 输入命中区域和渲染坐标必须使用同一套缩放参数,才能保证鼠标点击位置
* 与屏幕上看到的按钮、卡片位置一致。
*/
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);
+32
View File
@@ -0,0 +1,32 @@
#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);
+206 -1
View File
@@ -1,23 +1,228 @@
#pragma once
/**
* @file TetrisLogicInternal.h
* @brief 声明棋盘逻辑、Rogue 结算和特殊方块效果使用的内部接口。
*/
#include "Tetris.h"
extern Point pendingChainBombCenter;
extern bool pendingChainBombFollowup;
extern int pendingLineClearEffectTicks;
extern int pendingLineClearEffectRows[8];
extern int pendingLineClearEffectRowCount;
extern int pendingLineClearEffectLineCount;
// Internal 头文件只暴露跨 cpp 文件共享的辅助函数,外部窗口层仍通过 Tetris.h 调用公开接口。
/**
* @brief 计算指定方块在棋盘顶部的统一生成位置。
* @param brickType 方块类型编号。
* @return 生成坐标,可能位于可视区域上方。
*/
Point GetSpawnPoint(int brickType);
/**
* @brief 收集当前方块将要固定到棋盘上的格子,并写入工作区。
* @param overflowTop 返回是否有方块格位于可视区域顶部之外。
* @param fixedCells 返回普通落地格数组。
* @param fixedCellCount 返回普通落地格数量。
* @param explosiveCells 返回爆破方块落地格数组。
* @param explosiveCellCount 返回爆破格数量。
*/
void CollectAndWriteFixedCells(
bool& overflowTop,
Point fixedCells[],
int& fixedCellCount,
Point explosiveCells[],
int& explosiveCellCount);
/**
* @brief 处理方块固定时的顶部溢出、终末清场和最后一搏。
* @param overflowTop 是否出现顶部溢出。
* @return 游戏可以继续返回 true,需要结束返回 false。
*/
bool ResolveFixingOverflow(bool overflowTop);
/**
* @brief 生成下一枚活动方块,并刷新 Hold、特殊方块和预测落点状态。
*/
void SpawnNextFallingPiece();
/**
* @brief 从底向上扫描满行并删除,记录本次消除的原始行号。
* @param clearedRows 返回最多 8 个被消除行号。
* @param clearedRowCount 返回记录的行号数量。
* @return 本次标准消行数量。
*/
int ScanAndDeleteFullLines(int clearedRows[], int& clearedRowCount);
/**
* @brief 根据当前界面状态立即播放或暂存消行动画。
* @param clearedRows 已消除行号数组。
* @param clearedRowCount 行号数量。
* @param clearedLines 本次消行数量。
*/
void DispatchLineClearEffect(const int clearedRows[], int clearedRowCount, int clearedLines);
/**
* @brief 处理连环炸弹因消行触发的一次追加爆破。
* @param clearedLines 本次标准消行数量。
*/
void ResolveChainBombFollowup(int clearedLines);
/**
* @brief 重置经典或 Rogue 模式使用的玩家统计数据。
* @param stats 需要重置的统计结构。
* @param useRogueRules 是否按 Rogue 模式设置初始经验需求。
*/
void ResetPlayerStats(PlayerStats& stats, bool useRogueRules);
/**
* @brief 设置界面右侧显示的即时反馈标题、内容和持续时间。
* @param title 反馈标题。
* @param detail 反馈详情。
* @param 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);
/**
* @brief 判断棋盘格是否为彩虹特殊方块。
* @param cellValue 棋盘格存储值。
* @return 彩虹方块返回 true,否则返回 false。
*/
bool IsRainbowBoardCell(int cellValue);
/**
* @brief 触发小型黑洞并返回被清除的固定方块数量。
* @param maxCellsToClear 最多清除的格子数。
* @return 实际清除格子数。
*/
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();
/**
* @brief 清除指定中心点周围的爆破范围并返回清除格数。
* @param centerY 爆破中心行。
* @param centerX 爆破中心列。
* @return 实际清除格子数。
*/
int ClearExplosiveAreaAt(int centerY, int centerX);
/**
* @brief 清除指定列并返回清除格数。
* @param column 目标列号。
* @return 实际清除格子数。
*/
int ClearColumnAt(int column);
/**
* @brief 使用指定颜色特效清除指定列并返回清除格数。
* @param column 目标列号。
* @param flashColor 清除高亮颜色。
* @return 实际清除格子数。
*/
int ClearColumnAtWithColor(int column, COLORREF flashColor);
/**
* @brief 清除指定行并返回清除格数。
* @param row 目标行号。
* @return 实际清除格子数。
*/
int ClearRowAt(int row);
/**
* @brief 尝试填补局部空洞以稳定棋盘结构。
* @return 实际填补格子数。
*/
int TryStabilizeBoard();
/**
* @brief 为当前方块刷新 Rogue 特殊方块标记。
* @param 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();
/**
* @brief 消费队首下一方块并补充新的预览方块。
* @return 新的当前方块类型编号。
*/
int ConsumeNextType();
/**
* @brief 结算一次标准消行带来的 Rogue 玩法效果。
* @param 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);
+32
View File
@@ -0,0 +1,32 @@
#pragma once
/**
* @file TetrisRenderInternal.h
* @brief 声明渲染模块内部使用的 GDI+ 图片加载接口。
*/
#include "Tetris.h"
#include <objidl.h>
#include <gdiplus.h>
// 本内部头文件只给渲染拆分模块使用,外部代码仍通过 TDrawScreen 调用绘制入口。
/**
* @brief 加载并缓存主背景图片。
* @return 成功时返回缓存位图指针,失败时返回 nullptr。
*/
Gdiplus::Bitmap* LoadBackgroundImage();
/**
* @brief 按序号加载并缓存致谢页图片。
* @param index 致谢页图片序号。
* @return 成功时返回缓存位图指针,失败或越界时返回 nullptr。
*/
Gdiplus::Bitmap* LoadCreditImage(int index);
/**
* @brief 绘制完整游戏界面,供 TDrawScreen 总入口调用。
* @param hdc 目标绘图设备上下文。
* @param hWnd 当前窗口句柄。
*/
void RenderFullScreen(HDC hdc, HWND hWnd);
+10
View File
@@ -1,28 +1,38 @@
#pragma once
/**
* @file resource.h
* @brief 定义菜单、图标、对话框和命令等 Windows 资源编号。
*/
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ 生成的包含文件。
// 供 Tetris.rc 使用
//
// 字符串资源:窗口标题等文本由 Win32 启动流程按编号读取。
#define IDS_APP_TITLE 103
// 图标、对话框、菜单和命令编号需要与 Tetris.rc 中的资源定义保持一致。
#define IDR_MAINFRAME 128
#define IDD_TETRIS_DIALOG 102
#define IDD_ABOUTBOX 103
#define IDM_ABOUT 104
#define IDM_EXIT 105
#define IDM_SKILL_DEMO 106
#define IDI_TETRIS 107
#define IDI_SMALL 108
#define IDC_TETRIS 109
#define IDC_MYICON 2
#ifndef IDC_STATIC
// 静态文本控件使用 -1,表示运行时不需要通过控件 ID 单独访问。
#define IDC_STATIC -1
#endif
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
// 以下编号由资源编辑器维护,手工改动容易导致新增资源编号冲突。
#define _APS_NO_MFC 130
#define _APS_NEXT_RESOURCE_VALUE 129
#define _APS_NEXT_COMMAND_VALUE 32771
+7 -6
View File
@@ -1,17 +1,18 @@
// stdafx.h : 标准系统包含文件的包含文件,
// 或是经常使用但不常更改的
// 特定于项目的包含文件
//
/**
* @file stdafx.h
* @brief 集中包含 Windows、C 运行时和项目常用基础头文件。
*/
#pragma once
#include "targetver.h"
// 精简 Windows 头文件,缩短编译时间,同时保留本项目需要的 Win32/GDI API。
#define WIN32_LEAN_AND_MEAN // 从 Windows 头文件中排除极少使用的信息
// Windows 头文件:
#include <windows.h>
// C 运行时头文件
// C 运行时头文件:本项目使用随机数、内存工具、TCHAR 字符串和时间函数。
#include <stdlib.h>
#include <malloc.h>
#include <memory.h>
@@ -19,4 +20,4 @@
#include <time.h>
// TODO: 在此处引用程序需要的其他头文件
// 其他模块各自包含自己的业务头文件,避免预编译头承担过多项目依赖。
+8 -3
View File
@@ -1,8 +1,13 @@
#pragma once
// 包括 SDKDDKVer.h 将定义可用的最高版本的 Windows 平台。
/**
* @file targetver.h
* @brief 设置 Windows SDK 目标平台版本,供 Win32 头文件选择可用 API。
*/
// 如果要为以前的 Windows 平台生成应用程序,请包括 WinSDKVer.h,并将
// WIN32_WINNT 宏设置为要支持的平台,然后再包括 SDKDDKVer.h。
// 包括 SDKDDKVer.h 将定义可用的最高版本 Windows 平台宏。
// 若课程演示环境需要兼容更旧 Windows,可在这里先包含 WinSDKVer.h
// 再设置 WIN32_WINNT;当前项目直接使用 SDK 默认最高版本。
#include <SDKDDKVer.h>
Binary file not shown.
+110 -805
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+978
View File
@@ -0,0 +1,978 @@
#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'))
{
// Rogue 侧栏强化列表较长,J/K 只调整说明列表的滚动位置。
int direction = (key == 'J') ? 1 : -1;
AdjustScrollOffset(upgradeListScrollOffset, direction * GetScrollStep(hWnd, 52));
InvalidateRect(hWnd, nullptr, FALSE);
return true;
}
switch (key)
{
// 主动技能和 Hold 的按键统一在这里分发,技能内部会自行检查次数和模式。
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()
{
// 固定方块后立即检查满行,Rogue 模式还可能因为经验变化打开升级界面。
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);
}
+431
View File
@@ -0,0 +1,431 @@
#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;
}
+251
View File
@@ -0,0 +1,251 @@
#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()
{
// 根据当前播放方式选择对应的释放接口,避免 MCI 设备或 PlaySound 残留。
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)
{
// WAV 优先使用 PlaySound,依赖少、兼容性最好。
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)
{
// OGG 通过 MCI 尝试普通打开和 mpegvideo 强制类型两条路径。
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)
{
// 兼容旧资源名 background.wav,保证替换素材后仍能播放。
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)
{
// 依次查找 AVI 和 MP4,并同时支持构建目录与项目根目录运行。
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)
{
// MCI 全屏播放失败时退回系统默认播放器,并等待播放器进程结束。
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;
}
+342
View File
@@ -0,0 +1,342 @@
#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)
{
// 下落速度会被 Rogue 强化和临时状态动态修改,因此每次变化都重新注册定时器。
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;
// 狂热、缓流、极限缓速和 Hold 缓速都会影响下落间隔,需要同步重置主定时器。
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)
{
// 极限玩家只在真实 Rogue 战局中计时,暂停、结束和技能演示都不推进危险等级。
if (currentMode != MODE_ROGUE ||
IsRogueSkillDemoMode() ||
rogueStats.extremePlayerLevel <= 0 ||
currentScreen != SCREEN_PLAYING ||
suspendFlag ||
gameOverFlag)
{
return false;
}
if (rogueStats.extremeDangerTicks > 0)
{
// 计时尚未结束时只递减倒计时,不改变速度。
rogueStats.extremeDangerTicks--;
return false;
}
// 每 30 个主计时周期未完成四消就提高危险等级,并立即刷新下落速度。
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;
}
// Rogue 难度随时间推进,速度变化后需要重新安排下一次自动下落。
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;
}
// 主定时器集中推进演示、Rogue 临时状态、危险等级和自然下落。
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);
}
}
+82
View File
@@ -0,0 +1,82 @@
#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)
{
// 目录不能作为媒体或图片资源使用,因此排除 FILE_ATTRIBUTE_DIRECTORY。
DWORD attributes = GetFileAttributesW(path.c_str());
return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) == 0;
}
@@ -0,0 +1,773 @@
#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;
// 方块改造按 7 种方块分别记录等级,重开时逐项清空。
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)
{
// 使用 lstrcpyn 限长复制,避免长描述写出固定缓冲区。
feedbackState.visibleTicks = ticks;
lstrcpyn(feedbackState.title, title, sizeof(feedbackState.title) / sizeof(TCHAR));
lstrcpyn(feedbackState.detail, detail, sizeof(feedbackState.detail) / sizeof(TCHAR));
}
/**
* @brief
*/
void ResetVisualEffects()
{
// 主状态和各类效果槽位只需把 ticks 清零,渲染层会自动忽略非活动项。
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)
{
// 模式切换后直接复用 Restart,保证经典和 Rogue 都从干净状态开始。
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;
}
}
+323
View File
@@ -0,0 +1,323 @@
#include "stdafx.h"
/**
* @file TetrisCoreHelpers.cpp
* @brief
*/
#include "Tetris.h"
#include "TetrisLogicInternal.h"
/**
* @brief
*
* 4x4
* 使
*
* @param brickType
* @param brickState
* @param minRow
* @param maxRow
* @param minCol
* @param maxCol
*/
static void GetBrickBounds(int brickType, int brickState, int& minRow, int& maxRow, int& minCol, int& maxCol)
{
// 初始值设置在矩阵之外,遍历到第一个非空格后会被收缩到真实边界。
minRow = 4;
maxRow = -1;
minCol = 4;
maxCol = -1;
for (int i = 0; i < 4; i++)
{
for (int j = 0; j < 4; j++)
{
if (bricks[brickType][brickState][i][j] != 0)
{
if (i < minRow)
{
minRow = i;
}
if (i > maxRow)
{
maxRow = i;
}
if (j < minCol)
{
minCol = j;
}
if (j > maxCol)
{
maxCol = j;
}
}
}
}
}
/**
* @brief
*
*
*
*
*
* @param brickType
* @return Point
*/
Point GetSpawnPoint(int brickType)
{
int minRow, maxRow, minCol, maxCol;
GetBrickBounds(brickType, 0, minRow, maxRow, minCol, maxCol);
// 只使用初始状态的包围盒计算出生点,保持每种方块生成位置稳定。
int brickWidth = maxCol - minCol + 1;
int brickHeight = maxRow - minRow + 1;
Point spawnPoint;
spawnPoint.x = (nGameWidth - brickWidth) / 2 - minCol;
spawnPoint.y = -brickHeight;
return spawnPoint;
}
/**
* @brief
* @param overflowTop
* @param fixedCells
* @param fixedCellCount
* @param explosiveCells
* @param explosiveCellCount
*/
void CollectAndWriteFixedCells(
bool& overflowTop,
Point fixedCells[],
int& fixedCellCount,
Point explosiveCells[],
int& explosiveCellCount)
{
overflowTop = false;
fixedCellCount = 0;
explosiveCellCount = 0;
for (int i = 0; i < 4; i++)
{
for (int j = 0; j < 4; j++)
{
if (bricks[type][state][i][j] == 0)
{
continue;
}
int fixY = point.y + i;
int fixX = point.x + j;
// 顶部溢出只记录状态,真正的复活或结束逻辑在后续统一处理。
if (fixY < 0)
{
overflowTop = true;
}
if (fixY >= 0 && fixY < GetRoguePlayableHeight() && fixX >= 0 && fixX < nGameWidth)
{
workRegion[fixY][fixX] = currentPieceIsRainbow ? 8 : bricks[type][state][i][j];
if (fixedCellCount < 4)
{
fixedCells[fixedCellCount].x = fixX;
fixedCells[fixedCellCount].y = fixY;
fixedCellCount++;
}
if (currentPieceIsExplosive && explosiveCellCount < 4)
{
explosiveCells[explosiveCellCount].x = fixX;
explosiveCells[explosiveCellCount].y = fixY;
explosiveCellCount++;
}
}
}
}
}
/**
* @brief
* @param overflowTop
* @return true false
*/
bool ResolveFixingOverflow(bool overflowTop)
{
if (!overflowTop)
{
return true;
}
// 终末清场优先级高于普通最后一搏,会消耗一次最后一搏和一枚清屏炸弹。
if (currentMode == MODE_ROGUE && rogueStats.terminalClearLevel > 0 && rogueStats.lastChanceCount > 0 && rogueStats.screenBombCount > 0)
{
rogueStats.lastChanceCount--;
rogueStats.screenBombCount--;
int clearedByTerminal = TriggerScreenBomb();
rogueStats.feverTicks = 10;
currentFallInterval = GetRogueFallInterval();
TCHAR terminalDetail[128];
_stprintf_s(
terminalDetail,
_T("终末清场启动,清除 %d 格,并进入 10 秒狂热。"),
clearedByTerminal);
SetFeedbackMessage(_T("终末清场"), terminalDetail, 14);
return true;
}
// 最后一搏只清理底部三行,让顶部溢出的局面获得一次继续机会。
if (currentMode == MODE_ROGUE && rogueStats.lastChanceCount > 0)
{
rogueStats.lastChanceCount--;
for (int i = 0; i < 3; i++)
{
DeleteOneLine(GetRoguePlayableHeight() - 1);
}
SetFeedbackMessage(
_T("最后一搏"),
_T("底部 3 行被清除,战局得以延续。"),
14);
return true;
}
gameOverFlag = true;
return false;
}
/**
* @brief Hold
*/
void SpawnNextFallingPiece()
{
// 消耗预览队列后重置本回合状态,确保 Hold 和特殊标记只影响新方块。
type = ConsumeNextType();
nType = nextTypes[0];
state = 0;
holdUsedThisTurn = false;
RollCurrentPieceSpecialFlags(true);
point = GetSpawnPoint(type);
target = point;
ComputeTarget();
}
/**
* @brief
* @param clearedRows 8
* @param clearedRowCount
* @return
*/
int ScanAndDeleteFullLines(int clearedRows[], int& clearedRowCount)
{
int clearedLines = 0;
clearedRowCount = 0;
// 从底向上扫描,删除后 i++ 让当前位置继续检查新落下来的行。
int playableHeight = GetRoguePlayableHeight();
for (int i = playableHeight - 1; i >= 0; i--)
{
bool fullLine = true;
for (int j = 0; j < nGameWidth; j++)
{
if (workRegion[i][j] == 0)
{
fullLine = false;
break;
}
}
if (fullLine)
{
if (clearedRowCount < 8)
{
clearedRows[clearedRowCount] = i;
clearedRowCount++;
}
DeleteOneLine(i);
clearedLines++;
i++;
}
}
return clearedLines;
}
/**
* @brief
* @param clearedRows
* @param clearedRowCount
* @param clearedLines
*/
void DispatchLineClearEffect(const int clearedRows[], int clearedRowCount, int clearedLines)
{
if (currentScreen == SCREEN_UPGRADE)
{
QueueLineClearEffect(clearedRows, clearedRowCount, clearedLines);
}
else
{
TriggerLineClearEffect(clearedRows, clearedRowCount, clearedLines);
}
}
/**
* @brief 3x3
* @param clearedLines
*/
void ResolveChainBombFollowup(int clearedLines)
{
// 没有标准消行时,连环炸弹追加爆破不触发,并清掉挂起标记。
if (!pendingChainBombFollowup || clearedLines <= 0)
{
pendingChainBombFollowup = false;
return;
}
pendingChainBombFollowup = false;
// 追加爆破以第一次爆破落地点为中心,只执行一次 3x3 清除。
int followupCleared = 0;
int centerY = pendingChainBombCenter.y;
int centerX = pendingChainBombCenter.x;
Point followupCells[9] = {};
for (int y = centerY - 1; y <= centerY + 1; y++)
{
for (int x = centerX - 1; x <= centerX + 1; x++)
{
if (y >= 0 && y < GetRoguePlayableHeight() && x >= 0 && x < nGameWidth && workRegion[y][x] != 0)
{
if (followupCleared < 9)
{
followupCells[followupCleared].x = x;
followupCells[followupCleared].y = y;
}
workRegion[y][x] = 0;
followupCleared++;
}
}
}
if (currentMode == MODE_ROGUE && followupCleared > 0)
{
TriggerCellClearEffect(followupCells, followupCleared < 9 ? followupCleared : 9, true);
int followupScore = 0;
int followupExp = 0;
AwardRogueSkillClearRewards(followupCleared, followupScore, followupExp, false);
TCHAR followupDetail[128];
_stprintf_s(
followupDetail,
_T("追加爆炸清除 %d 格 +%d 分 +%d EXP"),
followupCleared,
followupScore,
followupExp);
SetFeedbackMessage(_T("连环炸弹"), followupDetail, 12);
}
}
+263
View File
@@ -0,0 +1,263 @@
#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)
{
// Rogue 模式下特殊清除也能获得得分和经验,但不直接触发升级菜单。
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();
}
+157
View File
@@ -0,0 +1,157 @@
#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)
{
// 空路径和不存在的文件不交给 GDI+,减少无效加载开销。
if (path.empty() || !FileExists(path))
{
return nullptr;
}
// GDI+ 返回对象后仍需检查状态,失败对象要立即释放。
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
File diff suppressed because it is too large Load Diff
+11 -3
View File
@@ -1,8 +1,16 @@
// stdafx.cpp : 只包括标准包含文件的源文件
// Tetris.pch 将作为预编译头
// stdafx.obj 将包含预编译类型信息
/**
* @file stdafx.cpp
* @brief stdafx.h
*/
#include "stdafx.h"
/**
* @file stdafx.cpp
* @brief stdafx.h
*
* Visual Studio
*/
// TODO: 在 STDAFX.H 中
// 引用任何所需的附加头文件,而不是在此文件中引用