#include "stdafx.h" #include "Tetris.h" #include #include #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, LPTSTR lpCmdLine, int nCmdShow) { UNREFERENCED_PARAMETER(hPrevInstance); UNREFERENCED_PARAMETER(lpCmdLine); HMODULE user32Module = GetModuleHandle(_T("user32.dll")); if (user32Module != nullptr) { typedef BOOL(WINAPI* SetProcessDPIAwareFunc)(); SetProcessDPIAwareFunc setProcessDPIAware = (SetProcessDPIAwareFunc)GetProcAddress(user32Module, "SetProcessDPIAware"); if (setProcessDPIAware != nullptr) { setProcessDPIAware(); } } LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING); LoadString(hInstance, IDC_TETRIS, szWindowClass, MAX_LOADSTRING); lstrcpy(szTitle, _T("\u4fc4\u7f57\u65af\u65b9\u5757")); MyRegisterClass(hInstance); if (!InitInstance(hInstance, nCmdShow)) { return FALSE; } HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_TETRIS)); MSG msg; while (GetMessage(&msg, nullptr, 0, 0)) { if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)) { TranslateMessage(&msg); DispatchMessage(&msg); } } return (int)msg.wParam; } ATOM MyRegisterClass(HINSTANCE hInstance) { WNDCLASSEX wcex; wcex.cbSize = sizeof(WNDCLASSEX); wcex.style = CS_HREDRAW | CS_VREDRAW; wcex.lpfnWndProc = WndProc; wcex.cbClsExtra = 0; wcex.cbWndExtra = 0; wcex.hInstance = hInstance; wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_TETRIS)); wcex.hCursor = LoadCursor(nullptr, IDC_ARROW); wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); wcex.lpszMenuName = nullptr; wcex.lpszClassName = szWindowClass; wcex.hIconSm = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_SMALL)); return RegisterClassEx(&wcex); } BOOL InitInstance(HINSTANCE hInstance, int nCmdShow) { RECT rect = { 0, 0, WINDOW_CLIENT_WIDTH, WINDOW_CLIENT_HEIGHT }; AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, FALSE); hInst = hInstance; HWND hWnd = CreateWindow( szWindowClass, szTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, rect.right - rect.left, rect.bottom - rect.top, nullptr, nullptr, hInstance, nullptr); if (!hWnd) { return FALSE; } ShowWindow(hWnd, nCmdShow); UpdateWindow(hWnd); return TRUE; } 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: { int wmId = LOWORD(wParam); switch (wmId) { case IDM_SKILL_DEMO: OpenSkillDemoScreen(); InvalidateRect(hWnd, nullptr, FALSE); break; case IDM_ABOUT: DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About); break; case IDM_EXIT: DestroyWindow(hWnd); break; default: return DefWindowProc(hWnd, message, wParam, 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) { 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); } } 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)) { ToggleBackgroundMusic(hWnd); break; } if (currentScreen != SCREEN_MENU && IsPointInRect(GetBackButtonRect(hWnd), mouseX, mouseY)) { 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)) { 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; } 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; } 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) { ReturnToMainMenu(); } 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') { ReturnToMainMenu(); 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); break; case WM_ERASEBKGND: return 1; case WM_PAINT: { PAINTSTRUCT ps; HDC hdc = BeginPaint(hWnd, &ps); RECT clientRect; GetClientRect(hWnd, &clientRect); HDC memDC = CreateCompatibleDC(hdc); HBITMAP memBitmap = CreateCompatibleBitmap( hdc, clientRect.right - clientRect.left, clientRect.bottom - clientRect.top); HBITMAP oldBitmap = (HBITMAP)SelectObject(memDC, memBitmap); TDrawScreen(memDC, hWnd); BitBlt( hdc, 0, 0, clientRect.right - clientRect.left, clientRect.bottom - clientRect.top, memDC, 0, 0, SRCCOPY); SelectObject(memDC, oldBitmap); DeleteObject(memBitmap); DeleteDC(memDC); EndPaint(hWnd, &ps); } 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); } StopBackgroundMusic(); timeEndPeriod(1); PostQuitMessage(0); break; default: return DefWindowProc(hWnd, message, wParam, lParam); } return 0; } INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) { UNREFERENCED_PARAMETER(lParam); switch (message) { case WM_INITDIALOG: SetWindowText(hDlg, _T("\u5173\u4e8e\u4fc4\u7f57\u65af\u65b9\u5757")); return (INT_PTR)TRUE; case WM_COMMAND: if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL) { EndDialog(hDlg, LOWORD(wParam)); return (INT_PTR)TRUE; } break; } return (INT_PTR)FALSE; }