diff --git a/assets/video/video.mp4 b/assets/video/video.mp4 new file mode 100644 index 0000000..89d8369 Binary files /dev/null and b/assets/video/video.mp4 differ diff --git a/src/include/Tetris.h b/src/include/Tetris.h index 5b21b2a..5f7ded8 100644 --- a/src/include/Tetris.h +++ b/src/include/Tetris.h @@ -253,6 +253,9 @@ void ResetVisualEffects(); bool TickVisualEffects(); void TriggerLineClearEffect(const int* rows, int rowCount, int linesCleared); void PlayPendingLineClearEffect(); +void TriggerCellClearEffect(const Point* cells, int cellCount, bool strongBurst); +void AwardRogueSkillClearRewards(int clearedCells, int& scoreGain, int& expGain, bool allowLevelProgress); +void ApplyBoardGravity(); int GetRogueFallInterval(); int GetRoguePlayableHeight(); int GetRogueLockedRows(); diff --git a/src/source/TetrisLogic.cpp b/src/source/TetrisLogic.cpp index 936ebd8..4b563d7 100644 --- a/src/source/TetrisLogic.cpp +++ b/src/source/TetrisLogic.cpp @@ -480,6 +480,25 @@ void TriggerLineClearEffect(const int* rows, int rowCount, int linesCleared) AddFloatingText(nGameWidth * 50, (rowSum * 100 / rowCount) - 20, text, linesCleared >= 4 ? RGB(255, 232, 120) : RGB(255, 250, 252)); } +void TriggerCellClearEffect(const Point* cells, int cellCount, bool strongBurst) +{ + if (cells == nullptr || cellCount <= 0) + { + return; + } + + for (int i = 0; i < cellCount; i++) + { + if (cells[i].x < 0 || cells[i].x >= nGameWidth || cells[i].y < 0 || cells[i].y >= nGameHeight) + { + continue; + } + + COLORREF particleColor = BrickColor[(cells[i].x + cells[i].y) % 7]; + AddBurstParticles(cells[i].x * 100 + 50, cells[i].y * 100 + 50, particleColor, strongBurst); + } +} + bool IsPiecePlacementValid(int pieceType, int pieceState, Point position) { for (int i = 0; i < 4; i++) @@ -821,26 +840,28 @@ void Fixing() if (currentPieceIsExplosive) { - int explosiveScoreGain = 0; + int explosiveCellsCleared = 0; for (int i = 0; i < explosiveCellCount; i++) { - explosiveScoreGain += ClearExplosiveAreaAt(explosiveCells[i].y, explosiveCells[i].x); + explosiveCellsCleared += ClearExplosiveAreaAt(explosiveCells[i].y, explosiveCells[i].x); } - if (currentMode == MODE_ROGUE && explosiveScoreGain > 0) + int explosiveScoreGain = 0; + int explosiveExpGain = 0; + if (currentMode == MODE_ROGUE && explosiveCellsCleared > 0) { - explosiveScoreGain = explosiveScoreGain * rogueStats.scoreMultiplierPercent / 100; - rogueStats.score += explosiveScoreGain; - tScore = rogueStats.score; + AwardRogueSkillClearRewards(explosiveCellsCleared, explosiveScoreGain, explosiveExpGain, false); + ApplyBoardGravity(); } TCHAR explosiveDetail[128]; _stprintf_s( explosiveDetail, - _T("爆破清除 %d 格 +%d 分"), - explosiveScoreGain > 0 ? explosiveScoreGain * 100 / rogueStats.scoreMultiplierPercent : 0, - explosiveScoreGain); + _T("爆破清除 %d 格 +%d 分 +%d EXP"), + explosiveCellsCleared, + explosiveScoreGain, + explosiveExpGain); SetFeedbackMessage(_T("爆破核心"), explosiveDetail, 12); if (rogueStats.chainBombLevel > 0 && explosiveCellCount > 0) @@ -865,16 +886,13 @@ void Fixing() int laserCellsCleared = ClearColumnAt(laserColumn); if (currentMode == MODE_ROGUE && laserCellsCleared > 0) { - int laserScore = laserCellsCleared * rogueStats.scoreMultiplierPercent / 100; - if (laserScore < laserCellsCleared) - { - laserScore = laserCellsCleared; - } - rogueStats.score += laserScore; - tScore = rogueStats.score; + int laserScore = 0; + int laserExp = 0; + AwardRogueSkillClearRewards(laserCellsCleared, laserScore, laserExp, false); + ApplyBoardGravity(); TCHAR laserDetail[128]; - _stprintf_s(laserDetail, _T("激光贯穿一列,清除 %d 格 +%d 分"), laserCellsCleared, laserScore); + _stprintf_s(laserDetail, _T("激光贯穿一列,清除 %d 格 +%d 分 +%d EXP"), laserCellsCleared, laserScore, laserExp); SetFeedbackMessage(_T("棱镜激光"), laserDetail, 12); } } @@ -910,16 +928,13 @@ void Fixing() if (currentMode == MODE_ROGUE && totalCrossCleared > 0) { - int crossScore = totalCrossCleared * rogueStats.scoreMultiplierPercent / 100; - if (crossScore < totalCrossCleared) - { - crossScore = totalCrossCleared; - } - rogueStats.score += crossScore; - tScore = rogueStats.score; + int crossScore = 0; + int crossExp = 0; + AwardRogueSkillClearRewards(totalCrossCleared, crossScore, crossExp, false); + ApplyBoardGravity(); TCHAR crossDetail[128]; - _stprintf_s(crossDetail, _T("十字冲击清除 %d 格 +%d 分"), totalCrossCleared, crossScore); + _stprintf_s(crossDetail, _T("十字冲击清除 %d 格 +%d 分 +%d EXP"), totalCrossCleared, crossScore, crossExp); SetFeedbackMessage(_T("十字方块"), crossDetail, 12); } } @@ -1038,6 +1053,7 @@ int DeleteLines() int followupCleared = 0; int centerY = pendingChainBombCenter.y; int centerX = pendingChainBombCenter.x; + Point followupCells[9] = {}; for (int y = centerY - 1; y <= centerY + 1; y++) { @@ -1045,6 +1061,11 @@ int DeleteLines() { if (y >= 0 && y < GetRoguePlayableHeight() && x >= 0 && x < nGameWidth && workRegion[y][x] != 0) { + if (followupCleared < 9) + { + followupCells[followupCleared].x = x; + followupCells[followupCleared].y = y; + } workRegion[y][x] = 0; followupCleared++; } @@ -1053,20 +1074,19 @@ int DeleteLines() if (currentMode == MODE_ROGUE && followupCleared > 0) { - int followupScore = followupCleared * rogueStats.scoreMultiplierPercent / 100; - if (followupScore < followupCleared) - { - followupScore = followupCleared; - } - rogueStats.score += followupScore; - tScore = rogueStats.score; + TriggerCellClearEffect(followupCells, followupCleared < 9 ? followupCleared : 9, true); + int followupScore = 0; + int followupExp = 0; + AwardRogueSkillClearRewards(followupCleared, followupScore, followupExp, false); + ApplyBoardGravity(); TCHAR followupDetail[128]; _stprintf_s( followupDetail, - _T("追加爆炸清除 %d 格 +%d 分"), + _T("追加爆炸清除 %d 格 +%d 分 +%d EXP"), followupCleared, - followupScore); + followupScore, + followupExp); SetFeedbackMessage(_T("连环炸弹"), followupDetail, 12); } } @@ -1080,16 +1100,13 @@ int DeleteLines() int miniBlackHoleCleared = TriggerMiniBlackHole(5); if (miniBlackHoleCleared > 0) { - int miniScore = miniBlackHoleCleared * rogueStats.scoreMultiplierPercent / 100; - if (miniScore < miniBlackHoleCleared) - { - miniScore = miniBlackHoleCleared; - } - rogueStats.score += miniScore; - tScore = rogueStats.score; + int miniScore = 0; + int miniExp = 0; + AwardRogueSkillClearRewards(miniBlackHoleCleared, miniScore, miniExp, false); + ApplyBoardGravity(); TCHAR miniDetail[128]; - _stprintf_s(miniDetail, _T("彩虹消行撕开小型黑洞,清除 %d 格 +%d 分"), miniBlackHoleCleared, miniScore); + _stprintf_s(miniDetail, _T("彩虹消行撕开小型黑洞,清除 %d 格 +%d 分 +%d EXP"), miniBlackHoleCleared, miniScore, miniExp); SetFeedbackMessage(_T("虚空核心"), miniDetail, 12); } } diff --git a/src/source/TetrisRogue.cpp b/src/source/TetrisRogue.cpp index f5cf9f3..41e25fb 100644 --- a/src/source/TetrisRogue.cpp +++ b/src/source/TetrisRogue.cpp @@ -723,18 +723,25 @@ int TriggerMiniBlackHole(int maxCellsToClear) } int clearedCellCount = 0; + Point clearedCells[16] = {}; for (int y = GetRoguePlayableHeight() - 1; y >= 0 && clearedCellCount < maxCellsToClear; y--) { for (int x = 0; x < nGameWidth && clearedCellCount < maxCellsToClear; x++) { if (workRegion[y][x] == targetBlock) { + if (clearedCellCount < 16) + { + clearedCells[clearedCellCount].x = x; + clearedCells[clearedCellCount].y = y; + } workRegion[y][x] = 0; clearedCellCount++; } } } + TriggerCellClearEffect(clearedCells, clearedCellCount < 16 ? clearedCellCount : 16, true); return clearedCellCount; } @@ -769,6 +776,7 @@ int ClearExplosiveAreaAt(int centerY, int centerX) { int radius = (currentMode == MODE_ROGUE && rogueStats.chainBombLevel > 0) ? 2 : 1; int clearedCellCount = 0; + Point clearedCells[25] = {}; for (int y = centerY - radius; y <= centerY + radius; y++) { @@ -778,6 +786,11 @@ int ClearExplosiveAreaAt(int centerY, int centerX) { if (workRegion[y][x] != 0) { + if (clearedCellCount < 25) + { + clearedCells[clearedCellCount].x = x; + clearedCells[clearedCellCount].y = y; + } clearedCellCount++; workRegion[y][x] = 0; } @@ -785,12 +798,14 @@ int ClearExplosiveAreaAt(int centerY, int centerX) } } + TriggerCellClearEffect(clearedCells, clearedCellCount < 25 ? clearedCellCount : 25, true); return clearedCellCount; } int ClearColumnAt(int column) { int clearedCellCount = 0; + Point clearedCells[20] = {}; if (column < 0 || column >= nGameWidth) { @@ -801,17 +816,24 @@ int ClearColumnAt(int column) { if (workRegion[y][column] != 0) { + if (clearedCellCount < 20) + { + clearedCells[clearedCellCount].x = column; + clearedCells[clearedCellCount].y = y; + } workRegion[y][column] = 0; clearedCellCount++; } } + TriggerCellClearEffect(clearedCells, clearedCellCount, false); return clearedCellCount; } int ClearRowAt(int row) { int clearedCellCount = 0; + Point clearedCells[10] = {}; if (row < 0 || row >= GetRoguePlayableHeight()) { @@ -822,11 +844,17 @@ int ClearRowAt(int row) { if (workRegion[row][x] != 0) { + if (clearedCellCount < 10) + { + clearedCells[clearedCellCount].x = x; + clearedCells[clearedCellCount].y = row; + } workRegion[row][x] = 0; clearedCellCount++; } } + TriggerCellClearEffect(clearedCells, clearedCellCount, false); return clearedCellCount; } @@ -904,24 +932,32 @@ static int TriggerBlackHole() } int clearedCellCount = 0; + Point clearedCells[200] = {}; for (int y = 0; y < GetRoguePlayableHeight(); y++) { for (int x = 0; x < nGameWidth; x++) { if (workRegion[y][x] == targetBlock) { + if (clearedCellCount < 200) + { + clearedCells[clearedCellCount].x = x; + clearedCells[clearedCellCount].y = y; + } workRegion[y][x] = 0; clearedCellCount++; } } } + TriggerCellClearEffect(clearedCells, clearedCellCount < 200 ? clearedCellCount : 200, true); return clearedCellCount; } int TriggerScreenBomb() { int clearedCellCount = 0; + Point clearedCells[50] = {}; for (int i = 0; i < 5; i++) { @@ -935,6 +971,11 @@ int TriggerScreenBomb() { if (workRegion[row][x] != 0) { + if (clearedCellCount < 50) + { + clearedCells[clearedCellCount].x = x; + clearedCells[clearedCellCount].y = row; + } clearedCellCount++; } } @@ -942,6 +983,7 @@ int TriggerScreenBomb() DeleteOneLine(row); } + TriggerCellClearEffect(clearedCells, clearedCellCount < 50 ? clearedCellCount : 50, true); return clearedCellCount; } @@ -953,6 +995,7 @@ static int TriggerChainBlast(int lineAnchor) } int clearedCellCount = 0; + Point clearedCells[16] = {}; int blastCount = 2 + rogueStats.chainBlastLevel; for (int blastIndex = 0; blastIndex < blastCount; blastIndex++) @@ -971,11 +1014,17 @@ static int TriggerChainBlast(int lineAnchor) if (workRegion[randomY][randomX] != 0) { + if (clearedCellCount < 16) + { + clearedCells[clearedCellCount].x = randomX; + clearedCells[clearedCellCount].y = randomY; + } workRegion[randomY][randomX] = 0; clearedCellCount++; } } + TriggerCellClearEffect(clearedCells, clearedCellCount < 16 ? clearedCellCount : 16, false); return clearedCellCount; } @@ -1447,6 +1496,89 @@ static void ApplyUpgradeById(int upgradeId, int targetPieceType, int applyCount) } } +void AwardRogueSkillClearRewards(int clearedCells, int& scoreGain, int& expGain, bool allowLevelProgress) +{ + scoreGain = 0; + expGain = 0; + if (currentMode != MODE_ROGUE || clearedCells <= 0) + { + return; + } + + scoreGain = clearedCells * rogueStats.scoreMultiplierPercent / 100; + expGain = clearedCells * rogueStats.expMultiplierPercent / 100; + + if (rogueStats.doubleGrowthLevel > 0) + { + int growthMultiplierPercent = 100 + rogueStats.doubleGrowthLevel * 15; + scoreGain = scoreGain * growthMultiplierPercent / 100; + expGain = expGain * growthMultiplierPercent / 100; + } + + if (rogueStats.feverTicks > 0) + { + scoreGain *= 2; + expGain *= 2; + } + + if (scoreGain < clearedCells) + { + scoreGain = clearedCells; + } + if (expGain < clearedCells) + { + expGain = clearedCells; + } + + rogueStats.score += scoreGain; + rogueStats.exp += expGain; + tScore = rogueStats.score; + + if (rogueStats.screenBombLevel > 0) + { + rogueStats.screenBombCharge += clearedCells; + while (rogueStats.screenBombCharge >= 30) + { + rogueStats.screenBombCharge -= 30; + rogueStats.screenBombCount++; + } + } + + if (allowLevelProgress) + { + int levelUps = ApplyLevelProgress(rogueStats); + if (levelUps > 0) + { + upgradeUiState.pendingCount += levelUps; + OpenUpgradeMenu(); + } + } +} + +void ApplyBoardGravity() +{ + int playableHeight = GetRoguePlayableHeight(); + for (int x = 0; x < nGameWidth; x++) + { + int writeY = playableHeight - 1; + for (int y = playableHeight - 1; y >= 0; y--) + { + if (workRegion[y][x] != 0) + { + int cell = workRegion[y][x]; + workRegion[y][x] = 0; + workRegion[writeY][x] = cell; + writeY--; + } + } + + for (int y = writeY; y >= 0; y--) + { + workRegion[y][x] = 0; + } + } +} + void ApplyLineClearResult(int linesCleared) { if (linesCleared <= 0) @@ -1586,20 +1718,18 @@ void ApplyLineClearResult(int linesCleared) if (chainBlastCells > 0) { - int chainBlastScore = chainBlastCells * rogueStats.scoreMultiplierPercent / 100; - if (chainBlastScore < chainBlastCells) - { - chainBlastScore = chainBlastCells; - } - rogueStats.score += chainBlastScore; - tScore = rogueStats.score; + int chainBlastScore = 0; + int chainBlastExp = 0; + AwardRogueSkillClearRewards(chainBlastCells, chainBlastScore, chainBlastExp, false); + ApplyBoardGravity(); TCHAR blastDetail[128]; _stprintf_s( blastDetail, - _T("余波炸裂,清除 %d 格 +%d 分"), + _T("余波炸裂,清除 %d 格 +%d 分 +%d EXP"), chainBlastCells, - chainBlastScore); + chainBlastScore, + chainBlastExp); SetFeedbackMessage(_T("连锁火花"), blastDetail, 12); } } @@ -1607,6 +1737,7 @@ void ApplyLineClearResult(int linesCleared) if (linesCleared == 4 && rogueStats.thunderTetrisLevel > 0) { int thunderRowsCleared = 0; + Point thunderCells[20] = {}; for (int i = 0; i < 2; i++) { int randomRow = rand() % GetRoguePlayableHeight(); @@ -1614,6 +1745,11 @@ void ApplyLineClearResult(int linesCleared) { if (workRegion[randomRow][x] != 0) { + if (thunderRowsCleared < 20) + { + thunderCells[thunderRowsCleared].x = x; + thunderCells[thunderRowsCleared].y = randomRow; + } workRegion[randomRow][x] = 0; thunderRowsCleared++; } @@ -1622,13 +1758,11 @@ void ApplyLineClearResult(int linesCleared) if (thunderRowsCleared > 0) { - int thunderScore = thunderRowsCleared * rogueStats.scoreMultiplierPercent / 100; - if (thunderScore < thunderRowsCleared) - { - thunderScore = thunderRowsCleared; - } - rogueStats.score += thunderScore; - tScore = rogueStats.score; + TriggerCellClearEffect(thunderCells, thunderRowsCleared < 20 ? thunderRowsCleared : 20, true); + int thunderScore = 0; + int thunderExp = 0; + AwardRogueSkillClearRewards(thunderRowsCleared, thunderScore, thunderExp, false); + ApplyBoardGravity(); SetFeedbackMessage(_T("雷霆四消"), _T("雷击落下,额外清理了 2 行范围内的方块。"), 12); } } @@ -1643,20 +1777,10 @@ void ApplyLineClearResult(int linesCleared) if (laserCellsCleared > 0) { - int laserScore = laserCellsCleared * rogueStats.scoreMultiplierPercent / 100; - int laserExp = laserCellsCleared * rogueStats.expMultiplierPercent / 100; - if (laserScore < laserCellsCleared) - { - laserScore = laserCellsCleared; - } - if (laserExp < laserCellsCleared) - { - laserExp = laserCellsCleared; - } - - rogueStats.score += laserScore; - rogueStats.exp += laserExp; - tScore = rogueStats.score; + int laserScore = 0; + int laserExp = 0; + AwardRogueSkillClearRewards(laserCellsCleared, laserScore, laserExp, false); + ApplyBoardGravity(); TCHAR thunderLaserDetail[128]; _stprintf_s( @@ -1769,19 +1893,24 @@ static void ResolvePendingUpgradeShockwave() effectRows[i] = GetRoguePlayableHeight() - 1 - i; } TriggerLineClearEffect(effectRows, effectRowCount, clearedRows); + int scoreGain = 0; + int expGain = 0; + AwardRogueSkillClearRewards(clearedRows * nGameWidth, scoreGain, expGain, true); TCHAR shockwaveDetail[128]; if (evolutionImpact) { _stprintf_s( shockwaveDetail, - _T("进化能量爆发,清除底部 %d 行,并进入 10 秒双倍 EXP。"), - clearedRows); + _T("进化能量爆发,清除底部 %d 行 +%d 分 +%d EXP。"), + clearedRows, + scoreGain, + expGain); SetFeedbackMessage(_T("进化冲击"), shockwaveDetail, 14); } else { - _stprintf_s(shockwaveDetail, _T("灵感涌现后,冲击波清除底部 %d 行。"), clearedRows); + _stprintf_s(shockwaveDetail, _T("灵感涌现后,冲击波清除底部 %d 行 +%d 分 +%d EXP。"), clearedRows, scoreGain, expGain); SetFeedbackMessage(_T("升级冲击波"), shockwaveDetail, 12); } } @@ -1989,21 +2118,18 @@ void UseScreenBomb() rogueStats.screenBombCount--; int clearedCells = TriggerScreenBomb(); - int scoreGain = clearedCells * rogueStats.scoreMultiplierPercent / 100; - if (scoreGain < clearedCells) - { - scoreGain = clearedCells; - } - - rogueStats.score += scoreGain; - tScore = rogueStats.score; + ApplyBoardGravity(); + int scoreGain = 0; + int expGain = 0; + AwardRogueSkillClearRewards(clearedCells, scoreGain, expGain, true); TCHAR detail[128]; _stprintf_s( detail, - _T("炸开底部 5 行,清除 %d 格 +%d 分"), + _T("炸开底部 5 行,清除 %d 格 +%d 分 +%d EXP"), clearedCells, - scoreGain); + scoreGain, + expGain); SetFeedbackMessage(_T("清屏炸弹引爆"), detail, 12); currentFallInterval = GetRogueFallInterval(); @@ -2036,14 +2162,10 @@ void UseBlackHole() rogueStats.blackHoleCharges--; - int scoreGain = clearedCells * rogueStats.scoreMultiplierPercent / 100; - if (scoreGain < clearedCells) - { - scoreGain = clearedCells; - } - - rogueStats.score += scoreGain; - tScore = rogueStats.score; + ApplyBoardGravity(); + int scoreGain = 0; + int expGain = 0; + AwardRogueSkillClearRewards(clearedCells, scoreGain, expGain, true); if (rogueStats.voidCoreLevel > 0) { @@ -2055,17 +2177,19 @@ void UseBlackHole() { _stprintf_s( detail, - _T("吞噬最多的一种方块,清除 %d 格 +%d 分 并召来 1 个彩虹方块"), + _T("吞噬最多的一种方块,清除 %d 格 +%d 分 +%d EXP 并召来 1 个彩虹方块"), clearedCells, - scoreGain); + scoreGain, + expGain); } else { _stprintf_s( detail, - _T("吞噬最多的一种方块,清除 %d 格 +%d 分"), + _T("吞噬最多的一种方块,清除 %d 格 +%d 分 +%d EXP"), clearedCells, - scoreGain); + scoreGain, + expGain); } SetFeedbackMessage(_T("黑洞展开"), detail, 12);