diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..2f905da --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,40 @@ +# AGENTS.md + +## 项目名称 +使用大模型辅助开发俄罗斯方块程序 + +## 基本开发要求 + +### 编程语言 +- 使用 C++。 +- 仅使用课程已学基础语法:数组、循环、分支、函数、结构体等。 +- 不使用 `class`、继承、多态等面向对象特性。 + +## 项目结构 + +- 源码主要位于 `src` 目录。 +- 不要随意改动已有全局变量、函数声明和文件结构。 +- 如需新增创新功能,可以新增 `.cpp` 文件。 + +## 构建方式 + +优先使用项目根目录下的构建脚本: + +```powershell +.\build-mingw.ps1 +``` + +## 代码质量要求 + +1. 每次只实现一个明确功能。 +2. 每个函数必须有功能描述注释。 +3. 变量命名保持和原框架一致。 +4. 不随意改动已有全局变量和函数声明。 +5. 生成代码后必须人工审查。 +6. 每个阶段完成后必须编译运行。 +7. 出现 bug 时,应记录问题、原因和修复过程。 + +## 开发注意事项 + +1. 每次补全前后都要保存版本,便于报告展示。 +2. 现场汇报时,所有组员都可能被提问,不能只有一人理解代码。 diff --git a/assets/images/qls.jpg b/assets/images/qls.jpg new file mode 100644 index 0000000..eedbb38 Binary files /dev/null and b/assets/images/qls.jpg differ diff --git a/assets/images/wyk.jpg b/assets/images/wyk.jpg new file mode 100644 index 0000000..a34fd59 Binary files /dev/null and b/assets/images/wyk.jpg differ diff --git a/src/include/Tetris.h b/src/include/Tetris.h index 0d9d0a0..8dd9f44 100644 --- a/src/include/Tetris.h +++ b/src/include/Tetris.h @@ -227,6 +227,9 @@ extern Point target; extern MenuState menuState; extern HelpState helpState; extern int helpScrollOffset; +extern int creditPageIndex; +extern int creditAnimationTicks; +extern int creditAnimationDirection; extern int upgradeListScrollOffset; extern PlayerStats classicStats; extern PlayerStats rogueStats; @@ -267,6 +270,8 @@ void ReturnToMainMenu(); void ReviveAfterVideo(); void SetFeedbackMessage(const TCHAR* title, const TCHAR* detail, int ticks); void OpenRulesScreen(); +void OpenCreditScreen(); +void ChangeCreditPage(int direction); void OpenUpgradeMenu(); void ConfirmUpgradeSelection(); void ResetUpgradeUiState(); @@ -277,6 +282,7 @@ void UseAirReshape(); void ResetPendingRogueVisualEvents(); void ResetVisualEffects(); bool TickVisualEffects(); +bool TickCreditAnimation(); void TriggerLineClearEffect(const int* rows, int rowCount, int linesCleared); void PlayPendingLineClearEffect(); void TriggerCellClearEffect(const Point* cells, int cellCount, bool strongBurst); diff --git a/src/source/Tetris.cpp b/src/source/Tetris.cpp index 9561cc4..90a63c1 100644 --- a/src/source/Tetris.cpp +++ b/src/source/Tetris.cpp @@ -6,8 +6,11 @@ #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]; @@ -16,6 +19,7 @@ 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"; @@ -29,6 +33,18 @@ 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 将指定滚动偏移按步长调整,并限制在非负范围内。 */ @@ -195,6 +211,29 @@ static RECT GetHelpBackHintRect(HWND hWnd) 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); @@ -681,11 +720,22 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) switch (message) { case WM_CREATE: + timeBeginPeriod(1); 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); + } InvalidateRect(hWnd, nullptr, FALSE); break; case WM_COMMAND: @@ -705,6 +755,12 @@ 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); + } + break; case WM_TIMER: if (wParam == EFFECT_TIMER_ID) { @@ -714,6 +770,14 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) } 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) { @@ -902,10 +966,14 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { StartGameWithMode(MODE_ROGUE); } - else + else if (i == 2) { OpenRulesScreen(); } + else + { + OpenCreditScreen(); + } ResetGameTimer(hWnd); InvalidateRect(hWnd, nullptr, FALSE); break; @@ -936,8 +1004,25 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) } else if (IsPointInRect(GetHelpBackHintRect(hWnd), mouseX, mouseY)) { - helpState.currentPage = 0; - helpScrollOffset = 0; + if (helpState.currentPage == 4) + { + ReturnToMainMenu(); + } + else + { + 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; @@ -1108,10 +1193,14 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { StartGameWithMode(MODE_ROGUE); } - else + else if (menuState.selectedIndex == 2) { OpenRulesScreen(); } + else + { + OpenCreditScreen(); + } ResetGameTimer(hWnd); InvalidateRect(hWnd, nullptr, FALSE); break; @@ -1141,6 +1230,11 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) } InvalidateRect(hWnd, nullptr, FALSE); } + else if (helpState.currentPage == 4) + { + ChangeCreditPage(-1); + InvalidateRect(hWnd, nullptr, FALSE); + } break; case VK_DOWN: case VK_RIGHT: @@ -1155,6 +1249,11 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) } InvalidateRect(hWnd, nullptr, FALSE); } + else if (helpState.currentPage == 4) + { + ChangeCreditPage(1); + InvalidateRect(hWnd, nullptr, FALSE); + } break; case VK_RETURN: case VK_SPACE: @@ -1172,6 +1271,10 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { ReturnToMainMenu(); } + else if (helpState.currentPage == 4) + { + ReturnToMainMenu(); + } else { helpState.currentPage = 0; @@ -1454,7 +1557,17 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) 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); + } StopBackgroundMusic(); + timeEndPeriod(1); PostQuitMessage(0); break; default: diff --git a/src/source/TetrisLogic.cpp b/src/source/TetrisLogic.cpp index 1f40d9f..dbeb5bf 100644 --- a/src/source/TetrisLogic.cpp +++ b/src/source/TetrisLogic.cpp @@ -13,9 +13,12 @@ bool reviveAvailable = false; int workRegion[20][10] = { 0 }; Point point = { 0, 0 }; Point target = { 0, 0 }; -MenuState menuState = { 0, 2 }; +MenuState menuState = { 0, 4 }; HelpState helpState = { 0, 3, 0 }; int helpScrollOffset = 0; +int creditPageIndex = 0; +int creditAnimationTicks = 0; +int creditAnimationDirection = 0; int upgradeListScrollOffset = 0; PlayerStats classicStats = { 0, 1, 0, 0, 0 }; PlayerStats rogueStats = { 0, 1, 0, 30, 0, 100, 100, 0 }; diff --git a/src/source/TetrisLogicInnovation.cpp b/src/source/TetrisLogicInnovation.cpp index a543caa..d45be8d 100644 --- a/src/source/TetrisLogicInnovation.cpp +++ b/src/source/TetrisLogicInnovation.cpp @@ -161,6 +161,20 @@ bool TickVisualEffects() return active; } +/** + * @brief 推进致谢页左右切换动画,并返回是否需要刷新界面。 + */ +bool TickCreditAnimation() +{ + if (creditAnimationTicks > 0) + { + creditAnimationTicks--; + return true; + } + + return false; +} + /** * @brief 添加一段棋盘坐标系中的浮动文字效果。 */ @@ -523,11 +537,14 @@ void ReturnToMainMenu() ResetVisualEffects(); ResetPendingRogueVisualEvents(); helpScrollOffset = 0; + creditPageIndex = 0; + creditAnimationTicks = 0; + creditAnimationDirection = 0; upgradeListScrollOffset = 0; pendingLineClearEffectTicks = 0; pendingLineClearEffectRowCount = 0; pendingLineClearEffectLineCount = 0; - menuState.optionCount = 3; + menuState.optionCount = 4; ResetUpgradeUiState(); if (menuState.selectedIndex < 0 || menuState.selectedIndex >= menuState.optionCount) @@ -547,4 +564,60 @@ void OpenRulesScreen() helpState.optionCount = 3; helpState.currentPage = 0; helpScrollOffset = 0; + creditPageIndex = 0; + creditAnimationTicks = 0; + creditAnimationDirection = 0; +} + +/** + * @brief 打开致谢界面并重置致谢页切换状态。 + */ +void OpenCreditScreen() +{ + currentScreen = SCREEN_RULES; + suspendFlag = false; + helpState.selectedIndex = 0; + helpState.optionCount = 3; + helpState.currentPage = 4; + helpScrollOffset = 0; + creditPageIndex = 0; + creditAnimationTicks = 0; + creditAnimationDirection = 0; +} + +/** + * @brief 切换致谢页图片,并启动左右滑动动画。 + */ +void ChangeCreditPage(int direction) +{ + if (direction == 0) + { + return; + } + + int oldPageIndex = creditPageIndex; + if (direction > 0) + { + creditPageIndex++; + creditAnimationDirection = 1; + } + else + { + creditPageIndex--; + creditAnimationDirection = -1; + } + + if (creditPageIndex < 0) + { + creditPageIndex = 1; + } + if (creditPageIndex > 1) + { + creditPageIndex = 0; + } + + if (creditPageIndex != oldPageIndex) + { + creditAnimationTicks = 60; + } } diff --git a/src/source/TetrisRender.cpp b/src/source/TetrisRender.cpp index 224af09..bc390c7 100644 --- a/src/source/TetrisRender.cpp +++ b/src/source/TetrisRender.cpp @@ -126,6 +126,66 @@ static Bitmap* LoadBackgroundImage() return backgroundImage; } +/** + * @brief 按序号加载致谢页图片资源。 + */ +static Bitmap* LoadCreditImage(int index) +{ + static ULONG_PTR gdiplusToken = 0; + static Bitmap* creditImages[2] = {}; + static bool attempted[2] = {}; + + if (index < 0 || index >= 2) + { + return nullptr; + } + + if (!attempted[index]) + { + attempted[index] = true; + + GdiplusStartupInput startupInput; + if (GdiplusStartup(&gdiplusToken, &startupInput, nullptr) == Ok) + { + const wchar_t* imageNames[2] = + { + L"assets\\images\\qls.jpg", + L"assets\\images\\wyk.jpg" + }; + const std::wstring candidates[] = + { + BuildAssetPath(imageNames[index]), + BuildWorkingDirAssetPath(imageNames[index]) + }; + + 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) + { + creditImages[index] = loadedImage; + break; + } + + delete loadedImage; + } + } + } + + return creditImages[index]; +} + void TDrawScreen(HDC hdc, HWND hWnd) { RECT clientRect; @@ -383,12 +443,15 @@ void TDrawScreen(HDC hdc, HWND hWnd) SX(34), SY(34) }; - DrawPanelCardAlpha( - backButtonRect, - RGB(255, 242, 247), - RGB(222, 130, 166), - 12, - 214); + HBRUSH backBrush = CreateSolidBrush(RGB(255, 242, 247)); + HPEN backFramePen = CreatePen(PS_SOLID, 1, RGB(222, 130, 166)); + HBRUSH oldBackBrush = (HBRUSH)SelectObject(hdc, backBrush); + HPEN oldBackFramePen = (HPEN)SelectObject(hdc, backFramePen); + RoundRect(hdc, backButtonRect.left, backButtonRect.top, backButtonRect.right, backButtonRect.bottom, SS(12), SS(12)); + SelectObject(hdc, oldBackBrush); + SelectObject(hdc, oldBackFramePen); + DeleteObject(backFramePen); + DeleteObject(backBrush); HPEN backPen = CreatePen(PS_SOLID, SS(3), RGB(128, 70, 100)); HPEN oldBackPen = (HPEN)SelectObject(hdc, backPen); @@ -436,18 +499,20 @@ void TDrawScreen(HDC hdc, HWND hWnd) SetTextColor(hdc, textColor); DrawText(hdc, _T("选择你的战局"), -1, &subtitleRect, DT_CENTER | DT_VCENTER | DT_SINGLELINE); - const TCHAR* modeNames[3] = + const TCHAR* modeNames[4] = { _T("\u7ecf\u5178\u6a21\u5f0f"), _T("Rogue \u6a21\u5f0f"), - _T("\u5e2e\u52a9") + _T("\u5e2e\u52a9"), + _T("\u81f4\u8c22") }; - const TCHAR* modeDescriptions[3] = + const TCHAR* modeDescriptions[4] = { _T("纯粹方块挑战,专注消行、堆叠与生存。"), _T("在不断升级的棋盘中收集强化,构筑本局专属流派。"), - _T("查看操作、模式规则与全部强化效果。") + _T("查看操作、模式规则与全部强化效果。"), + _T("感谢程序测试者与代码贡献者。") }; for (int i = 0; i < menuState.optionCount; i++) @@ -568,6 +633,10 @@ void TDrawScreen(HDC hdc, HWND hWnd) { helpTitle = _T("\u5f3a\u5316\u56fe\u9274"); } + else if (helpState.currentPage == 4) + { + helpTitle = _T("\u81f4\u8c22"); + } DrawText(hdc, helpTitle, -1, &rulesTitleRect, DT_LEFT | DT_VCENTER | DT_SINGLELINE); HPEN rulesAccentPen = CreatePen(PS_SOLID, SS(3), accentColor); @@ -807,7 +876,198 @@ void TDrawScreen(HDC hdc, HWND hWnd) DeleteObject(contentClipRegion); DeleteObject(oldClipRegion); } - if (helpState.currentPage != 3) + else if (helpState.currentPage == 4) + { + const int creditAnimationTotalTicks = 60; + const TCHAR* creditNames[2] = + { + _T("qls"), + _T("wyk") + }; + const TCHAR* creditTexts[2] = + { + _T("\u611f\u8c22\u6fc0\u60c5\u6295\u8eab\u4e8e\u6d4b\u8bd5\u4e4b\u4e2d\u7684Lisa"), + _T("\u611f\u8c22\u70ed\u5ff1coding\u7684\u5c0f\u86cb\u7cd5") + }; + + int currentCredit = creditPageIndex; + if (currentCredit < 0) + { + currentCredit = 0; + } + if (currentCredit > 1) + { + currentCredit = 1; + } + + int previousCredit = currentCredit - creditAnimationDirection; + if (previousCredit < 0) + { + previousCredit = 1; + } + if (previousCredit > 1) + { + previousCredit = 0; + } + + RECT imageArea = + { + contentRect.left + SS(92), + contentRect.top + SS(10), + contentRect.right - SS(92), + contentRect.bottom - SS(112) + }; + RECT textArea = + { + contentRect.left + SS(72), + contentRect.bottom - SS(94), + contentRect.right - SS(72), + contentRect.bottom - SS(18) + }; + + HRGN oldClipRegion = CreateRectRgn(0, 0, 0, 0); + int hasOldClipRegion = GetClipRgn(hdc, oldClipRegion); + HRGN contentClipRegion = CreateRectRgn(contentRect.left, contentRect.top, contentRect.right, contentRect.bottom); + SelectClipRgn(hdc, contentClipRegion); + + int slideDistance = contentRect.right - contentRect.left; + int currentOffset = 0; + int previousOffset = 0; + if (creditAnimationTicks > 0 && creditAnimationDirection != 0) + { + currentOffset = creditAnimationDirection * slideDistance * creditAnimationTicks / creditAnimationTotalTicks; + previousOffset = currentOffset - creditAnimationDirection * slideDistance; + } + + Bitmap* preloadedCreditImageA = LoadCreditImage(0); + Bitmap* preloadedCreditImageB = LoadCreditImage(1); + Graphics creditGraphics(hdc); + creditGraphics.SetInterpolationMode(InterpolationModeHighQualityBilinear); + + auto DrawCreditCard = [&](int cardIndex, int offset) + { + RECT shiftedImageArea = imageArea; + RECT shiftedTextArea = textArea; + OffsetRect(&shiftedImageArea, offset, 0); + OffsetRect(&shiftedTextArea, offset, 0); + + Bitmap* creditImage = (cardIndex == 0) ? preloadedCreditImageA : preloadedCreditImageB; + if (creditImage != nullptr) + { + int imageWidth = static_cast(creditImage->GetWidth()); + int imageHeight = static_cast(creditImage->GetHeight()); + int areaWidth = shiftedImageArea.right - shiftedImageArea.left; + int areaHeight = shiftedImageArea.bottom - shiftedImageArea.top; + int drawWidth = areaWidth; + int drawHeight = imageHeight * drawWidth / imageWidth; + if (drawHeight > areaHeight) + { + drawHeight = areaHeight; + drawWidth = imageWidth * drawHeight / imageHeight; + } + + int drawLeft = shiftedImageArea.left + (areaWidth - drawWidth) / 2; + int drawTop = shiftedImageArea.top + (areaHeight - drawHeight) / 2; + creditGraphics.DrawImage(creditImage, Rect(drawLeft, drawTop, drawWidth, drawHeight)); + } + else + { + HBRUSH missingBrush = CreateSolidBrush(RGB(255, 245, 249)); + HPEN missingPen = CreatePen(PS_DASH, SS(2), frameColor); + oldBrush = (HBRUSH)SelectObject(hdc, missingBrush); + oldPen = (HPEN)SelectObject(hdc, missingPen); + RoundRect(hdc, shiftedImageArea.left, shiftedImageArea.top, shiftedImageArea.right, shiftedImageArea.bottom, SS(24), SS(24)); + SelectObject(hdc, oldBrush); + SelectObject(hdc, oldPen); + DeleteObject(missingPen); + DeleteObject(missingBrush); + + SetTextColor(hdc, RGB(128, 104, 118)); + SelectObject(hdc, bodyFont); + DrawText(hdc, _T("\u56fe\u7247\u8d44\u6e90\u672a\u627e\u5230"), -1, &shiftedImageArea, DT_CENTER | DT_VCENTER | DT_SINGLELINE); + } + + if (creditAnimationTicks <= 0) + { + SetTextColor(hdc, titleColor); + SelectObject(hdc, sectionFont); + RECT nameRect = + { + shiftedTextArea.left, + shiftedTextArea.top, + shiftedTextArea.right, + shiftedTextArea.top + SS(30) + }; + DrawText(hdc, creditNames[cardIndex], -1, &nameRect, DT_CENTER | DT_VCENTER | DT_SINGLELINE); + + SetTextColor(hdc, textColor); + SelectObject(hdc, bodyFont); + RECT detailRect = + { + shiftedTextArea.left, + shiftedTextArea.top + SS(34), + shiftedTextArea.right, + shiftedTextArea.bottom + }; + DrawText(hdc, creditTexts[cardIndex], -1, &detailRect, DT_CENTER | DT_TOP | DT_WORDBREAK); + } + }; + + if (creditAnimationTicks > 0 && creditAnimationDirection != 0) + { + DrawCreditCard(previousCredit, previousOffset); + } + DrawCreditCard(currentCredit, currentOffset); + + if (hasOldClipRegion == 1) + { + SelectClipRgn(hdc, oldClipRegion); + } + else + { + SelectClipRgn(hdc, nullptr); + } + DeleteObject(contentClipRegion); + DeleteObject(oldClipRegion); + + RECT leftArrow = + { + rulesCard.left + SS(52), + (rulesCard.top + rulesCard.bottom) / 2 - SS(27), + rulesCard.left + SS(106), + (rulesCard.top + rulesCard.bottom) / 2 + SS(27) + }; + RECT rightArrow = + { + rulesCard.right - SS(106), + (rulesCard.top + rulesCard.bottom) / 2 - SS(27), + rulesCard.right - SS(52), + (rulesCard.top + rulesCard.bottom) / 2 + SS(27) + }; + + HBRUSH arrowBrush = CreateSolidBrush(RGB(255, 245, 249)); + HPEN arrowPen = CreatePen(PS_SOLID, SS(2), accentColor); + oldBrush = (HBRUSH)SelectObject(hdc, arrowBrush); + oldPen = (HPEN)SelectObject(hdc, arrowPen); + RoundRect(hdc, leftArrow.left, leftArrow.top, leftArrow.right, leftArrow.bottom, SS(18), SS(18)); + RoundRect(hdc, rightArrow.left, rightArrow.top, rightArrow.right, rightArrow.bottom, SS(18), SS(18)); + SelectObject(hdc, oldBrush); + SelectObject(hdc, oldPen); + DeleteObject(arrowPen); + DeleteObject(arrowBrush); + + HPEN chevronPen = CreatePen(PS_SOLID, SS(4), titleColor); + oldPen = (HPEN)SelectObject(hdc, chevronPen); + MoveToEx(hdc, leftArrow.left + SS(32), leftArrow.top + SS(16), nullptr); + LineTo(hdc, leftArrow.left + SS(22), leftArrow.top + SS(27)); + LineTo(hdc, leftArrow.left + SS(32), leftArrow.bottom - SS(16)); + MoveToEx(hdc, rightArrow.right - SS(32), rightArrow.top + SS(16), nullptr); + LineTo(hdc, rightArrow.right - SS(22), rightArrow.top + SS(27)); + LineTo(hdc, rightArrow.right - SS(32), rightArrow.bottom - SS(16)); + SelectObject(hdc, oldPen); + DeleteObject(chevronPen); + } + if (helpState.currentPage != 3 && helpState.currentPage != 4) { RECT calculateRect = { contentRect.left, contentRect.top, contentRect.right, contentRect.top }; DrawText(hdc, pageText, -1, &calculateRect, pageFlags | DT_CALCRECT); @@ -855,9 +1115,12 @@ void TDrawScreen(HDC hdc, HWND hWnd) }; const TCHAR* helpHint = helpState.currentPage == 0 ? _T("\u65b9\u5411\u952e / WASD \u5207\u6362\uff0cEnter / Space \u786e\u8ba4\uff0cEsc / M \u8fd4\u56de\u4e3b\u83dc\u5355") - : _T("\u9f20\u6807\u6eda\u8f6e\u4e0a\u4e0b\u7ffb\u52a8\uff0cEsc / Backspace / M \u8fd4\u56de\u5e2e\u52a9"); + : (helpState.currentPage == 4 + ? _T("\u5de6\u53f3\u65b9\u5411\u952e / A D \u5207\u6362\uff0cEsc / Backspace / M \u8fd4\u56de\u4e3b\u83dc\u5355") + : _T("\u9f20\u6807\u6eda\u8f6e\u4e0a\u4e0b\u7ffb\u52a8\uff0cEsc / Backspace / M \u8fd4\u56de\u5e2e\u52a9")); DrawText(hdc, helpHint, -1, &backHintRect, DT_CENTER | DT_VCENTER | DT_SINGLELINE); + SelectClipRgn(hdc, nullptr); DrawBackButton(); DrawMusicButton(); SelectObject(hdc, oldFont);