#include "stdafx.h" /** * @file TetrisGameExtensions.cpp * @brief 实现玩家统计、视觉特效、模式切换、复活和帮助/致谢页面状态管理。 */ #include "TetrisLogicInternal.h" int pendingLineClearEffectTicks = 0; int pendingLineClearEffectRows[8] = {}; int pendingLineClearEffectRowCount = 0; int pendingLineClearEffectLineCount = 0; /** * @brief 重置经典或 Rogue 模式使用的玩家统计数据。 * @param stats 需要重置的统计结构。 * @param useRogueRules 是否按 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 设置界面右侧显示的即时反馈标题、内容和持续时间。 * @param title 反馈标题。 * @param detail 反馈详情。 * @param ticks 显示持续的游戏计时次数。 */ 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; } for (int i = 0; i < 64; i++) { cellFlashEffects[i].ticks = 0; } for (int i = 0; i < 80; i++) { gravityFallEffects[i].ticks = 0; } } /** * @brief 推进视觉效果计时,并返回是否仍有动画需要刷新。 * @return 仍有动画需要刷新返回 true,否则返回 false。 */ 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; } } for (int i = 0; i < 64; i++) { if (cellFlashEffects[i].ticks > 0) { cellFlashEffects[i].ticks--; active = true; } } for (int i = 0; i < 80; i++) { if (gravityFallEffects[i].ticks > 0) { gravityFallEffects[i].ticks--; active = true; } } return active; } /** * @brief 推进致谢页左右切换动画,并返回是否需要刷新界面。 * @return 需要刷新界面返回 true,否则返回 false。 */ bool TickCreditAnimation() { if (creditAnimationTicks > 0) { creditAnimationTicks--; return true; } return false; } /** * @brief 添加一段棋盘坐标系中的浮动文字效果。 * @param boardX 棋盘内部横坐标,使用 100 为一格的坐标系。 * @param boardY 棋盘内部纵坐标,使用 100 为一格的坐标系。 * @param text 浮动文字内容。 * @param color 文字颜色。 */ 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 添加一个棋盘坐标系中的粒子效果。 * @param boardX 粒子起始横坐标。 * @param boardY 粒子起始纵坐标。 * @param velocityX 横向速度。 * @param velocityY 纵向速度。 * @param size 粒子尺寸。 * @param color 粒子颜色。 */ 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 在指定棋盘坐标周围生成一组爆裂粒子。 * @param boardX 爆裂中心横坐标。 * @param boardY 爆裂中心纵坐标。 * @param baseColor 主粒子颜色。 * @param strongBurst 是否使用更强的粒子数量和速度。 */ 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 添加一个被清除格子的短时高亮效果。 * @param x 棋盘列号。 * @param y 棋盘行号。 * @param color 高亮颜色。 * @param strongFlash 是否使用更长的强高亮。 */ static void AddCellFlash(int x, int y, COLORREF color, bool strongFlash) { for (int i = 0; i < 64; i++) { if (cellFlashEffects[i].ticks <= 0) { cellFlashEffects[i].ticks = strongFlash ? 18 : 14; cellFlashEffects[i].totalTicks = cellFlashEffects[i].ticks; cellFlashEffects[i].x = x; cellFlashEffects[i].y = y; cellFlashEffects[i].color = color; return; } } } /** * @brief 暂存消行动画,等待升级选择结束后再播放。 * @param rows 被消除的行号数组。 * @param rowCount 行号数量。 * @param linesCleared 实际消除行数。 */ 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 触发标准消行动画和浮动文字。 * @param rows 被消除的行号数组。 * @param rowCount 行号数量。 * @param linesCleared 实际消除行数。 */ 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 为指定棋盘格集合触发清除粒子效果。 * @param cells 被清除格子数组。 * @param cellCount 格子数量。 * @param strongBurst 是否使用更强的粒子爆裂效果。 */ void TriggerCellClearEffect(const Point* cells, int cellCount, bool strongBurst) { TriggerColoredCellClearEffect(cells, cellCount, RGB(255, 238, 120), strongBurst); } /** * @brief 为指定棋盘格集合触发带颜色区分的清除高亮和粒子效果。 * @param cells 被清除格子数组。 * @param cellCount 格子数量。 * @param flashColor 高亮颜色。 * @param strongBurst 是否使用更强的粒子爆裂效果。 */ void TriggerColoredCellClearEffect(const Point* cells, int cellCount, COLORREF flashColor, 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]; AddCellFlash(cells[i].x, cells[i].y, flashColor, strongBurst); AddBurstParticles(cells[i].x * 100 + 50, cells[i].y * 100 + 50, particleColor, strongBurst); } } /** * @brief 为一个受重力下落的固定方块记录纵向残影和落点粒子。 * @param x 棋盘列号。 * @param fromY 起始行号。 * @param toY 目标行号。 * @param cellValue 方块格子值。 */ void TriggerGravityFallEffect(int x, int fromY, int toY, int cellValue) { if (x < 0 || x >= nGameWidth || fromY < 0 || fromY >= nGameHeight || toY < 0 || toY >= nGameHeight || toY <= fromY || cellValue == 0) { return; } int effectIndex = -1; for (int i = 0; i < 80; i++) { if (gravityFallEffects[i].ticks <= 0) { effectIndex = i; break; } } if (effectIndex < 0) { return; } int totalTicks = 12 + (toY - fromY) * 2; if (totalTicks > 26) { totalTicks = 26; } gravityFallEffects[effectIndex].ticks = totalTicks; gravityFallEffects[effectIndex].totalTicks = totalTicks; gravityFallEffects[effectIndex].x = x; gravityFallEffects[effectIndex].fromY = fromY; gravityFallEffects[effectIndex].toY = toY; gravityFallEffects[effectIndex].cellValue = cellValue; COLORREF particleColor = BrickColor[(cellValue - 1) % 7]; AddParticle(x * 100 + 50, toY * 100 + 18, -2 - rand() % 5, -12 - rand() % 7, 4, particleColor); AddParticle(x * 100 + 50, toY * 100 + 18, 2 + rand() % 5, -12 - rand() % 7, 4, particleColor); AddCellFlash(x, toY, RGB(210, 245, 255), true); } /** * @brief 判断指定方块、旋转状态和位置是否可以合法放置。 * @param pieceType 方块类型编号。 * @param pieceState 方块旋转状态。 * @param position 待检测的左上角坐标。 * @return 可以放置返回 true,否则返回 false。 */ 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 尝试把旋转后的方块横向偏移指定格数后放置。 * @param nextState 旋转后的状态编号。 * @param offsetX 横向试探偏移。 * @return 偏移后可以放置返回 true,否则返回 false。 */ 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 按指定模式开始新游戏。 * @param mode 游戏模式,取值来自 GameMode。 */ void StartGameWithMode(int mode) { rogueDemoMode = false; currentMode = mode; currentScreen = SCREEN_PLAYING; upgradeListScrollOffset = 0; Restart(); currentFallInterval = (currentMode == MODE_ROGUE) ? GetRogueFallInterval() : 500; tScore = (currentMode == MODE_CLASSIC) ? classicStats.score : rogueStats.score; } /** * @brief 返回主菜单并清理游戏中的临时界面状态。 */ void ReturnToMainMenu() { rogueDemoMode = false; currentScreen = SCREEN_MENU; suspendFlag = false; gameOverFlag = false; ResetVisualEffects(); ResetPendingRogueVisualEvents(); helpScrollOffset = 0; creditPageIndex = 0; creditAnimationTicks = 0; creditAnimationDirection = 0; upgradeListScrollOffset = 0; pendingLineClearEffectTicks = 0; pendingLineClearEffectRowCount = 0; pendingLineClearEffectLineCount = 0; menuState.optionCount = 4; ResetUpgradeUiState(); if (menuState.selectedIndex < 0 || menuState.selectedIndex >= menuState.optionCount) { menuState.selectedIndex = 0; } } /** * @brief 打开规则说明界面并重置说明页状态。 */ void OpenRulesScreen() { rogueDemoMode = false; currentScreen = SCREEN_RULES; suspendFlag = false; helpState.selectedIndex = 0; helpState.optionCount = 4; helpState.currentPage = 0; helpScrollOffset = 0; creditPageIndex = 0; creditAnimationTicks = 0; creditAnimationDirection = 0; } /** * @brief 打开 Rogue 技能演示选择页并重置帮助页状态。 */ void OpenSkillDemoScreen() { rogueDemoMode = false; currentScreen = SCREEN_RULES; suspendFlag = false; helpState.selectedIndex = 0; helpState.optionCount = 4; helpState.currentPage = 5; helpScrollOffset = 0; creditPageIndex = 0; creditAnimationTicks = 0; creditAnimationDirection = 0; } /** * @brief 打开致谢界面并重置致谢页切换状态。 */ void OpenCreditScreen() { rogueDemoMode = false; currentScreen = SCREEN_RULES; suspendFlag = false; helpState.selectedIndex = 0; helpState.optionCount = 4; helpState.currentPage = 4; helpScrollOffset = 0; creditPageIndex = 0; creditAnimationTicks = 0; creditAnimationDirection = 0; } /** * @brief 切换致谢页图片,并启动左右滑动动画。 * @param direction 小于 0 向前切换,大于 0 向后切换。 */ void ChangeCreditPage(int direction) { constexpr int creditPageCount = 5; if (direction == 0) { return; } int oldPageIndex = creditPageIndex; if (direction > 0) { creditPageIndex++; creditAnimationDirection = 1; } else { creditPageIndex--; creditAnimationDirection = -1; } if (creditPageIndex < 0) { creditPageIndex = creditPageCount - 1; } if (creditPageIndex >= creditPageCount) { creditPageIndex = 0; } if (creditPageIndex != oldPageIndex) { creditAnimationTicks = 60; } }