diff --git a/README.md b/README.md index 221d718..a4ea24c 100644 --- a/README.md +++ b/README.md @@ -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`:窗口层、渲染层和资源工具的内部声明 项目适合作为程序设计课程大作业展示,也便于在答辩时讲解窗口程序、游戏循环、碰撞检测、状态管理和功能扩展。 diff --git a/build-mingw.ps1 b/build-mingw.ps1 index 874a726..a1507ec 100644 --- a/build-mingw.ps1 +++ b/build-mingw.ps1 @@ -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 = @() diff --git a/src/include/TetrisAppInternal.h b/src/include/TetrisAppInternal.h new file mode 100644 index 0000000..8a15778 --- /dev/null +++ b/src/include/TetrisAppInternal.h @@ -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); diff --git a/src/include/TetrisAssets.h b/src/include/TetrisAssets.h new file mode 100644 index 0000000..10e8b42 --- /dev/null +++ b/src/include/TetrisAssets.h @@ -0,0 +1,19 @@ +#pragma once + +#include "stdafx.h" +#include + +/** + * @brief 根据程序所在目录拼出项目资源文件的绝对路径。 + */ +std::wstring BuildAssetPath(const wchar_t* relativePath); + +/** + * @brief 根据当前工作目录拼出项目资源文件的绝对路径。 + */ +std::wstring BuildWorkingDirAssetPath(const wchar_t* relativePath); + +/** + * @brief 判断指定路径是否存在且不是目录。 + */ +bool FileExists(const std::wstring& path); diff --git a/src/include/TetrisLogicInternal.h b/src/include/TetrisLogicInternal.h index d4f0879..f0d9f61 100644 --- a/src/include/TetrisLogicInternal.h +++ b/src/include/TetrisLogicInternal.h @@ -108,3 +108,13 @@ int ConsumeNextType(); * @brief 结算一次标准消行带来的 Rogue 玩法效果。 */ 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); diff --git a/src/include/TetrisRenderInternal.h b/src/include/TetrisRenderInternal.h new file mode 100644 index 0000000..d3f518b --- /dev/null +++ b/src/include/TetrisRenderInternal.h @@ -0,0 +1,15 @@ +#pragma once + +#include "Tetris.h" +#include +#include + +/** + * @brief 加载并缓存主背景图片。 + */ +Gdiplus::Bitmap* LoadBackgroundImage(); + +/** + * @brief 按序号加载并缓存致谢页图片。 + */ +Gdiplus::Bitmap* LoadCreditImage(int index); diff --git a/src/source/Tetris.cpp b/src/source/Tetris.cpp index fd32fcb..ce80cf8 100644 --- a/src/source/Tetris.cpp +++ b/src/source/Tetris.cpp @@ -1,646 +1,19 @@ -#include "stdafx.h" +#include "stdafx.h" #include "Tetris.h" -#include -#include +#include "TetrisAppInternal.h" #define MAX_LOADSTRING 100 -#define GAME_TIMER_ID 1 -#define EFFECT_TIMER_ID 2 -#define CREDIT_TIMER_ID 3 -#define WM_CREDIT_TICK (WM_APP + 1) -#define GAME_TIMER_INTERVAL 500 -#define EFFECT_TIMER_INTERVAL 16 -#define CREDIT_TIMER_INTERVAL 5 HINSTANCE hInst; TCHAR szTitle[MAX_LOADSTRING]; TCHAR szWindowClass[MAX_LOADSTRING]; bool bgmEnabled = true; -static bool bgmPlaying = false; -static bool bgmUsingMci = false; -static MMRESULT creditTimerHandle = 0; -static constexpr const wchar_t* kBgmAlias = L"TereisBgm"; -static constexpr const wchar_t* kReviveVideoAlias = L"TereisReviveVideo"; ATOM MyRegisterClass(HINSTANCE hInstance); BOOL InitInstance(HINSTANCE, int); LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); INT_PTR CALLBACK About(HWND, UINT, WPARAM, LPARAM); -static std::wstring BuildAssetPath(const wchar_t* relativePath); -static std::wstring BuildWorkingDirAssetPath(const wchar_t* relativePath); -static bool FileExists(const std::wstring& path); -static void StopBackgroundMusic(); -static void StartBackgroundMusic(); - -/** - * @brief 多媒体定时器回调,用于高频率请求致谢页动画刷新。 - */ -static void CALLBACK CreditTimerCallback(UINT, UINT, DWORD_PTR userData, DWORD_PTR, DWORD_PTR) -{ - HWND hWnd = reinterpret_cast(userData); - if (hWnd != nullptr) - { - PostMessage(hWnd, WM_CREDIT_TICK, 0, 0); - } -} - -/** - * @brief 将指定滚动偏移按步长调整,并限制在非负范围内。 - */ -static void AdjustScrollOffset(int& scrollOffset, int delta) -{ - scrollOffset += delta; - if (scrollOffset < 0) - { - scrollOffset = 0; - } - if (scrollOffset > 2400) - { - scrollOffset = 2400; - } -} - -/** - * @brief 按当前窗口缩放返回一次滚动操作的像素距离。 - */ -static int GetScrollStep(HWND hWnd, int baseStep) -{ - 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; - } - - return MulDiv(baseStep, scale, 1000); -} - -struct LayoutMetrics -{ - int scale; - int offsetX; - int offsetY; - int layoutWidth; - int layoutHeight; - int grid; -}; - -static 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; -} - -static int ScaleValue(const LayoutMetrics& metrics, int value) -{ - return MulDiv(value, metrics.scale, 1000); -} - -static int ScaleXValue(const LayoutMetrics& metrics, int value) -{ - return metrics.offsetX + MulDiv(value, metrics.scale, 1000); -} - -static 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; -} - -static 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; -} - -static 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; -} - -static 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; -} - -static 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 获取致谢页左右切换按钮的绘制和点击区域。 - */ -static 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; -} - -static 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; -} - -static 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; -} - -static RECT GetBackButtonRect(HWND hWnd) -{ - LayoutMetrics metrics = GetLayoutMetrics(hWnd); - RECT rect = - { - ScaleXValue(metrics, 6), - ScaleYValue(metrics, 6), - ScaleXValue(metrics, 34), - ScaleYValue(metrics, 34) - }; - return rect; -} - -static void ResetGameTimer(HWND hWnd) -{ - KillTimer(hWnd, GAME_TIMER_ID); - SetTimer(hWnd, GAME_TIMER_ID, currentFallInterval > 0 ? currentFallInterval : GAME_TIMER_INTERVAL, nullptr); -} - -static 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(); - } - - auto tryPlayWithMci = [&](bool forceMpegVideo) -> bool - { - 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) - { - return false; - } - - 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); - return playResult == 0; - }; - - bool played = tryPlayWithMci(true); - if (!played) - { - played = tryPlayWithMci(false); - } - - 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; -} - -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 bool FileExists(const std::wstring& path) -{ - DWORD attributes = GetFileAttributesW(path.c_str()); - return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) == 0; -} - -static 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; -} - -static bool IsPointInRect(const RECT& rect, int x, int y) -{ - return x >= rect.left && x < rect.right && y >= rect.top && y < rect.bottom; -} - -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; -} - -static 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; -} - -static 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; -} - -static void ToggleBackgroundMusic(HWND hWnd) -{ - bgmEnabled = !bgmEnabled; - if (bgmEnabled) - { - StartBackgroundMusic(); - } - else - { - StopBackgroundMusic(); - } - InvalidateRect(hWnd, nullptr, FALSE); -} int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, @@ -748,18 +121,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) srand((unsigned int)time(nullptr)); ReturnToMainMenu(); StartBackgroundMusic(); - ResetGameTimer(hWnd); - SetTimer(hWnd, EFFECT_TIMER_ID, EFFECT_TIMER_INTERVAL, nullptr); - creditTimerHandle = timeSetEvent( - CREDIT_TIMER_INTERVAL, - 1, - CreditTimerCallback, - reinterpret_cast(hWnd), - TIME_PERIODIC | TIME_CALLBACK_FUNCTION); - if (creditTimerHandle == 0) - { - SetTimer(hWnd, CREDIT_TIMER_ID, CREDIT_TIMER_INTERVAL, nullptr); - } + StartAppTimers(hWnd); InvalidateRect(hWnd, nullptr, FALSE); break; case WM_COMMAND: @@ -784,899 +146,25 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) } break; case WM_CREDIT_TICK: - if (currentScreen == SCREEN_RULES && helpState.currentPage == 4 && TickCreditAnimation()) - { - InvalidateRect(hWnd, nullptr, FALSE); - } + HandleCreditTick(hWnd); break; case WM_TIMER: - if (wParam == EFFECT_TIMER_ID) - { - if (TickVisualEffects()) - { - InvalidateRect(hWnd, nullptr, FALSE); - } - break; - } - if (wParam == CREDIT_TIMER_ID && creditTimerHandle == 0) - { - if (currentScreen == SCREEN_RULES && helpState.currentPage == 4 && TickCreditAnimation()) - { - InvalidateRect(hWnd, nullptr, FALSE); - } - break; - } - - if (wParam == GAME_TIMER_ID) - { - bool shouldRefresh = false; - - if (feedbackState.visibleTicks > 0) - { - feedbackState.visibleTicks--; - shouldRefresh = true; - } - - if (IsRogueSkillDemoMode()) - { - if (TickRogueSkillDemo()) - { - shouldRefresh = true; - } - } - - 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.extremePlayerLevel > 0 && - currentScreen == SCREEN_PLAYING && - !suspendFlag && - !gameOverFlag) - { - if (rogueStats.extremeDangerTicks > 0) - { - rogueStats.extremeDangerTicks--; - } - else - { - rogueStats.extremeDangerTicks = 30; - if (rogueStats.extremeDangerLevel < 5) - { - rogueStats.extremeDangerLevel++; - } - currentFallInterval = GetRogueFallInterval(); - ResetGameTimer(hWnd); - feedbackState.visibleTicks = 10; - lstrcpyn(feedbackState.title, _T("极限压力升高"), sizeof(feedbackState.title) / sizeof(TCHAR)); - lstrcpyn(feedbackState.detail, _T("30 秒内没有完成四消,危险等级提升,下落速度进一步加快。"), sizeof(feedbackState.detail) / sizeof(TCHAR)); - shouldRefresh = true; - } - } - - if (currentMode == MODE_ROGUE && !IsRogueSkillDemoMode() && rogueStats.holdSlowTicks > 0) - { - rogueStats.holdSlowTicks--; - currentFallInterval = GetRogueFallInterval(); - ResetGameTimer(hWnd); - shouldRefresh = true; - } - - if (currentScreen == SCREEN_PLAYING && - !suspendFlag && - !gameOverFlag) - { - if (currentMode == MODE_ROGUE && !IsRogueSkillDemoMode()) - { - int previousFallInterval = currentFallInterval; - AdvanceRogueDifficulty(currentFallInterval > 0 ? currentFallInterval : GAME_TIMER_INTERVAL); - if (currentFallInterval != previousFallInterval) - { - ResetGameTimer(hWnd); - } - } - - if (currentMode == MODE_ROGUE && !IsRogueSkillDemoMode() && rogueStats.timeDilationLevel > 0 && rogueStats.timeDilationTicks <= 0) - { - 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) - { - rogueStats.timeDilationTicks = 8; - currentFallInterval = GetRogueFallInterval(); - ResetGameTimer(hWnd); - feedbackState.visibleTicks = 10; - lstrcpyn(feedbackState.title, _T("\u65f6\u95f4\u7f13\u6d41"), sizeof(feedbackState.title) / sizeof(TCHAR)); - lstrcpyn(feedbackState.detail, _T("堆叠高度超过 15 行,接下来 8 秒下落速度降低 30%。"), sizeof(feedbackState.detail) / sizeof(TCHAR)); - shouldRefresh = true; - } - } - - if (CanMoveDown()) - { - MoveDown(); - } - else - { - Fixing(); - if (!gameOverFlag) - { - DeleteLines(); - CheckRogueLevelProgress(); - } - } - - if (!gameOverFlag) - { - ComputeTarget(); - } - - shouldRefresh = true; - } - - if (shouldRefresh) - { - InvalidateRect(hWnd, nullptr, FALSE); - } - } + HandleTimerMessage(hWnd, wParam); break; case WM_SIZE: InvalidateRect(hWnd, nullptr, FALSE); break; case WM_LBUTTONUP: - { - int mouseX = static_cast(LOWORD(lParam)); - int mouseY = static_cast(HIWORD(lParam)); - RECT musicButtonRect = GetMusicButtonRect(hWnd); - if (IsPointInRect(musicButtonRect, mouseX, mouseY)) + if (!HandleMouseClick(hWnd, lParam)) { - ToggleBackgroundMusic(hWnd); - break; - } - - if (currentScreen != SCREEN_MENU && IsPointInRect(GetBackButtonRect(hWnd), mouseX, mouseY)) - { - 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); - break; - } - - if (currentScreen == SCREEN_MENU) - { - for (int i = 0; i < menuState.optionCount; i++) - { - if (!IsPointInRect(GetMenuOptionRect(hWnd, i), mouseX, mouseY)) - { - continue; - } - - menuState.selectedIndex = i; - if (i == 0) - { - StartGameWithMode(MODE_CLASSIC); - } - else if (i == 1) - { - StartGameWithMode(MODE_ROGUE); - } - else if (i == 2) - { - OpenRulesScreen(); - } - else - { - OpenCreditScreen(); - } - ResetGameTimer(hWnd); - InvalidateRect(hWnd, nullptr, FALSE); - break; - } - break; - } - - if (currentScreen == SCREEN_RULES) - { - 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); - break; - } - } - if (IsPointInRect(GetHelpBackHintRect(hWnd), mouseX, mouseY)) - { - ReturnToMainMenu(); - InvalidateRect(hWnd, nullptr, FALSE); - } - } - else if (helpState.currentPage == 5) - { - if (IsPointInRect(GetHelpBackHintRect(hWnd), mouseX, mouseY)) - { - helpState.currentPage = 0; - helpState.selectedIndex = 3; - helpScrollOffset = 0; - InvalidateRect(hWnd, nullptr, FALSE); - } - else - { - 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); - break; - } - } - } - } - else 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); - } - break; - } - - if (currentScreen == SCREEN_UPGRADE) - { - 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); - break; - } - break; - } - - if (currentScreen == SCREEN_PLAYING && suspendFlag) - { - if (IsPointInRect(GetOverlayButtonRect(hWnd, 0, 2), mouseX, mouseY)) - { - suspendFlag = false; - InvalidateRect(hWnd, nullptr, FALSE); - break; - } - if (IsPointInRect(GetOverlayButtonRect(hWnd, 1, 2), mouseX, mouseY)) - { - ReturnToMainMenu(); - InvalidateRect(hWnd, nullptr, FALSE); - break; - } - } - - 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); - break; - } - if (IsPointInRect(GetOverlayButtonRect(hWnd, 1, 3), mouseX, mouseY)) - { - StartGameWithMode(currentMode); - ResetGameTimer(hWnd); - InvalidateRect(hWnd, nullptr, FALSE); - break; - } - if (IsPointInRect(GetOverlayButtonRect(hWnd, 2, 3), mouseX, mouseY)) - { - ReturnToMainMenu(); - InvalidateRect(hWnd, nullptr, FALSE); - break; - } - } - else - { - if (IsPointInRect(GetOverlayButtonRect(hWnd, 0, 2), mouseX, mouseY)) - { - StartGameWithMode(currentMode); - ResetGameTimer(hWnd); - InvalidateRect(hWnd, nullptr, FALSE); - break; - } - if (IsPointInRect(GetOverlayButtonRect(hWnd, 1, 2), mouseX, mouseY)) - { - ReturnToMainMenu(); - InvalidateRect(hWnd, nullptr, FALSE); - break; - } - } - } - - return DefWindowProc(hWnd, message, wParam, lParam); - } - case WM_MOUSEWHEEL: - { - 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); - break; - } - if (currentScreen == SCREEN_PLAYING && currentMode == MODE_ROGUE) - { - AdjustScrollOffset(upgradeListScrollOffset, direction * scrollStep); - InvalidateRect(hWnd, nullptr, FALSE); - break; + return DefWindowProc(hWnd, message, wParam, lParam); } break; - } + case WM_MOUSEWHEEL: + HandleMouseWheel(hWnd, wParam); + break; case WM_KEYDOWN: - if (currentScreen == SCREEN_MENU) - { - switch (wParam) - { - 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: - 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); - break; - case VK_ESCAPE: - DestroyWindow(hWnd); - break; - default: - break; - } - break; - } - - if (currentScreen == SCREEN_RULES) - { - switch (wParam) - { - 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': - if (helpState.currentPage == 0) - { - ReturnToMainMenu(); - } - else if (helpState.currentPage == 4) - { - helpState.currentPage = 0; - helpState.selectedIndex = 3; - helpScrollOffset = 0; - } - else if (helpState.currentPage == 5) - { - helpState.currentPage = 0; - helpState.selectedIndex = 3; - helpScrollOffset = 0; - } - else - { - helpState.currentPage = 0; - helpScrollOffset = 0; - } - InvalidateRect(hWnd, nullptr, FALSE); - break; - default: - break; - } - break; - } - - if (currentScreen == SCREEN_UPGRADE) - { - int upgradeColumnCount = upgradeUiState.optionCount <= 3 ? upgradeUiState.optionCount : 3; - if (upgradeColumnCount < 1) - { - upgradeColumnCount = 1; - } - - switch (wParam) - { - 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; - } - - if (upgradeUiState.selectedIndex < rowEnd) - { - upgradeUiState.selectedIndex++; - } - else - { - upgradeUiState.selectedIndex = 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; - } - break; - } - - if (IsRogueSkillDemoMode()) - { - if (wParam == 'N') - { - AdvanceRogueSkillDemo(); - InvalidateRect(hWnd, nullptr, FALSE); - break; - } - else if (wParam == 'R') - { - RestartCurrentRogueSkillDemo(); - ResetGameTimer(hWnd); - InvalidateRect(hWnd, nullptr, FALSE); - break; - } - else if (wParam == VK_ESCAPE || wParam == VK_BACK || wParam == 'M') - { - OpenSkillDemoScreen(); - InvalidateRect(hWnd, nullptr, FALSE); - break; - } - } - - if (!IsRogueSkillDemoMode() && wParam == 'M') - { - ReturnToMainMenu(); - InvalidateRect(hWnd, nullptr, FALSE); - break; - } - - if (!IsRogueSkillDemoMode() && wParam == 'R') - { - StartGameWithMode(currentMode); - ResetGameTimer(hWnd); - InvalidateRect(hWnd, nullptr, FALSE); - break; - } - - if (!IsRogueSkillDemoMode() && wParam == 'P') - { - suspendFlag = !suspendFlag; - InvalidateRect(hWnd, nullptr, FALSE); - break; - } - - if (wParam == 'G') - { - targetFlag = !targetFlag; - InvalidateRect(hWnd, nullptr, FALSE); - break; - } - - if (gameOverFlag && reviveAvailable && wParam == 'V') - { - if (PlayReviveVideo(hWnd)) - { - ReviveAfterVideo(); - ResetGameTimer(hWnd); - } - else - { - SetFeedbackMessage(_T("视频播放失败"), _T("无法打开复活视频,复活机会未消耗。"), 14); - } - InvalidateRect(hWnd, nullptr, FALSE); - break; - } - - if (gameOverFlag || suspendFlag) - { - break; - } - - if (currentMode == MODE_ROGUE && (wParam == 'J' || wParam == 'K')) - { - int direction = (wParam == 'J') ? 1 : -1; - AdjustScrollOffset(upgradeListScrollOffset, direction * GetScrollStep(hWnd, 52)); - InvalidateRect(hWnd, nullptr, FALSE); - break; - } - - switch (wParam) - { - 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); + HandleKeyDown(hWnd, wParam); break; case WM_ERASEBKGND: return 1; @@ -1711,17 +199,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) } break; case WM_DESTROY: - KillTimer(hWnd, GAME_TIMER_ID); - KillTimer(hWnd, EFFECT_TIMER_ID); - if (creditTimerHandle != 0) - { - timeKillEvent(creditTimerHandle); - creditTimerHandle = 0; - } - else - { - KillTimer(hWnd, CREDIT_TIMER_ID); - } + StopAppTimers(hWnd); StopBackgroundMusic(); timeEndPeriod(1); PostQuitMessage(0); @@ -1754,3 +232,5 @@ INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) return (INT_PTR)FALSE; } + + diff --git a/src/source/TetrisLogic.cpp b/src/source/TetrisLogic.cpp index 3fd2b55..45e308a 100644 --- a/src/source/TetrisLogic.cpp +++ b/src/source/TetrisLogic.cpp @@ -1,4 +1,4 @@ -#include "stdafx.h" +#include "stdafx.h" #include "Tetris.h" #include "TetrisLogicInternal.h" @@ -431,66 +431,7 @@ void Fixing() } } - 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); - } + ApplyRainbowLandingEffect(overflowTop, fixedCells, fixedCellCount); if (overflowTop) { @@ -531,129 +472,7 @@ void Fixing() } } - 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) { @@ -876,3 +695,4 @@ void Restart() ComputeTarget(); } + diff --git a/src/source/TetrisRender.cpp b/src/source/TetrisRender.cpp index b439c24..29a4be3 100644 --- a/src/source/TetrisRender.cpp +++ b/src/source/TetrisRender.cpp @@ -1,8 +1,8 @@ -#include "stdafx.h" +#include "stdafx.h" #include "Tetris.h" +#include "TetrisRenderInternal.h" #include #include -#include #pragma comment(lib, "gdiplus.lib") @@ -34,169 +34,6 @@ 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 按序号加载致谢页图片资源。 - */ -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; @@ -2855,3 +2692,4 @@ void TDrawScreen(HDC hdc, HWND hWnd) DeleteObject(bodyFont); DeleteObject(smallFont); } + diff --git a/src/source/app/TetrisInput.cpp b/src/source/app/TetrisInput.cpp new file mode 100644 index 0000000..bb2679b --- /dev/null +++ b/src/source/app/TetrisInput.cpp @@ -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(LOWORD(lParam)); + int mouseY = static_cast(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); +} diff --git a/src/source/app/TetrisLayout.cpp b/src/source/app/TetrisLayout.cpp new file mode 100644 index 0000000..7027e43 --- /dev/null +++ b/src/source/app/TetrisLayout.cpp @@ -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; +} diff --git a/src/source/app/TetrisMedia.cpp b/src/source/app/TetrisMedia.cpp new file mode 100644 index 0000000..becc922 --- /dev/null +++ b/src/source/app/TetrisMedia.cpp @@ -0,0 +1,229 @@ +#include "stdafx.h" +#include "Tetris.h" +#include "TetrisAppInternal.h" +#include "TetrisAssets.h" +#include +#include + +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; +} diff --git a/src/source/app/TetrisTimers.cpp b/src/source/app/TetrisTimers.cpp new file mode 100644 index 0000000..86281ee --- /dev/null +++ b/src/source/app/TetrisTimers.cpp @@ -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(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(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); + } +} diff --git a/src/source/common/TetrisAssets.cpp b/src/source/common/TetrisAssets.cpp new file mode 100644 index 0000000..1ab196f --- /dev/null +++ b/src/source/common/TetrisAssets.cpp @@ -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; +} diff --git a/src/source/TetrisLogicInnovation.cpp b/src/source/extensions/TetrisGameExtensions.cpp similarity index 100% rename from src/source/TetrisLogicInnovation.cpp rename to src/source/extensions/TetrisGameExtensions.cpp diff --git a/src/source/logic/TetrisPieceEffects.cpp b/src/source/logic/TetrisPieceEffects.cpp new file mode 100644 index 0000000..72aee6a --- /dev/null +++ b/src/source/logic/TetrisPieceEffects.cpp @@ -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(); +} diff --git a/src/source/render/TetrisRenderAssets.cpp b/src/source/render/TetrisRenderAssets.cpp new file mode 100644 index 0000000..8d42f7f --- /dev/null +++ b/src/source/render/TetrisRenderAssets.cpp @@ -0,0 +1,138 @@ +#include "stdafx.h" +#include "TetrisRenderInternal.h" +#include "TetrisAssets.h" +#include +#include + +#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]; +} diff --git a/src/source/TetrisRogue.cpp b/src/source/rogue/TetrisRogue.cpp similarity index 100% rename from src/source/TetrisRogue.cpp rename to src/source/rogue/TetrisRogue.cpp