添加粒子效果

This commit is contained in:
2026-04-26 13:20:00 +08:00
parent 03bf93afaa
commit b537d3c193
4 changed files with 266 additions and 0 deletions
+35
View File
@@ -143,6 +143,35 @@ struct FeedbackState
TCHAR detail[128]; 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 enum ScreenState
{ {
SCREEN_MENU = 0, SCREEN_MENU = 0,
@@ -180,6 +209,9 @@ extern PlayerStats classicStats;
extern PlayerStats rogueStats; extern PlayerStats rogueStats;
extern UpgradeUiState upgradeUiState; extern UpgradeUiState upgradeUiState;
extern FeedbackState feedbackState; extern FeedbackState feedbackState;
extern ClearEffectState clearEffectState;
extern FloatingTextEffect floatingTextEffects[8];
extern ParticleEffect particleEffects[24];
extern int currentScreen; extern int currentScreen;
extern int currentMode; extern int currentMode;
extern int currentFallInterval; extern int currentFallInterval;
@@ -215,6 +247,9 @@ void HoldCurrentPiece();
void UseScreenBomb(); void UseScreenBomb();
void UseBlackHole(); void UseBlackHole();
void UseAirReshape(); void UseAirReshape();
void ResetVisualEffects();
bool TickVisualEffects();
void TriggerLineClearEffect(const int* rows, int rowCount, int linesCleared);
int GetRogueFallInterval(); int GetRogueFallInterval();
int GetRoguePlayableHeight(); int GetRoguePlayableHeight();
int GetRogueLockedRows(); int GetRogueLockedRows();
+13
View File
@@ -4,7 +4,9 @@
#define MAX_LOADSTRING 100 #define MAX_LOADSTRING 100
#define GAME_TIMER_ID 1 #define GAME_TIMER_ID 1
#define EFFECT_TIMER_ID 2
#define GAME_TIMER_INTERVAL 500 #define GAME_TIMER_INTERVAL 500
#define EFFECT_TIMER_INTERVAL 33
HINSTANCE hInst; HINSTANCE hInst;
TCHAR szTitle[MAX_LOADSTRING]; TCHAR szTitle[MAX_LOADSTRING];
@@ -355,6 +357,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
ReturnToMainMenu(); ReturnToMainMenu();
StartBackgroundMusic(); StartBackgroundMusic();
ResetGameTimer(hWnd); ResetGameTimer(hWnd);
SetTimer(hWnd, EFFECT_TIMER_ID, EFFECT_TIMER_INTERVAL, nullptr);
InvalidateRect(hWnd, nullptr, FALSE); InvalidateRect(hWnd, nullptr, FALSE);
break; break;
case WM_COMMAND: case WM_COMMAND:
@@ -375,6 +378,15 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
} }
break; break;
case WM_TIMER: case WM_TIMER:
if (wParam == EFFECT_TIMER_ID)
{
if (TickVisualEffects())
{
InvalidateRect(hWnd, nullptr, FALSE);
}
break;
}
if (wParam == GAME_TIMER_ID) if (wParam == GAME_TIMER_ID)
{ {
bool shouldRefresh = false; bool shouldRefresh = false;
@@ -823,6 +835,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
break; break;
case WM_DESTROY: case WM_DESTROY:
KillTimer(hWnd, GAME_TIMER_ID); KillTimer(hWnd, GAME_TIMER_ID);
KillTimer(hWnd, EFFECT_TIMER_ID);
StopBackgroundMusic(); StopBackgroundMusic();
PostQuitMessage(0); PostQuitMessage(0);
break; break;
+137
View File
@@ -17,6 +17,9 @@ PlayerStats classicStats = { 0, 1, 0, 0, 0 };
PlayerStats rogueStats = { 0, 1, 0, 30, 0, 100, 100, 0 }; PlayerStats rogueStats = { 0, 1, 0, 30, 0, 100, 100, 0 };
UpgradeUiState upgradeUiState = { 0, 0, 0, 0, {} }; UpgradeUiState upgradeUiState = { 0, 0, 0, 0, {} };
FeedbackState feedbackState = { 0, _T(""), _T("") }; FeedbackState feedbackState = { 0, _T(""), _T("") };
ClearEffectState clearEffectState = { 0, 0, 0, {} };
FloatingTextEffect floatingTextEffects[8] = {};
ParticleEffect particleEffects[24] = {};
int currentScreen = SCREEN_MENU; int currentScreen = SCREEN_MENU;
int currentMode = MODE_CLASSIC; int currentMode = MODE_CLASSIC;
int currentFallInterval = 500; 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)); 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) bool IsPiecePlacementValid(int pieceType, int pieceState, Point position)
{ {
for (int i = 0; i < 4; i++) for (int i = 0; i < 4; i++)
@@ -741,6 +867,8 @@ int DeleteLines()
{ {
int clearedLines = 0; int clearedLines = 0;
bool clearedWithRainbow = false; bool clearedWithRainbow = false;
int clearedRows[8] = {};
int clearedRowCount = 0;
int playableHeight = GetRoguePlayableHeight(); int playableHeight = GetRoguePlayableHeight();
for (int i = playableHeight - 1; i >= 0; i--) for (int i = playableHeight - 1; i >= 0; i--)
@@ -758,6 +886,12 @@ int DeleteLines()
if (fullLine) if (fullLine)
{ {
if (clearedRowCount < 8)
{
clearedRows[clearedRowCount] = i;
clearedRowCount++;
}
for (int j = 0; j < nGameWidth; j++) for (int j = 0; j < nGameWidth; j++)
{ {
if (IsRainbowBoardCell(workRegion[i][j])) if (IsRainbowBoardCell(workRegion[i][j]))
@@ -773,6 +907,7 @@ int DeleteLines()
} }
ApplyLineClearResult(clearedLines); ApplyLineClearResult(clearedLines);
TriggerLineClearEffect(clearedRows, clearedRowCount, clearedLines);
if (pendingChainBombFollowup && clearedLines > 0) if (pendingChainBombFollowup && clearedLines > 0)
{ {
@@ -898,6 +1033,7 @@ void Restart()
feedbackState.visibleTicks = 0; feedbackState.visibleTicks = 0;
feedbackState.title[0] = _T('\0'); feedbackState.title[0] = _T('\0');
feedbackState.detail[0] = _T('\0'); feedbackState.detail[0] = _T('\0');
ResetVisualEffects();
holdType = -1; holdType = -1;
holdUsedThisTurn = false; holdUsedThisTurn = false;
RollCurrentPieceSpecialFlags(false); RollCurrentPieceSpecialFlags(false);
@@ -929,6 +1065,7 @@ void ReturnToMainMenu()
currentScreen = SCREEN_MENU; currentScreen = SCREEN_MENU;
suspendFlag = false; suspendFlag = false;
gameOverFlag = false; gameOverFlag = false;
ResetVisualEffects();
menuState.optionCount = 3; menuState.optionCount = 3;
upgradeUiState.pendingCount = 0; upgradeUiState.pendingCount = 0;
upgradeUiState.picksRemaining = 0; upgradeUiState.picksRemaining = 0;
+81
View File
@@ -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<INT>(gameRect.left + SS(2)),
static_cast<INT>(top),
static_cast<INT>(gameRect.right - gameRect.left - SS(4)),
static_cast<INT>(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); HFONT oldFont = (HFONT)SelectObject(hdc, titleFont);
DrawPanelHeader(leftPanelRect, _T("战局信息"), 120); DrawPanelHeader(leftPanelRect, _T("战局信息"), 120);
DrawPanelHeader(rightPanelRect, _T("预览与战术"), 148); DrawPanelHeader(rightPanelRect, _T("预览与战术"), 148);