diff --git a/TODO.md b/TODO.md index d330441..3d0af05 100644 --- a/TODO.md +++ b/TODO.md @@ -2,66 +2,66 @@ 说明:本清单基于 [Rogue_Tetris_功能设计文档](./Rogue_Tetris_功能设计文档.md) 整理,后续实现严格遵守以下约束: -- [ ] 不使用 `class`、继承、多态等面向对象特性 -- [ ] 以 `struct + 全局状态 + 纯函数/过程函数 + 模块化源文件` 组织代码 -- [ ] 保留当前原版俄罗斯方块玩法,作为“经典模式” -- [ ] 新增开始菜单,进入游戏前先选择模式 -- [ ] 模式至少包含:`经典模式`、`Rogue 模式` -- [ ] 强化选择界面固定为“三选一”三个选项框,视觉参考《吸血鬼幸存者》 +- [x] 不使用 `class`、继承、多态等面向对象特性 +- [x] 以 `struct + 全局状态 + 纯函数/过程函数 + 模块化源文件` 组织代码 +- [x] 保留当前原版俄罗斯方块玩法,作为“经典模式” +- [x] 新增开始菜单,进入游戏前先选择模式 +- [x] 模式至少包含:`经典模式`、`Rogue 模式` +- [x] 强化选择界面固定为“三选一”三个选项框,视觉参考《吸血鬼幸存者》 - [ ] 美术、图标、特效、音频先全部使用占位符资源 ## 阶段 0:重构准备与技术基线 目标:在不破坏现有可运行版本的前提下,为肉鸽系统预留结构。 -- [ ] 盘点当前全局变量、输入流程、计时器流程、渲染流程 -- [ ] 明确哪些旧逻辑保留,哪些逻辑需要拆分成独立过程函数 -- [ ] 明确保留当前版本作为“经典模式”基线,不把 Rogue 逻辑硬塞进唯一流程 -- [ ] 设计新的状态拆分方案:游戏主状态、棋盘状态、当前方块状态、玩家成长状态、强化状态、UI 状态 -- [ ] 增加“菜单状态 / 模式选择状态”设计 +- [x] 盘点当前全局变量、输入流程、计时器流程、渲染流程 +- [x] 明确哪些旧逻辑保留,哪些逻辑需要拆分成独立过程函数 +- [x] 明确保留当前版本作为“经典模式”基线,不把 Rogue 逻辑硬塞进唯一流程 +- [x] 设计新的状态拆分方案:游戏主状态、棋盘状态、当前方块状态、玩家成长状态、强化状态、UI 状态 +- [x] 增加“菜单状态 / 模式选择状态”设计 - [ ] 统一命名风格,避免后续继续扩散 `type/state/nType` 这类语义偏弱命名 -- [ ] 明确“升级中暂停”和“普通暂停”是两个不同状态 +- [x] 明确“升级中暂停”和“普通暂停”是两个不同状态 - [ ] 确定占位符资源策略:纯色块、边框、占位图标、占位按钮 完成标准: -- [ ] 写出核心数据结构草案 -- [ ] 写出模块拆分草案 +- [x] 写出核心数据结构草案 +- [x] 写出模块拆分草案 - [ ] 当前版本仍可正常编译运行 ## 阶段 1:开始菜单与模式选择 目标:先把“玩哪种模式”从启动流程里分出来,保证经典模式和 Rogue 模式能共存。 -- [ ] 新增开始菜单界面,不启动即直接进入对局 -- [ ] 菜单至少提供两个可选项:`经典模式`、`Rogue 模式` -- [ ] 增加菜单输入处理:上下/左右切换,确认进入 -- [ ] 增加当前选中模式的高亮显示 -- [ ] 增加菜单状态与对局状态之间的切换逻辑 -- [ ] 从菜单进入经典模式时,沿用当前原版规则 -- [ ] 从菜单进入 Rogue 模式时,进入带经验升级与强化的流程 +- [x] 新增开始菜单界面,不启动即直接进入对局 +- [x] 菜单至少提供两个可选项:`经典模式`、`Rogue 模式` +- [x] 增加菜单输入处理:上下/左右切换,确认进入 +- [x] 增加当前选中模式的高亮显示 +- [x] 增加菜单状态与对局状态之间的切换逻辑 +- [x] 从菜单进入经典模式时,沿用当前原版规则 +- [x] 从菜单进入 Rogue 模式时,进入带经验升级与强化的流程 - [ ] 预留未来扩展入口,如“退出游戏”或“帮助说明” 完成标准: -- [ ] 启动后先进入菜单 -- [ ] 两种模式都能正常进入 -- [ ] 经典模式不受 Rogue 功能影响 +- [x] 启动后先进入菜单 +- [x] 两种模式都能正常进入 +- [x] 经典模式不受 Rogue 功能影响 ## 阶段 2:核心数据结构改造 目标:把当前普通俄罗斯方块状态扩展成可承载 Rogue 系统的数据。 -- [ ] 新增 `MenuState` 或等效结构,记录菜单选项与当前选中项 -- [ ] 新增 `GameState` 相关结构,记录运行态、暂停态、升级选择态、结算态 -- [ ] 新增 `ModeState` 或模式标记,区分经典模式与 Rogue 模式 +- [x] 新增 `MenuState` 或等效结构,记录菜单选项与当前选中项 +- [x] 新增 `GameState` 相关结构,记录运行态、暂停态、升级选择态、结算态 +- [x] 新增 `ModeState` 或模式标记,区分经典模式与 Rogue 模式 - [ ] 新增 `PieceState` 结构,管理当前方块、下一方块、目标落点、是否可 Hold -- [ ] 新增 `PlayerStats` 结构,包含分数、等级、经验、所需经验、连击、总消行数 +- [x] 新增 `PlayerStats` 结构,包含分数、等级、经验、所需经验、连击、总消行数 - [ ] 新增 `UpgradeEntry` 结构,包含 `id`、名称、描述、分类、当前层数、是否可重复 - [ ] 新增 `UpgradeRuntime` 结构,保存已获得强化和运行时效果值 -- [ ] 新增 `UiState` 结构,保存当前三个升级选项、当前高亮项、弹窗可见性 +- [x] 新增 `UiState` 结构,保存当前三个升级选项、当前高亮项、弹窗可见性 - [ ] 将现有散落的全局变量逐步收敛到几个结构体中 -- [ ] 保留纯过程式调用,不引入成员函数 +- [x] 保留纯过程式调用,不引入成员函数 完成标准: @@ -72,33 +72,33 @@ 目标:先把公共逻辑和模式专属逻辑分开,避免后续互相污染。 -- [ ] 抽离经典模式与 Rogue 模式共享的核心逻辑:移动、旋转、落地、消行、渲染基础棋盘 -- [ ] 经典模式保持当前简单计分与基础 UI -- [ ] Rogue 模式单独接入经验、等级、强化、升级界面 -- [ ] 输入处理按模式分流,避免经典模式响应强化界面按键 -- [ ] 渲染流程按模式分层:公共层、经典模式层、Rogue 模式层 +- [x] 抽离经典模式与 Rogue 模式共享的核心逻辑:移动、旋转、落地、消行、渲染基础棋盘 +- [x] 经典模式保持当前简单计分与基础 UI +- [x] Rogue 模式单独接入经验、等级、强化、升级界面 +- [x] 输入处理按模式分流,避免经典模式响应强化界面按键 +- [x] 渲染流程按模式分层:公共层、经典模式层、Rogue 模式层 完成标准: -- [ ] 两种模式共用一套基础方块逻辑 -- [ ] Rogue 新功能不会破坏经典模式 +- [x] 两种模式共用一套基础方块逻辑 +- [x] Rogue 新功能不会破坏经典模式 ## 阶段 4:分数、经验、等级主循环 目标:把“消行 -> 得分 -> 获得经验 -> 触发升级”接入 Rogue 模式主循环。 -- [ ] 重写消行结算函数,返回本次消除行数 -- [ ] 按文档接入基础得分:1/2/3/4 行分别对应不同分值 -- [ ] 按文档接入经验收益:1/2/3/4 行分别给予不同 EXP +- [x] 重写消行结算函数,返回本次消除行数 +- [x] 按文档接入基础得分:1/2/3/4 行分别对应不同分值 +- [x] 按文档接入经验收益:1/2/3/4 行分别给予不同 EXP - [ ] 接入倍率计算:分数倍率、经验倍率、四消奖励、连击奖励 -- [ ] 新增升级判定函数,支持一次结算后连续升多级 -- [ ] 升级触发时停止下落计时与普通输入 +- [x] 新增升级判定函数,支持一次结算后连续升多级 +- [x] 升级触发时停止下落计时与普通输入 - [ ] 在右侧信息面板显示等级、EXP、所需 EXP、当前已获得强化摘要 完成标准: -- [ ] 消行后可以稳定增加分数和经验 -- [ ] 达到阈值后必定进入升级选择状态 +- [x] 消行后可以稳定增加分数和经验 +- [x] 达到阈值后必定进入升级选择状态 ## 阶段 5:强化池与三选一系统 @@ -106,50 +106,50 @@ - [ ] 建立强化池初始化函数,不使用类,采用静态数组或 `std::vector` - [ ] 先实现一批 P1 基础强化,优先选 6 个左右最稳定的: -- [ ] `score_multiplier` +- [x] `score_multiplier` - [ ] `combo_bonus` -- [ ] `slow_fall` +- [x] `slow_fall` - [ ] `preview_plus_one` -- [ ] `exp_multiplier` +- [x] `exp_multiplier` - [ ] `last_chance` - [ ] 随机抽取 3 个不重复选项,避免当前局内明显无效选项 - [ ] 支持重复强化的层数叠加 -- [ ] 选中后立即应用效果并返回游戏 -- [ ] 为后续强化扩展预留 `applyUpgradeById()` 分发函数 +- [x] 选中后立即应用效果并返回游戏 +- [x] 为后续强化扩展预留 `applyUpgradeById()` 分发函数 完成标准: -- [ ] 每次升级都稳定弹出三个选项框 -- [ ] 选择任一选项后效果立即生效 +- [x] 每次升级都稳定弹出三个选项框 +- [x] 选择任一选项后效果立即生效 ## 阶段 6:升级界面 UI 目标:实现参考《吸血鬼幸存者》的三选一视觉结构,先用占位表现。 -- [ ] 增加全屏或半透明遮罩,压暗游戏场景 -- [ ] 中央显示三个横向或纵向排列的选项框 +- [x] 增加全屏或半透明遮罩,压暗游戏场景 +- [x] 中央显示三个横向或纵向排列的选项框 - [ ] 每个选项框至少包含:占位图标、强化名、强化说明、强化分类、当前层数 -- [ ] 支持键盘选择:`A/D` 或方向键切换,`Enter/Space` 确认 +- [x] 支持键盘选择:`A/D` 或方向键切换,`Enter/Space` 确认 - [ ] 高亮态、选中态、禁用态视觉区分明确 -- [ ] 保证三个框尺寸一致、布局稳定,不因文本长度错位 -- [ ] 右侧现有信息面板在升级状态下保留或弱化显示,但不能抢焦点 +- [x] 保证三个框尺寸一致、布局稳定,不因文本长度错位 +- [x] 右侧现有信息面板在升级状态下保留或弱化显示,但不能抢焦点 - [ ] 占位资源统一放入 `assets`,命名先按 `placeholder_*` 完成标准: -- [ ] 升级界面可以独立显示 -- [ ] 三个选项框可操作、可确认、可关闭 -- [ ] 视觉结构已经接近目标形式,后续只需替换资源 +- [x] 升级界面可以独立显示 +- [x] 三个选项框可操作、可确认、可关闭 +- [x] 视觉结构已经接近目标形式,后续只需替换资源 ## 阶段 7:玩法强化第一轮 目标:把最影响体验的几个强化真正接入玩法。 -- [ ] 分数倍率:影响所有结算得分 +- [x] 分数倍率:影响所有结算得分 - [ ] 连击奖励:连续多次成功消行追加奖励 -- [ ] 慢速下落:降低自然下落速度 +- [x] 慢速下落:降低自然下落速度 - [ ] 额外预览:从 1 个 Next 扩展到 2~3 个 -- [ ] EXP 强化:提升经验获取倍率 +- [x] EXP 强化:提升经验获取倍率 - [ ] 最后一搏:失败前自动清除底部 3 行,仅触发一次 完成标准: @@ -181,7 +181,7 @@ - [ ] 右侧面板补充等级、EXP 条、强化列表、当前倍率 - [ ] 增加本次结算浮动提示:`+Score`、`+EXP`、`Level Up` - [ ] 为特殊强化增加简短提示文案 -- [ ] 增加暂停、升级、失败三种不同遮罩样式 +- [x] 增加暂停、升级、失败三种不同遮罩样式 - [ ] 增加占位图标、占位按钮、占位边框资源 - [ ] 检查中文排版、字号、留白、长文本换行 @@ -196,7 +196,7 @@ - [ ] 调整等级需求曲线 - [ ] 调整强化出现权重,避免过强或无效强化频繁出现 - [ ] 调整下落速度、经验速度、升级频率 -- [ ] 修复升级状态下的非法输入问题 +- [x] 修复升级状态下的非法输入问题 - [ ] 修复连续升级、失败复活、爆破消行等边界问题 - [ ] 增加最基础回归测试清单 @@ -204,8 +204,8 @@ - [ ] 开局正常生成方块 - [ ] 消行得分与经验正确 -- [ ] 升级必出 3 个选项框 -- [ ] 选择后恢复游戏且不丢状态 +- [x] 升级必出 3 个选项框 +- [x] 选择后恢复游戏且不丢状态 - [ ] 复活、暂停、失败、重开互不冲突 ## 建议实现顺序 @@ -217,14 +217,14 @@ ## 当前最近一轮执行清单 -- [ ] 加入开始菜单状态 -- [ ] 做出模式选择菜单占位 UI -- [ ] 接入 `经典模式 / Rogue 模式` 两个入口 -- [ ] 建立新的状态结构体与头文件声明 -- [ ] 拆分经典模式与 Rogue 模式主循环 -- [ ] 重写消行结算返回值 -- [ ] 加入 `PlayerStats` -- [ ] 加入经验与升级判定 -- [ ] 加入升级状态切换 -- [ ] 做出三个升级选项框的占位 UI +- [x] 加入开始菜单状态 +- [x] 做出模式选择菜单占位 UI +- [x] 接入 `经典模式 / Rogue 模式` 两个入口 +- [x] 建立新的状态结构体与头文件声明 +- [x] 拆分经典模式与 Rogue 模式主循环 +- [x] 重写消行结算返回值 +- [x] 加入 `PlayerStats` +- [x] 加入经验与升级判定 +- [x] 加入升级状态切换 +- [x] 做出三个升级选项框的占位 UI - [ ] 接入第一批 6 个基础强化 diff --git a/src/include/Tetris.h b/src/include/Tetris.h index ad5358a..7c9b23a 100644 --- a/src/include/Tetris.h +++ b/src/include/Tetris.h @@ -64,7 +64,8 @@ enum ScreenState { SCREEN_MENU = 0, SCREEN_PLAYING = 1, - SCREEN_UPGRADE = 2 + SCREEN_UPGRADE = 2, + SCREEN_RULES = 3 }; enum GameMode @@ -108,6 +109,7 @@ void ComputeTarget(); void Restart(); void StartGameWithMode(int mode); void ReturnToMainMenu(); +void OpenRulesScreen(); void OpenUpgradeMenu(); void ConfirmUpgradeSelection(); diff --git a/src/source/Tetris.cpp b/src/source/Tetris.cpp index 89f992d..70a1136 100644 --- a/src/source/Tetris.cpp +++ b/src/source/Tetris.cpp @@ -203,7 +203,18 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) break; case VK_RETURN: case VK_SPACE: - StartGameWithMode(menuState.selectedIndex); + if (menuState.selectedIndex == 0) + { + StartGameWithMode(MODE_CLASSIC); + } + else if (menuState.selectedIndex == 1) + { + StartGameWithMode(MODE_ROGUE); + } + else + { + OpenRulesScreen(); + } ResetGameTimer(hWnd); InvalidateRect(hWnd, nullptr, FALSE); break; @@ -216,6 +227,22 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) break; } + if (currentScreen == SCREEN_RULES) + { + switch (wParam) + { + case VK_ESCAPE: + case VK_BACK: + case 'M': + ReturnToMainMenu(); + InvalidateRect(hWnd, nullptr, FALSE); + break; + default: + break; + } + break; + } + if (currentScreen == SCREEN_UPGRADE) { switch (wParam) diff --git a/src/source/TetrisLogic.cpp b/src/source/TetrisLogic.cpp index 16d90c3..a28ef61 100644 --- a/src/source/TetrisLogic.cpp +++ b/src/source/TetrisLogic.cpp @@ -687,7 +687,7 @@ void ReturnToMainMenu() currentScreen = SCREEN_MENU; suspendFlag = false; gameOverFlag = false; - menuState.optionCount = 2; + menuState.optionCount = 3; upgradeUiState.pendingCount = 0; if (menuState.selectedIndex < 0 || menuState.selectedIndex >= menuState.optionCount) @@ -696,6 +696,12 @@ void ReturnToMainMenu() } } +void OpenRulesScreen() +{ + currentScreen = SCREEN_RULES; + suspendFlag = false; +} + void OpenUpgradeMenu() { if (currentMode != MODE_ROGUE || upgradeUiState.pendingCount <= 0) diff --git a/src/source/TetrisRender.cpp b/src/source/TetrisRender.cpp index 13a156d..ad04ff3 100644 --- a/src/source/TetrisRender.cpp +++ b/src/source/TetrisRender.cpp @@ -156,16 +156,18 @@ void TDrawScreen(HDC hdc, HWND hWnd) SetTextColor(hdc, textColor); DrawText(hdc, _T("\u8bf7\u9009\u62e9\u5f00\u59cb\u6a21\u5f0f"), -1, &subtitleRect, DT_CENTER | DT_VCENTER | DT_SINGLELINE); - const TCHAR* modeNames[2] = + const TCHAR* modeNames[3] = { _T("\u7ecf\u5178\u6a21\u5f0f"), - _T("Rogue \u6a21\u5f0f") + _T("Rogue \u6a21\u5f0f"), + _T("\u6e38\u620f\u89c4\u5219") }; - const TCHAR* modeDescriptions[2] = + const TCHAR* modeDescriptions[3] = { _T("\u4fdd\u7559\u5f53\u524d\u539f\u7248\u4fc4\u7f57\u65af\u65b9\u5757\u73a9\u6cd5\uff0c\u76f4\u63a5\u5f00\u59cb\u5bf9\u5c40\u3002"), - _T("\u8fdb\u5165 Rogue \u6a21\u5f0f\uff0c\u5df2\u63a5\u5165\u72ec\u7acb HUD \u4e0e\u7b49\u7ea7 / \u7ecf\u9a8c\u7ed3\u7b97\u3002") + _T("\u8fdb\u5165 Rogue \u6a21\u5f0f\uff0c\u5df2\u63a5\u5165\u72ec\u7acb HUD \u4e0e\u7b49\u7ea7 / \u7ecf\u9a8c\u7ed3\u7b97\u3002"), + _T("\u5355\u72ec\u67e5\u770b\u64cd\u4f5c\u65b9\u5f0f\u3001Rogue \u673a\u5236\u4e0e\u5f53\u524d\u7248\u672c\u8bf4\u660e\u3002") }; for (int i = 0; i < menuState.optionCount; i++) @@ -226,7 +228,142 @@ void TDrawScreen(HDC hdc, HWND hWnd) SelectObject(hdc, smallFont); SetTextColor(hdc, RGB(128, 104, 118)); - DrawText(hdc, _T("\u65b9\u5411\u952e / WASD \u5207\u6362\uff0cEnter \u6216 Space \u5f00\u59cb\uff0cEsc \u9000\u51fa"), -1, &hintRect, DT_CENTER | DT_VCENTER | DT_SINGLELINE); + DrawText(hdc, _T("\u65b9\u5411\u952e / WASD \u5207\u6362\uff0cEnter \u6216 Space \u786e\u8ba4\uff0cEsc \u9000\u51fa"), -1, &hintRect, DT_CENTER | DT_VCENTER | DT_SINGLELINE); + + SelectObject(hdc, oldFont); + DeleteObject(titleFont); + DeleteObject(sectionFont); + DeleteObject(bodyFont); + DeleteObject(smallFont); + return; + } + + if (currentScreen == SCREEN_RULES) + { + RECT rulesCard = + { + SX(76), + SY(54), + SX(WINDOW_CLIENT_WIDTH - 76), + SY(WINDOW_CLIENT_HEIGHT - 54) + }; + + HPEN rulesPen = CreatePen(PS_SOLID, 1, frameColor); + HBRUSH rulesBrush = CreateSolidBrush(cardColor); + HFONT oldFont = (HFONT)SelectObject(hdc, titleFont); + oldPen = (HPEN)SelectObject(hdc, rulesPen); + oldBrush = (HBRUSH)SelectObject(hdc, rulesBrush); + RoundRect(hdc, rulesCard.left, rulesCard.top, rulesCard.right, rulesCard.bottom, SS(34), SS(34)); + SelectObject(hdc, oldBrush); + SelectObject(hdc, oldPen); + DeleteObject(rulesBrush); + DeleteObject(rulesPen); + + SetTextColor(hdc, titleColor); + RECT rulesTitleRect = + { + rulesCard.left + SS(36), + rulesCard.top + SS(26), + rulesCard.right - SS(36), + rulesCard.top + SS(78) + }; + DrawText(hdc, _T("\u6e38\u620f\u89c4\u5219"), -1, &rulesTitleRect, DT_LEFT | DT_VCENTER | DT_SINGLELINE); + + HPEN rulesAccentPen = CreatePen(PS_SOLID, SS(3), accentColor); + oldPen = (HPEN)SelectObject(hdc, rulesAccentPen); + MoveToEx(hdc, rulesCard.left + SS(38), rulesCard.top + SS(92), nullptr); + LineTo(hdc, rulesCard.left + SS(186), rulesCard.top + SS(92)); + SelectObject(hdc, oldPen); + DeleteObject(rulesAccentPen); + + RECT leftSection = + { + rulesCard.left + SS(36), + rulesCard.top + SS(126), + rulesCard.left + SS(330), + rulesCard.bottom - SS(86) + }; + + RECT rightSection = + { + rulesCard.left + SS(360), + rulesCard.top + SS(126), + rulesCard.right - SS(36), + rulesCard.bottom - SS(86) + }; + + SetTextColor(hdc, textColor); + SelectObject(hdc, sectionFont); + + RECT sectionTitle = + { + leftSection.left, + leftSection.top, + leftSection.right, + leftSection.top + SS(34) + }; + DrawText(hdc, _T("\u57fa\u672c\u64cd\u4f5c"), -1, §ionTitle, DT_LEFT | DT_VCENTER | DT_SINGLELINE); + + SelectObject(hdc, bodyFont); + RECT leftBody = + { + leftSection.left, + leftSection.top + SS(48), + leftSection.right, + leftSection.bottom + }; + DrawText( + hdc, + _T("\u2190 / A\uff1a\u5411\u5de6\u79fb\u52a8\r\n") + _T("\u2192 / D\uff1a\u5411\u53f3\u79fb\u52a8\r\n") + _T("\u2191 / W\uff1a\u65cb\u8f6c\u65b9\u5757\r\n") + _T("\u2193 / S\uff1a\u8f6f\u964d\r\n") + _T("Space\uff1a\u786c\u964d\r\n") + _T("P\uff1a\u6682\u505c / \u7ee7\u7eed\r\n") + _T("R\uff1a\u91cd\u5f00\u5f53\u524d\u5bf9\u5c40\r\n") + _T("M\uff1a\u8fd4\u56de\u4e3b\u83dc\u5355"), + -1, + &leftBody, + DT_LEFT | DT_TOP | DT_WORDBREAK); + + SelectObject(hdc, sectionFont); + RECT rulesSectionTitle = + { + rightSection.left, + rightSection.top, + rightSection.right, + rightSection.top + SS(34) + }; + DrawText(hdc, _T("\u6a21\u5f0f\u89c4\u5219"), -1, &rulesSectionTitle, DT_LEFT | DT_VCENTER | DT_SINGLELINE); + + SelectObject(hdc, bodyFont); + RECT rulesBody = + { + rightSection.left, + rightSection.top + SS(48), + rightSection.right, + rightSection.bottom + }; + DrawText( + hdc, + _T("\u7ecf\u5178\u6a21\u5f0f\uff1a\u4fdd\u6301\u539f\u7248\u4fc4\u7f57\u65af\u65b9\u5757\u73a9\u6cd5\uff0c\u4ee5\u6d88\u884c\u548c\u751f\u5b58\u4e3a\u4e3b\u3002\r\n\r\n") + _T("Rogue \u6a21\u5f0f\uff1a\u6d88\u884c\u540e\u9664\u4e86\u83b7\u5f97\u5206\u6570\uff0c\u8fd8\u4f1a\u83b7\u5f97 EXP\u3002EXP \u8fbe\u5230\u9608\u503c\u540e\u89e6\u53d1\u5347\u7ea7\uff0c\u4ece\u4e09\u4e2a\u5f3a\u5316\u4e2d\u9009\u4e00\u4e2a\u3002\r\n\r\n") + _T("\u5f53\u524d\u5df2\u63a5\u5165\uff1a\u5206\u6570\u500d\u7387\u3001EXP \u500d\u7387\u3001\u6162\u901f\u4e0b\u843d\u3002\r\n\r\n") + _T("\u63d0\u793a\uff1a\u6682\u505c\u3001\u5931\u8d25\u548c\u5347\u7ea7\u4f1a\u8fdb\u5165\u4e0d\u540c\u754c\u9762\uff0c\u8bf7\u6839\u636e\u5c4f\u5e55\u63d0\u793a\u64cd\u4f5c\u3002"), + -1, + &rulesBody, + DT_LEFT | DT_TOP | DT_WORDBREAK); + + SelectObject(hdc, smallFont); + SetTextColor(hdc, RGB(128, 104, 118)); + RECT backHintRect = + { + rulesCard.left + SS(36), + rulesCard.bottom - SS(58), + rulesCard.right - SS(36), + rulesCard.bottom - SS(24) + }; + DrawText(hdc, _T("Esc / Backspace / M \u8fd4\u56de\u4e3b\u83dc\u5355"), -1, &backHintRect, DT_CENTER | DT_VCENTER | DT_SINGLELINE); SelectObject(hdc, oldFont); DeleteObject(titleFont); @@ -457,14 +594,14 @@ void TDrawScreen(HDC hdc, HWND hWnd) SetTextColor(hdc, textColor); } - TextOut(hdc, panelRect.left + SS(24), panelRect.top + SS(320), _T("\u4e0b\u4e00\u4e2a\u65b9\u5757"), lstrlen(_T("\u4e0b\u4e00\u4e2a\u65b9\u5757"))); + TextOut(hdc, panelRect.left + SS(24), panelRect.top + SS(430), _T("\u4e0b\u4e00\u4e2a\u65b9\u5757"), lstrlen(_T("\u4e0b\u4e00\u4e2a\u65b9\u5757"))); RECT nextCard = { panelRect.left + SS(24), - panelRect.top + SS(430), + panelRect.top + SS(472), panelRect.left + SS(24) + grid * 4 + SS(32), - panelRect.top + SS(430) + grid * 4 + SS(32) + panelRect.top + SS(472) + grid * 4 + SS(32) }; HBRUSH nextCardBrush = CreateSolidBrush(RGB(255, 238, 244)); @@ -504,13 +641,10 @@ void TDrawScreen(HDC hdc, HWND hWnd) } } - SelectObject(hdc, sectionFont); - TextOut(hdc, panelRect.left + SS(24), panelRect.top + SS(610), _T("\u64cd\u4f5c\u63d0\u793a"), lstrlen(_T("\u64cd\u4f5c\u63d0\u793a"))); - - SelectObject(hdc, bodyFont); - TextOut(hdc, panelRect.left + SS(24), panelRect.top + SS(652), _T("\u65b9\u5411\u952e / WASD\uff1a\u79fb\u52a8 / \u65cb\u8f6c"), lstrlen(_T("\u65b9\u5411\u952e / WASD\uff1a\u79fb\u52a8 / \u65cb\u8f6c"))); - TextOut(hdc, panelRect.left + SS(24), panelRect.top + SS(688), _T("Space\uff1a\u5feb\u901f\u4e0b\u843d"), lstrlen(_T("Space\uff1a\u5feb\u901f\u4e0b\u843d"))); - TextOut(hdc, panelRect.left + SS(24), panelRect.top + SS(724), _T("P\uff1a\u6682\u505c R\uff1a\u91cd\u65b0\u5f00\u59cb"), lstrlen(_T("P\uff1a\u6682\u505c R\uff1a\u91cd\u65b0\u5f00\u59cb"))); + SelectObject(hdc, smallFont); + SetTextColor(hdc, RGB(128, 104, 118)); + TextOut(hdc, panelRect.left + SS(24), panelRect.top + SS(656), _T("M \uff1a\u8fd4\u56de\u83dc\u5355"), lstrlen(_T("M \uff1a\u8fd4\u56de\u83dc\u5355"))); + TextOut(hdc, panelRect.left + SS(24), panelRect.top + SS(688), _T("\u89c4\u5219\u8bf4\u660e\u8bf7\u5728\u4e3b\u83dc\u5355\u8fdb\u5165"), lstrlen(_T("\u89c4\u5219\u8bf4\u660e\u8bf7\u5728\u4e3b\u83dc\u5355\u8fdb\u5165"))); if (suspendFlag || gameOverFlag) {