Compare commits
11 Commits
2f435f5ca6
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 70b1665b31 | |||
| 59e491038d | |||
| d5f6cea2ed | |||
| 84017ae6b7 | |||
| 7fe0244a99 | |||
| be95bd25e1 | |||
| b98d2c9d59 | |||
| a331162349 | |||
| 58ab400949 | |||
| 1c000c3c21 | |||
| 0840a807b5 |
@@ -1,5 +1,6 @@
|
|||||||
# Build outputs
|
# Build outputs
|
||||||
/.vscode-build/
|
/.vscode-build/
|
||||||
|
/.worktrees/
|
||||||
/build/
|
/build/
|
||||||
/bin/
|
/bin/
|
||||||
/obj/
|
/obj/
|
||||||
|
|||||||
@@ -38,3 +38,4 @@
|
|||||||
|
|
||||||
1. 每次补全前后都要保存版本,便于报告展示。
|
1. 每次补全前后都要保存版本,便于报告展示。
|
||||||
2. 现场汇报时,所有组员都可能被提问,不能只有一人理解代码。
|
2. 现场汇报时,所有组员都可能被提问,不能只有一人理解代码。
|
||||||
|
3. 作业要求LLM对话迭代生成代码 虽然我们不是 我们是vibe coding 但是在后续生成报告相关的时候 要伪装的像是LLM对话迭代生成的
|
||||||
|
|||||||
-138
@@ -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
|
|
||||||
```
|
|
||||||
@@ -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.
|
||||||
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Notice
|
||||||
|
|
||||||
|
本仓库是程序设计课程大作业项目,仅用于课程学习、课堂展示和个人技术交流,不用于商业发布。
|
||||||
|
|
||||||
|
## 授权范围
|
||||||
|
|
||||||
|
- `src/`、构建脚本和项目文档中的原创代码内容按 `LICENSE` 中的 MIT License 授权。
|
||||||
|
- `assets/`、`report/images/` 和 `report/code-snippets/` 中的图片、音频、视频等非代码素材不包含在 MIT License 授权范围内。
|
||||||
|
- 如需二次发布、公开分发可执行文件或用于课程以外场景,请先替换或移除未取得独立授权的素材。
|
||||||
|
|
||||||
|
## 素材来源
|
||||||
|
|
||||||
|
- 音乐素材:来自《千恋*万花》,仅作为课程大作业学习展示使用,版权归原权利方所有。
|
||||||
|
- 图片素材:由 AI 生成或用于课程报告展示。
|
||||||
|
- 图标、视频和其他资源:仅随课程项目用于演示程序功能,不代表已获得商业使用授权。
|
||||||
|
|
||||||
|
## 使用提醒
|
||||||
|
|
||||||
|
如果将项目上传到公开平台,建议在发布说明中保留本文件,并明确说明素材来源和授权限制。若需要更严格地规避素材版权风险,可以只公开源码和文档,删除 `assets/` 下的媒体文件。
|
||||||
|
|
||||||
@@ -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`。
|
||||||
|
|||||||
@@ -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 反思、成员分工”的要求。
|
||||||
|
|||||||
-147
@@ -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
@@ -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 = @()
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -1,12 +1,19 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @file resource.h
|
||||||
|
* @brief 定义菜单、图标、对话框和命令等 Windows 资源编号。
|
||||||
|
*/
|
||||||
|
|
||||||
//{{NO_DEPENDENCIES}}
|
//{{NO_DEPENDENCIES}}
|
||||||
// Microsoft Visual C++ 生成的包含文件。
|
// Microsoft Visual C++ 生成的包含文件。
|
||||||
// 供 Tetris.rc 使用
|
// 供 Tetris.rc 使用
|
||||||
//
|
//
|
||||||
|
|
||||||
|
// 字符串资源:窗口标题等文本由 Win32 启动流程按编号读取。
|
||||||
#define IDS_APP_TITLE 103
|
#define IDS_APP_TITLE 103
|
||||||
|
|
||||||
|
// 图标、对话框、菜单和命令编号需要与 Tetris.rc 中的资源定义保持一致。
|
||||||
#define IDR_MAINFRAME 128
|
#define IDR_MAINFRAME 128
|
||||||
#define IDD_TETRIS_DIALOG 102
|
#define IDD_TETRIS_DIALOG 102
|
||||||
#define IDD_ABOUTBOX 103
|
#define IDD_ABOUTBOX 103
|
||||||
@@ -19,11 +26,13 @@
|
|||||||
#define IDC_MYICON 2
|
#define IDC_MYICON 2
|
||||||
|
|
||||||
#ifndef IDC_STATIC
|
#ifndef IDC_STATIC
|
||||||
|
// 静态文本控件使用 -1,表示运行时不需要通过控件 ID 单独访问。
|
||||||
#define IDC_STATIC -1
|
#define IDC_STATIC -1
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef APSTUDIO_INVOKED
|
#ifdef APSTUDIO_INVOKED
|
||||||
#ifndef APSTUDIO_READONLY_SYMBOLS
|
#ifndef APSTUDIO_READONLY_SYMBOLS
|
||||||
|
// 以下编号由资源编辑器维护,手工改动容易导致新增资源编号冲突。
|
||||||
#define _APS_NO_MFC 130
|
#define _APS_NO_MFC 130
|
||||||
#define _APS_NEXT_RESOURCE_VALUE 129
|
#define _APS_NEXT_RESOURCE_VALUE 129
|
||||||
#define _APS_NEXT_COMMAND_VALUE 32771
|
#define _APS_NEXT_COMMAND_VALUE 32771
|
||||||
|
|||||||
@@ -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: 在此处引用程序需要的其他头文件
|
// 其他模块各自包含自己的业务头文件,避免预编译头承担过多项目依赖。
|
||||||
|
|||||||
@@ -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
File diff suppressed because it is too large
Load Diff
+46
-419
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+15
-2849
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
+74
-2
@@ -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)
|
||||||
{
|
{
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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
@@ -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 中
|
||||||
// 引用任何所需的附加头文件,而不是在此文件中引用
|
// 引用任何所需的附加头文件,而不是在此文件中引用
|
||||||
|
|||||||
Reference in New Issue
Block a user