#include "stdafx.h" /** * @file TetrisCoreHelpers.cpp * @brief 存放基础逻辑框架函数之外的内部辅助流程。 */ #include "Tetris.h" #include "TetrisLogicInternal.h" /** * @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; } /** * @brief 收集当前方块将要固定到棋盘上的格子,并标记是否越过顶部。 * @param overflowTop 返回是否有方块格位于可视区域顶部之外。 * @param fixedCells 返回普通落地格,用于后续特殊效果定位。 * @param fixedCellCount 返回普通落地格数量。 * @param explosiveCells 返回爆破方块落地格。 * @param explosiveCellCount 返回爆破方块落地格数量。 */ void CollectAndWriteFixedCells( bool& overflowTop, Point fixedCells[], int& fixedCellCount, Point explosiveCells[], int& explosiveCellCount) { overflowTop = false; fixedCellCount = 0; explosiveCellCount = 0; for (int i = 0; i < 4; i++) { for (int j = 0; j < 4; j++) { if (bricks[type][state][i][j] == 0) { continue; } 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 (fixedCellCount < 4) { fixedCells[fixedCellCount].x = fixX; fixedCells[fixedCellCount].y = fixY; fixedCellCount++; } if (currentPieceIsExplosive && explosiveCellCount < 4) { explosiveCells[explosiveCellCount].x = fixX; explosiveCells[explosiveCellCount].y = fixY; explosiveCellCount++; } } } } } /** * @brief 处理方块固定时的顶部溢出、终末清场和最后一搏。 * @param overflowTop 是否出现顶部溢出。 * @return 溢出已被处理且游戏可以继续时返回 true;需要结束游戏时返回 false。 */ bool ResolveFixingOverflow(bool overflowTop) { if (!overflowTop) { return true; } // 终末清场优先级高于普通最后一搏,会消耗一次最后一搏和一枚清屏炸弹。 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); return true; } // 最后一搏只清理底部三行,让顶部溢出的局面获得一次继续机会。 if (currentMode == MODE_ROGUE && rogueStats.lastChanceCount > 0) { rogueStats.lastChanceCount--; for (int i = 0; i < 3; i++) { DeleteOneLine(GetRoguePlayableHeight() - 1); } SetFeedbackMessage( _T("最后一搏"), _T("底部 3 行被清除,战局得以延续。"), 14); return true; } gameOverFlag = true; return false; } /** * @brief 生成下一枚活动方块,并刷新 Hold、特殊方块和预测落点状态。 */ void SpawnNextFallingPiece() { // 消耗预览队列后重置本回合状态,确保 Hold 和特殊标记只影响新方块。 type = ConsumeNextType(); nType = nextTypes[0]; state = 0; holdUsedThisTurn = false; RollCurrentPieceSpecialFlags(true); point = GetSpawnPoint(type); target = point; ComputeTarget(); } /** * @brief 从底向上扫描满行并删除,记录本次消除的原始行号。 * @param clearedRows 返回最多 8 个被消除行号,用于播放消行动画。 * @param clearedRowCount 返回记录的行号数量。 * @return 本次标准消行数量。 */ int ScanAndDeleteFullLines(int clearedRows[], int& clearedRowCount) { int clearedLines = 0; clearedRowCount = 0; // 从底向上扫描,删除后 i++ 让当前位置继续检查新落下来的行。 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++; } DeleteOneLine(i); clearedLines++; i++; } } return clearedLines; } /** * @brief 根据当前界面状态立即播放或暂存消行动画。 * @param clearedRows 已消除行号数组。 * @param clearedRowCount 行号数量。 * @param clearedLines 本次消行数量。 */ void DispatchLineClearEffect(const int clearedRows[], int clearedRowCount, int clearedLines) { if (currentScreen == SCREEN_UPGRADE) { QueueLineClearEffect(clearedRows, clearedRowCount, clearedLines); } else { TriggerLineClearEffect(clearedRows, clearedRowCount, clearedLines); } } /** * @brief 处理连环炸弹因消行触发的一次追加 3x3 爆破。 * @param clearedLines 本次标准消行数量。 */ void ResolveChainBombFollowup(int clearedLines) { // 没有标准消行时,连环炸弹追加爆破不触发,并清掉挂起标记。 if (!pendingChainBombFollowup || clearedLines <= 0) { pendingChainBombFollowup = false; return; } pendingChainBombFollowup = false; // 追加爆破以第一次爆破落地点为中心,只执行一次 3x3 清除。 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); TCHAR followupDetail[128]; _stprintf_s( followupDetail, _T("追加爆炸清除 %d 格 +%d 分 +%d EXP"), followupCleared, followupScore, followupExp); SetFeedbackMessage(_T("连环炸弹"), followupDetail, 12); } }