Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| be95bd25e1 | |||
| b98d2c9d59 | |||
| a331162349 | |||
| 58ab400949 | |||
| 1c000c3c21 | |||
| 0840a807b5 |
@@ -1,5 +1,6 @@
|
||||
# Build outputs
|
||||
/.vscode-build/
|
||||
/.worktrees/
|
||||
/build/
|
||||
/bin/
|
||||
/obj/
|
||||
|
||||
@@ -98,6 +98,15 @@ Tereis/
|
||||
├─ src/
|
||||
│ ├─ include/ 头文件
|
||||
│ ├─ source/ 源文件
|
||||
│ │ ├─ Tetris.cpp 程序入口、窗口和消息框架
|
||||
│ │ ├─ TetrisLogic.cpp 基础俄罗斯方块逻辑框架
|
||||
│ │ ├─ TetrisRender.cpp 基础绘制框架
|
||||
│ │ ├─ common/ 资源路径、文件检查等通用工具
|
||||
│ │ ├─ app/ 媒体播放、布局命中、输入和定时器处理
|
||||
│ │ ├─ extensions/ 框架外通用扩展、界面状态和视觉效果
|
||||
│ │ ├─ logic/ 特殊方块落地效果等逻辑扩展
|
||||
│ │ ├─ render/ 图片加载等渲染内部支持
|
||||
│ │ └─ rogue/ Rogue 模式、强化和技能系统
|
||||
│ └─ resources/ Windows 资源脚本
|
||||
├─ assets/
|
||||
│ ├─ audio/ 背景音乐
|
||||
@@ -130,6 +139,8 @@ Tereis/
|
||||
C:\mingw64\bin\
|
||||
```
|
||||
|
||||
构建脚本会递归收集 `src/source` 下的 `.cpp` 文件。新增功能代码可以放入功能目录,不需要手动维护固定源码列表。
|
||||
|
||||
## 构建与运行
|
||||
|
||||
在项目根目录执行:
|
||||
@@ -194,10 +205,16 @@ powershell -NoProfile -ExecutionPolicy Bypass -File .\build-mingw.ps1 -Run
|
||||
|
||||
本项目以过程式 C++ 写法为主,核心逻辑分布如下:
|
||||
|
||||
- `src/source/Tetris.cpp`:窗口、消息循环、输入和鼠标交互
|
||||
- `src/source/Tetris.cpp`:Win32 程序入口、窗口创建和消息分发主干
|
||||
- `src/source/TetrisLogic.cpp`:基础方块逻辑、消行和状态重置
|
||||
- `src/source/TetrisRogue.cpp`:Rogue 模式、强化、技能和成长系统
|
||||
- `src/source/TetrisRender.cpp`:界面绘制、面板、动画和特效
|
||||
- `src/source/common/TetrisAssets.cpp`:资源路径拼接和文件存在判断
|
||||
- `src/source/app/`:背景音乐、复活视频、窗口布局命中、鼠标键盘和定时器处理
|
||||
- `src/source/logic/TetrisPieceEffects.cpp`:彩虹、爆破、激光、十字和稳定结构等落地效果
|
||||
- `src/source/extensions/TetrisGameExtensions.cpp`:框架外通用状态切换、复活、说明页、视觉效果等扩展支持
|
||||
- `src/source/render/TetrisRenderAssets.cpp`:背景图、致谢页图片等 GDI+ 图片资源加载
|
||||
- `src/source/rogue/TetrisRogue.cpp`:Rogue 模式、强化、技能和成长系统
|
||||
- `src/include/Tetris.h`:主要结构体、全局状态和函数声明
|
||||
- `src/include/TetrisAppInternal.h`、`src/include/TetrisRenderInternal.h`、`src/include/TetrisAssets.h`:窗口层、渲染层和资源工具的内部声明
|
||||
|
||||
项目适合作为程序设计课程大作业展示,也便于在答辩时讲解窗口程序、游戏循环、碰撞检测、状态管理和功能扩展。
|
||||
|
||||
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
|
||||
|
||||
$Sources = @(
|
||||
(Join-Path $SourceDir "stdafx.cpp"),
|
||||
(Join-Path $SourceDir "Tetris.cpp"),
|
||||
(Join-Path $SourceDir "TetrisLogic.cpp"),
|
||||
(Join-Path $SourceDir "TetrisLogicInnovation.cpp"),
|
||||
(Join-Path $SourceDir "TetrisRogue.cpp"),
|
||||
(Join-Path $SourceDir "TetrisRender.cpp")
|
||||
)
|
||||
$Sources = Get-ChildItem -Path $SourceDir -Recurse -Filter "*.cpp" |
|
||||
Sort-Object FullName |
|
||||
Select-Object -ExpandProperty FullName
|
||||
|
||||
$LinkInputs = @()
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* @file Tetris.h
|
||||
* @brief 定义俄罗斯方块项目的全局常量、结构体、枚举、全局状态和公开函数接口。
|
||||
*/
|
||||
|
||||
#include "resource.h"
|
||||
#include "stdafx.h"
|
||||
#include <mmsystem.h>
|
||||
@@ -264,60 +269,329 @@ extern bool currentPieceIsRainbow;
|
||||
extern int bricks[7][4][4][4];
|
||||
extern COLORREF BrickColor[7];
|
||||
|
||||
/**
|
||||
* @brief 判断当前活动方块是否还能向下移动一格。
|
||||
* @return 可以下落返回 true,否则返回 false。
|
||||
*/
|
||||
bool CanMoveDown();
|
||||
|
||||
/**
|
||||
* @brief 判断当前活动方块是否还能向左移动一格。
|
||||
* @return 可以左移返回 true,否则返回 false。
|
||||
*/
|
||||
bool CanMoveLeft();
|
||||
|
||||
/**
|
||||
* @brief 判断当前活动方块是否还能向右移动一格。
|
||||
* @return 可以右移返回 true,否则返回 false。
|
||||
*/
|
||||
bool CanMoveRight();
|
||||
|
||||
/**
|
||||
* @brief 将当前活动方块向下移动一格。
|
||||
*/
|
||||
void MoveDown();
|
||||
|
||||
/**
|
||||
* @brief 将当前活动方块向左移动一格。
|
||||
*/
|
||||
void MoveLeft();
|
||||
|
||||
/**
|
||||
* @brief 将当前活动方块向右移动一格。
|
||||
*/
|
||||
void MoveRight();
|
||||
|
||||
/**
|
||||
* @brief 尝试旋转当前活动方块,Rogue 完美旋转会额外尝试左右偏移。
|
||||
*/
|
||||
void Rotate();
|
||||
|
||||
/**
|
||||
* @brief 将当前活动方块直接下落到预测落点。
|
||||
*/
|
||||
void DropDown();
|
||||
|
||||
/**
|
||||
* @brief 将当前活动方块固定到棋盘并生成下一块。
|
||||
*/
|
||||
void Fixing();
|
||||
|
||||
/**
|
||||
* @brief 删除指定行并让上方棋盘整体下落。
|
||||
* @param number 要删除的棋盘行号。
|
||||
*/
|
||||
void DeleteOneLine(int number);
|
||||
|
||||
/**
|
||||
* @brief 扫描棋盘、删除所有满行并触发消行结算。
|
||||
* @return 本次删除的行数。
|
||||
*/
|
||||
int DeleteLines();
|
||||
|
||||
/**
|
||||
* @brief 计算当前活动方块的预测落点。
|
||||
*/
|
||||
void ComputeTarget();
|
||||
|
||||
/**
|
||||
* @brief 重置棋盘、方块、统计和视觉状态,开始一局新游戏。
|
||||
*/
|
||||
void Restart();
|
||||
|
||||
/**
|
||||
* @brief 按指定模式开始新游戏。
|
||||
* @param mode 游戏模式,取值来自 GameMode。
|
||||
*/
|
||||
void StartGameWithMode(int mode);
|
||||
|
||||
/**
|
||||
* @brief 返回主菜单并清理临时玩法与界面状态。
|
||||
*/
|
||||
void ReturnToMainMenu();
|
||||
|
||||
/**
|
||||
* @brief 复活视频播放成功后恢复游戏并清理顶部空间。
|
||||
*/
|
||||
void ReviveAfterVideo();
|
||||
|
||||
/**
|
||||
* @brief 从帮助页进入 Rogue 技能演示的第一项。
|
||||
*/
|
||||
void StartRogueSkillDemo();
|
||||
|
||||
/**
|
||||
* @brief 从帮助页进入指定 Rogue 技能演示。
|
||||
* @param demoIndex 技能演示序号。
|
||||
*/
|
||||
void StartRogueSkillDemoAt(int demoIndex);
|
||||
|
||||
/**
|
||||
* @brief 重新开始当前 Rogue 技能演示场景。
|
||||
*/
|
||||
void RestartCurrentRogueSkillDemo();
|
||||
|
||||
/**
|
||||
* @brief 判断当前是否处于 Rogue 技能演示模式。
|
||||
* @return 演示模式中返回 true,否则返回 false。
|
||||
*/
|
||||
bool IsRogueSkillDemoMode();
|
||||
|
||||
/**
|
||||
* @brief 推进 Rogue 技能演示计时。
|
||||
* @return 演示模式正在运行返回 true,否则返回 false。
|
||||
*/
|
||||
bool TickRogueSkillDemo();
|
||||
|
||||
/**
|
||||
* @brief 切换到下一项 Rogue 技能演示。
|
||||
*/
|
||||
void AdvanceRogueSkillDemo();
|
||||
|
||||
/**
|
||||
* @brief 获取 Rogue 技能演示条目数量。
|
||||
* @return 可选择的演示条目总数。
|
||||
*/
|
||||
int GetRogueSkillDemoCount();
|
||||
|
||||
/**
|
||||
* @brief 获取指定 Rogue 技能演示名称。
|
||||
* @param demoIndex 技能演示序号。
|
||||
* @return 名称字符串,越界时返回空字符串。
|
||||
*/
|
||||
const TCHAR* GetRogueSkillDemoName(int demoIndex);
|
||||
|
||||
/**
|
||||
* @brief 获取指定 Rogue 技能演示说明。
|
||||
* @param demoIndex 技能演示序号。
|
||||
* @return 说明字符串,越界时返回空字符串。
|
||||
*/
|
||||
const TCHAR* GetRogueSkillDemoDetail(int demoIndex);
|
||||
|
||||
/**
|
||||
* @brief 获取当前 Rogue 技能演示名称。
|
||||
* @return 当前名称,非演示模式返回空字符串。
|
||||
*/
|
||||
const TCHAR* GetCurrentRogueSkillDemoName();
|
||||
|
||||
/**
|
||||
* @brief 设置右侧战斗日志反馈信息。
|
||||
* @param title 反馈标题。
|
||||
* @param detail 反馈详情。
|
||||
* @param ticks 保持显示的游戏计时次数。
|
||||
*/
|
||||
void SetFeedbackMessage(const TCHAR* title, const TCHAR* detail, int ticks);
|
||||
|
||||
/**
|
||||
* @brief 打开帮助首页。
|
||||
*/
|
||||
void OpenRulesScreen();
|
||||
|
||||
/**
|
||||
* @brief 打开 Rogue 技能演示选择页。
|
||||
*/
|
||||
void OpenSkillDemoScreen();
|
||||
|
||||
/**
|
||||
* @brief 打开致谢页。
|
||||
*/
|
||||
void OpenCreditScreen();
|
||||
|
||||
/**
|
||||
* @brief 切换致谢页图片。
|
||||
* @param direction 小于 0 向前切换,大于 0 向后切换。
|
||||
*/
|
||||
void ChangeCreditPage(int direction);
|
||||
|
||||
/**
|
||||
* @brief 打开 Rogue 升级选择界面。
|
||||
*/
|
||||
void OpenUpgradeMenu();
|
||||
|
||||
/**
|
||||
* @brief 确认当前升级选择并恢复游戏流程。
|
||||
*/
|
||||
void ConfirmUpgradeSelection();
|
||||
|
||||
/**
|
||||
* @brief 重置升级选择界面状态。
|
||||
*/
|
||||
void ResetUpgradeUiState();
|
||||
|
||||
/**
|
||||
* @brief 使用或解锁后处理 Hold 备用仓逻辑。
|
||||
*/
|
||||
void HoldCurrentPiece();
|
||||
|
||||
/**
|
||||
* @brief 使用清屏炸弹主动技能。
|
||||
*/
|
||||
void UseScreenBomb();
|
||||
|
||||
/**
|
||||
* @brief 使用黑洞主动技能。
|
||||
*/
|
||||
void UseBlackHole();
|
||||
|
||||
/**
|
||||
* @brief 使用空中换形主动技能。
|
||||
*/
|
||||
void UseAirReshape();
|
||||
|
||||
/**
|
||||
* @brief 重置 Rogue 待播放视觉事件。
|
||||
*/
|
||||
void ResetPendingRogueVisualEvents();
|
||||
|
||||
/**
|
||||
* @brief 清空所有视觉效果状态。
|
||||
*/
|
||||
void ResetVisualEffects();
|
||||
|
||||
/**
|
||||
* @brief 推进视觉效果动画。
|
||||
* @return 仍有动画需要刷新返回 true,否则返回 false。
|
||||
*/
|
||||
bool TickVisualEffects();
|
||||
|
||||
/**
|
||||
* @brief 推进致谢页切换动画。
|
||||
* @return 需要刷新界面返回 true,否则返回 false。
|
||||
*/
|
||||
bool TickCreditAnimation();
|
||||
|
||||
/**
|
||||
* @brief 触发标准消行动画。
|
||||
* @param rows 被消除的行号数组。
|
||||
* @param rowCount 行号数量。
|
||||
* @param linesCleared 实际消除行数。
|
||||
*/
|
||||
void TriggerLineClearEffect(const int* rows, int rowCount, int linesCleared);
|
||||
|
||||
/**
|
||||
* @brief 播放之前因升级界面暂存的消行动画。
|
||||
*/
|
||||
void PlayPendingLineClearEffect();
|
||||
|
||||
/**
|
||||
* @brief 触发指定棋盘格的默认清除特效。
|
||||
* @param cells 被清除格子数组。
|
||||
* @param cellCount 格子数量。
|
||||
* @param strongBurst 是否使用更强的爆裂粒子。
|
||||
*/
|
||||
void TriggerCellClearEffect(const Point* cells, int cellCount, bool strongBurst);
|
||||
|
||||
/**
|
||||
* @brief 触发指定棋盘格的自定义颜色清除特效。
|
||||
* @param cells 被清除格子数组。
|
||||
* @param cellCount 格子数量。
|
||||
* @param flashColor 高亮颜色。
|
||||
* @param strongBurst 是否使用更强的爆裂粒子。
|
||||
*/
|
||||
void TriggerColoredCellClearEffect(const Point* cells, int cellCount, COLORREF flashColor, bool strongBurst);
|
||||
|
||||
/**
|
||||
* @brief 记录一个固定方块受重力下落的轨迹。
|
||||
* @param x 棋盘列号。
|
||||
* @param fromY 下落起始行号。
|
||||
* @param toY 下落目标行号。
|
||||
* @param cellValue 方块格子数值。
|
||||
*/
|
||||
void TriggerGravityFallEffect(int x, int fromY, int toY, int cellValue);
|
||||
|
||||
/**
|
||||
* @brief 为 Rogue 主动或特殊技能清除格子发放奖励。
|
||||
* @param clearedCells 清除格子数。
|
||||
* @param scoreGain 返回本次得分增量。
|
||||
* @param expGain 返回本次经验增量。
|
||||
* @param allowLevelProgress 是否允许本次奖励触发升级流程。
|
||||
*/
|
||||
void AwardRogueSkillClearRewards(int clearedCells, int& scoreGain, int& expGain, bool allowLevelProgress);
|
||||
|
||||
/**
|
||||
* @brief 检查 Rogue 经验是否达到升级条件。
|
||||
*/
|
||||
void CheckRogueLevelProgress();
|
||||
|
||||
/**
|
||||
* @brief 对棋盘固定方块应用重力下落。
|
||||
*/
|
||||
void ApplyBoardGravity();
|
||||
|
||||
/**
|
||||
* @brief 计算当前 Rogue 模式下落间隔。
|
||||
* @return 下落计时器间隔,单位毫秒。
|
||||
*/
|
||||
int GetRogueFallInterval();
|
||||
|
||||
/**
|
||||
* @brief 获取 Rogue 当前可操作棋盘高度。
|
||||
* @return 未被底部封锁占用的行数。
|
||||
*/
|
||||
int GetRoguePlayableHeight();
|
||||
|
||||
/**
|
||||
* @brief 获取 Rogue 难度系统当前封锁的底部行数。
|
||||
* @return 封锁行数。
|
||||
*/
|
||||
int GetRogueLockedRows();
|
||||
|
||||
/**
|
||||
* @brief 按经过时间推进 Rogue 难度。
|
||||
* @param elapsedMs 本次推进的时间,单位毫秒。
|
||||
*/
|
||||
void AdvanceRogueDifficulty(int elapsedMs);
|
||||
|
||||
/**
|
||||
* @brief 获取进化强化的合成路线文本。
|
||||
* @param upgradeId 强化编号。
|
||||
* @return 路线文本;普通强化返回空或空指针。
|
||||
*/
|
||||
const TCHAR* GetUpgradeSynthesisPath(int upgradeId);
|
||||
|
||||
/**
|
||||
* @brief 绘制当前窗口中的完整游戏界面。
|
||||
* @param hdc 目标绘图设备上下文。
|
||||
* @param hWnd 当前窗口句柄,用于读取客户区大小。
|
||||
*/
|
||||
void TDrawScreen(HDC hdc, HWND hWnd);
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
#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;
|
||||
|
||||
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,30 @@
|
||||
#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
|
||||
|
||||
/**
|
||||
* @file TetrisLogicInternal.h
|
||||
* @brief 声明棋盘逻辑、Rogue 结算和特殊方块效果使用的内部接口。
|
||||
*/
|
||||
|
||||
#include "Tetris.h"
|
||||
|
||||
extern Point pendingChainBombCenter;
|
||||
@@ -11,86 +16,129 @@ extern int pendingLineClearEffectLineCount;
|
||||
|
||||
/**
|
||||
* @brief 计算指定方块在棋盘顶部的统一生成位置。
|
||||
* @param brickType 方块类型编号。
|
||||
* @return 生成坐标,可能位于可视区域上方。
|
||||
*/
|
||||
Point GetSpawnPoint(int brickType);
|
||||
|
||||
/**
|
||||
* @brief 重置经典或 Rogue 模式使用的玩家统计数据。
|
||||
* @param stats 需要重置的统计结构。
|
||||
* @param useRogueRules 是否按 Rogue 模式设置初始经验需求。
|
||||
*/
|
||||
void ResetPlayerStats(PlayerStats& stats, bool useRogueRules);
|
||||
|
||||
/**
|
||||
* @brief 设置界面右侧显示的即时反馈标题、内容和持续时间。
|
||||
* @param title 反馈标题。
|
||||
* @param detail 反馈详情。
|
||||
* @param ticks 显示持续的游戏计时次数。
|
||||
*/
|
||||
void SetFeedbackMessage(const TCHAR* title, const TCHAR* detail, int ticks);
|
||||
|
||||
/**
|
||||
* @brief 判断指定方块、旋转状态和位置是否可以合法放置。
|
||||
* @param pieceType 方块类型编号。
|
||||
* @param pieceState 方块旋转状态。
|
||||
* @param position 待检测的左上角坐标。
|
||||
* @return 可以放置返回 true,否则返回 false。
|
||||
*/
|
||||
bool IsPiecePlacementValid(int pieceType, int pieceState, Point position);
|
||||
|
||||
/**
|
||||
* @brief 判断棋盘格是否为彩虹特殊方块。
|
||||
* @param cellValue 棋盘格存储值。
|
||||
* @return 彩虹方块返回 true,否则返回 false。
|
||||
*/
|
||||
bool IsRainbowBoardCell(int cellValue);
|
||||
|
||||
/**
|
||||
* @brief 触发小型黑洞并返回被清除的固定方块数量。
|
||||
* @param maxCellsToClear 最多清除的格子数。
|
||||
* @return 实际清除格子数。
|
||||
*/
|
||||
int TriggerMiniBlackHole(int maxCellsToClear);
|
||||
|
||||
/**
|
||||
* @brief 触发彩虹方块行清除与覆盖行染色效果。
|
||||
* @param anchorRow 作为主色判断的中心行。
|
||||
* @param minRow 允许染色范围的最小行。
|
||||
* @param maxRow 允许染色范围的最大行。
|
||||
* @param recoloredCount 返回被染色的格子数。
|
||||
* @return 被清除的主色格子数。
|
||||
*/
|
||||
int TriggerRainbowColorShift(int anchorRow, int minRow, int maxRow, int& recoloredCount);
|
||||
|
||||
/**
|
||||
* @brief 引爆清屏炸弹并返回清除格数。
|
||||
* @return 实际清除格子数。
|
||||
*/
|
||||
int TriggerScreenBomb();
|
||||
|
||||
/**
|
||||
* @brief 清除指定中心点周围的爆破范围并返回清除格数。
|
||||
* @param centerY 爆破中心行。
|
||||
* @param centerX 爆破中心列。
|
||||
* @return 实际清除格子数。
|
||||
*/
|
||||
int ClearExplosiveAreaAt(int centerY, int centerX);
|
||||
|
||||
/**
|
||||
* @brief 清除指定列并返回清除格数。
|
||||
* @param column 目标列号。
|
||||
* @return 实际清除格子数。
|
||||
*/
|
||||
int ClearColumnAt(int column);
|
||||
|
||||
/**
|
||||
* @brief 使用指定颜色特效清除指定列并返回清除格数。
|
||||
* @param column 目标列号。
|
||||
* @param flashColor 清除高亮颜色。
|
||||
* @return 实际清除格子数。
|
||||
*/
|
||||
int ClearColumnAtWithColor(int column, COLORREF flashColor);
|
||||
|
||||
/**
|
||||
* @brief 清除指定行并返回清除格数。
|
||||
* @param row 目标行号。
|
||||
* @return 实际清除格子数。
|
||||
*/
|
||||
int ClearRowAt(int row);
|
||||
|
||||
/**
|
||||
* @brief 尝试填补局部空洞以稳定棋盘结构。
|
||||
* @return 实际填补格子数。
|
||||
*/
|
||||
int TryStabilizeBoard();
|
||||
|
||||
/**
|
||||
* @brief 为当前方块刷新 Rogue 特殊方块标记。
|
||||
* @param allowRandomSpecials 是否允许按强化概率随机生成特殊方块。
|
||||
*/
|
||||
void RollCurrentPieceSpecialFlags(bool allowRandomSpecials);
|
||||
|
||||
/**
|
||||
* @brief 暂存消行动画,等待升级选择结束后再播放。
|
||||
* @param rows 被消除的行号数组。
|
||||
* @param rowCount 行号数量。
|
||||
* @param linesCleared 实际消除行数。
|
||||
*/
|
||||
void QueueLineClearEffect(const int* rows, int rowCount, int linesCleared);
|
||||
|
||||
/**
|
||||
* @brief 记录固定方块受重力下落的轨迹,用于播放纵向残影特效。
|
||||
* @param x 棋盘列号。
|
||||
* @param fromY 起始行号。
|
||||
* @param toY 目标行号。
|
||||
* @param cellValue 方块格子值。
|
||||
*/
|
||||
void TriggerGravityFallEffect(int x, int fromY, int toY, int cellValue);
|
||||
|
||||
/**
|
||||
* @brief 尝试把旋转后的方块横向偏移指定格数后放置。
|
||||
* @param nextState 旋转后的状态编号。
|
||||
* @param offsetX 横向试探偏移。
|
||||
* @return 偏移后可以放置返回 true,否则返回 false。
|
||||
*/
|
||||
bool TryRotateWithOffset(int nextState, int offsetX);
|
||||
|
||||
@@ -101,10 +149,29 @@ void ResetNextQueue();
|
||||
|
||||
/**
|
||||
* @brief 消费队首下一方块并补充新的预览方块。
|
||||
* @return 新的当前方块类型编号。
|
||||
*/
|
||||
int ConsumeNextType();
|
||||
|
||||
/**
|
||||
* @brief 结算一次标准消行带来的 Rogue 玩法效果。
|
||||
* @param linesCleared 本次标准消行数量。
|
||||
*/
|
||||
void ApplyLineClearResult(int linesCleared);
|
||||
|
||||
/**
|
||||
* @brief 结算彩虹方块固定后的染色和清除效果。
|
||||
* @param overflowTop 固定时是否已经越过顶部。
|
||||
* @param fixedCells 当前方块写入棋盘的格子数组。
|
||||
* @param fixedCellCount 写入棋盘的格子数量。
|
||||
*/
|
||||
void ApplyRainbowLandingEffect(bool overflowTop, const Point* fixedCells, int fixedCellCount);
|
||||
|
||||
/**
|
||||
* @brief 结算爆破、激光、十字和稳定结构等特殊落地效果。
|
||||
* @param fixedCells 当前方块写入棋盘的格子数组。
|
||||
* @param fixedCellCount 写入棋盘的格子数量。
|
||||
* @param explosiveCells 爆破方块写入棋盘的格子数组。
|
||||
* @param explosiveCellCount 爆破格子数量。
|
||||
*/
|
||||
void ApplySpecialLandingEffects(const Point* fixedCells, int fixedCellCount, const Point* explosiveCells, int explosiveCellCount);
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* @file TetrisRenderInternal.h
|
||||
* @brief 声明渲染模块内部使用的 GDI+ 图片加载接口。
|
||||
*/
|
||||
|
||||
#include "Tetris.h"
|
||||
#include <objidl.h>
|
||||
#include <gdiplus.h>
|
||||
|
||||
/**
|
||||
* @brief 加载并缓存主背景图片。
|
||||
* @return 成功时返回缓存位图指针,失败时返回 nullptr。
|
||||
*/
|
||||
Gdiplus::Bitmap* LoadBackgroundImage();
|
||||
|
||||
/**
|
||||
* @brief 按序号加载并缓存致谢页图片。
|
||||
* @param index 致谢页图片序号。
|
||||
* @return 成功时返回缓存位图指针,失败或越界时返回 nullptr。
|
||||
*/
|
||||
Gdiplus::Bitmap* LoadCreditImage(int index);
|
||||
@@ -1,5 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* @file resource.h
|
||||
* @brief 定义菜单、图标、对话框和命令等 Windows 资源编号。
|
||||
*/
|
||||
|
||||
//{{NO_DEPENDENCIES}}
|
||||
// Microsoft Visual C++ 生成的包含文件。
|
||||
// 供 Tetris.rc 使用
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// stdafx.h : 标准系统包含文件的包含文件,
|
||||
// 或是经常使用但不常更改的
|
||||
// 特定于项目的包含文件
|
||||
//
|
||||
/**
|
||||
* @file stdafx.h
|
||||
* @brief 集中包含 Windows、C 运行时和项目常用基础头文件。
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* @file targetver.h
|
||||
* @brief 设置 Windows SDK 目标平台版本,供 Win32 头文件选择可用 API。
|
||||
*/
|
||||
|
||||
// 包括 SDKDDKVer.h 将定义可用的最高版本的 Windows 平台。
|
||||
|
||||
// 如果要为以前的 Windows 平台生成应用程序,请包括 WinSDKVer.h,并将
|
||||
|
||||
+84
-1526
File diff suppressed because it is too large
Load Diff
+220
-311
@@ -1,4 +1,9 @@
|
||||
#include "stdafx.h"
|
||||
#include "stdafx.h"
|
||||
/**
|
||||
* @file TetrisLogic.cpp
|
||||
* @brief 实现基础俄罗斯方块的移动、旋转、固定、消行、落点计算和重开逻辑。
|
||||
*/
|
||||
|
||||
#include "Tetris.h"
|
||||
#include "TetrisLogicInternal.h"
|
||||
|
||||
@@ -332,7 +337,7 @@ void MoveRight()
|
||||
*
|
||||
* 游戏中的每种方块都预置了 4 种旋转状态,该函数会先尝试切换到下一状态,
|
||||
* 然后检查旋转后的方块是否越界或与固定方块重叠。
|
||||
* 如果旋转后的状态非法,则恢复到旋转前的状态。
|
||||
* 如果旋转后的状态非法,Rogue 的完美旋转会继续尝试左右各偏移一格。
|
||||
*/
|
||||
void Rotate()
|
||||
{
|
||||
@@ -376,6 +381,129 @@ void DropDown()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 收集当前方块将要固定到棋盘上的格子,并标记是否越过顶部。
|
||||
* @param overflowTop 返回是否有方块格位于可视区域顶部之外。
|
||||
* @param fixedCells 返回普通落地格,用于后续特殊效果定位。
|
||||
* @param fixedCellCount 返回普通落地格数量。
|
||||
* @param explosiveCells 返回爆破方块落地格。
|
||||
* @param explosiveCellCount 返回爆破方块落地格数量。
|
||||
*/
|
||||
static 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。
|
||||
*/
|
||||
static 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、特殊方块和预测落点状态。
|
||||
*/
|
||||
static void SpawnNextFallingPiece()
|
||||
{
|
||||
// 消耗预览队列后重置本回合状态,确保 Hold 和特殊标记只影响新方块。
|
||||
type = ConsumeNextType();
|
||||
nType = nextTypes[0];
|
||||
state = 0;
|
||||
holdUsedThisTurn = false;
|
||||
RollCurrentPieceSpecialFlags(true);
|
||||
point = GetSpawnPoint(type);
|
||||
target = point;
|
||||
ComputeTarget();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 将当前活动方块固定到工作区,并生成下一个活动方块。
|
||||
*
|
||||
@@ -395,280 +523,23 @@ void Fixing()
|
||||
int explosiveCellCount = 0;
|
||||
pendingChainBombFollowup = false;
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
CollectAndWriteFixedCells(overflowTop, fixedCells, fixedCellCount, explosiveCells, explosiveCellCount);
|
||||
|
||||
ApplyRainbowLandingEffect(overflowTop, fixedCells, fixedCellCount);
|
||||
|
||||
if (!ResolveFixingOverflow(overflowTop))
|
||||
{
|
||||
for (int j = 0; j < 4; j++)
|
||||
{
|
||||
if (bricks[type][state][i][j] != 0)
|
||||
{
|
||||
int fixY = point.y + i;
|
||||
int fixX = point.x + j;
|
||||
|
||||
// 只要当前方块任意非空单元仍超出顶部,就标记为结束
|
||||
if (fixY < 0)
|
||||
{
|
||||
overflowTop = true;
|
||||
}
|
||||
|
||||
// 将当前方块在可视区域内的部分写入工作区
|
||||
if (fixY >= 0 && fixY < GetRoguePlayableHeight() && fixX >= 0 && fixX < nGameWidth)
|
||||
{
|
||||
workRegion[fixY][fixX] = currentPieceIsRainbow ? 8 : bricks[type][state][i][j];
|
||||
if (fixedCellCount < 4)
|
||||
{
|
||||
fixedCells[fixedCellCount].x = fixX;
|
||||
fixedCells[fixedCellCount].y = fixY;
|
||||
fixedCellCount++;
|
||||
}
|
||||
if (currentPieceIsExplosive && explosiveCellCount < 4)
|
||||
{
|
||||
explosiveCells[explosiveCellCount].x = fixX;
|
||||
explosiveCells[explosiveCellCount].y = fixY;
|
||||
explosiveCellCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentPieceIsExplosive)
|
||||
{
|
||||
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);
|
||||
}
|
||||
ApplySpecialLandingEffects(fixedCells, fixedCellCount, explosiveCells, explosiveCellCount);
|
||||
|
||||
if (currentMode == MODE_ROGUE)
|
||||
{
|
||||
currentFallInterval = GetRogueFallInterval();
|
||||
}
|
||||
|
||||
// 生成下一个活动方块
|
||||
type = ConsumeNextType();
|
||||
nType = nextTypes[0];
|
||||
state = 0;
|
||||
holdUsedThisTurn = false;
|
||||
RollCurrentPieceSpecialFlags(true);
|
||||
point = GetSpawnPoint(type);
|
||||
target = point;
|
||||
ComputeTarget();
|
||||
SpawnNextFallingPiece();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -697,18 +568,15 @@ void DeleteOneLine(int number)
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 检查并删除所有已满的行,同时更新当前得分。
|
||||
*
|
||||
* 该函数会从底部向上遍历工作区,判断每一行是否被完全填满。
|
||||
* 如果某一行全部非 0,则调用 DeleteOneLine 删除该行,
|
||||
* 并将该行上方的内容整体下移。为了避免连续满行被漏检,
|
||||
* 删除后会继续检查当前行号。每成功消除 1 行,当前得分增加 100 分。
|
||||
* @brief 从底向上扫描满行并删除,记录本次消除的原始行号。
|
||||
* @param clearedRows 返回最多 8 个被消除行号,用于播放消行动画。
|
||||
* @param clearedRowCount 返回记录的行号数量。
|
||||
* @return 本次标准消行数量。
|
||||
*/
|
||||
int DeleteLines()
|
||||
static int ScanAndDeleteFullLines(int clearedRows[], int& clearedRowCount)
|
||||
{
|
||||
int clearedLines = 0;
|
||||
int clearedRows[8] = {};
|
||||
int clearedRowCount = 0;
|
||||
clearedRowCount = 0;
|
||||
|
||||
int playableHeight = GetRoguePlayableHeight();
|
||||
for (int i = playableHeight - 1; i >= 0; i--)
|
||||
@@ -738,7 +606,17 @@ int DeleteLines()
|
||||
}
|
||||
}
|
||||
|
||||
ApplyLineClearResult(clearedLines);
|
||||
return clearedLines;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 根据当前界面状态立即播放或暂存消行动画。
|
||||
* @param clearedRows 已消除行号数组。
|
||||
* @param clearedRowCount 行号数量。
|
||||
* @param clearedLines 本次消行数量。
|
||||
*/
|
||||
static void DispatchLineClearEffect(const int clearedRows[], int clearedRowCount, int clearedLines)
|
||||
{
|
||||
if (currentScreen == SCREEN_UPGRADE)
|
||||
{
|
||||
QueueLineClearEffect(clearedRows, clearedRowCount, clearedLines);
|
||||
@@ -747,54 +625,84 @@ int DeleteLines()
|
||||
{
|
||||
TriggerLineClearEffect(clearedRows, clearedRowCount, clearedLines);
|
||||
}
|
||||
}
|
||||
|
||||
if (pendingChainBombFollowup && clearedLines > 0)
|
||||
/**
|
||||
* @brief 处理连环炸弹因消行触发的一次追加 3x3 爆破。
|
||||
* @param clearedLines 本次标准消行数量。
|
||||
*/
|
||||
static void ResolveChainBombFollowup(int clearedLines)
|
||||
{
|
||||
if (!pendingChainBombFollowup || clearedLines <= 0)
|
||||
{
|
||||
pendingChainBombFollowup = false;
|
||||
return;
|
||||
}
|
||||
|
||||
int followupCleared = 0;
|
||||
int centerY = pendingChainBombCenter.y;
|
||||
int centerX = pendingChainBombCenter.x;
|
||||
Point followupCells[9] = {};
|
||||
pendingChainBombFollowup = false;
|
||||
|
||||
for (int y = centerY - 1; y <= centerY + 1; y++)
|
||||
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++)
|
||||
{
|
||||
for (int x = centerX - 1; x <= centerX + 1; x++)
|
||||
if (y >= 0 && y < GetRoguePlayableHeight() && x >= 0 && x < nGameWidth && workRegion[y][x] != 0)
|
||||
{
|
||||
if (y >= 0 && y < GetRoguePlayableHeight() && x >= 0 && x < nGameWidth && workRegion[y][x] != 0)
|
||||
if (followupCleared < 9)
|
||||
{
|
||||
if (followupCleared < 9)
|
||||
{
|
||||
followupCells[followupCleared].x = x;
|
||||
followupCells[followupCleared].y = y;
|
||||
}
|
||||
workRegion[y][x] = 0;
|
||||
followupCleared++;
|
||||
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
|
||||
|
||||
if (currentMode == MODE_ROGUE && followupCleared > 0)
|
||||
{
|
||||
pendingChainBombFollowup = false;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 检查并删除所有已满的行,同时更新当前得分。
|
||||
*
|
||||
* 该函数会从底部向上遍历工作区,判断每一行是否被完全填满。
|
||||
* 如果某一行全部非 0,则调用 DeleteOneLine 删除该行,
|
||||
* 并将该行上方的内容整体下移。为了避免连续满行被漏检,
|
||||
* 删除后会继续检查当前行号。每成功消除 1 行,当前得分增加 100 分。
|
||||
*
|
||||
* @return 本次实际消除的行数。
|
||||
*/
|
||||
int DeleteLines()
|
||||
{
|
||||
int clearedRows[8] = {};
|
||||
int clearedRowCount = 0;
|
||||
int clearedLines = ScanAndDeleteFullLines(clearedRows, clearedRowCount);
|
||||
|
||||
// 消行数量先进入玩法结算,再根据是否正在升级决定动画立即播放还是暂存。
|
||||
ApplyLineClearResult(clearedLines);
|
||||
DispatchLineClearEffect(clearedRows, clearedRowCount, clearedLines);
|
||||
|
||||
// 连环炸弹的追加爆破只在爆破方块导致后续消行时触发一次。
|
||||
ResolveChainBombFollowup(clearedLines);
|
||||
|
||||
return clearedLines;
|
||||
}
|
||||
@@ -876,3 +784,4 @@ void Restart()
|
||||
ComputeTarget();
|
||||
}
|
||||
|
||||
|
||||
|
||||
+36
-168
@@ -1,13 +1,23 @@
|
||||
#include "stdafx.h"
|
||||
#include "stdafx.h"
|
||||
/**
|
||||
* @file TetrisRender.cpp
|
||||
* @brief 实现主菜单、帮助页、游戏棋盘、侧栏、覆盖层和升级界面的完整绘制逻辑。
|
||||
*/
|
||||
|
||||
#include "Tetris.h"
|
||||
#include "TetrisRenderInternal.h"
|
||||
#include <objidl.h>
|
||||
#include <gdiplus.h>
|
||||
#include <string>
|
||||
|
||||
#pragma comment(lib, "gdiplus.lib")
|
||||
|
||||
using namespace Gdiplus;
|
||||
|
||||
/**
|
||||
* @brief 按颜色缓存粒子画刷,减少动画绘制时重复创建 GDI 对象。
|
||||
* @param color 画刷颜色。
|
||||
* @return 可复用的实心画刷句柄;缓存满时返回临时新建画刷。
|
||||
*/
|
||||
static HBRUSH GetCachedParticleBrush(COLORREF color)
|
||||
{
|
||||
static COLORREF cachedColors[16] = {};
|
||||
@@ -34,174 +44,22 @@ static HBRUSH GetCachedParticleBrush(COLORREF color)
|
||||
return CreateSolidBrush(color);
|
||||
}
|
||||
|
||||
static 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;
|
||||
}
|
||||
|
||||
static 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;
|
||||
}
|
||||
|
||||
static Bitmap* LoadBackgroundImage()
|
||||
{
|
||||
static ULONG_PTR gdiplusToken = 0;
|
||||
static Bitmap* backgroundImage = nullptr;
|
||||
static bool attempted = false;
|
||||
|
||||
if (!attempted)
|
||||
{
|
||||
attempted = true;
|
||||
|
||||
GdiplusStartupInput startupInput;
|
||||
if (GdiplusStartup(&gdiplusToken, &startupInput, nullptr) == Ok)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (candidate.empty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
DWORD attributes = GetFileAttributesW(candidate.c_str());
|
||||
if (attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Bitmap* loadedImage = Bitmap::FromFile(candidate.c_str(), FALSE);
|
||||
if (loadedImage != nullptr && loadedImage->GetLastStatus() == Ok)
|
||||
{
|
||||
backgroundImage = loadedImage;
|
||||
break;
|
||||
}
|
||||
|
||||
delete loadedImage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return backgroundImage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 按序号加载致谢页图片资源。
|
||||
* @brief 绘制当前游戏窗口的完整界面。
|
||||
*
|
||||
* 函数按当前屏幕状态绘制主菜单、帮助页、游戏棋盘、侧栏、覆盖层和升级选择。
|
||||
* 由于大量绘图辅助逻辑共享当前缩放、字体和颜色,保持在同一函数内集中管理,
|
||||
* 避免拆分时改变 GDI 对象的选择和释放顺序。
|
||||
*
|
||||
* @param hdc 目标绘图设备上下文。
|
||||
* @param hWnd 当前窗口句柄,用于读取客户区大小。
|
||||
*/
|
||||
static Bitmap* LoadCreditImage(int index)
|
||||
{
|
||||
constexpr int creditPageCount = 4;
|
||||
static ULONG_PTR gdiplusToken = 0;
|
||||
static Bitmap* creditImages[creditPageCount] = {};
|
||||
static bool attempted[creditPageCount] = {};
|
||||
|
||||
if (index < 0 || index >= creditPageCount)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (!attempted[index])
|
||||
{
|
||||
attempted[index] = true;
|
||||
|
||||
GdiplusStartupInput startupInput;
|
||||
if (GdiplusStartup(&gdiplusToken, &startupInput, nullptr) == Ok)
|
||||
{
|
||||
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"
|
||||
};
|
||||
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++)
|
||||
{
|
||||
const std::wstring& candidate = creditExtraCandidates[i];
|
||||
if (candidate.empty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
DWORD attributes = GetFileAttributesW(candidate.c_str());
|
||||
if (attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Bitmap* loadedImage = Bitmap::FromFile(candidate.c_str(), FALSE);
|
||||
if (loadedImage != nullptr && loadedImage->GetLastStatus() == Ok)
|
||||
{
|
||||
creditImages[index] = loadedImage;
|
||||
break;
|
||||
}
|
||||
|
||||
delete loadedImage;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return creditImages[index];
|
||||
}
|
||||
|
||||
void TDrawScreen(HDC hdc, 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);
|
||||
@@ -282,6 +140,7 @@ void TDrawScreen(HDC hdc, HWND hWnd)
|
||||
const BYTE panelNestedAlpha = 128;
|
||||
const BYTE panelStrongAlpha = 168;
|
||||
|
||||
// 背景图片存在时优先绘制图片并叠加浅色遮罩,否则使用纯色和装饰形状。
|
||||
Bitmap* backgroundImage = LoadBackgroundImage();
|
||||
if (backgroundImage != nullptr)
|
||||
{
|
||||
@@ -318,6 +177,7 @@ void TDrawScreen(HDC hdc, HWND hWnd)
|
||||
DeleteObject(blobBrushB);
|
||||
}
|
||||
|
||||
// 本函数集中创建字体,所有提前 return 分支都要在返回前释放这些 GDI 对象。
|
||||
HFONT titleFont = CreateFont(
|
||||
-SS(36), 0, 0, 0, FW_BOLD, FALSE, FALSE, FALSE,
|
||||
DEFAULT_CHARSET, OUT_OUTLINE_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_NATURAL_QUALITY,
|
||||
@@ -341,6 +201,7 @@ void TDrawScreen(HDC hdc, HWND hWnd)
|
||||
SetBkMode(hdc, TRANSPARENT);
|
||||
SetTextColor(hdc, textColor);
|
||||
|
||||
// 以下局部绘图函数共享 hdc、缩放函数和颜色,避免每个小控件重复计算上下文。
|
||||
auto DrawPanelCard = [&](const RECT& rect, COLORREF fillColor, COLORREF borderColor, int radius)
|
||||
{
|
||||
HBRUSH cardBrush = CreateSolidBrush(fillColor);
|
||||
@@ -475,6 +336,7 @@ void TDrawScreen(HDC hdc, HWND hWnd)
|
||||
DeleteObject(backPen);
|
||||
};
|
||||
|
||||
// 主菜单独立绘制并提前返回,避免后续游戏棋盘和侧栏在菜单后面继续绘制。
|
||||
if (currentScreen == SCREEN_MENU)
|
||||
{
|
||||
RECT menuCard =
|
||||
@@ -602,6 +464,7 @@ void TDrawScreen(HDC hdc, HWND hWnd)
|
||||
return;
|
||||
}
|
||||
|
||||
// 帮助、规则、图鉴、致谢和技能演示入口共用规则页卡片框架。
|
||||
if (currentScreen == SCREEN_RULES)
|
||||
{
|
||||
RECT rulesCard =
|
||||
@@ -783,7 +646,7 @@ void TDrawScreen(HDC hdc, HWND hWnd)
|
||||
{
|
||||
_T("赏金纹章:所有得分收益提高 20%,可重复叠加,是最直接的分数成长。\r\n成长印记:所有 EXP 收益提高 25%,可重复叠加,用来更快进入后续构筑。\r\n缓坠羽翼:降低自然下落速度,最多叠加 4 次。\r\n连击律动:连续消行时追加得分和 EXP,断连后重新累计。\r\n先见之眼:额外显示 1 个后续方块;第三个预览由操控大师解锁。"),
|
||||
_T("最后一搏:首次濒死时自动清理底部 3 行,并保留本局继续机会。\r\n备用仓:解锁 C / Shift,将当前方块放入 Hold 仓或取出备用方块。\r\n完美旋转:旋转被阻挡时尝试左右修正,提高贴墙和缝隙旋转成功率。\r\n时间缓流:堆叠过高时自动减速,给危险局面留出处理时间。\r\n空中换形:可重复补充次数,按 V 将当前下落方块变成 I 块。"),
|
||||
_T("卸压清场:获得时立刻清除最高的一条占用行,直接降低顶部压力。\r\n底线清道夫:通过消行充能自动清底;每级降低需求,最多 4 级。\r\n清屏炸弹:可重复补充数量,按 X 主动清理底部 5 行,消行也会继续充能。\r\n黑洞奇点:可重复补充次数,按 Z 吞噬当前场上数量最多的一种颜色方块。"),
|
||||
_T("卸压清场:获得时立刻清除最高的一条占用行,直接降低顶部压力。\r\n底线清道夫:通过消行充能自动清底;每级降低需求,最多 4 级。\r\n清屏炸弹:一次性解锁,之后通过消行充能;满 16 行获得 1 枚,按 X 清底 5 行。\r\n黑洞奇点:可重复补充次数,按 Z 吞噬当前场上数量最多的一种颜色方块。"),
|
||||
_T("爆破核心:一次性解锁橙红边框方块,落地后以落点为中心清除 3x3 区域。\r\n棱镜激光:一次性解锁青色边框方块,按落地中心列清除整列固定方块。\r\n十字方块:一次性解锁绿色边框方块,按落地中心同时清除一行一列。\r\n彩虹方块:一次性解锁紫色边框方块,清中心行主色并把覆盖行染成场上主色。\r\n方块改造:提高指定方块出现概率,目前主要强化 I 块生成。"),
|
||||
_T("连锁火花:完成消行后追加随机破坏,适合配合稳定堆叠扩大收益。\r\n连环炸弹:强化爆破核心,将爆破范围从 3x3 扩大为 5x5。\r\n雷霆四消:三消或四消后追加雷击,额外清除局部方块。\r\n雷霆棱镜:三消或四消后追加激光清列,强化四消后的清场能力。"),
|
||||
_T("狂热节拍:累计清除 12 行进入狂热,狂热期间收益更高。\r\n怒火连段:连击越高倍率越高,适合连续小消和稳定节奏。\r\n无尽狂热:狂热期间继续消行会延长狂热时间。\r\n高压悬赏:游戏速度更快,但高压下收益也更高。\r\n豪赌四消:四消收益更高,但非四消表现更不稳定。\r\n极限玩家:危险高度下获得更高收益,同时承担更高操作压力。\r\n赌徒契约:后续强化有概率翻倍或落空,每级提高概率,最多 4 级。"),
|
||||
@@ -981,20 +844,22 @@ void TDrawScreen(HDC hdc, HWND hWnd)
|
||||
else if (helpState.currentPage == 4)
|
||||
{
|
||||
const int creditAnimationTotalTicks = 60;
|
||||
constexpr int creditPageCount = 4;
|
||||
constexpr int creditPageCount = 5;
|
||||
const TCHAR* creditNames[creditPageCount] =
|
||||
{
|
||||
_T("qls"),
|
||||
_T("wyk"),
|
||||
_T("juju"),
|
||||
_T("qhy")
|
||||
_T("swj"),
|
||||
_T("qhy"),
|
||||
_T("syc")
|
||||
};
|
||||
const TCHAR* creditTexts[creditPageCount] =
|
||||
{
|
||||
_T("\u611f\u8c22\u6fc0\u60c5\u6295\u8eab\u4e8e\u6d4b\u8bd5\u4e4b\u4e2d\u7684Lisa"),
|
||||
_T("\u611f\u8c22\u70ed\u5ff1coding\u7684\u5c0f\u86cb\u7cd5"),
|
||||
_T("\u611f\u8c22\u8bfe\u524d\u95f2\u91cc\u5077\u5fd9\u7684juju"),
|
||||
_T("\u611f\u8c22qhy\u7684\u5929\u624d\u6784\u60f3")
|
||||
_T("\u611f\u8c22qhy\u7684\u5929\u624d\u6784\u60f3"),
|
||||
_T("\u611f\u8c22syc\u63fd\u4e0b\u6240\u6709\u6742\u6d3b")
|
||||
};
|
||||
|
||||
int currentCredit = creditPageIndex;
|
||||
@@ -2463,6 +2328,7 @@ void TDrawScreen(HDC hdc, HWND hWnd)
|
||||
DT_LEFT | DT_TOP | DT_WORDBREAK);
|
||||
}
|
||||
|
||||
// 暂停和结束覆盖层只盖住棋盘区域,让两侧战斗信息仍然可见。
|
||||
if (suspendFlag || gameOverFlag)
|
||||
{
|
||||
RECT overlayRect =
|
||||
@@ -2590,6 +2456,7 @@ void TDrawScreen(HDC hdc, HWND hWnd)
|
||||
}
|
||||
}
|
||||
|
||||
// 升级选择界面在当前战局上方绘制半透明遮罩,保留背景局势作为上下文。
|
||||
if (currentScreen == SCREEN_UPGRADE)
|
||||
{
|
||||
RECT dimRect =
|
||||
@@ -2855,3 +2722,4 @@ void TDrawScreen(HDC hdc, HWND hWnd)
|
||||
DeleteObject(bodyFont);
|
||||
DeleteObject(smallFont);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,969 @@
|
||||
#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'))
|
||||
{
|
||||
int direction = (key == 'J') ? 1 : -1;
|
||||
AdjustScrollOffset(upgradeListScrollOffset, direction * GetScrollStep(hWnd, 52));
|
||||
InvalidateRect(hWnd, nullptr, FALSE);
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (key)
|
||||
{
|
||||
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()
|
||||
{
|
||||
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,422 @@
|
||||
#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,242 @@
|
||||
#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()
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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,324 @@
|
||||
#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)
|
||||
{
|
||||
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;
|
||||
|
||||
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)
|
||||
{
|
||||
if (currentMode != MODE_ROGUE ||
|
||||
IsRogueSkillDemoMode() ||
|
||||
rogueStats.extremePlayerLevel <= 0 ||
|
||||
currentScreen != SCREEN_PLAYING ||
|
||||
suspendFlag ||
|
||||
gameOverFlag)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (rogueStats.extremeDangerTicks > 0)
|
||||
{
|
||||
rogueStats.extremeDangerTicks--;
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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,79 @@
|
||||
#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)
|
||||
{
|
||||
DWORD attributes = GetFileAttributesW(path.c_str());
|
||||
return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) == 0;
|
||||
}
|
||||
+61
-2
@@ -1,4 +1,9 @@
|
||||
#include "stdafx.h"
|
||||
/**
|
||||
* @file TetrisGameExtensions.cpp
|
||||
* @brief 实现玩家统计、视觉特效、模式切换、复活和帮助/致谢页面状态管理。
|
||||
*/
|
||||
|
||||
#include "TetrisLogicInternal.h"
|
||||
|
||||
int pendingLineClearEffectTicks = 0;
|
||||
@@ -8,6 +13,8 @@ int pendingLineClearEffectLineCount = 0;
|
||||
|
||||
/**
|
||||
* @brief 重置经典或 Rogue 模式使用的玩家统计数据。
|
||||
* @param stats 需要重置的统计结构。
|
||||
* @param useRogueRules 是否按 Rogue 模式设置初始经验需求。
|
||||
*/
|
||||
void ResetPlayerStats(PlayerStats& stats, bool useRogueRules)
|
||||
{
|
||||
@@ -85,6 +92,9 @@ void ResetPlayerStats(PlayerStats& stats, bool useRogueRules)
|
||||
|
||||
/**
|
||||
* @brief 设置界面右侧显示的即时反馈标题、内容和持续时间。
|
||||
* @param title 反馈标题。
|
||||
* @param detail 反馈详情。
|
||||
* @param ticks 显示持续的游戏计时次数。
|
||||
*/
|
||||
void SetFeedbackMessage(const TCHAR* title, const TCHAR* detail, int ticks)
|
||||
{
|
||||
@@ -125,6 +135,7 @@ void ResetVisualEffects()
|
||||
|
||||
/**
|
||||
* @brief 推进视觉效果计时,并返回是否仍有动画需要刷新。
|
||||
* @return 仍有动画需要刷新返回 true,否则返回 false。
|
||||
*/
|
||||
bool TickVisualEffects()
|
||||
{
|
||||
@@ -177,6 +188,7 @@ bool TickVisualEffects()
|
||||
|
||||
/**
|
||||
* @brief 推进致谢页左右切换动画,并返回是否需要刷新界面。
|
||||
* @return 需要刷新界面返回 true,否则返回 false。
|
||||
*/
|
||||
bool TickCreditAnimation()
|
||||
{
|
||||
@@ -191,6 +203,10 @@ bool TickCreditAnimation()
|
||||
|
||||
/**
|
||||
* @brief 添加一段棋盘坐标系中的浮动文字效果。
|
||||
* @param boardX 棋盘内部横坐标,使用 100 为一格的坐标系。
|
||||
* @param boardY 棋盘内部纵坐标,使用 100 为一格的坐标系。
|
||||
* @param text 浮动文字内容。
|
||||
* @param color 文字颜色。
|
||||
*/
|
||||
static void AddFloatingText(int boardX, int boardY, const TCHAR* text, COLORREF color)
|
||||
{
|
||||
@@ -211,6 +227,12 @@ static void AddFloatingText(int boardX, int boardY, const TCHAR* text, COLORREF
|
||||
|
||||
/**
|
||||
* @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)
|
||||
{
|
||||
@@ -233,6 +255,10 @@ static void AddParticle(int boardX, int boardY, int velocityX, int velocityY, in
|
||||
|
||||
/**
|
||||
* @brief 在指定棋盘坐标周围生成一组爆裂粒子。
|
||||
* @param boardX 爆裂中心横坐标。
|
||||
* @param boardY 爆裂中心纵坐标。
|
||||
* @param baseColor 主粒子颜色。
|
||||
* @param strongBurst 是否使用更强的粒子数量和速度。
|
||||
*/
|
||||
static void AddBurstParticles(int boardX, int boardY, COLORREF baseColor, bool strongBurst)
|
||||
{
|
||||
@@ -296,6 +322,10 @@ static void AddBurstParticles(int boardX, int boardY, COLORREF baseColor, bool s
|
||||
|
||||
/**
|
||||
* @brief 添加一个被清除格子的短时高亮效果。
|
||||
* @param x 棋盘列号。
|
||||
* @param y 棋盘行号。
|
||||
* @param color 高亮颜色。
|
||||
* @param strongFlash 是否使用更长的强高亮。
|
||||
*/
|
||||
static void AddCellFlash(int x, int y, COLORREF color, bool strongFlash)
|
||||
{
|
||||
@@ -315,6 +345,9 @@ static void AddCellFlash(int x, int y, COLORREF color, bool strongFlash)
|
||||
|
||||
/**
|
||||
* @brief 暂存消行动画,等待升级选择结束后再播放。
|
||||
* @param rows 被消除的行号数组。
|
||||
* @param rowCount 行号数量。
|
||||
* @param linesCleared 实际消除行数。
|
||||
*/
|
||||
void QueueLineClearEffect(const int* rows, int rowCount, int linesCleared)
|
||||
{
|
||||
@@ -358,6 +391,9 @@ void PlayPendingLineClearEffect()
|
||||
|
||||
/**
|
||||
* @brief 触发标准消行动画和浮动文字。
|
||||
* @param rows 被消除的行号数组。
|
||||
* @param rowCount 行号数量。
|
||||
* @param linesCleared 实际消除行数。
|
||||
*/
|
||||
void TriggerLineClearEffect(const int* rows, int rowCount, int linesCleared)
|
||||
{
|
||||
@@ -414,6 +450,9 @@ void TriggerLineClearEffect(const int* rows, int rowCount, int linesCleared)
|
||||
|
||||
/**
|
||||
* @brief 为指定棋盘格集合触发清除粒子效果。
|
||||
* @param cells 被清除格子数组。
|
||||
* @param cellCount 格子数量。
|
||||
* @param strongBurst 是否使用更强的粒子爆裂效果。
|
||||
*/
|
||||
void TriggerCellClearEffect(const Point* cells, int cellCount, bool strongBurst)
|
||||
{
|
||||
@@ -422,6 +461,10 @@ 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)
|
||||
{
|
||||
@@ -445,6 +488,10 @@ void TriggerColoredCellClearEffect(const Point* cells, int cellCount, COLORREF f
|
||||
|
||||
/**
|
||||
* @brief 为一个受重力下落的固定方块记录纵向残影和落点粒子。
|
||||
* @param x 棋盘列号。
|
||||
* @param fromY 起始行号。
|
||||
* @param toY 目标行号。
|
||||
* @param cellValue 方块格子值。
|
||||
*/
|
||||
void TriggerGravityFallEffect(int x, int fromY, int toY, int cellValue)
|
||||
{
|
||||
@@ -490,6 +537,10 @@ void TriggerGravityFallEffect(int x, int fromY, int toY, int cellValue)
|
||||
|
||||
/**
|
||||
* @brief 判断指定方块、旋转状态和位置是否可以合法放置。
|
||||
* @param pieceType 方块类型编号。
|
||||
* @param pieceState 方块旋转状态。
|
||||
* @param position 待检测的左上角坐标。
|
||||
* @return 可以放置返回 true,否则返回 false。
|
||||
*/
|
||||
bool IsPiecePlacementValid(int pieceType, int pieceState, Point position)
|
||||
{
|
||||
@@ -522,6 +573,9 @@ bool IsPiecePlacementValid(int pieceType, int pieceState, Point position)
|
||||
|
||||
/**
|
||||
* @brief 尝试把旋转后的方块横向偏移指定格数后放置。
|
||||
* @param nextState 旋转后的状态编号。
|
||||
* @param offsetX 横向试探偏移。
|
||||
* @return 偏移后可以放置返回 true,否则返回 false。
|
||||
*/
|
||||
bool TryRotateWithOffset(int nextState, int offsetX)
|
||||
{
|
||||
@@ -574,6 +628,7 @@ void ReviveAfterVideo()
|
||||
|
||||
/**
|
||||
* @brief 按指定模式开始新游戏。
|
||||
* @param mode 游戏模式,取值来自 GameMode。
|
||||
*/
|
||||
void StartGameWithMode(int mode)
|
||||
{
|
||||
@@ -632,7 +687,7 @@ void OpenRulesScreen()
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 打开致谢界面并重置致谢页切换状态。
|
||||
* @brief 打开 Rogue 技能演示选择页并重置帮助页状态。
|
||||
*/
|
||||
void OpenSkillDemoScreen()
|
||||
{
|
||||
@@ -648,6 +703,9 @@ void OpenSkillDemoScreen()
|
||||
creditAnimationDirection = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 打开致谢界面并重置致谢页切换状态。
|
||||
*/
|
||||
void OpenCreditScreen()
|
||||
{
|
||||
rogueDemoMode = false;
|
||||
@@ -664,10 +722,11 @@ void OpenCreditScreen()
|
||||
|
||||
/**
|
||||
* @brief 切换致谢页图片,并启动左右滑动动画。
|
||||
* @param direction 小于 0 向前切换,大于 0 向后切换。
|
||||
*/
|
||||
void ChangeCreditPage(int direction)
|
||||
{
|
||||
constexpr int creditPageCount = 4;
|
||||
constexpr int creditPageCount = 5;
|
||||
if (direction == 0)
|
||||
{
|
||||
return;
|
||||
@@ -0,0 +1,255 @@
|
||||
#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)
|
||||
{
|
||||
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,153 @@
|
||||
#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)
|
||||
{
|
||||
if (path.empty() || !FileExists(path))
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
@@ -1,4 +1,9 @@
|
||||
#include "stdafx.h"
|
||||
/**
|
||||
* @file TetrisRogue.cpp
|
||||
* @brief 实现 Rogue 模式的强化池、等级成长、特殊方块、主动技能、难度和技能演示。
|
||||
*/
|
||||
|
||||
#include "TetrisLogicInternal.h"
|
||||
|
||||
/**
|
||||
@@ -73,7 +78,7 @@ static const UpgradeEntry kUpgradePool[] =
|
||||
{ UPGRADE_FEVER_MODE, 1, 92, false, _T("狂热节拍"), _T("进阶"), _T("累计消行 12 行后进入 12 秒狂热:得分与 EXP 翻倍。") },
|
||||
{ UPGRADE_RAGE_STACK, 1, 84, false, _T("怒火连段"), _T("进阶"), _T("连续消行越多,得分倍率追加越高。") },
|
||||
{ UPGRADE_INFINITE_FEVER, 1, 110, false, _T("无尽狂热"), _T("进化"), _T("狂热期间消行可延长狂热时间;连击越高,倍率越凶。") },
|
||||
{ UPGRADE_SCREEN_BOMB, -1, 90, true, _T("清屏炸弹"), _T("进阶"), _T("立刻获得 1 枚炸弹;可重复选择补充数量,之后累计消行 16 行再获得 1 枚。按 X 清底 5 行。") },
|
||||
{ UPGRADE_SCREEN_BOMB, 1, 90, false, _T("清屏炸弹"), _T("进阶"), _T("解锁清屏炸弹;之后累计消行 16 行获得 1 枚。按 X 清底 5 行。") },
|
||||
{ UPGRADE_TERMINAL_CLEAR, 1, 108, false, _T("终末清场"), _T("进化"), _T("最后一搏启动时,自动引爆 1 枚清屏炸弹,并进入 10 秒狂热。") },
|
||||
{ UPGRADE_DUAL_CHOICE, 1, 68, false, _T("双重抉择"), _T("进阶"), _T("每次升级可额外选择 1 个强化,但下一次升级所需 EXP +30%。") },
|
||||
{ UPGRADE_DESTINY_WHEEL, 1, 104, false, _T("命运轮盘"), _T("进化"), _T("升级时出现 6 个选项,可选择 2 个;其中 1 个会携带诅咒。") },
|
||||
@@ -576,11 +581,6 @@ static int GetUpgradeDynamicWeight(const UpgradeEntry& entry)
|
||||
weight -= 20;
|
||||
}
|
||||
|
||||
if (entry.id == UPGRADE_SCREEN_BOMB && rogueStats.screenBombCount > 0)
|
||||
{
|
||||
weight += 20;
|
||||
}
|
||||
|
||||
if (entry.id == UPGRADE_BLACK_HOLE && rogueStats.blackHoleCharges > 0)
|
||||
{
|
||||
weight += 20;
|
||||
@@ -1513,6 +1513,8 @@ static int GetRogueExpByLines(int linesCleared)
|
||||
|
||||
/**
|
||||
* @brief 结算经验条并返回本次连续升级次数。
|
||||
* @param stats 需要结算经验的玩家统计。
|
||||
* @return 本次触发的升级次数。
|
||||
*/
|
||||
static int ApplyLevelProgress(PlayerStats& stats)
|
||||
{
|
||||
@@ -1531,6 +1533,8 @@ static int ApplyLevelProgress(PlayerStats& stats)
|
||||
|
||||
/**
|
||||
* @brief 触发升级冲击波,清除底部指定数量的行。
|
||||
* @param rowsToClear 需要清除的底部行数。
|
||||
* @return 实际清除的行数。
|
||||
*/
|
||||
static int TriggerUpgradeShockwave(int rowsToClear)
|
||||
{
|
||||
@@ -1546,12 +1550,13 @@ static int TriggerUpgradeShockwave(int rowsToClear)
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 按权重和当前局势生成升级菜单中的候选强化。
|
||||
* @brief 收集当前可进入升级池的强化下标和动态权重。
|
||||
* @param selectableIndexes 返回候选在强化池中的下标。
|
||||
* @param selectableWeights 返回候选动态权重。
|
||||
* @return 候选数量。
|
||||
*/
|
||||
static void FillUpgradeOptions()
|
||||
static int CollectSelectableUpgrades(int selectableIndexes[], int selectableWeights[])
|
||||
{
|
||||
int selectableIndexes[kUpgradePoolSize] = { 0 };
|
||||
int selectableWeights[kUpgradePoolSize] = { 0 };
|
||||
int selectableCount = 0;
|
||||
|
||||
for (int i = 0; i < kUpgradePoolSize; i++)
|
||||
@@ -1564,6 +1569,16 @@ static void FillUpgradeOptions()
|
||||
}
|
||||
}
|
||||
|
||||
return selectableCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 初始化本轮升级界面的数量、选中项和多选状态。
|
||||
* @param selectableCount 当前可选候选数量。
|
||||
* @return 本轮实际展示的选项数量。
|
||||
*/
|
||||
static int PrepareUpgradeOptionState(int selectableCount)
|
||||
{
|
||||
int optionLimit = (rogueStats.destinyWheelLevel > 0) ? 6 : 3;
|
||||
int optionCount = selectableCount < optionLimit ? selectableCount : optionLimit;
|
||||
upgradeUiState.optionCount = optionCount;
|
||||
@@ -1575,63 +1590,81 @@ static void FillUpgradeOptions()
|
||||
upgradeUiState.marked[i] = false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < optionCount; i++)
|
||||
return optionCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 按权重从候选数组中抽取一个槽位。
|
||||
* @param selectableWeights 候选权重数组。
|
||||
* @param selectableCount 候选数量。
|
||||
* @return 被抽中的候选槽位。
|
||||
*/
|
||||
static int PickUpgradeSlotByWeight(const int selectableWeights[], int selectableCount)
|
||||
{
|
||||
int totalWeight = 0;
|
||||
for (int weightIndex = 0; weightIndex < selectableCount; weightIndex++)
|
||||
{
|
||||
int totalWeight = 0;
|
||||
for (int weightIndex = 0; weightIndex < selectableCount; weightIndex++)
|
||||
{
|
||||
totalWeight += selectableWeights[weightIndex];
|
||||
}
|
||||
|
||||
int pickSlot = 0;
|
||||
if (totalWeight > 0)
|
||||
{
|
||||
int roll = rand() % totalWeight;
|
||||
int accumulatedWeight = 0;
|
||||
|
||||
for (int weightIndex = 0; weightIndex < selectableCount; weightIndex++)
|
||||
{
|
||||
accumulatedWeight += selectableWeights[weightIndex];
|
||||
if (roll < accumulatedWeight)
|
||||
{
|
||||
pickSlot = weightIndex;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int pickedIndex = selectableIndexes[pickSlot];
|
||||
const UpgradeEntry& pickedEntry = kUpgradePool[pickedIndex];
|
||||
|
||||
upgradeUiState.options[i].id = pickedEntry.id;
|
||||
upgradeUiState.options[i].currentLevel = GetUpgradeCurrentLevel(pickedEntry.id);
|
||||
upgradeUiState.options[i].targetPieceType = -1;
|
||||
upgradeUiState.options[i].rarity = GetUpgradeBaseRarity(pickedEntry.id);
|
||||
upgradeUiState.options[i].cursed = false;
|
||||
upgradeUiState.options[i].name = pickedEntry.name;
|
||||
upgradeUiState.options[i].category = pickedEntry.category;
|
||||
upgradeUiState.options[i].description = pickedEntry.description;
|
||||
|
||||
if (pickedEntry.id == UPGRADE_PIECE_TUNING)
|
||||
{
|
||||
int targetPieceType = 0;
|
||||
upgradeUiState.options[i].targetPieceType = targetPieceType;
|
||||
upgradeUiState.options[i].currentLevel = rogueStats.pieceTuningLevels[0];
|
||||
upgradeUiState.options[i].name = _T("方块改造");
|
||||
|
||||
static TCHAR tuningDescriptions[6][64];
|
||||
_stprintf_s(
|
||||
tuningDescriptions[i],
|
||||
_T("%s 块的生成概率提高。"),
|
||||
GetPieceShortName(0));
|
||||
upgradeUiState.options[i].description = tuningDescriptions[i];
|
||||
}
|
||||
|
||||
selectableIndexes[pickSlot] = selectableIndexes[selectableCount - 1];
|
||||
selectableWeights[pickSlot] = selectableWeights[selectableCount - 1];
|
||||
selectableCount--;
|
||||
totalWeight += selectableWeights[weightIndex];
|
||||
}
|
||||
|
||||
int pickSlot = 0;
|
||||
if (totalWeight > 0)
|
||||
{
|
||||
int roll = rand() % totalWeight;
|
||||
int accumulatedWeight = 0;
|
||||
|
||||
for (int weightIndex = 0; weightIndex < selectableCount; weightIndex++)
|
||||
{
|
||||
accumulatedWeight += selectableWeights[weightIndex];
|
||||
if (roll < accumulatedWeight)
|
||||
{
|
||||
pickSlot = weightIndex;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pickSlot;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 将抽中的强化池条目写入升级 UI 选项。
|
||||
* @param optionIndex UI 选项下标。
|
||||
* @param pickedEntry 被抽中的强化池条目。
|
||||
*/
|
||||
static void FillUpgradeOptionFromEntry(int optionIndex, const UpgradeEntry& pickedEntry)
|
||||
{
|
||||
upgradeUiState.options[optionIndex].id = pickedEntry.id;
|
||||
upgradeUiState.options[optionIndex].currentLevel = GetUpgradeCurrentLevel(pickedEntry.id);
|
||||
upgradeUiState.options[optionIndex].targetPieceType = -1;
|
||||
upgradeUiState.options[optionIndex].rarity = GetUpgradeBaseRarity(pickedEntry.id);
|
||||
upgradeUiState.options[optionIndex].cursed = false;
|
||||
upgradeUiState.options[optionIndex].name = pickedEntry.name;
|
||||
upgradeUiState.options[optionIndex].category = pickedEntry.category;
|
||||
upgradeUiState.options[optionIndex].description = pickedEntry.description;
|
||||
|
||||
if (pickedEntry.id == UPGRADE_PIECE_TUNING)
|
||||
{
|
||||
int targetPieceType = 0;
|
||||
upgradeUiState.options[optionIndex].targetPieceType = targetPieceType;
|
||||
upgradeUiState.options[optionIndex].currentLevel = rogueStats.pieceTuningLevels[0];
|
||||
upgradeUiState.options[optionIndex].name = _T("方块改造");
|
||||
|
||||
static TCHAR tuningDescriptions[6][64];
|
||||
_stprintf_s(
|
||||
tuningDescriptions[optionIndex],
|
||||
_T("%s 块的生成概率提高。"),
|
||||
GetPieceShortName(0));
|
||||
upgradeUiState.options[optionIndex].description = tuningDescriptions[optionIndex];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 命运轮盘生效时,为本轮候选随机附加一个诅咒标记。
|
||||
* @param optionCount 本轮展示选项数量。
|
||||
*/
|
||||
static void MarkDestinyCursedOption(int optionCount)
|
||||
{
|
||||
if (rogueStats.destinyWheelLevel > 0 && optionCount > 0)
|
||||
{
|
||||
int cursedIndex = rand() % optionCount;
|
||||
@@ -1639,8 +1672,37 @@ static void FillUpgradeOptions()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 按权重和当前局势生成升级菜单中的候选强化。
|
||||
*
|
||||
* 该函数只负责生成 UI 选项,不应用强化效果。候选池会先过滤前置、
|
||||
* 等级上限和互斥条件,再按动态权重不放回抽取,保证同一轮不会重复出现。
|
||||
*/
|
||||
static void FillUpgradeOptions()
|
||||
{
|
||||
int selectableIndexes[kUpgradePoolSize] = { 0 };
|
||||
int selectableWeights[kUpgradePoolSize] = { 0 };
|
||||
int selectableCount = CollectSelectableUpgrades(selectableIndexes, selectableWeights);
|
||||
int optionCount = PrepareUpgradeOptionState(selectableCount);
|
||||
|
||||
// 第二段:按权重不放回抽取候选,抽中后用末尾元素覆盖当前槽位。
|
||||
for (int i = 0; i < optionCount; i++)
|
||||
{
|
||||
int pickSlot = PickUpgradeSlotByWeight(selectableWeights, selectableCount);
|
||||
int pickedIndex = selectableIndexes[pickSlot];
|
||||
FillUpgradeOptionFromEntry(i, kUpgradePool[pickedIndex]);
|
||||
|
||||
selectableIndexes[pickSlot] = selectableIndexes[selectableCount - 1];
|
||||
selectableWeights[pickSlot] = selectableWeights[selectableCount - 1];
|
||||
selectableCount--;
|
||||
}
|
||||
|
||||
MarkDestinyCursedOption(optionCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 综合难度、强化和临时状态计算 Rogue 模式下落间隔。
|
||||
* @return 当前应使用的下落计时器间隔,单位毫秒。
|
||||
*/
|
||||
int GetRogueFallInterval()
|
||||
{
|
||||
@@ -1691,28 +1753,26 @@ int GetRogueFallInterval()
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 根据强化编号把对应效果写入 Rogue 属性。
|
||||
* @brief 应用成长、保命和基础操作类强化。
|
||||
* @param upgradeId 强化编号。
|
||||
* @param applyCount 本次应用次数。
|
||||
* @return 当前强化已处理返回 true。
|
||||
*/
|
||||
static void ApplyUpgradeById(int upgradeId, int targetPieceType, int applyCount)
|
||||
static bool ApplyGrowthOrOperationUpgrade(int upgradeId, int applyCount)
|
||||
{
|
||||
if (applyCount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switch (upgradeId)
|
||||
{
|
||||
case UPGRADE_SCORE_MULTIPLIER:
|
||||
rogueStats.scoreMultiplierPercent += 20 * applyCount;
|
||||
rogueStats.scoreUpgradeLevel += applyCount;
|
||||
break;
|
||||
return true;
|
||||
case UPGRADE_COMBO_BONUS:
|
||||
rogueStats.comboBonusStacks += applyCount;
|
||||
break;
|
||||
return true;
|
||||
case UPGRADE_EXP_MULTIPLIER:
|
||||
rogueStats.expMultiplierPercent += 25 * applyCount;
|
||||
rogueStats.expUpgradeLevel += applyCount;
|
||||
break;
|
||||
return true;
|
||||
case UPGRADE_SLOW_FALL:
|
||||
rogueStats.slowFallStacks += applyCount;
|
||||
if (rogueStats.slowFallStacks > 4)
|
||||
@@ -1720,24 +1780,58 @@ static void ApplyUpgradeById(int upgradeId, int targetPieceType, int applyCount)
|
||||
rogueStats.slowFallStacks = 4;
|
||||
}
|
||||
currentFallInterval = GetRogueFallInterval();
|
||||
break;
|
||||
return true;
|
||||
case UPGRADE_PREVIEW_PLUS_ONE:
|
||||
if (rogueStats.previewCount < 2)
|
||||
{
|
||||
rogueStats.previewCount = 2;
|
||||
}
|
||||
rogueStats.previewUpgradeLevel = rogueStats.previewCount - 1;
|
||||
break;
|
||||
return true;
|
||||
case UPGRADE_LAST_CHANCE:
|
||||
rogueStats.lastChanceCount += applyCount;
|
||||
rogueStats.lastChanceUpgradeLevel += applyCount;
|
||||
break;
|
||||
return true;
|
||||
case UPGRADE_HOLD_UNLOCK:
|
||||
rogueStats.holdUnlocked = 1;
|
||||
holdUsedThisTurn = false;
|
||||
break;
|
||||
case UPGRADE_PRESSURE_RELIEF:
|
||||
return true;
|
||||
case UPGRADE_PERFECT_ROTATE:
|
||||
rogueStats.perfectRotateLevel = 1;
|
||||
return true;
|
||||
case UPGRADE_TIME_DILATION:
|
||||
rogueStats.timeDilationLevel = 1;
|
||||
currentFallInterval = GetRogueFallInterval();
|
||||
return true;
|
||||
case UPGRADE_CONTROL_MASTER:
|
||||
rogueStats.controlMasterLevel = 1;
|
||||
if (rogueStats.previewCount < 3)
|
||||
{
|
||||
rogueStats.previewCount++;
|
||||
}
|
||||
rogueStats.previewUpgradeLevel = rogueStats.previewCount - 1;
|
||||
return true;
|
||||
case UPGRADE_DOUBLE_GROWTH:
|
||||
rogueStats.doubleGrowthLevel = 1;
|
||||
rogueStats.scoreMultiplierPercent += 15;
|
||||
rogueStats.expMultiplierPercent += 15;
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 应用特殊方块和落地派生类强化。
|
||||
* @param upgradeId 强化编号。
|
||||
* @param applyCount 本次应用次数。
|
||||
* @return 当前强化已处理返回 true。
|
||||
*/
|
||||
static bool ApplySpecialPieceUpgrade(int upgradeId, int applyCount)
|
||||
{
|
||||
switch (upgradeId)
|
||||
{
|
||||
case UPGRADE_PRESSURE_RELIEF:
|
||||
rogueStats.pressureReliefLevel += applyCount;
|
||||
for (int i = 0; i < applyCount; i++)
|
||||
{
|
||||
@@ -1747,49 +1841,107 @@ static void ApplyUpgradeById(int upgradeId, int targetPieceType, int applyCount)
|
||||
DeleteOneLine(topOccupiedRow);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
case UPGRADE_SWEEPER:
|
||||
rogueStats.sweeperLevel += applyCount;
|
||||
if (rogueStats.sweeperLevel > 4)
|
||||
{
|
||||
rogueStats.sweeperLevel = 4;
|
||||
}
|
||||
break;
|
||||
return true;
|
||||
case UPGRADE_EXPLOSIVE_PIECE:
|
||||
rogueStats.explosiveLevel = 1;
|
||||
break;
|
||||
return true;
|
||||
case UPGRADE_CHAIN_BLAST:
|
||||
rogueStats.chainBlastLevel = 1;
|
||||
break;
|
||||
return true;
|
||||
case UPGRADE_CHAIN_BOMB:
|
||||
rogueStats.chainBombLevel = 1;
|
||||
break;
|
||||
return true;
|
||||
case UPGRADE_LASER_PIECE:
|
||||
rogueStats.laserLevel = 1;
|
||||
break;
|
||||
return true;
|
||||
case UPGRADE_THUNDER_TETRIS:
|
||||
rogueStats.thunderTetrisLevel = 1;
|
||||
break;
|
||||
return true;
|
||||
case UPGRADE_THUNDER_LASER:
|
||||
rogueStats.thunderLaserLevel = 1;
|
||||
break;
|
||||
return true;
|
||||
case UPGRADE_CROSS_PIECE:
|
||||
rogueStats.crossPieceLevel = 1;
|
||||
return true;
|
||||
case UPGRADE_RAINBOW_PIECE:
|
||||
rogueStats.rainbowPieceLevel = 1;
|
||||
return true;
|
||||
case UPGRADE_STABLE_STRUCTURE:
|
||||
rogueStats.stableStructureLevel += applyCount;
|
||||
return true;
|
||||
case UPGRADE_PIECE_TUNING:
|
||||
rogueStats.pieceTuningLevels[0] += applyCount;
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 应用主动技能、爆发和资源次数类强化。
|
||||
* @param upgradeId 强化编号。
|
||||
* @param applyCount 本次应用次数。
|
||||
* @return 当前强化已处理返回 true。
|
||||
*/
|
||||
static bool ApplyActiveSkillUpgrade(int upgradeId, int applyCount)
|
||||
{
|
||||
switch (upgradeId)
|
||||
{
|
||||
case UPGRADE_FEVER_MODE:
|
||||
rogueStats.feverLevel = 1;
|
||||
break;
|
||||
return true;
|
||||
case UPGRADE_RAGE_STACK:
|
||||
rogueStats.rageStackLevel = 1;
|
||||
break;
|
||||
return true;
|
||||
case UPGRADE_INFINITE_FEVER:
|
||||
rogueStats.infiniteFeverLevel = 1;
|
||||
break;
|
||||
return true;
|
||||
case UPGRADE_SCREEN_BOMB:
|
||||
rogueStats.screenBombLevel += applyCount;
|
||||
rogueStats.screenBombCount += applyCount;
|
||||
break;
|
||||
rogueStats.screenBombLevel = 1;
|
||||
return true;
|
||||
case UPGRADE_TERMINAL_CLEAR:
|
||||
rogueStats.terminalClearLevel = 1;
|
||||
break;
|
||||
return true;
|
||||
case UPGRADE_BLOCK_STORM:
|
||||
rogueStats.blockStormLevel = 1;
|
||||
rogueStats.blockStormPiecesRemaining = 5;
|
||||
nextTypes[0] = 0;
|
||||
nextTypes[1] = 0;
|
||||
nextTypes[2] = 0;
|
||||
return true;
|
||||
case UPGRADE_BLACK_HOLE:
|
||||
rogueStats.blackHoleLevel = 1;
|
||||
rogueStats.blackHoleCharges += 2 * applyCount;
|
||||
return true;
|
||||
case UPGRADE_AIR_RESHAPE:
|
||||
rogueStats.reshapeLevel += applyCount;
|
||||
rogueStats.reshapeCharges += 2 * applyCount;
|
||||
return true;
|
||||
case UPGRADE_VOID_CORE:
|
||||
rogueStats.voidCoreLevel = 1;
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 应用风险、升级规则和进化类强化。
|
||||
* @param upgradeId 强化编号。
|
||||
* @param applyCount 本次应用次数。
|
||||
* @return 当前强化已处理返回 true。
|
||||
*/
|
||||
static bool ApplyRiskOrEvolutionUpgrade(int upgradeId, int applyCount)
|
||||
{
|
||||
switch (upgradeId)
|
||||
{
|
||||
case UPGRADE_DUAL_CHOICE:
|
||||
rogueStats.dualChoiceLevel = 1;
|
||||
rogueStats.requiredExp = rogueStats.requiredExp * 130 / 100;
|
||||
@@ -1797,95 +1949,74 @@ static void ApplyUpgradeById(int upgradeId, int targetPieceType, int applyCount)
|
||||
{
|
||||
rogueStats.requiredExp = 10;
|
||||
}
|
||||
break;
|
||||
return true;
|
||||
case UPGRADE_DESTINY_WHEEL:
|
||||
rogueStats.destinyWheelLevel = 1;
|
||||
break;
|
||||
case UPGRADE_PERFECT_ROTATE:
|
||||
rogueStats.perfectRotateLevel = 1;
|
||||
break;
|
||||
case UPGRADE_TIME_DILATION:
|
||||
rogueStats.timeDilationLevel = 1;
|
||||
currentFallInterval = GetRogueFallInterval();
|
||||
break;
|
||||
return true;
|
||||
case UPGRADE_HIGH_PRESSURE:
|
||||
rogueStats.highPressureLevel = 1;
|
||||
rogueStats.scoreMultiplierPercent += 50;
|
||||
rogueStats.expMultiplierPercent += 50;
|
||||
currentFallInterval = GetRogueFallInterval();
|
||||
break;
|
||||
return true;
|
||||
case UPGRADE_TETRIS_GAMBLE:
|
||||
rogueStats.tetrisGambleLevel = 1;
|
||||
break;
|
||||
return true;
|
||||
case UPGRADE_EXTREME_PLAYER:
|
||||
rogueStats.extremePlayerLevel = 1;
|
||||
rogueStats.extremeDangerTicks = 30;
|
||||
rogueStats.extremeDangerLevel = 0;
|
||||
currentFallInterval = GetRogueFallInterval();
|
||||
break;
|
||||
return true;
|
||||
case UPGRADE_UPGRADE_SHOCKWAVE:
|
||||
rogueStats.upgradeShockwaveLevel = 1;
|
||||
break;
|
||||
return true;
|
||||
case UPGRADE_EVOLUTION_IMPACT:
|
||||
rogueStats.evolutionImpactLevel = 1;
|
||||
break;
|
||||
case UPGRADE_CONTROL_MASTER:
|
||||
rogueStats.controlMasterLevel = 1;
|
||||
if (rogueStats.previewCount < 3)
|
||||
{
|
||||
rogueStats.previewCount++;
|
||||
}
|
||||
rogueStats.previewUpgradeLevel = rogueStats.previewCount - 1;
|
||||
break;
|
||||
case UPGRADE_BLOCK_STORM:
|
||||
rogueStats.blockStormLevel = 1;
|
||||
rogueStats.blockStormPiecesRemaining = 5;
|
||||
nextTypes[0] = 0;
|
||||
nextTypes[1] = 0;
|
||||
nextTypes[2] = 0;
|
||||
break;
|
||||
case UPGRADE_CROSS_PIECE:
|
||||
rogueStats.crossPieceLevel = 1;
|
||||
break;
|
||||
case UPGRADE_BLACK_HOLE:
|
||||
rogueStats.blackHoleLevel = 1;
|
||||
rogueStats.blackHoleCharges += 2 * applyCount;
|
||||
break;
|
||||
case UPGRADE_AIR_RESHAPE:
|
||||
rogueStats.reshapeLevel += applyCount;
|
||||
rogueStats.reshapeCharges += 2 * applyCount;
|
||||
break;
|
||||
case UPGRADE_RAINBOW_PIECE:
|
||||
rogueStats.rainbowPieceLevel = 1;
|
||||
break;
|
||||
case UPGRADE_VOID_CORE:
|
||||
rogueStats.voidCoreLevel = 1;
|
||||
break;
|
||||
case UPGRADE_STABLE_STRUCTURE:
|
||||
rogueStats.stableStructureLevel += applyCount;
|
||||
break;
|
||||
case UPGRADE_DOUBLE_GROWTH:
|
||||
rogueStats.doubleGrowthLevel = 1;
|
||||
rogueStats.scoreMultiplierPercent += 15;
|
||||
rogueStats.expMultiplierPercent += 15;
|
||||
break;
|
||||
case UPGRADE_PIECE_TUNING:
|
||||
rogueStats.pieceTuningLevels[0] += applyCount;
|
||||
break;
|
||||
return true;
|
||||
case UPGRADE_GAMBLER:
|
||||
rogueStats.gamblerLevel += applyCount;
|
||||
if (rogueStats.gamblerLevel > 4)
|
||||
{
|
||||
rogueStats.gamblerLevel = 4;
|
||||
}
|
||||
break;
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 根据强化编号把对应效果写入 Rogue 属性。
|
||||
*
|
||||
* 该函数集中修改 Rogue 属性和少量即时棋盘效果。调用者已经完成赌徒契约、
|
||||
* 多选确认和诅咒处理,因此这里保持强化效果本身的顺序和数值不变。
|
||||
*
|
||||
* @param upgradeId 强化编号。
|
||||
* @param targetPieceType 方块改造目标方块类型;当前实现固定使用 I 块。
|
||||
* @param applyCount 本次效果应用次数,赌徒落空时可能为 0。
|
||||
*/
|
||||
static void ApplyUpgradeById(int upgradeId, int targetPieceType, int applyCount)
|
||||
{
|
||||
(void)targetPieceType;
|
||||
|
||||
if (applyCount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyGrowthOrOperationUpgrade(upgradeId, applyCount) ||
|
||||
ApplySpecialPieceUpgrade(upgradeId, applyCount) ||
|
||||
ApplyActiveSkillUpgrade(upgradeId, applyCount) ||
|
||||
ApplyRiskOrEvolutionUpgrade(upgradeId, applyCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 为技能清除的格子结算得分和经验奖励。
|
||||
* @param clearedCells 清除格子数。
|
||||
* @param scoreGain 返回本次得分增量。
|
||||
* @param expGain 返回本次经验增量。
|
||||
* @param allowLevelProgress 是否允许本次奖励触发升级流程。
|
||||
*/
|
||||
void AwardRogueSkillClearRewards(int clearedCells, int& scoreGain, int& expGain, bool allowLevelProgress)
|
||||
{
|
||||
@@ -1925,16 +2056,6 @@ void AwardRogueSkillClearRewards(int clearedCells, int& scoreGain, int& expGain,
|
||||
rogueStats.exp += expGain;
|
||||
tScore = rogueStats.score;
|
||||
|
||||
if (rogueStats.screenBombLevel > 0)
|
||||
{
|
||||
rogueStats.screenBombCharge += clearedCells;
|
||||
while (rogueStats.screenBombCharge >= kScreenBombLineThreshold)
|
||||
{
|
||||
rogueStats.screenBombCharge -= kScreenBombLineThreshold;
|
||||
rogueStats.screenBombCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (allowLevelProgress && !rogueDemoMode)
|
||||
{
|
||||
int levelUps = ApplyLevelProgress(rogueStats);
|
||||
@@ -1968,6 +2089,7 @@ void CheckRogueLevelProgress()
|
||||
|
||||
upgradeUiState.pendingCount += levelUps;
|
||||
|
||||
// 升级冲击波和进化冲击延后到升级菜单关闭后播放,避免菜单遮挡反馈。
|
||||
int shockwaveRows = 0;
|
||||
if (rogueStats.evolutionImpactLevel > 0)
|
||||
{
|
||||
@@ -2033,6 +2155,7 @@ void ApplyBoardGravity()
|
||||
|
||||
/**
|
||||
* @brief 结算一次标准消行带来的 Rogue 得分、经验、连击和派生效果。
|
||||
* @param linesCleared 本次标准消行数量。
|
||||
*/
|
||||
void ApplyLineClearResult(int linesCleared)
|
||||
{
|
||||
@@ -2053,6 +2176,7 @@ void ApplyLineClearResult(int linesCleared)
|
||||
return;
|
||||
}
|
||||
|
||||
// 基础收益先计算,再依次套用风险、成长、狂热、赌徒和连击类修正。
|
||||
int scoreGain = GetRogueScoreByLines(linesCleared);
|
||||
scoreGain = scoreGain * rogueStats.scoreMultiplierPercent / 100;
|
||||
|
||||
@@ -2436,8 +2560,114 @@ void OpenUpgradeMenu()
|
||||
currentScreen = SCREEN_UPGRADE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 结束一轮升级选择,如果还有待处理升级则刷新下一轮选项。
|
||||
* @return 仍停留在升级界面返回 true;已回到游戏返回 false。
|
||||
*/
|
||||
static bool FinishUpgradeSelectionRound()
|
||||
{
|
||||
if (upgradeUiState.pendingCount > 0)
|
||||
{
|
||||
upgradeUiState.pendingCount--;
|
||||
}
|
||||
|
||||
if (upgradeUiState.pendingCount > 0)
|
||||
{
|
||||
FillUpgradeOptions();
|
||||
currentScreen = SCREEN_UPGRADE;
|
||||
return true;
|
||||
}
|
||||
|
||||
upgradeUiState.optionCount = 0;
|
||||
upgradeUiState.picksRemaining = 0;
|
||||
upgradeUiState.markedCount = 0;
|
||||
|
||||
currentScreen = SCREEN_PLAYING;
|
||||
ResolvePendingUpgradeShockwave();
|
||||
PlayPendingLineClearEffect();
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 构造单选强化的反馈正文。
|
||||
* @param selectedOption 被选择的强化选项。
|
||||
* @param gamblerSuffix 赌徒契约附加说明。
|
||||
* @param feedbackDetail 返回反馈正文。
|
||||
* @param feedbackDetailCapacity 反馈正文缓冲区长度。
|
||||
*/
|
||||
static void BuildSingleUpgradeFeedback(const UpgradeOption& selectedOption, const TCHAR* gamblerSuffix, TCHAR feedbackDetail[], int feedbackDetailCapacity)
|
||||
{
|
||||
if (selectedOption.id == UPGRADE_PIECE_TUNING && selectedOption.targetPieceType >= 0)
|
||||
{
|
||||
_stprintf_s(
|
||||
feedbackDetail,
|
||||
feedbackDetailCapacity,
|
||||
_T("%s 块的生成概率提高%s"),
|
||||
GetPieceShortName(0),
|
||||
gamblerSuffix);
|
||||
}
|
||||
else
|
||||
{
|
||||
_stprintf_s(feedbackDetail, feedbackDetailCapacity, _T("%s%s"), selectedOption.description, gamblerSuffix);
|
||||
}
|
||||
|
||||
if (selectedOption.cursed)
|
||||
{
|
||||
_stprintf_s(
|
||||
feedbackDetail + lstrlen(feedbackDetail),
|
||||
feedbackDetailCapacity - lstrlen(feedbackDetail),
|
||||
_T(" 诅咒缠身:下一次升级所需 EXP 提高 25%。"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 应用多选升级中一个已标记的选项并拼接反馈。
|
||||
* @param selectedOption 被应用的选项。
|
||||
* @param appliedSelections 已应用选项数量。
|
||||
* @param feedbackTitle 返回反馈标题。
|
||||
* @param feedbackDetail 返回反馈正文。
|
||||
*/
|
||||
static void ApplyMarkedUpgradeOption(const UpgradeOption& selectedOption, int& appliedSelections, TCHAR feedbackTitle[], TCHAR feedbackDetail[])
|
||||
{
|
||||
TCHAR gamblerSuffix[64] = _T("");
|
||||
int applyCount = RollGamblerApplyCount(gamblerSuffix, 64, true);
|
||||
|
||||
ApplyUpgradeById(selectedOption.id, selectedOption.targetPieceType, applyCount);
|
||||
upgradeUiState.totalChosenCount++;
|
||||
|
||||
if (selectedOption.cursed)
|
||||
{
|
||||
ApplyDestinyCurse();
|
||||
}
|
||||
|
||||
if (appliedSelections == 0)
|
||||
{
|
||||
_stprintf_s(feedbackTitle, 64, _T("获得强化:%s"), selectedOption.name);
|
||||
}
|
||||
else
|
||||
{
|
||||
_stprintf_s(feedbackTitle, 64, _T("获得强化 x%d"), appliedSelections + 1);
|
||||
}
|
||||
|
||||
if (lstrlen(feedbackDetail) > 0)
|
||||
{
|
||||
_stprintf_s(feedbackDetail + lstrlen(feedbackDetail), 128 - lstrlen(feedbackDetail), _T(";"));
|
||||
}
|
||||
_stprintf_s(
|
||||
feedbackDetail + lstrlen(feedbackDetail),
|
||||
128 - lstrlen(feedbackDetail),
|
||||
_T("%s%s%s"),
|
||||
selectedOption.name,
|
||||
gamblerSuffix,
|
||||
selectedOption.cursed ? _T(" [诅咒]") : _T(""));
|
||||
appliedSelections++;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 确认升级菜单中的选择并应用对应强化效果。
|
||||
*
|
||||
* 多选流程会先检查标记数量,再按卡片顺序逐个应用;单选流程应用后会移除
|
||||
* 当前卡片。所有待升级次数结算完成后,才恢复游戏、播放暂存消行动画和冲击波。
|
||||
*/
|
||||
void ConfirmUpgradeSelection()
|
||||
{
|
||||
@@ -2446,6 +2676,7 @@ void ConfirmUpgradeSelection()
|
||||
return;
|
||||
}
|
||||
|
||||
// 命运轮盘或双重抉择使用多选分支,避免选完第一张后候选池被立即刷新。
|
||||
if (upgradeUiState.picksRemaining > 1)
|
||||
{
|
||||
if (upgradeUiState.markedCount != upgradeUiState.picksRemaining)
|
||||
@@ -2466,62 +2697,15 @@ void ConfirmUpgradeSelection()
|
||||
}
|
||||
|
||||
UpgradeOption selectedOption = upgradeUiState.options[optionIndex];
|
||||
TCHAR gamblerSuffix[64] = _T("");
|
||||
int applyCount = RollGamblerApplyCount(gamblerSuffix, 64, true);
|
||||
|
||||
ApplyUpgradeById(selectedOption.id, selectedOption.targetPieceType, applyCount);
|
||||
upgradeUiState.totalChosenCount++;
|
||||
|
||||
if (selectedOption.cursed)
|
||||
{
|
||||
ApplyDestinyCurse();
|
||||
}
|
||||
|
||||
if (appliedSelections == 0)
|
||||
{
|
||||
_stprintf_s(feedbackTitle, _T("获得强化:%s"), selectedOption.name);
|
||||
}
|
||||
else
|
||||
{
|
||||
_stprintf_s(feedbackTitle, _T("获得强化 x%d"), appliedSelections + 1);
|
||||
}
|
||||
|
||||
if (lstrlen(feedbackDetail) > 0)
|
||||
{
|
||||
_stprintf_s(feedbackDetail + lstrlen(feedbackDetail), 128 - lstrlen(feedbackDetail), _T(";"));
|
||||
}
|
||||
_stprintf_s(
|
||||
feedbackDetail + lstrlen(feedbackDetail),
|
||||
128 - lstrlen(feedbackDetail),
|
||||
_T("%s%s%s"),
|
||||
selectedOption.name,
|
||||
gamblerSuffix,
|
||||
selectedOption.cursed ? _T(" [诅咒]") : _T(""));
|
||||
appliedSelections++;
|
||||
ApplyMarkedUpgradeOption(selectedOption, appliedSelections, feedbackTitle, feedbackDetail);
|
||||
}
|
||||
|
||||
SetFeedbackMessage(feedbackTitle, feedbackDetail, 12);
|
||||
if (upgradeUiState.pendingCount > 0)
|
||||
{
|
||||
upgradeUiState.pendingCount--;
|
||||
}
|
||||
|
||||
if (upgradeUiState.pendingCount > 0)
|
||||
{
|
||||
FillUpgradeOptions();
|
||||
currentScreen = SCREEN_UPGRADE;
|
||||
return;
|
||||
}
|
||||
|
||||
upgradeUiState.optionCount = 0;
|
||||
upgradeUiState.picksRemaining = 0;
|
||||
upgradeUiState.markedCount = 0;
|
||||
currentScreen = SCREEN_PLAYING;
|
||||
ResolvePendingUpgradeShockwave();
|
||||
PlayPendingLineClearEffect();
|
||||
FinishUpgradeSelectionRound();
|
||||
return;
|
||||
}
|
||||
|
||||
// 普通升级只应用当前高亮卡片,并把剩余卡片前移,支持后续 picksRemaining。
|
||||
UpgradeOption selectedOption = upgradeUiState.options[upgradeUiState.selectedIndex];
|
||||
TCHAR gamblerSuffix[64] = _T("");
|
||||
int applyCount = RollGamblerApplyCount(gamblerSuffix, 64, false);
|
||||
@@ -2531,26 +2715,11 @@ void ConfirmUpgradeSelection()
|
||||
TCHAR feedbackTitle[64];
|
||||
TCHAR feedbackDetail[128];
|
||||
_stprintf_s(feedbackTitle, _T("获得强化:%s"), selectedOption.name);
|
||||
if (selectedOption.id == UPGRADE_PIECE_TUNING && selectedOption.targetPieceType >= 0)
|
||||
{
|
||||
_stprintf_s(
|
||||
feedbackDetail,
|
||||
_T("%s 块的生成概率提高%s"),
|
||||
GetPieceShortName(0),
|
||||
gamblerSuffix);
|
||||
}
|
||||
else
|
||||
{
|
||||
_stprintf_s(feedbackDetail, _T("%s%s"), selectedOption.description, gamblerSuffix);
|
||||
}
|
||||
BuildSingleUpgradeFeedback(selectedOption, gamblerSuffix, feedbackDetail, 128);
|
||||
|
||||
if (selectedOption.cursed)
|
||||
{
|
||||
ApplyDestinyCurse();
|
||||
_stprintf_s(
|
||||
feedbackDetail + lstrlen(feedbackDetail),
|
||||
128 - lstrlen(feedbackDetail),
|
||||
_T(" 诅咒缠身:下一次升级所需 EXP 提高 25%。"));
|
||||
}
|
||||
|
||||
SetFeedbackMessage(feedbackTitle, feedbackDetail, 12);
|
||||
@@ -2580,25 +2749,7 @@ void ConfirmUpgradeSelection()
|
||||
return;
|
||||
}
|
||||
|
||||
if (upgradeUiState.pendingCount > 0)
|
||||
{
|
||||
upgradeUiState.pendingCount--;
|
||||
}
|
||||
|
||||
if (upgradeUiState.pendingCount > 0)
|
||||
{
|
||||
FillUpgradeOptions();
|
||||
currentScreen = SCREEN_UPGRADE;
|
||||
return;
|
||||
}
|
||||
|
||||
upgradeUiState.optionCount = 0;
|
||||
upgradeUiState.picksRemaining = 0;
|
||||
upgradeUiState.markedCount = 0;
|
||||
|
||||
currentScreen = SCREEN_PLAYING;
|
||||
ResolvePendingUpgradeShockwave();
|
||||
PlayPendingLineClearEffect();
|
||||
FinishUpgradeSelectionRound();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1,6 +1,7 @@
|
||||
// stdafx.cpp : 只包括标准包含文件的源文件
|
||||
// Tetris.pch 将作为预编译头
|
||||
// stdafx.obj 将包含预编译类型信息
|
||||
/**
|
||||
* @file stdafx.cpp
|
||||
* @brief 预编译头源文件,只包含 stdafx.h 以生成共享编译信息。
|
||||
*/
|
||||
|
||||
#include "stdafx.h"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user