13 Commits

Author SHA1 Message Date
Qi-huanye fd383481fd 尝试适配vs 2026-05-08 18:32:28 +08:00
Qi-huanye a0bae4b6b8 补充vs使用文档 2026-05-08 16:13:48 +08:00
Qi-huanye 70b1665b31 补充开源说明 2026-05-06 17:12:19 +08:00
porcelain 59e491038d 更新TODO 2026-05-05 23:05:46 +08:00
Qi-huanye d5f6cea2ed 进一步补充详细注释 2026-05-01 16:27:27 +08:00
Qi-huanye 84017ae6b7 再次整理文件结构 2026-05-01 16:03:34 +08:00
Qi-huanye 7fe0244a99 文件调整 2026-04-29 15:56:31 +08:00
Qi-huanye be95bd25e1 清屏炸弹不可多选 2026-04-29 15:45:02 +08:00
Qi-huanye b98d2c9d59 调整致谢页并清理worktree 2026-04-29 15:27:06 +08:00
Qi-huanye a331162349 进一步拆分与注释补强 2026-04-29 15:21:14 +08:00
Qi-huanye 58ab400949 调整照片 2026-04-29 15:05:37 +08:00
Qi-huanye 1c000c3c21 补强注释 2026-04-28 23:18:51 +08:00
Qi-huanye 0840a807b5 项目架构重构,代码整理 2026-04-28 22:44:31 +08:00
41 changed files with 8135 additions and 5691 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/` 下的媒体文件。
+150 -4
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
@@ -98,6 +202,15 @@ Tereis/
├─ src/ ├─ src/
│ ├─ include/ 头文件 │ ├─ include/ 头文件
│ ├─ source/ 源文件 │ ├─ source/ 源文件
│ │ ├─ Tetris.cpp 程序入口、窗口和消息框架
│ │ ├─ TetrisLogic.cpp 基础俄罗斯方块逻辑框架
│ │ ├─ TetrisRender.cpp 基础绘制框架
│ │ ├─ common/ 资源路径、文件检查等通用工具
│ │ ├─ app/ 媒体播放、布局命中、输入和定时器处理
│ │ ├─ extensions/ 框架外通用扩展、界面状态和视觉效果
│ │ ├─ logic/ 特殊方块落地效果等逻辑扩展
│ │ ├─ render/ 图片加载等渲染内部支持
│ │ └─ rogue/ Rogue 模式、强化和技能系统
│ └─ resources/ Windows 资源脚本 │ └─ resources/ Windows 资源脚本
├─ assets/ ├─ assets/
│ ├─ audio/ 背景音乐 │ ├─ audio/ 背景音乐
@@ -109,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++ 兼容运行说明
``` ```
## 构建环境 ## 构建环境
@@ -130,6 +242,8 @@ Tereis/
C:\mingw64\bin\ C:\mingw64\bin\
``` ```
构建脚本会递归收集 `src/source` 下的 `.cpp` 文件。新增功能代码可以放入功能目录,不需要手动维护固定源码列表。
## 构建与运行 ## 构建与运行
在项目根目录执行: 在项目根目录执行:
@@ -150,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`
@@ -194,10 +322,28 @@ powershell -NoProfile -ExecutionPolicy Bypass -File .\build-mingw.ps1 -Run
本项目以过程式 C++ 写法为主,核心逻辑分布如下: 本项目以过程式 C++ 写法为主,核心逻辑分布如下:
- `src/source/Tetris.cpp`窗口、消息循环、输入和鼠标交互 - `src/source/Tetris.cpp`Win32 程序入口、窗口创建和消息分发主干
- `src/source/TetrisLogic.cpp`:基础方块逻辑、消行和状态重置 - `src/source/TetrisLogic.cpp`:基础方块逻辑、消行和状态重置
- `src/source/TetrisRogue.cpp`:Rogue 模式、强化、技能和成长系统
- `src/source/TetrisRender.cpp`:界面绘制、面板、动画和特效 - `src/source/TetrisRender.cpp`:界面绘制、面板、动画和特效
- `src/source/common/TetrisAssets.cpp`:资源路径拼接和文件存在判断
- `src/source/app/`:背景音乐、复活视频、窗口布局命中、鼠标键盘和定时器处理
- `src/source/logic/TetrisPieceEffects.cpp`:彩虹、爆破、激光、十字和稳定结构等落地效果
- `src/source/extensions/TetrisGameExtensions.cpp`:框架外通用状态切换、复活、说明页、视觉效果等扩展支持
- `src/source/render/TetrisRenderAssets.cpp`:背景图、致谢页图片等 GDI+ 图片资源加载
- `src/source/rogue/TetrisRogue.cpp`:Rogue 模式、强化、技能和成长系统
- `src/include/Tetris.h`:主要结构体、全局状态和函数声明 - `src/include/Tetris.h`:主要结构体、全局状态和函数声明
- `src/include/TetrisAppInternal.h``src/include/TetrisRenderInternal.h``src/include/TetrisAssets.h`:窗口层、渲染层和资源工具的内部声明
项目适合作为程序设计课程大作业展示,也便于在答辩时讲解窗口程序、游戏循环、碰撞检测、状态管理和功能扩展。 项目适合作为程序设计课程大作业展示,也便于在答辩时讲解窗口程序、游戏循环、碰撞检测、状态管理和功能扩展。
## 开源协议与素材说明
本项目为程序设计课程大作业,仅供课程学习、课堂展示和个人技术交流使用,不用于商业发布。
- 源代码、构建脚本和原创文档内容采用 MIT License,详见 `LICENSE`
- `assets/``report/images/``report/code-snippets/` 中的音频、图片、视频等非代码素材不包含在 MIT License 授权范围内。
- 音乐素材来自《千恋*万花》,仅作为课程大作业学习展示使用,版权归原权利方所有。
- 图片素材主要由 AI 生成或用于课程报告展示。
- 如需二次发布、公开分发可执行文件或用于课程以外场景,请先替换或移除未取得独立授权的素材。
更完整的素材来源和授权限制说明见 `NOTICE.md`
+354 -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

