diff --git a/src/include/Tetris.h b/src/include/Tetris.h index 9579c97..d330de4 100644 --- a/src/include/Tetris.h +++ b/src/include/Tetris.h @@ -11,6 +11,7 @@ #pragma comment(lib, "winmm.lib") +// 棋盘和窗口基础尺寸,渲染层会按当前窗口大小统一缩放这些设计稿尺寸。 constexpr int GRID = 40; constexpr int nGameWidth = 10; constexpr int nGameHeight = 20; @@ -24,18 +25,27 @@ constexpr int WINDOW_CLIENT_WIDTH = WINDOW_PADDING * 2 + nGameWidth * GRID + SID constexpr int BOARD_CLIENT_HEIGHT = WINDOW_PADDING * 2 + nGameHeight * GRID + 20; constexpr int WINDOW_CLIENT_HEIGHT = (BOARD_CLIENT_HEIGHT > SIDE_PANEL_HEIGHT) ? BOARD_CLIENT_HEIGHT : SIDE_PANEL_HEIGHT; +/** + * @brief 棋盘坐标点,x 表示列号,y 表示行号。 + */ struct Point { int x; int y; }; +/** + * @brief 主菜单导航状态。 + */ struct MenuState { int selectedIndex; int optionCount; }; +/** + * @brief 帮助、规则、致谢和技能演示页面共享的导航状态。 + */ struct HelpState { int selectedIndex; @@ -43,6 +53,12 @@ struct HelpState int currentPage; }; +/** + * @brief 记录经典模式和 Rogue 模式的分数、等级、强化与临时状态。 + * + * 课程要求不使用 class,因此所有与玩家成长有关的数据都集中放在结构体字段中, + * 由逻辑层函数按过程式方式读取和修改。 + */ struct PlayerStats { int score; @@ -115,6 +131,9 @@ struct PlayerStats int pieceTuningLevels[7]; }; +/** + * @brief 升级界面中已经生成并显示给玩家的一个候选强化。 + */ struct UpgradeOption { int id; @@ -127,6 +146,9 @@ struct UpgradeOption const TCHAR* description; }; +/** + * @brief 强化池中的基础配置项,用于生成升级界面候选。 + */ struct UpgradeEntry { int id; @@ -138,6 +160,9 @@ struct UpgradeEntry const TCHAR* description; }; +/** + * @brief Rogue 升级选择界面的临时 UI 状态。 + */ struct UpgradeUiState { int selectedIndex; @@ -150,6 +175,9 @@ struct UpgradeUiState UpgradeOption options[6]; }; +/** + * @brief 右侧战斗日志或提示条的显示状态。 + */ struct FeedbackState { int visibleTicks; @@ -157,6 +185,9 @@ struct FeedbackState TCHAR detail[128]; }; +/** + * @brief 标准消行动画状态。 + */ struct ClearEffectState { int ticks; @@ -165,6 +196,9 @@ struct ClearEffectState int rows[8]; }; +/** + * @brief 棋盘上浮动文字特效的单个实例。 + */ struct FloatingTextEffect { int ticks; @@ -175,6 +209,9 @@ struct FloatingTextEffect COLORREF color; }; +/** + * @brief 棋盘粒子特效的单个实例。 + */ struct ParticleEffect { int ticks; @@ -187,6 +224,9 @@ struct ParticleEffect COLORREF color; }; +/** + * @brief 被清除格子的短时闪烁高亮状态。 + */ struct CellFlashEffect { int ticks; @@ -196,6 +236,9 @@ struct CellFlashEffect COLORREF color; }; +/** + * @brief 固定方块受重力下落时的残影轨迹状态。 + */ struct GravityFallEffect { int ticks; @@ -206,6 +249,9 @@ struct GravityFallEffect int cellValue; }; +/** + * @brief 当前应用所在的大界面。 + */ enum ScreenState { SCREEN_MENU = 0, @@ -214,12 +260,18 @@ enum ScreenState SCREEN_RULES = 3 }; +/** + * @brief 当前游戏玩法模式。 + */ enum GameMode { MODE_CLASSIC = 0, MODE_ROGUE = 1 }; +/** + * @brief 强化候选的稀有度,用于渲染不同颜色和排序说明。 + */ enum UpgradeRarity { UPGRADE_RARITY_COMMON = 0, @@ -227,6 +279,7 @@ enum UpgradeRarity UPGRADE_RARITY_RARE = 2 }; +// 以下全局状态沿用老师框架的过程式组织方式,各模块通过函数集中维护这些变量。 extern int nType; extern int type; extern int state; diff --git a/src/include/TetrisAppInternal.h b/src/include/TetrisAppInternal.h index f4f3fb8..79151d3 100644 --- a/src/include/TetrisAppInternal.h +++ b/src/include/TetrisAppInternal.h @@ -15,6 +15,12 @@ constexpr int GAME_TIMER_INTERVAL = 500; constexpr int EFFECT_TIMER_INTERVAL = 16; constexpr int CREDIT_TIMER_INTERVAL = 5; +/** + * @brief 当前窗口缩放后的布局参数。 + * + * 输入命中区域和渲染坐标必须使用同一套缩放参数,才能保证鼠标点击位置 + * 与屏幕上看到的按钮、卡片位置一致。 + */ struct LayoutMetrics { int scale; diff --git a/src/include/TetrisAssets.h b/src/include/TetrisAssets.h index 0fdb957..4e81675 100644 --- a/src/include/TetrisAssets.h +++ b/src/include/TetrisAssets.h @@ -8,6 +8,8 @@ #include "stdafx.h" #include +// 资源路径函数同时服务图片、音频和视频加载,调用方只传相对路径。 + /** * @brief 根据程序所在目录拼出项目资源文件的绝对路径。 * @param relativePath 相对于项目根目录的资源路径。 diff --git a/src/include/TetrisLogicInternal.h b/src/include/TetrisLogicInternal.h index a98f4d8..a213ab6 100644 --- a/src/include/TetrisLogicInternal.h +++ b/src/include/TetrisLogicInternal.h @@ -14,6 +14,8 @@ extern int pendingLineClearEffectRows[8]; extern int pendingLineClearEffectRowCount; extern int pendingLineClearEffectLineCount; +// Internal 头文件只暴露跨 cpp 文件共享的辅助函数,外部窗口层仍通过 Tetris.h 调用公开接口。 + /** * @brief 计算指定方块在棋盘顶部的统一生成位置。 * @param brickType 方块类型编号。 diff --git a/src/include/TetrisRenderInternal.h b/src/include/TetrisRenderInternal.h index 867519c..9012acc 100644 --- a/src/include/TetrisRenderInternal.h +++ b/src/include/TetrisRenderInternal.h @@ -9,6 +9,8 @@ #include #include +// 本内部头文件只给渲染拆分模块使用,外部代码仍通过 TDrawScreen 调用绘制入口。 + /** * @brief 加载并缓存主背景图片。 * @return 成功时返回缓存位图指针,失败时返回 nullptr。 diff --git a/src/include/resource.h b/src/include/resource.h index 9f8f29b..b7697c8 100644 --- a/src/include/resource.h +++ b/src/include/resource.h @@ -10,8 +10,10 @@ // 供 Tetris.rc 使用 // +// 字符串资源:窗口标题等文本由 Win32 启动流程按编号读取。 #define IDS_APP_TITLE 103 +// 图标、对话框、菜单和命令编号需要与 Tetris.rc 中的资源定义保持一致。 #define IDR_MAINFRAME 128 #define IDD_TETRIS_DIALOG 102 #define IDD_ABOUTBOX 103 @@ -24,11 +26,13 @@ #define IDC_MYICON 2 #ifndef IDC_STATIC +// 静态文本控件使用 -1,表示运行时不需要通过控件 ID 单独访问。 #define IDC_STATIC -1 #endif #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS +// 以下编号由资源编辑器维护,手工改动容易导致新增资源编号冲突。 #define _APS_NO_MFC 130 #define _APS_NEXT_RESOURCE_VALUE 129 #define _APS_NEXT_COMMAND_VALUE 32771 diff --git a/src/include/stdafx.h b/src/include/stdafx.h index 1a62466..92c6287 100644 --- a/src/include/stdafx.h +++ b/src/include/stdafx.h @@ -7,11 +7,12 @@ #include "targetver.h" +// 精简 Windows 头文件,缩短编译时间,同时保留本项目需要的 Win32/GDI API。 #define WIN32_LEAN_AND_MEAN // 从 Windows 头文件中排除极少使用的信息 // Windows 头文件: #include -// C 运行时头文件 +// C 运行时头文件:本项目使用随机数、内存工具、TCHAR 字符串和时间函数。 #include #include #include @@ -19,4 +20,4 @@ #include -// TODO: 在此处引用程序需要的其他头文件 +// 其他模块各自包含自己的业务头文件,避免预编译头承担过多项目依赖。 diff --git a/src/include/targetver.h b/src/include/targetver.h index 3d8b85c..60e5b1b 100644 --- a/src/include/targetver.h +++ b/src/include/targetver.h @@ -5,9 +5,9 @@ * @brief 设置 Windows SDK 目标平台版本,供 Win32 头文件选择可用 API。 */ -// 包括 SDKDDKVer.h 将定义可用的最高版本的 Windows 平台。 +// 包括 SDKDDKVer.h 将定义可用的最高版本 Windows 平台宏。 -// 如果要为以前的 Windows 平台生成应用程序,请包括 WinSDKVer.h,并将 -// WIN32_WINNT 宏设置为要支持的平台,然后再包括 SDKDDKVer.h。 +// 若课程演示环境需要兼容更旧 Windows,可在这里先包含 WinSDKVer.h, +// 再设置 WIN32_WINNT;当前项目直接使用 SDK 默认最高版本。 #include diff --git a/src/resources/Tetris.rc b/src/resources/Tetris.rc index 304d2a6..3512254 100644 Binary files a/src/resources/Tetris.rc and b/src/resources/Tetris.rc differ diff --git a/src/source/Tetris.cpp b/src/source/Tetris.cpp index 606bd6c..3d6430d 100644 --- a/src/source/Tetris.cpp +++ b/src/source/Tetris.cpp @@ -194,6 +194,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) break; case WM_COMMAND: { + // 处理资源菜单命令;自绘菜单的鼠标和键盘输入不走这里。 int wmId = LOWORD(wParam); switch (wmId) @@ -214,15 +215,19 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) } break; case WM_CREDIT_TICK: + // 多媒体定时器线程只投递消息,真正刷新仍回到窗口线程执行。 HandleCreditTick(hWnd); break; case WM_TIMER: + // 所有窗口定时器统一交给应用层计时器模块分发。 HandleTimerMessage(hWnd, wParam); break; case WM_SIZE: + // 窗口尺寸变化后重绘,布局函数会按新客户区重新计算缩放。 InvalidateRect(hWnd, nullptr, FALSE); break; case WM_LBUTTONUP: + // 输入模块未消费的鼠标消息继续交给 Win32 默认处理。 if (!HandleMouseClick(hWnd, lParam)) { return DefWindowProc(hWnd, message, wParam, lParam); @@ -235,6 +240,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) HandleKeyDown(hWnd, wParam); break; case WM_ERASEBKGND: + // 背景由双缓冲完整绘制,阻止系统擦背景可以减少闪烁。 return 1; case WM_PAINT: { @@ -251,6 +257,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) clientRect.bottom - clientRect.top); HBITMAP oldBitmap = (HBITMAP)SelectObject(memDC, memBitmap); + // 所有自绘内容先画到内存位图,再一次性复制到窗口。 TDrawScreen(memDC, hWnd); BitBlt( hdc, @@ -296,10 +303,12 @@ INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam) switch (message) { case WM_INITDIALOG: + // 资源模板标题是英文,这里在初始化时替换成中文标题。 SetWindowText(hDlg, _T("\u5173\u4e8e\u4fc4\u7f57\u65af\u65b9\u5757")); return (INT_PTR)TRUE; case WM_COMMAND: + // 关于框只需要响应确定和取消,其他命令交回默认流程。 if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL) { EndDialog(hDlg, LOWORD(wParam)); diff --git a/src/source/TetrisLogic.cpp b/src/source/TetrisLogic.cpp index 7230476..5a4c78a 100644 --- a/src/source/TetrisLogic.cpp +++ b/src/source/TetrisLogic.cpp @@ -50,6 +50,7 @@ bool pendingChainBombFollowup = false; int bricks[7][4][4][4] = { + // 方块形状表:7 种方块、每种 4 个旋转状态、每个状态使用 4x4 矩阵描述。 { {{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}}, @@ -96,6 +97,7 @@ int bricks[7][4][4][4] = COLORREF BrickColor[7] = { + // 渲染层按方块编号取色;数组顺序必须与 bricks 中的类型编号一致。 RGB(244, 144, 165), RGB(255, 181, 197), RGB(170, 215, 255), @@ -270,6 +272,7 @@ void MoveRight() */ void Rotate() { + // 第一阶段:直接尝试原地旋转。 int nextState = (state + 1) % 4; if (IsPiecePlacementValid(type, nextState, point)) { @@ -279,6 +282,7 @@ void Rotate() if (currentMode == MODE_ROGUE && rogueStats.perfectRotateLevel > 0) { + // 第二阶段:Rogue 完美旋转解锁后,尝试左右各一格的墙踢修正。 if (TryRotateWithOffset(nextState, -1)) { state = nextState; @@ -322,6 +326,7 @@ void DropDown() */ void Fixing() { + // 第一阶段:收集落地格子,并把可见区域内的格子写入工作区。 bool overflowTop = false; Point fixedCells[4] = {}; int fixedCellCount = 0; @@ -331,13 +336,16 @@ void Fixing() CollectAndWriteFixedCells(overflowTop, fixedCells, fixedCellCount, explosiveCells, explosiveCellCount); + // 第二阶段:彩虹方块先按落地中心行处理染色与清除。 ApplyRainbowLandingEffect(overflowTop, fixedCells, fixedCellCount); + // 第三阶段:统一处理顶部溢出,可能触发最后一搏或直接游戏结束。 if (!ResolveFixingOverflow(overflowTop)) { return; } + // 第四阶段:结算爆破、激光、十字和稳定结构等普通特殊落地效果。 ApplySpecialLandingEffects(fixedCells, fixedCellCount, explosiveCells, explosiveCellCount); if (currentMode == MODE_ROGUE) @@ -345,6 +353,7 @@ void Fixing() currentFallInterval = GetRogueFallInterval(); } + // 第五阶段:刷新下一枚活动方块,开始新的下落回合。 SpawnNextFallingPiece(); } @@ -447,6 +456,7 @@ void ComputeTarget() */ void Restart() { + // 第一阶段:清空棋盘数组,移除上一局所有固定方块。 for (int i = 0; i < nGameHeight; i++) { for (int j = 0; j < nGameWidth; j++) @@ -455,12 +465,14 @@ void Restart() } } + // 第二阶段:恢复基本游戏标志和默认下落速度。 gameOverFlag = false; suspendFlag = false; targetFlag = true; reviveAvailable = true; currentFallInterval = 500; + // 第三阶段:重置两种模式的统计、升级 UI、反馈和所有视觉特效。 ResetPlayerStats(classicStats, false); ResetPlayerStats(rogueStats, true); ResetUpgradeUiState(); @@ -477,6 +489,7 @@ void Restart() RollCurrentPieceSpecialFlags(false); tScore = 0; + // 第四阶段:初始化下一方块队列,并生成当前活动方块。 ResetNextQueue(); type = ConsumeNextType(); nType = nextTypes[0]; diff --git a/src/source/TetrisRender.cpp b/src/source/TetrisRender.cpp index 6fb9c7b..e4d1cb1 100644 --- a/src/source/TetrisRender.cpp +++ b/src/source/TetrisRender.cpp @@ -18,5 +18,6 @@ */ void TDrawScreen(HDC hdc, HWND hWnd) { + // 保留老师框架中的绘制函数名,内部委托到拆分后的完整渲染实现。 RenderFullScreen(hdc, hWnd); } diff --git a/src/source/app/TetrisInput.cpp b/src/source/app/TetrisInput.cpp index 44ab2d0..4f793f2 100644 --- a/src/source/app/TetrisInput.cpp +++ b/src/source/app/TetrisInput.cpp @@ -775,6 +775,7 @@ static bool HandleDemoPlayingKey(HWND hWnd, WPARAM key) */ static bool HandleBattleControlKey(HWND hWnd, WPARAM key) { + // 菜单、重开和暂停只对真实战局开放,避免破坏技能演示预设流程。 if (!IsRogueSkillDemoMode() && key == 'M') { ReturnToMainMenu(); @@ -799,6 +800,7 @@ static bool HandleBattleControlKey(HWND hWnd, WPARAM key) if (key == 'G') { + // 落点提示是显示开关,不改变棋盘或方块状态。 targetFlag = !targetFlag; InvalidateRect(hWnd, nullptr, FALSE); return true; @@ -806,6 +808,7 @@ static bool HandleBattleControlKey(HWND hWnd, WPARAM key) if (gameOverFlag && reviveAvailable && key == 'V') { + // 复活机会只有视频成功播放后才消耗,失败时保留机会并给出反馈。 if (PlayReviveVideo(hWnd)) { ReviveAfterVideo(); @@ -832,6 +835,7 @@ static bool HandleRogueSkillKey(HWND hWnd, WPARAM key) { if (currentMode == MODE_ROGUE && (key == 'J' || key == 'K')) { + // Rogue 侧栏强化列表较长,J/K 只调整说明列表的滚动位置。 int direction = (key == 'J') ? 1 : -1; AdjustScrollOffset(upgradeListScrollOffset, direction * GetScrollStep(hWnd, 52)); InvalidateRect(hWnd, nullptr, FALSE); @@ -840,6 +844,7 @@ static bool HandleRogueSkillKey(HWND hWnd, WPARAM key) switch (key) { + // 主动技能和 Hold 的按键统一在这里分发,技能内部会自行检查次数和模式。 case 'C': case VK_SHIFT: case VK_LSHIFT: @@ -865,6 +870,7 @@ static bool HandleRogueSkillKey(HWND hWnd, WPARAM key) */ static void FixPieceAndResolveLines() { + // 固定方块后立即检查满行,Rogue 模式还可能因为经验变化打开升级界面。 Fixing(); if (!gameOverFlag) { @@ -898,6 +904,7 @@ static bool HandlePieceMovementKey(WPARAM key) return true; case VK_DOWN: case 'S': + // 软降被阻挡时等价于本回合落地,立即进入固定和消行流程。 if (CanMoveDown()) { MoveDown(); @@ -912,6 +919,7 @@ static bool HandlePieceMovementKey(WPARAM key) Rotate(); return true; case VK_SPACE: + // 硬降先移动到最低合法位置,再一次性固定结算。 DropDown(); FixPieceAndResolveLines(); return true; @@ -958,6 +966,7 @@ static void HandlePlayingKey(HWND hWnd, WPARAM key) */ void HandleKeyDown(HWND hWnd, WPARAM wParam) { + // 按当前界面从上到下分发:菜单、帮助、升级界面优先消费按键。 if (HandleMenuKey(hWnd, wParam) || HandleRulesKey(hWnd, wParam) || HandleUpgradeKey(hWnd, wParam)) diff --git a/src/source/app/TetrisLayout.cpp b/src/source/app/TetrisLayout.cpp index df13d92..083c62a 100644 --- a/src/source/app/TetrisLayout.cpp +++ b/src/source/app/TetrisLayout.cpp @@ -13,6 +13,7 @@ */ void AdjustScrollOffset(int& scrollOffset, int delta) { + // 先应用本次滚动增量,再统一夹紧到允许范围内。 scrollOffset += delta; if (scrollOffset < 0) { @@ -46,6 +47,7 @@ LayoutMetrics GetLayoutMetrics(HWND hWnd) RECT clientRect; GetClientRect(hWnd, &clientRect); + // 以设计稿窗口为基准计算缩放,取较小比例保证完整界面不被裁切。 int clientWidth = clientRect.right - clientRect.left; int clientHeight = clientRect.bottom - clientRect.top; int scaleX = MulDiv(clientWidth, 1000, WINDOW_CLIENT_WIDTH); @@ -60,6 +62,7 @@ LayoutMetrics GetLayoutMetrics(HWND hWnd) metrics.scale = scale; metrics.layoutWidth = MulDiv(WINDOW_CLIENT_WIDTH, scale, 1000); metrics.layoutHeight = MulDiv(WINDOW_CLIENT_HEIGHT, scale, 1000); + // 横向居中显示,纵向从顶部开始,方便窗口高度不足时保持棋盘起点稳定。 metrics.offsetX = (clientWidth - metrics.layoutWidth) / 2; metrics.offsetY = 0; metrics.grid = MulDiv(GRID, scale, 1000); @@ -289,6 +292,8 @@ RECT GetUpgradeCardRect(HWND hWnd, int index) { LayoutMetrics metrics = GetLayoutMetrics(hWnd); RECT overlayRect = GetUpgradeOverlayRect(hWnd); + + // 根据当前候选数量自动决定列数;最多三列,两行用于命运轮盘六选项。 int gap = ScaleValue(metrics, 18); int horizontalPadding = ScaleValue(metrics, 36); int verticalTop = overlayRect.top + ScaleValue(metrics, 138); @@ -347,6 +352,8 @@ RECT GetOverlayButtonRect(HWND hWnd, int index, int buttonCount) { LayoutMetrics metrics = GetLayoutMetrics(hWnd); RECT overlayRect = GetGameOverlayRect(hWnd); + + // 游戏结束可能有三个按钮,暂停只有两个按钮,因此间距和边距分开计算。 int gap = buttonCount == 3 ? ScaleValue(metrics, 8) : ScaleValue(metrics, 18); int sidePadding = buttonCount == 3 ? ScaleValue(metrics, 14) : ScaleValue(metrics, 34); int width = (overlayRect.right - overlayRect.left - sidePadding * 2 - gap * (buttonCount - 1)) / buttonCount; @@ -383,6 +390,8 @@ RECT GetBackButtonRect(HWND hWnd) RECT GetMusicButtonRect(HWND hWnd) { LayoutMetrics metrics = GetLayoutMetrics(hWnd); + + // 音乐按钮保持最小可点击尺寸,避免窗口缩小时变得难以点中。 int size = ScaleValue(metrics, 28); if (size < 22) { diff --git a/src/source/app/TetrisMedia.cpp b/src/source/app/TetrisMedia.cpp index 006feda..fcc6fd8 100644 --- a/src/source/app/TetrisMedia.cpp +++ b/src/source/app/TetrisMedia.cpp @@ -23,6 +23,7 @@ static constexpr const wchar_t* kReviveVideoAlias = L"TereisReviveVideo"; */ static bool TryPlayMciLoop(const std::wstring& path, bool forceMpegVideo) { + // 资源不存在时直接失败,让上层继续尝试下一个候选路径或格式。 if (!FileExists(path)) { return false; @@ -61,6 +62,7 @@ static bool TryPlayMciLoop(const std::wstring& path, bool forceMpegVideo) */ void StopBackgroundMusic() { + // 根据当前播放方式选择对应的释放接口,避免 MCI 设备或 PlaySound 残留。 if (bgmUsingMci) { mciSendStringW((std::wstring(L"stop ") + kBgmAlias).c_str(), nullptr, 0, nullptr); @@ -80,6 +82,7 @@ void StopBackgroundMusic() */ void StartBackgroundMusic() { + // 音乐被关闭或已经在播放时,不重复查找资源和启动设备。 if (!bgmEnabled || bgmPlaying) { return; @@ -94,6 +97,7 @@ void StartBackgroundMusic() for (const std::wstring& candidate : bgmWavCandidates) { + // WAV 优先使用 PlaySound,依赖少、兼容性最好。 if (FileExists(candidate) && PlaySoundW(candidate.c_str(), nullptr, SND_FILENAME | SND_ASYNC | SND_LOOP)) { @@ -112,6 +116,7 @@ void StartBackgroundMusic() for (const std::wstring& candidate : oggCandidates) { + // OGG 通过 MCI 尝试普通打开和 mpegvideo 强制类型两条路径。 if (TryPlayMciLoop(candidate, false) || TryPlayMciLoop(candidate, true)) { return; @@ -127,6 +132,7 @@ void StartBackgroundMusic() for (const std::wstring& candidate : fallbackWavCandidates) { + // 兼容旧资源名 background.wav,保证替换素材后仍能播放。 if (FileExists(candidate) && PlaySoundW(candidate.c_str(), nullptr, SND_FILENAME | SND_ASYNC | SND_LOOP)) { @@ -164,6 +170,7 @@ void ToggleBackgroundMusic(HWND hWnd) */ bool PlayReviveVideo(HWND hWnd) { + // 依次查找 AVI 和 MP4,并同时支持构建目录与项目根目录运行。 std::wstring videoPath = BuildAssetPath(L"assets\\video\\video.avi"); if (!FileExists(videoPath)) { @@ -185,6 +192,7 @@ bool PlayReviveVideo(HWND hWnd) bool shouldResumeBgm = bgmEnabled; if (bgmPlaying) { + // 视频播放期间暂停背景音乐,播放结束后按开关状态恢复。 StopBackgroundMusic(); } @@ -214,6 +222,7 @@ bool PlayReviveVideo(HWND hWnd) if (!played) { + // MCI 全屏播放失败时退回系统默认播放器,并等待播放器进程结束。 SHELLEXECUTEINFOW shellInfo = {}; shellInfo.cbSize = sizeof(shellInfo); shellInfo.fMask = SEE_MASK_NOCLOSEPROCESS; diff --git a/src/source/app/TetrisTimers.cpp b/src/source/app/TetrisTimers.cpp index d0e653f..5b3e3d8 100644 --- a/src/source/app/TetrisTimers.cpp +++ b/src/source/app/TetrisTimers.cpp @@ -27,6 +27,7 @@ static void CALLBACK CreditTimerCallback(UINT, UINT, DWORD_PTR userData, DWORD_P */ void ResetGameTimer(HWND hWnd) { + // 下落速度会被 Rogue 强化和临时状态动态修改,因此每次变化都重新注册定时器。 KillTimer(hWnd, GAME_TIMER_ID); SetTimer(hWnd, GAME_TIMER_ID, currentFallInterval > 0 ? currentFallInterval : GAME_TIMER_INTERVAL, nullptr); } @@ -37,8 +38,11 @@ void ResetGameTimer(HWND hWnd) */ void StartAppTimers(HWND hWnd) { + // 主定时器负责方块下落,特效定时器负责高帧率视觉动画。 ResetGameTimer(hWnd); SetTimer(hWnd, EFFECT_TIMER_ID, EFFECT_TIMER_INTERVAL, nullptr); + + // 致谢页动画需要更高刷新频率,优先使用多媒体定时器。 creditTimerHandle = timeSetEvent( CREDIT_TIMER_INTERVAL, 1, @@ -58,6 +62,7 @@ void StartAppTimers(HWND hWnd) */ void StopAppTimers(HWND hWnd) { + // 退出或窗口销毁时释放所有可能创建过的计时器资源。 KillTimer(hWnd, GAME_TIMER_ID); KillTimer(hWnd, EFFECT_TIMER_ID); if (creditTimerHandle != 0) @@ -92,6 +97,7 @@ static bool TickRogueTimedStates(HWND hWnd) { bool shouldRefresh = false; + // 狂热、缓流、极限缓速和 Hold 缓速都会影响下落间隔,需要同步重置主定时器。 if (currentMode == MODE_ROGUE && !IsRogueSkillDemoMode() && rogueStats.feverTicks > 0) { rogueStats.feverTicks--; @@ -139,6 +145,7 @@ static bool TickRogueTimedStates(HWND hWnd) */ static bool TickExtremeDanger(HWND hWnd) { + // 极限玩家只在真实 Rogue 战局中计时,暂停、结束和技能演示都不推进危险等级。 if (currentMode != MODE_ROGUE || IsRogueSkillDemoMode() || rogueStats.extremePlayerLevel <= 0 || @@ -151,10 +158,12 @@ static bool TickExtremeDanger(HWND hWnd) if (rogueStats.extremeDangerTicks > 0) { + // 计时尚未结束时只递减倒计时,不改变速度。 rogueStats.extremeDangerTicks--; return false; } + // 每 30 个主计时周期未完成四消就提高危险等级,并立即刷新下落速度。 rogueStats.extremeDangerTicks = 30; if (rogueStats.extremeDangerLevel < 5) { @@ -176,6 +185,7 @@ static bool TickExtremeDanger(HWND hWnd) */ static bool TryStartTimeDilation(HWND hWnd) { + // 时间缓流是自动保命效果,已经在持续时不会重复触发。 if (currentMode != MODE_ROGUE || IsRogueSkillDemoMode() || rogueStats.timeDilationLevel <= 0 || @@ -186,6 +196,7 @@ static bool TryStartTimeDilation(HWND hWnd) int occupiedHeight = 0; int playableHeight = GetRoguePlayableHeight(); + // 自上而下寻找第一行有方块的位置,由此换算当前堆叠高度。 for (int y = 0; y < playableHeight; y++) { bool hasCell = false; @@ -231,6 +242,7 @@ static bool TickGameFall(HWND hWnd) return false; } + // Rogue 难度随时间推进,速度变化后需要重新安排下一次自动下落。 if (currentMode == MODE_ROGUE && !IsRogueSkillDemoMode()) { int previousFallInterval = currentFallInterval; @@ -243,6 +255,7 @@ static bool TickGameFall(HWND hWnd) TryStartTimeDilation(hWnd); + // 能下落时只移动一格;被阻挡时固定方块,并进入消行与升级结算。 if (CanMoveDown()) { MoveDown(); @@ -259,6 +272,7 @@ static bool TickGameFall(HWND hWnd) if (!gameOverFlag) { + // 真实方块位置变化后刷新预测落点,供渲染层绘制目标提示。 ComputeTarget(); } @@ -274,6 +288,7 @@ void HandleTimerMessage(HWND hWnd, WPARAM timerId) { if (timerId == EFFECT_TIMER_ID) { + // 视觉特效独立于主下落速度,用固定帧率推进。 if (TickVisualEffects()) { InvalidateRect(hWnd, nullptr, FALSE); @@ -283,6 +298,7 @@ void HandleTimerMessage(HWND hWnd, WPARAM timerId) if (timerId == CREDIT_TIMER_ID && creditTimerHandle == 0) { + // 多媒体定时器不可用时,普通窗口定时器承担致谢页动画刷新。 HandleCreditTick(hWnd); return; } @@ -295,10 +311,12 @@ void HandleTimerMessage(HWND hWnd, WPARAM timerId) bool shouldRefresh = false; if (feedbackState.visibleTicks > 0) { + // 右侧反馈信息按主计时周期自动消退。 feedbackState.visibleTicks--; shouldRefresh = true; } + // 主定时器集中推进演示、Rogue 临时状态、危险等级和自然下落。 if (IsRogueSkillDemoMode() && TickRogueSkillDemo()) { shouldRefresh = true; diff --git a/src/source/common/TetrisAssets.cpp b/src/source/common/TetrisAssets.cpp index bffa11d..76cf2de 100644 --- a/src/source/common/TetrisAssets.cpp +++ b/src/source/common/TetrisAssets.cpp @@ -20,6 +20,7 @@ std::wstring BuildAssetPath(const wchar_t* relativePath) wchar_t modulePath[MAX_PATH] = {}; GetModuleFileNameW(nullptr, modulePath, MAX_PATH); + // 先取可执行文件所在目录,再根据构建目录层级回到项目根目录。 std::wstring basePath(modulePath); size_t lastSlash = basePath.find_last_of(L"\\/"); if (lastSlash != std::wstring::npos) @@ -56,6 +57,7 @@ std::wstring BuildWorkingDirAssetPath(const wchar_t* relativePath) return L""; } + // 当前工作目录可能已经是项目根目录,直接拼接相对资源路径。 std::wstring candidate = std::wstring(currentDirectory) + L"\\" + relativePath; wchar_t fullPath[MAX_PATH] = {}; DWORD result = GetFullPathNameW(candidate.c_str(), MAX_PATH, fullPath, nullptr); @@ -74,6 +76,7 @@ std::wstring BuildWorkingDirAssetPath(const wchar_t* relativePath) */ bool FileExists(const std::wstring& path) { + // 目录不能作为媒体或图片资源使用,因此排除 FILE_ATTRIBUTE_DIRECTORY。 DWORD attributes = GetFileAttributesW(path.c_str()); return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) == 0; } diff --git a/src/source/extensions/TetrisGameExtensions.cpp b/src/source/extensions/TetrisGameExtensions.cpp index e1f427a..29159d9 100644 --- a/src/source/extensions/TetrisGameExtensions.cpp +++ b/src/source/extensions/TetrisGameExtensions.cpp @@ -18,10 +18,12 @@ int pendingLineClearEffectLineCount = 0; */ 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; @@ -84,6 +86,7 @@ void ResetPlayerStats(PlayerStats& stats, bool useRogueRules) stats.difficultyElapsedMs = 0; stats.difficultyLevel = 0; stats.lockedRows = 0; + // 方块改造按 7 种方块分别记录等级,重开时逐项清空。 for (int i = 0; i < 7; i++) { stats.pieceTuningLevels[i] = 0; @@ -98,6 +101,7 @@ void ResetPlayerStats(PlayerStats& stats, bool useRogueRules) */ void SetFeedbackMessage(const TCHAR* title, const TCHAR* detail, int ticks) { + // 使用 lstrcpyn 限长复制,避免长描述写出固定缓冲区。 feedbackState.visibleTicks = ticks; lstrcpyn(feedbackState.title, title, sizeof(feedbackState.title) / sizeof(TCHAR)); lstrcpyn(feedbackState.detail, detail, sizeof(feedbackState.detail) / sizeof(TCHAR)); @@ -108,6 +112,7 @@ void SetFeedbackMessage(const TCHAR* title, const TCHAR* detail, int ticks) */ void ResetVisualEffects() { + // 主状态和各类效果槽位只需把 ticks 清零,渲染层会自动忽略非活动项。 clearEffectState.ticks = 0; clearEffectState.totalTicks = 0; clearEffectState.rowCount = 0; @@ -141,6 +146,7 @@ bool TickVisualEffects() { bool active = false; + // 所有效果共用倒计时推进,任意效果仍活动就请求界面刷新。 if (clearEffectState.ticks > 0) { clearEffectState.ticks--; @@ -210,6 +216,7 @@ bool TickCreditAnimation() */ static void AddFloatingText(int boardX, int boardY, const TCHAR* text, COLORREF color) { + // 复用第一个空闲槽位,槽位满时丢弃新效果,避免动态分配。 for (int i = 0; i < 8; i++) { if (floatingTextEffects[i].ticks <= 0) @@ -589,6 +596,7 @@ bool TryRotateWithOffset(int nextState, int offsetX) */ void ReviveAfterVideo() { + // 只有游戏结束且复活机会仍在时才能进入复活流程。 if (!gameOverFlag || !reviveAvailable) { return; @@ -606,6 +614,7 @@ void ReviveAfterVideo() rowsToClear = 5; } + // 清理顶部一段空间,避免新方块刚生成又立即判定失败。 for (int y = 0; y < rowsToClear && y < playableHeight; y++) { for (int x = 0; x < nGameWidth; x++) @@ -614,6 +623,7 @@ void ReviveAfterVideo() } } + // 复活后重新取一个活动方块,并刷新落点提示。 type = ConsumeNextType(); nType = nextTypes[0]; state = 0; @@ -632,6 +642,7 @@ void ReviveAfterVideo() */ void StartGameWithMode(int mode) { + // 模式切换后直接复用 Restart,保证经典和 Rogue 都从干净状态开始。 rogueDemoMode = false; currentMode = mode; currentScreen = SCREEN_PLAYING; @@ -646,6 +657,7 @@ void StartGameWithMode(int mode) */ void ReturnToMainMenu() { + // 回到主菜单时关闭所有临时战局、帮助页和升级界面状态。 rogueDemoMode = false; currentScreen = SCREEN_MENU; suspendFlag = false; @@ -732,6 +744,7 @@ void ChangeCreditPage(int direction) return; } + // 页码循环切换,同时记录动画方向用于渲染滑动效果。 int oldPageIndex = creditPageIndex; if (direction > 0) { diff --git a/src/source/logic/TetrisCoreHelpers.cpp b/src/source/logic/TetrisCoreHelpers.cpp index 26e55f8..bc164ff 100644 --- a/src/source/logic/TetrisCoreHelpers.cpp +++ b/src/source/logic/TetrisCoreHelpers.cpp @@ -22,6 +22,7 @@ */ static void GetBrickBounds(int brickType, int brickState, int& minRow, int& maxRow, int& minCol, int& maxCol) { + // 初始值设置在矩阵之外,遍历到第一个非空格后会被收缩到真实边界。 minRow = 4; maxRow = -1; minCol = 4; @@ -69,6 +70,7 @@ 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; @@ -147,6 +149,7 @@ bool ResolveFixingOverflow(bool overflowTop) return true; } + // 终末清场优先级高于普通最后一搏,会消耗一次最后一搏和一枚清屏炸弹。 if (currentMode == MODE_ROGUE && rogueStats.terminalClearLevel > 0 && rogueStats.lastChanceCount > 0 && rogueStats.screenBombCount > 0) { rogueStats.lastChanceCount--; @@ -165,6 +168,7 @@ bool ResolveFixingOverflow(bool overflowTop) return true; } + // 最后一搏只清理底部三行,让顶部溢出的局面获得一次继续机会。 if (currentMode == MODE_ROGUE && rogueStats.lastChanceCount > 0) { rogueStats.lastChanceCount--; @@ -212,6 +216,7 @@ int ScanAndDeleteFullLines(int clearedRows[], int& clearedRowCount) int clearedLines = 0; clearedRowCount = 0; + // 从底向上扫描,删除后 i++ 让当前位置继续检查新落下来的行。 int playableHeight = GetRoguePlayableHeight(); for (int i = playableHeight - 1; i >= 0; i--) { @@ -267,6 +272,7 @@ void DispatchLineClearEffect(const int clearedRows[], int clearedRowCount, int c */ void ResolveChainBombFollowup(int clearedLines) { + // 没有标准消行时,连环炸弹追加爆破不触发,并清掉挂起标记。 if (!pendingChainBombFollowup || clearedLines <= 0) { pendingChainBombFollowup = false; @@ -275,6 +281,7 @@ void ResolveChainBombFollowup(int clearedLines) pendingChainBombFollowup = false; + // 追加爆破以第一次爆破落地点为中心,只执行一次 3x3 清除。 int followupCleared = 0; int centerY = pendingChainBombCenter.y; int centerX = pendingChainBombCenter.x; diff --git a/src/source/logic/TetrisPieceEffects.cpp b/src/source/logic/TetrisPieceEffects.cpp index cf1de84..968c714 100644 --- a/src/source/logic/TetrisPieceEffects.cpp +++ b/src/source/logic/TetrisPieceEffects.cpp @@ -14,6 +14,7 @@ */ void ApplyRainbowLandingEffect(bool overflowTop, const Point* fixedCells, int fixedCellCount) { + // 顶部溢出时优先交给失败/复活逻辑处理,避免在不可见区域触发奖励。 if (overflowTop || !currentPieceIsRainbow) { return; @@ -39,6 +40,7 @@ void ApplyRainbowLandingEffect(bool overflowTop, const Point* fixedCells, int fi rainbowAnchorRow = GetRoguePlayableHeight() - 1; } + // 第二阶段:按锚点行执行彩虹清除和覆盖行染色。 int rainbowRecoloredCount = 0; int rainbowClearedCount = TriggerRainbowColorShift(rainbowAnchorRow, point.y, point.y + 3, rainbowRecoloredCount); int rainbowScore = 0; @@ -48,6 +50,7 @@ void ApplyRainbowLandingEffect(bool overflowTop, const Point* fixedCells, int fi int voidExp = 0; if (currentMode == MODE_ROGUE && rainbowClearedCount > 0) { + // Rogue 模式下特殊清除也能获得得分和经验,但不直接触发升级菜单。 AwardRogueSkillClearRewards(rainbowClearedCount, rainbowScore, rainbowExp, false); if (rogueStats.voidCoreLevel > 0) { @@ -86,11 +89,13 @@ void ApplyRainbowLandingEffect(bool overflowTop, const Point* fixedCells, int fi */ static void ApplyExplosiveLandingEffect(const Point* explosiveCells, int explosiveCellCount) { + // 非爆破方块直接跳过,保持普通方块落地流程轻量。 if (!currentPieceIsExplosive) { return; } + // 每个落地格都作为爆心清除范围,连环炸弹会扩大底层清除函数的范围。 int explosiveCellsCleared = 0; for (int i = 0; i < explosiveCellCount; i++) { @@ -128,6 +133,7 @@ static void ApplyExplosiveLandingEffect(const Point* explosiveCells, int explosi */ static void ApplyLaserLandingEffect(const Point* fixedCells, int fixedCellCount) { + // 激光方块以落地格平均列作为贯穿列,减少不同形状造成的位置偏差。 if (!currentPieceIsLaser) { return; @@ -172,6 +178,7 @@ static void ApplyLaserLandingEffect(const Point* fixedCells, int fixedCellCount) */ static void ApplyCrossLandingEffect(const Point* fixedCells, int fixedCellCount) { + // 十字方块同时计算中心行和中心列,后续分别触发行清除与列清除。 if (!currentPieceIsCross) { return; @@ -248,6 +255,7 @@ static void ApplyStableStructureEffect() */ void ApplySpecialLandingEffects(const Point* fixedCells, int fixedCellCount, const Point* explosiveCells, int explosiveCellCount) { + // 多种特殊标记按固定顺序结算,保证同一落地事件的反馈和奖励稳定。 ApplyExplosiveLandingEffect(explosiveCells, explosiveCellCount); ApplyLaserLandingEffect(fixedCells, fixedCellCount); ApplyCrossLandingEffect(fixedCells, fixedCellCount); diff --git a/src/source/render/TetrisRenderAssets.cpp b/src/source/render/TetrisRenderAssets.cpp index 1cc5b9f..90e5afc 100644 --- a/src/source/render/TetrisRenderAssets.cpp +++ b/src/source/render/TetrisRenderAssets.cpp @@ -20,11 +20,13 @@ using namespace Gdiplus; */ static Bitmap* TryLoadBitmap(const std::wstring& path) { + // 空路径和不存在的文件不交给 GDI+,减少无效加载开销。 if (path.empty() || !FileExists(path)) { return nullptr; } + // GDI+ 返回对象后仍需检查状态,失败对象要立即释放。 Bitmap* loadedImage = Bitmap::FromFile(path.c_str(), FALSE); if (loadedImage != nullptr && loadedImage->GetLastStatus() == Ok) { @@ -65,6 +67,7 @@ Bitmap* LoadBackgroundImage() static Bitmap* backgroundImage = nullptr; static bool attempted = false; + // 背景图只查找一次,失败后也记住结果,避免每帧重复访问磁盘。 if (!attempted) { attempted = true; @@ -110,6 +113,7 @@ Bitmap* LoadCreditImage(int index) return nullptr; } + // 每张致谢图单独缓存,只有首次进入对应页时才加载。 if (!attempted[index]) { attempted[index] = true; diff --git a/src/source/render/TetrisRenderMain.cpp b/src/source/render/TetrisRenderMain.cpp index 49d6d4f..eddb549 100644 --- a/src/source/render/TetrisRenderMain.cpp +++ b/src/source/render/TetrisRenderMain.cpp @@ -1121,6 +1121,7 @@ void RenderFullScreen(HDC hdc, HWND hWnd) DrawPanelCardAlpha(leftPanelRect, cardColor, frameColor, 30, shellAlpha); DrawPanelCardAlpha(rightPanelRect, cardColor, frameColor, 30, shellAlpha); + // 游戏界面主体绘制顺序:棋盘底板、网格、锁定区、固定方块、落点、活动方块、特效、侧栏。 RECT innerRect = { gameRect.left + SS(6), @@ -1178,6 +1179,7 @@ void RenderFullScreen(HDC hdc, HWND hWnd) int lockedRows = GetRogueLockedRows(); if (lockedRows > 0) { + // Rogue 难度封锁区覆盖在棋盘底部,提示玩家这些行不能再放置方块。 RECT lockedRect = { gameRect.left, @@ -1216,6 +1218,7 @@ void RenderFullScreen(HDC hdc, HWND hWnd) for (int i = 0; i < nGameHeight; i++) { + // 先绘制已经固定在 workRegion 中的方块,活动方块稍后单独绘制在上层。 for (int j = 0; j < nGameWidth; j++) { if (workRegion[i][j] != 0) @@ -1248,6 +1251,7 @@ void RenderFullScreen(HDC hdc, HWND hWnd) if (targetFlag && !gameOverFlag) { + // 预测落点只画虚线轮廓,不写入棋盘数据。 HPEN targetPen = CreatePen(PS_DOT, SS(2), RGB(255, 240, 245)); oldPen = (HPEN)SelectObject(hdc, targetPen); oldBrush = (HBRUSH)SelectObject(hdc, GetStockObject(NULL_BRUSH)); @@ -1282,6 +1286,7 @@ void RenderFullScreen(HDC hdc, HWND hWnd) if (!gameOverFlag) { + // 活动方块根据特殊标记使用不同描边颜色,方便识别技能方块。 for (int i = 0; i < 4; i++) { for (int j = 0; j < 4; j++) @@ -1534,6 +1539,7 @@ void RenderFullScreen(HDC hdc, HWND hWnd) } HFONT oldFont = (HFONT)SelectObject(hdc, titleFont); + // 侧栏分成左侧战局信息和右侧预览/操作提示,两边共用同一套字体对象。 DrawPanelHeader(leftPanelRect, _T("战局信息"), 120); DrawPanelHeader(rightPanelRect, _T("预览与战术"), 148); @@ -1592,12 +1598,14 @@ void RenderFullScreen(HDC hdc, HWND hWnd) if (currentMode == MODE_ROGUE) { + // Rogue 模式左栏额外显示等级、经验、倍率、已有强化和限时状态。 int progressTop = IsRogueSkillDemoMode() ? 350 : 270; int progressBottom = IsRogueSkillDemoMode() ? 568 : 488; int upgradeTop = IsRogueSkillDemoMode() ? 590 : 510; if (IsRogueSkillDemoMode()) { + // 技能演示模式在侧栏显示当前演示技能名,帮助汇报时定位正在展示的机制。 RECT demoSkillRect = { leftPanelRect.left + SS(20), @@ -1745,6 +1753,7 @@ void RenderFullScreen(HDC hdc, HWND hWnd) }; DrawPanelCardAlpha(upgradeListRect, RGB(255, 248, 251), RGB(233, 191, 208), 24, panelAlpha); + // 强化列表文本较长,渲染时根据 upgradeListScrollOffset 做裁剪区域内滚动。 SelectObject(hdc, sectionFont); SetTextColor(hdc, textColor); TextOut(hdc, upgradeListRect.left + SS(18), upgradeListRect.top + SS(16), _T("已掌握强化"), lstrlen(_T("已掌握强化"))); @@ -2459,6 +2468,7 @@ void RenderFullScreen(HDC hdc, HWND hWnd) // 升级选择界面在当前战局上方绘制半透明遮罩,保留背景局势作为上下文。 if (currentScreen == SCREEN_UPGRADE) { + // 升级界面覆盖在战局之上,先绘制半透明遮罩,再绘制选项卡片。 RECT dimRect = { SX(20), @@ -2543,6 +2553,7 @@ void RenderFullScreen(HDC hdc, HWND hWnd) int availableHeight = overlayRect.bottom - verticalTop - SS(72) - (rowCount - 1) * gap; int cardHeight = availableHeight / rowCount; + // 卡片布局与输入层 GetUpgradeCardRect 保持同一套计算规则。 for (int i = 0; i < upgradeUiState.optionCount; i++) { bool isSelected = (i == upgradeUiState.selectedIndex); @@ -2571,6 +2582,7 @@ void RenderFullScreen(HDC hdc, HWND hWnd) if (upgradeUiState.options[i].cursed) { + // 诅咒选项使用暗色边框,并把底部说明替换成惩罚提示。 cardFill = isSelected ? RGB(238, 232, 238) : RGB(250, 245, 248); cardBorder = RGB(38, 34, 42); descColor = RGB(72, 62, 72); diff --git a/src/source/rogue/TetrisRogue.cpp b/src/source/rogue/TetrisRogue.cpp index 2943981..3c27bbe 100644 --- a/src/source/rogue/TetrisRogue.cpp +++ b/src/source/rogue/TetrisRogue.cpp @@ -228,6 +228,7 @@ static void StartRogueSkillDemoInternal(int demoIndex, bool autoAdvance); */ static int GetNextPreviewLimit() { + // 经典模式固定只显示一个预览,Rogue 模式再按强化等级限制到 1~3 个。 if (currentMode != MODE_ROGUE) { return 1; @@ -251,6 +252,7 @@ static int GetNextPreviewLimit() */ int GetRogueLockedRows() { + // 底部封锁只属于 Rogue 难度系统,其他模式保持完整 20 行棋盘。 if (currentMode != MODE_ROGUE) { return 0; @@ -297,6 +299,7 @@ static void ClearLockedRows() */ void AdvanceRogueDifficulty(int elapsedMs) { + // 技能演示、暂停、结束和非游玩界面都不推进真实难度。 if (currentMode != MODE_ROGUE || rogueDemoMode || currentScreen != SCREEN_PLAYING || suspendFlag || gameOverFlag || elapsedMs <= 0) { return; @@ -307,6 +310,7 @@ void AdvanceRogueDifficulty(int elapsedMs) while (rogueStats.difficultyElapsedMs >= kDifficultyStepMs) { + // 每累计一个难度周期提升速度,后面再根据等级换算封锁行数。 rogueStats.difficultyElapsedMs -= kDifficultyStepMs; rogueStats.difficultyLevel++; difficultyChanged = true; @@ -1680,6 +1684,7 @@ static void MarkDestinyCursedOption(int optionCount) */ static void FillUpgradeOptions() { + // 第一段:根据当前强化等级、前置条件和互斥条件收集候选池。 int selectableIndexes[kUpgradePoolSize] = { 0 }; int selectableWeights[kUpgradePoolSize] = { 0 }; int selectableCount = CollectSelectableUpgrades(selectableIndexes, selectableWeights); @@ -1706,9 +1711,11 @@ static void FillUpgradeOptions() */ int GetRogueFallInterval() { + // 基础速度由永久缓降和随时间推进的难度共同决定。 int baseInterval = 500 + rogueStats.slowFallStacks * 80; baseInterval -= rogueStats.difficultyLevel * kDifficultySpeedStepMs; + // 风险类强化提高速度,保命或奖励类临时状态降低速度。 if (rogueStats.highPressureLevel > 0) { baseInterval = baseInterval * 85 / 100; @@ -2022,6 +2029,7 @@ void AwardRogueSkillClearRewards(int clearedCells, int& scoreGain, int& expGain, { scoreGain = 0; expGain = 0; + // 非 Rogue 模式或未清除格子时不发放技能奖励。 if (currentMode != MODE_ROGUE || clearedCells <= 0) { return; @@ -2058,6 +2066,7 @@ void AwardRogueSkillClearRewards(int clearedCells, int& scoreGain, int& expGain, if (allowLevelProgress && !rogueDemoMode) { + // 主动技能允许触发升级时,经验条满会立即积累待选强化并打开升级菜单。 int levelUps = ApplyLevelProgress(rogueStats); if (levelUps > 0) { @@ -2128,6 +2137,7 @@ void CheckRogueLevelProgress() void ApplyBoardGravity() { int playableHeight = GetRoguePlayableHeight(); + // 每一列独立压缩到可操作区域底部,保留方块颜色和特殊值。 for (int x = 0; x < nGameWidth; x++) { int writeY = playableHeight - 1; @@ -2161,6 +2171,7 @@ void ApplyLineClearResult(int linesCleared) { if (linesCleared <= 0) { + // Rogue 连击只在连续消行时保持,空落地会断连。 if (currentMode == MODE_ROGUE) { rogueStats.comboChain = 0; @@ -2170,6 +2181,7 @@ void ApplyLineClearResult(int linesCleared) if (currentMode == MODE_CLASSIC) { + // 经典模式沿用老师框架的简单计分:每行 100 分。 classicStats.totalLinesCleared += linesCleared; classicStats.score += linesCleared * 100; tScore = classicStats.score; @@ -2211,6 +2223,7 @@ void ApplyLineClearResult(int linesCleared) int gamblerBonusPercent = 0; if (rogueStats.gamblerLevel > 0) { + // 赌徒契约让本次收益在正负区间随机波动。 int variance = 20 + (rogueStats.gamblerLevel - 1) * 10; if (variance > 50) { @@ -2289,6 +2302,7 @@ void ApplyLineClearResult(int linesCleared) if (rogueStats.chainBlastLevel > 0) { + // 连锁火花在消行附近制造额外破坏,并按技能清除规则给少量奖励。 int chainBlastCells = 0; for (int i = 0; i < linesCleared; i++) { @@ -2757,6 +2771,7 @@ void ConfirmUpgradeSelection() */ void HoldCurrentPiece() { + // 第一阶段:检查模式、解锁状态和本回合是否已经使用过 Hold。 if (currentMode != MODE_ROGUE || currentScreen != SCREEN_PLAYING || suspendFlag || gameOverFlag) { return; @@ -2772,6 +2787,7 @@ void HoldCurrentPiece() return; } + // 第二阶段:把当前方块放入备用仓,并决定换出旧备用方块还是消费下一块。 int previousHoldType = holdType; holdType = type; state = 0; @@ -2793,10 +2809,12 @@ void HoldCurrentPiece() target = point; if (currentMode == MODE_ROGUE && rogueStats.controlMasterLevel > 0) { + // 操控大师在 Hold 后给予短暂缓速,帮助玩家重新摆位。 rogueStats.holdSlowTicks = 4; currentFallInterval = GetRogueFallInterval(); } + // 第三阶段:验证换出的方块能否合法生成,避免交换到无解位置。 if (!IsPiecePlacementValid(type, state, point)) { gameOverFlag = true; @@ -2821,6 +2839,7 @@ void HoldCurrentPiece() */ void UseScreenBomb() { + // 清屏炸弹是主动技能,先检查模式、解锁状态和可用次数。 if (currentMode != MODE_ROGUE || currentScreen != SCREEN_PLAYING || suspendFlag || gameOverFlag) { return; @@ -2836,6 +2855,7 @@ void UseScreenBomb() return; } + // 扣除次数后清理底部区域,再按技能清除格子数发放奖励。 rogueStats.screenBombCount--; int clearedCells = TriggerScreenBomb(); int scoreGain = 0; @@ -2860,6 +2880,7 @@ void UseScreenBomb() */ void UseBlackHole() { + // 黑洞按棋盘上数量最多的固定方块类型进行吞噬。 if (currentMode != MODE_ROGUE || currentScreen != SCREEN_PLAYING || suspendFlag || gameOverFlag) { return; @@ -2875,6 +2896,7 @@ void UseBlackHole() return; } + // 只有确实清除了格子才消耗次数,避免空棋盘误扣资源。 int clearedCells = TriggerBlackHole(); if (clearedCells <= 0) { @@ -2890,6 +2912,7 @@ void UseBlackHole() if (rogueStats.voidCoreLevel > 0) { + // 虚空核心让黑洞额外召来一个待生成的彩虹方块。 rogueStats.pendingRainbowPieceCount++; } @@ -2923,6 +2946,7 @@ void UseBlackHole() */ void UseAirReshape() { + // 空中换形把当前方块改为 I 块,需要先确认次数和当前局面。 if (currentMode != MODE_ROGUE || currentScreen != SCREEN_PLAYING || suspendFlag || gameOverFlag) { return; @@ -2946,6 +2970,7 @@ void UseAirReshape() int originalState = state; bool transformed = false; + // 依次尝试横/竖两种 I 块状态,以及当前列附近的多个偏移。 for (int stateIndex = 0; stateIndex < 2 && !transformed; stateIndex++) { int nextState = candidateStates[stateIndex]; @@ -2967,6 +2992,7 @@ void UseAirReshape() if (!transformed) { + // 所有候选位置都不合法时恢复原方块,不消耗换形次数。 type = originalType; state = originalState; point = originalPoint; @@ -3072,6 +3098,7 @@ void RestartCurrentRogueSkillDemo() */ static void StartRogueSkillDemoInternal(int demoIndex, bool autoAdvance) { + // 入口统一夹紧下标,避免帮助页传入非法序号导致数组越界。 if (demoIndex < 0) { demoIndex = 0; @@ -3081,6 +3108,7 @@ static void StartRogueSkillDemoInternal(int demoIndex, bool autoAdvance) demoIndex = kRogueDemoStepCount - 1; } + // 先切到 Rogue 游玩界面,再用 Restart 复用完整的新局初始化流程。 currentMode = MODE_ROGUE; currentScreen = SCREEN_PLAYING; rogueDemoMode = true; @@ -3089,6 +3117,7 @@ static void StartRogueSkillDemoInternal(int demoIndex, bool autoAdvance) rogueDemoAutoAdvance = autoAdvance; Restart(); + // Restart 会重置部分状态,因此演示模式标记和显示开关需要重新写回。 rogueDemoMode = true; reviveAvailable = false; suspendFlag = false; @@ -3105,6 +3134,7 @@ static void StartRogueSkillDemoInternal(int demoIndex, bool autoAdvance) */ bool TickRogueSkillDemo() { + // 非演示模式不消耗计时;手动演示时 autoAdvance 为 false,只保持刷新。 if (!rogueDemoMode) { return false; @@ -3127,6 +3157,7 @@ bool TickRogueSkillDemo() */ void AdvanceRogueSkillDemo() { + // 循环切换演示条目,最后一项之后回到第一项。 if (!rogueDemoMode) { return; @@ -3146,6 +3177,7 @@ void AdvanceRogueSkillDemo() */ static void ResetRogueDemoBoard() { + // 清空真实棋盘和临时状态,保证每个演示场景互不影响。 for (int y = 0; y < nGameHeight; y++) { for (int x = 0; x < nGameWidth; x++) @@ -3263,6 +3295,7 @@ static void FillRogueDemoRowExcept(int row, int gapStart, int gapWidth, int base */ static void SetRogueDemoCurrentPiece(int pieceType, int pieceState, int x, int y) { + // 演示方块直接指定类型、旋转和位置,同时清空预览队列方便观察。 type = pieceType; nType = 0; state = pieceState; @@ -3280,12 +3313,14 @@ static void SetRogueDemoCurrentPiece(int pieceType, int pieceState, int x, int y */ static void ApplyRogueSkillDemoStep() { + // 第一阶段:清空棋盘,显示当前演示名称和操作说明。 ResetRogueDemoBoard(); const RogueDemoStep& demoStep = kRogueDemoSteps[rogueDemoStepIndex]; ShowRogueDemoFloatingName(demoStep.name); SetFeedbackMessage(demoStep.name, demoStep.detail, 12); + // 第二阶段:根据演示类型布置预设棋盘、强化等级和当前活动方块。 switch (demoStep.kind) { case DEMO_SCORE_MULTIPLIER: diff --git a/src/source/stdafx.cpp b/src/source/stdafx.cpp index 0e9d621..e136e70 100644 --- a/src/source/stdafx.cpp +++ b/src/source/stdafx.cpp @@ -5,5 +5,12 @@ #include "stdafx.h" +/** + * @file stdafx.cpp + * @brief 预编译头源文件,用于让构建系统生成 stdafx.h 对应的预编译结果。 + * + * 本文件不包含业务逻辑;保留它是为了兼容 Visual Studio 模板和现有构建结构。 + */ + // TODO: 在 STDAFX.H 中 // 引用任何所需的附加头文件,而不是在此文件中引用