diff --git a/build-mingw.ps1 b/build-mingw.ps1 index 8e1dd03..874a726 100644 --- a/build-mingw.ps1 +++ b/build-mingw.ps1 @@ -65,6 +65,7 @@ $Sources = @( (Join-Path $SourceDir "stdafx.cpp"), (Join-Path $SourceDir "Tetris.cpp"), (Join-Path $SourceDir "TetrisLogic.cpp"), + (Join-Path $SourceDir "TetrisLogicInnovation.cpp"), (Join-Path $SourceDir "TetrisRogue.cpp"), (Join-Path $SourceDir "TetrisRender.cpp") ) diff --git a/src/include/TetrisLogicInternal.h b/src/include/TetrisLogicInternal.h index 0af909f..1a824c7 100644 --- a/src/include/TetrisLogicInternal.h +++ b/src/include/TetrisLogicInternal.h @@ -4,20 +4,97 @@ extern Point pendingChainBombCenter; extern bool pendingChainBombFollowup; +extern int pendingLineClearEffectTicks; +extern int pendingLineClearEffectRows[8]; +extern int pendingLineClearEffectRowCount; +extern int pendingLineClearEffectLineCount; +/** + * @brief 计算指定方块在棋盘顶部的统一生成位置。 + */ Point GetSpawnPoint(int brickType); + +/** + * @brief 重置经典或 Rogue 模式使用的玩家统计数据。 + */ void ResetPlayerStats(PlayerStats& stats, bool useRogueRules); + +/** + * @brief 设置界面右侧显示的即时反馈标题、内容和持续时间。 + */ void SetFeedbackMessage(const TCHAR* title, const TCHAR* detail, int ticks); + +/** + * @brief 判断指定方块、旋转状态和位置是否可以合法放置。 + */ bool IsPiecePlacementValid(int pieceType, int pieceState, Point position); + +/** + * @brief 判断棋盘格是否为彩虹特殊方块。 + */ bool IsRainbowBoardCell(int cellValue); + +/** + * @brief 触发小型黑洞并返回被清除的固定方块数量。 + */ int TriggerMiniBlackHole(int maxCellsToClear); + +/** + * @brief 触发彩虹方块补洞效果并返回补齐格数。 + */ int TriggerRainbowRowCompletion(int minRow, int maxRow); + +/** + * @brief 引爆清屏炸弹并返回清除格数。 + */ int TriggerScreenBomb(); + +/** + * @brief 清除指定中心点周围的爆破范围并返回清除格数。 + */ int ClearExplosiveAreaAt(int centerY, int centerX); + +/** + * @brief 清除指定列并返回清除格数。 + */ int ClearColumnAt(int column); + +/** + * @brief 清除指定行并返回清除格数。 + */ int ClearRowAt(int row); + +/** + * @brief 尝试填补局部空洞以稳定棋盘结构。 + */ int TryStabilizeBoard(); + +/** + * @brief 为当前方块刷新 Rogue 特殊方块标记。 + */ void RollCurrentPieceSpecialFlags(bool allowRandomSpecials); + +/** + * @brief 暂存消行动画,等待升级选择结束后再播放。 + */ +void QueueLineClearEffect(const int* rows, int rowCount, int linesCleared); + +/** + * @brief 尝试把旋转后的方块横向偏移指定格数后放置。 + */ +bool TryRotateWithOffset(int nextState, int offsetX); + +/** + * @brief 重置下一方块预览队列。 + */ void ResetNextQueue(); + +/** + * @brief 消费队首下一方块并补充新的预览方块。 + */ int ConsumeNextType(); + +/** + * @brief 结算一次标准消行带来的 Rogue 玩法效果。 + */ void ApplyLineClearResult(int linesCleared); diff --git a/src/source/TetrisLogic.cpp b/src/source/TetrisLogic.cpp index 9cb1bcf..01cd786 100644 --- a/src/source/TetrisLogic.cpp +++ b/src/source/TetrisLogic.cpp @@ -34,10 +34,6 @@ bool currentPieceIsCross = false; bool currentPieceIsRainbow = false; Point pendingChainBombCenter = { 0, 0 }; bool pendingChainBombFollowup = false; -static int pendingLineClearEffectTicks = 0; -static int pendingLineClearEffectRows[8] = {}; -static int pendingLineClearEffectRowCount = 0; -static int pendingLineClearEffectLineCount = 0; int bricks[7][4][4][4] = { @@ -167,369 +163,6 @@ Point GetSpawnPoint(int brickType) return spawnPoint; } -void ResetPlayerStats(PlayerStats& stats, bool useRogueRules) -{ - stats.score = 0; - stats.level = 1; - stats.exp = 0; - stats.requiredExp = useRogueRules ? 10 : 0; - stats.totalLinesCleared = 0; - stats.scoreMultiplierPercent = 100; - stats.expMultiplierPercent = 100; - stats.slowFallStacks = 0; - stats.comboBonusStacks = 0; - stats.comboChain = 0; - stats.previewCount = 1; - stats.lastChanceCount = 0; - stats.scoreUpgradeLevel = 0; - stats.expUpgradeLevel = 0; - stats.previewUpgradeLevel = 0; - stats.lastChanceUpgradeLevel = 0; - stats.holdUnlocked = 0; - stats.pressureReliefLevel = 0; - stats.sweeperLevel = 0; - stats.sweeperCharge = 0; - stats.explosiveLevel = 0; - stats.explosivePieceCounter = 0; - stats.chainBlastLevel = 0; - stats.chainBombLevel = 0; - stats.laserLevel = 0; - stats.thunderTetrisLevel = 0; - stats.thunderLaserLevel = 0; - stats.feverLevel = 0; - stats.rageStackLevel = 0; - stats.infiniteFeverLevel = 0; - stats.feverLineCharge = 0; - stats.feverTicks = 0; - stats.screenBombLevel = 0; - stats.screenBombCharge = 0; - stats.screenBombCount = 0; - stats.terminalClearLevel = 0; - stats.dualChoiceLevel = 0; - stats.destinyWheelLevel = 0; - stats.perfectRotateLevel = 0; - stats.timeDilationLevel = 0; - stats.timeDilationTicks = 0; - stats.highPressureLevel = 0; - stats.tetrisGambleLevel = 0; - stats.extremePlayerLevel = 0; - stats.extremeSlowTicks = 0; - stats.extremeDangerTicks = 30; - stats.extremeDangerLevel = 0; - stats.upgradeShockwaveLevel = 0; - stats.evolutionImpactLevel = 0; - stats.controlMasterLevel = 0; - stats.holdSlowTicks = 0; - stats.blockStormLevel = 0; - stats.blockStormPiecesRemaining = 0; - stats.blackHoleLevel = 0; - stats.blackHoleCharges = 0; - stats.reshapeLevel = 0; - stats.reshapeCharges = 0; - stats.rainbowPieceLevel = 0; - stats.voidCoreLevel = 0; - stats.pendingRainbowPieceCount = 0; - stats.stableStructureLevel = 0; - stats.doubleGrowthLevel = 0; - stats.gamblerLevel = 0; - stats.difficultyElapsedMs = 0; - stats.difficultyLevel = 0; - stats.lockedRows = 0; - for (int i = 0; i < 7; i++) - { - stats.pieceTuningLevels[i] = 0; - } -} - -void SetFeedbackMessage(const TCHAR* title, const TCHAR* detail, int ticks) -{ - feedbackState.visibleTicks = ticks; - lstrcpyn(feedbackState.title, title, sizeof(feedbackState.title) / 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 < 96; 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 < 96; 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, int velocityX, int velocityY, int size, COLORREF color) -{ - for (int i = 0; i < 96; i++) - { - if (particleEffects[i].ticks <= 0) - { - particleEffects[i].ticks = 12 + rand() % 7; - particleEffects[i].totalTicks = particleEffects[i].ticks; - particleEffects[i].boardX = boardX; - particleEffects[i].boardY = boardY; - particleEffects[i].velocityX = velocityX; - particleEffects[i].velocityY = velocityY; - particleEffects[i].size = size; - particleEffects[i].color = color; - return; - } - } -} - -static void AddBurstParticles(int boardX, int boardY, COLORREF baseColor, bool strongBurst) -{ - int burstCount = strongBurst ? 4 : 2; - for (int i = 0; i < burstCount; i++) - { - int angleSeed = rand() % 8; - int speed = strongBurst ? (9 + rand() % 9) : (6 + rand() % 7); - int velocityX = 0; - int velocityY = 0; - - switch (angleSeed) - { - case 0: - velocityX = speed; - velocityY = -rand() % 4; - break; - case 1: - velocityX = -speed; - velocityY = -rand() % 4; - break; - case 2: - velocityX = (rand() % 5) - 2; - velocityY = -speed; - break; - case 3: - velocityX = (rand() % 5) - 2; - velocityY = speed / 2; - break; - case 4: - velocityX = speed; - velocityY = -speed; - break; - case 5: - velocityX = -speed; - velocityY = -speed; - break; - case 6: - velocityX = speed; - velocityY = speed / 3; - break; - default: - velocityX = -speed; - velocityY = speed / 3; - break; - } - - velocityX += (rand() % 7) - 3; - velocityY += (rand() % 7) - 3; - - COLORREF color = (i % 3 == 0) ? RGB(255, 248, 220) : baseColor; - AddParticle( - boardX + (rand() % 31) - 15, - boardY + (rand() % 31) - 15, - velocityX, - velocityY, - strongBurst ? (4 + rand() % 5) : (3 + rand() % 4), - color); - } -} - -static void QueueLineClearEffect(const int* rows, int rowCount, int linesCleared) -{ - if (rows == nullptr || rowCount <= 0 || linesCleared <= 0) - { - return; - } - - if (rowCount > 8) - { - rowCount = 8; - } - - pendingLineClearEffectTicks = 1; - pendingLineClearEffectRowCount = rowCount; - pendingLineClearEffectLineCount = linesCleared; - for (int i = 0; i < rowCount; i++) - { - pendingLineClearEffectRows[i] = rows[i]; - } -} - -void PlayPendingLineClearEffect() -{ - if (pendingLineClearEffectTicks <= 0) - { - return; - } - - pendingLineClearEffectTicks = 0; - TriggerLineClearEffect( - pendingLineClearEffectRows, - pendingLineClearEffectRowCount, - pendingLineClearEffectLineCount); - pendingLineClearEffectRowCount = 0; - pendingLineClearEffectLineCount = 0; -} - -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++) - { - COLORREF particleColor = BrickColor[(x + rows[i]) % 7]; - int centerX = x * 100 + 50; - int centerY = rows[i] * 100 + 50; - AddBurstParticles(centerX, centerY, particleColor, linesCleared >= 4); - - if (linesCleared >= 4) - { - AddParticle( - centerX, - centerY, - ((x < nGameWidth / 2) ? -1 : 1) * (16 + rand() % 12), - -16 - rand() % 10, - 4 + rand() % 3, - RGB(255, 238, 120)); - } - } - } - - 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)); -} - -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++) - { - for (int j = 0; j < 4; j++) - { - if (bricks[pieceType][pieceState][i][j] == 0) - { - continue; - } - - int checkY = position.y + i; - int checkX = position.x + j; - - if (checkX < 0 || checkX >= nGameWidth || checkY >= GetRoguePlayableHeight()) - { - return false; - } - - if (checkY >= 0 && workRegion[checkY][checkX] != 0) - { - return false; - } - } - } - - return true; -} - /** * @brief 判断当前方块是否可以继续向下移动。 * @@ -650,13 +283,6 @@ bool CanMoveRight() return true; } -static bool TryRotateWithOffset(int nextState, int offsetX) -{ - Point rotatedPoint = point; - rotatedPoint.x += offsetX; - return IsPiecePlacementValid(type, nextState, rotatedPoint); -} - /** * @brief 将当前活动方块向下移动一格。 * @@ -1198,80 +824,3 @@ void Restart() ComputeTarget(); } -void ReviveAfterVideo() -{ - if (!gameOverFlag || !reviveAvailable) - { - return; - } - - reviveAvailable = false; - gameOverFlag = false; - suspendFlag = false; - currentScreen = SCREEN_PLAYING; - - int playableHeight = GetRoguePlayableHeight(); - int rowsToClear = playableHeight / 3; - if (rowsToClear < 5) - { - rowsToClear = 5; - } - - for (int y = 0; y < rowsToClear && y < playableHeight; y++) - { - for (int x = 0; x < nGameWidth; x++) - { - workRegion[y][x] = 0; - } - } - - type = ConsumeNextType(); - nType = nextTypes[0]; - state = 0; - holdUsedThisTurn = false; - RollCurrentPieceSpecialFlags(true); - point = GetSpawnPoint(type); - target = point; - ComputeTarget(); - - SetFeedbackMessage(_T("复活成功"), _T("已清理顶部空间,本局复活机会已用完。"), 14); -} - -void StartGameWithMode(int mode) -{ - currentMode = mode; - currentScreen = SCREEN_PLAYING; - Restart(); - currentFallInterval = (currentMode == MODE_ROGUE) ? GetRogueFallInterval() : 500; - tScore = (currentMode == MODE_CLASSIC) ? classicStats.score : rogueStats.score; -} - -void ReturnToMainMenu() -{ - currentScreen = SCREEN_MENU; - suspendFlag = false; - gameOverFlag = false; - ResetVisualEffects(); - ResetPendingRogueVisualEvents(); - pendingLineClearEffectTicks = 0; - pendingLineClearEffectRowCount = 0; - pendingLineClearEffectLineCount = 0; - menuState.optionCount = 3; - upgradeUiState.pendingCount = 0; - upgradeUiState.picksRemaining = 0; - upgradeUiState.markedCount = 0; - - if (menuState.selectedIndex < 0 || menuState.selectedIndex >= menuState.optionCount) - { - menuState.selectedIndex = 0; - } -} - -void OpenRulesScreen() -{ - currentScreen = SCREEN_RULES; - suspendFlag = false; - helpState.selectedIndex = 0; - helpState.optionCount = 3; - helpState.currentPage = 0; -} diff --git a/src/source/TetrisLogicInnovation.cpp b/src/source/TetrisLogicInnovation.cpp new file mode 100644 index 0000000..e532149 --- /dev/null +++ b/src/source/TetrisLogicInnovation.cpp @@ -0,0 +1,506 @@ +#include "stdafx.h" +#include "TetrisLogicInternal.h" + +int pendingLineClearEffectTicks = 0; +int pendingLineClearEffectRows[8] = {}; +int pendingLineClearEffectRowCount = 0; +int pendingLineClearEffectLineCount = 0; + +/** + * @brief 重置经典或 Rogue 模式使用的玩家统计数据。 + */ +void ResetPlayerStats(PlayerStats& stats, bool useRogueRules) +{ + stats.score = 0; + stats.level = 1; + stats.exp = 0; + stats.requiredExp = useRogueRules ? 10 : 0; + stats.totalLinesCleared = 0; + stats.scoreMultiplierPercent = 100; + stats.expMultiplierPercent = 100; + stats.slowFallStacks = 0; + stats.comboBonusStacks = 0; + stats.comboChain = 0; + stats.previewCount = 1; + stats.lastChanceCount = 0; + stats.scoreUpgradeLevel = 0; + stats.expUpgradeLevel = 0; + stats.previewUpgradeLevel = 0; + stats.lastChanceUpgradeLevel = 0; + stats.holdUnlocked = 0; + stats.pressureReliefLevel = 0; + stats.sweeperLevel = 0; + stats.sweeperCharge = 0; + stats.explosiveLevel = 0; + stats.explosivePieceCounter = 0; + stats.chainBlastLevel = 0; + stats.chainBombLevel = 0; + stats.laserLevel = 0; + stats.thunderTetrisLevel = 0; + stats.thunderLaserLevel = 0; + stats.feverLevel = 0; + stats.rageStackLevel = 0; + stats.infiniteFeverLevel = 0; + stats.feverLineCharge = 0; + stats.feverTicks = 0; + stats.screenBombLevel = 0; + stats.screenBombCharge = 0; + stats.screenBombCount = 0; + stats.terminalClearLevel = 0; + stats.dualChoiceLevel = 0; + stats.destinyWheelLevel = 0; + stats.perfectRotateLevel = 0; + stats.timeDilationLevel = 0; + stats.timeDilationTicks = 0; + stats.highPressureLevel = 0; + stats.tetrisGambleLevel = 0; + stats.extremePlayerLevel = 0; + stats.extremeSlowTicks = 0; + stats.extremeDangerTicks = 30; + stats.extremeDangerLevel = 0; + stats.upgradeShockwaveLevel = 0; + stats.evolutionImpactLevel = 0; + stats.controlMasterLevel = 0; + stats.holdSlowTicks = 0; + stats.blockStormLevel = 0; + stats.blockStormPiecesRemaining = 0; + stats.blackHoleLevel = 0; + stats.blackHoleCharges = 0; + stats.reshapeLevel = 0; + stats.reshapeCharges = 0; + stats.rainbowPieceLevel = 0; + stats.voidCoreLevel = 0; + stats.pendingRainbowPieceCount = 0; + stats.stableStructureLevel = 0; + stats.doubleGrowthLevel = 0; + stats.gamblerLevel = 0; + stats.difficultyElapsedMs = 0; + stats.difficultyLevel = 0; + stats.lockedRows = 0; + for (int i = 0; i < 7; i++) + { + stats.pieceTuningLevels[i] = 0; + } +} + +/** + * @brief 设置界面右侧显示的即时反馈标题、内容和持续时间。 + */ +void SetFeedbackMessage(const TCHAR* title, const TCHAR* detail, int ticks) +{ + feedbackState.visibleTicks = ticks; + lstrcpyn(feedbackState.title, title, sizeof(feedbackState.title) / sizeof(TCHAR)); + lstrcpyn(feedbackState.detail, detail, sizeof(feedbackState.detail) / sizeof(TCHAR)); +} + +/** + * @brief 清空所有消行、浮动文字和粒子视觉效果。 + */ +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 < 96; i++) + { + particleEffects[i].ticks = 0; + } +} + +/** + * @brief 推进视觉效果计时,并返回是否仍有动画需要刷新。 + */ +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 < 96; i++) + { + if (particleEffects[i].ticks > 0) + { + particleEffects[i].ticks--; + active = true; + } + } + + return active; +} + +/** + * @brief 添加一段棋盘坐标系中的浮动文字效果。 + */ +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; + } + } +} + +/** + * @brief 添加一个棋盘坐标系中的粒子效果。 + */ +static void AddParticle(int boardX, int boardY, int velocityX, int velocityY, int size, COLORREF color) +{ + for (int i = 0; i < 96; i++) + { + if (particleEffects[i].ticks <= 0) + { + particleEffects[i].ticks = 12 + rand() % 7; + particleEffects[i].totalTicks = particleEffects[i].ticks; + particleEffects[i].boardX = boardX; + particleEffects[i].boardY = boardY; + particleEffects[i].velocityX = velocityX; + particleEffects[i].velocityY = velocityY; + particleEffects[i].size = size; + particleEffects[i].color = color; + return; + } + } +} + +/** + * @brief 在指定棋盘坐标周围生成一组爆裂粒子。 + */ +static void AddBurstParticles(int boardX, int boardY, COLORREF baseColor, bool strongBurst) +{ + int burstCount = strongBurst ? 4 : 2; + for (int i = 0; i < burstCount; i++) + { + int angleSeed = rand() % 8; + int speed = strongBurst ? (9 + rand() % 9) : (6 + rand() % 7); + int velocityX = 0; + int velocityY = 0; + + switch (angleSeed) + { + case 0: + velocityX = speed; + velocityY = -rand() % 4; + break; + case 1: + velocityX = -speed; + velocityY = -rand() % 4; + break; + case 2: + velocityX = (rand() % 5) - 2; + velocityY = -speed; + break; + case 3: + velocityX = (rand() % 5) - 2; + velocityY = speed / 2; + break; + case 4: + velocityX = speed; + velocityY = -speed; + break; + case 5: + velocityX = -speed; + velocityY = -speed; + break; + case 6: + velocityX = speed; + velocityY = speed / 3; + break; + default: + velocityX = -speed; + velocityY = speed / 3; + break; + } + + velocityX += (rand() % 7) - 3; + velocityY += (rand() % 7) - 3; + + COLORREF color = (i % 3 == 0) ? RGB(255, 248, 220) : baseColor; + AddParticle( + boardX + (rand() % 31) - 15, + boardY + (rand() % 31) - 15, + velocityX, + velocityY, + strongBurst ? (4 + rand() % 5) : (3 + rand() % 4), + color); + } +} + +/** + * @brief 暂存消行动画,等待升级选择结束后再播放。 + */ +void QueueLineClearEffect(const int* rows, int rowCount, int linesCleared) +{ + if (rows == nullptr || rowCount <= 0 || linesCleared <= 0) + { + return; + } + + if (rowCount > 8) + { + rowCount = 8; + } + + pendingLineClearEffectTicks = 1; + pendingLineClearEffectRowCount = rowCount; + pendingLineClearEffectLineCount = linesCleared; + for (int i = 0; i < rowCount; i++) + { + pendingLineClearEffectRows[i] = rows[i]; + } +} + +/** + * @brief 播放之前暂存的消行动画。 + */ +void PlayPendingLineClearEffect() +{ + if (pendingLineClearEffectTicks <= 0) + { + return; + } + + pendingLineClearEffectTicks = 0; + TriggerLineClearEffect( + pendingLineClearEffectRows, + pendingLineClearEffectRowCount, + pendingLineClearEffectLineCount); + pendingLineClearEffectRowCount = 0; + pendingLineClearEffectLineCount = 0; +} + +/** + * @brief 触发标准消行动画和浮动文字。 + */ +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++) + { + COLORREF particleColor = BrickColor[(x + rows[i]) % 7]; + int centerX = x * 100 + 50; + int centerY = rows[i] * 100 + 50; + AddBurstParticles(centerX, centerY, particleColor, linesCleared >= 4); + + if (linesCleared >= 4) + { + AddParticle( + centerX, + centerY, + ((x < nGameWidth / 2) ? -1 : 1) * (16 + rand() % 12), + -16 - rand() % 10, + 4 + rand() % 3, + RGB(255, 238, 120)); + } + } + } + + 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)); +} + +/** + * @brief 为指定棋盘格集合触发清除粒子效果。 + */ +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); + } +} + +/** + * @brief 判断指定方块、旋转状态和位置是否可以合法放置。 + */ +bool IsPiecePlacementValid(int pieceType, int pieceState, Point position) +{ + for (int i = 0; i < 4; i++) + { + for (int j = 0; j < 4; j++) + { + if (bricks[pieceType][pieceState][i][j] == 0) + { + continue; + } + + int checkY = position.y + i; + int checkX = position.x + j; + + if (checkX < 0 || checkX >= nGameWidth || checkY >= GetRoguePlayableHeight()) + { + return false; + } + + if (checkY >= 0 && workRegion[checkY][checkX] != 0) + { + return false; + } + } + } + + return true; +} + +/** + * @brief 尝试把旋转后的方块横向偏移指定格数后放置。 + */ +bool TryRotateWithOffset(int nextState, int offsetX) +{ + Point rotatedPoint = point; + rotatedPoint.x += offsetX; + return IsPiecePlacementValid(type, nextState, rotatedPoint); +} + +/** + * @brief 视频复活后清理顶部空间并恢复一局游戏。 + */ +void ReviveAfterVideo() +{ + if (!gameOverFlag || !reviveAvailable) + { + return; + } + + reviveAvailable = false; + gameOverFlag = false; + suspendFlag = false; + currentScreen = SCREEN_PLAYING; + + int playableHeight = GetRoguePlayableHeight(); + int rowsToClear = playableHeight / 3; + if (rowsToClear < 5) + { + rowsToClear = 5; + } + + for (int y = 0; y < rowsToClear && y < playableHeight; y++) + { + for (int x = 0; x < nGameWidth; x++) + { + workRegion[y][x] = 0; + } + } + + type = ConsumeNextType(); + nType = nextTypes[0]; + state = 0; + holdUsedThisTurn = false; + RollCurrentPieceSpecialFlags(true); + point = GetSpawnPoint(type); + target = point; + ComputeTarget(); + + SetFeedbackMessage(_T("复活成功"), _T("已清理顶部空间,本局复活机会已用完。"), 14); +} + +/** + * @brief 按指定模式开始新游戏。 + */ +void StartGameWithMode(int mode) +{ + currentMode = mode; + currentScreen = SCREEN_PLAYING; + Restart(); + currentFallInterval = (currentMode == MODE_ROGUE) ? GetRogueFallInterval() : 500; + tScore = (currentMode == MODE_CLASSIC) ? classicStats.score : rogueStats.score; +} + +/** + * @brief 返回主菜单并清理游戏中的临时界面状态。 + */ +void ReturnToMainMenu() +{ + currentScreen = SCREEN_MENU; + suspendFlag = false; + gameOverFlag = false; + ResetVisualEffects(); + ResetPendingRogueVisualEvents(); + pendingLineClearEffectTicks = 0; + pendingLineClearEffectRowCount = 0; + pendingLineClearEffectLineCount = 0; + menuState.optionCount = 3; + upgradeUiState.pendingCount = 0; + upgradeUiState.picksRemaining = 0; + upgradeUiState.markedCount = 0; + + if (menuState.selectedIndex < 0 || menuState.selectedIndex >= menuState.optionCount) + { + menuState.selectedIndex = 0; + } +} + +/** + * @brief 打开规则说明界面并重置说明页状态。 + */ +void OpenRulesScreen() +{ + currentScreen = SCREEN_RULES; + suspendFlag = false; + helpState.selectedIndex = 0; + helpState.optionCount = 3; + helpState.currentPage = 0; +} diff --git a/src/source/TetrisRogue.cpp b/src/source/TetrisRogue.cpp index 979ceb6..7d6ae59 100644 --- a/src/source/TetrisRogue.cpp +++ b/src/source/TetrisRogue.cpp @@ -1,6 +1,14 @@ #include "stdafx.h" #include "TetrisLogicInternal.h" +/** + * @brief Rogue 创新玩法独立实现文件。 + * + * 本文件集中放置从基础俄罗斯方块版本扩展出的创新设计,包括强化池、 + * 等级成长、特殊方块、主动技能、难度收缩、连锁奖励和升级选择流程。 + * 代码保持过程式函数组织,不使用 class、继承或多态。 + */ + enum UpgradeId { UPGRADE_SCORE_MULTIPLIER = 0, @@ -127,6 +135,9 @@ static void ApplyUpgradeById(int upgradeId, int targetPieceType, int applyCount) static void ApplyDestinyCurse(); static void ClearLockedRows(); +/** + * @brief 限制 Rogue 模式的下一方块预览数量。 + */ static int GetNextPreviewLimit() { if (currentMode != MODE_ROGUE) @@ -147,6 +158,9 @@ static int GetNextPreviewLimit() return rogueStats.previewCount; } +/** + * @brief 获取 Rogue 难度系统当前封锁的底部行数。 + */ int GetRogueLockedRows() { if (currentMode != MODE_ROGUE) @@ -162,11 +176,17 @@ int GetRogueLockedRows() return rogueStats.lockedRows > kMaxRogueLockedRows ? kMaxRogueLockedRows : rogueStats.lockedRows; } +/** + * @brief 计算当前模式下棋盘可操作区域的高度。 + */ int GetRoguePlayableHeight() { return nGameHeight - GetRogueLockedRows(); } +/** + * @brief 清空 Rogue 模式中被难度系统封锁的底部区域。 + */ static void ClearLockedRows() { int lockedRows = GetRogueLockedRows(); @@ -184,6 +204,9 @@ static void ClearLockedRows() } } +/** + * @brief 根据经过时间推进 Rogue 难度、封锁行数和下落速度。 + */ void AdvanceRogueDifficulty(int elapsedMs) { if (currentMode != MODE_ROGUE || currentScreen != SCREEN_PLAYING || suspendFlag || gameOverFlag || elapsedMs <= 0) @@ -232,6 +255,9 @@ void AdvanceRogueDifficulty(int elapsedMs) SetFeedbackMessage(lockedRowsChanged ? _T("战场收缩") : _T("压力升高"), difficultyDetail, 12); } +/** + * @brief 读取指定强化在当前 Rogue 属性中的等级或拥有状态。 + */ static int GetUpgradeCurrentLevel(int upgradeId) { switch (upgradeId) @@ -319,6 +345,9 @@ static int GetUpgradeCurrentLevel(int upgradeId) } } +/** + * @brief 返回进化类强化在界面中展示的合成路线说明。 + */ const TCHAR* GetUpgradeSynthesisPath(int upgradeId) { switch (upgradeId) @@ -346,6 +375,9 @@ const TCHAR* GetUpgradeSynthesisPath(int upgradeId) } } +/** + * @brief 获取指定强化的基础稀有度分类。 + */ static int GetUpgradeBaseRarity(int upgradeId) { switch (upgradeId) @@ -390,6 +422,9 @@ static int GetUpgradeBaseRarity(int upgradeId) } } +/** + * @brief 判断指定强化是否已被更高阶合成强化替代。 + */ static bool IsUpgradePrerequisiteConsumed(int upgradeId) { switch (upgradeId) @@ -425,6 +460,9 @@ static bool IsUpgradePrerequisiteConsumed(int upgradeId) } } +/** + * @brief 根据当前局势动态计算强化选项的抽取权重。 + */ static int GetUpgradeDynamicWeight(const UpgradeEntry& entry) { int weight = entry.baseWeight; @@ -484,6 +522,9 @@ static int GetUpgradeDynamicWeight(const UpgradeEntry& entry) return weight < 1 ? 1 : weight; } +/** + * @brief 把方块类型编号转换为界面展示用的短名称。 + */ static const TCHAR* GetPieceShortName(int pieceType) { static const TCHAR* kNames[7] = @@ -499,6 +540,9 @@ static const TCHAR* GetPieceShortName(int pieceType) return kNames[pieceType]; } +/** + * @brief 判断强化是否满足当前等级、前置和互斥条件。 + */ static bool IsUpgradeSelectable(const UpgradeEntry& entry) { if (IsUpgradePrerequisiteConsumed(entry.id)) @@ -581,6 +625,9 @@ static bool IsUpgradeSelectable(const UpgradeEntry& entry) return GetUpgradeCurrentLevel(entry.id) < entry.maxLevel; } +/** + * @brief 查找棋盘中最高的已占用行,用于评估局势压力。 + */ static int GetTopOccupiedRow() { for (int i = 0; i < GetRoguePlayableHeight(); i++) @@ -597,6 +644,9 @@ static int GetTopOccupiedRow() return -1; } +/** + * @brief 计算底线清道夫触发一次自动清底需要的消行充能。 + */ static int GetSweeperThreshold() { int reduction = (rogueStats.sweeperLevel - 1); @@ -604,6 +654,9 @@ static int GetSweeperThreshold() return threshold < 2 ? 2 : threshold; } +/** + * @brief 根据爆破强化等级随机判定当前方块是否获得爆破特性。 + */ static bool RollExplosivePiece() { if (currentMode != MODE_ROGUE || rogueStats.explosiveLevel <= 0) @@ -636,6 +689,9 @@ static bool RollExplosivePiece() return true; } +/** + * @brief 根据激光强化等级随机判定当前方块是否获得激光特性。 + */ static bool RollLaserPiece() { if (currentMode != MODE_ROGUE || rogueStats.laserLevel <= 0) @@ -661,6 +717,9 @@ static bool RollLaserPiece() return (rand() % 100) < chancePercent; } +/** + * @brief 根据十字强化等级随机判定当前方块是否获得十字清除特性。 + */ static bool RollCrossPiece() { if (currentMode != MODE_ROGUE || rogueStats.crossPieceLevel <= 0) @@ -686,6 +745,9 @@ static bool RollCrossPiece() return (rand() % 100) < chancePercent; } +/** + * @brief 根据彩虹强化等级随机判定当前方块是否获得彩虹补洞特性。 + */ static bool RollRainbowPiece() { if (currentMode != MODE_ROGUE || rogueStats.rainbowPieceLevel <= 0) @@ -701,11 +763,17 @@ static bool RollRainbowPiece() return (rand() % 100) < chancePercent; } +/** + * @brief 判断棋盘格是否属于彩虹方块固定后的特殊格。 + */ bool IsRainbowBoardCell(int cellValue) { return cellValue == 8; } +/** + * @brief 触发小型黑洞,随机吞噬限定数量的固定方块。 + */ int TriggerMiniBlackHole(int maxCellsToClear) { int blockCounts[8] = { 0 }; @@ -761,6 +829,9 @@ int TriggerMiniBlackHole(int maxCellsToClear) return clearedCellCount; } +/** + * @brief 为当前活动方块刷新爆破、激光、十字和彩虹等特殊标记。 + */ void RollCurrentPieceSpecialFlags(bool allowRandomSpecials) { if (!allowRandomSpecials) @@ -788,6 +859,9 @@ void RollCurrentPieceSpecialFlags(bool allowRandomSpecials) currentPieceIsRainbow = !currentPieceIsExplosive && !currentPieceIsLaser && !currentPieceIsCross && RollRainbowPiece(); } +/** + * @brief 以指定棋盘格为中心清除爆破范围内的固定方块。 + */ int ClearExplosiveAreaAt(int centerY, int centerX) { int radius = (currentMode == MODE_ROGUE && rogueStats.chainBombLevel > 0) ? 2 : 1; @@ -818,6 +892,9 @@ int ClearExplosiveAreaAt(int centerY, int centerX) return clearedCellCount; } +/** + * @brief 清除指定列中的所有固定方块并返回清除数量。 + */ int ClearColumnAt(int column) { int clearedCellCount = 0; @@ -846,6 +923,9 @@ int ClearColumnAt(int column) return clearedCellCount; } +/** + * @brief 清除指定行中的所有固定方块并返回清除数量。 + */ int ClearRowAt(int row) { int clearedCellCount = 0; @@ -874,6 +954,9 @@ int ClearRowAt(int row) return clearedCellCount; } +/** + * @brief 让彩虹方块尝试补齐其覆盖行内的空缺格。 + */ int TriggerRainbowRowCompletion(int minRow, int maxRow) { int filledCellCount = 0; @@ -916,6 +999,9 @@ int TriggerRainbowRowCompletion(int minRow, int maxRow) return filledCellCount; } +/** + * @brief 触发黑洞,吞噬棋盘中数量最多的一类固定方块。 + */ static int TriggerBlackHole() { int blockCounts[8] = { 0 }; @@ -970,6 +1056,9 @@ static int TriggerBlackHole() return clearedCellCount; } +/** + * @brief 引爆清屏炸弹,清除当前可玩区域底部多行方块。 + */ int TriggerScreenBomb() { int clearedCellCount = 0; @@ -1003,6 +1092,9 @@ int TriggerScreenBomb() return clearedCellCount; } +/** + * @brief 围绕消行位置触发连锁火花,随机破坏附近固定方块。 + */ static int TriggerChainBlast(int lineAnchor) { if (currentMode != MODE_ROGUE || rogueStats.chainBlastLevel <= 0) @@ -1044,6 +1136,9 @@ static int TriggerChainBlast(int lineAnchor) return clearedCellCount; } +/** + * @brief 根据 Rogue 强化和权重系统随机生成下一个方块类型。 + */ static int RollNextPieceType() { if (currentMode == MODE_ROGUE && rogueStats.blockStormPiecesRemaining > 0) @@ -1082,6 +1177,9 @@ static int RollNextPieceType() return rand() % 7; } +/** + * @brief 尝试用稳定结构强化填补局部空洞并返回填补数量。 + */ int TryStabilizeBoard() { if (currentMode != MODE_ROGUE || rogueStats.stableStructureLevel <= 0) @@ -1129,6 +1227,9 @@ int TryStabilizeBoard() return 0; } +/** + * @brief 重置下一方块队列并按当前预览上限填充。 + */ void ResetNextQueue() { for (int i = 0; i < 3; i++) @@ -1137,6 +1238,9 @@ void ResetNextQueue() } } +/** + * @brief 取出队首方块并补充新的下一方块。 + */ int ConsumeNextType() { int nextType = nextTypes[0]; @@ -1146,6 +1250,9 @@ int ConsumeNextType() return nextType; } +/** + * @brief 按 Rogue 规则计算指定消行数对应的基础得分。 + */ static int GetRogueScoreByLines(int linesCleared) { switch (linesCleared) @@ -1158,6 +1265,9 @@ static int GetRogueScoreByLines(int linesCleared) } } +/** + * @brief 按 Rogue 规则计算指定消行数对应的基础经验。 + */ static int GetRogueExpByLines(int linesCleared) { switch (linesCleared) @@ -1170,6 +1280,9 @@ static int GetRogueExpByLines(int linesCleared) } } +/** + * @brief 结算经验条并返回本次连续升级次数。 + */ static int ApplyLevelProgress(PlayerStats& stats) { int levelUps = 0; @@ -1185,6 +1298,9 @@ static int ApplyLevelProgress(PlayerStats& stats) return levelUps; } +/** + * @brief 触发升级冲击波,清除底部指定数量的行。 + */ static int TriggerUpgradeShockwave(int rowsToClear) { int clearedRows = 0; @@ -1198,6 +1314,9 @@ static int TriggerUpgradeShockwave(int rowsToClear) return clearedRows; } +/** + * @brief 按权重和当前局势生成升级菜单中的候选强化。 + */ static void FillUpgradeOptions() { int selectableIndexes[kUpgradePoolSize] = { 0 }; @@ -1289,6 +1408,9 @@ static void FillUpgradeOptions() } } +/** + * @brief 综合难度、强化和临时状态计算 Rogue 模式下落间隔。 + */ int GetRogueFallInterval() { int baseInterval = 500 + rogueStats.slowFallStacks * 80; @@ -1337,6 +1459,9 @@ int GetRogueFallInterval() return baseInterval; } +/** + * @brief 根据强化编号把对应效果写入 Rogue 属性。 + */ static void ApplyUpgradeById(int upgradeId, int targetPieceType, int applyCount) { if (applyCount <= 0) @@ -1518,6 +1643,9 @@ static void ApplyUpgradeById(int upgradeId, int targetPieceType, int applyCount) } } +/** + * @brief 为技能清除的格子结算得分和经验奖励。 + */ void AwardRogueSkillClearRewards(int clearedCells, int& scoreGain, int& expGain, bool allowLevelProgress) { scoreGain = 0; @@ -1577,6 +1705,9 @@ void AwardRogueSkillClearRewards(int clearedCells, int& scoreGain, int& expGain, } } +/** + * @brief 检查 Rogue 经验是否升级,并在需要时打开强化选择界面。 + */ void CheckRogueLevelProgress() { if (currentMode != MODE_ROGUE || currentScreen != SCREEN_PLAYING) @@ -1624,6 +1755,9 @@ void CheckRogueLevelProgress() OpenUpgradeMenu(); } +/** + * @brief 对棋盘固定方块应用重力,使悬空方块自然下落。 + */ void ApplyBoardGravity() { int playableHeight = GetRoguePlayableHeight(); @@ -1648,6 +1782,9 @@ void ApplyBoardGravity() } } +/** + * @brief 结算一次标准消行带来的 Rogue 得分、经验、连击和派生效果。 + */ void ApplyLineClearResult(int linesCleared) { if (linesCleared <= 0) @@ -1929,6 +2066,9 @@ void ApplyLineClearResult(int linesCleared) currentFallInterval = GetRogueFallInterval(); } +/** + * @brief 应用命运轮盘的诅咒,提高下一次升级所需经验。 + */ static void ApplyDestinyCurse() { rogueStats.requiredExp = rogueStats.requiredExp * 125 / 100; @@ -1938,6 +2078,9 @@ static void ApplyDestinyCurse() } } +/** + * @brief 在升级选择结束后处理延迟触发的升级冲击波。 + */ static void ResolvePendingUpgradeShockwave() { if (pendingUpgradeShockwaveRows <= 0) @@ -1984,12 +2127,18 @@ static void ResolvePendingUpgradeShockwave() } } +/** + * @brief 重置 Rogue 模式中等待播放的特殊视觉事件。 + */ void ResetPendingRogueVisualEvents() { pendingUpgradeShockwaveRows = 0; pendingEvolutionImpactShockwave = false; } +/** + * @brief 在 Rogue 模式中打开升级强化选择界面。 + */ void OpenUpgradeMenu() { if (currentMode != MODE_ROGUE || upgradeUiState.pendingCount <= 0) @@ -2001,6 +2150,9 @@ void OpenUpgradeMenu() currentScreen = SCREEN_UPGRADE; } +/** + * @brief 确认升级菜单中的选择并应用对应强化效果。 + */ void ConfirmUpgradeSelection() { if (currentScreen != SCREEN_UPGRADE || upgradeUiState.optionCount <= 0) @@ -2205,6 +2357,9 @@ void ConfirmUpgradeSelection() PlayPendingLineClearEffect(); } +/** + * @brief 执行 Hold 操作,在备用仓与当前方块之间交换。 + */ void HoldCurrentPiece() { if (currentMode != MODE_ROGUE || currentScreen != SCREEN_PLAYING || suspendFlag || gameOverFlag) @@ -2266,6 +2421,9 @@ void HoldCurrentPiece() } } +/** + * @brief 响应玩家主动使用清屏炸弹的操作。 + */ void UseScreenBomb() { if (currentMode != MODE_ROGUE || currentScreen != SCREEN_PLAYING || suspendFlag || gameOverFlag) @@ -2303,6 +2461,9 @@ void UseScreenBomb() ComputeTarget(); } +/** + * @brief 响应玩家主动释放黑洞的操作。 + */ void UseBlackHole() { if (currentMode != MODE_ROGUE || currentScreen != SCREEN_PLAYING || suspendFlag || gameOverFlag) @@ -2364,6 +2525,9 @@ void UseBlackHole() ComputeTarget(); } +/** + * @brief 响应玩家主动使用空中换形,将当前方块重塑为 I 块。 + */ void UseAirReshape() { if (currentMode != MODE_ROGUE || currentScreen != SCREEN_PLAYING || suspendFlag || gameOverFlag)