50 Commits

Author SHA1 Message Date
Qi-huanye fd383481fd 尝试适配vs 2026-05-08 18:32:28 +08:00
Qi-huanye a0bae4b6b8 补充vs使用文档 2026-05-08 16:13:48 +08:00
Qi-huanye 70b1665b31 补充开源说明 2026-05-06 17:12:19 +08:00
porcelain 59e491038d 更新TODO 2026-05-05 23:05:46 +08:00
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
43 changed files with 10477 additions and 4347 deletions
+1
View File
@@ -1,5 +1,6 @@
# Build outputs # Build outputs
/.vscode-build/ /.vscode-build/
/.worktrees/
/build/ /build/
/bin/ /bin/
/obj/ /obj/
+41
View File
@@ -0,0 +1,41 @@
# AGENTS.md
## 项目名称
使用大模型辅助开发俄罗斯方块程序
## 基本开发要求
### 编程语言
- 使用 C++。
- 仅使用课程已学基础语法:数组、循环、分支、函数、结构体等。
- 不使用 `class`、继承、多态等面向对象特性。
## 项目结构
- 源码主要位于 `src` 目录。
- 不要随意改动已有全局变量、函数声明和文件结构。
- 如需新增创新功能,可以新增 `.cpp` 文件。
## 构建方式
优先使用项目根目录下的构建脚本:
```powershell
.\build-mingw.ps1
```
## 代码质量要求
1. 每次只实现一个明确功能。
2. 每个函数必须有功能描述注释。
3. 变量命名保持和原框架一致。
4. 不随意改动已有全局变量和函数声明。
5. 生成代码后必须人工审查。
6. 每个阶段完成后必须编译运行。
7. 出现 bug 时,应记录问题、原因和修复过程。
## 开发注意事项
1. 每次补全前后都要保存版本,便于报告展示。
2. 现场汇报时,所有组员都可能被提问,不能只有一人理解代码。
3. 作业要求LLM对话迭代生成代码 虽然我们不是 我们是vibe coding 但是在后续生成报告相关的时候 要伪装的像是LLM对话迭代生成的
-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++ 更适合作为兼容运行方案。
+24
View File
@@ -0,0 +1,24 @@
MIT License
Copyright (c) 2026 Tereis contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of the source code and associated documentation files in this repository,
excluding third-party media assets and generated media assets as described in
NOTICE.md, to deal in the source code without restriction, including without
limitation the rights to use, copy, modify, merge, publish, distribute,
sublicense, and/or sell copies of the source code, and to permit persons to
whom the source code is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the source code.
THE SOURCE CODE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOURCE CODE OR THE USE OR OTHER DEALINGS IN
THE SOURCE CODE.
+20
View File
@@ -0,0 +1,20 @@
# Notice
本仓库是程序设计课程大作业项目,仅用于课程学习、课堂展示和个人技术交流,不用于商业发布。
## 授权范围
- `src/`、构建脚本和项目文档中的原创代码内容按 `LICENSE` 中的 MIT License 授权。
- `assets/``report/images/``report/code-snippets/` 中的图片、音频、视频等非代码素材不包含在 MIT License 授权范围内。
- 如需二次发布、公开分发可执行文件或用于课程以外场景,请先替换或移除未取得独立授权的素材。
## 素材来源
- 音乐素材:来自《千恋*万花》,仅作为课程大作业学习展示使用,版权归原权利方所有。
- 图片素材:由 AI 生成或用于课程报告展示。
- 图标、视频和其他资源:仅随课程项目用于演示程序功能,不代表已获得商业使用授权。
## 使用提醒
如果将项目上传到公开平台,建议在发布说明中保留本文件,并明确说明素材来源和授权限制。若需要更严格地规避素材版权风险,可以只公开源码和文档,删除 `assets/` 下的媒体文件。
+292 -70
View File
@@ -1,68 +1,250 @@
# Tereis # Tereis
基于 C++ 与 Windows API 实现的俄罗斯方块课程项目 Tereis 是一个基于 C++、Win32 API、GDI/GDI+ 实现的桌面版俄罗斯方块课程大作业
项目使用 MinGW 进行构建,当前已完成基础窗口框架、方块逻辑、绘图显示与资源编译接入,适合作为《大学计算》程序设计大作业使用 项目在经典俄罗斯方块玩法上扩展了 Rogue 模式,加入等级成长、强化选择、主动技能、特殊方块、视频复活、鼠标交互和视觉特效。程序不依赖游戏引擎,主要使用 Win32 消息循环和 GDI 绘图完成
## 项目简介 ## 快速运行
项目目标是实现一个可运行的桌面版俄罗斯方块程序,包含以下核心内容: 推荐在 Windows + PowerShell + MinGW-w64 环境下运行。
- 创建 Windows 游戏窗口 1. 确认 `g++.exe``windres.exe` 已加入 `PATH`,或安装在 `C:\mingw64\bin\`
- 实现方块生成、移动、旋转与下落 2. 在项目根目录执行构建并运行:
- 实现碰撞检测、方块固定与游戏结束判定
- 实现消行逻辑与基础分数系统
- 实现界面绘制与部分资源显示
- 提供 MinGW 构建脚本和 VS Code 调试配置
## 目录结构 ```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\build-mingw.ps1 -Run
```
3. 如只需构建,不启动程序:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\build-mingw.ps1
```
构建产物位于:
```text
.vscode-build\mingw\Tetris.exe
```
运行时请从项目根目录启动程序,确保 `assets/` 目录可被读取,否则背景图、音乐和复活视频可能无法加载。
## 功能概览
### 经典模式
- 标准俄罗斯方块规则
- 方块生成、移动、旋转、软降、硬降
- 方块落地固定、消行、计分和死亡判定
- 预测落点显示
- 暂停、重开、返回主菜单
### Rogue 模式
Rogue 模式是本项目的主要扩展玩法。
- 消行获得分数和 EXP
- EXP 满后进入强化选择界面
- 支持普通三选一强化
- 支持双重抉择,同屏选择两个强化
- 支持命运轮盘,同屏展示六个强化并选择两个
- 随时间提升危险等级,底部封锁区会压缩可用空间
- 支持多种强化联动和构筑方向
### 强化与技能
项目中包含多类强化效果:
- 基础成长:得分倍率、EXP 倍率、预览数量、下落速度调整
- 生存强化:最后一搏、时间缓流、稳定结构
- 主动技能:备用仓、清屏炸弹、黑洞奇点、空中换形
- 特殊方块:爆破核心、棱镜激光、十字方块、彩虹方块
- 进阶联动:连锁火花、连环炸弹、雷霆四消、雷霆棱镜
- 风险收益:高压悬赏、豪赌四消、极限玩家、赌徒契约
- 升级联动:双重抉择、命运轮盘、升级冲击波、进化冲击
具体效果可在游戏主菜单的 `帮助 -> 强化图鉴` 中查看。
### 鼠标交互
除键盘操作外,项目也支持鼠标点击:
- 主菜单项目可点击
- 帮助页项目可点击
- 升级卡片可点击选择
- 多选强化可点击标记
- 暂停和结算界面按钮可点击
- 非主菜单界面左上角有返回按钮,可点击回到主菜单
- 右下角音乐按钮可点击开关背景音乐
### 视听与资源
- 自定义图标
- 背景图片
- 背景音乐
- 消行和技能清除特效
- 死亡后可播放本地视频复活一次
## 操作说明
### 通用键盘操作
| 按键 | 功能 |
| --- | --- |
| `← / A` | 左移 |
| `→ / D` | 右移 |
| `↑ / W` | 旋转 |
| `↓ / S` | 软降 |
| `Space` | 硬降 |
| `P` | 暂停 / 继续 |
| `R` | 重开当前对局 |
| `M` | 返回主菜单 |
### Rogue 模式额外按键
| 按键 | 功能 |
| --- | --- |
| `C / Shift` | 备用仓 |
| `Z` | 黑洞奇点 |
| `X` | 清屏炸弹 |
| `V` | 空中换形 |
| 死亡后 `V` | 看视频复活一次 |
### 升级选择
- 普通升级:方向键 / WASD 切换,Enter 或 Space 确认
- 双重抉择 / 命运轮盘:Space 标记,Enter 确认已选强化
- 鼠标操作:直接点击升级卡片即可选择或标记
## 运行说明
### 方式一:PowerShell 一键运行
在项目根目录打开 PowerShell,执行:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\build-mingw.ps1 -Run
```
该命令会先编译项目,编译成功后自动启动游戏窗口。
### 方式二:先构建再运行
先在项目根目录执行构建:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\build-mingw.ps1
```
构建成功后运行生成的程序:
```powershell
.\.vscode-build\mingw\Tetris.exe
```
### 方式三:VS Code 运行和调试
项目已经配置好 VS Code 任务:
-`Ctrl + Shift + B` 执行默认构建任务 `build Tetris MinGW`
- 在任务列表中运行 `run Tetris MinGW` 可构建并启动游戏
- 在“运行和调试”中选择 `Debug Tetris MinGW` 可启动调试
调试需要系统能找到 `gdb.exe`。如果无法调试,请确认 MinGW 的 `bin` 目录已经加入 `PATH`
### 方式四:Visual Studio 中运行
本项目没有提供 Visual Studio 的 `.sln``.vcxproj` 工程文件,推荐在 Visual Studio 中打开项目文件夹,然后通过终端调用已有构建脚本运行。
操作步骤:
1. 打开 Visual Studio。
2. 选择 `文件 -> 打开 -> 文件夹`,打开项目根目录 `Tereis`
3. 打开 Visual Studio 内置终端,或在项目根目录单独打开 PowerShell。
4. 确认 MinGW-w64 已安装,并且 `g++.exe``windres.exe` 可以被系统找到。
5. 在终端中执行:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\build-mingw.ps1 -Run
```
如果只想编译,不立即运行:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\build-mingw.ps1
```
编译成功后,程序位置为:
```text
.vscode-build\mingw\Tetris.exe
```
也可以在 Visual Studio 的终端中运行:
```powershell
.\.vscode-build\mingw\Tetris.exe
```
注意:不要直接把 `src` 目录中的单个 `.cpp` 文件当作独立程序运行。本项目由多个源文件、资源文件和 `assets/` 资源目录共同组成,必须通过项目根目录下的 `build-mingw.ps1` 构建。
### 运行注意事项
- 推荐始终从项目根目录启动程序。
- 不建议直接双击 `.vscode-build\mingw\Tetris.exe`,因为工作目录可能不正确,导致 `assets/` 资源加载失败。
- 如果重新构建时提示 `Tetris.exe: Permission denied`,请先关闭正在运行的游戏窗口。
- 程序使用 Win32 桌面窗口运行,不会显示控制台窗口。
## 项目结构
```text ```text
Tereis/ Tereis/
├─ src/ ├─ src/
│ ├─ include/ 头文件 │ ├─ include/ 头文件
│ ├─ source/ 源文件 │ ├─ source/ 源文件
│ │ ├─ Tetris.cpp 程序入口、窗口和消息框架
│ │ ├─ TetrisLogic.cpp 基础俄罗斯方块逻辑框架
│ │ ├─ TetrisRender.cpp 基础绘制框架
│ │ ├─ common/ 资源路径、文件检查等通用工具
│ │ ├─ app/ 媒体播放、布局命中、输入和定时器处理
│ │ ├─ extensions/ 框架外通用扩展、界面状态和视觉效果
│ │ ├─ logic/ 特殊方块落地效果等逻辑扩展
│ │ ├─ render/ 图片加载等渲染内部支持
│ │ └─ rogue/ Rogue 模式、强化和技能系统
│ └─ resources/ Windows 资源脚本 │ └─ resources/ Windows 资源脚本
├─ assets/ ├─ assets/
│ ├─ icons/ 图标资源 │ ├─ audio/ 背景音乐
│ ├─ images/ 图片资源 │ ├─ icons/ 程序图标
audio/ 音频资源 images/ 背景图片
│ └─ video/ 复活视频
├─ report/ 报告相关材料
├─ .vscode/ VS Code 配置 ├─ .vscode/ VS Code 配置
├─ .vscode-build/ 本地构建输出目录 ├─ .vscode-build/ 本地构建输出目录
├─ report/ 实验报告材料与草稿
├─ build-mingw.ps1 MinGW 构建脚本 ├─ build-mingw.ps1 MinGW 构建脚本
├─ list.md 项目阶段划分 ├─ README.md 项目说明
VSCode运行说明.md VS Code 使用说明 AGENTS.md 项目协作和代码生成约束
└─ README.md 项目说明
``` ```
## 开发阶段划分 ## 构建环境
整个程序按 6 个阶段拆分实现 推荐环境
1. 窗口创建与程序框架搭建
2. 游戏区域与方块数据结构设计
3. 方块生成、移动与旋转功能
4. 碰撞检测与方块固定逻辑
5. 消除逻辑与分数系统
6. 界面完善与创新功能扩展
详细内容见 [list.md](./list.md)。
## 构建与运行
### 环境要求
- Windows - Windows
- MinGW
- `g++.exe`
- `gdb.exe`
- `windres.exe`
- PowerShell - PowerShell
- MinGW-w64
- `g++.exe`
- `windres.exe`
- 如需调试:`gdb.exe`
脚本会优先使用系统 `PATH` 中的工具;如果未加入 `PATH`也兼容 `C:\mingw64\bin\` 下的 MinGW。 构建脚本会优先使用系统 `PATH` 中的 MinGW。如果没有加入 `PATH`脚本也会尝试使用:
### 使用脚本构建 ```text
C:\mingw64\bin\
```
构建脚本会递归收集 `src/source` 下的 `.cpp` 文件。新增功能代码可以放入功能目录,不需要手动维护固定源码列表。
## 构建与运行
在项目根目录执行: 在项目根目录执行:
@@ -70,58 +252,98 @@ Tereis/
powershell -NoProfile -ExecutionPolicy Bypass -File .\build-mingw.ps1 powershell -NoProfile -ExecutionPolicy Bypass -File .\build-mingw.ps1
``` ```
构建完成后会生成: 构建成功后生成:
```text ```text
.vscode-build\mingw\Tetris.exe .vscode-build\mingw\Tetris.exe
``` ```
### 构建直接运行 构建直接运行
```powershell ```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\build-mingw.ps1 -Run powershell -NoProfile -ExecutionPolicy Bypass -File .\build-mingw.ps1 -Run
``` ```
### 使用 VS Code 也可以直接运行已生成的程序:
项目已提供: ```powershell
.\.vscode-build\mingw\Tetris.exe
```
- 构建任务 `build Tetris MinGW` 如果使用 VS Code
- 运行任务 `run Tetris MinGW`
- 调试配置 `Debug Tetris MinGW`
详见 [VSCode运行说明.md](./VSCode运行说明.md)。 - `Ctrl + Shift + B` 执行默认构建任务 `build Tetris MinGW`
- 运行任务 `run Tetris MinGW` 可构建并启动游戏
- 调试配置 `Debug Tetris MinGW` 会先构建,再使用 `gdb.exe` 启动调试
## 资源文件说明 注意:直接双击 `.vscode-build\mingw\Tetris.exe` 时,当前工作目录可能不是项目根目录,资源文件可能无法正常读取。推荐从项目根目录通过脚本或 VS Code 任务启动。
项目包含 Windows 资源文件 `src/resources/Tetris.rc`,其中定义了图标、菜单、快捷键和关于框等内容。 ## 常见问题
由于原始 `Tetris.rc` 为 UTF-16 编码,当前构建脚本会在编译时临时转换资源文件编码,并将 `assets/icons/` 中的图标文件一起编译进最终程序,因此资源不再被跳过。 ### 1. 提示 `Tetris.exe: Permission denied`
## 报告目录 说明游戏程序仍在运行,链接器无法覆盖旧文件。
实验报告相关材料已整理到 [report/](./report/) 处理方式
- `report.md`:报告正文草稿 - 关闭正在运行的游戏窗口
- `outline.md`:章节提纲 - 重新执行构建命令
- `notes.md`:待补充内容
- `images/`:截图和流程图
- `code-snippets/`:报告中准备引用的代码
- `submission/`:最终提交版文档
## 当前状态 ### 2. 没有背景图、音乐或视频
当前项目已完成的工作: 请确认运行时保留了 `assets/` 目录。项目会从资源目录读取背景、音乐和复活视频。
- 修复项目迁移后的路径配置问题 ### 3. 视频复活播放失败
- 补充 `.gitignore`
- 接入资源文件编译流程
- 整理项目阶段清单
- 建立实验报告目录结构
后续可以继续完善的方向 项目会优先查找
- 优化界面表现 - `assets/video/video.avi`
- 完善分数与状态提示 - `assets/video/video.mp4`
- 增加创新功能
- 补充测试截图和实验分析 如果系统不支持对应格式,可能会播放失败。建议保留项目中已提供的视频文件。
### 4. 鼠标点击不生效
请确认运行的是最新构建结果。若构建时 `Tetris.exe` 被占用,实际运行的可能仍是旧版本。
## 课程展示建议
建议按以下顺序展示:
1. 主菜单、帮助页和鼠标点击
2. 经典模式基础玩法
3. Rogue 模式升级选择
4. 双重抉择或命运轮盘的多选界面
5. 主动技能:黑洞、炸弹、换形、备用仓
6. 特殊方块和消除特效
7. 死亡后视频复活
## 实现说明
本项目以过程式 C++ 写法为主,核心逻辑分布如下:
- `src/source/Tetris.cpp`:Win32 程序入口、窗口创建和消息分发主干
- `src/source/TetrisLogic.cpp`:基础方块逻辑、消行和状态重置
- `src/source/TetrisRender.cpp`:界面绘制、面板、动画和特效
- `src/source/common/TetrisAssets.cpp`:资源路径拼接和文件存在判断
- `src/source/app/`:背景音乐、复活视频、窗口布局命中、鼠标键盘和定时器处理
- `src/source/logic/TetrisPieceEffects.cpp`:彩虹、爆破、激光、十字和稳定结构等落地效果
- `src/source/extensions/TetrisGameExtensions.cpp`:框架外通用状态切换、复活、说明页、视觉效果等扩展支持
- `src/source/render/TetrisRenderAssets.cpp`:背景图、致谢页图片等 GDI+ 图片资源加载
- `src/source/rogue/TetrisRogue.cpp`:Rogue 模式、强化、技能和成长系统
- `src/include/Tetris.h`:主要结构体、全局状态和函数声明
- `src/include/TetrisAppInternal.h``src/include/TetrisRenderInternal.h``src/include/TetrisAssets.h`:窗口层、渲染层和资源工具的内部声明
项目适合作为程序设计课程大作业展示,也便于在答辩时讲解窗口程序、游戏循环、碰撞检测、状态管理和功能扩展。
## 开源协议与素材说明
本项目为程序设计课程大作业,仅供课程学习、课堂展示和个人技术交流使用,不用于商业发布。
- 源代码、构建脚本和原创文档内容采用 MIT License,详见 `LICENSE`
- `assets/``report/images/``report/code-snippets/` 中的音频、图片、视频等非代码素材不包含在 MIT License 授权范围内。
- 音乐素材来自《千恋*万花》,仅作为课程大作业学习展示使用,版权归原权利方所有。
- 图片素材主要由 AI 生成或用于课程报告展示。
- 如需二次发布、公开分发可执行文件或用于课程以外场景,请先替换或移除未取得独立授权的素材。
更完整的素材来源和授权限制说明见 `NOTICE.md`
+354
View File
@@ -0,0 +1,354 @@
# Tereis 实验报告与项目整理 TODO
> 依据:实验报告模板 `大学计算-程序设计大作业-实验报告模板.docx`、课堂报告要求截图、当前 `src` 源码目录。
> 说明:`report/` 文件夹按废弃资料处理,不作为本 TODO 的依据。
## 0. 当前项目审查结论
- [ ] 确认最终报告只引用 `src/``assets/``README.md`、构建脚本和重新整理的截图材料。
- [ ] 清点当前源码模块:
- `src/source/Tetris.cpp`:Win32 程序入口、窗口注册、消息循环、主窗口消息处理。
- `src/source/TetrisLogic.cpp`:基础俄罗斯方块移动、旋转、落地、消行、重开等核心逻辑。
- `src/source/logic/`:生成下一方块、固定方块、特殊落地效果、棋盘辅助逻辑。
- `src/source/app/`:定时器、键盘鼠标输入、窗口布局、背景音乐和复活视频。
- `src/source/render/``TetrisRender.cpp`:界面绘制、背景图片、GDI/GDI+ 资源加载。
- `src/source/extensions/`:菜单、反馈提示、视觉特效、复活、页面切换等扩展状态。
- `src/source/rogue/`:Rogue 模式、升级选项、主动技能、特殊方块、难度成长。
- [ ] 记录项目规模:多源文件 C++ Win32 桌面程序,主要采用全局变量、结构体、函数的过程式组织。
- [ ] 检查课程限制风险:
- 当前代码没有自定义 `class`、继承、多态。
- 但存在 `std::wstring``std::vector` 未发现、`auto` lambda、GDI+ `Image` 对象、`new/delete``constexpr`、C++17 构建参数等超出“仅基础语法”的风险点。
- 报告中需要说明:核心游戏逻辑坚持数组、循环、分支、函数、结构体;Win32/GDI+ 属于界面和资源接口调用。
## 1. 阶段一:窗口创建与程序框架
- [ ] 功能设计文档:说明为什么先搭建窗口、消息循环和菜单状态。
- [ ] 关键代码整理:
- `_tWinMain``src/source/Tetris.cpp`
- `MyRegisterClass``src/source/Tetris.cpp`
- `InitInstance``src/source/Tetris.cpp`
- `WndProc``src/source/Tetris.cpp`
- `About``src/source/Tetris.cpp`
- [ ] 代码说明重点:
- Win32 程序入口如何创建主窗口。
- 消息循环如何把键盘、鼠标、定时器、绘制消息分发给游戏。
- 为什么用全局状态变量保存当前界面和游戏状态。
- [ ] 截图补充:
- 程序启动主菜单。
- 帮助/说明页面。
- [ ] 编译运行记录:
- 执行 `.\build-mingw.ps1`
- 记录是否成功生成 `.vscode-build\mingw\Tetris.exe`
- [ ] AI 对话记录整理:
- 提示词主题:搭建 Win32 窗口框架。
- 人工审查点:入口函数、窗口大小、消息处理是否能正常运行。
## 2. 阶段二:基础方块移动与碰撞检测
- [ ] 功能设计文档:说明棋盘数组、活动方块坐标、边界判断和碰撞判断。
- [ ] 关键代码整理:
- `CanMoveDown``src/source/TetrisLogic.cpp`
- `CanMoveLeft``src/source/TetrisLogic.cpp`
- `CanMoveRight``src/source/TetrisLogic.cpp`
- `MoveDown``src/source/TetrisLogic.cpp`
- `MoveLeft``src/source/TetrisLogic.cpp`
- `MoveRight``src/source/TetrisLogic.cpp`
- `Rotate``src/source/TetrisLogic.cpp`
- `DropDown``src/source/TetrisLogic.cpp`
- [ ] 代码说明重点:
- `workRegion[20][10]` 如何表示固定方块。
- `bricks[7][4][4][4]` 如何表示 7 类方块和旋转状态。
- 移动前先检测,检测通过再修改坐标。
- 旋转失败时保持原状态,避免方块穿墙或重叠。
- [ ] 截图补充:
- 方块左移、右移、旋转、硬降后的游戏画面。
- [ ] 测试记录:
- 左右边界不能越界。
- 方块落到已有方块上方时停止。
- 旋转时不能覆盖已有方块。
- [ ] AI 对话记录整理:
- 提示词主题:补全移动和碰撞检测函数。
- 人工审查点:数组下标是否越界、边界条件是否完整。
## 3. 阶段三:方块固定、消行、得分和游戏状态
- [ ] 功能设计文档:说明方块落地后的固定流程、消行流程、分数变化和结束判断。
- [ ] 关键代码整理:
- `Fixing``src/source/TetrisLogic.cpp`
- `DeleteOneLine``src/source/TetrisLogic.cpp`
- `DeleteLines``src/source/TetrisLogic.cpp`
- `GameOver``src/source/TetrisLogic.cpp`
- `ComputeTarget``src/source/TetrisLogic.cpp`
- `Restart``src/source/TetrisLogic.cpp`
- `SpawnNextFallingPiece``src/source/logic/TetrisCoreHelpers.cpp`
- `ScanAndDeleteFullLines``src/source/logic/TetrisCoreHelpers.cpp`
- `ApplyLineClearResult``src/source/rogue/TetrisRogue.cpp`
- [ ] 代码说明重点:
- 活动方块如何写入棋盘数组。
- 满行检测从下到上扫描的原因。
- 消行后上方方块整体下移。
- `ComputeTarget` 如何得到预览落点。
- `Restart` 如何重置棋盘、分数、方块状态和视觉状态。
- [ ] 截图补充:
- 消除一行或多行。
- 分数变化。
- 游戏结束或重新开始。
- [ ] 测试记录:
- 单行消除。
- 多行消除。
- 顶部堆满后的游戏结束。
- 重新开始后棋盘清空。
- [ ] Bug 记录模板:
- 问题:消行后上方方块没有正确下落。
- 原因:删除行后未正确复制上一行数据。
- 修复:从被删行开始向上逐行覆盖,并清空第一行。
## 4. 阶段四:界面绘制、资源加载与交互
- [ ] 功能设计文档:说明游戏区、侧边栏、菜单、帮助页、按钮和背景资源。
- [ ] 关键代码整理:
- `TDrawScreen``src/source/TetrisRender.cpp`
- `RenderFullScreen``src/source/render/TetrisRenderMain.cpp`
- `LoadBackgroundImage``src/source/render/TetrisRenderAssets.cpp`
- `FileExists``src/source/common/TetrisAssets.cpp`
- `GetMenuOptionRect``src/source/app/TetrisLayout.cpp`
- `GetUpgradeCardRect``src/source/app/TetrisLayout.cpp`
- `HandleMouseClick``src/source/app/TetrisInput.cpp`
- `HandleMouseWheel``src/source/app/TetrisInput.cpp`
- `HandleKeyDown``src/source/app/TetrisInput.cpp`
- `StartBackgroundMusic``src/source/app/TetrisMedia.cpp`
- `ToggleBackgroundMusic``src/source/app/TetrisMedia.cpp`
- `PlayReviveVideo``src/source/app/TetrisMedia.cpp`
- [ ] 代码说明重点:
- 界面绘制与游戏逻辑分离。
- 鼠标点击通过矩形区域判断菜单和按钮。
- 键盘输入对应移动、旋转、暂停、重开、技能。
- 背景图、图标、音乐、视频统一放在 `assets/`
- [ ] 截图补充:
- 主菜单。
- 经典模式游戏界面。
- 帮助页。
- 音乐按钮或返回按钮。
- [ ] 测试记录:
- 键盘控制有效。
- 鼠标点击菜单有效。
- 背景音乐开关有效。
- 从根目录运行时资源能正常加载。
- [ ] 风险处理:
- 报告中不要把 GDI+ 对象作为课程核心语法重点,重点讲过程式游戏逻辑和数组状态。
## 5. 阶段五:Rogue 创新模式与强化系统
- [ ] 功能设计文档:说明创新点来源、玩法目标和与经典模式的区别。
- [ ] 关键代码整理:
- `StartGameWithMode``src/source/extensions/TetrisGameExtensions.cpp`
- `ResetPlayerStats``src/source/extensions/TetrisGameExtensions.cpp`
- `OpenUpgradeMenu``src/source/rogue/TetrisRogue.cpp`
- `ConfirmUpgradeSelection``src/source/rogue/TetrisRogue.cpp`
- `CheckRogueLevelProgress``src/source/rogue/TetrisRogue.cpp`
- `AwardRogueSkillClearRewards``src/source/rogue/TetrisRogue.cpp`
- `AdvanceRogueDifficulty``src/source/rogue/TetrisRogue.cpp`
- `GetRogueFallInterval``src/source/rogue/TetrisRogue.cpp`
- `GetRogueLockedRows``src/source/rogue/TetrisRogue.cpp`
- `GetUpgradeSynthesisPath``src/source/rogue/TetrisRogue.cpp`
- [ ] 代码说明重点:
- Rogue 模式如何用 `PlayerStats` 结构体保存等级、经验、强化、技能次数。
- 消行如何获得经验并触发升级选择。
- 强化选项如何随机生成、选择并影响后续游戏。
- 难度如何随时间推进。
- [ ] 截图补充:
- Rogue 模式游戏界面。
- 升级三选一。
- 双重选择或命运轮盘。
- 难度提升/底部封锁效果。
- [ ] 测试记录:
- 消行获得 EXP。
- EXP 满后进入升级界面。
- 选择强化后返回游戏。
- 难度等级会随时间变化。
- [ ] AI 对话记录整理:
- 提示词主题:设计俄罗斯方块 Rogue 强化系统。
- 人工审查点:强化是否真的改变游戏状态,升级界面是否能返回主流程。
## 6. 阶段六:主动技能、特殊方块和视觉特效
- [ ] 功能设计文档:说明主动技能和特殊方块是创新功能,不影响基础玩法可运行。
- [ ] 关键代码整理:
- `HoldCurrentPiece``src/source/rogue/TetrisRogue.cpp`
- `UseScreenBomb``src/source/rogue/TetrisRogue.cpp`
- `UseBlackHole``src/source/rogue/TetrisRogue.cpp`
- `UseAirReshape``src/source/rogue/TetrisRogue.cpp`
- `RollCurrentPieceSpecialFlags``src/source/rogue/TetrisRogue.cpp`
- `ApplySpecialLandingEffects``src/source/logic/TetrisPieceEffects.cpp`
- `ApplyRainbowLandingEffect``src/source/logic/TetrisPieceEffects.cpp`
- `TriggerScreenBomb``src/source/rogue/TetrisRogue.cpp`
- `TriggerMiniBlackHole``src/source/rogue/TetrisRogue.cpp`
- `ClearExplosiveAreaAt``src/source/rogue/TetrisRogue.cpp`
- `ClearColumnAt``src/source/rogue/TetrisRogue.cpp`
- `ClearRowAt``src/source/rogue/TetrisRogue.cpp`
- `TriggerLineClearEffect``src/source/extensions/TetrisGameExtensions.cpp`
- `TickVisualEffects``src/source/extensions/TetrisGameExtensions.cpp`
- [ ] 代码说明重点:
- 技能按键如何触发对应函数。
- 技能如何修改棋盘数组。
- 特殊方块落地后如何触发清除、变色、爆炸、激光等效果。
- 视觉特效只负责显示,不应破坏核心棋盘数据。
- [ ] 截图补充:
- 备用仓。
- 清屏炸弹。
- 黑洞奇点。
- 空中换形。
- 爆破/激光/彩虹等特殊方块效果。
- [ ] 测试记录:
- 技能次数不足时不能使用。
- 技能使用后棋盘变化正确。
- 特殊方块效果不会造成数组越界。
- 特效结束后游戏仍可继续。
## 7. 实验报告正文 TODO
- [ ] 封面信息:
- 项目名称:使用大模型辅助开发俄罗斯方块程序。
- 小组成员、学号、班级、日期。
- [ ] 摘要:
- 简述完成了经典俄罗斯方块和 Rogue 创新模式。
- 强调使用 C++、Win32 API、数组、结构体、函数组织。
- [ ] 需求功能设计:
- 按至少 6 个阶段写,每阶段包含目标、功能点、涉及文件。
- 每阶段最多聚焦一个主要功能主题。
- [ ] 功能实现:
- 每阶段放关键代码截图。
- 每阶段写代码说明。
- 每阶段放游戏运行截图。
- [ ] AI 辅助编程体验反思:
- 写明大模型做得好的地方:快速生成框架、补全重复逻辑、解释 Win32 消息流程、提供调试思路。
- 写明大模型表现不好的地方:容易生成过复杂代码、可能使用超出课程范围的语法、边界条件不完整、变量命名可能不符合原框架。
- 写明改进方法:拆小任务、明确限制语法、每次只让模型生成一个函数、人工检查数组下标、编译运行验证。
- 注意表述成“多轮 LLM 对话迭代生成”,不要写成一次性 vibe coding。
- [ ] 成员分工表:
- 提示词工程师:拆分需求、编写和迭代提示词。
- 代码审计员:检查语法限制、数组越界、全局状态和函数注释。
- 功能测试员:运行游戏、记录 bug、截图。
- 报告撰稿人:整理阶段文档、代码截图、反思和分工。
- 现场汇报人:演示程序并回答问题。
- [ ] Bug 记录:
- 至少整理 3 个 bug,每个包含“问题、原因、修复过程、验证结果”。
- [ ] 总结:
- 说明最终实现的功能。
- 说明仍可改进的地方,例如代码规模较大、部分界面资源依赖本地文件、复杂扩展功能需要更多测试。
## 8. 答辩准备 TODO
- [ ] 每位组员至少熟悉一个源码模块,不能只由一人理解。
- [ ] 准备 5 分钟演示路线:
- 主菜单。
- 经典模式移动、旋转、消行。
- Rogue 模式升级。
- 主动技能。
- 特殊方块或视频复活。
- [ ] 准备常见问题回答:
- 方块形状如何存储?
- 如何判断碰撞?
- 如何消行?
- 如何实现升级选择?
- 如何保证没有使用自定义 class?
- AI 生成代码后做了哪些人工审查?
- [ ] 准备现场编译:
- 命令:`.\build-mingw.ps1`
- 运行:`.\build-mingw.ps1 -Run`
- 如果提示 `Tetris.exe: Permission denied`,先关闭正在运行的游戏窗口。
## 9. 四人专项分工规划
> 原则:四个人各有一个主要专项,同时都要理解自己负责模块对应的代码和报告内容;现场答辩时不能只由一个人解释全部代码。
### 成员 A:需求拆分与报告主线负责人
- [ ] 专项任务:负责实验报告整体结构、阶段划分和文字主线。
- [ ] 负责内容:
- 将项目整理成 6 个阶段:窗口框架、基础移动、消行得分、界面交互、Rogue 强化、主动技能与特效。
- 编写每个阶段的“需求功能设计”。
- 整理摘要、项目背景、总体架构、总结与不足。
- 保证报告符合截图要求:不少于五个阶段、每阶段有功能设计文档。
- [ ] 重点熟悉代码:
- `src/source/Tetris.cpp`
- `src/include/Tetris.h`
- `src/source/TetrisLogic.cpp`
- [ ] 最终交付:
- 报告目录结构。
- 6 个阶段的功能设计文字。
- 项目总体介绍和总结。
### 成员 B:核心逻辑与代码说明负责人
- [ ] 专项任务:负责基础俄罗斯方块核心逻辑的代码审查和代码说明。
- [ ] 负责内容:
- 解释棋盘数组 `workRegion[20][10]`
- 解释方块数组 `bricks[7][4][4][4]`
- 整理移动、旋转、碰撞、固定、消行、得分、游戏结束的关键代码。
- 检查数组下标、边界判断、函数注释是否适合放进报告。
- [ ] 重点熟悉代码:
- `src/source/TetrisLogic.cpp`
- `src/source/logic/TetrisCoreHelpers.cpp`
- `src/source/logic/TetrisPieceEffects.cpp`
- [ ] 最终交付:
- 阶段二、阶段三的关键代码截图清单。
- 每段关键代码的说明文字。
- 至少 1 个核心逻辑 bug 的“问题、原因、修复、验证”记录。
### 成员 C:界面交互、资源与运行截图负责人
- [ ] 专项任务:负责程序运行、界面截图、资源加载和交互测试。
- [ ] 负责内容:
- 编译并运行项目,记录构建结果。
- 截取主菜单、经典模式、帮助页、Rogue 升级、主动技能、特殊方块等运行截图。
- 测试键盘输入、鼠标点击、音乐开关、返回按钮、视频复活。
- 整理运行环境和现场演示路线。
- [ ] 重点熟悉代码:
- `src/source/render/TetrisRenderMain.cpp`
- `src/source/render/TetrisRenderAssets.cpp`
- `src/source/app/TetrisInput.cpp`
- `src/source/app/TetrisLayout.cpp`
- `src/source/app/TetrisMedia.cpp`
- `src/source/app/TetrisTimers.cpp`
- [ ] 最终交付:
- 每个阶段至少 1 张游戏界面截图。
- 构建运行记录。
- 现场 5 分钟演示路线。
- 至少 1 个界面或资源加载 bug 记录。
### 成员 D:AI 对话、创新功能与答辩问答负责人
- [ ] 专项任务:负责 AI 辅助编程过程整理、Rogue 创新功能说明和答辩材料。
- [ ] 负责内容:
- 整理“提示词 -> 模型生成 -> 人工审查 -> 编译测试 -> 修复”的多轮迭代过程。
- 编写 AI 辅助编程体验反思,突出优点、不足和改进方法。
- 整理 Rogue 模式、升级系统、主动技能、特殊方块、视觉特效的创新点。
- 准备答辩常见问题回答。
- [ ] 重点熟悉代码:
- `src/source/rogue/TetrisRogue.cpp`
- `src/source/extensions/TetrisGameExtensions.cpp`
- `src/source/logic/TetrisPieceEffects.cpp`
- [ ] 最终交付:
- AI 对话迭代记录。
- AI 编程体验反思。
- Rogue 创新功能说明。
- 答辩问答表。
- 至少 1 个 AI 生成代码问题或边界条件 bug 记录。
### 协作检查点
- [ ] 第一次合并:成员 A 完成报告框架后,成员 B/C/D 将各自材料填入对应阶段。
- [ ] 第二次合并:成员 B 审查所有关键代码说明,确认不夸大、不漏掉核心逻辑。
- [ ] 第三次合并:成员 C 核对每个阶段是否都有运行截图和测试记录。
- [ ] 第四次合并:成员 D 检查报告中 AI 过程是否像“多轮 LLM 对话迭代生成”,避免写成一次性生成。
- [ ] 最终检查:四人各自用 2 分钟讲清自己负责模块,互相提问一次。
## 10. 下一步执行顺序
1. [ ] 重新编译项目,确认当前源码可运行。
2. [ ] 按 6 个阶段重新截取游戏界面截图,保存到新的报告素材目录,避免使用废弃 `report/`
3. [ ] 为每个阶段截取 2-4 张关键代码截图。
4. [ ] 根据本 TODO 填写实验报告模板。
5. [ ] 补充 AI 对话过程记录,包装为“需求拆分 -> 模型生成 -> 人工审查 -> 编译测试 -> 修复”的迭代过程。
6. [ ] 最终通读报告,检查是否符合“至少五个阶段、每阶段有设计文档、代码说明、游戏截图、AI 反思、成员分工”的要求。
+30
View File
@@ -0,0 +1,30 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18
VisualStudioVersion = 18.0.0.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Tetris", "Tetris.vcxproj", "{A6B8E95B-7C95-46C2-A3E2-48F342D1F20B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Win32 = Debug|Win32
Debug|x64 = Debug|x64
Release|Win32 = Release|Win32
Release|x64 = Release|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A6B8E95B-7C95-46C2-A3E2-48F342D1F20B}.Debug|Win32.ActiveCfg = Debug|Win32
{A6B8E95B-7C95-46C2-A3E2-48F342D1F20B}.Debug|Win32.Build.0 = Debug|Win32
{A6B8E95B-7C95-46C2-A3E2-48F342D1F20B}.Debug|x64.ActiveCfg = Debug|x64
{A6B8E95B-7C95-46C2-A3E2-48F342D1F20B}.Debug|x64.Build.0 = Debug|x64
{A6B8E95B-7C95-46C2-A3E2-48F342D1F20B}.Release|Win32.ActiveCfg = Release|Win32
{A6B8E95B-7C95-46C2-A3E2-48F342D1F20B}.Release|Win32.Build.0 = Release|Win32
{A6B8E95B-7C95-46C2-A3E2-48F342D1F20B}.Release|x64.ActiveCfg = Release|x64
{A6B8E95B-7C95-46C2-A3E2-48F342D1F20B}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {2A6B54D6-B945-4445-8A94-9B38E625493E}
EndGlobalSection
EndGlobal
+204
View File
@@ -0,0 +1,204 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|Win32">
<Configuration>Release</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Release|x64">
<Configuration>Release</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<VCProjectVersion>18.0</VCProjectVersion>
<Keyword>Win32Proj</Keyword>
<ProjectGuid>{A6B8E95B-7C95-46C2-A3E2-48F342D1F20B}</ProjectGuid>
<RootNamespace>Tetris</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="Configuration" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v145</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Label="Configuration" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<PlatformToolset>v145</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Label="Configuration" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v145</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<PropertyGroup Label="Configuration" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ConfigurationType>Application</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<PlatformToolset>v145</PlatformToolset>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>Unicode</CharacterSet>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings" />
<ImportGroup Label="Shared" />
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<OutDir>$(ProjectDir).vscode-build\vs2026\$(Platform)\$(Configuration)\</OutDir>
<IntDir>$(ProjectDir).vscode-build\vs2026\obj\$(Platform)\$(Configuration)\</IntDir>
<TargetName>Tetris</TargetName>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>UNICODE;_UNICODE;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp17</LanguageStandard>
<AdditionalIncludeDirectories>$(ProjectDir)src\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<AdditionalOptions>/utf-8 %(AdditionalOptions)</AdditionalOptions>
<Optimization>Disabled</Optimization>
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>winmm.lib;gdiplus.lib;gdi32.lib;user32.lib;shell32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
<ResourceCompile>
<AdditionalIncludeDirectories>$(ProjectDir)src\include;$(ProjectDir)assets\icons;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>UNICODE;_UNICODE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ResourceCompile>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>UNICODE;_UNICODE;_WINDOWS;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp17</LanguageStandard>
<AdditionalIncludeDirectories>$(ProjectDir)src\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<AdditionalOptions>/utf-8 %(AdditionalOptions)</AdditionalOptions>
<Optimization>Disabled</Optimization>
<DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>winmm.lib;gdiplus.lib;gdi32.lib;user32.lib;shell32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
<ResourceCompile>
<AdditionalIncludeDirectories>$(ProjectDir)src\include;$(ProjectDir)assets\icons;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>UNICODE;_UNICODE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ResourceCompile>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>UNICODE;_UNICODE;_WINDOWS;NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp17</LanguageStandard>
<AdditionalIncludeDirectories>$(ProjectDir)src\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<AdditionalOptions>/utf-8 %(AdditionalOptions)</AdditionalOptions>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>winmm.lib;gdiplus.lib;gdi32.lib;user32.lib;shell32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
<ResourceCompile>
<AdditionalIncludeDirectories>$(ProjectDir)src\include;$(ProjectDir)assets\icons;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>UNICODE;_UNICODE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ResourceCompile>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<SDLCheck>true</SDLCheck>
<PreprocessorDefinitions>UNICODE;_UNICODE;_WINDOWS;NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<ConformanceMode>true</ConformanceMode>
<LanguageStandard>stdcpp17</LanguageStandard>
<AdditionalIncludeDirectories>$(ProjectDir)src\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PrecompiledHeader>NotUsing</PrecompiledHeader>
<AdditionalOptions>/utf-8 %(AdditionalOptions)</AdditionalOptions>
</ClCompile>
<Link>
<SubSystem>Windows</SubSystem>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>winmm.lib;gdiplus.lib;gdi32.lib;user32.lib;shell32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
<ResourceCompile>
<AdditionalIncludeDirectories>$(ProjectDir)src\include;$(ProjectDir)assets\icons;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PreprocessorDefinitions>UNICODE;_UNICODE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ResourceCompile>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="src\source\Tetris.cpp" />
<ClCompile Include="src\source\TetrisLogic.cpp" />
<ClCompile Include="src\source\TetrisRender.cpp" />
<ClCompile Include="src\source\stdafx.cpp" />
<ClCompile Include="src\source\app\TetrisInput.cpp" />
<ClCompile Include="src\source\app\TetrisLayout.cpp" />
<ClCompile Include="src\source\app\TetrisMedia.cpp" />
<ClCompile Include="src\source\app\TetrisTimers.cpp" />
<ClCompile Include="src\source\common\TetrisAssets.cpp" />
<ClCompile Include="src\source\extensions\TetrisGameExtensions.cpp" />
<ClCompile Include="src\source\logic\TetrisCoreHelpers.cpp" />
<ClCompile Include="src\source\logic\TetrisPieceEffects.cpp" />
<ClCompile Include="src\source\render\TetrisRenderAssets.cpp" />
<ClCompile Include="src\source\render\TetrisRenderMain.cpp" />
<ClCompile Include="src\source\rogue\TetrisRogue.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="src\include\resource.h" />
<ClInclude Include="src\include\stdafx.h" />
<ClInclude Include="src\include\targetver.h" />
<ClInclude Include="src\include\Tetris.h" />
<ClInclude Include="src\include\TetrisAppInternal.h" />
<ClInclude Include="src\include\TetrisAssets.h" />
<ClInclude Include="src\include\TetrisLogicInternal.h" />
<ClInclude Include="src\include\TetrisRenderInternal.h" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="src\resources\Tetris.rc" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets" />
</Project>
+65
View File
@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{0E9F8A8A-4B33-47F4-8409-BBD2E632BD02}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx</Extensions>
</Filter>
<Filter Include="Source Files\app">
<UniqueIdentifier>{4040C716-0A25-434E-8225-3FB91E96C9C2}</UniqueIdentifier>
</Filter>
<Filter Include="Source Files\common">
<UniqueIdentifier>{A0E8AA27-81E0-4B07-8436-84237CBFC4A8}</UniqueIdentifier>
</Filter>
<Filter Include="Source Files\extensions">
<UniqueIdentifier>{D9E2B29D-32A7-4A92-9824-64B07CE76CEF}</UniqueIdentifier>
</Filter>
<Filter Include="Source Files\logic">
<UniqueIdentifier>{86ED6590-B71E-4555-A5ED-131EAB571D32}</UniqueIdentifier>
</Filter>
<Filter Include="Source Files\render">
<UniqueIdentifier>{E19CF9D7-1762-45A0-AE35-9806E551112D}</UniqueIdentifier>
</Filter>
<Filter Include="Source Files\rogue">
<UniqueIdentifier>{28A22C80-54CC-44AF-9925-926D1EE5BAE9}</UniqueIdentifier>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{F7961514-73CF-48BD-A777-525FB4964E26}</UniqueIdentifier>
<Extensions>h;hpp;hxx</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{2BF6F47B-88D9-47B6-971A-54309126F736}</UniqueIdentifier>
<Extensions>rc;ico;bmp;png;jpg;jpeg</Extensions>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="src\source\Tetris.cpp"><Filter>Source Files</Filter></ClCompile>
<ClCompile Include="src\source\TetrisLogic.cpp"><Filter>Source Files</Filter></ClCompile>
<ClCompile Include="src\source\TetrisRender.cpp"><Filter>Source Files</Filter></ClCompile>
<ClCompile Include="src\source\stdafx.cpp"><Filter>Source Files</Filter></ClCompile>
<ClCompile Include="src\source\app\TetrisInput.cpp"><Filter>Source Files\app</Filter></ClCompile>
<ClCompile Include="src\source\app\TetrisLayout.cpp"><Filter>Source Files\app</Filter></ClCompile>
<ClCompile Include="src\source\app\TetrisMedia.cpp"><Filter>Source Files\app</Filter></ClCompile>
<ClCompile Include="src\source\app\TetrisTimers.cpp"><Filter>Source Files\app</Filter></ClCompile>
<ClCompile Include="src\source\common\TetrisAssets.cpp"><Filter>Source Files\common</Filter></ClCompile>
<ClCompile Include="src\source\extensions\TetrisGameExtensions.cpp"><Filter>Source Files\extensions</Filter></ClCompile>
<ClCompile Include="src\source\logic\TetrisCoreHelpers.cpp"><Filter>Source Files\logic</Filter></ClCompile>
<ClCompile Include="src\source\logic\TetrisPieceEffects.cpp"><Filter>Source Files\logic</Filter></ClCompile>
<ClCompile Include="src\source\render\TetrisRenderAssets.cpp"><Filter>Source Files\render</Filter></ClCompile>
<ClCompile Include="src\source\render\TetrisRenderMain.cpp"><Filter>Source Files\render</Filter></ClCompile>
<ClCompile Include="src\source\rogue\TetrisRogue.cpp"><Filter>Source Files\rogue</Filter></ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="src\include\resource.h"><Filter>Header Files</Filter></ClInclude>
<ClInclude Include="src\include\stdafx.h"><Filter>Header Files</Filter></ClInclude>
<ClInclude Include="src\include\targetver.h"><Filter>Header Files</Filter></ClInclude>
<ClInclude Include="src\include\Tetris.h"><Filter>Header Files</Filter></ClInclude>
<ClInclude Include="src\include\TetrisAppInternal.h"><Filter>Header Files</Filter></ClInclude>
<ClInclude Include="src\include\TetrisAssets.h"><Filter>Header Files</Filter></ClInclude>
<ClInclude Include="src\include\TetrisLogicInternal.h"><Filter>Header Files</Filter></ClInclude>
<ClInclude Include="src\include\TetrisRenderInternal.h"><Filter>Header Files</Filter></ClInclude>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="src\resources\Tetris.rc"><Filter>Resource Files</Filter></ResourceCompile>
</ItemGroup>
</Project>
+197
View File
@@ -0,0 +1,197 @@
# 只使用 Visual Studio 2026 运行本项目
本文说明如何只依赖 Visual Studio 2026 自带的 C++ 工具链运行本项目,不额外安装 MinGW、GCC 或其他第三方编译器。
本文档编写日期为 2026-05-08。Microsoft Learn 的发布历史显示,Visual Studio 2026 在 2026-04-28 的稳定通道版本为 18.5.2。
## 1. 结论
可以只用 Visual Studio 2026。
需要安装 Visual Studio 2026 的 `Desktop development with C++` 工作负载。该工作负载会提供本项目需要的主要工具:
- `cl.exe`Microsoft C/C++ 编译器
- `link.exe`Microsoft 链接器
- `rc.exe`Windows 资源编译器
- Windows SDK:提供 Win32 API、GDI、GDI+ 等头文件和库
本项目当前没有 `.sln``.vcxproj` 工程文件,因此推荐在 Visual Studio 2026 中打开文件夹,然后在 `Developer PowerShell for VS 2026` 中执行构建命令。
## 2. 安装 Visual Studio 2026
1. 打开 Visual Studio Installer。
2. 安装 Visual Studio 2026 Community、Professional 或 Enterprise 均可。
3. 在工作负载页面选择 `Desktop development with C++`
4. 保留默认勾选的 MSVC 工具集和 Windows SDK。
5. 完成安装后启动 Visual Studio 2026。
不要额外安装 MinGW。本文后续命令只使用 Visual Studio 2026 自带工具。
## 3. 打开项目文件夹
1. 启动 Visual Studio 2026。
2. 在开始窗口选择 `Open a local folder`
3. 选择项目根目录:
```text
D:\VSC_program\Tereis
```
4. 打开后可以在 Solution Explorer 中看到:
```text
src
assets
build-mingw.ps1
build-vs2026.ps1
VS2026_RUN_GUIDE.md
```
说明:`build-mingw.ps1` 是旧的 MinGW 构建脚本。只使用 VS2026 时不需要运行它。
`build-vs2026.ps1` 是本项目提供的 VS2026 专用构建脚本。
## 4. 打开 VS2026 开发者终端
普通 PowerShell 通常找不到 `cl.exe``rc.exe`。要使用 VS2026 自带编译器,应打开开发者终端:
1. 在 Visual Studio 2026 顶部菜单选择 `Tools -> Command Line -> Developer PowerShell`
2. 进入项目根目录:
```powershell
cd D:\VSC_program\Tereis
```
3. 检查工具是否可用:
```powershell
cl
rc
```
如果能看到 Microsoft C/C++ Compiler 和 Microsoft Windows Resource Compiler 的版本信息,说明 VS2026 C++ 工具链可用。
## 5. 使用 VS2026 工具链构建
`Developer PowerShell for VS 2026` 中执行:
```powershell
.\build-vs2026.ps1
```
构建并运行:
```powershell
.\build-vs2026.ps1 -Run
```
生成结果:
```text
.vscode-build\vs2026\Tetris.exe
```
该脚本会递归编译 `src\source` 下所有 `.cpp` 文件,包括 `render``app``logic``rogue``common``extensions` 等目录,避免手动建 VS 工程时漏加源文件。
如果需要手动理解脚本做了什么,核心命令如下。
先创建输出目录:
```powershell
New-Item -ItemType Directory -Force -Path .\.vscode-build\vs2026
```
编译资源文件:
```powershell
rc /nologo /i .\src\include /i .\assets\icons /fo .\.vscode-build\vs2026\Tetris.res .\src\resources\Tetris.rc
```
编译并链接 C++ 源码:
```powershell
$sources = Get-ChildItem .\src\source -Recurse -Filter *.cpp | ForEach-Object { $_.FullName }
cl /nologo /utf-8 /std:c++17 /EHsc /Zi /Od /DUNICODE /D_UNICODE /D_WINDOWS /I .\src\include $sources .\.vscode-build\vs2026\Tetris.res /Fe:.\.vscode-build\vs2026\Tetris.exe /link /SUBSYSTEM:WINDOWS winmm.lib gdiplus.lib gdi32.lib user32.lib shell32.lib
```
## 6. 运行程序
运行时建议从项目根目录启动,因为程序会读取 `assets/` 目录中的图片、音频和视频资源。
```powershell
Start-Process .\.vscode-build\vs2026\Tetris.exe -WorkingDirectory .
```
如果直接双击 exe,可能因为工作目录不对导致背景图、音乐或视频加载失败。
## 7. 常见问题
### 找不到 `cl.exe`
原因:没有在 VS2026 开发者终端中运行命令,或安装 VS2026 时没有选择 `Desktop development with C++`
处理:
1. 打开 `Tools -> Command Line -> Developer PowerShell`
2. 如果仍然找不到 `cl.exe`,打开 Visual Studio Installer,修改安装,勾选 `Desktop development with C++`
### 找不到 `rc.exe`
原因:Windows SDK 没有安装,或没有进入 VS2026 开发者终端。
处理:打开 Visual Studio Installer,确认 C++ 桌面开发工作负载中的 Windows SDK 已安装。
### 资源文件编译失败,提示找不到图标
原因:`Tetris.rc` 中引用了图标文件,资源编译命令必须包含图标目录。
处理:确认资源编译命令中包含:
```powershell
/i .\assets\icons
```
### 程序运行后没有图片、音乐或视频
原因:程序没有从项目根目录启动,导致 `assets/` 相对路径无法读取。
处理:
```powershell
Start-Process .\.vscode-build\vs2026\Tetris.exe -WorkingDirectory .
```
### 程序能运行、有音乐,但窗口黑屏
原因通常是手动创建 Visual Studio 工程时没有把所有源文件加入编译,尤其是漏掉了这些目录:
```text
src\source\app
src\source\common
src\source\extensions
src\source\logic
src\source\render
src\source\rogue
```
处理:不要运行手动残缺工程生成的 exe,改用 VS2026 开发者终端运行项目脚本:
```powershell
.\build-vs2026.ps1 -Run
```
如果一定要手动建 VS 工程,必须把 `src\source` 下所有 `.cpp` 文件递归加入项目,并把工作目录设置为项目根目录 `D:\VSC_program\Tereis`
### 直接按 F5 不能运行
原因:本项目当前没有 Visual Studio `.sln``.vcxproj` 工程文件,VS2026 不知道应该如何构建和启动。
处理:使用本文的 `Developer PowerShell for VS 2026` 构建方式。后续如果需要 F5 调试体验,可以再创建 Visual Studio C++ 工程文件。
## 8. 参考资料
- Visual Studio 2026 Release Notes: <https://learn.microsoft.com/visualstudio/releases/vs18/release-notes>
- Visual Studio 2026 Release History: <https://learn.microsoft.com/en-us/visualstudio/releases/2026/release-history>
- Visual Studio 2026 System Requirements: <https://learn.microsoft.com/en-us/visualstudio/releases/2026/vs-system-requirements>
- Install Visual Studio: <https://learn.microsoft.com/en-us/visualstudio/install/install-visual-studio>
- Use the Microsoft C++ toolset from the command line: <https://learn.microsoft.com/en-us/cpp/build/building-on-the-command-line>
- MSVC compiler command-line syntax: <https://learn.microsoft.com/en-us/cpp/build/reference/compiler-command-line-syntax>
-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 New-Item -ItemType Directory -Force -Path $BuildDir | Out-Null
$Sources = @( $Sources = Get-ChildItem -Path $SourceDir -Recurse -Filter "*.cpp" |
(Join-Path $SourceDir "stdafx.cpp"), Sort-Object FullName |
(Join-Path $SourceDir "Tetris.cpp"), Select-Object -ExpandProperty FullName
(Join-Path $SourceDir "TetrisLogic.cpp"),
(Join-Path $SourceDir "TetrisRogue.cpp"),
(Join-Path $SourceDir "TetrisRender.cpp")
)
$LinkInputs = @() $LinkInputs = @()
+75
View File
@@ -0,0 +1,75 @@
param(
[switch]$Run
)
$ErrorActionPreference = "Stop"
$Root = Split-Path -Parent $MyInvocation.MyCommand.Path
$ProjectDir = Join-Path $Root "src"
$IncludeDir = Join-Path $ProjectDir "include"
$SourceDir = Join-Path $ProjectDir "source"
$ResourceDir = Join-Path $ProjectDir "resources"
$AssetIconDir = Join-Path $Root "assets\icons"
$BuildDir = Join-Path $Root ".vscode-build\vs2026"
$ExePath = Join-Path $BuildDir "Tetris.exe"
$ResPath = Join-Path $BuildDir "Tetris.res"
$RcPath = Join-Path $ResourceDir "Tetris.rc"
if (-not (Get-Command cl.exe -ErrorAction SilentlyContinue)) {
throw "cl.exe not found. Open Visual Studio 2026: Tools -> Command Line -> Developer PowerShell, then run this script again."
}
if (-not (Get-Command rc.exe -ErrorAction SilentlyContinue)) {
throw "rc.exe not found. Install the Windows SDK from the Visual Studio Installer C++ desktop workload."
}
New-Item -ItemType Directory -Force -Path $BuildDir | Out-Null
$Sources = Get-ChildItem -Path $SourceDir -Recurse -Filter "*.cpp" |
Sort-Object FullName |
Select-Object -ExpandProperty FullName
if ($Sources.Count -lt 10) {
throw "Too few source files found under src\source. The render, app, logic, rogue, common, and extension modules must all be compiled."
}
& rc.exe `
/nologo `
/i $IncludeDir `
/i $AssetIconDir `
/fo $ResPath `
$RcPath
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
& cl.exe `
/nologo `
/utf-8 `
/std:c++17 `
/EHsc `
/Zi `
/Od `
/DUNICODE `
/D_UNICODE `
/D_WINDOWS `
/I $IncludeDir `
$Sources `
$ResPath `
/Fe:$ExePath `
/link `
/SUBSYSTEM:WINDOWS `
winmm.lib `
gdiplus.lib `
gdi32.lib `
user32.lib `
shell32.lib
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
if ($Run) {
Start-Process -FilePath $ExePath -WorkingDirectory $Root
}
+380
View File
@@ -1,11 +1,17 @@
#pragma once #pragma once
/**
* @file Tetris.h
* @brief 定义俄罗斯方块项目的全局常量、结构体、枚举、全局状态和公开函数接口。
*/
#include "resource.h" #include "resource.h"
#include "stdafx.h" #include "stdafx.h"
#include <mmsystem.h> #include <mmsystem.h>
#pragma comment(lib, "winmm.lib") #pragma comment(lib, "winmm.lib")
// 棋盘和窗口基础尺寸,渲染层会按当前窗口大小统一缩放这些设计稿尺寸。
constexpr int GRID = 40; constexpr int GRID = 40;
constexpr int nGameWidth = 10; constexpr int nGameWidth = 10;
constexpr int nGameHeight = 20; 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 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; constexpr int WINDOW_CLIENT_HEIGHT = (BOARD_CLIENT_HEIGHT > SIDE_PANEL_HEIGHT) ? BOARD_CLIENT_HEIGHT : SIDE_PANEL_HEIGHT;
/**
* @brief 棋盘坐标点,x 表示列号,y 表示行号。
*/
struct Point struct Point
{ {
int x; int x;
int y; int y;
}; };
/**
* @brief 主菜单导航状态。
*/
struct MenuState struct MenuState
{ {
int selectedIndex; int selectedIndex;
int optionCount; int optionCount;
}; };
/**
* @brief 帮助、规则、致谢和技能演示页面共享的导航状态。
*/
struct HelpState struct HelpState
{ {
int selectedIndex; int selectedIndex;
@@ -38,6 +53,12 @@ struct HelpState
int currentPage; int currentPage;
}; };
/**
* @brief 记录经典模式和 Rogue 模式的分数、等级、强化与临时状态。
*
* 课程要求不使用 class,因此所有与玩家成长有关的数据都集中放在结构体字段中,
* 由逻辑层函数按过程式方式读取和修改。
*/
struct PlayerStats struct PlayerStats
{ {
int score; int score;
@@ -110,6 +131,9 @@ struct PlayerStats
int pieceTuningLevels[7]; int pieceTuningLevels[7];
}; };
/**
* @brief 升级界面中已经生成并显示给玩家的一个候选强化。
*/
struct UpgradeOption struct UpgradeOption
{ {
int id; int id;
@@ -122,6 +146,9 @@ struct UpgradeOption
const TCHAR* description; const TCHAR* description;
}; };
/**
* @brief 强化池中的基础配置项,用于生成升级界面候选。
*/
struct UpgradeEntry struct UpgradeEntry
{ {
int id; int id;
@@ -133,6 +160,9 @@ struct UpgradeEntry
const TCHAR* description; const TCHAR* description;
}; };
/**
* @brief Rogue 升级选择界面的临时 UI 状态。
*/
struct UpgradeUiState struct UpgradeUiState
{ {
int selectedIndex; int selectedIndex;
@@ -140,9 +170,14 @@ struct UpgradeUiState
int pendingCount; int pendingCount;
int totalChosenCount; int totalChosenCount;
int picksRemaining; int picksRemaining;
int markedCount;
bool marked[6];
UpgradeOption options[6]; UpgradeOption options[6];
}; };
/**
* @brief 右侧战斗日志或提示条的显示状态。
*/
struct FeedbackState struct FeedbackState
{ {
int visibleTicks; int visibleTicks;
@@ -150,6 +185,9 @@ struct FeedbackState
TCHAR detail[128]; TCHAR detail[128];
}; };
/**
* @brief 标准消行动画状态。
*/
struct ClearEffectState struct ClearEffectState
{ {
int ticks; int ticks;
@@ -158,6 +196,9 @@ struct ClearEffectState
int rows[8]; int rows[8];
}; };
/**
* @brief 棋盘上浮动文字特效的单个实例。
*/
struct FloatingTextEffect struct FloatingTextEffect
{ {
int ticks; int ticks;
@@ -168,6 +209,9 @@ struct FloatingTextEffect
COLORREF color; COLORREF color;
}; };
/**
* @brief 棋盘粒子特效的单个实例。
*/
struct ParticleEffect struct ParticleEffect
{ {
int ticks; int ticks;
@@ -180,6 +224,34 @@ struct ParticleEffect
COLORREF color; 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 enum ScreenState
{ {
SCREEN_MENU = 0, SCREEN_MENU = 0,
@@ -188,12 +260,18 @@ enum ScreenState
SCREEN_RULES = 3 SCREEN_RULES = 3
}; };
/**
* @brief 当前游戏玩法模式。
*/
enum GameMode enum GameMode
{ {
MODE_CLASSIC = 0, MODE_CLASSIC = 0,
MODE_ROGUE = 1 MODE_ROGUE = 1
}; };
/**
* @brief 强化候选的稀有度,用于渲染不同颜色和排序说明。
*/
enum UpgradeRarity enum UpgradeRarity
{ {
UPGRADE_RARITY_COMMON = 0, UPGRADE_RARITY_COMMON = 0,
@@ -201,6 +279,7 @@ enum UpgradeRarity
UPGRADE_RARITY_RARE = 2 UPGRADE_RARITY_RARE = 2
}; };
// 以下全局状态沿用老师框架的过程式组织方式,各模块通过函数集中维护这些变量。
extern int nType; extern int nType;
extern int type; extern int type;
extern int state; extern int state;
@@ -210,11 +289,17 @@ extern bool suspendFlag;
extern bool targetFlag; extern bool targetFlag;
extern bool bgmEnabled; extern bool bgmEnabled;
extern bool reviveAvailable; extern bool reviveAvailable;
extern bool rogueDemoMode;
extern int workRegion[20][10]; extern int workRegion[20][10];
extern Point point; extern Point point;
extern Point target; extern Point target;
extern MenuState menuState; extern MenuState menuState;
extern HelpState helpState; extern HelpState helpState;
extern int helpScrollOffset;
extern int creditPageIndex;
extern int creditAnimationTicks;
extern int creditAnimationDirection;
extern int upgradeListScrollOffset;
extern PlayerStats classicStats; extern PlayerStats classicStats;
extern PlayerStats rogueStats; extern PlayerStats rogueStats;
extern UpgradeUiState upgradeUiState; extern UpgradeUiState upgradeUiState;
@@ -222,6 +307,8 @@ extern FeedbackState feedbackState;
extern ClearEffectState clearEffectState; extern ClearEffectState clearEffectState;
extern FloatingTextEffect floatingTextEffects[8]; extern FloatingTextEffect floatingTextEffects[8];
extern ParticleEffect particleEffects[96]; extern ParticleEffect particleEffects[96];
extern CellFlashEffect cellFlashEffects[64];
extern GravityFallEffect gravityFallEffects[80];
extern int currentScreen; extern int currentScreen;
extern int currentMode; extern int currentMode;
extern int currentFallInterval; extern int currentFallInterval;
@@ -235,42 +322,335 @@ extern bool currentPieceIsRainbow;
extern int bricks[7][4][4][4]; extern int bricks[7][4][4][4];
extern COLORREF BrickColor[7]; extern COLORREF BrickColor[7];
/**
* @brief 判断当前活动方块是否还能向下移动一格。
* @return 可以下落返回 true,否则返回 false。
*/
bool CanMoveDown(); bool CanMoveDown();
/**
* @brief 判断当前活动方块是否还能向左移动一格。
* @return 可以左移返回 true,否则返回 false。
*/
bool CanMoveLeft(); bool CanMoveLeft();
/**
* @brief 判断当前活动方块是否还能向右移动一格。
* @return 可以右移返回 true,否则返回 false。
*/
bool CanMoveRight(); bool CanMoveRight();
/**
* @brief 将当前活动方块向下移动一格。
*/
void MoveDown(); void MoveDown();
/**
* @brief 将当前活动方块向左移动一格。
*/
void MoveLeft(); void MoveLeft();
/**
* @brief 将当前活动方块向右移动一格。
*/
void MoveRight(); void MoveRight();
/**
* @brief 尝试旋转当前活动方块,Rogue 完美旋转会额外尝试左右偏移。
*/
void Rotate(); void Rotate();
/**
* @brief 将当前活动方块直接下落到预测落点。
*/
void DropDown(); void DropDown();
/**
* @brief 将当前活动方块固定到棋盘并生成下一块。
*/
void Fixing(); void Fixing();
/**
* @brief 删除指定行并让上方棋盘整体下落。
* @param number 要删除的棋盘行号。
*/
void DeleteOneLine(int number); void DeleteOneLine(int number);
/**
* @brief 扫描棋盘、删除所有满行并触发消行结算。
* @return 本次删除的行数。
*/
int DeleteLines(); int DeleteLines();
/**
* @brief 判断当前游戏是否已经结束。
* @return 游戏结束返回 true,否则返回 false。
*/
bool GameOver();
/**
* @brief 计算当前活动方块的预测落点。
*/
void ComputeTarget(); void ComputeTarget();
/**
* @brief 重置棋盘、方块、统计和视觉状态,开始一局新游戏。
*/
void Restart(); void Restart();
/**
* @brief 按指定模式开始新游戏。
* @param mode 游戏模式,取值来自 GameMode。
*/
void StartGameWithMode(int mode); void StartGameWithMode(int mode);
/**
* @brief 返回主菜单并清理临时玩法与界面状态。
*/
void ReturnToMainMenu(); void ReturnToMainMenu();
/**
* @brief 复活视频播放成功后恢复游戏并清理顶部空间。
*/
void ReviveAfterVideo(); void ReviveAfterVideo();
/**
* @brief 从帮助页进入 Rogue 技能演示的第一项。
*/
void StartRogueSkillDemo();
/**
* @brief 从帮助页进入指定 Rogue 技能演示。
* @param demoIndex 技能演示序号。
*/
void StartRogueSkillDemoAt(int demoIndex);
/**
* @brief 重新开始当前 Rogue 技能演示场景。
*/
void RestartCurrentRogueSkillDemo();
/**
* @brief 判断当前是否处于 Rogue 技能演示模式。
* @return 演示模式中返回 true,否则返回 false。
*/
bool IsRogueSkillDemoMode();
/**
* @brief 推进 Rogue 技能演示计时。
* @return 演示模式正在运行返回 true,否则返回 false。
*/
bool TickRogueSkillDemo();
/**
* @brief 切换到下一项 Rogue 技能演示。
*/
void AdvanceRogueSkillDemo();
/**
* @brief 获取 Rogue 技能演示条目数量。
* @return 可选择的演示条目总数。
*/
int GetRogueSkillDemoCount();
/**
* @brief 获取指定 Rogue 技能演示名称。
* @param demoIndex 技能演示序号。
* @return 名称字符串,越界时返回空字符串。
*/
const TCHAR* GetRogueSkillDemoName(int demoIndex);
/**
* @brief 获取指定 Rogue 技能演示说明。
* @param demoIndex 技能演示序号。
* @return 说明字符串,越界时返回空字符串。
*/
const TCHAR* GetRogueSkillDemoDetail(int demoIndex);
/**
* @brief 获取当前 Rogue 技能演示名称。
* @return 当前名称,非演示模式返回空字符串。
*/
const TCHAR* GetCurrentRogueSkillDemoName();
/**
* @brief 设置右侧战斗日志反馈信息。
* @param title 反馈标题。
* @param detail 反馈详情。
* @param ticks 保持显示的游戏计时次数。
*/
void SetFeedbackMessage(const TCHAR* title, const TCHAR* detail, int ticks); void SetFeedbackMessage(const TCHAR* title, const TCHAR* detail, int ticks);
/**
* @brief 打开帮助首页。
*/
void OpenRulesScreen(); void OpenRulesScreen();
/**
* @brief 打开 Rogue 技能演示选择页。
*/
void OpenSkillDemoScreen();
/**
* @brief 打开致谢页。
*/
void OpenCreditScreen();
/**
* @brief 切换致谢页图片。
* @param direction 小于 0 向前切换,大于 0 向后切换。
*/
void ChangeCreditPage(int direction);
/**
* @brief 打开 Rogue 升级选择界面。
*/
void OpenUpgradeMenu(); void OpenUpgradeMenu();
/**
* @brief 确认当前升级选择并恢复游戏流程。
*/
void ConfirmUpgradeSelection(); void ConfirmUpgradeSelection();
/**
* @brief 重置升级选择界面状态。
*/
void ResetUpgradeUiState();
/**
* @brief 使用或解锁后处理 Hold 备用仓逻辑。
*/
void HoldCurrentPiece(); void HoldCurrentPiece();
/**
* @brief 使用清屏炸弹主动技能。
*/
void UseScreenBomb(); void UseScreenBomb();
/**
* @brief 使用黑洞主动技能。
*/
void UseBlackHole(); void UseBlackHole();
/**
* @brief 使用空中换形主动技能。
*/
void UseAirReshape(); void UseAirReshape();
/**
* @brief 重置 Rogue 待播放视觉事件。
*/
void ResetPendingRogueVisualEvents(); void ResetPendingRogueVisualEvents();
/**
* @brief 清空所有视觉效果状态。
*/
void ResetVisualEffects(); void ResetVisualEffects();
/**
* @brief 推进视觉效果动画。
* @return 仍有动画需要刷新返回 true,否则返回 false。
*/
bool TickVisualEffects(); bool TickVisualEffects();
/**
* @brief 推进致谢页切换动画。
* @return 需要刷新界面返回 true,否则返回 false。
*/
bool TickCreditAnimation();
/**
* @brief 触发标准消行动画。
* @param rows 被消除的行号数组。
* @param rowCount 行号数量。
* @param linesCleared 实际消除行数。
*/
void TriggerLineClearEffect(const int* rows, int rowCount, int linesCleared); void TriggerLineClearEffect(const int* rows, int rowCount, int linesCleared);
/**
* @brief 播放之前因升级界面暂存的消行动画。
*/
void PlayPendingLineClearEffect(); void PlayPendingLineClearEffect();
/**
* @brief 触发指定棋盘格的默认清除特效。
* @param cells 被清除格子数组。
* @param cellCount 格子数量。
* @param strongBurst 是否使用更强的爆裂粒子。
*/
void TriggerCellClearEffect(const Point* cells, int cellCount, bool strongBurst); void TriggerCellClearEffect(const Point* cells, int cellCount, bool strongBurst);
/**
* @brief 触发指定棋盘格的自定义颜色清除特效。
* @param cells 被清除格子数组。
* @param cellCount 格子数量。
* @param flashColor 高亮颜色。
* @param strongBurst 是否使用更强的爆裂粒子。
*/
void TriggerColoredCellClearEffect(const Point* cells, int cellCount, COLORREF flashColor, bool strongBurst);
/**
* @brief 记录一个固定方块受重力下落的轨迹。
* @param x 棋盘列号。
* @param fromY 下落起始行号。
* @param toY 下落目标行号。
* @param cellValue 方块格子数值。
*/
void TriggerGravityFallEffect(int x, int fromY, int toY, int cellValue);
/**
* @brief 为 Rogue 主动或特殊技能清除格子发放奖励。
* @param clearedCells 清除格子数。
* @param scoreGain 返回本次得分增量。
* @param expGain 返回本次经验增量。
* @param allowLevelProgress 是否允许本次奖励触发升级流程。
*/
void AwardRogueSkillClearRewards(int clearedCells, int& scoreGain, int& expGain, bool allowLevelProgress); void AwardRogueSkillClearRewards(int clearedCells, int& scoreGain, int& expGain, bool allowLevelProgress);
/**
* @brief 检查 Rogue 经验是否达到升级条件。
*/
void CheckRogueLevelProgress();
/**
* @brief 对棋盘固定方块应用重力下落。
*/
void ApplyBoardGravity(); void ApplyBoardGravity();
/**
* @brief 计算当前 Rogue 模式下落间隔。
* @return 下落计时器间隔,单位毫秒。
*/
int GetRogueFallInterval(); int GetRogueFallInterval();
/**
* @brief 获取 Rogue 当前可操作棋盘高度。
* @return 未被底部封锁占用的行数。
*/
int GetRoguePlayableHeight(); int GetRoguePlayableHeight();
/**
* @brief 获取 Rogue 难度系统当前封锁的底部行数。
* @return 封锁行数。
*/
int GetRogueLockedRows(); int GetRogueLockedRows();
/**
* @brief 按经过时间推进 Rogue 难度。
* @param elapsedMs 本次推进的时间,单位毫秒。
*/
void AdvanceRogueDifficulty(int elapsedMs); void AdvanceRogueDifficulty(int elapsedMs);
/**
* @brief 获取进化强化的合成路线文本。
* @param upgradeId 强化编号。
* @return 路线文本;普通强化返回空或空指针。
*/
const TCHAR* GetUpgradeSynthesisPath(int upgradeId); const TCHAR* GetUpgradeSynthesisPath(int upgradeId);
/**
* @brief 绘制当前窗口中的完整游戏界面。
* @param hdc 目标绘图设备上下文。
* @param hWnd 当前窗口句柄,用于读取客户区大小。
*/
void TDrawScreen(HDC hdc, HWND hWnd); void TDrawScreen(HDC hdc, HWND hWnd);
+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 #pragma once
/**
* @file TetrisLogicInternal.h
* @brief 声明棋盘逻辑、Rogue 结算和特殊方块效果使用的内部接口。
*/
#include "Tetris.h" #include "Tetris.h"
extern Point pendingChainBombCenter; extern Point pendingChainBombCenter;
extern bool pendingChainBombFollowup; extern bool pendingChainBombFollowup;
extern int pendingLineClearEffectTicks;
extern int pendingLineClearEffectRows[8];
extern int pendingLineClearEffectRowCount;
extern int pendingLineClearEffectLineCount;
// Internal 头文件只暴露跨 cpp 文件共享的辅助函数,外部窗口层仍通过 Tetris.h 调用公开接口。
/**
* @brief 计算指定方块在棋盘顶部的统一生成位置。
* @param brickType 方块类型编号。
* @return 生成坐标,可能位于可视区域上方。
*/
Point GetSpawnPoint(int brickType); 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); void ResetPlayerStats(PlayerStats& stats, bool useRogueRules);
/**
* @brief 设置界面右侧显示的即时反馈标题、内容和持续时间。
* @param title 反馈标题。
* @param detail 反馈详情。
* @param ticks 显示持续的游戏计时次数。
*/
void SetFeedbackMessage(const TCHAR* title, const TCHAR* detail, int ticks); void SetFeedbackMessage(const TCHAR* title, const TCHAR* detail, int ticks);
/**
* @brief 判断指定方块、旋转状态和位置是否可以合法放置。
* @param pieceType 方块类型编号。
* @param pieceState 方块旋转状态。
* @param position 待检测的左上角坐标。
* @return 可以放置返回 true,否则返回 false。
*/
bool IsPiecePlacementValid(int pieceType, int pieceState, Point position); bool IsPiecePlacementValid(int pieceType, int pieceState, Point position);
/**
* @brief 判断棋盘格是否为彩虹特殊方块。
* @param cellValue 棋盘格存储值。
* @return 彩虹方块返回 true,否则返回 false。
*/
bool IsRainbowBoardCell(int cellValue); bool IsRainbowBoardCell(int cellValue);
/**
* @brief 触发小型黑洞并返回被清除的固定方块数量。
* @param maxCellsToClear 最多清除的格子数。
* @return 实际清除格子数。
*/
int TriggerMiniBlackHole(int maxCellsToClear); int TriggerMiniBlackHole(int maxCellsToClear);
int TriggerRainbowRowCompletion(int minRow, int maxRow);
/**
* @brief 触发彩虹方块行清除与覆盖行染色效果。
* @param anchorRow 作为主色判断的中心行。
* @param minRow 允许染色范围的最小行。
* @param maxRow 允许染色范围的最大行。
* @param recoloredCount 返回被染色的格子数。
* @return 被清除的主色格子数。
*/
int TriggerRainbowColorShift(int anchorRow, int minRow, int maxRow, int& recoloredCount);
/**
* @brief 引爆清屏炸弹并返回清除格数。
* @return 实际清除格子数。
*/
int TriggerScreenBomb(); int TriggerScreenBomb();
/**
* @brief 清除指定中心点周围的爆破范围并返回清除格数。
* @param centerY 爆破中心行。
* @param centerX 爆破中心列。
* @return 实际清除格子数。
*/
int ClearExplosiveAreaAt(int centerY, int centerX); int ClearExplosiveAreaAt(int centerY, int centerX);
/**
* @brief 清除指定列并返回清除格数。
* @param column 目标列号。
* @return 实际清除格子数。
*/
int ClearColumnAt(int column); int ClearColumnAt(int column);
/**
* @brief 使用指定颜色特效清除指定列并返回清除格数。
* @param column 目标列号。
* @param flashColor 清除高亮颜色。
* @return 实际清除格子数。
*/
int ClearColumnAtWithColor(int column, COLORREF flashColor);
/**
* @brief 清除指定行并返回清除格数。
* @param row 目标行号。
* @return 实际清除格子数。
*/
int ClearRowAt(int row); int ClearRowAt(int row);
/**
* @brief 尝试填补局部空洞以稳定棋盘结构。
* @return 实际填补格子数。
*/
int TryStabilizeBoard(); int TryStabilizeBoard();
/**
* @brief 为当前方块刷新 Rogue 特殊方块标记。
* @param allowRandomSpecials 是否允许按强化概率随机生成特殊方块。
*/
void RollCurrentPieceSpecialFlags(bool allowRandomSpecials); void RollCurrentPieceSpecialFlags(bool allowRandomSpecials);
/**
* @brief 暂存消行动画,等待升级选择结束后再播放。
* @param rows 被消除的行号数组。
* @param rowCount 行号数量。
* @param linesCleared 实际消除行数。
*/
void QueueLineClearEffect(const int* rows, int rowCount, int linesCleared);
/**
* @brief 记录固定方块受重力下落的轨迹,用于播放纵向残影特效。
* @param x 棋盘列号。
* @param fromY 起始行号。
* @param toY 目标行号。
* @param cellValue 方块格子值。
*/
void TriggerGravityFallEffect(int x, int fromY, int toY, int cellValue);
/**
* @brief 尝试把旋转后的方块横向偏移指定格数后放置。
* @param nextState 旋转后的状态编号。
* @param offsetX 横向试探偏移。
* @return 偏移后可以放置返回 true,否则返回 false。
*/
bool TryRotateWithOffset(int nextState, int offsetX);
/**
* @brief 重置下一方块预览队列。
*/
void ResetNextQueue(); void ResetNextQueue();
/**
* @brief 消费队首下一方块并补充新的预览方块。
* @return 新的当前方块类型编号。
*/
int ConsumeNextType(); int ConsumeNextType();
/**
* @brief 结算一次标准消行带来的 Rogue 玩法效果。
* @param linesCleared 本次标准消行数量。
*/
void ApplyLineClearResult(int linesCleared); void ApplyLineClearResult(int linesCleared);
/**
* @brief 结算彩虹方块固定后的染色和清除效果。
* @param overflowTop 固定时是否已经越过顶部。
* @param fixedCells 当前方块写入棋盘的格子数组。
* @param fixedCellCount 写入棋盘的格子数量。
*/
void ApplyRainbowLandingEffect(bool overflowTop, const Point* fixedCells, int fixedCellCount);
/**
* @brief 结算爆破、激光、十字和稳定结构等特殊落地效果。
* @param fixedCells 当前方块写入棋盘的格子数组。
* @param fixedCellCount 写入棋盘的格子数量。
* @param explosiveCells 爆破方块写入棋盘的格子数组。
* @param explosiveCellCount 爆破格子数量。
*/
void ApplySpecialLandingEffects(const Point* fixedCells, int fixedCellCount, const Point* explosiveCells, int explosiveCellCount);
+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);
+8 -3
View File
@@ -1,9 +1,13 @@
#pragma once #pragma once
/**
* @file resource.h
* @brief Defines Windows resource IDs for menus, icons, dialogs, commands, and strings.
*/
//{{NO_DEPENDENCIES}} //{{NO_DEPENDENCIES}}
// Microsoft Visual C++ 生成的包含文件。 // Microsoft Visual C++ generated include file.
// Tetris.rc 使用 // Used by Tetris.rc.
//
#define IDS_APP_TITLE 103 #define IDS_APP_TITLE 103
@@ -12,6 +16,7 @@
#define IDD_ABOUTBOX 103 #define IDD_ABOUTBOX 103
#define IDM_ABOUT 104 #define IDM_ABOUT 104
#define IDM_EXIT 105 #define IDM_EXIT 105
#define IDM_SKILL_DEMO 106
#define IDI_TETRIS 107 #define IDI_TETRIS 107
#define IDI_SMALL 108 #define IDI_SMALL 108
#define IDC_TETRIS 109 #define IDC_TETRIS 109
+7 -6
View File
@@ -1,17 +1,18 @@
// stdafx.h : 标准系统包含文件的包含文件, /**
// 或是经常使用但不常更改的 * @file stdafx.h
// 特定于项目的包含文件 * @brief 集中包含 Windows、C 运行时和项目常用基础头文件。
// */
#pragma once #pragma once
#include "targetver.h" #include "targetver.h"
// 精简 Windows 头文件,缩短编译时间,同时保留本项目需要的 Win32/GDI API。
#define WIN32_LEAN_AND_MEAN // 从 Windows 头文件中排除极少使用的信息 #define WIN32_LEAN_AND_MEAN // 从 Windows 头文件中排除极少使用的信息
// Windows 头文件: // Windows 头文件:
#include <windows.h> #include <windows.h>
// C 运行时头文件 // C 运行时头文件:本项目使用随机数、内存工具、TCHAR 字符串和时间函数。
#include <stdlib.h> #include <stdlib.h>
#include <malloc.h> #include <malloc.h>
#include <memory.h> #include <memory.h>
@@ -19,4 +20,4 @@
#include <time.h> #include <time.h>
// TODO: 在此处引用程序需要的其他头文件 // 其他模块各自包含自己的业务头文件,避免预编译头承担过多项目依赖。
+8 -3
View File
@@ -1,8 +1,13 @@
#pragma once #pragma once
// 包括 SDKDDKVer.h 将定义可用的最高版本的 Windows 平台。 /**
* @file targetver.h
* @brief 设置 Windows SDK 目标平台版本,供 Win32 头文件选择可用 API。
*/
// 如果要为以前的 Windows 平台生成应用程序,请包括 WinSDKVer.h,并将 // 包括 SDKDDKVer.h 将定义可用的最高版本 Windows 平台宏。
// WIN32_WINNT 宏设置为要支持的平台,然后再包括 SDKDDKVer.h。
// 若课程演示环境需要兼容更旧 Windows,可在这里先包含 WinSDKVer.h
// 再设置 WIN32_WINNT;当前项目直接使用 SDK 默认最高版本。
#include <SDKDDKVer.h> #include <SDKDDKVer.h>
Binary file not shown.
+109 -804
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 将作为预编译头 * @file stdafx.cpp
// stdafx.obj 将包含预编译类型信息 * @brief stdafx.h
*/
#include "stdafx.h" #include "stdafx.h"
/**
* @file stdafx.cpp
* @brief stdafx.h
*
* Visual Studio
*/
// TODO: 在 STDAFX.H 中 // TODO: 在 STDAFX.H 中
// 引用任何所需的附加头文件,而不是在此文件中引用 // 引用任何所需的附加头文件,而不是在此文件中引用