diff --git a/src/include/Tetris.h b/src/include/Tetris.h index 2fa7de2..5715c3f 100644 --- a/src/include/Tetris.h +++ b/src/include/Tetris.h @@ -143,6 +143,35 @@ struct FeedbackState TCHAR detail[128]; }; +struct ClearEffectState +{ + int ticks; + int totalTicks; + int rowCount; + int rows[8]; +}; + +struct FloatingTextEffect +{ + int ticks; + int totalTicks; + int boardX; + int boardY; + TCHAR text[64]; + COLORREF color; +}; + +struct ParticleEffect +{ + int ticks; + int totalTicks; + int boardX; + int boardY; + int velocityX; + int velocityY; + COLORREF color; +}; + enum ScreenState { SCREEN_MENU = 0, @@ -180,6 +209,9 @@ extern PlayerStats classicStats; extern PlayerStats rogueStats; extern UpgradeUiState upgradeUiState; extern FeedbackState feedbackState; +extern ClearEffectState clearEffectState; +extern FloatingTextEffect floatingTextEffects[8]; +extern ParticleEffect particleEffects[24]; extern int currentScreen; extern int currentMode; extern int currentFallInterval; @@ -215,6 +247,9 @@ void HoldCurrentPiece(); void UseScreenBomb(); void UseBlackHole(); void UseAirReshape(); +void ResetVisualEffects(); +bool TickVisualEffects(); +void TriggerLineClearEffect(const int* rows, int rowCount, int linesCleared); int GetRogueFallInterval(); int GetRoguePlayableHeight(); int GetRogueLockedRows(); diff --git a/src/source/Tetris.cpp b/src/source/Tetris.cpp index b4840df..3a04a57 100644 --- a/src/source/Tetris.cpp +++ b/src/source/Tetris.cpp @@ -4,7 +4,9 @@ #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]; @@ -355,6 +357,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) ReturnToMainMenu(); StartBackgroundMusic(); ResetGameTimer(hWnd); + SetTimer(hWnd, EFFECT_TIMER_ID, EFFECT_TIMER_INTERVAL, nullptr); InvalidateRect(hWnd, nullptr, FALSE); break; case WM_COMMAND: @@ -375,6 +378,15 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM 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; @@ -823,6 +835,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); StopBackgroundMusic(); PostQuitMessage(0); break; diff --git a/src/source/TetrisLogic.cpp b/src/source/TetrisLogic.cpp index 55bec2d..cf25aae 100644 --- a/src/source/TetrisLogic.cpp +++ b/src/source/TetrisLogic.cpp @@ -17,6 +17,9 @@ PlayerStats classicStats = { 0, 1, 0, 0, 0 }; PlayerStats rogueStats = { 0, 1, 0, 30, 0, 100, 100, 0 }; UpgradeUiState upgradeUiState = { 0, 0, 0, 0, {} }; FeedbackState feedbackState = { 0, _T(""), _T("") }; +ClearEffectState clearEffectState = { 0, 0, 0, {} }; +FloatingTextEffect floatingTextEffects[8] = {}; +ParticleEffect particleEffects[24] = {}; int currentScreen = SCREEN_MENU; int currentMode = MODE_CLASSIC; int currentFallInterval = 500; @@ -239,6 +242,129 @@ void SetFeedbackMessage(const TCHAR* title, const TCHAR* detail, int ticks) lstrcpyn(feedbackState.detail, detail, sizeof(feedbackState.detail) / sizeof(TCHAR)); } +void ResetVisualEffects() +{ + clearEffectState.ticks = 0; + clearEffectState.totalTicks = 0; + clearEffectState.rowCount = 0; + + for (int i = 0; i < 8; i++) + { + floatingTextEffects[i].ticks = 0; + } + + for (int i = 0; i < 24; i++) + { + particleEffects[i].ticks = 0; + } +} + +bool TickVisualEffects() +{ + bool active = false; + + if (clearEffectState.ticks > 0) + { + clearEffectState.ticks--; + active = true; + } + + for (int i = 0; i < 8; i++) + { + if (floatingTextEffects[i].ticks > 0) + { + floatingTextEffects[i].ticks--; + active = true; + } + } + + for (int i = 0; i < 24; i++) + { + if (particleEffects[i].ticks > 0) + { + particleEffects[i].ticks--; + active = true; + } + } + + return active; +} + +static void AddFloatingText(int boardX, int boardY, const TCHAR* text, COLORREF color) +{ + for (int i = 0; i < 8; i++) + { + if (floatingTextEffects[i].ticks <= 0) + { + floatingTextEffects[i].ticks = 22; + floatingTextEffects[i].totalTicks = 22; + floatingTextEffects[i].boardX = boardX; + floatingTextEffects[i].boardY = boardY; + floatingTextEffects[i].color = color; + lstrcpyn(floatingTextEffects[i].text, text, sizeof(floatingTextEffects[i].text) / sizeof(TCHAR)); + return; + } + } +} + +static void AddParticle(int boardX, int boardY, COLORREF color) +{ + for (int i = 0; i < 24; i++) + { + if (particleEffects[i].ticks <= 0) + { + particleEffects[i].ticks = 10 + rand() % 5; + particleEffects[i].totalTicks = particleEffects[i].ticks; + particleEffects[i].boardX = boardX; + particleEffects[i].boardY = boardY; + particleEffects[i].velocityX = (rand() % 11) - 5; + particleEffects[i].velocityY = -8 + (rand() % 5); + particleEffects[i].color = color; + return; + } + } +} + +void TriggerLineClearEffect(const int* rows, int rowCount, int linesCleared) +{ + if (rows == nullptr || rowCount <= 0 || linesCleared <= 0) + { + return; + } + + if (rowCount > 8) + { + rowCount = 8; + } + + clearEffectState.ticks = 16; + clearEffectState.totalTicks = 16; + clearEffectState.rowCount = rowCount; + + int rowSum = 0; + for (int i = 0; i < rowCount; i++) + { + clearEffectState.rows[i] = rows[i]; + rowSum += rows[i]; + for (int x = 0; x < nGameWidth; x += 3) + { + COLORREF particleColor = BrickColor[(x + rows[i]) % 7]; + AddParticle(x * 100 + 50, rows[i] * 100 + 50, particleColor); + } + } + + TCHAR text[64]; + if (linesCleared >= 4) + { + _stprintf_s(text, _T("TETRIS")); + } + else + { + _stprintf_s(text, _T("%d LINE%s"), linesCleared, linesCleared > 1 ? _T("S") : _T("")); + } + AddFloatingText(nGameWidth * 50, (rowSum * 100 / rowCount) - 20, text, linesCleared >= 4 ? RGB(255, 232, 120) : RGB(255, 250, 252)); +} + bool IsPiecePlacementValid(int pieceType, int pieceState, Point position) { for (int i = 0; i < 4; i++) @@ -741,6 +867,8 @@ int DeleteLines() { int clearedLines = 0; bool clearedWithRainbow = false; + int clearedRows[8] = {}; + int clearedRowCount = 0; int playableHeight = GetRoguePlayableHeight(); for (int i = playableHeight - 1; i >= 0; i--) @@ -758,6 +886,12 @@ int DeleteLines() if (fullLine) { + if (clearedRowCount < 8) + { + clearedRows[clearedRowCount] = i; + clearedRowCount++; + } + for (int j = 0; j < nGameWidth; j++) { if (IsRainbowBoardCell(workRegion[i][j])) @@ -773,6 +907,7 @@ int DeleteLines() } ApplyLineClearResult(clearedLines); + TriggerLineClearEffect(clearedRows, clearedRowCount, clearedLines); if (pendingChainBombFollowup && clearedLines > 0) { @@ -898,6 +1033,7 @@ void Restart() feedbackState.visibleTicks = 0; feedbackState.title[0] = _T('\0'); feedbackState.detail[0] = _T('\0'); + ResetVisualEffects(); holdType = -1; holdUsedThisTurn = false; RollCurrentPieceSpecialFlags(false); @@ -929,6 +1065,7 @@ void ReturnToMainMenu() currentScreen = SCREEN_MENU; suspendFlag = false; gameOverFlag = false; + ResetVisualEffects(); menuState.optionCount = 3; upgradeUiState.pendingCount = 0; upgradeUiState.picksRemaining = 0; diff --git a/src/source/TetrisRender.cpp b/src/source/TetrisRender.cpp index ecedd69..f9843fd 100644 --- a/src/source/TetrisRender.cpp +++ b/src/source/TetrisRender.cpp @@ -839,6 +839,87 @@ void TDrawScreen(HDC hdc, HWND hWnd) } } + if (clearEffectState.ticks > 0 && clearEffectState.totalTicks > 0) + { + int elapsed = clearEffectState.totalTicks - clearEffectState.ticks; + int alpha = 42 + clearEffectState.ticks * 150 / clearEffectState.totalTicks; + int inset = SS(elapsed * 2); + Graphics flashGraphics(hdc); + + for (int i = 0; i < clearEffectState.rowCount; i++) + { + int row = clearEffectState.rows[i]; + if (row < 0 || row >= nGameHeight) + { + continue; + } + + int top = gameRect.top + row * grid + inset; + int height = grid - inset * 2; + if (height < SS(4)) + { + height = SS(4); + } + + SolidBrush flashBrush(Color(alpha, 255, 248, 174)); + flashGraphics.FillRectangle( + &flashBrush, + static_cast(gameRect.left + SS(2)), + static_cast(top), + static_cast(gameRect.right - gameRect.left - SS(4)), + static_cast(height)); + } + } + + for (int i = 0; i < 24; i++) + { + if (particleEffects[i].ticks <= 0 || particleEffects[i].totalTicks <= 0) + { + continue; + } + + int elapsed = particleEffects[i].totalTicks - particleEffects[i].ticks; + int particleX = gameRect.left + particleEffects[i].boardX * grid / 100 + SS(particleEffects[i].velocityX * elapsed / 2); + int particleY = gameRect.top + particleEffects[i].boardY * grid / 100 + SS(particleEffects[i].velocityY * elapsed / 2 + elapsed * elapsed / 10); + int particleSize = SS(3 + (elapsed % 2)); + + RECT particleRect = + { + particleX - particleSize, + particleY - particleSize, + particleX + particleSize, + particleY + particleSize + }; + + HBRUSH particleBrush = CreateSolidBrush(particleEffects[i].color); + FillRect(hdc, &particleRect, particleBrush); + DeleteObject(particleBrush); + } + + for (int i = 0; i < 8; i++) + { + if (floatingTextEffects[i].ticks <= 0 || floatingTextEffects[i].totalTicks <= 0) + { + continue; + } + + int elapsed = floatingTextEffects[i].totalTicks - floatingTextEffects[i].ticks; + int textX = gameRect.left + floatingTextEffects[i].boardX * grid / 100; + int textY = gameRect.top + floatingTextEffects[i].boardY * grid / 100 - SS(elapsed * 4); + + HFONT oldFloatFont = (HFONT)SelectObject(hdc, sectionFont); + SetTextColor(hdc, floatingTextEffects[i].color); + RECT floatRect = + { + textX - SS(160), + textY - SS(24), + textX + SS(160), + textY + SS(28) + }; + DrawText(hdc, floatingTextEffects[i].text, -1, &floatRect, DT_CENTER | DT_VCENTER | DT_SINGLELINE); + SelectObject(hdc, oldFloatFont); + } + HFONT oldFont = (HFONT)SelectObject(hdc, titleFont); DrawPanelHeader(leftPanelRect, _T("战局信息"), 120); DrawPanelHeader(rightPanelRect, _T("预览与战术"), 148);