项目架构重构,代码整理
This commit is contained in:
@@ -98,6 +98,15 @@ Tereis/
|
|||||||
├─ src/
|
├─ src/
|
||||||
│ ├─ include/ 头文件
|
│ ├─ include/ 头文件
|
||||||
│ ├─ source/ 源文件
|
│ ├─ source/ 源文件
|
||||||
|
│ │ ├─ Tetris.cpp 程序入口、窗口和消息框架
|
||||||
|
│ │ ├─ TetrisLogic.cpp 基础俄罗斯方块逻辑框架
|
||||||
|
│ │ ├─ TetrisRender.cpp 基础绘制框架
|
||||||
|
│ │ ├─ common/ 资源路径、文件检查等通用工具
|
||||||
|
│ │ ├─ app/ 媒体播放、布局命中、输入和定时器处理
|
||||||
|
│ │ ├─ extensions/ 框架外通用扩展、界面状态和视觉效果
|
||||||
|
│ │ ├─ logic/ 特殊方块落地效果等逻辑扩展
|
||||||
|
│ │ ├─ render/ 图片加载等渲染内部支持
|
||||||
|
│ │ └─ rogue/ Rogue 模式、强化和技能系统
|
||||||
│ └─ resources/ Windows 资源脚本
|
│ └─ resources/ Windows 资源脚本
|
||||||
├─ assets/
|
├─ assets/
|
||||||
│ ├─ audio/ 背景音乐
|
│ ├─ audio/ 背景音乐
|
||||||
@@ -130,6 +139,8 @@ Tereis/
|
|||||||
C:\mingw64\bin\
|
C:\mingw64\bin\
|
||||||
```
|
```
|
||||||
|
|
||||||
|
构建脚本会递归收集 `src/source` 下的 `.cpp` 文件。新增功能代码可以放入功能目录,不需要手动维护固定源码列表。
|
||||||
|
|
||||||
## 构建与运行
|
## 构建与运行
|
||||||
|
|
||||||
在项目根目录执行:
|
在项目根目录执行:
|
||||||
@@ -194,10 +205,16 @@ powershell -NoProfile -ExecutionPolicy Bypass -File .\build-mingw.ps1 -Run
|
|||||||
|
|
||||||
本项目以过程式 C++ 写法为主,核心逻辑分布如下:
|
本项目以过程式 C++ 写法为主,核心逻辑分布如下:
|
||||||
|
|
||||||
- `src/source/Tetris.cpp`:窗口、消息循环、输入和鼠标交互
|
- `src/source/Tetris.cpp`:Win32 程序入口、窗口创建和消息分发主干
|
||||||
- `src/source/TetrisLogic.cpp`:基础方块逻辑、消行和状态重置
|
- `src/source/TetrisLogic.cpp`:基础方块逻辑、消行和状态重置
|
||||||
- `src/source/TetrisRogue.cpp`:Rogue 模式、强化、技能和成长系统
|
|
||||||
- `src/source/TetrisRender.cpp`:界面绘制、面板、动画和特效
|
- `src/source/TetrisRender.cpp`:界面绘制、面板、动画和特效
|
||||||
|
- `src/source/common/TetrisAssets.cpp`:资源路径拼接和文件存在判断
|
||||||
|
- `src/source/app/`:背景音乐、复活视频、窗口布局命中、鼠标键盘和定时器处理
|
||||||
|
- `src/source/logic/TetrisPieceEffects.cpp`:彩虹、爆破、激光、十字和稳定结构等落地效果
|
||||||
|
- `src/source/extensions/TetrisGameExtensions.cpp`:框架外通用状态切换、复活、说明页、视觉效果等扩展支持
|
||||||
|
- `src/source/render/TetrisRenderAssets.cpp`:背景图、致谢页图片等 GDI+ 图片资源加载
|
||||||
|
- `src/source/rogue/TetrisRogue.cpp`:Rogue 模式、强化、技能和成长系统
|
||||||
- `src/include/Tetris.h`:主要结构体、全局状态和函数声明
|
- `src/include/Tetris.h`:主要结构体、全局状态和函数声明
|
||||||
|
- `src/include/TetrisAppInternal.h`、`src/include/TetrisRenderInternal.h`、`src/include/TetrisAssets.h`:窗口层、渲染层和资源工具的内部声明
|
||||||
|
|
||||||
项目适合作为程序设计课程大作业展示,也便于在答辩时讲解窗口程序、游戏循环、碰撞检测、状态管理和功能扩展。
|
项目适合作为程序设计课程大作业展示,也便于在答辩时讲解窗口程序、游戏循环、碰撞检测、状态管理和功能扩展。
|
||||||
|
|||||||
+3
-8
@@ -61,14 +61,9 @@ foreach ($Candidate in $WindresCandidates) {
|
|||||||
|
|
||||||
New-Item -ItemType Directory -Force -Path $BuildDir | Out-Null
|
New-Item -ItemType Directory -Force -Path $BuildDir | Out-Null
|
||||||
|
|
||||||
$Sources = @(
|
$Sources = Get-ChildItem -Path $SourceDir -Recurse -Filter "*.cpp" |
|
||||||
(Join-Path $SourceDir "stdafx.cpp"),
|
Sort-Object FullName |
|
||||||
(Join-Path $SourceDir "Tetris.cpp"),
|
Select-Object -ExpandProperty FullName
|
||||||
(Join-Path $SourceDir "TetrisLogic.cpp"),
|
|
||||||
(Join-Path $SourceDir "TetrisLogicInnovation.cpp"),
|
|
||||||
(Join-Path $SourceDir "TetrisRogue.cpp"),
|
|
||||||
(Join-Path $SourceDir "TetrisRender.cpp")
|
|
||||||
)
|
|
||||||
|
|
||||||
$LinkInputs = @()
|
$LinkInputs = @()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#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 根据当前窗口大小计算整体界面缩放与偏移。
|
||||||
|
*/
|
||||||
|
LayoutMetrics GetLayoutMetrics(HWND hWnd);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 按当前布局比例缩放一个尺寸值。
|
||||||
|
*/
|
||||||
|
int ScaleValue(const LayoutMetrics& metrics, int value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 按当前布局比例缩放横坐标并叠加窗口偏移。
|
||||||
|
*/
|
||||||
|
int ScaleXValue(const LayoutMetrics& metrics, int value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 按当前布局比例缩放纵坐标并叠加窗口偏移。
|
||||||
|
*/
|
||||||
|
int ScaleYValue(const LayoutMetrics& metrics, int value);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取主菜单选项的点击区域。
|
||||||
|
*/
|
||||||
|
RECT GetMenuOptionRect(HWND hWnd, int index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取帮助页选项的点击区域。
|
||||||
|
*/
|
||||||
|
RECT GetHelpOptionRect(HWND hWnd, int index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取技能演示列表项的点击区域。
|
||||||
|
*/
|
||||||
|
RECT GetHelpSkillDemoItemRect(HWND hWnd, int index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取帮助页底部返回提示的点击区域。
|
||||||
|
*/
|
||||||
|
RECT GetHelpBackHintRect(HWND hWnd);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取致谢页左右切换按钮的点击区域。
|
||||||
|
*/
|
||||||
|
RECT GetCreditArrowRect(HWND hWnd, int direction);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取升级选择卡片的点击区域。
|
||||||
|
*/
|
||||||
|
RECT GetUpgradeCardRect(HWND hWnd, int index);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取暂停或结束覆盖层按钮的点击区域。
|
||||||
|
*/
|
||||||
|
RECT GetOverlayButtonRect(HWND hWnd, int index, int buttonCount);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取左上角返回按钮的点击区域。
|
||||||
|
*/
|
||||||
|
RECT GetBackButtonRect(HWND hWnd);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取右下角音乐按钮的点击区域。
|
||||||
|
*/
|
||||||
|
RECT GetMusicButtonRect(HWND hWnd);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 判断点坐标是否落在矩形内部。
|
||||||
|
*/
|
||||||
|
bool IsPointInRect(const RECT& rect, int x, int y);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 将滚动偏移按步长调整并限制在有效范围内。
|
||||||
|
*/
|
||||||
|
void AdjustScrollOffset(int& scrollOffset, int delta);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 获取适配当前窗口缩放的一次滚动步长。
|
||||||
|
*/
|
||||||
|
int GetScrollStep(HWND hWnd, int baseStep);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 重置主下落定时器。
|
||||||
|
*/
|
||||||
|
void ResetGameTimer(HWND hWnd);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 启动游戏、特效和致谢页动画定时器。
|
||||||
|
*/
|
||||||
|
void StartAppTimers(HWND hWnd);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 停止游戏、特效和致谢页动画定时器。
|
||||||
|
*/
|
||||||
|
void StopAppTimers(HWND hWnd);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 处理致谢页高频动画刷新消息。
|
||||||
|
*/
|
||||||
|
void HandleCreditTick(HWND hWnd);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 处理窗口定时器消息。
|
||||||
|
*/
|
||||||
|
void HandleTimerMessage(HWND hWnd, WPARAM timerId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 启动背景音乐。
|
||||||
|
*/
|
||||||
|
void StartBackgroundMusic();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 停止背景音乐。
|
||||||
|
*/
|
||||||
|
void StopBackgroundMusic();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 切换背景音乐开关并刷新窗口。
|
||||||
|
*/
|
||||||
|
void ToggleBackgroundMusic(HWND hWnd);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 播放复活视频,播放成功返回 true。
|
||||||
|
*/
|
||||||
|
bool PlayReviveVideo(HWND hWnd);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 处理鼠标左键释放事件,返回是否已处理。
|
||||||
|
*/
|
||||||
|
bool HandleMouseClick(HWND hWnd, LPARAM lParam);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 处理鼠标滚轮事件。
|
||||||
|
*/
|
||||||
|
void HandleMouseWheel(HWND hWnd, WPARAM wParam);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 处理键盘按键事件。
|
||||||
|
*/
|
||||||
|
void HandleKeyDown(HWND hWnd, WPARAM wParam);
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "stdafx.h"
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 根据程序所在目录拼出项目资源文件的绝对路径。
|
||||||
|
*/
|
||||||
|
std::wstring BuildAssetPath(const wchar_t* relativePath);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 根据当前工作目录拼出项目资源文件的绝对路径。
|
||||||
|
*/
|
||||||
|
std::wstring BuildWorkingDirAssetPath(const wchar_t* relativePath);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 判断指定路径是否存在且不是目录。
|
||||||
|
*/
|
||||||
|
bool FileExists(const std::wstring& path);
|
||||||
@@ -108,3 +108,13 @@ int ConsumeNextType();
|
|||||||
* @brief 结算一次标准消行带来的 Rogue 玩法效果。
|
* @brief 结算一次标准消行带来的 Rogue 玩法效果。
|
||||||
*/
|
*/
|
||||||
void ApplyLineClearResult(int linesCleared);
|
void ApplyLineClearResult(int linesCleared);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 结算彩虹方块固定后的染色和清除效果。
|
||||||
|
*/
|
||||||
|
void ApplyRainbowLandingEffect(bool overflowTop, const Point* fixedCells, int fixedCellCount);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 结算爆破、激光、十字和稳定结构等特殊落地效果。
|
||||||
|
*/
|
||||||
|
void ApplySpecialLandingEffects(const Point* fixedCells, int fixedCellCount, const Point* explosiveCells, int explosiveCellCount);
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Tetris.h"
|
||||||
|
#include <objidl.h>
|
||||||
|
#include <gdiplus.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 加载并缓存主背景图片。
|
||||||
|
*/
|
||||||
|
Gdiplus::Bitmap* LoadBackgroundImage();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 按序号加载并缓存致谢页图片。
|
||||||
|
*/
|
||||||
|
Gdiplus::Bitmap* LoadCreditImage(int index);
|
||||||
+12
-1532
File diff suppressed because it is too large
Load Diff
+4
-184
@@ -1,4 +1,4 @@
|
|||||||
#include "stdafx.h"
|
#include "stdafx.h"
|
||||||
#include "Tetris.h"
|
#include "Tetris.h"
|
||||||
#include "TetrisLogicInternal.h"
|
#include "TetrisLogicInternal.h"
|
||||||
|
|
||||||
@@ -431,66 +431,7 @@ void Fixing()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!overflowTop && currentPieceIsRainbow)
|
ApplyRainbowLandingEffect(overflowTop, fixedCells, fixedCellCount);
|
||||||
{
|
|
||||||
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 (overflowTop)
|
||||||
{
|
{
|
||||||
@@ -531,129 +472,7 @@ void Fixing()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentPieceIsExplosive)
|
ApplySpecialLandingEffects(fixedCells, fixedCellCount, explosiveCells, explosiveCellCount);
|
||||||
{
|
|
||||||
int explosiveCellsCleared = 0;
|
|
||||||
|
|
||||||
for (int i = 0; i < explosiveCellCount; i++)
|
|
||||||
{
|
|
||||||
explosiveCellsCleared += ClearExplosiveAreaAt(explosiveCells[i].y, explosiveCells[i].x);
|
|
||||||
}
|
|
||||||
|
|
||||||
int explosiveScoreGain = 0;
|
|
||||||
int explosiveExpGain = 0;
|
|
||||||
if (currentMode == MODE_ROGUE && explosiveCellsCleared > 0)
|
|
||||||
{
|
|
||||||
AwardRogueSkillClearRewards(explosiveCellsCleared, explosiveScoreGain, explosiveExpGain, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
TCHAR explosiveDetail[128];
|
|
||||||
_stprintf_s(
|
|
||||||
explosiveDetail,
|
|
||||||
_T("爆破清除 %d 格 +%d 分 +%d EXP"),
|
|
||||||
explosiveCellsCleared,
|
|
||||||
explosiveScoreGain,
|
|
||||||
explosiveExpGain);
|
|
||||||
SetFeedbackMessage(_T("爆破核心"), explosiveDetail, 12);
|
|
||||||
|
|
||||||
if (rogueStats.chainBombLevel > 0 && explosiveCellCount > 0)
|
|
||||||
{
|
|
||||||
pendingChainBombCenter = explosiveCells[0];
|
|
||||||
pendingChainBombFollowup = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentPieceIsLaser)
|
|
||||||
{
|
|
||||||
int laserColumn = point.x + 1;
|
|
||||||
if (fixedCellCount > 0)
|
|
||||||
{
|
|
||||||
int xSum = 0;
|
|
||||||
for (int i = 0; i < fixedCellCount; i++)
|
|
||||||
{
|
|
||||||
xSum += fixedCells[i].x;
|
|
||||||
}
|
|
||||||
laserColumn = (xSum + fixedCellCount / 2) / fixedCellCount;
|
|
||||||
}
|
|
||||||
if (laserColumn < 0)
|
|
||||||
{
|
|
||||||
laserColumn = 0;
|
|
||||||
}
|
|
||||||
if (laserColumn >= nGameWidth)
|
|
||||||
{
|
|
||||||
laserColumn = nGameWidth - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int laserCellsCleared = ClearColumnAt(laserColumn);
|
|
||||||
if (currentMode == MODE_ROGUE && laserCellsCleared > 0)
|
|
||||||
{
|
|
||||||
int laserScore = 0;
|
|
||||||
int laserExp = 0;
|
|
||||||
AwardRogueSkillClearRewards(laserCellsCleared, laserScore, laserExp, false);
|
|
||||||
|
|
||||||
TCHAR laserDetail[128];
|
|
||||||
_stprintf_s(laserDetail, _T("激光贯穿第 %d 列,清除 %d 格 +%d 分 +%d EXP"), laserColumn + 1, laserCellsCleared, laserScore, laserExp);
|
|
||||||
SetFeedbackMessage(_T("棱镜激光"), laserDetail, 12);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentPieceIsCross)
|
|
||||||
{
|
|
||||||
int crossRow = point.y + 1;
|
|
||||||
int crossColumn = point.x + 1;
|
|
||||||
if (fixedCellCount > 0)
|
|
||||||
{
|
|
||||||
int xSum = 0;
|
|
||||||
int ySum = 0;
|
|
||||||
for (int i = 0; i < fixedCellCount; i++)
|
|
||||||
{
|
|
||||||
xSum += fixedCells[i].x;
|
|
||||||
ySum += fixedCells[i].y;
|
|
||||||
}
|
|
||||||
crossColumn = (xSum + fixedCellCount / 2) / fixedCellCount;
|
|
||||||
crossRow = (ySum + fixedCellCount / 2) / fixedCellCount;
|
|
||||||
}
|
|
||||||
if (crossRow < 0)
|
|
||||||
{
|
|
||||||
crossRow = 0;
|
|
||||||
}
|
|
||||||
if (crossRow >= GetRoguePlayableHeight())
|
|
||||||
{
|
|
||||||
crossRow = GetRoguePlayableHeight() - 1;
|
|
||||||
}
|
|
||||||
if (crossColumn < 0)
|
|
||||||
{
|
|
||||||
crossColumn = 0;
|
|
||||||
}
|
|
||||||
if (crossColumn >= nGameWidth)
|
|
||||||
{
|
|
||||||
crossColumn = nGameWidth - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int crossCellsCleared = ClearRowAt(crossRow);
|
|
||||||
int columnCellsCleared = ClearColumnAtWithColor(crossColumn, RGB(196, 255, 132));
|
|
||||||
if (workRegion[crossRow][crossColumn] == 0 && columnCellsCleared > 0)
|
|
||||||
{
|
|
||||||
// center cell may already be counted by row clear
|
|
||||||
}
|
|
||||||
int totalCrossCleared = crossCellsCleared + columnCellsCleared;
|
|
||||||
|
|
||||||
if (currentMode == MODE_ROGUE && totalCrossCleared > 0)
|
|
||||||
{
|
|
||||||
int crossScore = 0;
|
|
||||||
int crossExp = 0;
|
|
||||||
AwardRogueSkillClearRewards(totalCrossCleared, crossScore, crossExp, false);
|
|
||||||
|
|
||||||
TCHAR crossDetail[128];
|
|
||||||
_stprintf_s(crossDetail, _T("十字冲击第 %d 行 / 第 %d 列,清除 %d 格 +%d 分 +%d EXP"), crossRow + 1, crossColumn + 1, totalCrossCleared, crossScore, crossExp);
|
|
||||||
SetFeedbackMessage(_T("十字方块"), crossDetail, 12);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!currentPieceIsRainbow && TryStabilizeBoard() > 0)
|
|
||||||
{
|
|
||||||
SetFeedbackMessage(_T("稳定结构"), _T("附近空洞被自动填补,阵型更加稳固。"), 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentMode == MODE_ROGUE)
|
if (currentMode == MODE_ROGUE)
|
||||||
{
|
{
|
||||||
@@ -876,3 +695,4 @@ void Restart()
|
|||||||
ComputeTarget();
|
ComputeTarget();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+3
-165
@@ -1,8 +1,8 @@
|
|||||||
#include "stdafx.h"
|
#include "stdafx.h"
|
||||||
#include "Tetris.h"
|
#include "Tetris.h"
|
||||||
|
#include "TetrisRenderInternal.h"
|
||||||
#include <objidl.h>
|
#include <objidl.h>
|
||||||
#include <gdiplus.h>
|
#include <gdiplus.h>
|
||||||
#include <string>
|
|
||||||
|
|
||||||
#pragma comment(lib, "gdiplus.lib")
|
#pragma comment(lib, "gdiplus.lib")
|
||||||
|
|
||||||
@@ -34,169 +34,6 @@ static HBRUSH GetCachedParticleBrush(COLORREF color)
|
|||||||
return CreateSolidBrush(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 按序号加载致谢页图片资源。
|
|
||||||
*/
|
|
||||||
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)
|
void TDrawScreen(HDC hdc, HWND hWnd)
|
||||||
{
|
{
|
||||||
RECT clientRect;
|
RECT clientRect;
|
||||||
@@ -2855,3 +2692,4 @@ void TDrawScreen(HDC hdc, HWND hWnd)
|
|||||||
DeleteObject(bodyFont);
|
DeleteObject(bodyFont);
|
||||||
DeleteObject(smallFont);
|
DeleteObject(smallFont);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,797 @@
|
|||||||
|
#include "stdafx.h"
|
||||||
|
#include "TetrisAppInternal.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 打开当前菜单选中的页面或开始对应模式。
|
||||||
|
*/
|
||||||
|
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 处理返回按钮的统一点击行为。
|
||||||
|
*/
|
||||||
|
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 处理主菜单点击。
|
||||||
|
*/
|
||||||
|
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 处理规则、帮助、致谢和技能演示页点击。
|
||||||
|
*/
|
||||||
|
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 处理升级选择界面点击。
|
||||||
|
*/
|
||||||
|
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 处理暂停和结束覆盖层点击。
|
||||||
|
*/
|
||||||
|
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 处理鼠标左键释放事件,返回是否已处理。
|
||||||
|
*/
|
||||||
|
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 处理鼠标滚轮事件。
|
||||||
|
*/
|
||||||
|
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 处理主菜单键盘导航。
|
||||||
|
*/
|
||||||
|
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 处理帮助和致谢页键盘导航。
|
||||||
|
*/
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
helpState.selectedIndex--;
|
||||||
|
if (helpState.selectedIndex < 0)
|
||||||
|
{
|
||||||
|
helpState.selectedIndex = helpState.optionCount - 1;
|
||||||
|
}
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
else if (helpState.currentPage == 4)
|
||||||
|
{
|
||||||
|
ChangeCreditPage(-1);
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
else if (helpState.currentPage == 5)
|
||||||
|
{
|
||||||
|
helpState.selectedIndex--;
|
||||||
|
if (helpState.selectedIndex < 0)
|
||||||
|
{
|
||||||
|
helpState.selectedIndex = GetRogueSkillDemoCount() - 1;
|
||||||
|
}
|
||||||
|
if (helpState.selectedIndex * 68 < helpScrollOffset)
|
||||||
|
{
|
||||||
|
helpScrollOffset = helpState.selectedIndex * 68;
|
||||||
|
}
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case VK_DOWN:
|
||||||
|
case VK_RIGHT:
|
||||||
|
case 'S':
|
||||||
|
case 'D':
|
||||||
|
if (helpState.currentPage == 0)
|
||||||
|
{
|
||||||
|
helpState.selectedIndex++;
|
||||||
|
if (helpState.selectedIndex >= helpState.optionCount)
|
||||||
|
{
|
||||||
|
helpState.selectedIndex = 0;
|
||||||
|
}
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
else if (helpState.currentPage == 4)
|
||||||
|
{
|
||||||
|
ChangeCreditPage(1);
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
else if (helpState.currentPage == 5)
|
||||||
|
{
|
||||||
|
helpState.selectedIndex++;
|
||||||
|
if (helpState.selectedIndex >= GetRogueSkillDemoCount())
|
||||||
|
{
|
||||||
|
helpState.selectedIndex = 0;
|
||||||
|
helpScrollOffset = 0;
|
||||||
|
}
|
||||||
|
else if (helpState.selectedIndex * 68 > helpScrollOffset + 360)
|
||||||
|
{
|
||||||
|
helpScrollOffset = helpState.selectedIndex * 68 - 360;
|
||||||
|
}
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case VK_RETURN:
|
||||||
|
case VK_SPACE:
|
||||||
|
if (helpState.currentPage == 0)
|
||||||
|
{
|
||||||
|
if (helpState.selectedIndex == 3)
|
||||||
|
{
|
||||||
|
helpState.currentPage = 5;
|
||||||
|
helpState.selectedIndex = 0;
|
||||||
|
helpScrollOffset = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
helpState.currentPage = helpState.selectedIndex + 1;
|
||||||
|
helpScrollOffset = 0;
|
||||||
|
}
|
||||||
|
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':
|
||||||
|
{
|
||||||
|
int previousPage = helpState.currentPage;
|
||||||
|
if (helpState.currentPage == 0)
|
||||||
|
{
|
||||||
|
ReturnToMainMenu();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
helpState.currentPage = 0;
|
||||||
|
if (previousPage == 4 || previousPage == 5)
|
||||||
|
{
|
||||||
|
helpState.selectedIndex = 3;
|
||||||
|
}
|
||||||
|
helpScrollOffset = 0;
|
||||||
|
}
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 处理升级选择界面键盘导航。
|
||||||
|
*/
|
||||||
|
static bool HandleUpgradeKey(HWND hWnd, WPARAM key)
|
||||||
|
{
|
||||||
|
if (currentScreen != SCREEN_UPGRADE)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int upgradeColumnCount = upgradeUiState.optionCount <= 3 ? upgradeUiState.optionCount : 3;
|
||||||
|
if (upgradeColumnCount < 1)
|
||||||
|
{
|
||||||
|
upgradeColumnCount = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (key)
|
||||||
|
{
|
||||||
|
case VK_LEFT:
|
||||||
|
case 'A':
|
||||||
|
if (upgradeUiState.optionCount > 1)
|
||||||
|
{
|
||||||
|
int rowStart = upgradeUiState.selectedIndex - (upgradeUiState.selectedIndex % upgradeColumnCount);
|
||||||
|
if (upgradeUiState.selectedIndex > rowStart)
|
||||||
|
{
|
||||||
|
upgradeUiState.selectedIndex--;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
int rowEnd = rowStart + upgradeColumnCount - 1;
|
||||||
|
if (rowEnd >= upgradeUiState.optionCount)
|
||||||
|
{
|
||||||
|
rowEnd = upgradeUiState.optionCount - 1;
|
||||||
|
}
|
||||||
|
upgradeUiState.selectedIndex = rowEnd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
break;
|
||||||
|
case VK_RIGHT:
|
||||||
|
case 'D':
|
||||||
|
if (upgradeUiState.optionCount > 1)
|
||||||
|
{
|
||||||
|
int rowStart = upgradeUiState.selectedIndex - (upgradeUiState.selectedIndex % upgradeColumnCount);
|
||||||
|
int rowEnd = rowStart + upgradeColumnCount - 1;
|
||||||
|
if (rowEnd >= upgradeUiState.optionCount)
|
||||||
|
{
|
||||||
|
rowEnd = upgradeUiState.optionCount - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
upgradeUiState.selectedIndex = (upgradeUiState.selectedIndex < rowEnd) ? upgradeUiState.selectedIndex + 1 : rowStart;
|
||||||
|
}
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
break;
|
||||||
|
case VK_UP:
|
||||||
|
case 'W':
|
||||||
|
if (upgradeUiState.selectedIndex >= upgradeColumnCount)
|
||||||
|
{
|
||||||
|
upgradeUiState.selectedIndex -= upgradeColumnCount;
|
||||||
|
}
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
break;
|
||||||
|
case VK_DOWN:
|
||||||
|
case 'S':
|
||||||
|
if (upgradeUiState.selectedIndex + upgradeColumnCount < upgradeUiState.optionCount)
|
||||||
|
{
|
||||||
|
upgradeUiState.selectedIndex += upgradeColumnCount;
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
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++;
|
||||||
|
}
|
||||||
|
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 处理游戏过程中的按键。
|
||||||
|
*/
|
||||||
|
static void HandlePlayingKey(HWND hWnd, WPARAM key)
|
||||||
|
{
|
||||||
|
if (IsRogueSkillDemoMode())
|
||||||
|
{
|
||||||
|
if (key == 'N')
|
||||||
|
{
|
||||||
|
AdvanceRogueSkillDemo();
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (key == 'R')
|
||||||
|
{
|
||||||
|
RestartCurrentRogueSkillDemo();
|
||||||
|
ResetGameTimer(hWnd);
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (key == VK_ESCAPE || key == VK_BACK || key == 'M')
|
||||||
|
{
|
||||||
|
OpenSkillDemoScreen();
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsRogueSkillDemoMode() && key == 'M')
|
||||||
|
{
|
||||||
|
ReturnToMainMenu();
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsRogueSkillDemoMode() && key == 'R')
|
||||||
|
{
|
||||||
|
StartGameWithMode(currentMode);
|
||||||
|
ResetGameTimer(hWnd);
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsRogueSkillDemoMode() && key == 'P')
|
||||||
|
{
|
||||||
|
suspendFlag = !suspendFlag;
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key == 'G')
|
||||||
|
{
|
||||||
|
targetFlag = !targetFlag;
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gameOverFlag && reviveAvailable && key == 'V')
|
||||||
|
{
|
||||||
|
if (PlayReviveVideo(hWnd))
|
||||||
|
{
|
||||||
|
ReviveAfterVideo();
|
||||||
|
ResetGameTimer(hWnd);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SetFeedbackMessage(_T("视频播放失败"), _T("无法打开复活视频,复活机会未消耗。"), 14);
|
||||||
|
}
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gameOverFlag || suspendFlag)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (key)
|
||||||
|
{
|
||||||
|
case VK_LEFT:
|
||||||
|
case 'A':
|
||||||
|
if (CanMoveLeft())
|
||||||
|
{
|
||||||
|
MoveLeft();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case VK_RIGHT:
|
||||||
|
case 'D':
|
||||||
|
if (CanMoveRight())
|
||||||
|
{
|
||||||
|
MoveRight();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case VK_DOWN:
|
||||||
|
case 'S':
|
||||||
|
if (CanMoveDown())
|
||||||
|
{
|
||||||
|
MoveDown();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Fixing();
|
||||||
|
if (!gameOverFlag)
|
||||||
|
{
|
||||||
|
DeleteLines();
|
||||||
|
CheckRogueLevelProgress();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case VK_UP:
|
||||||
|
case 'W':
|
||||||
|
Rotate();
|
||||||
|
break;
|
||||||
|
case VK_SPACE:
|
||||||
|
DropDown();
|
||||||
|
Fixing();
|
||||||
|
if (!gameOverFlag)
|
||||||
|
{
|
||||||
|
DeleteLines();
|
||||||
|
CheckRogueLevelProgress();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'C':
|
||||||
|
case VK_SHIFT:
|
||||||
|
case VK_LSHIFT:
|
||||||
|
case VK_RSHIFT:
|
||||||
|
HoldCurrentPiece();
|
||||||
|
break;
|
||||||
|
case 'Z':
|
||||||
|
UseBlackHole();
|
||||||
|
break;
|
||||||
|
case 'X':
|
||||||
|
UseScreenBomb();
|
||||||
|
break;
|
||||||
|
case 'V':
|
||||||
|
UseAirReshape();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!gameOverFlag)
|
||||||
|
{
|
||||||
|
ComputeTarget();
|
||||||
|
}
|
||||||
|
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 处理键盘按键事件。
|
||||||
|
*/
|
||||||
|
void HandleKeyDown(HWND hWnd, WPARAM wParam)
|
||||||
|
{
|
||||||
|
if (HandleMenuKey(hWnd, wParam) ||
|
||||||
|
HandleRulesKey(hWnd, wParam) ||
|
||||||
|
HandleUpgradeKey(hWnd, wParam))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HandlePlayingKey(hWnd, wParam);
|
||||||
|
}
|
||||||
@@ -0,0 +1,322 @@
|
|||||||
|
#include "stdafx.h"
|
||||||
|
#include "TetrisAppInternal.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 将指定滚动偏移按步长调整,并限制在非负范围内。
|
||||||
|
*/
|
||||||
|
void AdjustScrollOffset(int& scrollOffset, int delta)
|
||||||
|
{
|
||||||
|
scrollOffset += delta;
|
||||||
|
if (scrollOffset < 0)
|
||||||
|
{
|
||||||
|
scrollOffset = 0;
|
||||||
|
}
|
||||||
|
if (scrollOffset > 2400)
|
||||||
|
{
|
||||||
|
scrollOffset = 2400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 按当前窗口缩放返回一次滚动操作的像素距离。
|
||||||
|
*/
|
||||||
|
int GetScrollStep(HWND hWnd, int baseStep)
|
||||||
|
{
|
||||||
|
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
|
||||||
|
return MulDiv(baseStep, metrics.scale, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 根据当前窗口大小计算整体界面缩放与偏移。
|
||||||
|
*/
|
||||||
|
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 按当前布局比例缩放一个尺寸值。
|
||||||
|
*/
|
||||||
|
int ScaleValue(const LayoutMetrics& metrics, int value)
|
||||||
|
{
|
||||||
|
return MulDiv(value, metrics.scale, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 按当前布局比例缩放横坐标并叠加窗口偏移。
|
||||||
|
*/
|
||||||
|
int ScaleXValue(const LayoutMetrics& metrics, int value)
|
||||||
|
{
|
||||||
|
return metrics.offsetX + MulDiv(value, metrics.scale, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 按当前布局比例缩放纵坐标并叠加窗口偏移。
|
||||||
|
*/
|
||||||
|
int ScaleYValue(const LayoutMetrics& metrics, int value)
|
||||||
|
{
|
||||||
|
return metrics.offsetY + MulDiv(value, metrics.scale, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
RECT GetBackButtonRect(HWND hWnd)
|
||||||
|
{
|
||||||
|
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
|
||||||
|
RECT rect =
|
||||||
|
{
|
||||||
|
ScaleXValue(metrics, 6),
|
||||||
|
ScaleYValue(metrics, 6),
|
||||||
|
ScaleXValue(metrics, 34),
|
||||||
|
ScaleYValue(metrics, 34)
|
||||||
|
};
|
||||||
|
return rect;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,229 @@
|
|||||||
|
#include "stdafx.h"
|
||||||
|
#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 循环播放指定音乐文件。
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
|
||||||
|
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 切换背景音乐开关并刷新窗口。
|
||||||
|
*/
|
||||||
|
void ToggleBackgroundMusic(HWND hWnd)
|
||||||
|
{
|
||||||
|
bgmEnabled = !bgmEnabled;
|
||||||
|
if (bgmEnabled)
|
||||||
|
{
|
||||||
|
StartBackgroundMusic();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
StopBackgroundMusic();
|
||||||
|
}
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 播放复活视频,先尝试 MCI,全屏播放失败时退回系统默认播放器。
|
||||||
|
*/
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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,303 @@
|
|||||||
|
#include "stdafx.h"
|
||||||
|
#include "TetrisAppInternal.h"
|
||||||
|
|
||||||
|
static MMRESULT creditTimerHandle = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 多媒体定时器回调,用于高频率请求致谢页动画刷新。
|
||||||
|
*/
|
||||||
|
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 重置主下落定时器。
|
||||||
|
*/
|
||||||
|
void ResetGameTimer(HWND hWnd)
|
||||||
|
{
|
||||||
|
KillTimer(hWnd, GAME_TIMER_ID);
|
||||||
|
SetTimer(hWnd, GAME_TIMER_ID, currentFallInterval > 0 ? currentFallInterval : GAME_TIMER_INTERVAL, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 启动游戏、特效和致谢页动画定时器。
|
||||||
|
*/
|
||||||
|
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 停止游戏、特效和致谢页动画定时器。
|
||||||
|
*/
|
||||||
|
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 处理致谢页高频动画刷新消息。
|
||||||
|
*/
|
||||||
|
void HandleCreditTick(HWND hWnd)
|
||||||
|
{
|
||||||
|
if (currentScreen == SCREEN_RULES && helpState.currentPage == 4 && TickCreditAnimation())
|
||||||
|
{
|
||||||
|
InvalidateRect(hWnd, nullptr, FALSE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 推进 Rogue 限时状态并按需要重置下落定时器。
|
||||||
|
*/
|
||||||
|
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 检查极限玩家的危险等级计时。
|
||||||
|
*/
|
||||||
|
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 检查高堆叠触发的时间缓流。
|
||||||
|
*/
|
||||||
|
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 推进一次自动下落逻辑。
|
||||||
|
*/
|
||||||
|
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 处理窗口定时器消息。
|
||||||
|
*/
|
||||||
|
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,60 @@
|
|||||||
|
#include "stdafx.h"
|
||||||
|
#include "TetrisAssets.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 根据程序所在目录拼出项目资源文件的绝对路径。
|
||||||
|
*/
|
||||||
|
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 根据当前工作目录拼出项目资源文件的绝对路径。
|
||||||
|
*/
|
||||||
|
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 判断指定路径是否存在且不是目录。
|
||||||
|
*/
|
||||||
|
bool FileExists(const std::wstring& path)
|
||||||
|
{
|
||||||
|
DWORD attributes = GetFileAttributesW(path.c_str());
|
||||||
|
return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) == 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
#include "stdafx.h"
|
||||||
|
#include "TetrisLogicInternal.h"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 结算彩虹方块固定后的染色和清除效果。
|
||||||
|
*/
|
||||||
|
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 结算爆破方块的范围清除效果。
|
||||||
|
*/
|
||||||
|
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 结算激光方块的整列清除效果。
|
||||||
|
*/
|
||||||
|
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 结算十字方块的整行整列清除效果。
|
||||||
|
*/
|
||||||
|
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 结算爆破、激光、十字和稳定结构等特殊落地效果。
|
||||||
|
*/
|
||||||
|
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,138 @@
|
|||||||
|
#include "stdafx.h"
|
||||||
|
#include "TetrisRenderInternal.h"
|
||||||
|
#include "TetrisAssets.h"
|
||||||
|
#include <objidl.h>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#pragma comment(lib, "gdiplus.lib")
|
||||||
|
|
||||||
|
using namespace Gdiplus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 尝试从指定路径加载 GDI+ 位图。
|
||||||
|
*/
|
||||||
|
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+ 已初始化,返回初始化是否成功。
|
||||||
|
*/
|
||||||
|
static bool EnsureGdiplusStarted()
|
||||||
|
{
|
||||||
|
static ULONG_PTR gdiplusToken = 0;
|
||||||
|
static bool attempted = false;
|
||||||
|
static bool started = false;
|
||||||
|
|
||||||
|
if (!attempted)
|
||||||
|
{
|
||||||
|
attempted = true;
|
||||||
|
GdiplusStartupInput startupInput;
|
||||||
|
started = GdiplusStartup(&gdiplusToken, &startupInput, nullptr) == Ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
return started;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief 加载并缓存主背景图片。
|
||||||
|
*/
|
||||||
|
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 按序号加载并缓存致谢页图片。
|
||||||
|
*/
|
||||||
|
Bitmap* LoadCreditImage(int index)
|
||||||
|
{
|
||||||
|
constexpr int creditPageCount = 4;
|
||||||
|
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"
|
||||||
|
};
|
||||||
|
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];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user