diff --git a/TODO.md b/TODO.md index 3d0af05..9c7ceca 100644 --- a/TODO.md +++ b/TODO.md @@ -107,13 +107,13 @@ - [ ] 建立强化池初始化函数,不使用类,采用静态数组或 `std::vector` - [ ] 先实现一批 P1 基础强化,优先选 6 个左右最稳定的: - [x] `score_multiplier` -- [ ] `combo_bonus` +- [x] `combo_bonus` - [x] `slow_fall` -- [ ] `preview_plus_one` +- [x] `preview_plus_one` - [x] `exp_multiplier` -- [ ] `last_chance` -- [ ] 随机抽取 3 个不重复选项,避免当前局内明显无效选项 -- [ ] 支持重复强化的层数叠加 +- [x] `last_chance` +- [x] 随机抽取 3 个不重复选项,避免当前局内明显无效选项 +- [x] 支持重复强化的层数叠加 - [x] 选中后立即应用效果并返回游戏 - [x] 为后续强化扩展预留 `applyUpgradeById()` 分发函数 @@ -128,7 +128,7 @@ - [x] 增加全屏或半透明遮罩,压暗游戏场景 - [x] 中央显示三个横向或纵向排列的选项框 -- [ ] 每个选项框至少包含:占位图标、强化名、强化说明、强化分类、当前层数 +- [x] 每个选项框至少包含:占位图标、强化名、强化说明、强化分类、当前层数 - [x] 支持键盘选择:`A/D` 或方向键切换,`Enter/Space` 确认 - [ ] 高亮态、选中态、禁用态视觉区分明确 - [x] 保证三个框尺寸一致、布局稳定,不因文本长度错位 @@ -146,15 +146,15 @@ 目标:把最影响体验的几个强化真正接入玩法。 - [x] 分数倍率:影响所有结算得分 -- [ ] 连击奖励:连续多次成功消行追加奖励 +- [x] 连击奖励:连续多次成功消行追加奖励 - [x] 慢速下落:降低自然下落速度 -- [ ] 额外预览:从 1 个 Next 扩展到 2~3 个 +- [x] 额外预览:从 1 个 Next 扩展到 2~3 个 - [x] EXP 强化:提升经验获取倍率 -- [ ] 最后一搏:失败前自动清除底部 3 行,仅触发一次 +- [x] 最后一搏:失败前自动清除底部 3 行,仅触发一次 完成标准: -- [ ] 每个强化都能在实际对局中观察到效果 +- [x] 每个强化都能在实际对局中观察到效果 - [ ] 强化效果叠加后不会引发明显逻辑冲突 ## 阶段 8:特殊机制第二轮 @@ -227,4 +227,4 @@ - [x] 加入经验与升级判定 - [x] 加入升级状态切换 - [x] 做出三个升级选项框的占位 UI -- [ ] 接入第一批 6 个基础强化 +- [x] 接入第一批 6 个基础强化 diff --git a/src/include/Tetris.h b/src/include/Tetris.h index 7c9b23a..4b586b7 100644 --- a/src/include/Tetris.h +++ b/src/include/Tetris.h @@ -41,11 +41,30 @@ struct PlayerStats int scoreMultiplierPercent; int expMultiplierPercent; int slowFallStacks; + int comboBonusStacks; + int comboChain; + int previewCount; + int lastChanceCount; + int scoreUpgradeLevel; + int expUpgradeLevel; + int previewUpgradeLevel; + int lastChanceUpgradeLevel; }; struct UpgradeOption { int id; + int currentLevel; + const TCHAR* name; + const TCHAR* category; + const TCHAR* description; +}; + +struct UpgradeEntry +{ + int id; + int maxLevel; + bool repeatable; const TCHAR* name; const TCHAR* category; const TCHAR* description; @@ -91,6 +110,7 @@ extern UpgradeUiState upgradeUiState; extern int currentScreen; extern int currentMode; extern int currentFallInterval; +extern int nextTypes[3]; extern int bricks[7][4][4][4]; extern COLORREF BrickColor[7]; diff --git a/src/source/TetrisLogic.cpp b/src/source/TetrisLogic.cpp index a28ef61..b2bc2ca 100644 --- a/src/source/TetrisLogic.cpp +++ b/src/source/TetrisLogic.cpp @@ -18,14 +18,30 @@ UpgradeUiState upgradeUiState = { 0, 0, 0, 0, {} }; int currentScreen = SCREEN_MENU; int currentMode = MODE_CLASSIC; int currentFallInterval = 500; +int nextTypes[3] = { 0, 0, 0 }; enum UpgradeId { UPGRADE_SCORE_MULTIPLIER = 0, UPGRADE_EXP_MULTIPLIER = 1, - UPGRADE_SLOW_FALL = 2 + UPGRADE_SLOW_FALL = 2, + UPGRADE_COMBO_BONUS = 3, + UPGRADE_PREVIEW_PLUS_ONE = 4, + UPGRADE_LAST_CHANCE = 5 }; +static const UpgradeEntry kUpgradePool[] = +{ + { UPGRADE_SCORE_MULTIPLIER, -1, true, _T("\u5206\u6570\u500d\u7387"), _T("\u5f97\u5206"), _T("\u6240\u6709\u5f97\u5206\u63d0\u9ad8 20%\u3002") }, + { UPGRADE_COMBO_BONUS, -1, true, _T("\u8fde\u51fb\u52a0\u6210"), _T("\u8282\u594f"), _T("\u8fde\u7eed\u6d88\u884c\u65f6\u8ffd\u52a0\u8fde\u51fb\u5956\u52b1\u3002") }, + { UPGRADE_SLOW_FALL, -1, true, _T("\u6162\u901f\u4e0b\u843d"), _T("\u64cd\u4f5c"), _T("\u81ea\u7136\u4e0b\u843d\u53d8\u6162\uff0c\u6bcf\u6b21\u63d0\u9ad8 80ms\u3002") }, + { UPGRADE_PREVIEW_PLUS_ONE, 3, false, _T("\u989d\u5916\u9884\u89c8"), _T("\u89c6\u91ce"), _T("\u4e0b\u4e00\u4e2a\u65b9\u5757\u9884\u89c8 +1\uff0c\u6700\u591a 3 \u4e2a\u3002") }, + { UPGRADE_EXP_MULTIPLIER, -1, true, _T("\u7ecf\u9a8c\u5f3a\u5316"), _T("\u6210\u957f"), _T("\u540e\u7eed\u6d88\u884c\u83b7\u5f97 EXP \u63d0\u9ad8 25%\u3002") }, + { UPGRADE_LAST_CHANCE, 1, false, _T("\u6700\u540e\u4e00\u640f"), _T("\u4fdd\u547d"), _T("\u9996\u6b21\u9876\u6b7b\u65f6\u81ea\u52a8\u6e05\u9664\u5e95\u90e8 3 \u884c\u5e76\u7ee7\u7eed\u6e38\u620f\u3002") } +}; + +static constexpr int kUpgradePoolSize = sizeof(kUpgradePool) / sizeof(kUpgradePool[0]); + int bricks[7][4][4][4] = { { @@ -164,6 +180,87 @@ static void ResetPlayerStats(PlayerStats& stats, bool useRogueRules) 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; +} + +static int GetNextPreviewLimit() +{ + if (currentMode != MODE_ROGUE) + { + return 1; + } + + if (rogueStats.previewCount < 1) + { + return 1; + } + + if (rogueStats.previewCount > 3) + { + return 3; + } + + return rogueStats.previewCount; +} + +static int GetUpgradeCurrentLevel(int upgradeId) +{ + switch (upgradeId) + { + case UPGRADE_SCORE_MULTIPLIER: + return rogueStats.scoreUpgradeLevel; + case UPGRADE_EXP_MULTIPLIER: + return rogueStats.expUpgradeLevel; + case UPGRADE_SLOW_FALL: + return rogueStats.slowFallStacks; + case UPGRADE_COMBO_BONUS: + return rogueStats.comboBonusStacks; + case UPGRADE_PREVIEW_PLUS_ONE: + return rogueStats.previewUpgradeLevel; + case UPGRADE_LAST_CHANCE: + return rogueStats.lastChanceUpgradeLevel; + default: + return 0; + } +} + +static bool IsUpgradeSelectable(const UpgradeEntry& entry) +{ + if (entry.repeatable) + { + return true; + } + + if (entry.maxLevel <= 0) + { + return GetUpgradeCurrentLevel(entry.id) == 0; + } + + return GetUpgradeCurrentLevel(entry.id) < entry.maxLevel; +} + +static void ResetNextQueue() +{ + for (int i = 0; i < 3; i++) + { + nextTypes[i] = rand() % 7; + } +} + +static int ConsumeNextType() +{ + int nextType = nextTypes[0]; + nextTypes[0] = nextTypes[1]; + nextTypes[1] = nextTypes[2]; + nextTypes[2] = rand() % 7; + return nextType; } static int GetRogueScoreByLines(int linesCleared) @@ -207,23 +304,36 @@ static int ApplyLevelProgress(PlayerStats& stats) static void FillUpgradeOptions() { - upgradeUiState.optionCount = 3; + int selectableIndexes[kUpgradePoolSize] = { 0 }; + int selectableCount = 0; + + for (int i = 0; i < kUpgradePoolSize; i++) + { + if (IsUpgradeSelectable(kUpgradePool[i])) + { + selectableIndexes[selectableCount++] = i; + } + } + + int optionCount = selectableCount < 3 ? selectableCount : 3; + upgradeUiState.optionCount = optionCount; upgradeUiState.selectedIndex = 0; - upgradeUiState.options[0].id = UPGRADE_SCORE_MULTIPLIER; - upgradeUiState.options[0].name = _T("\u5206\u6570\u500d\u7387"); - upgradeUiState.options[0].category = _T("\u5f97\u5206"); - upgradeUiState.options[0].description = _T("\u6240\u6709\u5f97\u5206\u63d0\u9ad8 20%\u3002"); + for (int i = 0; i < optionCount; i++) + { + int pickSlot = rand() % selectableCount; + int pickedIndex = selectableIndexes[pickSlot]; + const UpgradeEntry& pickedEntry = kUpgradePool[pickedIndex]; - upgradeUiState.options[1].id = UPGRADE_EXP_MULTIPLIER; - upgradeUiState.options[1].name = _T("\u7ecf\u9a8c\u5f3a\u5316"); - upgradeUiState.options[1].category = _T("\u6210\u957f"); - upgradeUiState.options[1].description = _T("\u540e\u7eed\u6d88\u884c\u83b7\u5f97 EXP \u63d0\u9ad8 25%\u3002"); + upgradeUiState.options[i].id = pickedEntry.id; + upgradeUiState.options[i].currentLevel = GetUpgradeCurrentLevel(pickedEntry.id); + upgradeUiState.options[i].name = pickedEntry.name; + upgradeUiState.options[i].category = pickedEntry.category; + upgradeUiState.options[i].description = pickedEntry.description; - upgradeUiState.options[2].id = UPGRADE_SLOW_FALL; - upgradeUiState.options[2].name = _T("\u6162\u901f\u4e0b\u843d"); - upgradeUiState.options[2].category = _T("\u64cd\u4f5c"); - upgradeUiState.options[2].description = _T("\u81ea\u7136\u4e0b\u843d\u53d8\u6162\uff0c\u6bcf\u6b21\u63d0\u9ad8 80ms\u3002"); + selectableIndexes[pickSlot] = selectableIndexes[selectableCount - 1]; + selectableCount--; + } } static int GetRogueFallInterval() @@ -237,14 +347,30 @@ static void ApplyUpgradeById(int upgradeId) { case UPGRADE_SCORE_MULTIPLIER: rogueStats.scoreMultiplierPercent += 20; + rogueStats.scoreUpgradeLevel++; + break; + case UPGRADE_COMBO_BONUS: + rogueStats.comboBonusStacks++; break; case UPGRADE_EXP_MULTIPLIER: rogueStats.expMultiplierPercent += 25; + rogueStats.expUpgradeLevel++; break; case UPGRADE_SLOW_FALL: rogueStats.slowFallStacks++; currentFallInterval = GetRogueFallInterval(); break; + case UPGRADE_PREVIEW_PLUS_ONE: + if (rogueStats.previewCount < 3) + { + rogueStats.previewCount++; + } + rogueStats.previewUpgradeLevel = rogueStats.previewCount - 1; + break; + case UPGRADE_LAST_CHANCE: + rogueStats.lastChanceCount = 1; + rogueStats.lastChanceUpgradeLevel = 1; + break; default: break; } @@ -254,6 +380,10 @@ static void ApplyLineClearResult(int linesCleared) { if (linesCleared <= 0) { + if (currentMode == MODE_ROGUE) + { + rogueStats.comboChain = 0; + } return; } @@ -271,6 +401,12 @@ static void ApplyLineClearResult(int linesCleared) int expGain = GetRogueExpByLines(linesCleared); expGain = expGain * rogueStats.expMultiplierPercent / 100; + rogueStats.comboChain++; + if (rogueStats.comboBonusStacks > 0 && rogueStats.comboChain > 1) + { + scoreGain += (rogueStats.comboChain - 1) * rogueStats.comboBonusStacks * 50; + } + rogueStats.totalLinesCleared += linesCleared; rogueStats.score += scoreGain; rogueStats.exp += expGain; @@ -533,13 +669,25 @@ void Fixing() if (overflowTop) { - gameOverFlag = true; - return; + if (currentMode == MODE_ROGUE && rogueStats.lastChanceCount > 0) + { + rogueStats.lastChanceCount--; + + for (int i = 0; i < 3; i++) + { + DeleteOneLine(nGameHeight - 1); + } + } + else + { + gameOverFlag = true; + return; + } } // 生成下一个活动方块 - type = nType; - nType = rand() % 7; + type = ConsumeNextType(); + nType = nextTypes[0]; state = 0; point = GetSpawnPoint(type); target = point; @@ -664,8 +812,9 @@ void Restart() upgradeUiState.totalChosenCount = 0; tScore = 0; - type = rand() % 7; - nType = rand() % 7; + ResetNextQueue(); + type = ConsumeNextType(); + nType = nextTypes[0]; state = 0; point = GetSpawnPoint(type); target = point; diff --git a/src/source/TetrisRender.cpp b/src/source/TetrisRender.cpp index ad04ff3..67e795f 100644 --- a/src/source/TetrisRender.cpp +++ b/src/source/TetrisRender.cpp @@ -578,9 +578,21 @@ void TDrawScreen(HDC hdc, HWND hWnd) _stprintf_s(fallText, _T("\u4e0b\u843d\u95f4\u9694 %dms"), currentFallInterval); TextOut(hdc, panelRect.left + SS(24), panelRect.top + SS(356), fallText, lstrlen(fallText)); + TCHAR comboText[64]; + _stprintf_s(comboText, _T("\u8fde\u51fb\u94fe %d \u5956\u52b1\u5c42\u6570 %d"), rogueStats.comboChain, rogueStats.comboBonusStacks); + TextOut(hdc, panelRect.left + SS(24), panelRect.top + SS(392), comboText, lstrlen(comboText)); + + TCHAR summaryText[64]; + _stprintf_s( + summaryText, + _T("\u9884\u89c8 %d \u4fdd\u547d %d"), + rogueStats.previewCount, + rogueStats.lastChanceCount); + TextOut(hdc, panelRect.left + SS(24), panelRect.top + SS(428), summaryText, lstrlen(summaryText)); + SelectObject(hdc, smallFont); SetTextColor(hdc, RGB(128, 104, 118)); - TextOut(hdc, panelRect.left + SS(24), panelRect.top + SS(392), _T("\u5f53\u524d\u5df2\u9009\u5f3a\u5316\u6570\u91cf\u4ec5\u7528\u4e8e\u6d4b\u8bd5\u7248"), lstrlen(_T("\u5f53\u524d\u5df2\u9009\u5f3a\u5316\u6570\u91cf\u4ec5\u7528\u4e8e\u6d4b\u8bd5\u7248"))); + TextOut(hdc, panelRect.left + SS(24), panelRect.top + SS(464), _T("\u53f3\u4fa7 HUD \u5df2\u5f00\u59cb\u663e\u793a Rogue \u5f53\u524d\u6210\u957f\u72b6\u6001"), lstrlen(_T("\u53f3\u4fa7 HUD \u5df2\u5f00\u59cb\u663e\u793a Rogue \u5f53\u524d\u6210\u957f\u72b6\u6001"))); SelectObject(hdc, sectionFont); SetTextColor(hdc, textColor); } @@ -596,47 +608,66 @@ void TDrawScreen(HDC hdc, HWND hWnd) 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 = + int previewCount = 1; + if (currentMode == MODE_ROGUE) { - panelRect.left + SS(24), - panelRect.top + SS(472), - panelRect.left + SS(24) + grid * 4 + SS(32), - panelRect.top + SS(472) + grid * 4 + SS(32) - }; - - HBRUSH nextCardBrush = CreateSolidBrush(RGB(255, 238, 244)); - HPEN nextCardPen = CreatePen(PS_SOLID, 1, RGB(233, 191, 208)); - oldPen = (HPEN)SelectObject(hdc, nextCardPen); - oldBrush = (HBRUSH)SelectObject(hdc, nextCardBrush); - RoundRect(hdc, nextCard.left, nextCard.top, nextCard.right, nextCard.bottom, SS(22), SS(22)); - SelectObject(hdc, oldBrush); - SelectObject(hdc, oldPen); - DeleteObject(nextCardBrush); - DeleteObject(nextCardPen); - - for (int i = 0; i < 4; i++) - { - for (int j = 0; j < 4; j++) + previewCount = rogueStats.previewCount; + if (previewCount < 1) { - if (bricks[nType][0][i][j] != 0) - { - RECT brickRect = - { - nextCard.left + SS(16) + j * grid, - nextCard.top + SS(16) + i * grid, - nextCard.left + SS(16) + (j + 1) * grid - SS(2), - nextCard.top + SS(16) + (i + 1) * grid - SS(2) - }; + previewCount = 1; + } + if (previewCount > 3) + { + previewCount = 3; + } + } - HBRUSH brickBrush = CreateSolidBrush(BrickColor[nType]); - HPEN brickPen = CreatePen(PS_SOLID, 1, RGB(255, 248, 250)); - oldPen = (HPEN)SelectObject(hdc, brickPen); - oldBrush = (HBRUSH)SelectObject(hdc, brickBrush); - RoundRect(hdc, brickRect.left, brickRect.top, brickRect.right, brickRect.bottom, SS(10), SS(10)); - SelectObject(hdc, oldBrush); - SelectObject(hdc, oldPen); - DeleteObject(brickBrush); - DeleteObject(brickPen); + for (int previewIndex = 0; previewIndex < previewCount; previewIndex++) + { + RECT nextCard = + { + panelRect.left + SS(24) + previewIndex * SS(94), + panelRect.top + SS(472), + panelRect.left + SS(24) + previewIndex * SS(94) + grid * 2 + SS(40), + panelRect.top + SS(472) + grid * 2 + SS(40) + }; + + HBRUSH nextCardBrush = CreateSolidBrush(RGB(255, 238, 244)); + HPEN nextCardPen = CreatePen(PS_SOLID, 1, RGB(233, 191, 208)); + oldPen = (HPEN)SelectObject(hdc, nextCardPen); + oldBrush = (HBRUSH)SelectObject(hdc, nextCardBrush); + RoundRect(hdc, nextCard.left, nextCard.top, nextCard.right, nextCard.bottom, SS(22), SS(22)); + SelectObject(hdc, oldBrush); + SelectObject(hdc, oldPen); + DeleteObject(nextCardBrush); + DeleteObject(nextCardPen); + + int previewType = nextTypes[previewIndex]; + + for (int i = 0; i < 4; i++) + { + for (int j = 0; j < 4; j++) + { + if (bricks[previewType][0][i][j] != 0) + { + RECT brickRect = + { + nextCard.left + SS(14) + j * (grid / 2), + nextCard.top + SS(14) + i * (grid / 2), + nextCard.left + SS(14) + (j + 1) * (grid / 2) - SS(1), + nextCard.top + SS(14) + (i + 1) * (grid / 2) - SS(1) + }; + + HBRUSH brickBrush = CreateSolidBrush(BrickColor[previewType]); + HPEN brickPen = CreatePen(PS_SOLID, 1, RGB(255, 248, 250)); + oldPen = (HPEN)SelectObject(hdc, brickPen); + oldBrush = (HBRUSH)SelectObject(hdc, brickBrush); + RoundRect(hdc, brickRect.left, brickRect.top, brickRect.right, brickRect.bottom, SS(8), SS(8)); + SelectObject(hdc, oldBrush); + SelectObject(hdc, oldPen); + DeleteObject(brickBrush); + DeleteObject(brickPen); + } } } } @@ -811,6 +842,17 @@ void TDrawScreen(HDC hdc, HWND hWnd) }; DrawText(hdc, upgradeUiState.options[i].category, -1, &categoryRect, DT_LEFT | DT_VCENTER | DT_SINGLELINE); + TCHAR levelText[32]; + _stprintf_s(levelText, _T("Lv.%d"), upgradeUiState.options[i].currentLevel + 1); + RECT levelRect = + { + cardRect.right - SS(88), + cardRect.top + SS(24), + cardRect.right - SS(20), + cardRect.top + SS(54) + }; + DrawText(hdc, levelText, -1, &levelRect, DT_RIGHT | DT_VCENTER | DT_SINGLELINE); + SelectObject(hdc, sectionFont); SetTextColor(hdc, isSelected ? titleColor : textColor); RECT nameRect = @@ -829,9 +871,20 @@ void TDrawScreen(HDC hdc, HWND hWnd) cardRect.left + SS(20), cardRect.top + SS(182), cardRect.right - SS(20), - cardRect.bottom - SS(30) + cardRect.bottom - SS(64) }; DrawText(hdc, upgradeUiState.options[i].description, -1, &descRect, DT_LEFT | DT_WORDBREAK); + + SelectObject(hdc, smallFont); + RECT footerRect = + { + cardRect.left + SS(20), + cardRect.bottom - SS(52), + cardRect.right - SS(20), + cardRect.bottom - SS(22) + }; + SetTextColor(hdc, RGB(128, 104, 118)); + DrawText(hdc, _T("\u5360\u4f4d\u56fe\u6807 / \u5360\u4f4d\u7a00\u6709\u5ea6"), -1, &footerRect, DT_LEFT | DT_VCENTER | DT_SINGLELINE); } SelectObject(hdc, smallFont);