+3 -8
View File
@@ -61,14 +61,9 @@ foreach ($Candidate in $WindresCandidates) {
New-Item -ItemType Directory -Force -Path $BuildDir | Out-Null New-Item -ItemType Directory -Force -Path $BuildDir | Out-Null
$Sources = @( $Sources = Get-ChildItem -Path $SourceDir -Recurse -Filter "*.cpp" |
(Join-Path $SourceDir "stdafx.cpp"), Sort-Object FullName |
(Join-Path $SourceDir "Tetris.cpp"), Select-Object -ExpandProperty FullName
(Join-Path $SourceDir "TetrisLogic.cpp"),
(Join-Path $SourceDir "TetrisLogicInnovation.cpp"),
(Join-Path $SourceDir "TetrisRogue.cpp"),
(Join-Path $SourceDir "TetrisRender.cpp")
)
$LinkInputs = @() $LinkInputs = @()
+75
View File
@@ -0,0 +1,75 @@
param(
[switch]$Run
)
$ErrorActionPreference = "Stop"
$Root = Split-Path -Parent $MyInvocation.MyCommand.Path
$ProjectDir = Join-Path $Root "src"
$IncludeDir = Join-Path $ProjectDir "include"
$SourceDir = Join-Path $ProjectDir "source"
$ResourceDir = Join-Path $ProjectDir "resources"
$AssetIconDir = Join-Path $Root "assets\icons"
$BuildDir = Join-Path $Root ".vscode-build\vs2026"
$ExePath = Join-Path $BuildDir "Tetris.exe"
$ResPath = Join-Path $BuildDir "Tetris.res"
$RcPath = Join-Path $ResourceDir "Tetris.rc"
if (-not (Get-Command cl.exe -ErrorAction SilentlyContinue)) {
throw "cl.exe not found. Open Visual Studio 2026: Tools -> Command Line -> Developer PowerShell, then run this script again."
}
if (-not (Get-Command rc.exe -ErrorAction SilentlyContinue)) {
throw "rc.exe not found. Install the Windows SDK from the Visual Studio Installer C++ desktop workload."
}
New-Item -ItemType Directory -Force -Path $BuildDir | Out-Null
$Sources = Get-ChildItem -Path $SourceDir -Recurse -Filter "*.cpp" |
Sort-Object FullName |
Select-Object -ExpandProperty FullName
if ($Sources.Count -lt 10) {
throw "Too few source files found under src\source. The render, app, logic, rogue, common, and extension modules must all be compiled."
}
& rc.exe `
/nologo `
/i $IncludeDir `
/i $AssetIconDir `
/fo $ResPath `
$RcPath
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
& cl.exe `
/nologo `
/utf-8 `
/std:c++17 `
/EHsc `
/Zi `
/Od `
/DUNICODE `
/D_UNICODE `
/D_WINDOWS `
/I $IncludeDir `
$Sources `
$ResPath `
/Fe:$ExePath `
/link `
/SUBSYSTEM:WINDOWS `
winmm.lib `
gdiplus.lib `
gdi32.lib `
user32.lib `
shell32.lib
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
if ($Run) {
Start-Process -FilePath $ExePath -WorkingDirectory $Root
}
+333
View File
@@ -1,11 +1,17 @@
#pragma once #pragma once
/**
* @file Tetris.h
* @brief 定义俄罗斯方块项目的全局常量、结构体、枚举、全局状态和公开函数接口。
*/
#include "resource.h" #include "resource.h"
#include "stdafx.h" #include "stdafx.h"
#include <mmsystem.h> #include <mmsystem.h>
#pragma comment(lib, "winmm.lib") #pragma comment(lib, "winmm.lib")
// 棋盘和窗口基础尺寸,渲染层会按当前窗口大小统一缩放这些设计稿尺寸。
constexpr int GRID = 40; constexpr int GRID = 40;
constexpr int nGameWidth = 10; constexpr int nGameWidth = 10;
constexpr int nGameHeight = 20; constexpr int nGameHeight = 20;
@@ -19,18 +25,27 @@ constexpr int WINDOW_CLIENT_WIDTH = WINDOW_PADDING * 2 + nGameWidth * GRID + SID
constexpr int BOARD_CLIENT_HEIGHT = WINDOW_PADDING * 2 + nGameHeight * GRID + 20; constexpr int BOARD_CLIENT_HEIGHT = WINDOW_PADDING * 2 + nGameHeight * GRID + 20;
constexpr int WINDOW_CLIENT_HEIGHT = (BOARD_CLIENT_HEIGHT > SIDE_PANEL_HEIGHT) ? BOARD_CLIENT_HEIGHT : SIDE_PANEL_HEIGHT; constexpr int WINDOW_CLIENT_HEIGHT = (BOARD_CLIENT_HEIGHT > SIDE_PANEL_HEIGHT) ? BOARD_CLIENT_HEIGHT : SIDE_PANEL_HEIGHT;
/**
* @brief 棋盘坐标点,x 表示列号,y 表示行号。
*/
struct Point struct Point
{ {
int x; int x;
int y; int y;
}; };
/**
* @brief 主菜单导航状态。
*/
struct MenuState struct MenuState
{ {
int selectedIndex; int selectedIndex;
int optionCount; int optionCount;
}; };
/**
* @brief 帮助、规则、致谢和技能演示页面共享的导航状态。
*/
struct HelpState struct HelpState
{ {
int selectedIndex; int selectedIndex;
@@ -38,6 +53,12 @@ struct HelpState
int currentPage; int currentPage;
}; };
/**
* @brief 记录经典模式和 Rogue 模式的分数、等级、强化与临时状态。
*
* 课程要求不使用 class,因此所有与玩家成长有关的数据都集中放在结构体字段中,
* 由逻辑层函数按过程式方式读取和修改。
*/
struct PlayerStats struct PlayerStats
{ {
int score; int score;
@@ -110,6 +131,9 @@ struct PlayerStats
int pieceTuningLevels[7]; int pieceTuningLevels[7];
}; };
/**
* @brief 升级界面中已经生成并显示给玩家的一个候选强化。
*/
struct UpgradeOption struct UpgradeOption
{ {
int id; int id;
@@ -122,6 +146,9 @@ struct UpgradeOption
const TCHAR* description; const TCHAR* description;
}; };
/**
* @brief 强化池中的基础配置项,用于生成升级界面候选。
*/
struct UpgradeEntry struct UpgradeEntry
{ {
int id; int id;
@@ -133,6 +160,9 @@ struct UpgradeEntry
const TCHAR* description; const TCHAR* description;
}; };
/**
* @brief Rogue 升级选择界面的临时 UI 状态。
*/
struct UpgradeUiState struct UpgradeUiState
{ {
int selectedIndex; int selectedIndex;
@@ -145,6 +175,9 @@ struct UpgradeUiState
UpgradeOption options[6]; UpgradeOption options[6];
}; };
/**
* @brief 右侧战斗日志或提示条的显示状态。
*/
struct FeedbackState struct FeedbackState
{ {
int visibleTicks; int visibleTicks;
@@ -152,6 +185,9 @@ struct FeedbackState
TCHAR detail[128]; TCHAR detail[128];
}; };
/**
* @brief 标准消行动画状态。
*/
struct ClearEffectState struct ClearEffectState
{ {
int ticks; int ticks;
@@ -160,6 +196,9 @@ struct ClearEffectState
int rows[8]; int rows[8];
}; };
/**
* @brief 棋盘上浮动文字特效的单个实例。
*/
struct FloatingTextEffect struct FloatingTextEffect
{ {
int ticks; int ticks;
@@ -170,6 +209,9 @@ struct FloatingTextEffect
COLORREF color; COLORREF color;
}; };
/**
* @brief 棋盘粒子特效的单个实例。
*/
struct ParticleEffect struct ParticleEffect
{ {
int ticks; int ticks;
@@ -182,6 +224,9 @@ struct ParticleEffect
COLORREF color; COLORREF color;
}; };
/**
* @brief 被清除格子的短时闪烁高亮状态。
*/
struct CellFlashEffect struct CellFlashEffect
{ {
int ticks; int ticks;
@@ -191,6 +236,9 @@ struct CellFlashEffect
COLORREF color; COLORREF color;
}; };
/**
* @brief 固定方块受重力下落时的残影轨迹状态。
*/
struct GravityFallEffect struct GravityFallEffect
{ {
int ticks; int ticks;
@@ -201,6 +249,9 @@ struct GravityFallEffect
int cellValue; int cellValue;
}; };
/**
* @brief 当前应用所在的大界面。
*/
enum ScreenState enum ScreenState
{ {
SCREEN_MENU = 0, SCREEN_MENU = 0,
@@ -209,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,
@@ -222,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;
@@ -264,60 +322,335 @@ extern bool currentPieceIsRainbow;
extern int bricks[7][4][4][4]; extern int bricks[7][4][4][4];
extern COLORREF BrickColor[7]; extern COLORREF BrickColor[7];
/**
* @brief 判断当前活动方块是否还能向下移动一格。
* @return 可以下落返回 true,否则返回 false。
*/
bool CanMoveDown(); bool CanMoveDown();
/**
* @brief 判断当前活动方块是否还能向左移动一格。
* @return 可以左移返回 true,否则返回 false。
*/
bool CanMoveLeft(); bool CanMoveLeft();
/**
* @brief 判断当前活动方块是否还能向右移动一格。
* @return 可以右移返回 true,否则返回 false。
*/
bool CanMoveRight(); bool CanMoveRight();
/**
* @brief 将当前活动方块向下移动一格。
*/
void MoveDown(); void MoveDown();
/**
* @brief 将当前活动方块向左移动一格。
*/
void MoveLeft(); void MoveLeft();
/**
* @brief 将当前活动方块向右移动一格。
*/
void MoveRight(); void MoveRight();
/**
* @brief 尝试旋转当前活动方块,Rogue 完美旋转会额外尝试左右偏移。
*/
void Rotate(); void Rotate();
/**
* @brief 将当前活动方块直接下落到预测落点。
*/
void DropDown(); void DropDown();
/**
* @brief 将当前活动方块固定到棋盘并生成下一块。
*/
void Fixing(); void Fixing();
/**
* @brief 删除指定行并让上方棋盘整体下落。
* @param number 要删除的棋盘行号。
*/
void DeleteOneLine(int number); void DeleteOneLine(int number);
/**
* @brief 扫描棋盘、删除所有满行并触发消行结算。
* @return 本次删除的行数。
*/
int DeleteLines(); int DeleteLines();
/**
* @brief 判断当前游戏是否已经结束。
* @return 游戏结束返回 true,否则返回 false。
*/
bool GameOver();
/**
* @brief 计算当前活动方块的预测落点。
*/
void ComputeTarget(); void ComputeTarget();
/**
* @brief 重置棋盘、方块、统计和视觉状态,开始一局新游戏。
*/
void Restart(); void Restart();
/**
* @brief 按指定模式开始新游戏。
* @param mode 游戏模式,取值来自 GameMode。
*/
void StartGameWithMode(int mode); void StartGameWithMode(int mode);
/**
* @brief 返回主菜单并清理临时玩法与界面状态。
*/
void ReturnToMainMenu(); void ReturnToMainMenu();
/**
* @brief 复活视频播放成功后恢复游戏并清理顶部空间。
*/
void ReviveAfterVideo(); void ReviveAfterVideo();
/**
* @brief 从帮助页进入 Rogue 技能演示的第一项。
*/
void StartRogueSkillDemo(); void StartRogueSkillDemo();
/**
* @brief 从帮助页进入指定 Rogue 技能演示。
* @param demoIndex 技能演示序号。
*/
void StartRogueSkillDemoAt(int demoIndex); void StartRogueSkillDemoAt(int demoIndex);
/**
* @brief 重新开始当前 Rogue 技能演示场景。
*/
void RestartCurrentRogueSkillDemo(); void RestartCurrentRogueSkillDemo();
/**
* @brief 判断当前是否处于 Rogue 技能演示模式。
* @return 演示模式中返回 true,否则返回 false。
*/
bool IsRogueSkillDemoMode(); bool IsRogueSkillDemoMode();
/**
* @brief 推进 Rogue 技能演示计时。
* @return 演示模式正在运行返回 true,否则返回 false。
*/
bool TickRogueSkillDemo(); bool TickRogueSkillDemo();
/**
* @brief 切换到下一项 Rogue 技能演示。
*/
void AdvanceRogueSkillDemo(); void AdvanceRogueSkillDemo();
/**
* @brief 获取 Rogue 技能演示条目数量。
* @return 可选择的演示条目总数。
*/
int GetRogueSkillDemoCount(); int GetRogueSkillDemoCount();
/**
* @brief 获取指定 Rogue 技能演示名称。
* @param demoIndex 技能演示序号。
* @return 名称字符串,越界时返回空字符串。
*/
const TCHAR* GetRogueSkillDemoName(int demoIndex); const TCHAR* GetRogueSkillDemoName(int demoIndex);
/**
* @brief 获取指定 Rogue 技能演示说明。
* @param demoIndex 技能演示序号。
* @return 说明字符串,越界时返回空字符串。
*/
const TCHAR* GetRogueSkillDemoDetail(int demoIndex); const TCHAR* GetRogueSkillDemoDetail(int demoIndex);
/**
* @brief 获取当前 Rogue 技能演示名称。
* @return 当前名称,非演示模式返回空字符串。
*/
const TCHAR* GetCurrentRogueSkillDemoName(); const TCHAR* GetCurrentRogueSkillDemoName();
/**
* @brief 设置右侧战斗日志反馈信息。
* @param title 反馈标题。
* @param detail 反馈详情。
* @param ticks 保持显示的游戏计时次数。
*/
void SetFeedbackMessage(const TCHAR* title, const TCHAR* detail, int ticks); void SetFeedbackMessage(const TCHAR* title, const TCHAR* detail, int ticks);
/**
* @brief 打开帮助首页。
*/
void OpenRulesScreen(); void OpenRulesScreen();
/**
* @brief 打开 Rogue 技能演示选择页。
*/
void OpenSkillDemoScreen(); void OpenSkillDemoScreen();
/**
* @brief 打开致谢页。
*/
void OpenCreditScreen(); void OpenCreditScreen();
/**
* @brief 切换致谢页图片。
* @param direction 小于 0 向前切换,大于 0 向后切换。
*/
void ChangeCreditPage(int direction); void ChangeCreditPage(int direction);
/**
* @brief 打开 Rogue 升级选择界面。
*/
void OpenUpgradeMenu(); void OpenUpgradeMenu();
/**
* @brief 确认当前升级选择并恢复游戏流程。
*/
void ConfirmUpgradeSelection(); void ConfirmUpgradeSelection();
/**
* @brief 重置升级选择界面状态。
*/
void ResetUpgradeUiState(); void ResetUpgradeUiState();
/**
* @brief 使用或解锁后处理 Hold 备用仓逻辑。
*/
void HoldCurrentPiece(); void HoldCurrentPiece();
/**
* @brief 使用清屏炸弹主动技能。
*/
void UseScreenBomb(); void UseScreenBomb();
/**
* @brief 使用黑洞主动技能。
*/
void UseBlackHole(); void UseBlackHole();
/**
* @brief 使用空中换形主动技能。
*/
void UseAirReshape(); void UseAirReshape();
/**
* @brief 重置 Rogue 待播放视觉事件。
*/
void ResetPendingRogueVisualEvents(); void ResetPendingRogueVisualEvents();
/**
* @brief 清空所有视觉效果状态。
*/
void ResetVisualEffects(); void ResetVisualEffects();
/**
* @brief 推进视觉效果动画。
* @return 仍有动画需要刷新返回 true,否则返回 false。
*/
bool TickVisualEffects(); bool TickVisualEffects();
/**
* @brief 推进致谢页切换动画。
* @return 需要刷新界面返回 true,否则返回 false。
*/
bool TickCreditAnimation(); bool TickCreditAnimation();
/**
* @brief 触发标准消行动画。
* @param rows 被消除的行号数组。
* @param rowCount 行号数量。
* @param linesCleared 实际消除行数。
*/
void TriggerLineClearEffect(const int* rows, int rowCount, int linesCleared); void TriggerLineClearEffect(const int* rows, int rowCount, int linesCleared);
/**
* @brief 播放之前因升级界面暂存的消行动画。
*/
void PlayPendingLineClearEffect(); void PlayPendingLineClearEffect();
/**
* @brief 触发指定棋盘格的默认清除特效。
* @param cells 被清除格子数组。
* @param cellCount 格子数量。
* @param strongBurst 是否使用更强的爆裂粒子。
*/
void TriggerCellClearEffect(const Point* cells, int cellCount, bool strongBurst); void TriggerCellClearEffect(const Point* cells, int cellCount, bool strongBurst);
/**
* @brief 触发指定棋盘格的自定义颜色清除特效。
* @param cells 被清除格子数组。
* @param cellCount 格子数量。
* @param flashColor 高亮颜色。
* @param strongBurst 是否使用更强的爆裂粒子。
*/
void TriggerColoredCellClearEffect(const Point* cells, int cellCount, COLORREF flashColor, bool strongBurst); void TriggerColoredCellClearEffect(const Point* cells, int cellCount, COLORREF flashColor, bool strongBurst);
/**
* @brief 记录一个固定方块受重力下落的轨迹。
* @param x 棋盘列号。
* @param fromY 下落起始行号。
* @param toY 下落目标行号。
* @param cellValue 方块格子数值。
*/
void TriggerGravityFallEffect(int x, int fromY, int toY, int cellValue); void TriggerGravityFallEffect(int x, int fromY, int toY, int cellValue);
/**
* @brief 为 Rogue 主动或特殊技能清除格子发放奖励。
* @param clearedCells 清除格子数。
* @param scoreGain 返回本次得分增量。
* @param expGain 返回本次经验增量。
* @param allowLevelProgress 是否允许本次奖励触发升级流程。
*/
void AwardRogueSkillClearRewards(int clearedCells, int& scoreGain, int& expGain, bool allowLevelProgress); void AwardRogueSkillClearRewards(int clearedCells, int& scoreGain, int& expGain, bool allowLevelProgress);
/**
* @brief 检查 Rogue 经验是否达到升级条件。
*/
void CheckRogueLevelProgress(); void CheckRogueLevelProgress();
/**
* @brief 对棋盘固定方块应用重力下落。
*/
void ApplyBoardGravity(); void ApplyBoardGravity();
/**
* @brief 计算当前 Rogue 模式下落间隔。
* @return 下落计时器间隔,单位毫秒。
*/
int GetRogueFallInterval(); int GetRogueFallInterval();
/**
* @brief 获取 Rogue 当前可操作棋盘高度。
* @return 未被底部封锁占用的行数。
*/
int GetRoguePlayableHeight(); int GetRoguePlayableHeight();
/**
* @brief 获取 Rogue 难度系统当前封锁的底部行数。
* @return 封锁行数。
*/
int GetRogueLockedRows(); int GetRogueLockedRows();
/**
* @brief 按经过时间推进 Rogue 难度。
* @param elapsedMs 本次推进的时间,单位毫秒。
*/
void AdvanceRogueDifficulty(int elapsedMs); void AdvanceRogueDifficulty(int elapsedMs);
/**
* @brief 获取进化强化的合成路线文本。
* @param upgradeId 强化编号。
* @return 路线文本;普通强化返回空或空指针。
*/
const TCHAR* GetUpgradeSynthesisPath(int upgradeId); const TCHAR* GetUpgradeSynthesisPath(int upgradeId);
/**
* @brief 绘制当前窗口中的完整游戏界面。
* @param hdc 目标绘图设备上下文。
* @param hWnd 当前窗口句柄,用于读取客户区大小。
*/
void TDrawScreen(HDC hdc, HWND hWnd); void TDrawScreen(HDC hdc, HWND hWnd);
+233
View File
@@ -0,0 +1,233 @@
#pragma once
/**
* @file TetrisAppInternal.h
* @brief 声明窗口布局、输入、计时器和媒体播放等应用层内部接口。
*/
#include "Tetris.h"
constexpr int GAME_TIMER_ID = 1;
constexpr int EFFECT_TIMER_ID = 2;
constexpr int CREDIT_TIMER_ID = 3;
constexpr int WM_CREDIT_TICK = WM_APP + 1;
constexpr int GAME_TIMER_INTERVAL = 500;
constexpr int EFFECT_TIMER_INTERVAL = 16;
constexpr int CREDIT_TIMER_INTERVAL = 5;
/**
* @brief 当前窗口缩放后的布局参数。
*
* 输入命中区域和渲染坐标必须使用同一套缩放参数,才能保证鼠标点击位置
* 与屏幕上看到的按钮、卡片位置一致。
*/
struct LayoutMetrics
{
int scale;
int offsetX;
int offsetY;
int layoutWidth;
int layoutHeight;
int grid;
};
/**
* @brief 根据当前窗口大小计算整体界面缩放与偏移。
* @param hWnd 当前窗口句柄。
* @return 布局缩放、偏移和网格尺寸。
*/
LayoutMetrics GetLayoutMetrics(HWND hWnd);
/**
* @brief 按当前布局比例缩放一个尺寸值。
* @param metrics 当前布局参数。
* @param value 设计稿尺寸值。
* @return 缩放后的像素尺寸。
*/
int ScaleValue(const LayoutMetrics& metrics, int value);
/**
* @brief 按当前布局比例缩放横坐标并叠加窗口偏移。
* @param metrics 当前布局参数。
* @param value 设计稿横坐标。
* @return 实际窗口横坐标。
*/
int ScaleXValue(const LayoutMetrics& metrics, int value);
/**
* @brief 按当前布局比例缩放纵坐标并叠加窗口偏移。
* @param metrics 当前布局参数。
* @param value 设计稿纵坐标。
* @return 实际窗口纵坐标。
*/
int ScaleYValue(const LayoutMetrics& metrics, int value);
/**
* @brief 获取主菜单选项的点击区域。
* @param hWnd 当前窗口句柄。
* @param index 菜单选项序号。
* @return 选项在窗口中的矩形区域。
*/
RECT GetMenuOptionRect(HWND hWnd, int index);
/**
* @brief 获取帮助页选项的点击区域。
* @param hWnd 当前窗口句柄。
* @param index 帮助首页选项序号。
* @return 选项在窗口中的矩形区域。
*/
RECT GetHelpOptionRect(HWND hWnd, int index);
/**
* @brief 获取技能演示列表项的点击区域。
* @param hWnd 当前窗口句柄。
* @param index 技能演示条目序号。
* @return 条目在窗口中的矩形区域。
*/
RECT GetHelpSkillDemoItemRect(HWND hWnd, int index);
/**
* @brief 获取帮助页底部返回提示的点击区域。
* @param hWnd 当前窗口句柄。
* @return 返回提示在窗口中的矩形区域。
*/
RECT GetHelpBackHintRect(HWND hWnd);
/**
* @brief 获取致谢页左右切换按钮的点击区域。
* @param hWnd 当前窗口句柄。
* @param direction 小于 0 表示左箭头,大于 0 表示右箭头。
* @return 切换按钮在窗口中的矩形区域。
*/
RECT GetCreditArrowRect(HWND hWnd, int direction);
/**
* @brief 获取升级选择卡片的点击区域。
* @param hWnd 当前窗口句柄。
* @param index 强化卡片序号。
* @return 卡片在窗口中的矩形区域。
*/
RECT GetUpgradeCardRect(HWND hWnd, int index);
/**
* @brief 获取暂停或结束覆盖层按钮的点击区域。
* @param hWnd 当前窗口句柄。
* @param index 按钮序号。
* @param buttonCount 覆盖层当前按钮总数。
* @return 按钮在窗口中的矩形区域。
*/
RECT GetOverlayButtonRect(HWND hWnd, int index, int buttonCount);
/**
* @brief 获取左上角返回按钮的点击区域。
* @param hWnd 当前窗口句柄。
* @return 返回按钮在窗口中的矩形区域。
*/
RECT GetBackButtonRect(HWND hWnd);
/**
* @brief 获取右下角音乐按钮的点击区域。
* @param hWnd 当前窗口句柄。
* @return 音乐按钮在窗口中的矩形区域。
*/
RECT GetMusicButtonRect(HWND hWnd);
/**
* @brief 判断点坐标是否落在矩形内部。
* @param rect 待判断矩形。
* @param x 点的横坐标。
* @param y 点的纵坐标。
* @return 点在矩形内返回 true,否则返回 false。
*/
bool IsPointInRect(const RECT& rect, int x, int y);
/**
* @brief 将滚动偏移按步长调整并限制在有效范围内。
* @param scrollOffset 需要修改的滚动偏移。
* @param delta 本次滚动增量。
*/
void AdjustScrollOffset(int& scrollOffset, int delta);
/**
* @brief 获取适配当前窗口缩放的一次滚动步长。
* @param hWnd 当前窗口句柄。
* @param baseStep 设计稿中的基础滚动步长。
* @return 缩放后的滚动步长。
*/
int GetScrollStep(HWND hWnd, int baseStep);
/**
* @brief 重置主下落定时器。
* @param hWnd 当前窗口句柄。
*/
void ResetGameTimer(HWND hWnd);
/**
* @brief 启动游戏、特效和致谢页动画定时器。
* @param hWnd 当前窗口句柄。
*/
void StartAppTimers(HWND hWnd);
/**
* @brief 停止游戏、特效和致谢页动画定时器。
* @param hWnd 当前窗口句柄。
*/
void StopAppTimers(HWND hWnd);
/**
* @brief 处理致谢页高频动画刷新消息。
* @param hWnd 当前窗口句柄。
*/
void HandleCreditTick(HWND hWnd);
/**
* @brief 处理窗口定时器消息。
* @param hWnd 当前窗口句柄。
* @param timerId 触发的定时器编号。
*/
void HandleTimerMessage(HWND hWnd, WPARAM timerId);
/**
* @brief 启动背景音乐。
*/
void StartBackgroundMusic();
/**
* @brief 停止背景音乐。
*/
void StopBackgroundMusic();
/**
* @brief 切换背景音乐开关并刷新窗口。
* @param hWnd 当前窗口句柄。
*/
void ToggleBackgroundMusic(HWND hWnd);
/**
* @brief 播放复活视频,播放成功返回 true。
* @param hWnd 当前窗口句柄,用于 MCI 播放和父窗口绑定。
* @return 播放成功返回 true,否则返回 false。
*/
bool PlayReviveVideo(HWND hWnd);
/**
* @brief 处理鼠标左键释放事件,返回是否已处理。
* @param hWnd 当前窗口句柄。
* @param lParam 鼠标消息坐标参数。
* @return 事件已被界面逻辑消费返回 true,否则返回 false。
*/
bool HandleMouseClick(HWND hWnd, LPARAM lParam);
/**
* @brief 处理鼠标滚轮事件。
* @param hWnd 当前窗口句柄。
* @param wParam 鼠标滚轮消息参数。
*/
void HandleMouseWheel(HWND hWnd, WPARAM wParam);
/**
* @brief 处理键盘按键事件。
* @param hWnd 当前窗口句柄。
* @param wParam 按键虚拟键码。
*/
void HandleKeyDown(HWND hWnd, WPARAM wParam);
+32
View File
@@ -0,0 +1,32 @@
#pragma once
/**
* @file TetrisAssets.h
* @brief 声明资源路径拼接和文件存在性检查工具函数。
*/
#include "stdafx.h"
#include <string>
// 资源路径函数同时服务图片、音频和视频加载,调用方只传相对路径。
/**
* @brief 根据程序所在目录拼出项目资源文件的绝对路径。
* @param relativePath 相对于项目根目录的资源路径。
* @return 规范化后的绝对路径;解析失败时返回拼接路径。
*/
std::wstring BuildAssetPath(const wchar_t* relativePath);
/**
* @brief 根据当前工作目录拼出项目资源文件的绝对路径。
* @param relativePath 相对于当前工作目录的资源路径。
* @return 规范化后的绝对路径;解析失败时返回拼接路径。
*/
std::wstring BuildWorkingDirAssetPath(const wchar_t* relativePath);
/**
* @brief 判断指定路径是否存在且不是目录。
* @param path 待检查的文件路径。
* @return 文件存在且不是目录返回 true,否则返回 false。
*/
bool FileExists(const std::wstring& path);
+118
View File
@@ -1,5 +1,10 @@
#pragma once #pragma once
/**
* @file TetrisLogicInternal.h
* @brief 声明棋盘逻辑、Rogue 结算和特殊方块效果使用的内部接口。
*/
#include "Tetris.h" #include "Tetris.h"
extern Point pendingChainBombCenter; extern Point pendingChainBombCenter;
@@ -9,88 +14,182 @@ extern int pendingLineClearEffectRows[8];
extern int pendingLineClearEffectRowCount; extern int pendingLineClearEffectRowCount;
extern int pendingLineClearEffectLineCount; extern int pendingLineClearEffectLineCount;
// Internal 头文件只暴露跨 cpp 文件共享的辅助函数,外部窗口层仍通过 Tetris.h 调用公开接口。
/** /**
* @brief 计算指定方块在棋盘顶部的统一生成位置。 * @brief 计算指定方块在棋盘顶部的统一生成位置。
* @param brickType 方块类型编号。
* @return 生成坐标,可能位于可视区域上方。
*/ */
Point GetSpawnPoint(int brickType); Point GetSpawnPoint(int brickType);
/**
* @brief 收集当前方块将要固定到棋盘上的格子,并写入工作区。
* @param overflowTop 返回是否有方块格位于可视区域顶部之外。
* @param fixedCells 返回普通落地格数组。
* @param fixedCellCount 返回普通落地格数量。
* @param explosiveCells 返回爆破方块落地格数组。
* @param explosiveCellCount 返回爆破格数量。
*/
void CollectAndWriteFixedCells(
bool& overflowTop,
Point fixedCells[],
int& fixedCellCount,
Point explosiveCells[],
int& explosiveCellCount);
/**
* @brief 处理方块固定时的顶部溢出、终末清场和最后一搏。
* @param overflowTop 是否出现顶部溢出。
* @return 游戏可以继续返回 true,需要结束返回 false。
*/
bool ResolveFixingOverflow(bool overflowTop);
/**
* @brief 生成下一枚活动方块,并刷新 Hold、特殊方块和预测落点状态。
*/
void SpawnNextFallingPiece();
/**
* @brief 从底向上扫描满行并删除,记录本次消除的原始行号。
* @param clearedRows 返回最多 8 个被消除行号。
* @param clearedRowCount 返回记录的行号数量。
* @return 本次标准消行数量。
*/
int ScanAndDeleteFullLines(int clearedRows[], int& clearedRowCount);
/**
* @brief 根据当前界面状态立即播放或暂存消行动画。
* @param clearedRows 已消除行号数组。
* @param clearedRowCount 行号数量。
* @param clearedLines 本次消行数量。
*/
void DispatchLineClearEffect(const int clearedRows[], int clearedRowCount, int clearedLines);
/**
* @brief 处理连环炸弹因消行触发的一次追加爆破。
* @param clearedLines 本次标准消行数量。
*/
void ResolveChainBombFollowup(int clearedLines);
/** /**
* @brief 重置经典或 Rogue 模式使用的玩家统计数据。 * @brief 重置经典或 Rogue 模式使用的玩家统计数据。
* @param stats 需要重置的统计结构。
* @param useRogueRules 是否按 Rogue 模式设置初始经验需求。
*/ */
void ResetPlayerStats(PlayerStats& stats, bool useRogueRules); void ResetPlayerStats(PlayerStats& stats, bool useRogueRules);
/** /**
* @brief 设置界面右侧显示的即时反馈标题、内容和持续时间。 * @brief 设置界面右侧显示的即时反馈标题、内容和持续时间。
* @param title 反馈标题。
* @param detail 反馈详情。
* @param ticks 显示持续的游戏计时次数。
*/ */
void SetFeedbackMessage(const TCHAR* title, const TCHAR* detail, int ticks); void SetFeedbackMessage(const TCHAR* title, const TCHAR* detail, int ticks);
/** /**
* @brief 判断指定方块、旋转状态和位置是否可以合法放置。 * @brief 判断指定方块、旋转状态和位置是否可以合法放置。
* @param pieceType 方块类型编号。
* @param pieceState 方块旋转状态。
* @param position 待检测的左上角坐标。
* @return 可以放置返回 true,否则返回 false。
*/ */
bool IsPiecePlacementValid(int pieceType, int pieceState, Point position); bool IsPiecePlacementValid(int pieceType, int pieceState, Point position);
/** /**
* @brief 判断棋盘格是否为彩虹特殊方块。 * @brief 判断棋盘格是否为彩虹特殊方块。
* @param cellValue 棋盘格存储值。
* @return 彩虹方块返回 true,否则返回 false。
*/ */
bool IsRainbowBoardCell(int cellValue); bool IsRainbowBoardCell(int cellValue);
/** /**
* @brief 触发小型黑洞并返回被清除的固定方块数量。 * @brief 触发小型黑洞并返回被清除的固定方块数量。
* @param maxCellsToClear 最多清除的格子数。
* @return 实际清除格子数。
*/ */
int TriggerMiniBlackHole(int maxCellsToClear); int TriggerMiniBlackHole(int maxCellsToClear);
/** /**
* @brief 触发彩虹方块行清除与覆盖行染色效果。 * @brief 触发彩虹方块行清除与覆盖行染色效果。
* @param anchorRow 作为主色判断的中心行。
* @param minRow 允许染色范围的最小行。
* @param maxRow 允许染色范围的最大行。
* @param recoloredCount 返回被染色的格子数。
* @return 被清除的主色格子数。
*/ */
int TriggerRainbowColorShift(int anchorRow, int minRow, int maxRow, int& recoloredCount); int TriggerRainbowColorShift(int anchorRow, int minRow, int maxRow, int& recoloredCount);
/** /**
* @brief 引爆清屏炸弹并返回清除格数。 * @brief 引爆清屏炸弹并返回清除格数。
* @return 实际清除格子数。
*/ */
int TriggerScreenBomb(); int TriggerScreenBomb();
/** /**
* @brief 清除指定中心点周围的爆破范围并返回清除格数。 * @brief 清除指定中心点周围的爆破范围并返回清除格数。
* @param centerY 爆破中心行。
* @param centerX 爆破中心列。
* @return 实际清除格子数。
*/ */
int ClearExplosiveAreaAt(int centerY, int centerX); int ClearExplosiveAreaAt(int centerY, int centerX);
/** /**
* @brief 清除指定列并返回清除格数。 * @brief 清除指定列并返回清除格数。
* @param column 目标列号。
* @return 实际清除格子数。
*/ */
int ClearColumnAt(int column); int ClearColumnAt(int column);
/** /**
* @brief 使用指定颜色特效清除指定列并返回清除格数。 * @brief 使用指定颜色特效清除指定列并返回清除格数。
* @param column 目标列号。
* @param flashColor 清除高亮颜色。
* @return 实际清除格子数。
*/ */
int ClearColumnAtWithColor(int column, COLORREF flashColor); int ClearColumnAtWithColor(int column, COLORREF flashColor);
/** /**
* @brief 清除指定行并返回清除格数。 * @brief 清除指定行并返回清除格数。
* @param row 目标行号。
* @return 实际清除格子数。
*/ */
int ClearRowAt(int row); int ClearRowAt(int row);
/** /**
* @brief 尝试填补局部空洞以稳定棋盘结构。 * @brief 尝试填补局部空洞以稳定棋盘结构。
* @return 实际填补格子数。
*/ */
int TryStabilizeBoard(); int TryStabilizeBoard();
/** /**
* @brief 为当前方块刷新 Rogue 特殊方块标记。 * @brief 为当前方块刷新 Rogue 特殊方块标记。
* @param allowRandomSpecials 是否允许按强化概率随机生成特殊方块。
*/ */
void RollCurrentPieceSpecialFlags(bool allowRandomSpecials); void RollCurrentPieceSpecialFlags(bool allowRandomSpecials);
/** /**
* @brief 暂存消行动画,等待升级选择结束后再播放。 * @brief 暂存消行动画,等待升级选择结束后再播放。
* @param rows 被消除的行号数组。
* @param rowCount 行号数量。
* @param linesCleared 实际消除行数。
*/ */
void QueueLineClearEffect(const int* rows, int rowCount, int linesCleared); void QueueLineClearEffect(const int* rows, int rowCount, int linesCleared);
/** /**
* @brief 记录固定方块受重力下落的轨迹,用于播放纵向残影特效。 * @brief 记录固定方块受重力下落的轨迹,用于播放纵向残影特效。
* @param x 棋盘列号。
* @param fromY 起始行号。
* @param toY 目标行号。
* @param cellValue 方块格子值。
*/ */
void TriggerGravityFallEffect(int x, int fromY, int toY, int cellValue); void TriggerGravityFallEffect(int x, int fromY, int toY, int cellValue);
/** /**
* @brief 尝试把旋转后的方块横向偏移指定格数后放置。 * @brief 尝试把旋转后的方块横向偏移指定格数后放置。
* @param nextState 旋转后的状态编号。
* @param offsetX 横向试探偏移。
* @return 偏移后可以放置返回 true,否则返回 false。
*/ */
bool TryRotateWithOffset(int nextState, int offsetX); bool TryRotateWithOffset(int nextState, int offsetX);
@@ -101,10 +200,29 @@ void ResetNextQueue();
/** /**
* @brief 消费队首下一方块并补充新的预览方块。 * @brief 消费队首下一方块并补充新的预览方块。
* @return 新的当前方块类型编号。
*/ */
int ConsumeNextType(); int ConsumeNextType();
/** /**
* @brief 结算一次标准消行带来的 Rogue 玩法效果。 * @brief 结算一次标准消行带来的 Rogue 玩法效果。
* @param linesCleared 本次标准消行数量。
*/ */
void ApplyLineClearResult(int linesCleared); void ApplyLineClearResult(int linesCleared);
/**
* @brief 结算彩虹方块固定后的染色和清除效果。
* @param overflowTop 固定时是否已经越过顶部。
* @param fixedCells 当前方块写入棋盘的格子数组。
* @param fixedCellCount 写入棋盘的格子数量。
*/
void ApplyRainbowLandingEffect(bool overflowTop, const Point* fixedCells, int fixedCellCount);
/**
* @brief 结算爆破、激光、十字和稳定结构等特殊落地效果。
* @param fixedCells 当前方块写入棋盘的格子数组。
* @param fixedCellCount 写入棋盘的格子数量。
* @param explosiveCells 爆破方块写入棋盘的格子数组。
* @param explosiveCellCount 爆破格子数量。
*/
void ApplySpecialLandingEffects(const Point* fixedCells, int fixedCellCount, const Point* explosiveCells, int explosiveCellCount);
+32
View File
@@ -0,0 +1,32 @@
#pragma once
/**
* @file TetrisRenderInternal.h
* @brief 声明渲染模块内部使用的 GDI+ 图片加载接口。
*/
#include "Tetris.h"
#include <objidl.h>
#include <gdiplus.h>
// 本内部头文件只给渲染拆分模块使用,外部代码仍通过 TDrawScreen 调用绘制入口。
/**
* @brief 加载并缓存主背景图片。
* @return 成功时返回缓存位图指针,失败时返回 nullptr。
*/
Gdiplus::Bitmap* LoadBackgroundImage();
/**
* @brief 按序号加载并缓存致谢页图片。
* @param index 致谢页图片序号。
* @return 成功时返回缓存位图指针,失败或越界时返回 nullptr。
*/
Gdiplus::Bitmap* LoadCreditImage(int index);
/**
* @brief 绘制完整游戏界面,供 TDrawScreen 总入口调用。
* @param hdc 目标绘图设备上下文。
* @param hWnd 当前窗口句柄。
*/
void RenderFullScreen(HDC hdc, HWND hWnd);
+7 -3
View File
@@ -1,9 +1,13 @@
#pragma once #pragma once
/**
* @file resource.h
* @brief Defines Windows resource IDs for menus, icons, dialogs, commands, and strings.
*/
//{{NO_DEPENDENCIES}} //{{NO_DEPENDENCIES}}
// Microsoft Visual C++ 生成的包含文件。 // Microsoft Visual C++ generated include file.
// Tetris.rc 使用 // Used by Tetris.rc.
//
#define IDS_APP_TITLE 103 #define IDS_APP_TITLE 103
+7 -6
View File
@@ -1,17 +1,18 @@
// stdafx.h : 标准系统包含文件的包含文件, /**
// 或是经常使用但不常更改的 * @file stdafx.h
// 特定于项目的包含文件 * @brief 集中包含 Windows、C 运行时和项目常用基础头文件。
// */
#pragma once #pragma once
#include "targetver.h" #include "targetver.h"
// 精简 Windows 头文件,缩短编译时间,同时保留本项目需要的 Win32/GDI API。
#define WIN32_LEAN_AND_MEAN // 从 Windows 头文件中排除极少使用的信息 #define WIN32_LEAN_AND_MEAN // 从 Windows 头文件中排除极少使用的信息
// Windows 头文件: // Windows 头文件:
#include <windows.h> #include <windows.h>
// C 运行时头文件 // C 运行时头文件:本项目使用随机数、内存工具、TCHAR 字符串和时间函数。
#include <stdlib.h> #include <stdlib.h>
#include <malloc.h> #include <malloc.h>
#include <memory.h> #include <memory.h>
@@ -19,4 +20,4 @@
#include <time.h> #include <time.h>
// TODO: 在此处引用程序需要的其他头文件 // 其他模块各自包含自己的业务头文件,避免预编译头承担过多项目依赖。
+8 -3
View File
@@ -1,8 +1,13 @@
#pragma once #pragma once
// 包括 SDKDDKVer.h 将定义可用的最高版本的 Windows 平台。 /**
* @file targetver.h
* @brief 设置 Windows SDK 目标平台版本,供 Win32 头文件选择可用 API。
*/
// 如果要为以前的 Windows 平台生成应用程序,请包括 WinSDKVer.h,并将 // 包括 SDKDDKVer.h 将定义可用的最高版本 Windows 平台宏。
// WIN32_WINNT 宏设置为要支持的平台,然后再包括 SDKDDKVer.h。
// 若课程演示环境需要兼容更旧 Windows,可在这里先包含 WinSDKVer.h
// 再设置 WIN32_WINNT;当前项目直接使用 SDK 默认最高版本。
#include <SDKDDKVer.h> #include <SDKDDKVer.h>
Binary file not shown.
+91 -1524
View File
File diff suppressed because it is too large Load Diff
+46 -419
View File
@@ -1,4 +1,9 @@
#include "stdafx.h" #include "stdafx.h"
/**
* @file TetrisLogic.cpp
* @brief 实现基础俄罗斯方块的移动、旋转、固定、消行、落点计算和重开逻辑。
*/
#include "Tetris.h" #include "Tetris.h"
#include "TetrisLogicInternal.h" #include "TetrisLogicInternal.h"
@@ -45,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}},
@@ -91,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),
@@ -100,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 判断当前方块是否可以继续向下移动。
* *
@@ -332,10 +268,11 @@ void MoveRight()
* *
* 游戏中的每种方块都预置了 4 种旋转状态,该函数会先尝试切换到下一状态, * 游戏中的每种方块都预置了 4 种旋转状态,该函数会先尝试切换到下一状态,
* 然后检查旋转后的方块是否越界或与固定方块重叠。 * 然后检查旋转后的方块是否越界或与固定方块重叠。
* 如果旋转后的状态非法,则恢复到旋转前的状态 * 如果旋转后的状态非法,Rogue 的完美旋转会继续尝试左右各偏移一格
*/ */
void Rotate() void Rotate()
{ {
// 第一阶段:直接尝试原地旋转。
int nextState = (state + 1) % 4; int nextState = (state + 1) % 4;
if (IsPiecePlacementValid(type, nextState, point)) if (IsPiecePlacementValid(type, nextState, point))
{ {
@@ -345,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;
@@ -388,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;
@@ -395,280 +334,27 @@ 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) ApplyRainbowLandingEffect(overflowTop, fixedCells, fixedCellCount);
{
overflowTop = true;
}
// 将当前方块在可视区域内的部分写入工作区 // 第三阶段:统一处理顶部溢出,可能触发最后一搏或直接游戏结束。
if (fixY >= 0 && fixY < GetRoguePlayableHeight() && fixX >= 0 && fixX < nGameWidth) if (!ResolveFixingOverflow(overflowTop))
{ {
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++;
}
}
}
}
}
if (!overflowTop && currentPieceIsRainbow)
{
int rainbowAnchorRow = point.y + 1;
if (fixedCellCount > 0)
{
int ySum = 0;
for (int i = 0; i < fixedCellCount; i++)
{
ySum += fixedCells[i].y;
}
rainbowAnchorRow = (ySum + fixedCellCount / 2) / fixedCellCount;
}
if (rainbowAnchorRow < 0)
{
rainbowAnchorRow = 0;
}
if (rainbowAnchorRow >= GetRoguePlayableHeight())
{
rainbowAnchorRow = GetRoguePlayableHeight() - 1;
}
int rainbowRecoloredCount = 0;
int rainbowClearedCount = TriggerRainbowColorShift(rainbowAnchorRow, point.y, point.y + 3, rainbowRecoloredCount);
int rainbowScore = 0;
int rainbowExp = 0;
int voidClearedCount = 0;
int voidScore = 0;
int voidExp = 0;
if (currentMode == MODE_ROGUE && rainbowClearedCount > 0)
{
AwardRogueSkillClearRewards(rainbowClearedCount, rainbowScore, rainbowExp, false);
if (rogueStats.voidCoreLevel > 0)
{
voidClearedCount = TriggerMiniBlackHole(5);
AwardRogueSkillClearRewards(voidClearedCount, voidScore, voidExp, false);
}
}
TCHAR rainbowDetail[128];
if (voidClearedCount > 0)
{
_stprintf_s(
rainbowDetail,
_T("第 %d 行清 %d 格,染色 %d 格,虚空追加 %d 格"),
rainbowAnchorRow + 1,
rainbowClearedCount,
rainbowRecoloredCount,
voidClearedCount);
}
else
{
_stprintf_s(
rainbowDetail,
_T("第 %d 行清除主色 %d 格,覆盖行染色 %d 格。"),
rainbowAnchorRow + 1,
rainbowClearedCount,
rainbowRecoloredCount);
}
SetFeedbackMessage(_T("彩虹方块"), rainbowDetail, 10);
}
if (overflowTop)
{
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);
}
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; return;
} }
}
if (currentPieceIsExplosive) // 第四阶段:结算爆破、激光、十字和稳定结构等普通特殊落地效果。
{ ApplySpecialLandingEffects(fixedCells, fixedCellCount, explosiveCells, explosiveCellCount);
int explosiveCellsCleared = 0;
for (int i = 0; i < explosiveCellCount; i++)
{
explosiveCellsCleared += ClearExplosiveAreaAt(explosiveCells[i].y, explosiveCells[i].x);
}
int explosiveScoreGain = 0;
int explosiveExpGain = 0;
if (currentMode == MODE_ROGUE && explosiveCellsCleared > 0)
{
AwardRogueSkillClearRewards(explosiveCellsCleared, explosiveScoreGain, explosiveExpGain, false);
}
TCHAR explosiveDetail[128];
_stprintf_s(
explosiveDetail,
_T("爆破清除 %d 格 +%d 分 +%d EXP"),
explosiveCellsCleared,
explosiveScoreGain,
explosiveExpGain);
SetFeedbackMessage(_T("爆破核心"), explosiveDetail, 12);
if (rogueStats.chainBombLevel > 0 && explosiveCellCount > 0)
{
pendingChainBombCenter = explosiveCells[0];
pendingChainBombFollowup = true;
}
}
if (currentPieceIsLaser)
{
int laserColumn = point.x + 1;
if (fixedCellCount > 0)
{
int xSum = 0;
for (int i = 0; i < fixedCellCount; i++)
{
xSum += fixedCells[i].x;
}
laserColumn = (xSum + fixedCellCount / 2) / fixedCellCount;
}
if (laserColumn < 0)
{
laserColumn = 0;
}
if (laserColumn >= nGameWidth)
{
laserColumn = nGameWidth - 1;
}
int laserCellsCleared = ClearColumnAt(laserColumn);
if (currentMode == MODE_ROGUE && laserCellsCleared > 0)
{
int laserScore = 0;
int laserExp = 0;
AwardRogueSkillClearRewards(laserCellsCleared, laserScore, laserExp, false);
TCHAR laserDetail[128];
_stprintf_s(laserDetail, _T("激光贯穿第 %d 列,清除 %d 格 +%d 分 +%d EXP"), laserColumn + 1, laserCellsCleared, laserScore, laserExp);
SetFeedbackMessage(_T("棱镜激光"), laserDetail, 12);
}
}
if (currentPieceIsCross)
{
int crossRow = point.y + 1;
int crossColumn = point.x + 1;
if (fixedCellCount > 0)
{
int xSum = 0;
int ySum = 0;
for (int i = 0; i < fixedCellCount; i++)
{
xSum += fixedCells[i].x;
ySum += fixedCells[i].y;
}
crossColumn = (xSum + fixedCellCount / 2) / fixedCellCount;
crossRow = (ySum + fixedCellCount / 2) / fixedCellCount;
}
if (crossRow < 0)
{
crossRow = 0;
}
if (crossRow >= GetRoguePlayableHeight())
{
crossRow = GetRoguePlayableHeight() - 1;
}
if (crossColumn < 0)
{
crossColumn = 0;
}
if (crossColumn >= nGameWidth)
{
crossColumn = nGameWidth - 1;
}
int crossCellsCleared = ClearRowAt(crossRow);
int columnCellsCleared = ClearColumnAtWithColor(crossColumn, RGB(196, 255, 132));
if (workRegion[crossRow][crossColumn] == 0 && columnCellsCleared > 0)
{
// center cell may already be counted by row clear
}
int totalCrossCleared = crossCellsCleared + columnCellsCleared;
if (currentMode == MODE_ROGUE && totalCrossCleared > 0)
{
int crossScore = 0;
int crossExp = 0;
AwardRogueSkillClearRewards(totalCrossCleared, crossScore, crossExp, false);
TCHAR crossDetail[128];
_stprintf_s(crossDetail, _T("十字冲击第 %d 行 / 第 %d 列,清除 %d 格 +%d 分 +%d EXP"), crossRow + 1, crossColumn + 1, totalCrossCleared, crossScore, crossExp);
SetFeedbackMessage(_T("十字方块"), crossDetail, 12);
}
}
if (!currentPieceIsRainbow && TryStabilizeBoard() > 0)
{
SetFeedbackMessage(_T("稳定结构"), _T("附近空洞被自动填补,阵型更加稳固。"), 10);
}
if (currentMode == MODE_ROGUE) if (currentMode == MODE_ROGUE)
{ {
currentFallInterval = GetRogueFallInterval(); currentFallInterval = GetRogueFallInterval();
} }
// 生成下一活动方块 // 第五阶段:刷新下一活动方块,开始新的下落回合。
type = ConsumeNextType(); SpawnNextFallingPiece();
nType = nextTypes[0];
state = 0;
holdUsedThisTurn = false;
RollCurrentPieceSpecialFlags(true);
point = GetSpawnPoint(type);
target = point;
ComputeTarget();
} }
/** /**
@@ -703,102 +389,38 @@ void DeleteOneLine(int number)
* 如果某一行全部非 0,则调用 DeleteOneLine 删除该行, * 如果某一行全部非 0,则调用 DeleteOneLine 删除该行,
* 并将该行上方的内容整体下移。为了避免连续满行被漏检, * 并将该行上方的内容整体下移。为了避免连续满行被漏检,
* 删除后会继续检查当前行号。每成功消除 1 行,当前得分增加 100 分。 * 删除后会继续检查当前行号。每成功消除 1 行,当前得分增加 100 分。
*
* @return 本次实际消除的行数。
*/ */
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 计算当前活动方块的预测落点位置。
* *
@@ -834,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++)
@@ -842,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();
@@ -864,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];
@@ -876,3 +502,4 @@ void Restart()
ComputeTarget(); ComputeTarget();
} }
File diff suppressed because it is too large Load Diff
+978
View File
@@ -0,0 +1,978 @@
#include "stdafx.h"
/**
* @file TetrisInput.cpp
* @brief 实现鼠标和键盘输入处理,负责菜单、帮助、升级界面和游戏操作分发。
*/
#include "TetrisAppInternal.h"
/**
* @brief 打开当前菜单选中的页面或开始对应模式。
* @param hWnd 当前窗口句柄,用于重置计时器和触发重绘。
*/
static void ActivateMenuSelection(HWND hWnd)
{
if (menuState.selectedIndex == 0)
{
StartGameWithMode(MODE_CLASSIC);
}
else if (menuState.selectedIndex == 1)
{
StartGameWithMode(MODE_ROGUE);
}
else if (menuState.selectedIndex == 2)
{
OpenRulesScreen();
}
else
{
OpenCreditScreen();
}
ResetGameTimer(hWnd);
InvalidateRect(hWnd, nullptr, FALSE);
}
/**
* @brief 处理返回按钮的统一点击行为。
* @param hWnd 当前窗口句柄,用于触发重绘。
*/
static void HandleBackButtonClick(HWND hWnd)
{
if (currentScreen == SCREEN_PLAYING && IsRogueSkillDemoMode())
{
OpenSkillDemoScreen();
}
else if (currentScreen == SCREEN_RULES && helpState.currentPage != 0)
{
helpState.selectedIndex = (helpState.currentPage == 5) ? 3 : helpState.currentPage - 1;
helpState.currentPage = 0;
helpScrollOffset = 0;
}
else
{
ReturnToMainMenu();
}
InvalidateRect(hWnd, nullptr, FALSE);
}
/**
* @brief 处理主菜单点击。
* @param hWnd 当前窗口句柄。
* @param mouseX 鼠标横坐标。
* @param mouseY 鼠标纵坐标。
* @return 当前界面是菜单时返回 true,否则返回 false。
*/
static bool HandleMenuClick(HWND hWnd, int mouseX, int mouseY)
{
if (currentScreen != SCREEN_MENU)
{
return false;
}
for (int i = 0; i < menuState.optionCount; i++)
{
if (!IsPointInRect(GetMenuOptionRect(hWnd, i), mouseX, mouseY))
{
continue;
}
menuState.selectedIndex = i;
ActivateMenuSelection(hWnd);
return true;
}
return true;
}
/**
* @brief 处理规则、帮助、致谢和技能演示页点击。
* @param hWnd 当前窗口句柄。
* @param mouseX 鼠标横坐标。
* @param mouseY 鼠标纵坐标。
* @return 当前界面是帮助页时返回 true,否则返回 false。
*/
static bool HandleRulesClick(HWND hWnd, int mouseX, int mouseY)
{
if (currentScreen != SCREEN_RULES)
{
return false;
}
if (helpState.currentPage == 0)
{
// 帮助首页的四个入口分别进入介绍、操作、图鉴和技能演示页。
for (int i = 0; i < helpState.optionCount; i++)
{
if (IsPointInRect(GetHelpOptionRect(hWnd, i), mouseX, mouseY))
{
helpState.selectedIndex = i;
if (i == 3)
{
helpState.currentPage = 5;
helpState.selectedIndex = 0;
helpScrollOffset = 0;
}
else
{
helpState.currentPage = i + 1;
helpScrollOffset = 0;
}
InvalidateRect(hWnd, nullptr, FALSE);
return true;
}
}
if (IsPointInRect(GetHelpBackHintRect(hWnd), mouseX, mouseY))
{
ReturnToMainMenu();
InvalidateRect(hWnd, nullptr, FALSE);
}
return true;
}
if (helpState.currentPage == 5)
{
if (IsPointInRect(GetHelpBackHintRect(hWnd), mouseX, mouseY))
{
helpState.currentPage = 0;
helpState.selectedIndex = 3;
helpScrollOffset = 0;
InvalidateRect(hWnd, nullptr, FALSE);
return true;
}
// 技能演示页的列表项直接启动对应预设棋盘。
int demoCount = GetRogueSkillDemoCount();
for (int i = 0; i < demoCount; i++)
{
if (IsPointInRect(GetHelpSkillDemoItemRect(hWnd, i), mouseX, mouseY))
{
helpState.selectedIndex = i;
StartRogueSkillDemoAt(i);
ResetGameTimer(hWnd);
InvalidateRect(hWnd, nullptr, FALSE);
return true;
}
}
return true;
}
if (IsPointInRect(GetHelpBackHintRect(hWnd), mouseX, mouseY))
{
helpState.selectedIndex = (helpState.currentPage == 5) ? 3 : helpState.currentPage - 1;
helpState.currentPage = 0;
helpScrollOffset = 0;
InvalidateRect(hWnd, nullptr, FALSE);
}
else if (helpState.currentPage == 4 && IsPointInRect(GetCreditArrowRect(hWnd, -1), mouseX, mouseY))
{
ChangeCreditPage(-1);
InvalidateRect(hWnd, nullptr, FALSE);
}
else if (helpState.currentPage == 4 && IsPointInRect(GetCreditArrowRect(hWnd, 1), mouseX, mouseY))
{
ChangeCreditPage(1);
InvalidateRect(hWnd, nullptr, FALSE);
}
return true;
}
/**
* @brief 处理升级选择界面点击。
* @param hWnd 当前窗口句柄。
* @param mouseX 鼠标横坐标。
* @param mouseY 鼠标纵坐标。
* @return 当前界面是升级选择时返回 true,否则返回 false。
*/
static bool HandleUpgradeClick(HWND hWnd, int mouseX, int mouseY)
{
if (currentScreen != SCREEN_UPGRADE)
{
return false;
}
for (int i = 0; i < upgradeUiState.optionCount; i++)
{
if (!IsPointInRect(GetUpgradeCardRect(hWnd, i), mouseX, mouseY))
{
continue;
}
upgradeUiState.selectedIndex = i;
// 多选强化先标记卡片,达到本次可选数量后再统一确认。
if (upgradeUiState.picksRemaining > 1)
{
bool currentlyMarked = upgradeUiState.marked[i];
if (currentlyMarked)
{
upgradeUiState.marked[i] = false;
if (upgradeUiState.markedCount > 0)
{
upgradeUiState.markedCount--;
}
}
else if (upgradeUiState.markedCount < upgradeUiState.picksRemaining)
{
upgradeUiState.marked[i] = true;
upgradeUiState.markedCount++;
}
if (upgradeUiState.markedCount == upgradeUiState.picksRemaining)
{
ConfirmUpgradeSelection();
ResetGameTimer(hWnd);
}
}
else
{
ConfirmUpgradeSelection();
ResetGameTimer(hWnd);
}
InvalidateRect(hWnd, nullptr, FALSE);
return true;
}
return true;
}
/**
* @brief 处理暂停和结束覆盖层点击。
* @param hWnd 当前窗口句柄。
* @param mouseX 鼠标横坐标。
* @param mouseY 鼠标纵坐标。
* @return 点击命中覆盖层按钮返回 true,否则返回 false。
*/
static bool HandleOverlayClick(HWND hWnd, int mouseX, int mouseY)
{
if (currentScreen == SCREEN_PLAYING && suspendFlag)
{
if (IsPointInRect(GetOverlayButtonRect(hWnd, 0, 2), mouseX, mouseY))
{
suspendFlag = false;
InvalidateRect(hWnd, nullptr, FALSE);
return true;
}
if (IsPointInRect(GetOverlayButtonRect(hWnd, 1, 2), mouseX, mouseY))
{
ReturnToMainMenu();
InvalidateRect(hWnd, nullptr, FALSE);
return true;
}
}
if (currentScreen == SCREEN_PLAYING && gameOverFlag)
{
if (reviveAvailable)
{
if (IsPointInRect(GetOverlayButtonRect(hWnd, 0, 3), mouseX, mouseY))
{
if (PlayReviveVideo(hWnd))
{
ReviveAfterVideo();
ResetGameTimer(hWnd);
}
InvalidateRect(hWnd, nullptr, FALSE);
return true;
}
if (IsPointInRect(GetOverlayButtonRect(hWnd, 1, 3), mouseX, mouseY))
{
StartGameWithMode(currentMode);
ResetGameTimer(hWnd);
InvalidateRect(hWnd, nullptr, FALSE);
return true;
}
if (IsPointInRect(GetOverlayButtonRect(hWnd, 2, 3), mouseX, mouseY))
{
ReturnToMainMenu();
InvalidateRect(hWnd, nullptr, FALSE);
return true;
}
}
else
{
if (IsPointInRect(GetOverlayButtonRect(hWnd, 0, 2), mouseX, mouseY))
{
StartGameWithMode(currentMode);
ResetGameTimer(hWnd);
InvalidateRect(hWnd, nullptr, FALSE);
return true;
}
if (IsPointInRect(GetOverlayButtonRect(hWnd, 1, 2), mouseX, mouseY))
{
ReturnToMainMenu();
InvalidateRect(hWnd, nullptr, FALSE);
return true;
}
}
}
return false;
}
/**
* @brief 处理鼠标左键释放事件,返回是否已处理。
* @param hWnd 当前窗口句柄。
* @param lParam 鼠标消息坐标参数。
* @return 事件已被界面逻辑消费返回 true,否则返回 false。
*/
bool HandleMouseClick(HWND hWnd, LPARAM lParam)
{
int mouseX = static_cast<short>(LOWORD(lParam));
int mouseY = static_cast<short>(HIWORD(lParam));
if (IsPointInRect(GetMusicButtonRect(hWnd), mouseX, mouseY))
{
ToggleBackgroundMusic(hWnd);
return true;
}
if (currentScreen != SCREEN_MENU && IsPointInRect(GetBackButtonRect(hWnd), mouseX, mouseY))
{
HandleBackButtonClick(hWnd);
return true;
}
if (HandleMenuClick(hWnd, mouseX, mouseY) ||
HandleRulesClick(hWnd, mouseX, mouseY) ||
HandleUpgradeClick(hWnd, mouseX, mouseY) ||
HandleOverlayClick(hWnd, mouseX, mouseY))
{
return true;
}
return false;
}
/**
* @brief 处理鼠标滚轮事件。
* @param hWnd 当前窗口句柄。
* @param wParam 鼠标滚轮消息参数。
*/
void HandleMouseWheel(HWND hWnd, WPARAM wParam)
{
int wheelDelta = GET_WHEEL_DELTA_WPARAM(wParam);
int direction = (wheelDelta > 0) ? -1 : 1;
int scrollStep = GetScrollStep(hWnd, 64);
if (currentScreen == SCREEN_RULES && helpState.currentPage != 0)
{
AdjustScrollOffset(helpScrollOffset, direction * scrollStep);
InvalidateRect(hWnd, nullptr, FALSE);
return;
}
if (currentScreen == SCREEN_PLAYING && currentMode == MODE_ROGUE)
{
AdjustScrollOffset(upgradeListScrollOffset, direction * scrollStep);
InvalidateRect(hWnd, nullptr, FALSE);
}
}
/**
* @brief 处理主菜单键盘导航。
* @param hWnd 当前窗口句柄。
* @param key 按键虚拟键码。
* @return 当前界面是菜单时返回 true,否则返回 false。
*/
static bool HandleMenuKey(HWND hWnd, WPARAM key)
{
if (currentScreen != SCREEN_MENU)
{
return false;
}
switch (key)
{
case VK_UP:
case VK_LEFT:
case 'W':
case 'A':
menuState.selectedIndex--;
if (menuState.selectedIndex < 0)
{
menuState.selectedIndex = menuState.optionCount - 1;
}
InvalidateRect(hWnd, nullptr, FALSE);
break;
case VK_DOWN:
case VK_RIGHT:
case 'S':
case 'D':
menuState.selectedIndex++;
if (menuState.selectedIndex >= menuState.optionCount)
{
menuState.selectedIndex = 0;
}
InvalidateRect(hWnd, nullptr, FALSE);
break;
case VK_RETURN:
case VK_SPACE:
ActivateMenuSelection(hWnd);
break;
case VK_ESCAPE:
DestroyWindow(hWnd);
break;
default:
break;
}
return true;
}
/**
* @brief 在帮助首页选项之间循环移动。
* @param direction 负数向前,正数向后。
*/
static void MoveHelpHomeSelection(int direction)
{
helpState.selectedIndex += direction;
if (helpState.selectedIndex < 0)
{
helpState.selectedIndex = helpState.optionCount - 1;
}
if (helpState.selectedIndex >= helpState.optionCount)
{
helpState.selectedIndex = 0;
}
}
/**
* @brief 在技能演示列表中移动选中项,并同步滚动偏移。
* @param direction 负数向上,正数向下。
*/
static void MoveSkillDemoSelection(int direction)
{
helpState.selectedIndex += direction;
if (helpState.selectedIndex < 0)
{
helpState.selectedIndex = GetRogueSkillDemoCount() - 1;
}
if (helpState.selectedIndex >= GetRogueSkillDemoCount())
{
helpState.selectedIndex = 0;
helpScrollOffset = 0;
}
else if (direction < 0 && helpState.selectedIndex * 68 < helpScrollOffset)
{
helpScrollOffset = helpState.selectedIndex * 68;
}
else if (direction > 0 && helpState.selectedIndex * 68 > helpScrollOffset + 360)
{
helpScrollOffset = helpState.selectedIndex * 68 - 360;
}
}
/**
* @brief 打开帮助首页当前选中的子页面。
*/
static void ActivateHelpSelection()
{
if (helpState.selectedIndex == 3)
{
helpState.currentPage = 5;
helpState.selectedIndex = 0;
helpScrollOffset = 0;
}
else
{
helpState.currentPage = helpState.selectedIndex + 1;
helpScrollOffset = 0;
}
}
/**
* @brief 从帮助子页返回帮助首页或主菜单。
*/
static void LeaveRulesPage()
{
int previousPage = helpState.currentPage;
if (helpState.currentPage == 0)
{
ReturnToMainMenu();
}
else
{
helpState.currentPage = 0;
if (previousPage == 4 || previousPage == 5)
{
helpState.selectedIndex = 3;
}
helpScrollOffset = 0;
}
}
/**
* @brief 处理帮助和致谢页键盘导航。
* @param hWnd 当前窗口句柄。
* @param key 按键虚拟键码。
* @return 当前界面是帮助页时返回 true,否则返回 false。
*/
static bool HandleRulesKey(HWND hWnd, WPARAM key)
{
if (currentScreen != SCREEN_RULES)
{
return false;
}
switch (key)
{
case VK_UP:
case VK_LEFT:
case 'W':
case 'A':
if (helpState.currentPage == 0)
{
MoveHelpHomeSelection(-1);
InvalidateRect(hWnd, nullptr, FALSE);
}
else if (helpState.currentPage == 4)
{
ChangeCreditPage(-1);
InvalidateRect(hWnd, nullptr, FALSE);
}
else if (helpState.currentPage == 5)
{
MoveSkillDemoSelection(-1);
InvalidateRect(hWnd, nullptr, FALSE);
}
break;
case VK_DOWN:
case VK_RIGHT:
case 'S':
case 'D':
if (helpState.currentPage == 0)
{
MoveHelpHomeSelection(1);
InvalidateRect(hWnd, nullptr, FALSE);
}
else if (helpState.currentPage == 4)
{
ChangeCreditPage(1);
InvalidateRect(hWnd, nullptr, FALSE);
}
else if (helpState.currentPage == 5)
{
MoveSkillDemoSelection(1);
InvalidateRect(hWnd, nullptr, FALSE);
}
break;
case VK_RETURN:
case VK_SPACE:
if (helpState.currentPage == 0)
{
ActivateHelpSelection();
InvalidateRect(hWnd, nullptr, FALSE);
}
else if (helpState.currentPage == 5)
{
StartRogueSkillDemoAt(helpState.selectedIndex);
ResetGameTimer(hWnd);
InvalidateRect(hWnd, nullptr, FALSE);
}
break;
case VK_ESCAPE:
case VK_BACK:
case 'M':
LeaveRulesPage();
InvalidateRect(hWnd, nullptr, FALSE);
break;
default:
break;
}
return true;
}
/**
* @brief 计算升级卡片网格列数。
* @return 当前升级界面使用的列数,至少为 1。
*/
static int GetUpgradeColumnCount()
{
int upgradeColumnCount = upgradeUiState.optionCount <= 3 ? upgradeUiState.optionCount : 3;
if (upgradeColumnCount < 1)
{
upgradeColumnCount = 1;
}
return upgradeColumnCount;
}
/**
* @brief 在升级卡片网格中横向移动选中项。
* @param direction 负数向左,正数向右。
*/
static void MoveUpgradeSelectionHorizontal(int direction)
{
if (upgradeUiState.optionCount <= 1)
{
return;
}
int upgradeColumnCount = GetUpgradeColumnCount();
int rowStart = upgradeUiState.selectedIndex - (upgradeUiState.selectedIndex % upgradeColumnCount);
int rowEnd = rowStart + upgradeColumnCount - 1;
if (rowEnd >= upgradeUiState.optionCount)
{
rowEnd = upgradeUiState.optionCount - 1;
}
if (direction < 0)
{
upgradeUiState.selectedIndex = (upgradeUiState.selectedIndex > rowStart) ? upgradeUiState.selectedIndex - 1 : rowEnd;
}
else
{
upgradeUiState.selectedIndex = (upgradeUiState.selectedIndex < rowEnd) ? upgradeUiState.selectedIndex + 1 : rowStart;
}
}
/**
* @brief 在升级卡片网格中纵向移动选中项。
* @param direction 负数向上,正数向下。
*/
static void MoveUpgradeSelectionVertical(int direction)
{
int upgradeColumnCount = GetUpgradeColumnCount();
if (direction < 0 && upgradeUiState.selectedIndex >= upgradeColumnCount)
{
upgradeUiState.selectedIndex -= upgradeColumnCount;
}
else if (direction > 0 && upgradeUiState.selectedIndex + upgradeColumnCount < upgradeUiState.optionCount)
{
upgradeUiState.selectedIndex += upgradeColumnCount;
}
}
/**
* @brief 切换多选升级中的当前卡片标记状态。
*/
static void ToggleUpgradeMarkedSelection()
{
if (upgradeUiState.picksRemaining <= 1 || upgradeUiState.optionCount <= 0)
{
return;
}
bool currentlyMarked = upgradeUiState.marked[upgradeUiState.selectedIndex];
if (currentlyMarked)
{
upgradeUiState.marked[upgradeUiState.selectedIndex] = false;
if (upgradeUiState.markedCount > 0)
{
upgradeUiState.markedCount--;
}
}
else if (upgradeUiState.markedCount < upgradeUiState.picksRemaining)
{
upgradeUiState.marked[upgradeUiState.selectedIndex] = true;
upgradeUiState.markedCount++;
}
}
/**
* @brief 处理升级选择界面键盘导航。
* @param hWnd 当前窗口句柄。
* @param key 按键虚拟键码。
* @return 当前界面是升级选择时返回 true,否则返回 false。
*/
static bool HandleUpgradeKey(HWND hWnd, WPARAM key)
{
if (currentScreen != SCREEN_UPGRADE)
{
return false;
}
switch (key)
{
case VK_LEFT:
case 'A':
MoveUpgradeSelectionHorizontal(-1);
InvalidateRect(hWnd, nullptr, FALSE);
break;
case VK_RIGHT:
case 'D':
MoveUpgradeSelectionHorizontal(1);
InvalidateRect(hWnd, nullptr, FALSE);
break;
case VK_UP:
case 'W':
MoveUpgradeSelectionVertical(-1);
InvalidateRect(hWnd, nullptr, FALSE);
break;
case VK_DOWN:
case 'S':
MoveUpgradeSelectionVertical(1);
InvalidateRect(hWnd, nullptr, FALSE);
break;
case VK_RETURN:
ConfirmUpgradeSelection();
ResetGameTimer(hWnd);
InvalidateRect(hWnd, nullptr, FALSE);
break;
case VK_SPACE:
if (upgradeUiState.picksRemaining > 1 && upgradeUiState.optionCount > 0)
{
ToggleUpgradeMarkedSelection();
InvalidateRect(hWnd, nullptr, FALSE);
}
else
{
ConfirmUpgradeSelection();
ResetGameTimer(hWnd);
InvalidateRect(hWnd, nullptr, FALSE);
}
break;
case 'M':
ReturnToMainMenu();
InvalidateRect(hWnd, nullptr, FALSE);
break;
default:
break;
}
return true;
}
/**
* @brief 处理 Rogue 技能演示模式的专用按键。
* @param hWnd 当前窗口句柄。
* @param key 按键虚拟键码。
* @return 已处理返回 true。
*/
static bool HandleDemoPlayingKey(HWND hWnd, WPARAM key)
{
if (!IsRogueSkillDemoMode())
{
return false;
}
if (key == 'N')
{
AdvanceRogueSkillDemo();
InvalidateRect(hWnd, nullptr, FALSE);
return true;
}
if (key == 'R')
{
RestartCurrentRogueSkillDemo();
ResetGameTimer(hWnd);
InvalidateRect(hWnd, nullptr, FALSE);
return true;
}
if (key == VK_ESCAPE || key == VK_BACK || key == 'M')
{
OpenSkillDemoScreen();
InvalidateRect(hWnd, nullptr, FALSE);
return true;
}
return false;
}
/**
* @brief 处理正常战局控制键,如菜单、重开、暂停、目标提示和复活。
* @param hWnd 当前窗口句柄。
* @param key 按键虚拟键码。
* @return 已处理返回 true。
*/
static bool HandleBattleControlKey(HWND hWnd, WPARAM key)
{
// 菜单、重开和暂停只对真实战局开放,避免破坏技能演示预设流程。
if (!IsRogueSkillDemoMode() && key == 'M')
{
ReturnToMainMenu();
InvalidateRect(hWnd, nullptr, FALSE);
return true;
}
if (!IsRogueSkillDemoMode() && key == 'R')
{
StartGameWithMode(currentMode);
ResetGameTimer(hWnd);
InvalidateRect(hWnd, nullptr, FALSE);
return true;
}
if (!IsRogueSkillDemoMode() && key == 'P')
{
suspendFlag = !suspendFlag;
InvalidateRect(hWnd, nullptr, FALSE);
return true;
}
if (key == 'G')
{
// 落点提示是显示开关,不改变棋盘或方块状态。
targetFlag = !targetFlag;
InvalidateRect(hWnd, nullptr, FALSE);
return true;
}
if (gameOverFlag && reviveAvailable && key == 'V')
{
// 复活机会只有视频成功播放后才消耗,失败时保留机会并给出反馈。
if (PlayReviveVideo(hWnd))
{
ReviveAfterVideo();
ResetGameTimer(hWnd);
}
else
{
SetFeedbackMessage(_T("视频播放失败"), _T("无法打开复活视频,复活机会未消耗。"), 14);
}
InvalidateRect(hWnd, nullptr, FALSE);
return true;
}
return false;
}
/**
* @brief 处理 Rogue 侧栏滚动和主动技能按键。
* @param hWnd 当前窗口句柄。
* @param key 按键虚拟键码。
* @return 已处理返回 true。
*/
static bool HandleRogueSkillKey(HWND hWnd, WPARAM key)
{
if (currentMode == MODE_ROGUE && (key == 'J' || key == 'K'))
{
// Rogue 侧栏强化列表较长,J/K 只调整说明列表的滚动位置。
int direction = (key == 'J') ? 1 : -1;
AdjustScrollOffset(upgradeListScrollOffset, direction * GetScrollStep(hWnd, 52));
InvalidateRect(hWnd, nullptr, FALSE);
return true;
}
switch (key)
{
// 主动技能和 Hold 的按键统一在这里分发,技能内部会自行检查次数和模式。
case 'C':
case VK_SHIFT:
case VK_LSHIFT:
case VK_RSHIFT:
HoldCurrentPiece();
return true;
case 'Z':
UseBlackHole();
return true;
case 'X':
UseScreenBomb();
return true;
case 'V':
UseAirReshape();
return true;
default:
return false;
}
}
/**
* @brief 固定当前方块后执行消行和 Rogue 升级检查。
*/
static void FixPieceAndResolveLines()
{
// 固定方块后立即检查满行,Rogue 模式还可能因为经验变化打开升级界面。
Fixing();
if (!gameOverFlag)
{
DeleteLines();
CheckRogueLevelProgress();
}
}
/**
* @brief 处理移动、旋转、软降和硬降等方块操作键。
* @param key 按键虚拟键码。
* @return 已处理返回 true。
*/
static bool HandlePieceMovementKey(WPARAM key)
{
switch (key)
{
case VK_LEFT:
case 'A':
if (CanMoveLeft())
{
MoveLeft();
}
return true;
case VK_RIGHT:
case 'D':
if (CanMoveRight())
{
MoveRight();
}
return true;
case VK_DOWN:
case 'S':
// 软降被阻挡时等价于本回合落地,立即进入固定和消行流程。
if (CanMoveDown())
{
MoveDown();
}
else
{
FixPieceAndResolveLines();
}
return true;
case VK_UP:
case 'W':
Rotate();
return true;
case VK_SPACE:
// 硬降先移动到最低合法位置,再一次性固定结算。
DropDown();
FixPieceAndResolveLines();
return true;
default:
return false;
}
}
/**
* @brief 处理游戏过程中的按键。
* @param hWnd 当前窗口句柄。
* @param key 按键虚拟键码。
*/
static void HandlePlayingKey(HWND hWnd, WPARAM key)
{
if (HandleDemoPlayingKey(hWnd, key) || HandleBattleControlKey(hWnd, key))
{
return;
}
if (gameOverFlag || suspendFlag)
{
return;
}
// 正常游玩按键先改变方块或触发技能,再统一刷新预测落点和界面。
if (!HandlePieceMovementKey(key))
{
HandleRogueSkillKey(hWnd, key);
}
if (!gameOverFlag)
{
ComputeTarget();
}
InvalidateRect(hWnd, nullptr, FALSE);
}
/**
* @brief 处理键盘按键事件。
* @param hWnd 当前窗口句柄。
* @param wParam 按键虚拟键码。
*/
void HandleKeyDown(HWND hWnd, WPARAM wParam)
{
// 按当前界面从上到下分发:菜单、帮助、升级界面优先消费按键。
if (HandleMenuKey(hWnd, wParam) ||
HandleRulesKey(hWnd, wParam) ||
HandleUpgradeKey(hWnd, wParam))
{
return;
}
HandlePlayingKey(hWnd, wParam);
}
+431
View File
@@ -0,0 +1,431 @@
#include "stdafx.h"
/**
* @file TetrisLayout.cpp
* @brief
*/
#include "TetrisAppInternal.h"
/**
* @brief
* @param scrollOffset
* @param delta
*/
void AdjustScrollOffset(int& scrollOffset, int delta)
{
// 先应用本次滚动增量,再统一夹紧到允许范围内。
scrollOffset += delta;
if (scrollOffset < 0)
{
scrollOffset = 0;
}
if (scrollOffset > 2400)
{
scrollOffset = 2400;
}
}
/**
* @brief
* @param hWnd
* @param baseStep 稿
* @return
*/
int GetScrollStep(HWND hWnd, int baseStep)
{
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
return MulDiv(baseStep, metrics.scale, 1000);
}
/**
* @brief
* @param hWnd
* @return
*/
LayoutMetrics GetLayoutMetrics(HWND hWnd)
{
RECT clientRect;
GetClientRect(hWnd, &clientRect);
// 以设计稿窗口为基准计算缩放,取较小比例保证完整界面不被裁切。
int clientWidth = clientRect.right - clientRect.left;
int clientHeight = clientRect.bottom - clientRect.top;
int scaleX = MulDiv(clientWidth, 1000, WINDOW_CLIENT_WIDTH);
int scaleY = MulDiv(clientHeight, 1000, WINDOW_CLIENT_HEIGHT);
int scale = (scaleX < scaleY) ? scaleX : scaleY;
if (scale < 500)
{
scale = 500;
}
LayoutMetrics metrics = {};
metrics.scale = scale;
metrics.layoutWidth = MulDiv(WINDOW_CLIENT_WIDTH, scale, 1000);
metrics.layoutHeight = MulDiv(WINDOW_CLIENT_HEIGHT, scale, 1000);
// 横向居中显示,纵向从顶部开始,方便窗口高度不足时保持棋盘起点稳定。
metrics.offsetX = (clientWidth - metrics.layoutWidth) / 2;
metrics.offsetY = 0;
metrics.grid = MulDiv(GRID, scale, 1000);
return metrics;
}
/**
* @brief
* @param metrics
* @param value 稿
* @return
*/
int ScaleValue(const LayoutMetrics& metrics, int value)
{
return MulDiv(value, metrics.scale, 1000);
}
/**
* @brief
* @param metrics
* @param value 稿
* @return
*/
int ScaleXValue(const LayoutMetrics& metrics, int value)
{
return metrics.offsetX + MulDiv(value, metrics.scale, 1000);
}
/**
* @brief
* @param metrics
* @param value 稿
* @return
*/
int ScaleYValue(const LayoutMetrics& metrics, int value)
{
return metrics.offsetY + MulDiv(value, metrics.scale, 1000);
}
/**
* @brief
* @param hWnd
* @return
*/
static RECT GetMenuCardRect(HWND hWnd)
{
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
RECT rect =
{
ScaleXValue(metrics, 110),
ScaleYValue(metrics, 70),
ScaleXValue(metrics, WINDOW_CLIENT_WIDTH - 110),
ScaleYValue(metrics, WINDOW_CLIENT_HEIGHT - 70)
};
return rect;
}
/**
* @brief
* @param hWnd
* @param index
* @return
*/
RECT GetMenuOptionRect(HWND hWnd, int index)
{
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
RECT menuCard = GetMenuCardRect(hWnd);
int top = menuCard.top + ScaleValue(metrics, 140) + index * ScaleValue(metrics, 130);
RECT rect =
{
menuCard.left + ScaleValue(metrics, 36),
top,
menuCard.right - ScaleValue(metrics, 36),
top + ScaleValue(metrics, 104)
};
return rect;
}
/**
* @brief
* @param hWnd
* @return
*/
static RECT GetRulesCardRect(HWND hWnd)
{
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
RECT rect =
{
ScaleXValue(metrics, 76),
ScaleYValue(metrics, 54),
ScaleXValue(metrics, WINDOW_CLIENT_WIDTH - 76),
ScaleYValue(metrics, WINDOW_CLIENT_HEIGHT - 54)
};
return rect;
}
/**
* @brief
* @param hWnd
* @param index
* @return
*/
RECT GetHelpOptionRect(HWND hWnd, int index)
{
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
RECT rulesCard = GetRulesCardRect(hWnd);
RECT contentRect =
{
rulesCard.left + ScaleValue(metrics, 36),
rulesCard.top + ScaleValue(metrics, 126),
rulesCard.right - ScaleValue(metrics, 36),
rulesCard.bottom - ScaleValue(metrics, 86)
};
int optionHeight = ScaleValue(metrics, 100);
int optionGap = ScaleValue(metrics, 22);
int optionTop = contentRect.top + ScaleValue(metrics, 18);
RECT rect =
{
contentRect.left,
optionTop + index * (optionHeight + optionGap),
contentRect.right,
optionTop + index * (optionHeight + optionGap) + optionHeight
};
return rect;
}
/**
* @brief
* @param hWnd
* @param index
* @return
*/
RECT GetHelpSkillDemoItemRect(HWND hWnd, int index)
{
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
RECT rulesCard = GetRulesCardRect(hWnd);
RECT contentRect =
{
rulesCard.left + ScaleValue(metrics, 36),
rulesCard.top + ScaleValue(metrics, 126),
rulesCard.right - ScaleValue(metrics, 36),
rulesCard.bottom - ScaleValue(metrics, 86)
};
int itemHeight = ScaleValue(metrics, 58);
int itemGap = ScaleValue(metrics, 10);
int itemTop = contentRect.top + ScaleValue(metrics, 8) - helpScrollOffset;
RECT rect =
{
contentRect.left,
itemTop + index * (itemHeight + itemGap),
contentRect.right,
itemTop + index * (itemHeight + itemGap) + itemHeight
};
return rect;
}
/**
* @brief
* @param hWnd
* @return
*/
RECT GetHelpBackHintRect(HWND hWnd)
{
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
RECT rulesCard = GetRulesCardRect(hWnd);
RECT rect =
{
rulesCard.left + ScaleValue(metrics, 36),
rulesCard.bottom - ScaleValue(metrics, 58),
rulesCard.right - ScaleValue(metrics, 36),
rulesCard.bottom - ScaleValue(metrics, 24)
};
return rect;
}
/**
* @brief
* @param hWnd
* @param direction 0 0
* @return
*/
RECT GetCreditArrowRect(HWND hWnd, int direction)
{
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
RECT rulesCard = GetRulesCardRect(hWnd);
int size = ScaleValue(metrics, 54);
int centerY = (rulesCard.top + rulesCard.bottom) / 2;
int left = direction < 0
? rulesCard.left + ScaleValue(metrics, 52)
: rulesCard.right - ScaleValue(metrics, 52) - size;
RECT rect =
{
left,
centerY - size / 2,
left + size,
centerY + size / 2
};
return rect;
}
/**
* @brief
* @param hWnd
* @return
*/
static RECT GetUpgradeOverlayRect(HWND hWnd)
{
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
RECT rect =
{
ScaleXValue(metrics, 60),
ScaleYValue(metrics, 80),
ScaleXValue(metrics, WINDOW_CLIENT_WIDTH - 60),
ScaleYValue(metrics, WINDOW_CLIENT_HEIGHT - 80)
};
return rect;
}
/**
* @brief
* @param hWnd
* @param index
* @return
*/
RECT GetUpgradeCardRect(HWND hWnd, int index)
{
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
RECT overlayRect = GetUpgradeOverlayRect(hWnd);
// 根据当前候选数量自动决定列数;最多三列,两行用于命运轮盘六选项。
int gap = ScaleValue(metrics, 18);
int horizontalPadding = ScaleValue(metrics, 36);
int verticalTop = overlayRect.top + ScaleValue(metrics, 138);
int columnCount = upgradeUiState.optionCount <= 3 ? upgradeUiState.optionCount : 3;
if (columnCount < 1)
{
columnCount = 1;
}
int rowCount = (upgradeUiState.optionCount + columnCount - 1) / columnCount;
if (rowCount < 1)
{
rowCount = 1;
}
int cardWidth = (overlayRect.right - overlayRect.left - horizontalPadding * 2 - gap * (columnCount - 1)) / columnCount;
int availableHeight = overlayRect.bottom - verticalTop - ScaleValue(metrics, 72) - (rowCount - 1) * gap;
int cardHeight = availableHeight / rowCount;
int column = index % columnCount;
int row = index / columnCount;
int left = overlayRect.left + horizontalPadding + column * (cardWidth + gap);
int top = verticalTop + row * (cardHeight + gap);
RECT rect = { left, top, left + cardWidth, top + cardHeight };
return rect;
}
/**
* @brief
* @param hWnd
* @return
*/
static RECT GetGameOverlayRect(HWND hWnd)
{
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
int panelGap = ScaleValue(metrics, SIDE_PANEL_GAP);
int panelWidth = ScaleValue(metrics, SIDE_PANEL_WIDTH);
int boardLeft = ScaleXValue(metrics, WINDOW_PADDING) + panelWidth + panelGap;
int boardTop = ScaleYValue(metrics, WINDOW_PADDING);
int boardWidth = nGameWidth * metrics.grid;
RECT rect =
{
boardLeft + ScaleValue(metrics, 28),
boardTop + metrics.grid * 6 + ScaleValue(metrics, 10),
boardLeft + boardWidth - ScaleValue(metrics, 28),
boardTop + metrics.grid * 10 + ScaleValue(metrics, 30)
};
return rect;
}
/**
* @brief
* @param hWnd
* @param index
* @param buttonCount
* @return
*/
RECT GetOverlayButtonRect(HWND hWnd, int index, int buttonCount)
{
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
RECT overlayRect = GetGameOverlayRect(hWnd);
// 游戏结束可能有三个按钮,暂停只有两个按钮,因此间距和边距分开计算。
int gap = buttonCount == 3 ? ScaleValue(metrics, 8) : ScaleValue(metrics, 18);
int sidePadding = buttonCount == 3 ? ScaleValue(metrics, 14) : ScaleValue(metrics, 34);
int width = (overlayRect.right - overlayRect.left - sidePadding * 2 - gap * (buttonCount - 1)) / buttonCount;
int height = ScaleValue(metrics, 44);
int left = overlayRect.left + sidePadding + index * (width + gap);
int top = overlayRect.top + ScaleValue(metrics, 94);
RECT rect = { left, top, left + width, top + height };
return rect;
}
/**
* @brief
* @param hWnd
* @return
*/
RECT GetBackButtonRect(HWND hWnd)
{
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
RECT rect =
{
ScaleXValue(metrics, 6),
ScaleYValue(metrics, 6),
ScaleXValue(metrics, 34),
ScaleYValue(metrics, 34)
};
return rect;
}
/**
* @brief
* @param hWnd
* @return
*/
RECT GetMusicButtonRect(HWND hWnd)
{
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
// 音乐按钮保持最小可点击尺寸,避免窗口缩小时变得难以点中。
int size = ScaleValue(metrics, 28);
if (size < 22)
{
size = 22;
}
int marginRight = ScaleValue(metrics, 12);
if (marginRight < 6)
{
marginRight = 6;
}
int marginBottom = ScaleValue(metrics, 12);
if (marginBottom < 6)
{
marginBottom = 6;
}
RECT buttonRect =
{
metrics.offsetX + metrics.layoutWidth - marginRight - size,
metrics.offsetY + metrics.layoutHeight - marginBottom - size,
metrics.offsetX + metrics.layoutWidth - marginRight,
metrics.offsetY + metrics.layoutHeight - marginBottom
};
return buttonRect;
}
/**
* @brief
* @param rect
* @param x
* @param y
* @return true false
*/
bool IsPointInRect(const RECT& rect, int x, int y)
{
return x >= rect.left && x < rect.right && y >= rect.top && y < rect.bottom;
}
+251
View File
@@ -0,0 +1,251 @@
#include "stdafx.h"
/**
* @file TetrisMedia.cpp
* @brief
*/
#include "Tetris.h"
#include "TetrisAppInternal.h"
#include "TetrisAssets.h"
#include <shellapi.h>
#include <string>
static bool bgmPlaying = false;
static bool bgmUsingMci = false;
static constexpr const wchar_t* kBgmAlias = L"TereisBgm";
static constexpr const wchar_t* kReviveVideoAlias = L"TereisReviveVideo";
/**
* @brief MCI
* @param path
* @param forceMpegVideo mpegvideo
* @return true false
*/
static bool TryPlayMciLoop(const std::wstring& path, bool forceMpegVideo)
{
// 资源不存在时直接失败,让上层继续尝试下一个候选路径或格式。
if (!FileExists(path))
{
return false;
}
mciSendStringW((std::wstring(L"close ") + kBgmAlias).c_str(), nullptr, 0, nullptr);
// MCI 对部分 OGG/视频容器识别不稳定,调用方会按不同类型尝试。
std::wstring openCommand = L"open \"" + path + L"\" ";
if (forceMpegVideo)
{
openCommand += L"type mpegvideo ";
}
openCommand += L"alias ";
openCommand += kBgmAlias;
if (mciSendStringW(openCommand.c_str(), nullptr, 0, nullptr) != 0)
{
return false;
}
std::wstring playCommand = std::wstring(L"play ") + kBgmAlias + L" repeat";
if (mciSendStringW(playCommand.c_str(), nullptr, 0, nullptr) != 0)
{
mciSendStringW((std::wstring(L"close ") + kBgmAlias).c_str(), nullptr, 0, nullptr);
return false;
}
bgmPlaying = true;
bgmUsingMci = true;
return true;
}
/**
* @brief 使
*/
void StopBackgroundMusic()
{
// 根据当前播放方式选择对应的释放接口,避免 MCI 设备或 PlaySound 残留。
if (bgmUsingMci)
{
mciSendStringW((std::wstring(L"stop ") + kBgmAlias).c_str(), nullptr, 0, nullptr);
mciSendStringW((std::wstring(L"close ") + kBgmAlias).c_str(), nullptr, 0, nullptr);
}
else
{
PlaySoundW(nullptr, nullptr, 0);
}
bgmPlaying = false;
bgmUsingMci = false;
}
/**
* @brief
*/
void StartBackgroundMusic()
{
// 音乐被关闭或已经在播放时,不重复查找资源和启动设备。
if (!bgmEnabled || bgmPlaying)
{
return;
}
const wchar_t* bgmWavRelativePath = L"assets\\audio\\bgm.wav";
const std::wstring bgmWavCandidates[] =
{
BuildAssetPath(bgmWavRelativePath),
BuildWorkingDirAssetPath(bgmWavRelativePath)
};
for (const std::wstring& candidate : bgmWavCandidates)
{
// WAV 优先使用 PlaySound,依赖少、兼容性最好。
if (FileExists(candidate) &&
PlaySoundW(candidate.c_str(), nullptr, SND_FILENAME | SND_ASYNC | SND_LOOP))
{
bgmPlaying = true;
bgmUsingMci = false;
return;
}
}
const wchar_t* oggRelativePath = L"assets\\audio\\bgm.ogg";
const std::wstring oggCandidates[] =
{
BuildAssetPath(oggRelativePath),
BuildWorkingDirAssetPath(oggRelativePath)
};
for (const std::wstring& candidate : oggCandidates)
{
// OGG 通过 MCI 尝试普通打开和 mpegvideo 强制类型两条路径。
if (TryPlayMciLoop(candidate, false) || TryPlayMciLoop(candidate, true))
{
return;
}
}
const wchar_t* fallbackWavRelativePath = L"assets\\audio\\background.wav";
const std::wstring fallbackWavCandidates[] =
{
BuildAssetPath(fallbackWavRelativePath),
BuildWorkingDirAssetPath(fallbackWavRelativePath)
};
for (const std::wstring& candidate : fallbackWavCandidates)
{
// 兼容旧资源名 background.wav,保证替换素材后仍能播放。
if (FileExists(candidate) &&
PlaySoundW(candidate.c_str(), nullptr, SND_FILENAME | SND_ASYNC | SND_LOOP))
{
bgmPlaying = true;
bgmUsingMci = false;
return;
}
}
bgmEnabled = false;
}
/**
* @brief
* @param hWnd
*/
void ToggleBackgroundMusic(HWND hWnd)
{
bgmEnabled = !bgmEnabled;
if (bgmEnabled)
{
StartBackgroundMusic();
}
else
{
StopBackgroundMusic();
}
InvalidateRect(hWnd, nullptr, FALSE);
}
/**
* @brief MCI退
* @param hWnd MCI ShellExecute
* @return true false
*/
bool PlayReviveVideo(HWND hWnd)
{
// 依次查找 AVI 和 MP4,并同时支持构建目录与项目根目录运行。
std::wstring videoPath = BuildAssetPath(L"assets\\video\\video.avi");
if (!FileExists(videoPath))
{
videoPath = BuildWorkingDirAssetPath(L"assets\\video\\video.avi");
}
if (!FileExists(videoPath))
{
videoPath = BuildAssetPath(L"assets\\video\\video.mp4");
}
if (!FileExists(videoPath))
{
videoPath = BuildWorkingDirAssetPath(L"assets\\video\\video.mp4");
}
if (!FileExists(videoPath))
{
return false;
}
bool shouldResumeBgm = bgmEnabled;
if (bgmPlaying)
{
// 视频播放期间暂停背景音乐,播放结束后按开关状态恢复。
StopBackgroundMusic();
}
// 先用 MCI 全屏同步播放;失败时再交给系统默认播放器。
bool played = false;
for (int attempt = 0; attempt < 2 && !played; attempt++)
{
bool forceMpegVideo = attempt == 0;
mciSendStringW((std::wstring(L"close ") + kReviveVideoAlias).c_str(), nullptr, 0, nullptr);
std::wstring openCommand = L"open \"" + videoPath + L"\" ";
if (forceMpegVideo)
{
openCommand += L"type mpegvideo ";
}
openCommand += L"alias ";
openCommand += kReviveVideoAlias;
if (mciSendStringW(openCommand.c_str(), nullptr, 0, hWnd) == 0)
{
std::wstring playCommand = std::wstring(L"play ") + kReviveVideoAlias + L" fullscreen wait";
MCIERROR playResult = mciSendStringW(playCommand.c_str(), nullptr, 0, hWnd);
mciSendStringW((std::wstring(L"close ") + kReviveVideoAlias).c_str(), nullptr, 0, nullptr);
played = playResult == 0;
}
}
if (!played)
{
// MCI 全屏播放失败时退回系统默认播放器,并等待播放器进程结束。
SHELLEXECUTEINFOW shellInfo = {};
shellInfo.cbSize = sizeof(shellInfo);
shellInfo.fMask = SEE_MASK_NOCLOSEPROCESS;
shellInfo.hwnd = hWnd;
shellInfo.lpVerb = L"open";
shellInfo.lpFile = videoPath.c_str();
shellInfo.nShow = SW_SHOWNORMAL;
if (ShellExecuteExW(&shellInfo))
{
if (shellInfo.hProcess != nullptr)
{
WaitForSingleObject(shellInfo.hProcess, INFINITE);
CloseHandle(shellInfo.hProcess);
}
played = true;
}
}
if (shouldResumeBgm)
{
StartBackgroundMusic();
}
return played;
}
+342
View File
@@ -0,0 +1,342 @@
#include "stdafx.h"
/**
* @file TetrisTimers.cpp
* @brief Rogue
*/
#include "TetrisAppInternal.h"
static MMRESULT creditTimerHandle = 0;
/**
* @brief
* @param userData
*/
static void CALLBACK CreditTimerCallback(UINT, UINT, DWORD_PTR userData, DWORD_PTR, DWORD_PTR)
{
HWND hWnd = reinterpret_cast<HWND>(userData);
if (hWnd != nullptr)
{
PostMessage(hWnd, WM_CREDIT_TICK, 0, 0);
}
}
/**
* @brief
* @param hWnd
*/
void ResetGameTimer(HWND hWnd)
{
// 下落速度会被 Rogue 强化和临时状态动态修改,因此每次变化都重新注册定时器。
KillTimer(hWnd, GAME_TIMER_ID);
SetTimer(hWnd, GAME_TIMER_ID, currentFallInterval > 0 ? currentFallInterval : GAME_TIMER_INTERVAL, nullptr);
}
/**
* @brief
* @param hWnd
*/
void StartAppTimers(HWND hWnd)
{
// 主定时器负责方块下落,特效定时器负责高帧率视觉动画。
ResetGameTimer(hWnd);
SetTimer(hWnd, EFFECT_TIMER_ID, EFFECT_TIMER_INTERVAL, nullptr);
// 致谢页动画需要更高刷新频率,优先使用多媒体定时器。
creditTimerHandle = timeSetEvent(
CREDIT_TIMER_INTERVAL,
1,
CreditTimerCallback,
reinterpret_cast<DWORD_PTR>(hWnd),
TIME_PERIODIC | TIME_CALLBACK_FUNCTION);
if (creditTimerHandle == 0)
{
// 多媒体定时器不可用时退回普通窗口定时器,保证致谢页仍可动画。
SetTimer(hWnd, CREDIT_TIMER_ID, CREDIT_TIMER_INTERVAL, nullptr);
}
}
/**
* @brief
* @param hWnd
*/
void StopAppTimers(HWND hWnd)
{
// 退出或窗口销毁时释放所有可能创建过的计时器资源。
KillTimer(hWnd, GAME_TIMER_ID);
KillTimer(hWnd, EFFECT_TIMER_ID);
if (creditTimerHandle != 0)
{
timeKillEvent(creditTimerHandle);
creditTimerHandle = 0;
}
else
{
KillTimer(hWnd, CREDIT_TIMER_ID);
}
}
/**
* @brief
* @param hWnd
*/
void HandleCreditTick(HWND hWnd)
{
if (currentScreen == SCREEN_RULES && helpState.currentPage == 4 && TickCreditAnimation())
{
InvalidateRect(hWnd, nullptr, FALSE);
}
}
/**
* @brief Rogue
* @param hWnd
* @return true
*/
static bool TickRogueTimedStates(HWND hWnd)
{
bool shouldRefresh = false;
// 狂热、缓流、极限缓速和 Hold 缓速都会影响下落间隔,需要同步重置主定时器。
if (currentMode == MODE_ROGUE && !IsRogueSkillDemoMode() && rogueStats.feverTicks > 0)
{
rogueStats.feverTicks--;
currentFallInterval = GetRogueFallInterval();
ResetGameTimer(hWnd);
shouldRefresh = true;
}
if (currentMode == MODE_ROGUE &&
!IsRogueSkillDemoMode() &&
rogueStats.timeDilationTicks > 0 &&
currentScreen == SCREEN_PLAYING &&
!suspendFlag &&
!gameOverFlag)
{
rogueStats.timeDilationTicks--;
currentFallInterval = GetRogueFallInterval();
ResetGameTimer(hWnd);
shouldRefresh = true;
}
if (currentMode == MODE_ROGUE && !IsRogueSkillDemoMode() && rogueStats.extremeSlowTicks > 0)
{
rogueStats.extremeSlowTicks--;
currentFallInterval = GetRogueFallInterval();
ResetGameTimer(hWnd);
shouldRefresh = true;
}
if (currentMode == MODE_ROGUE && !IsRogueSkillDemoMode() && rogueStats.holdSlowTicks > 0)
{
rogueStats.holdSlowTicks--;
currentFallInterval = GetRogueFallInterval();
ResetGameTimer(hWnd);
shouldRefresh = true;
}
return shouldRefresh;
}
/**
* @brief
* @param hWnd
* @return true
*/
static bool TickExtremeDanger(HWND hWnd)
{
// 极限玩家只在真实 Rogue 战局中计时,暂停、结束和技能演示都不推进危险等级。
if (currentMode != MODE_ROGUE ||
IsRogueSkillDemoMode() ||
rogueStats.extremePlayerLevel <= 0 ||
currentScreen != SCREEN_PLAYING ||
suspendFlag ||
gameOverFlag)
{
return false;
}
if (rogueStats.extremeDangerTicks > 0)
{
// 计时尚未结束时只递减倒计时,不改变速度。
rogueStats.extremeDangerTicks--;
return false;
}
// 每 30 个主计时周期未完成四消就提高危险等级,并立即刷新下落速度。
rogueStats.extremeDangerTicks = 30;
if (rogueStats.extremeDangerLevel < 5)
{
rogueStats.extremeDangerLevel++;
}
currentFallInterval = GetRogueFallInterval();
ResetGameTimer(hWnd);
SetFeedbackMessage(
_T("极限压力升高"),
_T("30 秒内没有完成四消,危险等级提升,下落速度进一步加快。"),
10);
return true;
}
/**
* @brief
* @param hWnd
* @return true
*/
static bool TryStartTimeDilation(HWND hWnd)
{
// 时间缓流是自动保命效果,已经在持续时不会重复触发。
if (currentMode != MODE_ROGUE ||
IsRogueSkillDemoMode() ||
rogueStats.timeDilationLevel <= 0 ||
rogueStats.timeDilationTicks > 0)
{
return false;
}
int occupiedHeight = 0;
int playableHeight = GetRoguePlayableHeight();
// 自上而下寻找第一行有方块的位置,由此换算当前堆叠高度。
for (int y = 0; y < playableHeight; y++)
{
bool hasCell = false;
for (int x = 0; x < nGameWidth; x++)
{
if (workRegion[y][x] != 0)
{
hasCell = true;
break;
}
}
if (hasCell)
{
occupiedHeight = playableHeight - y;
break;
}
}
if (occupiedHeight <= 15)
{
return false;
}
rogueStats.timeDilationTicks = 8;
currentFallInterval = GetRogueFallInterval();
ResetGameTimer(hWnd);
SetFeedbackMessage(
_T("时间缓流"),
_T("堆叠高度超过 15 行,接下来 8 秒下落速度降低 30%。"),
10);
return true;
}
/**
* @brief
* @param hWnd
* @return true
*/
static bool TickGameFall(HWND hWnd)
{
if (currentScreen != SCREEN_PLAYING || suspendFlag || gameOverFlag)
{
return false;
}
// Rogue 难度随时间推进,速度变化后需要重新安排下一次自动下落。
if (currentMode == MODE_ROGUE && !IsRogueSkillDemoMode())
{
int previousFallInterval = currentFallInterval;
AdvanceRogueDifficulty(currentFallInterval > 0 ? currentFallInterval : GAME_TIMER_INTERVAL);
if (currentFallInterval != previousFallInterval)
{
ResetGameTimer(hWnd);
}
}
TryStartTimeDilation(hWnd);
// 能下落时只移动一格;被阻挡时固定方块,并进入消行与升级结算。
if (CanMoveDown())
{
MoveDown();
}
else
{
Fixing();
if (!gameOverFlag)
{
DeleteLines();
CheckRogueLevelProgress();
}
}
if (!gameOverFlag)
{
// 真实方块位置变化后刷新预测落点,供渲染层绘制目标提示。
ComputeTarget();
}
return true;
}
/**
* @brief
* @param hWnd
* @param timerId
*/
void HandleTimerMessage(HWND hWnd, WPARAM timerId)
{
if (timerId == EFFECT_TIMER_ID)
{
// 视觉特效独立于主下落速度,用固定帧率推进。
if (TickVisualEffects())
{
InvalidateRect(hWnd, nullptr, FALSE);
}
return;
}
if (timerId == CREDIT_TIMER_ID && creditTimerHandle == 0)
{
// 多媒体定时器不可用时,普通窗口定时器承担致谢页动画刷新。
HandleCreditTick(hWnd);
return;
}
if (timerId != GAME_TIMER_ID)
{
return;
}
bool shouldRefresh = false;
if (feedbackState.visibleTicks > 0)
{
// 右侧反馈信息按主计时周期自动消退。
feedbackState.visibleTicks--;
shouldRefresh = true;
}
// 主定时器集中推进演示、Rogue 临时状态、危险等级和自然下落。
if (IsRogueSkillDemoMode() && TickRogueSkillDemo())
{
shouldRefresh = true;
}
if (TickRogueTimedStates(hWnd))
{
shouldRefresh = true;
}
if (TickExtremeDanger(hWnd))
{
shouldRefresh = true;
}
if (TickGameFall(hWnd))
{
shouldRefresh = true;
}
if (shouldRefresh)
{
InvalidateRect(hWnd, nullptr, FALSE);
}
}
+82
View File
@@ -0,0 +1,82 @@
#include "stdafx.h"
/**
* @file TetrisAssets.cpp
* @brief
*/
#include "TetrisAssets.h"
/**
* @brief
*
*
* assets
*
* @param relativePath
* @return
*/
std::wstring BuildAssetPath(const wchar_t* relativePath)
{
wchar_t modulePath[MAX_PATH] = {};
GetModuleFileNameW(nullptr, modulePath, MAX_PATH);
// 先取可执行文件所在目录,再根据构建目录层级回到项目根目录。
std::wstring basePath(modulePath);
size_t lastSlash = basePath.find_last_of(L"\\/");
if (lastSlash != std::wstring::npos)
{
basePath.resize(lastSlash);
}
// 可执行文件位于构建目录,向上两级回到项目根目录。
std::wstring projectRelative = basePath + L"\\..\\..\\" + relativePath;
wchar_t fullPath[MAX_PATH] = {};
DWORD result = GetFullPathNameW(projectRelative.c_str(), MAX_PATH, fullPath, nullptr);
if (result > 0 && result < MAX_PATH)
{
return fullPath;
}
return projectRelative;
}
/**
* @brief
*
* IDE
*
* @param relativePath
* @return
*/
std::wstring BuildWorkingDirAssetPath(const wchar_t* relativePath)
{
wchar_t currentDirectory[MAX_PATH] = {};
DWORD length = GetCurrentDirectoryW(MAX_PATH, currentDirectory);
if (length == 0 || length >= MAX_PATH)
{
return L"";
}
// 当前工作目录可能已经是项目根目录,直接拼接相对资源路径。
std::wstring candidate = std::wstring(currentDirectory) + L"\\" + relativePath;
wchar_t fullPath[MAX_PATH] = {};
DWORD result = GetFullPathNameW(candidate.c_str(), MAX_PATH, fullPath, nullptr);
if (result > 0 && result < MAX_PATH)
{
return fullPath;
}
return candidate;
}
/**
* @brief
* @param path
* @return true false
*/
bool FileExists(const std::wstring& path)
{
// 目录不能作为媒体或图片资源使用,因此排除 FILE_ATTRIBUTE_DIRECTORY。
DWORD attributes = GetFileAttributesW(path.c_str());
return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) == 0;
}
@@ -1,4 +1,9 @@
#include "stdafx.h" #include "stdafx.h"
/**
* @file TetrisGameExtensions.cpp
* @brief /
*/
#include "TetrisLogicInternal.h" #include "TetrisLogicInternal.h"
int pendingLineClearEffectTicks = 0; int pendingLineClearEffectTicks = 0;
@@ -8,13 +13,17 @@ int pendingLineClearEffectLineCount = 0;
/** /**
* @brief Rogue 使 * @brief Rogue 使
* @param stats
* @param useRogueRules Rogue
*/ */
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;
@@ -77,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;
@@ -85,9 +95,13 @@ void ResetPlayerStats(PlayerStats& stats, bool useRogueRules)
/** /**
* @brief * @brief
* @param title
* @param detail
* @param ticks
*/ */
void SetFeedbackMessage(const TCHAR* title, const TCHAR* detail, int ticks) void SetFeedbackMessage(const TCHAR* title, const TCHAR* detail, int ticks)
{ {
// 使用 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));
@@ -98,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;
@@ -125,11 +140,13 @@ void ResetVisualEffects()
/** /**
* @brief * @brief
* @return true false
*/ */
bool TickVisualEffects() bool TickVisualEffects()
{ {
bool active = false; bool active = false;
// 所有效果共用倒计时推进,任意效果仍活动就请求界面刷新。
if (clearEffectState.ticks > 0) if (clearEffectState.ticks > 0)
{ {
clearEffectState.ticks--; clearEffectState.ticks--;
@@ -177,6 +194,7 @@ bool TickVisualEffects()
/** /**
* @brief * @brief
* @return true false
*/ */
bool TickCreditAnimation() bool TickCreditAnimation()
{ {
@@ -191,9 +209,14 @@ bool TickCreditAnimation()
/** /**
* @brief * @brief
* @param boardX 使 100
* @param boardY 使 100
* @param text
* @param color
*/ */
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)
@@ -211,6 +234,12 @@ static void AddFloatingText(int boardX, int boardY, const TCHAR* text, COLORREF
/** /**
* @brief * @brief
* @param boardX
* @param boardY
* @param velocityX
* @param velocityY
* @param size
* @param color
*/ */
static void AddParticle(int boardX, int boardY, int velocityX, int velocityY, int size, COLORREF color) static void AddParticle(int boardX, int boardY, int velocityX, int velocityY, int size, COLORREF color)
{ {
@@ -233,6 +262,10 @@ static void AddParticle(int boardX, int boardY, int velocityX, int velocityY, in
/** /**
* @brief * @brief
* @param boardX
* @param boardY
* @param baseColor
* @param strongBurst 使
*/ */
static void AddBurstParticles(int boardX, int boardY, COLORREF baseColor, bool strongBurst) static void AddBurstParticles(int boardX, int boardY, COLORREF baseColor, bool strongBurst)
{ {
@@ -296,6 +329,10 @@ static void AddBurstParticles(int boardX, int boardY, COLORREF baseColor, bool s
/** /**
* @brief * @brief
* @param x
* @param y
* @param color
* @param strongFlash 使
*/ */
static void AddCellFlash(int x, int y, COLORREF color, bool strongFlash) static void AddCellFlash(int x, int y, COLORREF color, bool strongFlash)
{ {
@@ -315,6 +352,9 @@ static void AddCellFlash(int x, int y, COLORREF color, bool strongFlash)
/** /**
* @brief * @brief
* @param rows
* @param rowCount
* @param linesCleared
*/ */
void QueueLineClearEffect(const int* rows, int rowCount, int linesCleared) void QueueLineClearEffect(const int* rows, int rowCount, int linesCleared)
{ {
@@ -358,6 +398,9 @@ void PlayPendingLineClearEffect()
/** /**
* @brief * @brief
* @param rows
* @param rowCount
* @param linesCleared
*/ */
void TriggerLineClearEffect(const int* rows, int rowCount, int linesCleared) void TriggerLineClearEffect(const int* rows, int rowCount, int linesCleared)
{ {
@@ -414,6 +457,9 @@ void TriggerLineClearEffect(const int* rows, int rowCount, int linesCleared)
/** /**
* @brief * @brief
* @param cells
* @param cellCount
* @param strongBurst 使
*/ */
void TriggerCellClearEffect(const Point* cells, int cellCount, bool strongBurst) void TriggerCellClearEffect(const Point* cells, int cellCount, bool strongBurst)
{ {
@@ -422,6 +468,10 @@ void TriggerCellClearEffect(const Point* cells, int cellCount, bool strongBurst)
/** /**
* @brief * @brief
* @param cells
* @param cellCount
* @param flashColor
* @param strongBurst 使
*/ */
void TriggerColoredCellClearEffect(const Point* cells, int cellCount, COLORREF flashColor, bool strongBurst) void TriggerColoredCellClearEffect(const Point* cells, int cellCount, COLORREF flashColor, bool strongBurst)
{ {
@@ -445,6 +495,10 @@ void TriggerColoredCellClearEffect(const Point* cells, int cellCount, COLORREF f
/** /**
* @brief * @brief
* @param x
* @param fromY
* @param toY
* @param cellValue
*/ */
void TriggerGravityFallEffect(int x, int fromY, int toY, int cellValue) void TriggerGravityFallEffect(int x, int fromY, int toY, int cellValue)
{ {
@@ -490,6 +544,10 @@ void TriggerGravityFallEffect(int x, int fromY, int toY, int cellValue)
/** /**
* @brief * @brief
* @param pieceType
* @param pieceState
* @param position
* @return true false
*/ */
bool IsPiecePlacementValid(int pieceType, int pieceState, Point position) bool IsPiecePlacementValid(int pieceType, int pieceState, Point position)
{ {
@@ -522,6 +580,9 @@ bool IsPiecePlacementValid(int pieceType, int pieceState, Point position)
/** /**
* @brief * @brief
* @param nextState
* @param offsetX
* @return true false
*/ */
bool TryRotateWithOffset(int nextState, int offsetX) bool TryRotateWithOffset(int nextState, int offsetX)
{ {
@@ -535,6 +596,7 @@ bool TryRotateWithOffset(int nextState, int offsetX)
*/ */
void ReviveAfterVideo() void ReviveAfterVideo()
{ {
// 只有游戏结束且复活机会仍在时才能进入复活流程。
if (!gameOverFlag || !reviveAvailable) if (!gameOverFlag || !reviveAvailable)
{ {
return; return;
@@ -552,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++)
@@ -560,6 +623,7 @@ void ReviveAfterVideo()
} }
} }
// 复活后重新取一个活动方块,并刷新落点提示。
type = ConsumeNextType(); type = ConsumeNextType();
nType = nextTypes[0]; nType = nextTypes[0];
state = 0; state = 0;
@@ -574,9 +638,11 @@ void ReviveAfterVideo()
/** /**
* @brief * @brief
* @param mode GameMode
*/ */
void StartGameWithMode(int mode) void StartGameWithMode(int mode)
{ {
// 模式切换后直接复用 Restart,保证经典和 Rogue 都从干净状态开始。
rogueDemoMode = false; rogueDemoMode = false;
currentMode = mode; currentMode = mode;
currentScreen = SCREEN_PLAYING; currentScreen = SCREEN_PLAYING;
@@ -591,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;
@@ -632,7 +699,7 @@ void OpenRulesScreen()
} }
/** /**
* @brief * @brief Rogue
*/ */
void OpenSkillDemoScreen() void OpenSkillDemoScreen()
{ {
@@ -648,6 +715,9 @@ void OpenSkillDemoScreen()
creditAnimationDirection = 0; creditAnimationDirection = 0;
} }
/**
* @brief
*/
void OpenCreditScreen() void OpenCreditScreen()
{ {
rogueDemoMode = false; rogueDemoMode = false;
@@ -664,15 +734,17 @@ void OpenCreditScreen()
/** /**
* @brief * @brief
* @param direction 0 0
*/ */
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);
}
}
+263
View File
@@ -0,0 +1,263 @@
#include "stdafx.h"
/**
* @file TetrisPieceEffects.cpp
* @brief
*/
#include "TetrisLogicInternal.h"
/**
* @brief
* @param overflowTop
* @param fixedCells
* @param fixedCellCount
*/
void ApplyRainbowLandingEffect(bool overflowTop, const Point* fixedCells, int fixedCellCount)
{
// 顶部溢出时优先交给失败/复活逻辑处理,避免在不可见区域触发奖励。
if (overflowTop || !currentPieceIsRainbow)
{
return;
}
// 优先使用实际固定格子的平均行作为主色行,避免旋转形状偏移导致判定不自然。
int rainbowAnchorRow = point.y + 1;
if (fixedCellCount > 0)
{
int ySum = 0;
for (int i = 0; i < fixedCellCount; i++)
{
ySum += fixedCells[i].y;
}
rainbowAnchorRow = (ySum + fixedCellCount / 2) / fixedCellCount;
}
if (rainbowAnchorRow < 0)
{
rainbowAnchorRow = 0;
}
if (rainbowAnchorRow >= GetRoguePlayableHeight())
{
rainbowAnchorRow = GetRoguePlayableHeight() - 1;
}
// 第二阶段:按锚点行执行彩虹清除和覆盖行染色。
int rainbowRecoloredCount = 0;
int rainbowClearedCount = TriggerRainbowColorShift(rainbowAnchorRow, point.y, point.y + 3, rainbowRecoloredCount);
int rainbowScore = 0;
int rainbowExp = 0;
int voidClearedCount = 0;
int voidScore = 0;
int voidExp = 0;
if (currentMode == MODE_ROGUE && rainbowClearedCount > 0)
{
// Rogue 模式下特殊清除也能获得得分和经验,但不直接触发升级菜单。
AwardRogueSkillClearRewards(rainbowClearedCount, rainbowScore, rainbowExp, false);
if (rogueStats.voidCoreLevel > 0)
{
voidClearedCount = TriggerMiniBlackHole(5);
AwardRogueSkillClearRewards(voidClearedCount, voidScore, voidExp, false);
}
}
TCHAR rainbowDetail[128];
if (voidClearedCount > 0)
{
_stprintf_s(
rainbowDetail,
_T("第 %d 行清 %d 格,染色 %d 格,虚空追加 %d 格"),
rainbowAnchorRow + 1,
rainbowClearedCount,
rainbowRecoloredCount,
voidClearedCount);
}
else
{
_stprintf_s(
rainbowDetail,
_T("第 %d 行清除主色 %d 格,覆盖行染色 %d 格。"),
rainbowAnchorRow + 1,
rainbowClearedCount,
rainbowRecoloredCount);
}
SetFeedbackMessage(_T("彩虹方块"), rainbowDetail, 10);
}
/**
* @brief
* @param explosiveCells
* @param explosiveCellCount
*/
static void ApplyExplosiveLandingEffect(const Point* explosiveCells, int explosiveCellCount)
{
// 非爆破方块直接跳过,保持普通方块落地流程轻量。
if (!currentPieceIsExplosive)
{
return;
}
// 每个落地格都作为爆心清除范围,连环炸弹会扩大底层清除函数的范围。
int explosiveCellsCleared = 0;
for (int i = 0; i < explosiveCellCount; i++)
{
explosiveCellsCleared += ClearExplosiveAreaAt(explosiveCells[i].y, explosiveCells[i].x);
}
int explosiveScoreGain = 0;
int explosiveExpGain = 0;
if (currentMode == MODE_ROGUE && explosiveCellsCleared > 0)
{
AwardRogueSkillClearRewards(explosiveCellsCleared, explosiveScoreGain, explosiveExpGain, false);
}
TCHAR explosiveDetail[128];
_stprintf_s(
explosiveDetail,
_T("爆破清除 %d 格 +%d 分 +%d EXP"),
explosiveCellsCleared,
explosiveScoreGain,
explosiveExpGain);
SetFeedbackMessage(_T("爆破核心"), explosiveDetail, 12);
// 连环炸弹需要等标准消行判断完成后,再决定是否追加一次小爆炸。
if (rogueStats.chainBombLevel > 0 && explosiveCellCount > 0)
{
pendingChainBombCenter = explosiveCells[0];
pendingChainBombFollowup = true;
}
}
/**
* @brief
* @param fixedCells
* @param fixedCellCount
*/
static void ApplyLaserLandingEffect(const Point* fixedCells, int fixedCellCount)
{
// 激光方块以落地格平均列作为贯穿列,减少不同形状造成的位置偏差。
if (!currentPieceIsLaser)
{
return;
}
int laserColumn = point.x + 1;
if (fixedCellCount > 0)
{
int xSum = 0;
for (int i = 0; i < fixedCellCount; i++)
{
xSum += fixedCells[i].x;
}
laserColumn = (xSum + fixedCellCount / 2) / fixedCellCount;
}
if (laserColumn < 0)
{
laserColumn = 0;
}
if (laserColumn >= nGameWidth)
{
laserColumn = nGameWidth - 1;
}
int laserCellsCleared = ClearColumnAt(laserColumn);
if (currentMode == MODE_ROGUE && laserCellsCleared > 0)
{
int laserScore = 0;
int laserExp = 0;
AwardRogueSkillClearRewards(laserCellsCleared, laserScore, laserExp, false);
TCHAR laserDetail[128];
_stprintf_s(laserDetail, _T("激光贯穿第 %d 列,清除 %d 格 +%d 分 +%d EXP"), laserColumn + 1, laserCellsCleared, laserScore, laserExp);
SetFeedbackMessage(_T("棱镜激光"), laserDetail, 12);
}
}
/**
* @brief
* @param fixedCells
* @param fixedCellCount
*/
static void ApplyCrossLandingEffect(const Point* fixedCells, int fixedCellCount)
{
// 十字方块同时计算中心行和中心列,后续分别触发行清除与列清除。
if (!currentPieceIsCross)
{
return;
}
int crossRow = point.y + 1;
int crossColumn = point.x + 1;
if (fixedCellCount > 0)
{
int xSum = 0;
int ySum = 0;
for (int i = 0; i < fixedCellCount; i++)
{
xSum += fixedCells[i].x;
ySum += fixedCells[i].y;
}
crossColumn = (xSum + fixedCellCount / 2) / fixedCellCount;
crossRow = (ySum + fixedCellCount / 2) / fixedCellCount;
}
if (crossRow < 0)
{
crossRow = 0;
}
if (crossRow >= GetRoguePlayableHeight())
{
crossRow = GetRoguePlayableHeight() - 1;
}
if (crossColumn < 0)
{
crossColumn = 0;
}
if (crossColumn >= nGameWidth)
{
crossColumn = nGameWidth - 1;
}
int crossCellsCleared = ClearRowAt(crossRow);
int columnCellsCleared = ClearColumnAtWithColor(crossColumn, RGB(196, 255, 132));
if (workRegion[crossRow][crossColumn] == 0 && columnCellsCleared > 0)
{
// 中心格可能已经在行清除时被计数,这里保持原有结算方式。
}
int totalCrossCleared = crossCellsCleared + columnCellsCleared;
if (currentMode == MODE_ROGUE && totalCrossCleared > 0)
{
int crossScore = 0;
int crossExp = 0;
AwardRogueSkillClearRewards(totalCrossCleared, crossScore, crossExp, false);
TCHAR crossDetail[128];
_stprintf_s(crossDetail, _T("十字冲击第 %d 行 / 第 %d 列,清除 %d 格 +%d 分 +%d EXP"), crossRow + 1, crossColumn + 1, totalCrossCleared, crossScore, crossExp);
SetFeedbackMessage(_T("十字方块"), crossDetail, 12);
}
}
/**
* @brief
*/
static void ApplyStableStructureEffect()
{
if (!currentPieceIsRainbow && TryStabilizeBoard() > 0)
{
SetFeedbackMessage(_T("稳定结构"), _T("附近空洞被自动填补,阵型更加稳固。"), 10);
}
}
/**
* @brief
* @param fixedCells
* @param fixedCellCount
* @param explosiveCells
* @param explosiveCellCount
*/
void ApplySpecialLandingEffects(const Point* fixedCells, int fixedCellCount, const Point* explosiveCells, int explosiveCellCount)
{
// 多种特殊标记按固定顺序结算,保证同一落地事件的反馈和奖励稳定。
ApplyExplosiveLandingEffect(explosiveCells, explosiveCellCount);
ApplyLaserLandingEffect(fixedCells, fixedCellCount);
ApplyCrossLandingEffect(fixedCells, fixedCellCount);
ApplyStableStructureEffect();
}
+157
View File
@@ -0,0 +1,157 @@
#include "stdafx.h"
/**
* @file TetrisRenderAssets.cpp
* @brief GDI+
*/
#include "TetrisRenderInternal.h"
#include "TetrisAssets.h"
#include <objidl.h>
#include <string>
#pragma comment(lib, "gdiplus.lib")
using namespace Gdiplus;
/**
* @brief GDI+
* @param path
* @return nullptr
*/
static Bitmap* TryLoadBitmap(const std::wstring& path)
{
// 空路径和不存在的文件不交给 GDI+,减少无效加载开销。
if (path.empty() || !FileExists(path))
{
return nullptr;
}
// GDI+ 返回对象后仍需检查状态,失败对象要立即释放。
Bitmap* loadedImage = Bitmap::FromFile(path.c_str(), FALSE);
if (loadedImage != nullptr && loadedImage->GetLastStatus() == Ok)
{
return loadedImage;
}
delete loadedImage;
return nullptr;
}
/**
* @brief GDI+
* @return GDI+ true false
*/
static bool EnsureGdiplusStarted()
{
static ULONG_PTR gdiplusToken = 0;
static bool attempted = false;
static bool started = false;
if (!attempted)
{
// GDI+ 只需要初始化一次,静态标记避免重复启动。
attempted = true;
GdiplusStartupInput startupInput;
started = GdiplusStartup(&gdiplusToken, &startupInput, nullptr) == Ok;
}
return started;
}
/**
* @brief
* @return nullptr
*/
Bitmap* LoadBackgroundImage()
{
static Bitmap* backgroundImage = nullptr;
static bool attempted = false;
// 背景图只查找一次,失败后也记住结果,避免每帧重复访问磁盘。
if (!attempted)
{
attempted = true;
if (EnsureGdiplusStarted())
{
const std::wstring candidates[] =
{
BuildAssetPath(L"assets\\images\\background.png"),
BuildWorkingDirAssetPath(L"assets\\images\\background.png"),
BuildAssetPath(L"assets\\images\\background.bmp"),
BuildWorkingDirAssetPath(L"assets\\images\\background.bmp")
};
// 同时支持构建目录运行和项目根目录运行两种启动方式。
for (const std::wstring& candidate : candidates)
{
backgroundImage = TryLoadBitmap(candidate);
if (backgroundImage != nullptr)
{
break;
}
}
}
}
return backgroundImage;
}
/**
* @brief
* @param index
* @return nullptr
*/
Bitmap* LoadCreditImage(int index)
{
constexpr int creditPageCount = 5;
static Bitmap* creditImages[creditPageCount] = {};
static bool attempted[creditPageCount] = {};
if (index < 0 || index >= creditPageCount)
{
return nullptr;
}
// 每张致谢图单独缓存,只有首次进入对应页时才加载。
if (!attempted[index])
{
attempted[index] = true;
if (EnsureGdiplusStarted())
{
const wchar_t* imageNames[creditPageCount] =
{
L"assets\\images\\qls.jpg",
L"assets\\images\\wyk.jpg",
L"assets\\images\\swj.jpg",
L"assets\\images\\qhy.jpg",
L"assets\\images\\syc.jpg"
};
const std::wstring creditExtraCandidates[] =
{
BuildAssetPath(imageNames[index]),
BuildWorkingDirAssetPath(imageNames[index]),
BuildAssetPath(L"assets\\images\\qhy.png"),
BuildWorkingDirAssetPath(L"assets\\images\\qhy.png"),
BuildAssetPath(L"assets\\images\\qhy.jpeg"),
BuildWorkingDirAssetPath(L"assets\\images\\qhy.jpeg"),
BuildAssetPath(L"assets\\images\\qhy.bmp"),
BuildWorkingDirAssetPath(L"assets\\images\\qhy.bmp")
};
int candidateCount = (index == 3) ? 8 : 2;
// 第四张致谢图历史上有多种扩展名,这里保留兼容查找。
for (int i = 0; i < candidateCount; i++)
{
creditImages[index] = TryLoadBitmap(creditExtraCandidates[i]);
if (creditImages[index] != nullptr)
{
break;
}
}
}
}
return creditImages[index];
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+11 -3
View File
@@ -1,8 +1,16 @@
// stdafx.cpp : 只包括标准包含文件的源文件 /**
// Tetris.pch 将作为预编译头 * @file stdafx.cpp
// stdafx.obj 将包含预编译类型信息 * @brief stdafx.h
*/
#include "stdafx.h" #include "stdafx.h"
/**
* @file stdafx.cpp
* @brief stdafx.h
*
* Visual Studio
*/
// TODO: 在 STDAFX.H 中 // TODO: 在 STDAFX.H 中
// 引用任何所需的附加头文件,而不是在此文件中引用 // 引用任何所需的附加头文件,而不是在此文件中引用