#include "stdafx.h" #include "Tetris.h" #include #define MAX_LOADSTRING 100 #define GAME_TIMER_ID 1 #define EFFECT_TIMER_ID 2 #define GAME_TIMER_INTERVAL 500 #define EFFECT_TIMER_INTERVAL 33 HINSTANCE hInst; TCHAR szTitle[MAX_LOADSTRING]; TCHAR szWindowClass[MAX_LOADSTRING]; bool bgmEnabled = true; static bool bgmPlaying = false; static bool bgmUsingMci = false; static constexpr const wchar_t* kBgmAlias = L"TereisBgm"; ATOM MyRegisterClass(HINSTANCE hInstance); BOOL InitInstance(HINSTANCE, int); LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); INT_PTR CALLBACK About(HWND, UINT, WPARAM, LPARAM); static void ResetGameTimer(HWND hWnd) { KillTimer(hWnd, GAME_TIMER_ID); SetTimer(hWnd, GAME_TIMER_ID, currentFallInterval > 0 ? currentFallInterval : GAME_TIMER_INTERVAL, nullptr); } 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) { 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; } int layoutWidth = MulDiv(WINDOW_CLIENT_WIDTH, scale, 1000); int offsetX = (clientWidth - layoutWidth) / 2; int offsetY = 0; int size = MulDiv(28, scale, 1000); if (size < 22) { size = 22; } int marginRight = MulDiv(12, scale, 1000); if (marginRight < 6) { marginRight = 6; } int marginBottom = MulDiv(12, scale, 1000); if (marginBottom < 6) { marginBottom = 6; } RECT buttonRect = { offsetX + layoutWidth - marginRight - size, offsetY + MulDiv(WINDOW_CLIENT_HEIGHT, scale, 1000) - marginBottom - size, offsetX + layoutWidth - marginRight, offsetY + MulDiv(WINDOW_CLIENT_HEIGHT, scale, 1000) - 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: srand((unsigned int)time(nullptr)); ReturnToMainMenu(); StartBackgroundMusic(); ResetGameTimer(hWnd); SetTimer(hWnd, EFFECT_TIMER_ID, EFFECT_TIMER_INTERVAL, nullptr); InvalidateRect(hWnd, nullptr, FALSE); break; case WM_COMMAND: { int wmId = LOWORD(wParam); switch (wmId) { 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_TIMER: if (wParam == EFFECT_TIMER_ID) { if (TickVisualEffects()) { InvalidateRect(hWnd, nullptr, FALSE); } break; } if (wParam == GAME_TIMER_ID) { bool shouldRefresh = false; if (feedbackState.visibleTicks > 0) { feedbackState.visibleTicks--; shouldRefresh = true; } if (currentMode == MODE_ROGUE && rogueStats.feverTicks > 0) { rogueStats.feverTicks--; currentFallInterval = GetRogueFallInterval(); ResetGameTimer(hWnd); shouldRefresh = true; } if (currentMode == MODE_ROGUE && rogueStats.timeDilationTicks > 0 && currentScreen == SCREEN_PLAYING && !suspendFlag && !gameOverFlag) { rogueStats.timeDilationTicks--; currentFallInterval = GetRogueFallInterval(); ResetGameTimer(hWnd); shouldRefresh = true; } if (currentMode == MODE_ROGUE && rogueStats.extremeSlowTicks > 0) { rogueStats.extremeSlowTicks--; currentFallInterval = GetRogueFallInterval(); ResetGameTimer(hWnd); shouldRefresh = true; } if (currentMode == MODE_ROGUE && 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 && rogueStats.holdSlowTicks > 0) { rogueStats.holdSlowTicks--; currentFallInterval = GetRogueFallInterval(); ResetGameTimer(hWnd); shouldRefresh = true; } if (currentScreen == SCREEN_PLAYING && !suspendFlag && !gameOverFlag) { if (currentMode == MODE_ROGUE) { int previousFallInterval = currentFallInterval; AdvanceRogueDifficulty(currentFallInterval > 0 ? currentFallInterval : GAME_TIMER_INTERVAL); if (currentFallInterval != previousFallInterval) { ResetGameTimer(hWnd); } } if (currentMode == MODE_ROGUE && 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(); } } 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; } return DefWindowProc(hWnd, message, wParam, lParam); } 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 { OpenRulesScreen(); } ResetGameTimer(hWnd); InvalidateRect(hWnd, nullptr, FALSE); break; case VK_ESCAPE: DestroyWindow(hWnd); break; default: break; } break; } if (currentScreen == SCREEN_RULES) { switch (wParam) { case VK_ESCAPE: case VK_BACK: case 'M': ReturnToMainMenu(); 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: case VK_SPACE: ConfirmUpgradeSelection(); ResetGameTimer(hWnd); InvalidateRect(hWnd, nullptr, FALSE); break; default: break; } break; } if (wParam == 'M') { ReturnToMainMenu(); InvalidateRect(hWnd, nullptr, FALSE); break; } if (wParam == 'R') { StartGameWithMode(currentMode); ResetGameTimer(hWnd); InvalidateRect(hWnd, nullptr, FALSE); break; } if (wParam == 'P') { suspendFlag = !suspendFlag; InvalidateRect(hWnd, nullptr, FALSE); break; } if (wParam == 'G') { targetFlag = !targetFlag; InvalidateRect(hWnd, nullptr, FALSE); break; } if (gameOverFlag || suspendFlag) { 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(); } } break; case VK_UP: case 'W': Rotate(); break; case VK_SPACE: DropDown(); Fixing(); if (!gameOverFlag) { DeleteLines(); } break; case 'C': case VK_SHIFT: 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); StopBackgroundMusic(); 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; }