#include "stdafx.h" #include "Tetris.h" #include "TetrisLogicInternal.h" int nType = 0; int type = 0; int state = 0; int tScore = 0; bool gameOverFlag = false; bool suspendFlag = false; bool targetFlag = false; bool reviveAvailable = false; int workRegion[20][10] = { 0 }; Point point = { 0, 0 }; Point target = { 0, 0 }; MenuState menuState = { 0, 2 }; 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[96] = {}; int currentScreen = SCREEN_MENU; int currentMode = MODE_CLASSIC; int currentFallInterval = 500; int nextTypes[3] = { 0, 0, 0 }; int holdType = -1; bool holdUsedThisTurn = false; bool currentPieceIsExplosive = false; bool currentPieceIsLaser = false; 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] = { { {{0, 0, 0, 0}, {0, 0, 0, 0}, {1, 1, 1, 1}, {0, 0, 0, 0}}, {{0, 0, 1, 0}, {0, 0, 1, 0}, {0, 0, 1, 0}, {0, 0, 1, 0}}, {{0, 0, 0, 0}, {0, 0, 0, 0}, {1, 1, 1, 1}, {0, 0, 0, 0}}, {{0, 0, 1, 0}, {0, 0, 1, 0}, {0, 0, 1, 0}, {0, 0, 1, 0}} }, { {{0, 0, 0, 0}, {0, 0, 2, 0}, {0, 2, 2, 2}, {0, 0, 0, 0}}, {{0, 0, 0, 0}, {0, 0, 2, 0}, {0, 0, 2, 2}, {0, 0, 2, 0}}, {{0, 0, 0, 0}, {0, 0, 0, 0}, {0, 2, 2, 2}, {0, 0, 2, 0}}, {{0, 0, 0, 0}, {0, 0, 2, 0}, {0, 2, 2, 0}, {0, 0, 2, 0}} }, { {{0, 3, 0, 0}, {0, 3, 0, 0}, {0, 3, 3, 0}, {0, 0, 0, 0}}, {{0, 0, 0, 0}, {3, 3, 3, 0}, {3, 0, 0, 0}, {0, 0, 0, 0}}, {{3, 3, 0, 0}, {0, 3, 0, 0}, {0, 3, 0, 0}, {0, 0, 0, 0}}, {{0, 0, 3, 0}, {3, 3, 3, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}} }, { {{0, 0, 4, 0}, {0, 0, 4, 0}, {0, 4, 4, 0}, {0, 0, 0, 0}}, {{0, 4, 0, 0}, {0, 4, 4, 4}, {0, 0, 0, 0}, {0, 0, 0, 0}}, {{0, 0, 4, 4}, {0, 0, 4, 0}, {0, 0, 4, 0}, {0, 0, 0, 0}}, {{0, 0, 0, 0}, {0, 4, 4, 4}, {0, 0, 0, 4}, {0, 0, 0, 0}} }, { {{0, 0, 0, 0}, {0, 5, 5, 0}, {0, 5, 5, 0}, {0, 0, 0, 0}}, {{0, 0, 0, 0}, {0, 5, 5, 0}, {0, 5, 5, 0}, {0, 0, 0, 0}}, {{0, 0, 0, 0}, {0, 5, 5, 0}, {0, 5, 5, 0}, {0, 0, 0, 0}}, {{0, 0, 0, 0}, {0, 5, 5, 0}, {0, 5, 5, 0}, {0, 0, 0, 0}} }, { {{0, 6, 0, 0}, {0, 6, 6, 0}, {0, 0, 6, 0}, {0, 0, 0, 0}}, {{0, 0, 0, 0}, {0, 6, 6, 0}, {6, 6, 0, 0}, {0, 0, 0, 0}}, {{6, 0, 0, 0}, {6, 6, 0, 0}, {0, 6, 0, 0}, {0, 0, 0, 0}}, {{0, 6, 6, 0}, {6, 6, 0, 0}, {0, 0, 0, 0}, {0, 0, 0, 0}} }, { {{0, 0, 7, 0}, {0, 7, 7, 0}, {0, 7, 0, 0}, {0, 0, 0, 0}}, {{0, 7, 7, 0}, {0, 0, 7, 7}, {0, 0, 0, 0}, {0, 0, 0, 0}}, {{0, 0, 0, 7}, {0, 0, 7, 7}, {0, 0, 7, 0}, {0, 0, 0, 0}}, {{0, 0, 0, 0}, {0, 7, 7, 0}, {0, 0, 7, 7}, {0, 0, 0, 0}} } }; COLORREF BrickColor[7] = { RGB(244, 144, 165), RGB(255, 181, 197), RGB(170, 215, 255), RGB(134, 230, 220), RGB(255, 187, 143), RGB(255, 223, 146), RGB(197, 170, 255) }; /** * @brief 计算指定方块在指定旋转状态下的最小包围盒边界。 * * 该函数会遍历 4x4 形状矩阵,找出所有非空单元的上下左右边界, * 供后续统一计算生成位置和对齐方式时使用。 * * @param brickType 方块类型编号。 * @param brickState 方块旋转状态编号。 * @param minRow 返回最上方非空行号。 * @param maxRow 返回最下方非空行号。 * @param minCol 返回最左侧非空列号。 * @param maxCol 返回最右侧非空列号。 */ static void GetBrickBounds(int brickType, int brickState, int& minRow, int& maxRow, int& minCol, int& maxCol) { minRow = 4; maxRow = -1; minCol = 4; maxCol = -1; for (int i = 0; i < 4; i++) { for (int j = 0; j < 4; j++) { if (bricks[brickType][brickState][i][j] != 0) { if (i < minRow) { minRow = i; } if (i > maxRow) { maxRow = i; } if (j < minCol) { minCol = j; } if (j > maxCol) { maxCol = j; } } } } } /** * @brief 计算指定方块的统一生成位置。 * * 该函数会根据方块在初始旋转状态下的最小包围盒, * 自动把方块水平居中到游戏区附近,并将顶部非空行对齐到可视区域顶部。 * 这样不同形状的方块在生成时看起来会更加统一。 * * @param brickType 方块类型编号。 * @return Point 计算得到的生成坐标。 */ Point GetSpawnPoint(int brickType) { int minRow, maxRow, minCol, maxCol; GetBrickBounds(brickType, 0, minRow, maxRow, minCol, maxCol); int brickWidth = maxCol - minCol + 1; int brickHeight = maxRow - minRow + 1; Point spawnPoint; spawnPoint.x = (nGameWidth - brickWidth) / 2 - minCol; spawnPoint.y = -brickHeight; 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 ? 5 : 3; 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 判断当前方块是否可以继续向下移动。 * * 遍历当前处于活动状态下方块的 4x4 矩阵,计算其下落一步(Y 坐标加 1)后的位置, * 并检查每个非空方块单元: * 1. 是否超出了游戏工作区的底部边界(对应数组索引 >= 20)。 * 2. 是否与工作区下方已经固定的其他方块发生碰撞(即对应位置的值不为 0)。 * 如果遇到以上任意一种情况,则认为方块受到阻挡,无法继续下落。 * * @return bool 如果可以继续安全下落返回 true,否则返回 false。 */ bool CanMoveDown() { for (int i = 0; i < 4; i++) { for (int j = 0; j < 4; j++) { if (bricks[type][state][i][j] != 0) { int nextY = point.y + i + 1; int nextX = point.x + j; // 检查是否到达底部边界 if (nextY >= GetRoguePlayableHeight()) { return false; } // 检查下方是否有其他固定方块 if (nextY >= 0 && workRegion[nextY][nextX] != 0) { return false; } } } } return true; } /** * @brief 判断当前方块是否可以继续向左移动。 * * 遍历当前处于活动状态下方块的 4x4 矩阵,计算其向左移动一步(X 坐标减 1)后的位置, * 并检查每个非空方块单元: * 1. 是否超出了游戏工作区的左侧边界(对应数组索引 < 0)。 * 2. 是否与工作区左侧已经固定的其他方块发生碰撞(即对应位置的值不为 0)。 * 如果遇到以上任意一种情况,则认为方块受到阻挡,无法继续左移。 * * @return bool 如果可以继续安全左移返回 true,否则返回 false。 */ bool CanMoveLeft() { for (int i = 0; i < 4; i++) { for (int j = 0; j < 4; j++) { if (bricks[type][state][i][j] != 0) { int nextY = point.y + i; int nextX = point.x + j - 1; // 检查是否到达左侧边界 if (nextX < 0) { return false; } // 检查左侧是否有其他固定方块 if (nextY >= 0 && workRegion[nextY][nextX] != 0) { return false; } } } } return true; } /** * @brief 判断当前方块是否可以继续向右移动。 * * 遍历当前处于活动状态下方块的 4x4 矩阵,计算其向右移动一步(X 坐标加 1)后的位置, * 并检查每个非空方块单元: * 1. 是否超出了游戏工作区的右侧边界(对应数组索引 >= 10)。 * 2. 是否与工作区右侧已经固定的其他方块发生碰撞(即对应位置的值不为 0)。 * 如果遇到以上任意一种情况,则认为方块受到阻挡,无法继续右移。 * * @return bool 如果可以继续安全右移返回 true,否则返回 false。 */ bool CanMoveRight() { for (int i = 0; i < 4; i++) { for (int j = 0; j < 4; j++) { if (bricks[type][state][i][j] != 0) { int nextY = point.y + i; int nextX = point.x + j + 1; // 检查是否到达右侧边界 if (nextX >= nGameWidth) { return false; } // 检查右侧是否有其他固定方块 if (nextY >= 0 && workRegion[nextY][nextX] != 0) { return false; } } } } return true; } static bool TryRotateWithOffset(int nextState, int offsetX) { Point rotatedPoint = point; rotatedPoint.x += offsetX; return IsPiecePlacementValid(type, nextState, rotatedPoint); } /** * @brief 将当前活动方块向下移动一格。 * * 该函数只负责修改当前活动方块的纵坐标,将其在工作区中的位置向下推进 1 格。 * 是否允许下移由外部配合 CanMoveDown 函数提前判断。 */ void MoveDown() { // 当前方块下移一格 point.y++; } /** * @brief 将当前活动方块向左移动一格。 * * 该函数只负责修改当前活动方块的横坐标,将其在工作区中的位置向左推进 1 格。 * 是否允许左移由外部配合 CanMoveLeft 函数提前判断。 */ void MoveLeft() { // 当前方块左移一格 point.x--; } /** * @brief 将当前活动方块向右移动一格。 * * 该函数只负责修改当前活动方块的横坐标,将其在工作区中的位置向右推进 1 格。 * 是否允许右移由外部配合 CanMoveRight 函数提前判断。 */ void MoveRight() { // 当前方块右移一格 point.x++; } /** * @brief 旋转当前活动方块到下一种朝向。 * * 游戏中的每种方块都预置了 4 种旋转状态,该函数会先尝试切换到下一状态, * 然后检查旋转后的方块是否越界或与固定方块重叠。 * 如果旋转后的状态非法,则恢复到旋转前的状态。 */ void Rotate() { int nextState = (state + 1) % 4; if (IsPiecePlacementValid(type, nextState, point)) { state = nextState; return; } if (currentMode == MODE_ROGUE && rogueStats.perfectRotateLevel > 0) { if (TryRotateWithOffset(nextState, -1)) { state = nextState; point.x--; return; } if (TryRotateWithOffset(nextState, 1)) { state = nextState; point.x++; return; } } } /** * @brief 让当前活动方块快速下落到当前位置能够到达的最低点。 * * 该函数会持续检查当前方块是否还能继续下移,只要可以下移就重复调用 MoveDown, * 直到方块到达底部或被其他固定方块阻挡为止。 */ void DropDown() { // 只要还能继续下落,就不断下移 while (CanMoveDown()) { MoveDown(); } } /** * @brief 将当前活动方块固定到工作区,并生成下一个活动方块。 * * 遍历当前方块 4x4 形状矩阵,把其中所有非空单元写入工作区数组, * 表示该方块已经落地并转为固定状态。 * 如果固定时仍有任意非空单元位于可视区域顶部之外,则判定游戏结束。 * 此时当前方块在可视区域内的部分仍会保留在工作区中。 * 若未超出顶部,再将“下一方块”切换为新的当前方块,重置旋转状态, * 并把新方块生成到工作区上方的初始位置,同时刷新预测落点。 */ void Fixing() { bool overflowTop = false; Point explosiveCells[4] = {}; int explosiveCellCount = 0; int rainbowFilledCount = 0; pendingChainBombFollowup = false; for (int i = 0; i < 4; i++) { for (int j = 0; j < 4; j++) { if (bricks[type][state][i][j] != 0) { int fixY = point.y + i; int fixX = point.x + j; // 只要当前方块任意非空单元仍超出顶部,就标记为结束 if (fixY < 0) { overflowTop = true; } // 将当前方块在可视区域内的部分写入工作区 if (fixY >= 0 && fixY < GetRoguePlayableHeight() && fixX >= 0 && fixX < nGameWidth) { workRegion[fixY][fixX] = currentPieceIsRainbow ? 8 : bricks[type][state][i][j]; if (currentPieceIsExplosive && explosiveCellCount < 4) { explosiveCells[explosiveCellCount].x = fixX; explosiveCells[explosiveCellCount].y = fixY; explosiveCellCount++; } } } } } if (!overflowTop && currentPieceIsRainbow) { rainbowFilledCount = TriggerRainbowRowCompletion(point.y, point.y + 3); if (rainbowFilledCount > 0) { TCHAR rainbowDetail[128]; _stprintf_s(rainbowDetail, _T("彩虹能量补齐 %d 个缺口,消行机会扩大。"), rainbowFilledCount); SetFeedbackMessage(_T("彩虹方块"), rainbowDetail, 10); } } if (overflowTop) { if (currentMode == MODE_ROGUE && rogueStats.terminalClearLevel > 0 && rogueStats.lastChanceCount > 0 && rogueStats.screenBombCount > 0) { rogueStats.lastChanceCount--; rogueStats.screenBombCount--; int clearedByTerminal = TriggerScreenBomb(); rogueStats.feverTicks = 10; currentFallInterval = GetRogueFallInterval(); TCHAR terminalDetail[128]; _stprintf_s( terminalDetail, _T("终末清场启动,清除 %d 格,并进入 10 秒狂热。"), clearedByTerminal); SetFeedbackMessage(_T("终末清场"), terminalDetail, 14); } else if (currentMode == MODE_ROGUE && rogueStats.lastChanceCount > 0) { rogueStats.lastChanceCount--; for (int i = 0; i < 3; i++) { DeleteOneLine(GetRoguePlayableHeight() - 1); } SetFeedbackMessage( _T("最后一搏"), _T("底部 3 行被清除,战局得以延续。"), 14); } else { gameOverFlag = true; return; } } if (currentPieceIsExplosive) { int explosiveCellsCleared = 0; for (int i = 0; i < explosiveCellCount; i++) { explosiveCellsCleared += ClearExplosiveAreaAt(explosiveCells[i].y, explosiveCells[i].x); } int explosiveScoreGain = 0; int explosiveExpGain = 0; if (currentMode == MODE_ROGUE && explosiveCellsCleared > 0) { AwardRogueSkillClearRewards(explosiveCellsCleared, explosiveScoreGain, explosiveExpGain, false); ApplyBoardGravity(); } TCHAR explosiveDetail[128]; _stprintf_s( explosiveDetail, _T("爆破清除 %d 格 +%d 分 +%d EXP"), explosiveCellsCleared, explosiveScoreGain, explosiveExpGain); SetFeedbackMessage(_T("爆破核心"), explosiveDetail, 12); if (rogueStats.chainBombLevel > 0 && explosiveCellCount > 0) { pendingChainBombCenter = explosiveCells[0]; pendingChainBombFollowup = true; } } if (currentPieceIsLaser) { int laserColumn = point.x + 1; if (laserColumn < 0) { laserColumn = 0; } if (laserColumn >= nGameWidth) { laserColumn = nGameWidth - 1; } int laserCellsCleared = ClearColumnAt(laserColumn); if (currentMode == MODE_ROGUE && laserCellsCleared > 0) { int laserScore = 0; int laserExp = 0; AwardRogueSkillClearRewards(laserCellsCleared, laserScore, laserExp, false); ApplyBoardGravity(); TCHAR laserDetail[128]; _stprintf_s(laserDetail, _T("激光贯穿一列,清除 %d 格 +%d 分 +%d EXP"), laserCellsCleared, laserScore, laserExp); SetFeedbackMessage(_T("棱镜激光"), laserDetail, 12); } } if (currentPieceIsCross) { int crossRow = point.y + 1; int crossColumn = point.x + 1; if (crossRow < 0) { crossRow = 0; } if (crossRow >= GetRoguePlayableHeight()) { crossRow = GetRoguePlayableHeight() - 1; } if (crossColumn < 0) { crossColumn = 0; } if (crossColumn >= nGameWidth) { crossColumn = nGameWidth - 1; } int crossCellsCleared = ClearRowAt(crossRow); int columnCellsCleared = ClearColumnAt(crossColumn); if (workRegion[crossRow][crossColumn] == 0 && columnCellsCleared > 0) { // center cell may already be counted by row clear } int totalCrossCleared = crossCellsCleared + columnCellsCleared; if (currentMode == MODE_ROGUE && totalCrossCleared > 0) { int crossScore = 0; int crossExp = 0; AwardRogueSkillClearRewards(totalCrossCleared, crossScore, crossExp, false); ApplyBoardGravity(); TCHAR crossDetail[128]; _stprintf_s(crossDetail, _T("十字冲击清除 %d 格 +%d 分 +%d EXP"), totalCrossCleared, crossScore, crossExp); SetFeedbackMessage(_T("十字方块"), crossDetail, 12); } } if (TryStabilizeBoard() > 0) { SetFeedbackMessage(_T("稳定结构"), _T("附近空洞被自动填补,阵型更加稳固。"), 10); } if (currentMode == MODE_ROGUE) { currentFallInterval = GetRogueFallInterval(); } // 生成下一个活动方块 type = ConsumeNextType(); nType = nextTypes[0]; state = 0; holdUsedThisTurn = false; RollCurrentPieceSpecialFlags(true); point = GetSpawnPoint(type); target = point; ComputeTarget(); } /** * @brief 删除指定行,并让其上方所有行整体下移一格。 * * 该函数会先将目标行上方的所有数据逐行向下复制, * 再把最顶端一行清空,从而完成一次标准的消行下移操作。 * * @param number 需要被删除的目标行号。 */ void DeleteOneLine(int number) { for (int i = number; i > 0; i--) { for (int j = 0; j < nGameWidth; j++) { workRegion[i][j] = workRegion[i - 1][j]; } } // 清空最顶端一行 for (int j = 0; j < nGameWidth; j++) { workRegion[0][j] = 0; } } /** * @brief 检查并删除所有已满的行,同时更新当前得分。 * * 该函数会从底部向上遍历工作区,判断每一行是否被完全填满。 * 如果某一行全部非 0,则调用 DeleteOneLine 删除该行, * 并将该行上方的内容整体下移。为了避免连续满行被漏检, * 删除后会继续检查当前行号。每成功消除 1 行,当前得分增加 100 分。 */ 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--) { bool fullLine = true; for (int j = 0; j < nGameWidth; j++) { if (workRegion[i][j] == 0) { fullLine = false; break; } } if (fullLine) { if (clearedRowCount < 8) { clearedRows[clearedRowCount] = i; clearedRowCount++; } for (int j = 0; j < nGameWidth; j++) { if (IsRainbowBoardCell(workRegion[i][j])) { clearedWithRainbow = true; break; } } DeleteOneLine(i); clearedLines++; i++; } } ApplyLineClearResult(clearedLines); if (currentScreen == SCREEN_UPGRADE) { QueueLineClearEffect(clearedRows, clearedRowCount, clearedLines); } else { TriggerLineClearEffect(clearedRows, clearedRowCount, clearedLines); } if (pendingChainBombFollowup && clearedLines > 0) { pendingChainBombFollowup = false; int followupCleared = 0; int centerY = pendingChainBombCenter.y; int centerX = pendingChainBombCenter.x; Point followupCells[9] = {}; for (int y = centerY - 1; y <= centerY + 1; y++) { for (int x = centerX - 1; x <= centerX + 1; x++) { 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++; } } } if (currentMode == MODE_ROGUE && followupCleared > 0) { 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 分 +%d EXP"), followupCleared, followupScore, followupExp); SetFeedbackMessage(_T("连环炸弹"), followupDetail, 12); } } else { pendingChainBombFollowup = false; } if (currentMode == MODE_ROGUE && clearedWithRainbow && rogueStats.voidCoreLevel > 0) { int miniBlackHoleCleared = TriggerMiniBlackHole(5); if (miniBlackHoleCleared > 0) { int miniScore = 0; int miniExp = 0; AwardRogueSkillClearRewards(miniBlackHoleCleared, miniScore, miniExp, false); ApplyBoardGravity(); TCHAR miniDetail[128]; _stprintf_s(miniDetail, _T("彩虹消行撕开小型黑洞,清除 %d 格 +%d 分 +%d EXP"), miniBlackHoleCleared, miniScore, miniExp); SetFeedbackMessage(_T("虚空核心"), miniDetail, 12); } } return clearedLines; } /** * @brief 计算当前活动方块的预测落点位置。 * * 该函数以当前活动方块的位置为起点,使用虚拟下落的方式不断尝试向下移动, * 直到方块无法继续下落为止。最终得到的最低可达位置会写入 target, * 供后续界面绘制瞄准器或落点提示时使用。 * * 计算过程中不会改变当前方块的真实位置 point。 */ void ComputeTarget() { Point originalPoint = point; // 从当前方块位置开始向下试探 target = point; while (CanMoveDown()) { point.y++; target = point; } // 恢复当前方块的真实位置 point = originalPoint; } /** * @brief 重置整个游戏状态,开始新的一局。 * * 该函数会清空工作区中的所有固定方块数据,重置分数、结束标记和暂停标记, * 并重新初始化当前方块、下一方块、旋转状态以及生成位置。 * 最后会重新计算一次当前方块的预测落点。 */ void Restart() { for (int i = 0; i < nGameHeight; i++) { for (int j = 0; j < nGameWidth; j++) { workRegion[i][j] = 0; } } gameOverFlag = false; suspendFlag = false; targetFlag = true; reviveAvailable = true; currentFallInterval = 500; ResetPlayerStats(classicStats, false); ResetPlayerStats(rogueStats, true); upgradeUiState.selectedIndex = 0; upgradeUiState.optionCount = 0; upgradeUiState.pendingCount = 0; upgradeUiState.totalChosenCount = 0; upgradeUiState.picksRemaining = 0; feedbackState.visibleTicks = 0; feedbackState.title[0] = _T('\0'); feedbackState.detail[0] = _T('\0'); ResetPendingRogueVisualEvents(); ResetVisualEffects(); pendingLineClearEffectTicks = 0; pendingLineClearEffectRowCount = 0; pendingLineClearEffectLineCount = 0; holdType = -1; holdUsedThisTurn = false; RollCurrentPieceSpecialFlags(false); tScore = 0; ResetNextQueue(); type = ConsumeNextType(); nType = nextTypes[0]; state = 0; holdUsedThisTurn = false; RollCurrentPieceSpecialFlags(true); point = GetSpawnPoint(type); target = point; 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; if (menuState.selectedIndex < 0 || menuState.selectedIndex >= menuState.optionCount) { menuState.selectedIndex = 0; } } void OpenRulesScreen() { currentScreen = SCREEN_RULES; suspendFlag = false; }