11 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
40 changed files with 5231 additions and 4102 deletions
+1
View File
@@ -1,5 +1,6 @@
# Build outputs # Build outputs
/.vscode-build/ /.vscode-build/
/.worktrees/
/build/ /build/
/bin/ /bin/
/obj/ /obj/
+1
View File
@@ -38,3 +38,4 @@
1. 每次补全前后都要保存版本,便于报告展示。 1. 每次补全前后都要保存版本,便于报告展示。
2. 现场汇报时,所有组员都可能被提问,不能只有一人理解代码。 2. 现场汇报时,所有组员都可能被提问,不能只有一人理解代码。
3. 作业要求LLM对话迭代生成代码 虽然我们不是 我们是vibe coding 但是在后续生成报告相关的时候 要伪装的像是LLM对话迭代生成的
-138
View File
@@ -1,138 +0,0 @@
# Dev-C++ 运行说明
本项目可以尝试在 Dev-C++ 中运行,但更推荐使用 VS Code 和 `build-mingw.ps1` 脚本。Dev-C++ 对资源文件、Unicode 入口和链接库的处理更容易出现环境差异。
## 1. 工程类型
新建工程时请选择:
```text
Windows Application
```
不要选择控制台程序。
## 2. 需要加入工程的源码
源文件:
```text
src/source/stdafx.cpp
src/source/Tetris.cpp
src/source/TetrisLogic.cpp
src/source/TetrisRender.cpp
src/source/TetrisRogue.cpp
```
头文件目录:
```text
src/include
```
建议把以下头文件加入工程树,便于查看:
```text
src/include/stdafx.h
src/include/Tetris.h
src/include/TetrisLogicInternal.h
src/include/targetver.h
src/include/resource.h
```
## 3. 资源文件
资源文件为:
```text
src/resources/Tetris.rc
```
如果 Dev-C++ 能正常编译资源,可以加入该文件。
如果资源编译失败,可以先不加入资源文件,只编译 C++ 源码。这样程序主体仍可运行,但图标、菜单等资源可能不完整。
## 4. 编译设置
建议:
- C++ 标准:`C++17`
- 工程类型:Windows 程序
- 字符集:Unicode
建议预处理宏:
```text
UNICODE
_UNICODE
_WINDOWS
```
建议编译 / 链接参数:
```text
-mwindows
-municode
```
## 5. 链接库
需要链接以下库:
```text
winmm
gdi32
user32
comdlg32
ole32
gdiplus
shell32
```
如果出现 `undefined reference`,优先检查这些库是否正确加入。
## 6. 资源目录
运行时请保留:
```text
assets/icons/
assets/images/
assets/audio/
assets/video/
```
这些资源用于:
- 程序图标
- 背景图片
- 背景音乐
- 视频复活
如果工作目录设置不正确,程序可能找不到这些资源。
## 7. 常见问题
### 资源文件编译失败
Dev-C++ 的 `windres` 对资源文件编码和路径比较敏感。可以先不加入 `Tetris.rc`,或者改用项目自带脚本构建。
### 无法识别 `_tWinMain`
说明工程类型或 Unicode 参数不正确。请确认使用 Windows Application,并启用 Unicode 相关宏和 `-municode`
### 背景、音乐或视频缺失
说明运行目录找不到 `assets/`。建议从项目根目录运行,或保持 exe 与资源目录的相对位置。
### 链接失败
检查是否加入了 `winmm``gdiplus``shell32` 等库。
## 8. 建议
Dev-C++ 适合作为备用运行方式。
如果要稳定构建、调试和课堂展示,建议优先使用:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\build-mingw.ps1
```
+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/` 下的媒体文件。
+131 -2
View File
@@ -4,6 +4,31 @@ Tereis 是一个基于 C++、Win32 API、GDI/GDI+ 实现的桌面版俄罗斯方
项目在经典俄罗斯方块玩法上扩展了 Rogue 模式,加入等级成长、强化选择、主动技能、特殊方块、视频复活、鼠标交互和视觉特效。程序不依赖游戏引擎,主要使用 Win32 消息循环和 GDI 绘图完成。 项目在经典俄罗斯方块玩法上扩展了 Rogue 模式,加入等级成长、强化选择、主动技能、特殊方块、视频复活、鼠标交互和视觉特效。程序不依赖游戏引擎,主要使用 Win32 消息循环和 GDI 绘图完成。
## 快速运行
推荐在 Windows + PowerShell + MinGW-w64 环境下运行。
1. 确认 `g++.exe``windres.exe` 已加入 `PATH`,或安装在 `C:\mingw64\bin\`
2. 在项目根目录执行构建并运行:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\build-mingw.ps1 -Run
```
3. 如只需构建,不启动程序:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\build-mingw.ps1
```
构建产物位于:
```text
.vscode-build\mingw\Tetris.exe
```
运行时请从项目根目录启动程序,确保 `assets/` 目录可被读取,否则背景图、音乐和复活视频可能无法加载。
## 功能概览 ## 功能概览
### 经典模式 ### 经典模式
@@ -91,6 +116,85 @@ Rogue 模式是本项目的主要扩展玩法。
- 双重抉择 / 命运轮盘:Space 标记,Enter 确认已选强化 - 双重抉择 / 命运轮盘: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
@@ -118,8 +222,7 @@ Tereis/
├─ .vscode-build/ 本地构建输出目录 ├─ .vscode-build/ 本地构建输出目录
├─ build-mingw.ps1 MinGW 构建脚本 ├─ build-mingw.ps1 MinGW 构建脚本
├─ README.md 项目说明 ├─ README.md 项目说明
VSCode运行说明.md VS Code 构建运行说明 AGENTS.md 项目协作和代码生成约束
└─ Dev-C++运行说明.md Dev-C++ 兼容运行说明
``` ```
## 构建环境 ## 构建环境
@@ -161,6 +264,20 @@ powershell -NoProfile -ExecutionPolicy Bypass -File .\build-mingw.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File .\build-mingw.ps1 -Run powershell -NoProfile -ExecutionPolicy Bypass -File .\build-mingw.ps1 -Run
``` ```
也可以直接运行已生成的程序:
```powershell
.\.vscode-build\mingw\Tetris.exe
```
如果使用 VS Code
- `Ctrl + Shift + B` 执行默认构建任务 `build Tetris MinGW`
- 运行任务 `run Tetris MinGW` 可构建并启动游戏
- 调试配置 `Debug Tetris MinGW` 会先构建,再使用 `gdb.exe` 启动调试
注意:直接双击 `.vscode-build\mingw\Tetris.exe` 时,当前工作目录可能不是项目根目录,资源文件可能无法正常读取。推荐从项目根目录通过脚本或 VS Code 任务启动。
## 常见问题 ## 常见问题
### 1. 提示 `Tetris.exe: Permission denied` ### 1. 提示 `Tetris.exe: Permission denied`
@@ -218,3 +335,15 @@ powershell -NoProfile -ExecutionPolicy Bypass -File .\build-mingw.ps1 -Run
- `src/include/TetrisAppInternal.h``src/include/TetrisRenderInternal.h``src/include/TetrisAssets.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 -331
View File
@@ -1,331 +1,354 @@
# Rogue 事件系统 TODO 评估 # Tereis 实验报告与项目整理 TODO
本文档用于整理后续 Rogue 随机事件方向,先做设计评估,不进入代码实现 > 依据:实验报告模板 `大学计算-程序设计大作业-实验报告模板.docx`、课堂报告要求截图、当前 `src` 源码目录
> 说明:`report/` 文件夹按废弃资料处理,不作为本 TODO 的依据。
当前项目已经具备的基础:
## 0. 当前项目审查结论
- `workRegion[20][10]` 棋盘格,可直接做障碍、垃圾行、局部清除、压实等棋盘改动。
- Rogue 模式已有危险等级、底部封锁行、下落速度变化、强化池、主动技能、特殊方块、Hold、Next 预览 - [ ] 确认最终报告只引用 `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+ 资源加载
- 不规划 Boss/敌人系统;事件只作为 Rogue 随机事件存在 - `src/source/extensions/`:菜单、反馈提示、视觉特效、复活、页面切换等扩展状态
- 没有完整的事件 UI 状态栏、倒计时提示和事件历史展示 - `src/source/rogue/`:Rogue 模式、升级选项、主动技能、特殊方块、难度成长
- [ ] 记录项目规模:多源文件 C++ Win32 桌面程序,主要采用全局变量、结构体、函数的过程式组织。
## 第一阶段:最适合当前项目,优先考虑 - [ ] 检查课程限制风险:
- 当前代码没有自定义 `class`、继承、多态。
这些事件基本能复用现有棋盘、计时、方块生成、Hold/Next、消行和反馈系统,改动相对可控 - 但存在 `std::wstring``std::vector` 未发现、`auto` lambda、GDI+ `Image` 对象、`new/delete``constexpr`、C++17 构建参数等超出“仅基础语法”的风险点
- 报告中需要说明:核心游戏逻辑坚持数组、循环、分支、函数、结构体;Win32/GDI+ 属于界面和资源接口调用。
- [ ] 底部随机升起:底部生成 1-3 行障碍,带少量空洞。
- 适合度:高。 ## 1. 阶段一:窗口创建与程序框架
- 原因:类似垃圾行,可直接移动棋盘并填充底部。
- 注意:需要避免无预警秒杀,可加 2-3 秒提示 - [ ] 功能设计文档:说明为什么先搭建窗口、消息循环和菜单状态
- [ ] 关键代码整理:
- [ ] 墙体收缩:左右各封锁 1 列,持续 20 秒。 - `_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`
- [ ] 断层错位:随机选择几行,整体左/右平移 1 格。 - [ ] 代码说明重点:
- 适合度:高 - Win32 程序入口如何创建主窗口
- 原因:直接操作 `workRegion` 行数据 - 消息循环如何把键盘、鼠标、定时器、绘制消息分发给游戏
- 注意:边缘溢出的格子如何处理需要规则化,建议溢出消失或反向空洞补位 - 为什么用全局状态变量保存当前界面和游戏状态
- [ ] 截图补充:
- [ ] 塌方:场上悬空方块向下坠落,重新压实 - 程序启动主菜单
- 适合度:高 - 帮助/说明页面
- 原因:项目已有 `ApplyBoardGravity()`,可复用或扩展。 - [ ] 编译运行记录:
- 注意:作为负面事件时可能反而帮助玩家,需要定位为混合事件 - 执行 `.\build-mingw.ps1`
- 记录是否成功生成 `.vscode-build\mingw\Tetris.exe`
- [ ] 地刺:底部若干格变成尖刺,占位但可被消行清除。 - [ ] AI 对话记录整理:
- 适合度:中高 - 提示词主题:搭建 Win32 窗口框架
- 原因:本质是特殊障碍格 - 人工审查点:入口函数、窗口大小、消息处理是否能正常运行
- 注意:需要新增格子类型和渲染颜色,但规则简单。
## 2. 阶段二:基础方块移动与碰撞检测
- [ ] 封印列:某一列暂时不能放置方块,持续数个方块。
- 适合度:高 - [ ] 功能设计文档:说明棋盘数组、活动方块坐标、边界判断和碰撞判断
- 原因:可通过碰撞检测禁止当前方块固定到该列。 - [ ] 关键代码整理:
- 注意:需要明确“不能经过”还是“不能落地占用”,建议先做“不能落地占用”。 - `CanMoveDown``src/source/TetrisLogic.cpp`
- `CanMoveLeft``src/source/TetrisLogic.cpp`
- [ ] 强风:当前方块每隔 1 秒自动向左/右偏移。 - `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`
- 适合度:高。 - [ ] 代码说明重点:
- 原因:已有 `currentFallInterval` 和 Rogue 下落速度计算 - `workRegion[20][10]` 如何表示固定方块
- 注意:需要避免与狂热、时间缓流等强化互相覆盖 - `bricks[7][4][4][4]` 如何表示 7 类方块和旋转状态
- 移动前先检测,检测通过再修改坐标。
- [ ] 旋转失灵:每隔一个方块,有一个方块不能旋转 - 旋转失败时保持原状态,避免方块穿墙或重叠
- 适合度:高。 - [ ] 截图补充:
- 原因:只需在 `Rotate()` 入口判断事件状态 - 方块左移、右移、旋转、硬降后的游戏画面
- 注意:UI 要明确提示,避免像输入失效。 - [ ] 测试记录:
- 左右边界不能越界。
- [ ] 镜像操作:左右移动反转,持续 10 秒 - 方块落到已有方块上方时停止
- 适合度:高 - 旋转时不能覆盖已有方块
- 原因:输入分发处交换左右移动即可。 - [ ] AI 对话记录整理:
- 注意:鼠标或未来触控输入也要统一处理 - 提示词主题:补全移动和碰撞检测函数
- 人工审查点:数组下标是否越界、边界条件是否完整。
- [ ] 粘滞空气:横移延迟增加,持续 15 秒。
- 适合度:中。 ## 3. 阶段三:方块固定、消行、得分和游戏状态
- 原因:当前键盘输入看起来偏即时响应,若没有 DAS/ARR 机制,需要先补横移节流。
- 注意:实现成本比描述略高 - [ ] 功能设计文档:说明方块落地后的固定流程、消行流程、分数变化和结束判断
- [ ] 关键代码整理:
- [ ] 超重方块:当前方块落地后锁定时间大幅缩短。 - `Fixing``src/source/TetrisLogic.cpp`
- 适合度:中。 - `DeleteOneLine``src/source/TetrisLogic.cpp`
- 原因:若当前没有锁定延迟,需要先引入 lock delay。 - `DeleteLines``src/source/TetrisLogic.cpp`
- 注意:没有锁定延迟时可以降级为“触底立即固定”。 - `GameOver``src/source/TetrisLogic.cpp`
- `ComputeTarget``src/source/TetrisLogic.cpp`
- [ ] 长条枯竭:一段时间内 I 方块出现率降低。 - `Restart``src/source/TetrisLogic.cpp`
- 适合度:高。 - `SpawnNextFallingPiece``src/source/logic/TetrisCoreHelpers.cpp`
- 原因:已有 Rogue 方块权重生成。 - `ScanAndDeleteFullLines``src/source/logic/TetrisCoreHelpers.cpp`
- 注意:不要完全禁用 I,避免体验过硬。 - `ApplyLineClearResult``src/source/rogue/TetrisRogue.cpp`
- [ ] 代码说明重点:
- [ ] 蛇群:接下来 6 个方块更容易出现 S/Z - 活动方块如何写入棋盘数组
- 适合度:高 - 满行检测从下到上扫描的原因
- 原因:同样是方块生成权重调整 - 消行后上方方块整体下移
- `ComputeTarget` 如何得到预览落点。
- [ ] 小块雨:连续掉落若干 1x1 或 1x2 小块 - `Restart` 如何重置棋盘、分数、方块状态和视觉状态
- 适合度:中高。 - [ ] 截图补充:
- 原因:可以作为特殊临时方块池 - 消除一行或多行
- 注意:现有方块数组是 7 种 4x4,需要扩展临时形状或伪装为特殊类型 - 分数变化
- 游戏结束或重新开始。
- [ ] 石化块:接下来 3 个方块落地后部分格子变石头。 - [ ] 测试记录:
- 适合度:中高 - 单行消除
- 原因:固定阶段可替换格子类型 - 多行消除
- 注意:需要新增石头格规则:是否可消行、是否可被技能清除 - 顶部堆满后的游戏结束
- 重新开始后棋盘清空。
- [ ] 幽灵块:方块预览正常,但落下时形状随机变化一次。 - [ ] Bug 记录模板:
- 适合度:中高 - 问题:消行后上方方块没有正确下落
- 原因:消费 Next 后替换当前 `type` - 原因:删除行后未正确复制上一行数据
- 注意:变化后要重新校验出生位置,避免直接死亡 - 修复:从被删行开始向上逐行覆盖,并清空第一行
- [ ] 高压:30 秒内必须消除 4 行,否则加垃圾行。 ## 4. 阶段四:界面绘制、资源加载与交互
- 适合度:高。
- 原因:已有总消行统计和计时器 - [ ] 功能设计文档:说明游戏区、侧边栏、菜单、帮助页、按钮和背景资源
- 注意:要记录事件期间消行数,不用全局总数直接判断。 - [ ] 关键代码整理:
- `TDrawScreen``src/source/TetrisRender.cpp`
- [ ] 单消惩罚:单行消除会额外生成 1 行垃圾。 - `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`
- [ ] Hold 冻结:暂时无法使用 Hold。 - `PlayReviveVideo``src/source/app/TetrisMedia.cpp`
- 适合度:高。 - [ ] 代码说明重点:
- 原因:`HoldCurrentPiece()` 可直接判断事件状态 - 界面绘制与游戏逻辑分离
- 鼠标点击通过矩形区域判断菜单和按钮。
- [ ] 幽灵落点失效:影子落点隐藏 - 键盘输入对应移动、旋转、暂停、重开、技能
- 适合度:高 - 背景图、图标、音乐、视频统一放在 `assets/`
- 原因:渲染处跳过落点绘制。 - [ ] 截图补充:
- 主菜单。
- [ ] 预览故障:Next 队列隐藏或随机显示假预览 - 经典模式游戏界面
- 适合度:中高 - 帮助页
- 原因:渲染和实际队列分离即可 - 音乐按钮或返回按钮
- 注意:假预览要清楚是事件效果,否则会像 bug。 - [ ] 测试记录:
- 键盘控制有效。
- [ ] 盲盒方块:下一个方块落下前不显示形状 - 鼠标点击菜单有效
- 适合度:高 - 背景音乐开关有效
- 原因:比假预览更简单,只隐藏下一块显示 - 从根目录运行时资源能正常加载
- [ ] 风险处理:
- [ ] 色彩错乱:方块颜色随机打乱,影响识别 - 报告中不要把 GDI+ 对象作为课程核心语法重点,重点讲过程式游戏逻辑和数组状态
- 适合度:高。
- 原因:只影响渲染颜色表映射。 ## 5. 阶段五:Rogue 创新模式与强化系统
- [ ] 强化过热:主动技能冷却或充能需求翻倍,持续 30 秒 - [ ] 功能设计文档:说明创新点来源、玩法目标和与经典模式的区别
- 适合度:中高。 - [ ] 关键代码整理:
- 原因:已有清屏炸弹、黑洞、空中换形等主动能力。 - `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`
- 原因:已有强化等级字段,可加临时禁用表。 - [ ] 代码说明重点:
- 注意:需要避免禁用核心 UI/基础能力导致解释困难 - Rogue 模式如何用 `PlayerStats` 结构体保存等级、经验、强化、技能次数
- 消行如何获得经验并触发升级选择。
- [ ] 贪婪试炼:期间消行奖励翻倍,但每 10 秒加 1 行垃圾 - 强化选项如何随机生成、选择并影响后续游戏
- 适合度:高 - 难度如何随时间推进
- 原因:得分/经验倍率和垃圾行都能复用。 - [ ] 截图补充:
- Rogue 模式游戏界面。
- [ ] 混乱祝福:下落速度提高,但消行奖励翻倍 - 升级三选一
- 适合度:高 - 双重选择或命运轮盘
- 原因:与当前 Rogue 风险收益强化风格一致 - 难度提升/底部封锁效果
- [ ] 测试记录:
- [ ] 危险长条:下一个必定是 I,但落地后生成 1 行垃圾 - 消行获得 EXP
- 适合度:高 - EXP 满后进入升级界面
- 原因:直接改 Next 队列并挂一个落地后副作用 - 选择强化后返回游戏
- 难度等级会随时间变化。
- [ ] 猎杀时刻:生成目标块,清除后给奖励,失败则加障碍。 - [ ] AI 对话记录整理:
- 适合度:中高 - 提示词主题:设计俄罗斯方块 Rogue 强化系统
- 原因:需要目标格类型,但玩法清晰,展示效果好 - 人工审查点:强化是否真的改变游戏状态,升级界面是否能返回主流程
- [ ] 极限压缩:场地高度降低,但所有消行计为双倍。 ## 6. 阶段六:主动技能、特殊方块和视觉特效
- 适合度:中高。
- 原因:已有底部封锁和奖励计算 - [ ] 功能设计文档:说明主动技能和特殊方块是创新功能,不影响基础玩法可运行
- 注意:与常驻危险等级封锁叠加时要设上限。 - [ ] 关键代码整理:
- `HoldCurrentPiece``src/source/rogue/TetrisRogue.cpp`
## 第二阶段:可做,但需要先补系统能力 - `UseScreenBomb``src/source/rogue/TetrisRogue.cpp`
- `UseBlackHole``src/source/rogue/TetrisRogue.cpp`
这些事件有价值,但依赖特殊格子、持续状态、倒计时 UI、事件优先级或更复杂的结算顺序。 - `UseAirReshape``src/source/rogue/TetrisRogue.cpp`
- `RollCurrentPieceSpecialFlags``src/source/rogue/TetrisRogue.cpp`
- [ ] 裂缝:随机列变成危险列,若 10 秒内没消除该列附近,会生成障碍。 - `ApplySpecialLandingEffects``src/source/logic/TetrisPieceEffects.cpp`
- 依赖:危险列标记、倒计时、附近消行判定。 - `ApplyRainbowLandingEffect``src/source/logic/TetrisPieceEffects.cpp`
- `TriggerScreenBomb``src/source/rogue/TetrisRogue.cpp`
- [ ] 污染区:随机 3x3 区域被污染,污染格消除后会扩散一次。 - `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`
- 风险:要定义碎片是否立即固定、是否触发消行。 - [ ] 代码说明重点:
- 技能按键如何触发对应函数。
- [ ] 磁力干扰:方块靠近障碍块时会被吸附,加速锁定 - 技能如何修改棋盘数组
- 依赖:障碍格类型、锁定延迟或特殊横移规则 - 特殊方块落地后如何触发清除、变色、爆炸、激光等效果
- 风险:玩家可读性较差 - 视觉特效只负责显示,不应破坏核心棋盘数据
- [ ] 截图补充:
- [ ] 巨型块:下一个方块变成五格或六格异形块 - 备用仓
- 依赖:扩展方块形状系统 - 清屏炸弹
- 风险:现有 `bricks[7][4][4][4]` 固定为 7 种,需要重构或另建临时形状 - 黑洞奇点
- 空中换形。
- [ ] 爆裂块:下一个方块落地后随机炸掉相邻格子,可能好也可能坏 - 爆破/激光/彩虹等特殊方块效果
- 依赖:落地后局部爆破和反馈。 - [ ] 测试记录:
- 备注:已有爆破核心,可复用特效和清除函数 - 技能次数不足时不能使用
- 技能使用后棋盘变化正确。
- [ ] 锁链块:方块落地后被锁住,只有相邻消行才能解除 - 特殊方块效果不会造成数组越界
- 依赖:锁链格类型、相邻消行判定 - 特效结束后游戏仍可继续
- 风险:若锁住后不能正常消行,规则会很绕。
## 7. 实验报告正文 TODO
- [ ] 连击试炼:规定时间内保持连击,断连则触发惩罚。
- 依赖:连击生命周期定义。 - [ ] 封面信息:
- 备注:已有 `comboChain`,但需要确认何时断连 - 项目名称:使用大模型辅助开发俄罗斯方块程序
- 小组成员、学号、班级、日期。
- [ ] 精准清理:只有消除指定发光行才算有效消行。 - [ ] 摘要:
- 依赖:目标行标记、奖励过滤 - 简述完成了经典俄罗斯方块和 Rogue 创新模式
- 风险:需要处理非目标行消除是否仍清棋盘 - 强调使用 C++、Win32 API、数组、结构体、函数组织
- [ ] 需求功能设计:
- [ ] 过载清除:消行后不会立即消失,而是延迟 2 秒,期间仍占位 - 按至少 6 个阶段写,每阶段包含目标、功能点、涉及文件
- 依赖:延迟消行队列 - 每阶段最多聚焦一个主要功能主题
- 风险:会影响核心俄罗斯方块节奏,改动较大。 - [ ] 功能实现:
- 每阶段放关键代码截图。
- [ ] 腐蚀行:某些行如果长期不被消除,会逐渐变成石头 - 每阶段写代码说明
- 依赖:按行计时、石头格类型 - 每阶段放游戏运行截图
- [ ] AI 辅助编程体验反思:
- [ ] 献祭规则:每消 3 行会摧毁一个随机强化效果,持续短时间 - 写明大模型做得好的地方:快速生成框架、补全重复逻辑、解释 Win32 消息流程、提供调试思路
- 依赖:强化临时禁用/降级机制 - 写明大模型表现不好的地方:容易生成过复杂代码、可能使用超出课程范围的语法、边界条件不完整、变量命名可能不符合原框架
- 风险:永久摧毁太重,建议先做短时封印 - 写明改进方法:拆小任务、明确限制语法、每次只让模型生成一个函数、人工检查数组下标、编译运行验证
- 注意表述成“多轮 LLM 对话迭代生成”,不要写成一次性 vibe coding。
- [ ] 迷雾:只显示当前方块附近区域。 - [ ] 成员分工表:
- 依赖:渲染遮罩 - 提示词工程师:拆分需求、编写和迭代提示词
- 风险:实现不难,但视觉遮挡强,需要短时使用 - 代码审计员:检查语法限制、数组越界、全局状态和函数注释
- 功能测试员:运行游戏、记录 bug、截图。
- [ ] 倒计时遮蔽:场地部分区域被 UI 遮挡,数秒后消失 - 报告撰稿人:整理阶段文档、代码截图、反思和分工
- 依赖:渲染遮罩和事件 UI - 现场汇报人:演示程序并回答问题
- 风险:可能被认为是不公平遮挡。 - [ ] Bug 记录:
- 至少整理 3 个 bug,每个包含“问题、原因、修复过程、验证结果”。
- [ ] 假警报:显示即将生成垃圾行的预警,但部分是假的。 - [ ] 总结:
- 依赖:预警 UI 和真假队列 - 说明最终实现的功能
- 风险:需要先有稳定的真实预警系统 - 说明仍可改进的地方,例如代码规模较大、部分界面资源依赖本地文件、复杂扩展功能需要更多测试
- [ ] 诅咒回响:最近一次选择的强化产生副作用。 ## 8. 答辩准备 TODO
- 依赖:记录最近强化、为每类强化配置副作用。
- 风险:内容量较大 - [ ] 每位组员至少熟悉一个源码模块,不能只由一人理解
- [ ] 准备 5 分钟演示路线:
- [ ] 保险失效:复活、护盾、防死类强化暂时不可用 - 主菜单
- 依赖:临时禁用强化系统 - 经典模式移动、旋转、消行
- 备注:当前已有复活/最后一搏类能力,适合在禁用系统完成后做 - Rogue 模式升级
- 主动技能。
- [ ] 超载窗口:所有强化效果增强,但结束后生成大量垃圾行 - 特殊方块或视频复活
- 依赖:强化倍率覆盖层。 - [ ] 准备常见问题回答:
- 风险:需要为每个强化定义“增强”含义。 - 方块形状如何存储?
- 如何判断碰撞?
- [ ] 债务事件:立刻获得奖励,但未来 60 秒内难度提高。 - 如何消行?
- 依赖:奖励发放、难度临时增益。 - 如何实现升级选择?
- 备注:适合作为事件选择,而不是无条件负面事件。 - 如何保证没有使用自定义 class
- AI 生成代码后做了哪些人工审查?
- [ ] 不稳定炸弹:给一个炸弹块,能清障碍,但倒计时结束会爆坏场地。 - [ ] 准备现场编译:
- 依赖:临时特殊方块、倒计时、坏爆炸规则。 - 命令:`.\build-mingw.ps1`
- 运行:`.\build-mingw.ps1 -Run`
- [ ] 交易事件:牺牲一个强化,换取清屏/降难度 - 如果提示 `Tetris.exe: Permission denied`,先关闭正在运行的游戏窗口
- 依赖:事件选择 UI、强化移除或临时禁用。
## 9. 四人专项分工规划
- [ ] 祝福陷阱:获得临时强力效果,结束后触发一次负面事件。
- 依赖:事件链和延迟触发 > 原则:四个人各有一个主要专项,同时都要理解自己负责模块对应的代码和报告内容;现场答辩时不能只由一个人解释全部代码
## 第三阶段:攻击型随机事件,可选做 ### 成员 A:需求拆分与报告主线负责人
这些事件原本偏“敌人攻击”风格,但本项目不做 Boss 机制。若保留,只作为普通随机事件触发,不做血量、阶段、护盾和敌人 UI - [ ] 专项任务:负责实验报告整体结构、阶段划分和文字主线
- [ ] 负责内容:
- [ ] 炮击:指定列被标记,数秒后生成障碍块 - 将项目整理成 6 个阶段:窗口框架、基础移动、消行得分、界面交互、Rogue 强化、主动技能与特效
- 适合度:中高 - 编写每个阶段的“需求功能设计”
- 备注:可以和裂缝共用“列预警 + 延迟生成障碍”的逻辑 - 整理摘要、项目背景、总体架构、总结与不足
- 保证报告符合截图要求:不少于五个阶段、每阶段有功能设计文档。
- [ ] 毒液喷洒:随机格变污染块,消行时才清除。 - [ ] 重点熟悉代码:
- 适合度:中。 - `src/source/Tetris.cpp`
- 备注:依赖污染格系统。 - `src/include/Tetris.h`
- `src/source/TetrisLogic.cpp`
- [ ] 目标块入侵:场上出现几个需要消行击破的目标块。 - [ ] 最终交付:
- 适合度:中 - 报告目录结构
- 备注:可复用猎杀时刻目标块 - 6 个阶段的功能设计文字
- 项目总体介绍和总结。
- [ ] 护盾阶段:不做。
- 原因:依赖 Boss 血量、阶段和伤害规则,已经超出当前项目方向。 ### 成员 B:核心逻辑与代码说明负责人
- [ ] 反击规则:玩家每次消行,额外生成 1 行垃圾 - [ ] 专项任务:负责基础俄罗斯方块核心逻辑的代码审查和代码说明
- 适合度:中。 - [ ] 负责内容:
- 备注:可作为短时高压事件,但要避免和“单消惩罚”重复 - 解释棋盘数组 `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:界面交互、资源与运行截图负责人
- [ ] 设计并新增 `RogueEventState`:当前事件、剩余时间、剩余方块数、事件参数、临时倍率、禁用标记 - [ ] 专项任务:负责程序运行、界面截图、资源加载和交互测试
- [ ] 新增事件调度入口:只在 Rogue 模式中按时间/危险等级触发,避免经典模式受影响。 - [ ] 负责内容:
- [ ] 新增事件提示 UI:事件名、剩余秒数、简短效果;先用右侧反馈面板,不做复杂界面 - 编译并运行项目,记录构建结果
- [ ] 先完成 6 个低风险事件作为 MVP: - 截取主菜单、经典模式、帮助页、Rogue 升级、主动技能、特殊方块等运行截图。
- [ ] 底部随机升起 - 测试键盘输入、鼠标点击、音乐开关、返回按钮、视频复活。
- [ ] 强风 - 整理运行环境和现场演示路线。
- [ ]力紊乱 - [ ]点熟悉代码:
- [ ] 镜像操作 - `src/source/render/TetrisRenderMain.cpp`
- [ ] 长条枯竭 - `src/source/render/TetrisRenderAssets.cpp`
- [ ] 高压 - `src/source/app/TetrisInput.cpp`
- [ ] 再完成 4 个能体现肉鸽取舍的混合事件: - `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`
- [ ] 护盾阶段:需要 Boss 血量和阶段机制,当前项目明确不做。 - `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>
-147
View File
@@ -1,147 +0,0 @@
# VS Code 运行说明
本项目推荐使用 `VS Code + MinGW-w64 + PowerShell` 构建和调试。
## 1. 环境准备
需要安装:
- Visual Studio Code
- Microsoft C/C++ 扩展
- MinGW-w64
- PowerShell
MinGW 中至少需要:
- `g++.exe`
- `windres.exe`
- `gdb.exe`,仅调试时需要
## 2. 打开项目
请在 VS Code 中打开项目根目录,也就是包含以下文件和目录的位置:
```text
build-mingw.ps1
src/
assets/
.vscode/
README.md
```
不要只打开 `src/` 子目录,否则 VS Code 任务和调试配置无法正常工作。
## 3. 使用 VS Code 任务构建
按:
```text
Ctrl + Shift + B
```
默认会执行项目中的 MinGW 构建任务。
也可以打开命令面板,选择:
```text
Tasks: Run Task
```
然后运行:
- `build Tetris MinGW`
- `run Tetris MinGW`
## 4. 使用命令行构建
在 VS Code 终端中进入项目根目录,执行:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\build-mingw.ps1
```
构建后直接运行:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\build-mingw.ps1 -Run
```
## 5. 调试
`F5`,选择:
```text
Debug Tetris MinGW
```
调试配置会先构建项目,再启动:
```text
.vscode-build\mingw\Tetris.exe
```
如果提示找不到 `gdb.exe`,说明 MinGW 的调试器没有安装或没有加入 `PATH`
## 6. 构建输出
最终程序:
```text
.vscode-build\mingw\Tetris.exe
```
构建中间文件:
```text
.vscode-build\mingw\Tetris.utf8.rc
.vscode-build\mingw\Tetris.res.o
```
这些中间文件不需要手动维护。
## 7. 资源文件
项目运行依赖:
```text
assets/icons/
assets/images/
assets/audio/
assets/video/
```
如果只移动 `Tetris.exe` 而不带 `assets/`,会影响背景图、音乐和视频复活功能。
## 8. 常见问题
### 找不到 `g++.exe`
处理方式:
- 将 MinGW 的 `bin` 目录加入系统 `PATH`
- 或将 MinGW 安装到 `C:\mingw64\bin\`
### 找不到 `windres.exe`
资源文件无法编译,图标和菜单资源可能缺失。请检查 MinGW 安装是否完整。
### `Tetris.exe: Permission denied`
说明程序正在运行,构建时无法覆盖旧 exe。
处理方式:
1. 关闭游戏窗口
2. 重新构建
### 鼠标点击、按钮或界面不是最新版
通常是因为构建失败后仍在运行旧 exe。请确认构建命令成功完成。
## 9. 推荐运行流程
1. 打开项目根目录
2. 关闭旧的游戏窗口
3. 执行构建
4. 运行 `.vscode-build\mingw\Tetris.exe`
5. 如果要调试,按 `F5`
Binary file not shown.

Before

Width:  |  Height:  |  Size: 572 KiB

After

Width:  |  Height:  |  Size: 722 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 722 KiB

After

Width:  |  Height:  |  Size: 84 KiB

+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
}
+59
View File
@@ -11,6 +11,7 @@
#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;
@@ -24,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;
@@ -43,6 +53,12 @@ struct HelpState
int currentPage; int currentPage;
}; };
/**
* @brief 记录经典模式和 Rogue 模式的分数、等级、强化与临时状态。
*
* 课程要求不使用 class,因此所有与玩家成长有关的数据都集中放在结构体字段中,
* 由逻辑层函数按过程式方式读取和修改。
*/
struct PlayerStats struct PlayerStats
{ {
int score; int score;
@@ -115,6 +131,9 @@ struct PlayerStats
int pieceTuningLevels[7]; int pieceTuningLevels[7];
}; };
/**
* @brief 升级界面中已经生成并显示给玩家的一个候选强化。
*/
struct UpgradeOption struct UpgradeOption
{ {
int id; int id;
@@ -127,6 +146,9 @@ struct UpgradeOption
const TCHAR* description; const TCHAR* description;
}; };
/**
* @brief 强化池中的基础配置项,用于生成升级界面候选。
*/
struct UpgradeEntry struct UpgradeEntry
{ {
int id; int id;
@@ -138,6 +160,9 @@ struct UpgradeEntry
const TCHAR* description; const TCHAR* description;
}; };
/**
* @brief Rogue 升级选择界面的临时 UI 状态。
*/
struct UpgradeUiState struct UpgradeUiState
{ {
int selectedIndex; int selectedIndex;
@@ -150,6 +175,9 @@ struct UpgradeUiState
UpgradeOption options[6]; UpgradeOption options[6];
}; };
/**
* @brief 右侧战斗日志或提示条的显示状态。
*/
struct FeedbackState struct FeedbackState
{ {
int visibleTicks; int visibleTicks;
@@ -157,6 +185,9 @@ struct FeedbackState
TCHAR detail[128]; TCHAR detail[128];
}; };
/**
* @brief 标准消行动画状态。
*/
struct ClearEffectState struct ClearEffectState
{ {
int ticks; int ticks;
@@ -165,6 +196,9 @@ struct ClearEffectState
int rows[8]; int rows[8];
}; };
/**
* @brief 棋盘上浮动文字特效的单个实例。
*/
struct FloatingTextEffect struct FloatingTextEffect
{ {
int ticks; int ticks;
@@ -175,6 +209,9 @@ struct FloatingTextEffect
COLORREF color; COLORREF color;
}; };
/**
* @brief 棋盘粒子特效的单个实例。
*/
struct ParticleEffect struct ParticleEffect
{ {
int ticks; int ticks;
@@ -187,6 +224,9 @@ struct ParticleEffect
COLORREF color; COLORREF color;
}; };
/**
* @brief 被清除格子的短时闪烁高亮状态。
*/
struct CellFlashEffect struct CellFlashEffect
{ {
int ticks; int ticks;
@@ -196,6 +236,9 @@ struct CellFlashEffect
COLORREF color; COLORREF color;
}; };
/**
* @brief 固定方块受重力下落时的残影轨迹状态。
*/
struct GravityFallEffect struct GravityFallEffect
{ {
int ticks; int ticks;
@@ -206,6 +249,9 @@ struct GravityFallEffect
int cellValue; int cellValue;
}; };
/**
* @brief 当前应用所在的大界面。
*/
enum ScreenState enum ScreenState
{ {
SCREEN_MENU = 0, SCREEN_MENU = 0,
@@ -214,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,
@@ -227,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;
@@ -329,6 +382,12 @@ void DeleteOneLine(int number);
*/ */
int DeleteLines(); int DeleteLines();
/**
* @brief 判断当前游戏是否已经结束。
* @return 游戏结束返回 true,否则返回 false。
*/
bool GameOver();
/** /**
* @brief 计算当前活动方块的预测落点。 * @brief 计算当前活动方块的预测落点。
*/ */
+6
View File
@@ -15,6 +15,12 @@ constexpr int GAME_TIMER_INTERVAL = 500;
constexpr int EFFECT_TIMER_INTERVAL = 16; constexpr int EFFECT_TIMER_INTERVAL = 16;
constexpr int CREDIT_TIMER_INTERVAL = 5; constexpr int CREDIT_TIMER_INTERVAL = 5;
/**
* @brief 当前窗口缩放后的布局参数。
*
* 输入命中区域和渲染坐标必须使用同一套缩放参数,才能保证鼠标点击位置
* 与屏幕上看到的按钮、卡片位置一致。
*/
struct LayoutMetrics struct LayoutMetrics
{ {
int scale; int scale;
+2
View File
@@ -8,6 +8,8 @@
#include "stdafx.h" #include "stdafx.h"
#include <string> #include <string>
// 资源路径函数同时服务图片、音频和视频加载,调用方只传相对路径。
/** /**
* @brief 根据程序所在目录拼出项目资源文件的绝对路径。 * @brief 根据程序所在目录拼出项目资源文件的绝对路径。
* @param relativePath 相对于项目根目录的资源路径。 * @param relativePath 相对于项目根目录的资源路径。
+51
View File
@@ -14,6 +14,8 @@ extern int pendingLineClearEffectRows[8];
extern int pendingLineClearEffectRowCount; extern int pendingLineClearEffectRowCount;
extern int pendingLineClearEffectLineCount; extern int pendingLineClearEffectLineCount;
// Internal 头文件只暴露跨 cpp 文件共享的辅助函数,外部窗口层仍通过 Tetris.h 调用公开接口。
/** /**
* @brief 计算指定方块在棋盘顶部的统一生成位置。 * @brief 计算指定方块在棋盘顶部的统一生成位置。
* @param brickType 方块类型编号。 * @param brickType 方块类型编号。
@@ -21,6 +23,55 @@ extern int pendingLineClearEffectLineCount;
*/ */
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 模式使用的玩家统计数据。 * @brief 重置经典或 Rogue 模式使用的玩家统计数据。
* @param stats 需要重置的统计结构。 * @param stats 需要重置的统计结构。
+9
View File
@@ -9,6 +9,8 @@
#include <objidl.h> #include <objidl.h>
#include <gdiplus.h> #include <gdiplus.h>
// 本内部头文件只给渲染拆分模块使用,外部代码仍通过 TDrawScreen 调用绘制入口。
/** /**
* @brief 加载并缓存主背景图片。 * @brief 加载并缓存主背景图片。
* @return 成功时返回缓存位图指针,失败时返回 nullptr。 * @return 成功时返回缓存位图指针,失败时返回 nullptr。
@@ -21,3 +23,10 @@ Gdiplus::Bitmap* LoadBackgroundImage();
* @return 成功时返回缓存位图指针,失败或越界时返回 nullptr。 * @return 成功时返回缓存位图指针,失败或越界时返回 nullptr。
*/ */
Gdiplus::Bitmap* LoadCreditImage(int index); Gdiplus::Bitmap* LoadCreditImage(int index);
/**
* @brief 绘制完整游戏界面,供 TDrawScreen 总入口调用。
* @param hdc 目标绘图设备上下文。
* @param hWnd 当前窗口句柄。
*/
void RenderFullScreen(HDC hdc, HWND hWnd);
+3 -4
View File
@@ -2,13 +2,12 @@
/** /**
* @file resource.h * @file resource.h
* @brief 定义菜单、图标、对话框和命令等 Windows 资源编号。 * @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
+3 -2
View File
@@ -7,11 +7,12 @@
#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: 在此处引用程序需要的其他头文件 // 其他模块各自包含自己的业务头文件,避免预编译头承担过多项目依赖。
+3 -3
View File
@@ -5,9 +5,9 @@
* @brief 设置 Windows SDK 目标平台版本,供 Win32 头文件选择可用 API。 * @brief 设置 Windows SDK 目标平台版本,供 Win32 头文件选择可用 API。
*/ */
// 包括 SDKDDKVer.h 将定义可用的最高版本 Windows 平台。 // 包括 SDKDDKVer.h 将定义可用的最高版本 Windows 平台
// 如果要为以前的 Windows 平台生成应用程序,请包括 WinSDKVer.h并将 // 若课程演示环境需要兼容更旧 Windows,可在这里先包含 WinSDKVer.h
// WIN32_WINNT 宏设置为要支持的平台,然后再包括 SDKDDKVer.h // 再设置 WIN32_WINNT;当前项目直接使用 SDK 默认最高版本
#include <SDKDDKVer.h> #include <SDKDDKVer.h>
Binary file not shown.
+9
View File
@@ -194,6 +194,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
break; break;
case WM_COMMAND: case WM_COMMAND:
{ {
// 处理资源菜单命令;自绘菜单的鼠标和键盘输入不走这里。
int wmId = LOWORD(wParam); int wmId = LOWORD(wParam);
switch (wmId) switch (wmId)
@@ -214,15 +215,19 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
} }
break; break;
case WM_CREDIT_TICK: case WM_CREDIT_TICK:
// 多媒体定时器线程只投递消息,真正刷新仍回到窗口线程执行。
HandleCreditTick(hWnd); HandleCreditTick(hWnd);
break; break;
case WM_TIMER: case WM_TIMER:
// 所有窗口定时器统一交给应用层计时器模块分发。
HandleTimerMessage(hWnd, wParam); HandleTimerMessage(hWnd, wParam);
break; break;
case WM_SIZE: case WM_SIZE:
// 窗口尺寸变化后重绘,布局函数会按新客户区重新计算缩放。
InvalidateRect(hWnd, nullptr, FALSE); InvalidateRect(hWnd, nullptr, FALSE);
break; break;
case WM_LBUTTONUP: case WM_LBUTTONUP:
// 输入模块未消费的鼠标消息继续交给 Win32 默认处理。
if (!HandleMouseClick(hWnd, lParam)) if (!HandleMouseClick(hWnd, lParam))
{ {
return DefWindowProc(hWnd, message, wParam, lParam); return DefWindowProc(hWnd, message, wParam, lParam);
@@ -235,6 +240,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
HandleKeyDown(hWnd, wParam); HandleKeyDown(hWnd, wParam);
break; break;
case WM_ERASEBKGND: case WM_ERASEBKGND:
// 背景由双缓冲完整绘制,阻止系统擦背景可以减少闪烁。
return 1; return 1;
case WM_PAINT: case WM_PAINT:
{ {
@@ -251,6 +257,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
clientRect.bottom - clientRect.top); clientRect.bottom - clientRect.top);
HBITMAP oldBitmap = (HBITMAP)SelectObject(memDC, memBitmap); HBITMAP oldBitmap = (HBITMAP)SelectObject(memDC, memBitmap);
// 所有自绘内容先画到内存位图,再一次性复制到窗口。
TDrawScreen(memDC, hWnd); TDrawScreen(memDC, hWnd);
BitBlt( BitBlt(
hdc, hdc,
@@ -296,10 +303,12 @@ INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
switch (message) switch (message)
{ {
case WM_INITDIALOG: case WM_INITDIALOG:
// 资源模板标题是英文,这里在初始化时替换成中文标题。
SetWindowText(hDlg, _T("\u5173\u4e8e\u4fc4\u7f57\u65af\u65b9\u5757")); SetWindowText(hDlg, _T("\u5173\u4e8e\u4fc4\u7f57\u65af\u65b9\u5757"));
return (INT_PTR)TRUE; return (INT_PTR)TRUE;
case WM_COMMAND: case WM_COMMAND:
// 关于框只需要响应确定和取消,其他命令交回默认流程。
if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL) if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
{ {
EndDialog(hDlg, LOWORD(wParam)); EndDialog(hDlg, LOWORD(wParam));
+33 -235
View File
@@ -50,6 +50,7 @@ bool pendingChainBombFollowup = false;
int bricks[7][4][4][4] = int bricks[7][4][4][4] =
{ {
// 方块形状表:7 种方块、每种 4 个旋转状态、每个状态使用 4x4 矩阵描述。
{ {
{{0, 0, 0, 0}, {0, 0, 0, 0}, {1, 1, 1, 1}, {0, 0, 0, 0}}, {{0, 0, 0, 0}, {0, 0, 0, 0}, {1, 1, 1, 1}, {0, 0, 0, 0}},
{{0, 0, 1, 0}, {0, 0, 1, 0}, {0, 0, 1, 0}, {0, 0, 1, 0}}, {{0, 0, 1, 0}, {0, 0, 1, 0}, {0, 0, 1, 0}, {0, 0, 1, 0}},
@@ -96,6 +97,7 @@ int bricks[7][4][4][4] =
COLORREF BrickColor[7] = COLORREF BrickColor[7] =
{ {
// 渲染层按方块编号取色;数组顺序必须与 bricks 中的类型编号一致。
RGB(244, 144, 165), RGB(244, 144, 165),
RGB(255, 181, 197), RGB(255, 181, 197),
RGB(170, 215, 255), RGB(170, 215, 255),
@@ -105,77 +107,6 @@ COLORREF BrickColor[7] =
RGB(197, 170, 255) RGB(197, 170, 255)
}; };
/**
* @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 判断当前方块是否可以继续向下移动。 * @brief 判断当前方块是否可以继续向下移动。
* *
@@ -341,6 +272,7 @@ void MoveRight()
*/ */
void Rotate() void Rotate()
{ {
// 第一阶段:直接尝试原地旋转。
int nextState = (state + 1) % 4; int nextState = (state + 1) % 4;
if (IsPiecePlacementValid(type, nextState, point)) if (IsPiecePlacementValid(type, nextState, point))
{ {
@@ -350,6 +282,7 @@ void Rotate()
if (currentMode == MODE_ROGUE && rogueStats.perfectRotateLevel > 0) if (currentMode == MODE_ROGUE && rogueStats.perfectRotateLevel > 0)
{ {
// 第二阶段:Rogue 完美旋转解锁后,尝试左右各一格的墙踢修正。
if (TryRotateWithOffset(nextState, -1)) if (TryRotateWithOffset(nextState, -1))
{ {
state = nextState; state = nextState;
@@ -393,6 +326,7 @@ void DropDown()
*/ */
void Fixing() void Fixing()
{ {
// 第一阶段:收集落地格子,并把可见区域内的格子写入工作区。
bool overflowTop = false; bool overflowTop = false;
Point fixedCells[4] = {}; Point fixedCells[4] = {};
int fixedCellCount = 0; int fixedCellCount = 0;
@@ -400,83 +334,18 @@ void Fixing()
int explosiveCellCount = 0; int explosiveCellCount = 0;
pendingChainBombFollowup = false; pendingChainBombFollowup = false;
for (int i = 0; i < 4; i++) CollectAndWriteFixedCells(overflowTop, fixedCells, fixedCellCount, explosiveCells, explosiveCellCount);
{
for (int j = 0; j < 4; j++)
{
if (bricks[type][state][i][j] != 0)
{
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++;
}
}
}
}
}
// 第二阶段:彩虹方块先按落地中心行处理染色与清除。
ApplyRainbowLandingEffect(overflowTop, fixedCells, fixedCellCount); ApplyRainbowLandingEffect(overflowTop, fixedCells, fixedCellCount);
if (overflowTop) // 第三阶段:统一处理顶部溢出,可能触发最后一搏或直接游戏结束。
if (!ResolveFixingOverflow(overflowTop))
{ {
if (currentMode == MODE_ROGUE && rogueStats.terminalClearLevel > 0 && rogueStats.lastChanceCount > 0 && rogueStats.screenBombCount > 0) return;
{
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);
}
else if (currentMode == MODE_ROGUE && rogueStats.lastChanceCount > 0)
{
rogueStats.lastChanceCount--;
for (int i = 0; i < 3; i++)
{
DeleteOneLine(GetRoguePlayableHeight() - 1);
}
SetFeedbackMessage(
_T("最后一搏"),
_T("底部 3 行被清除,战局得以延续。"),
14);
}
else
{
gameOverFlag = true;
return;
}
} }
// 第四阶段:结算爆破、激光、十字和稳定结构等普通特殊落地效果。
ApplySpecialLandingEffects(fixedCells, fixedCellCount, explosiveCells, explosiveCellCount); ApplySpecialLandingEffects(fixedCells, fixedCellCount, explosiveCells, explosiveCellCount);
if (currentMode == MODE_ROGUE) if (currentMode == MODE_ROGUE)
@@ -484,15 +353,8 @@ void Fixing()
currentFallInterval = GetRogueFallInterval(); currentFallInterval = GetRogueFallInterval();
} }
// 生成下一活动方块 // 第五阶段:刷新下一活动方块,开始新的下落回合。
type = ConsumeNextType(); SpawnNextFallingPiece();
nType = nextTypes[0];
state = 0;
holdUsedThisTurn = false;
RollCurrentPieceSpecialFlags(true);
point = GetSpawnPoint(type);
target = point;
ComputeTarget();
} }
/** /**
@@ -532,101 +394,33 @@ void DeleteOneLine(int number)
*/ */
int DeleteLines() int DeleteLines()
{ {
int clearedLines = 0;
int clearedRows[8] = {}; int clearedRows[8] = {};
int clearedRowCount = 0; int clearedRowCount = 0;
int clearedLines = ScanAndDeleteFullLines(clearedRows, clearedRowCount);
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++;
}
}
// 消行数量先进入玩法结算,再根据是否正在升级决定动画立即播放还是暂存。 // 消行数量先进入玩法结算,再根据是否正在升级决定动画立即播放还是暂存。
ApplyLineClearResult(clearedLines); ApplyLineClearResult(clearedLines);
if (currentScreen == SCREEN_UPGRADE) DispatchLineClearEffect(clearedRows, clearedRowCount, clearedLines);
{
QueueLineClearEffect(clearedRows, clearedRowCount, clearedLines);
}
else
{
TriggerLineClearEffect(clearedRows, clearedRowCount, clearedLines);
}
// 连环炸弹的追加爆破只在爆破方块导致后续消行时触发一次。 // 连环炸弹的追加爆破只在爆破方块导致后续消行时触发一次。
if (pendingChainBombFollowup && clearedLines > 0) ResolveChainBombFollowup(clearedLines);
{
pendingChainBombFollowup = false;
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);
}
}
else
{
pendingChainBombFollowup = false;
}
return clearedLines; return clearedLines;
} }
/**
* @brief 判断当前游戏是否已经结束。
*
* 老师作业框架中保留该函数名,当前项目内部的结束状态统一记录在
* gameOverFlag 中,因此这里直接返回该标记,避免改变原有流程。
*
* @return 游戏结束返回 true,否则返回 false。
*/
bool GameOver()
{
return gameOverFlag;
}
/** /**
* @brief 计算当前活动方块的预测落点位置。 * @brief 计算当前活动方块的预测落点位置。
* *
@@ -662,6 +456,7 @@ void ComputeTarget()
*/ */
void Restart() void Restart()
{ {
// 第一阶段:清空棋盘数组,移除上一局所有固定方块。
for (int i = 0; i < nGameHeight; i++) for (int i = 0; i < nGameHeight; i++)
{ {
for (int j = 0; j < nGameWidth; j++) for (int j = 0; j < nGameWidth; j++)
@@ -670,12 +465,14 @@ void Restart()
} }
} }
// 第二阶段:恢复基本游戏标志和默认下落速度。
gameOverFlag = false; gameOverFlag = false;
suspendFlag = false; suspendFlag = false;
targetFlag = true; targetFlag = true;
reviveAvailable = true; reviveAvailable = true;
currentFallInterval = 500; currentFallInterval = 500;
// 第三阶段:重置两种模式的统计、升级 UI、反馈和所有视觉特效。
ResetPlayerStats(classicStats, false); ResetPlayerStats(classicStats, false);
ResetPlayerStats(rogueStats, true); ResetPlayerStats(rogueStats, true);
ResetUpgradeUiState(); ResetUpgradeUiState();
@@ -692,6 +489,7 @@ void Restart()
RollCurrentPieceSpecialFlags(false); RollCurrentPieceSpecialFlags(false);
tScore = 0; tScore = 0;
// 第四阶段:初始化下一方块队列,并生成当前活动方块。
ResetNextQueue(); ResetNextQueue();
type = ConsumeNextType(); type = ConsumeNextType();
nType = nextTypes[0]; nType = nextTypes[0];
File diff suppressed because it is too large Load Diff
+314 -178
View File
@@ -417,6 +417,88 @@ static bool HandleMenuKey(HWND hWnd, WPARAM key)
return true; 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 处理帮助和致谢页键盘导航。 * @brief 处理帮助和致谢页键盘导航。
* @param hWnd 当前窗口句柄。 * @param hWnd 当前窗口句柄。
@@ -438,11 +520,7 @@ static bool HandleRulesKey(HWND hWnd, WPARAM key)
case 'A': case 'A':
if (helpState.currentPage == 0) if (helpState.currentPage == 0)
{ {
helpState.selectedIndex--; MoveHelpHomeSelection(-1);
if (helpState.selectedIndex < 0)
{
helpState.selectedIndex = helpState.optionCount - 1;
}
InvalidateRect(hWnd, nullptr, FALSE); InvalidateRect(hWnd, nullptr, FALSE);
} }
else if (helpState.currentPage == 4) else if (helpState.currentPage == 4)
@@ -452,15 +530,7 @@ static bool HandleRulesKey(HWND hWnd, WPARAM key)
} }
else if (helpState.currentPage == 5) else if (helpState.currentPage == 5)
{ {
helpState.selectedIndex--; MoveSkillDemoSelection(-1);
if (helpState.selectedIndex < 0)
{
helpState.selectedIndex = GetRogueSkillDemoCount() - 1;
}
if (helpState.selectedIndex * 68 < helpScrollOffset)
{
helpScrollOffset = helpState.selectedIndex * 68;
}
InvalidateRect(hWnd, nullptr, FALSE); InvalidateRect(hWnd, nullptr, FALSE);
} }
break; break;
@@ -470,11 +540,7 @@ static bool HandleRulesKey(HWND hWnd, WPARAM key)
case 'D': case 'D':
if (helpState.currentPage == 0) if (helpState.currentPage == 0)
{ {
helpState.selectedIndex++; MoveHelpHomeSelection(1);
if (helpState.selectedIndex >= helpState.optionCount)
{
helpState.selectedIndex = 0;
}
InvalidateRect(hWnd, nullptr, FALSE); InvalidateRect(hWnd, nullptr, FALSE);
} }
else if (helpState.currentPage == 4) else if (helpState.currentPage == 4)
@@ -484,16 +550,7 @@ static bool HandleRulesKey(HWND hWnd, WPARAM key)
} }
else if (helpState.currentPage == 5) else if (helpState.currentPage == 5)
{ {
helpState.selectedIndex++; MoveSkillDemoSelection(1);
if (helpState.selectedIndex >= GetRogueSkillDemoCount())
{
helpState.selectedIndex = 0;
helpScrollOffset = 0;
}
else if (helpState.selectedIndex * 68 > helpScrollOffset + 360)
{
helpScrollOffset = helpState.selectedIndex * 68 - 360;
}
InvalidateRect(hWnd, nullptr, FALSE); InvalidateRect(hWnd, nullptr, FALSE);
} }
break; break;
@@ -501,17 +558,7 @@ static bool HandleRulesKey(HWND hWnd, WPARAM key)
case VK_SPACE: case VK_SPACE:
if (helpState.currentPage == 0) if (helpState.currentPage == 0)
{ {
if (helpState.selectedIndex == 3) ActivateHelpSelection();
{
helpState.currentPage = 5;
helpState.selectedIndex = 0;
helpScrollOffset = 0;
}
else
{
helpState.currentPage = helpState.selectedIndex + 1;
helpScrollOffset = 0;
}
InvalidateRect(hWnd, nullptr, FALSE); InvalidateRect(hWnd, nullptr, FALSE);
} }
else if (helpState.currentPage == 5) else if (helpState.currentPage == 5)
@@ -524,24 +571,9 @@ static bool HandleRulesKey(HWND hWnd, WPARAM key)
case VK_ESCAPE: case VK_ESCAPE:
case VK_BACK: case VK_BACK:
case 'M': case 'M':
{ LeaveRulesPage();
int previousPage = helpState.currentPage;
if (helpState.currentPage == 0)
{
ReturnToMainMenu();
}
else
{
helpState.currentPage = 0;
if (previousPage == 4 || previousPage == 5)
{
helpState.selectedIndex = 3;
}
helpScrollOffset = 0;
}
InvalidateRect(hWnd, nullptr, FALSE); InvalidateRect(hWnd, nullptr, FALSE);
break; break;
}
default: default:
break; break;
} }
@@ -549,6 +581,92 @@ static bool HandleRulesKey(HWND hWnd, WPARAM key)
return true; 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 处理升级选择界面键盘导航。 * @brief 处理升级选择界面键盘导航。
* @param hWnd 当前窗口句柄。 * @param hWnd 当前窗口句柄。
@@ -562,64 +680,26 @@ static bool HandleUpgradeKey(HWND hWnd, WPARAM key)
return false; return false;
} }
int upgradeColumnCount = upgradeUiState.optionCount <= 3 ? upgradeUiState.optionCount : 3;
if (upgradeColumnCount < 1)
{
upgradeColumnCount = 1;
}
switch (key) switch (key)
{ {
case VK_LEFT: case VK_LEFT:
case 'A': case 'A':
if (upgradeUiState.optionCount > 1) MoveUpgradeSelectionHorizontal(-1);
{
int rowStart = upgradeUiState.selectedIndex - (upgradeUiState.selectedIndex % upgradeColumnCount);
if (upgradeUiState.selectedIndex > rowStart)
{
upgradeUiState.selectedIndex--;
}
else
{
int rowEnd = rowStart + upgradeColumnCount - 1;
if (rowEnd >= upgradeUiState.optionCount)
{
rowEnd = upgradeUiState.optionCount - 1;
}
upgradeUiState.selectedIndex = rowEnd;
}
}
InvalidateRect(hWnd, nullptr, FALSE); InvalidateRect(hWnd, nullptr, FALSE);
break; break;
case VK_RIGHT: case VK_RIGHT:
case 'D': case 'D':
if (upgradeUiState.optionCount > 1) MoveUpgradeSelectionHorizontal(1);
{
int rowStart = upgradeUiState.selectedIndex - (upgradeUiState.selectedIndex % upgradeColumnCount);
int rowEnd = rowStart + upgradeColumnCount - 1;
if (rowEnd >= upgradeUiState.optionCount)
{
rowEnd = upgradeUiState.optionCount - 1;
}
upgradeUiState.selectedIndex = (upgradeUiState.selectedIndex < rowEnd) ? upgradeUiState.selectedIndex + 1 : rowStart;
}
InvalidateRect(hWnd, nullptr, FALSE); InvalidateRect(hWnd, nullptr, FALSE);
break; break;
case VK_UP: case VK_UP:
case 'W': case 'W':
if (upgradeUiState.selectedIndex >= upgradeColumnCount) MoveUpgradeSelectionVertical(-1);
{
upgradeUiState.selectedIndex -= upgradeColumnCount;
}
InvalidateRect(hWnd, nullptr, FALSE); InvalidateRect(hWnd, nullptr, FALSE);
break; break;
case VK_DOWN: case VK_DOWN:
case 'S': case 'S':
if (upgradeUiState.selectedIndex + upgradeColumnCount < upgradeUiState.optionCount) MoveUpgradeSelectionVertical(1);
{
upgradeUiState.selectedIndex += upgradeColumnCount;
}
InvalidateRect(hWnd, nullptr, FALSE); InvalidateRect(hWnd, nullptr, FALSE);
break; break;
case VK_RETURN: case VK_RETURN:
@@ -630,20 +710,7 @@ static bool HandleUpgradeKey(HWND hWnd, WPARAM key)
case VK_SPACE: case VK_SPACE:
if (upgradeUiState.picksRemaining > 1 && upgradeUiState.optionCount > 0) if (upgradeUiState.picksRemaining > 1 && upgradeUiState.optionCount > 0)
{ {
bool currentlyMarked = upgradeUiState.marked[upgradeUiState.selectedIndex]; ToggleUpgradeMarkedSelection();
if (currentlyMarked)
{
upgradeUiState.marked[upgradeUiState.selectedIndex] = false;
if (upgradeUiState.markedCount > 0)
{
upgradeUiState.markedCount--;
}
}
else if (upgradeUiState.markedCount < upgradeUiState.picksRemaining)
{
upgradeUiState.marked[upgradeUiState.selectedIndex] = true;
upgradeUiState.markedCount++;
}
InvalidateRect(hWnd, nullptr, FALSE); InvalidateRect(hWnd, nullptr, FALSE);
} }
else else
@@ -665,40 +732,55 @@ static bool HandleUpgradeKey(HWND hWnd, WPARAM key)
} }
/** /**
* @brief 处理游戏过程中的按键。 * @brief 处理 Rogue 技能演示模式的专用按键。
* @param hWnd 当前窗口句柄。 * @param hWnd 当前窗口句柄。
* @param key 按键虚拟键码。 * @param key 按键虚拟键码。
* @return 已处理返回 true。
*/ */
static void HandlePlayingKey(HWND hWnd, WPARAM key) static bool HandleDemoPlayingKey(HWND hWnd, WPARAM key)
{ {
if (IsRogueSkillDemoMode()) if (!IsRogueSkillDemoMode())
{ {
if (key == 'N') return false;
{
AdvanceRogueSkillDemo();
InvalidateRect(hWnd, nullptr, FALSE);
return;
}
if (key == 'R')
{
RestartCurrentRogueSkillDemo();
ResetGameTimer(hWnd);
InvalidateRect(hWnd, nullptr, FALSE);
return;
}
if (key == VK_ESCAPE || key == VK_BACK || key == 'M')
{
OpenSkillDemoScreen();
InvalidateRect(hWnd, nullptr, FALSE);
return;
}
} }
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') if (!IsRogueSkillDemoMode() && key == 'M')
{ {
ReturnToMainMenu(); ReturnToMainMenu();
InvalidateRect(hWnd, nullptr, FALSE); InvalidateRect(hWnd, nullptr, FALSE);
return; return true;
} }
if (!IsRogueSkillDemoMode() && key == 'R') if (!IsRogueSkillDemoMode() && key == 'R')
@@ -706,25 +788,27 @@ static void HandlePlayingKey(HWND hWnd, WPARAM key)
StartGameWithMode(currentMode); StartGameWithMode(currentMode);
ResetGameTimer(hWnd); ResetGameTimer(hWnd);
InvalidateRect(hWnd, nullptr, FALSE); InvalidateRect(hWnd, nullptr, FALSE);
return; return true;
} }
if (!IsRogueSkillDemoMode() && key == 'P') if (!IsRogueSkillDemoMode() && key == 'P')
{ {
suspendFlag = !suspendFlag; suspendFlag = !suspendFlag;
InvalidateRect(hWnd, nullptr, FALSE); InvalidateRect(hWnd, nullptr, FALSE);
return; return true;
} }
if (key == 'G') if (key == 'G')
{ {
// 落点提示是显示开关,不改变棋盘或方块状态。
targetFlag = !targetFlag; targetFlag = !targetFlag;
InvalidateRect(hWnd, nullptr, FALSE); InvalidateRect(hWnd, nullptr, FALSE);
return; return true;
} }
if (gameOverFlag && reviveAvailable && key == 'V') if (gameOverFlag && reviveAvailable && key == 'V')
{ {
// 复活机会只有视频成功播放后才消耗,失败时保留机会并给出反馈。
if (PlayReviveVideo(hWnd)) if (PlayReviveVideo(hWnd))
{ {
ReviveAfterVideo(); ReviveAfterVideo();
@@ -735,23 +819,73 @@ static void HandlePlayingKey(HWND hWnd, WPARAM key)
SetFeedbackMessage(_T("视频播放失败"), _T("无法打开复活视频,复活机会未消耗。"), 14); SetFeedbackMessage(_T("视频播放失败"), _T("无法打开复活视频,复活机会未消耗。"), 14);
} }
InvalidateRect(hWnd, nullptr, FALSE); InvalidateRect(hWnd, nullptr, FALSE);
return; return true;
} }
if (gameOverFlag || suspendFlag) return false;
{ }
return;
}
/**
* @brief 处理 Rogue 侧栏滚动和主动技能按键。
* @param hWnd 当前窗口句柄。
* @param key 按键虚拟键码。
* @return 已处理返回 true。
*/
static bool HandleRogueSkillKey(HWND hWnd, WPARAM key)
{
if (currentMode == MODE_ROGUE && (key == 'J' || key == 'K')) if (currentMode == MODE_ROGUE && (key == 'J' || key == 'K'))
{ {
// Rogue 侧栏强化列表较长,J/K 只调整说明列表的滚动位置。
int direction = (key == 'J') ? 1 : -1; int direction = (key == 'J') ? 1 : -1;
AdjustScrollOffset(upgradeListScrollOffset, direction * GetScrollStep(hWnd, 52)); AdjustScrollOffset(upgradeListScrollOffset, direction * GetScrollStep(hWnd, 52));
InvalidateRect(hWnd, nullptr, FALSE); InvalidateRect(hWnd, nullptr, FALSE);
return; 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) switch (key)
{ {
case VK_LEFT: case VK_LEFT:
@@ -760,60 +894,61 @@ static void HandlePlayingKey(HWND hWnd, WPARAM key)
{ {
MoveLeft(); MoveLeft();
} }
break; return true;
case VK_RIGHT: case VK_RIGHT:
case 'D': case 'D':
if (CanMoveRight()) if (CanMoveRight())
{ {
MoveRight(); MoveRight();
} }
break; return true;
case VK_DOWN: case VK_DOWN:
case 'S': case 'S':
// 软降被阻挡时等价于本回合落地,立即进入固定和消行流程。
if (CanMoveDown()) if (CanMoveDown())
{ {
MoveDown(); MoveDown();
} }
else else
{ {
Fixing(); FixPieceAndResolveLines();
if (!gameOverFlag)
{
DeleteLines();
CheckRogueLevelProgress();
}
} }
break; return true;
case VK_UP: case VK_UP:
case 'W': case 'W':
Rotate(); Rotate();
break; return true;
case VK_SPACE: case VK_SPACE:
// 硬降先移动到最低合法位置,再一次性固定结算。
DropDown(); DropDown();
Fixing(); FixPieceAndResolveLines();
if (!gameOverFlag) return true;
{
DeleteLines();
CheckRogueLevelProgress();
}
break;
case 'C':
case VK_SHIFT:
case VK_LSHIFT:
case VK_RSHIFT:
HoldCurrentPiece();
break;
case 'Z':
UseBlackHole();
break;
case 'X':
UseScreenBomb();
break;
case 'V':
UseAirReshape();
break;
default: default:
break; 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) if (!gameOverFlag)
@@ -831,6 +966,7 @@ static void HandlePlayingKey(HWND hWnd, WPARAM key)
*/ */
void HandleKeyDown(HWND hWnd, WPARAM wParam) void HandleKeyDown(HWND hWnd, WPARAM wParam)
{ {
// 按当前界面从上到下分发:菜单、帮助、升级界面优先消费按键。
if (HandleMenuKey(hWnd, wParam) || if (HandleMenuKey(hWnd, wParam) ||
HandleRulesKey(hWnd, wParam) || HandleRulesKey(hWnd, wParam) ||
HandleUpgradeKey(hWnd, wParam)) HandleUpgradeKey(hWnd, wParam))
+9
View File
@@ -13,6 +13,7 @@
*/ */
void AdjustScrollOffset(int& scrollOffset, int delta) void AdjustScrollOffset(int& scrollOffset, int delta)
{ {
// 先应用本次滚动增量,再统一夹紧到允许范围内。
scrollOffset += delta; scrollOffset += delta;
if (scrollOffset < 0) if (scrollOffset < 0)
{ {
@@ -46,6 +47,7 @@ LayoutMetrics GetLayoutMetrics(HWND hWnd)
RECT clientRect; RECT clientRect;
GetClientRect(hWnd, &clientRect); GetClientRect(hWnd, &clientRect);
// 以设计稿窗口为基准计算缩放,取较小比例保证完整界面不被裁切。
int clientWidth = clientRect.right - clientRect.left; int clientWidth = clientRect.right - clientRect.left;
int clientHeight = clientRect.bottom - clientRect.top; int clientHeight = clientRect.bottom - clientRect.top;
int scaleX = MulDiv(clientWidth, 1000, WINDOW_CLIENT_WIDTH); int scaleX = MulDiv(clientWidth, 1000, WINDOW_CLIENT_WIDTH);
@@ -60,6 +62,7 @@ LayoutMetrics GetLayoutMetrics(HWND hWnd)
metrics.scale = scale; metrics.scale = scale;
metrics.layoutWidth = MulDiv(WINDOW_CLIENT_WIDTH, scale, 1000); metrics.layoutWidth = MulDiv(WINDOW_CLIENT_WIDTH, scale, 1000);
metrics.layoutHeight = MulDiv(WINDOW_CLIENT_HEIGHT, scale, 1000); metrics.layoutHeight = MulDiv(WINDOW_CLIENT_HEIGHT, scale, 1000);
// 横向居中显示,纵向从顶部开始,方便窗口高度不足时保持棋盘起点稳定。
metrics.offsetX = (clientWidth - metrics.layoutWidth) / 2; metrics.offsetX = (clientWidth - metrics.layoutWidth) / 2;
metrics.offsetY = 0; metrics.offsetY = 0;
metrics.grid = MulDiv(GRID, scale, 1000); metrics.grid = MulDiv(GRID, scale, 1000);
@@ -289,6 +292,8 @@ RECT GetUpgradeCardRect(HWND hWnd, int index)
{ {
LayoutMetrics metrics = GetLayoutMetrics(hWnd); LayoutMetrics metrics = GetLayoutMetrics(hWnd);
RECT overlayRect = GetUpgradeOverlayRect(hWnd); RECT overlayRect = GetUpgradeOverlayRect(hWnd);
// 根据当前候选数量自动决定列数;最多三列,两行用于命运轮盘六选项。
int gap = ScaleValue(metrics, 18); int gap = ScaleValue(metrics, 18);
int horizontalPadding = ScaleValue(metrics, 36); int horizontalPadding = ScaleValue(metrics, 36);
int verticalTop = overlayRect.top + ScaleValue(metrics, 138); int verticalTop = overlayRect.top + ScaleValue(metrics, 138);
@@ -347,6 +352,8 @@ RECT GetOverlayButtonRect(HWND hWnd, int index, int buttonCount)
{ {
LayoutMetrics metrics = GetLayoutMetrics(hWnd); LayoutMetrics metrics = GetLayoutMetrics(hWnd);
RECT overlayRect = GetGameOverlayRect(hWnd); RECT overlayRect = GetGameOverlayRect(hWnd);
// 游戏结束可能有三个按钮,暂停只有两个按钮,因此间距和边距分开计算。
int gap = buttonCount == 3 ? ScaleValue(metrics, 8) : ScaleValue(metrics, 18); int gap = buttonCount == 3 ? ScaleValue(metrics, 8) : ScaleValue(metrics, 18);
int sidePadding = buttonCount == 3 ? ScaleValue(metrics, 14) : ScaleValue(metrics, 34); int sidePadding = buttonCount == 3 ? ScaleValue(metrics, 14) : ScaleValue(metrics, 34);
int width = (overlayRect.right - overlayRect.left - sidePadding * 2 - gap * (buttonCount - 1)) / buttonCount; int width = (overlayRect.right - overlayRect.left - sidePadding * 2 - gap * (buttonCount - 1)) / buttonCount;
@@ -383,6 +390,8 @@ RECT GetBackButtonRect(HWND hWnd)
RECT GetMusicButtonRect(HWND hWnd) RECT GetMusicButtonRect(HWND hWnd)
{ {
LayoutMetrics metrics = GetLayoutMetrics(hWnd); LayoutMetrics metrics = GetLayoutMetrics(hWnd);
// 音乐按钮保持最小可点击尺寸,避免窗口缩小时变得难以点中。
int size = ScaleValue(metrics, 28); int size = ScaleValue(metrics, 28);
if (size < 22) if (size < 22)
{ {
+9
View File
@@ -23,6 +23,7 @@ static constexpr const wchar_t* kReviveVideoAlias = L"TereisReviveVideo";
*/ */
static bool TryPlayMciLoop(const std::wstring& path, bool forceMpegVideo) static bool TryPlayMciLoop(const std::wstring& path, bool forceMpegVideo)
{ {
// 资源不存在时直接失败,让上层继续尝试下一个候选路径或格式。
if (!FileExists(path)) if (!FileExists(path))
{ {
return false; return false;
@@ -61,6 +62,7 @@ static bool TryPlayMciLoop(const std::wstring& path, bool forceMpegVideo)
*/ */
void StopBackgroundMusic() void StopBackgroundMusic()
{ {
// 根据当前播放方式选择对应的释放接口,避免 MCI 设备或 PlaySound 残留。
if (bgmUsingMci) if (bgmUsingMci)
{ {
mciSendStringW((std::wstring(L"stop ") + kBgmAlias).c_str(), nullptr, 0, nullptr); mciSendStringW((std::wstring(L"stop ") + kBgmAlias).c_str(), nullptr, 0, nullptr);
@@ -80,6 +82,7 @@ void StopBackgroundMusic()
*/ */
void StartBackgroundMusic() void StartBackgroundMusic()
{ {
// 音乐被关闭或已经在播放时,不重复查找资源和启动设备。
if (!bgmEnabled || bgmPlaying) if (!bgmEnabled || bgmPlaying)
{ {
return; return;
@@ -94,6 +97,7 @@ void StartBackgroundMusic()
for (const std::wstring& candidate : bgmWavCandidates) for (const std::wstring& candidate : bgmWavCandidates)
{ {
// WAV 优先使用 PlaySound,依赖少、兼容性最好。
if (FileExists(candidate) && if (FileExists(candidate) &&
PlaySoundW(candidate.c_str(), nullptr, SND_FILENAME | SND_ASYNC | SND_LOOP)) PlaySoundW(candidate.c_str(), nullptr, SND_FILENAME | SND_ASYNC | SND_LOOP))
{ {
@@ -112,6 +116,7 @@ void StartBackgroundMusic()
for (const std::wstring& candidate : oggCandidates) for (const std::wstring& candidate : oggCandidates)
{ {
// OGG 通过 MCI 尝试普通打开和 mpegvideo 强制类型两条路径。
if (TryPlayMciLoop(candidate, false) || TryPlayMciLoop(candidate, true)) if (TryPlayMciLoop(candidate, false) || TryPlayMciLoop(candidate, true))
{ {
return; return;
@@ -127,6 +132,7 @@ void StartBackgroundMusic()
for (const std::wstring& candidate : fallbackWavCandidates) for (const std::wstring& candidate : fallbackWavCandidates)
{ {
// 兼容旧资源名 background.wav,保证替换素材后仍能播放。
if (FileExists(candidate) && if (FileExists(candidate) &&
PlaySoundW(candidate.c_str(), nullptr, SND_FILENAME | SND_ASYNC | SND_LOOP)) PlaySoundW(candidate.c_str(), nullptr, SND_FILENAME | SND_ASYNC | SND_LOOP))
{ {
@@ -164,6 +170,7 @@ void ToggleBackgroundMusic(HWND hWnd)
*/ */
bool PlayReviveVideo(HWND hWnd) bool PlayReviveVideo(HWND hWnd)
{ {
// 依次查找 AVI 和 MP4,并同时支持构建目录与项目根目录运行。
std::wstring videoPath = BuildAssetPath(L"assets\\video\\video.avi"); std::wstring videoPath = BuildAssetPath(L"assets\\video\\video.avi");
if (!FileExists(videoPath)) if (!FileExists(videoPath))
{ {
@@ -185,6 +192,7 @@ bool PlayReviveVideo(HWND hWnd)
bool shouldResumeBgm = bgmEnabled; bool shouldResumeBgm = bgmEnabled;
if (bgmPlaying) if (bgmPlaying)
{ {
// 视频播放期间暂停背景音乐,播放结束后按开关状态恢复。
StopBackgroundMusic(); StopBackgroundMusic();
} }
@@ -214,6 +222,7 @@ bool PlayReviveVideo(HWND hWnd)
if (!played) if (!played)
{ {
// MCI 全屏播放失败时退回系统默认播放器,并等待播放器进程结束。
SHELLEXECUTEINFOW shellInfo = {}; SHELLEXECUTEINFOW shellInfo = {};
shellInfo.cbSize = sizeof(shellInfo); shellInfo.cbSize = sizeof(shellInfo);
shellInfo.fMask = SEE_MASK_NOCLOSEPROCESS; shellInfo.fMask = SEE_MASK_NOCLOSEPROCESS;
+18
View File
@@ -27,6 +27,7 @@ static void CALLBACK CreditTimerCallback(UINT, UINT, DWORD_PTR userData, DWORD_P
*/ */
void ResetGameTimer(HWND hWnd) void ResetGameTimer(HWND hWnd)
{ {
// 下落速度会被 Rogue 强化和临时状态动态修改,因此每次变化都重新注册定时器。
KillTimer(hWnd, GAME_TIMER_ID); KillTimer(hWnd, GAME_TIMER_ID);
SetTimer(hWnd, GAME_TIMER_ID, currentFallInterval > 0 ? currentFallInterval : GAME_TIMER_INTERVAL, nullptr); SetTimer(hWnd, GAME_TIMER_ID, currentFallInterval > 0 ? currentFallInterval : GAME_TIMER_INTERVAL, nullptr);
} }
@@ -37,8 +38,11 @@ void ResetGameTimer(HWND hWnd)
*/ */
void StartAppTimers(HWND hWnd) void StartAppTimers(HWND hWnd)
{ {
// 主定时器负责方块下落,特效定时器负责高帧率视觉动画。
ResetGameTimer(hWnd); ResetGameTimer(hWnd);
SetTimer(hWnd, EFFECT_TIMER_ID, EFFECT_TIMER_INTERVAL, nullptr); SetTimer(hWnd, EFFECT_TIMER_ID, EFFECT_TIMER_INTERVAL, nullptr);
// 致谢页动画需要更高刷新频率,优先使用多媒体定时器。
creditTimerHandle = timeSetEvent( creditTimerHandle = timeSetEvent(
CREDIT_TIMER_INTERVAL, CREDIT_TIMER_INTERVAL,
1, 1,
@@ -58,6 +62,7 @@ void StartAppTimers(HWND hWnd)
*/ */
void StopAppTimers(HWND hWnd) void StopAppTimers(HWND hWnd)
{ {
// 退出或窗口销毁时释放所有可能创建过的计时器资源。
KillTimer(hWnd, GAME_TIMER_ID); KillTimer(hWnd, GAME_TIMER_ID);
KillTimer(hWnd, EFFECT_TIMER_ID); KillTimer(hWnd, EFFECT_TIMER_ID);
if (creditTimerHandle != 0) if (creditTimerHandle != 0)
@@ -92,6 +97,7 @@ static bool TickRogueTimedStates(HWND hWnd)
{ {
bool shouldRefresh = false; bool shouldRefresh = false;
// 狂热、缓流、极限缓速和 Hold 缓速都会影响下落间隔,需要同步重置主定时器。
if (currentMode == MODE_ROGUE && !IsRogueSkillDemoMode() && rogueStats.feverTicks > 0) if (currentMode == MODE_ROGUE && !IsRogueSkillDemoMode() && rogueStats.feverTicks > 0)
{ {
rogueStats.feverTicks--; rogueStats.feverTicks--;
@@ -139,6 +145,7 @@ static bool TickRogueTimedStates(HWND hWnd)
*/ */
static bool TickExtremeDanger(HWND hWnd) static bool TickExtremeDanger(HWND hWnd)
{ {
// 极限玩家只在真实 Rogue 战局中计时,暂停、结束和技能演示都不推进危险等级。
if (currentMode != MODE_ROGUE || if (currentMode != MODE_ROGUE ||
IsRogueSkillDemoMode() || IsRogueSkillDemoMode() ||
rogueStats.extremePlayerLevel <= 0 || rogueStats.extremePlayerLevel <= 0 ||
@@ -151,10 +158,12 @@ static bool TickExtremeDanger(HWND hWnd)
if (rogueStats.extremeDangerTicks > 0) if (rogueStats.extremeDangerTicks > 0)
{ {
// 计时尚未结束时只递减倒计时,不改变速度。
rogueStats.extremeDangerTicks--; rogueStats.extremeDangerTicks--;
return false; return false;
} }
// 每 30 个主计时周期未完成四消就提高危险等级,并立即刷新下落速度。
rogueStats.extremeDangerTicks = 30; rogueStats.extremeDangerTicks = 30;
if (rogueStats.extremeDangerLevel < 5) if (rogueStats.extremeDangerLevel < 5)
{ {
@@ -176,6 +185,7 @@ static bool TickExtremeDanger(HWND hWnd)
*/ */
static bool TryStartTimeDilation(HWND hWnd) static bool TryStartTimeDilation(HWND hWnd)
{ {
// 时间缓流是自动保命效果,已经在持续时不会重复触发。
if (currentMode != MODE_ROGUE || if (currentMode != MODE_ROGUE ||
IsRogueSkillDemoMode() || IsRogueSkillDemoMode() ||
rogueStats.timeDilationLevel <= 0 || rogueStats.timeDilationLevel <= 0 ||
@@ -186,6 +196,7 @@ static bool TryStartTimeDilation(HWND hWnd)
int occupiedHeight = 0; int occupiedHeight = 0;
int playableHeight = GetRoguePlayableHeight(); int playableHeight = GetRoguePlayableHeight();
// 自上而下寻找第一行有方块的位置,由此换算当前堆叠高度。
for (int y = 0; y < playableHeight; y++) for (int y = 0; y < playableHeight; y++)
{ {
bool hasCell = false; bool hasCell = false;
@@ -231,6 +242,7 @@ static bool TickGameFall(HWND hWnd)
return false; return false;
} }
// Rogue 难度随时间推进,速度变化后需要重新安排下一次自动下落。
if (currentMode == MODE_ROGUE && !IsRogueSkillDemoMode()) if (currentMode == MODE_ROGUE && !IsRogueSkillDemoMode())
{ {
int previousFallInterval = currentFallInterval; int previousFallInterval = currentFallInterval;
@@ -243,6 +255,7 @@ static bool TickGameFall(HWND hWnd)
TryStartTimeDilation(hWnd); TryStartTimeDilation(hWnd);
// 能下落时只移动一格;被阻挡时固定方块,并进入消行与升级结算。
if (CanMoveDown()) if (CanMoveDown())
{ {
MoveDown(); MoveDown();
@@ -259,6 +272,7 @@ static bool TickGameFall(HWND hWnd)
if (!gameOverFlag) if (!gameOverFlag)
{ {
// 真实方块位置变化后刷新预测落点,供渲染层绘制目标提示。
ComputeTarget(); ComputeTarget();
} }
@@ -274,6 +288,7 @@ void HandleTimerMessage(HWND hWnd, WPARAM timerId)
{ {
if (timerId == EFFECT_TIMER_ID) if (timerId == EFFECT_TIMER_ID)
{ {
// 视觉特效独立于主下落速度,用固定帧率推进。
if (TickVisualEffects()) if (TickVisualEffects())
{ {
InvalidateRect(hWnd, nullptr, FALSE); InvalidateRect(hWnd, nullptr, FALSE);
@@ -283,6 +298,7 @@ void HandleTimerMessage(HWND hWnd, WPARAM timerId)
if (timerId == CREDIT_TIMER_ID && creditTimerHandle == 0) if (timerId == CREDIT_TIMER_ID && creditTimerHandle == 0)
{ {
// 多媒体定时器不可用时,普通窗口定时器承担致谢页动画刷新。
HandleCreditTick(hWnd); HandleCreditTick(hWnd);
return; return;
} }
@@ -295,10 +311,12 @@ void HandleTimerMessage(HWND hWnd, WPARAM timerId)
bool shouldRefresh = false; bool shouldRefresh = false;
if (feedbackState.visibleTicks > 0) if (feedbackState.visibleTicks > 0)
{ {
// 右侧反馈信息按主计时周期自动消退。
feedbackState.visibleTicks--; feedbackState.visibleTicks--;
shouldRefresh = true; shouldRefresh = true;
} }
// 主定时器集中推进演示、Rogue 临时状态、危险等级和自然下落。
if (IsRogueSkillDemoMode() && TickRogueSkillDemo()) if (IsRogueSkillDemoMode() && TickRogueSkillDemo())
{ {
shouldRefresh = true; shouldRefresh = true;
+3
View File
@@ -20,6 +20,7 @@ std::wstring BuildAssetPath(const wchar_t* relativePath)
wchar_t modulePath[MAX_PATH] = {}; wchar_t modulePath[MAX_PATH] = {};
GetModuleFileNameW(nullptr, modulePath, MAX_PATH); GetModuleFileNameW(nullptr, modulePath, MAX_PATH);
// 先取可执行文件所在目录,再根据构建目录层级回到项目根目录。
std::wstring basePath(modulePath); std::wstring basePath(modulePath);
size_t lastSlash = basePath.find_last_of(L"\\/"); size_t lastSlash = basePath.find_last_of(L"\\/");
if (lastSlash != std::wstring::npos) if (lastSlash != std::wstring::npos)
@@ -56,6 +57,7 @@ std::wstring BuildWorkingDirAssetPath(const wchar_t* relativePath)
return L""; return L"";
} }
// 当前工作目录可能已经是项目根目录,直接拼接相对资源路径。
std::wstring candidate = std::wstring(currentDirectory) + L"\\" + relativePath; std::wstring candidate = std::wstring(currentDirectory) + L"\\" + relativePath;
wchar_t fullPath[MAX_PATH] = {}; wchar_t fullPath[MAX_PATH] = {};
DWORD result = GetFullPathNameW(candidate.c_str(), MAX_PATH, fullPath, nullptr); DWORD result = GetFullPathNameW(candidate.c_str(), MAX_PATH, fullPath, nullptr);
@@ -74,6 +76,7 @@ std::wstring BuildWorkingDirAssetPath(const wchar_t* relativePath)
*/ */
bool FileExists(const std::wstring& path) bool FileExists(const std::wstring& path)
{ {
// 目录不能作为媒体或图片资源使用,因此排除 FILE_ATTRIBUTE_DIRECTORY。
DWORD attributes = GetFileAttributesW(path.c_str()); DWORD attributes = GetFileAttributesW(path.c_str());
return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) == 0; return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) == 0;
} }
+14 -1
View File
@@ -18,10 +18,12 @@ int pendingLineClearEffectLineCount = 0;
*/ */
void ResetPlayerStats(PlayerStats& stats, bool useRogueRules) void ResetPlayerStats(PlayerStats& stats, bool useRogueRules)
{ {
// 基础得分、等级和经验先恢复到新局起点。
stats.score = 0; stats.score = 0;
stats.level = 1; stats.level = 1;
stats.exp = 0; stats.exp = 0;
stats.requiredExp = useRogueRules ? 10 : 0; stats.requiredExp = useRogueRules ? 10 : 0;
// 强化等级、主动技能次数和限时状态全部清零,避免跨局继承。
stats.totalLinesCleared = 0; stats.totalLinesCleared = 0;
stats.scoreMultiplierPercent = 100; stats.scoreMultiplierPercent = 100;
stats.expMultiplierPercent = 100; stats.expMultiplierPercent = 100;
@@ -84,6 +86,7 @@ void ResetPlayerStats(PlayerStats& stats, bool useRogueRules)
stats.difficultyElapsedMs = 0; stats.difficultyElapsedMs = 0;
stats.difficultyLevel = 0; stats.difficultyLevel = 0;
stats.lockedRows = 0; stats.lockedRows = 0;
// 方块改造按 7 种方块分别记录等级,重开时逐项清空。
for (int i = 0; i < 7; i++) for (int i = 0; i < 7; i++)
{ {
stats.pieceTuningLevels[i] = 0; stats.pieceTuningLevels[i] = 0;
@@ -98,6 +101,7 @@ void ResetPlayerStats(PlayerStats& stats, bool useRogueRules)
*/ */
void SetFeedbackMessage(const TCHAR* title, const TCHAR* detail, int ticks) void SetFeedbackMessage(const TCHAR* title, const TCHAR* detail, int ticks)
{ {
// 使用 lstrcpyn 限长复制,避免长描述写出固定缓冲区。
feedbackState.visibleTicks = ticks; feedbackState.visibleTicks = ticks;
lstrcpyn(feedbackState.title, title, sizeof(feedbackState.title) / sizeof(TCHAR)); lstrcpyn(feedbackState.title, title, sizeof(feedbackState.title) / sizeof(TCHAR));
lstrcpyn(feedbackState.detail, detail, sizeof(feedbackState.detail) / sizeof(TCHAR)); lstrcpyn(feedbackState.detail, detail, sizeof(feedbackState.detail) / sizeof(TCHAR));
@@ -108,6 +112,7 @@ void SetFeedbackMessage(const TCHAR* title, const TCHAR* detail, int ticks)
*/ */
void ResetVisualEffects() void ResetVisualEffects()
{ {
// 主状态和各类效果槽位只需把 ticks 清零,渲染层会自动忽略非活动项。
clearEffectState.ticks = 0; clearEffectState.ticks = 0;
clearEffectState.totalTicks = 0; clearEffectState.totalTicks = 0;
clearEffectState.rowCount = 0; clearEffectState.rowCount = 0;
@@ -141,6 +146,7 @@ bool TickVisualEffects()
{ {
bool active = false; bool active = false;
// 所有效果共用倒计时推进,任意效果仍活动就请求界面刷新。
if (clearEffectState.ticks > 0) if (clearEffectState.ticks > 0)
{ {
clearEffectState.ticks--; clearEffectState.ticks--;
@@ -210,6 +216,7 @@ bool TickCreditAnimation()
*/ */
static void AddFloatingText(int boardX, int boardY, const TCHAR* text, COLORREF color) static void AddFloatingText(int boardX, int boardY, const TCHAR* text, COLORREF color)
{ {
// 复用第一个空闲槽位,槽位满时丢弃新效果,避免动态分配。
for (int i = 0; i < 8; i++) for (int i = 0; i < 8; i++)
{ {
if (floatingTextEffects[i].ticks <= 0) if (floatingTextEffects[i].ticks <= 0)
@@ -589,6 +596,7 @@ bool TryRotateWithOffset(int nextState, int offsetX)
*/ */
void ReviveAfterVideo() void ReviveAfterVideo()
{ {
// 只有游戏结束且复活机会仍在时才能进入复活流程。
if (!gameOverFlag || !reviveAvailable) if (!gameOverFlag || !reviveAvailable)
{ {
return; return;
@@ -606,6 +614,7 @@ void ReviveAfterVideo()
rowsToClear = 5; rowsToClear = 5;
} }
// 清理顶部一段空间,避免新方块刚生成又立即判定失败。
for (int y = 0; y < rowsToClear && y < playableHeight; y++) for (int y = 0; y < rowsToClear && y < playableHeight; y++)
{ {
for (int x = 0; x < nGameWidth; x++) for (int x = 0; x < nGameWidth; x++)
@@ -614,6 +623,7 @@ void ReviveAfterVideo()
} }
} }
// 复活后重新取一个活动方块,并刷新落点提示。
type = ConsumeNextType(); type = ConsumeNextType();
nType = nextTypes[0]; nType = nextTypes[0];
state = 0; state = 0;
@@ -632,6 +642,7 @@ void ReviveAfterVideo()
*/ */
void StartGameWithMode(int mode) void StartGameWithMode(int mode)
{ {
// 模式切换后直接复用 Restart,保证经典和 Rogue 都从干净状态开始。
rogueDemoMode = false; rogueDemoMode = false;
currentMode = mode; currentMode = mode;
currentScreen = SCREEN_PLAYING; currentScreen = SCREEN_PLAYING;
@@ -646,6 +657,7 @@ void StartGameWithMode(int mode)
*/ */
void ReturnToMainMenu() void ReturnToMainMenu()
{ {
// 回到主菜单时关闭所有临时战局、帮助页和升级界面状态。
rogueDemoMode = false; rogueDemoMode = false;
currentScreen = SCREEN_MENU; currentScreen = SCREEN_MENU;
suspendFlag = false; suspendFlag = false;
@@ -726,12 +738,13 @@ void OpenCreditScreen()
*/ */
void ChangeCreditPage(int direction) void ChangeCreditPage(int direction)
{ {
constexpr int creditPageCount = 4; constexpr int creditPageCount = 5;
if (direction == 0) if (direction == 0)
{ {
return; return;
} }
// 页码循环切换,同时记录动画方向用于渲染滑动效果。
int oldPageIndex = creditPageIndex; int oldPageIndex = creditPageIndex;
if (direction > 0) if (direction > 0)
{ {
+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);
}
}
+8
View File
@@ -14,6 +14,7 @@
*/ */
void ApplyRainbowLandingEffect(bool overflowTop, const Point* fixedCells, int fixedCellCount) void ApplyRainbowLandingEffect(bool overflowTop, const Point* fixedCells, int fixedCellCount)
{ {
// 顶部溢出时优先交给失败/复活逻辑处理,避免在不可见区域触发奖励。
if (overflowTop || !currentPieceIsRainbow) if (overflowTop || !currentPieceIsRainbow)
{ {
return; return;
@@ -39,6 +40,7 @@ void ApplyRainbowLandingEffect(bool overflowTop, const Point* fixedCells, int fi
rainbowAnchorRow = GetRoguePlayableHeight() - 1; rainbowAnchorRow = GetRoguePlayableHeight() - 1;
} }
// 第二阶段:按锚点行执行彩虹清除和覆盖行染色。
int rainbowRecoloredCount = 0; int rainbowRecoloredCount = 0;
int rainbowClearedCount = TriggerRainbowColorShift(rainbowAnchorRow, point.y, point.y + 3, rainbowRecoloredCount); int rainbowClearedCount = TriggerRainbowColorShift(rainbowAnchorRow, point.y, point.y + 3, rainbowRecoloredCount);
int rainbowScore = 0; int rainbowScore = 0;
@@ -48,6 +50,7 @@ void ApplyRainbowLandingEffect(bool overflowTop, const Point* fixedCells, int fi
int voidExp = 0; int voidExp = 0;
if (currentMode == MODE_ROGUE && rainbowClearedCount > 0) if (currentMode == MODE_ROGUE && rainbowClearedCount > 0)
{ {
// Rogue 模式下特殊清除也能获得得分和经验,但不直接触发升级菜单。
AwardRogueSkillClearRewards(rainbowClearedCount, rainbowScore, rainbowExp, false); AwardRogueSkillClearRewards(rainbowClearedCount, rainbowScore, rainbowExp, false);
if (rogueStats.voidCoreLevel > 0) if (rogueStats.voidCoreLevel > 0)
{ {
@@ -86,11 +89,13 @@ void ApplyRainbowLandingEffect(bool overflowTop, const Point* fixedCells, int fi
*/ */
static void ApplyExplosiveLandingEffect(const Point* explosiveCells, int explosiveCellCount) static void ApplyExplosiveLandingEffect(const Point* explosiveCells, int explosiveCellCount)
{ {
// 非爆破方块直接跳过,保持普通方块落地流程轻量。
if (!currentPieceIsExplosive) if (!currentPieceIsExplosive)
{ {
return; return;
} }
// 每个落地格都作为爆心清除范围,连环炸弹会扩大底层清除函数的范围。
int explosiveCellsCleared = 0; int explosiveCellsCleared = 0;
for (int i = 0; i < explosiveCellCount; i++) for (int i = 0; i < explosiveCellCount; i++)
{ {
@@ -128,6 +133,7 @@ static void ApplyExplosiveLandingEffect(const Point* explosiveCells, int explosi
*/ */
static void ApplyLaserLandingEffect(const Point* fixedCells, int fixedCellCount) static void ApplyLaserLandingEffect(const Point* fixedCells, int fixedCellCount)
{ {
// 激光方块以落地格平均列作为贯穿列,减少不同形状造成的位置偏差。
if (!currentPieceIsLaser) if (!currentPieceIsLaser)
{ {
return; return;
@@ -172,6 +178,7 @@ static void ApplyLaserLandingEffect(const Point* fixedCells, int fixedCellCount)
*/ */
static void ApplyCrossLandingEffect(const Point* fixedCells, int fixedCellCount) static void ApplyCrossLandingEffect(const Point* fixedCells, int fixedCellCount)
{ {
// 十字方块同时计算中心行和中心列,后续分别触发行清除与列清除。
if (!currentPieceIsCross) if (!currentPieceIsCross)
{ {
return; return;
@@ -248,6 +255,7 @@ static void ApplyStableStructureEffect()
*/ */
void ApplySpecialLandingEffects(const Point* fixedCells, int fixedCellCount, const Point* explosiveCells, int explosiveCellCount) void ApplySpecialLandingEffects(const Point* fixedCells, int fixedCellCount, const Point* explosiveCells, int explosiveCellCount)
{ {
// 多种特殊标记按固定顺序结算,保证同一落地事件的反馈和奖励稳定。
ApplyExplosiveLandingEffect(explosiveCells, explosiveCellCount); ApplyExplosiveLandingEffect(explosiveCells, explosiveCellCount);
ApplyLaserLandingEffect(fixedCells, fixedCellCount); ApplyLaserLandingEffect(fixedCells, fixedCellCount);
ApplyCrossLandingEffect(fixedCells, fixedCellCount); ApplyCrossLandingEffect(fixedCells, fixedCellCount);
+7 -2
View File
@@ -20,11 +20,13 @@ using namespace Gdiplus;
*/ */
static Bitmap* TryLoadBitmap(const std::wstring& path) static Bitmap* TryLoadBitmap(const std::wstring& path)
{ {
// 空路径和不存在的文件不交给 GDI+,减少无效加载开销。
if (path.empty() || !FileExists(path)) if (path.empty() || !FileExists(path))
{ {
return nullptr; return nullptr;
} }
// GDI+ 返回对象后仍需检查状态,失败对象要立即释放。
Bitmap* loadedImage = Bitmap::FromFile(path.c_str(), FALSE); Bitmap* loadedImage = Bitmap::FromFile(path.c_str(), FALSE);
if (loadedImage != nullptr && loadedImage->GetLastStatus() == Ok) if (loadedImage != nullptr && loadedImage->GetLastStatus() == Ok)
{ {
@@ -65,6 +67,7 @@ Bitmap* LoadBackgroundImage()
static Bitmap* backgroundImage = nullptr; static Bitmap* backgroundImage = nullptr;
static bool attempted = false; static bool attempted = false;
// 背景图只查找一次,失败后也记住结果,避免每帧重复访问磁盘。
if (!attempted) if (!attempted)
{ {
attempted = true; attempted = true;
@@ -101,7 +104,7 @@ Bitmap* LoadBackgroundImage()
*/ */
Bitmap* LoadCreditImage(int index) Bitmap* LoadCreditImage(int index)
{ {
constexpr int creditPageCount = 4; constexpr int creditPageCount = 5;
static Bitmap* creditImages[creditPageCount] = {}; static Bitmap* creditImages[creditPageCount] = {};
static bool attempted[creditPageCount] = {}; static bool attempted[creditPageCount] = {};
@@ -110,6 +113,7 @@ Bitmap* LoadCreditImage(int index)
return nullptr; return nullptr;
} }
// 每张致谢图单独缓存,只有首次进入对应页时才加载。
if (!attempted[index]) if (!attempted[index])
{ {
attempted[index] = true; attempted[index] = true;
@@ -121,7 +125,8 @@ Bitmap* LoadCreditImage(int index)
L"assets\\images\\qls.jpg", L"assets\\images\\qls.jpg",
L"assets\\images\\wyk.jpg", L"assets\\images\\wyk.jpg",
L"assets\\images\\swj.jpg", L"assets\\images\\swj.jpg",
L"assets\\images\\qhy.jpg" L"assets\\images\\qhy.jpg",
L"assets\\images\\syc.jpg"
}; };
const std::wstring creditExtraCandidates[] = const std::wstring creditExtraCandidates[] =
{ {
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+7
View File
@@ -5,5 +5,12 @@
#include "stdafx.h" #include "stdafx.h"
/**
* @file stdafx.cpp
* @brief 预编译头源文件,用于让构建系统生成 stdafx.h 对应的预编译结果。
*
* 本文件不包含业务逻辑;保留它是为了兼容 Visual Studio 模板和现有构建结构。
*/
// TODO: 在 STDAFX.H 中 // TODO: 在 STDAFX.H 中
// 引用任何所需的附加头文件,而不是在此文件中引用 // 引用任何所需的附加头文件,而不是在此文件中引用