2 Commits

Author SHA1 Message Date
Qi-huanye 1c000c3c21 补强注释 2026-04-28 23:18:51 +08:00
Qi-huanye 0840a807b5 项目架构重构,代码整理 2026-04-28 22:44:31 +08:00
23 changed files with 3207 additions and 1892 deletions
+19 -2
View File
@@ -98,6 +98,15 @@ Tereis/
├─ src/ ├─ src/
│ ├─ include/ 头文件 │ ├─ include/ 头文件
│ ├─ source/ 源文件 │ ├─ source/ 源文件
│ │ ├─ Tetris.cpp 程序入口、窗口和消息框架
│ │ ├─ TetrisLogic.cpp 基础俄罗斯方块逻辑框架
│ │ ├─ TetrisRender.cpp 基础绘制框架
│ │ ├─ common/ 资源路径、文件检查等通用工具
│ │ ├─ app/ 媒体播放、布局命中、输入和定时器处理
│ │ ├─ extensions/ 框架外通用扩展、界面状态和视觉效果
│ │ ├─ logic/ 特殊方块落地效果等逻辑扩展
│ │ ├─ render/ 图片加载等渲染内部支持
│ │ └─ rogue/ Rogue 模式、强化和技能系统
│ └─ resources/ Windows 资源脚本 │ └─ resources/ Windows 资源脚本
├─ assets/ ├─ assets/
│ ├─ audio/ 背景音乐 │ ├─ audio/ 背景音乐
@@ -130,6 +139,8 @@ Tereis/
C:\mingw64\bin\ C:\mingw64\bin\
``` ```
构建脚本会递归收集 `src/source` 下的 `.cpp` 文件。新增功能代码可以放入功能目录,不需要手动维护固定源码列表。
## 构建与运行 ## 构建与运行
在项目根目录执行: 在项目根目录执行:
@@ -194,10 +205,16 @@ powershell -NoProfile -ExecutionPolicy Bypass -File .\build-mingw.ps1 -Run
本项目以过程式 C++ 写法为主,核心逻辑分布如下: 本项目以过程式 C++ 写法为主,核心逻辑分布如下:
- `src/source/Tetris.cpp`窗口、消息循环、输入和鼠标交互 - `src/source/Tetris.cpp`Win32 程序入口、窗口创建和消息分发主干
- `src/source/TetrisLogic.cpp`:基础方块逻辑、消行和状态重置 - `src/source/TetrisLogic.cpp`:基础方块逻辑、消行和状态重置
- `src/source/TetrisRogue.cpp`:Rogue 模式、强化、技能和成长系统
- `src/source/TetrisRender.cpp`:界面绘制、面板、动画和特效 - `src/source/TetrisRender.cpp`:界面绘制、面板、动画和特效
- `src/source/common/TetrisAssets.cpp`:资源路径拼接和文件存在判断
- `src/source/app/`:背景音乐、复活视频、窗口布局命中、鼠标键盘和定时器处理
- `src/source/logic/TetrisPieceEffects.cpp`:彩虹、爆破、激光、十字和稳定结构等落地效果
- `src/source/extensions/TetrisGameExtensions.cpp`:框架外通用状态切换、复活、说明页、视觉效果等扩展支持
- `src/source/render/TetrisRenderAssets.cpp`:背景图、致谢页图片等 GDI+ 图片资源加载
- `src/source/rogue/TetrisRogue.cpp`:Rogue 模式、强化、技能和成长系统
- `src/include/Tetris.h`:主要结构体、全局状态和函数声明 - `src/include/Tetris.h`:主要结构体、全局状态和函数声明
- `src/include/TetrisAppInternal.h``src/include/TetrisRenderInternal.h``src/include/TetrisAssets.h`:窗口层、渲染层和资源工具的内部声明
项目适合作为程序设计课程大作业展示,也便于在答辩时讲解窗口程序、游戏循环、碰撞检测、状态管理和功能扩展。 项目适合作为程序设计课程大作业展示,也便于在答辩时讲解窗口程序、游戏循环、碰撞检测、状态管理和功能扩展。
+3 -8
View File
@@ -61,14 +61,9 @@ foreach ($Candidate in $WindresCandidates) {
New-Item -ItemType Directory -Force -Path $BuildDir | Out-Null New-Item -ItemType Directory -Force -Path $BuildDir | Out-Null
$Sources = @( $Sources = Get-ChildItem -Path $SourceDir -Recurse -Filter "*.cpp" |
(Join-Path $SourceDir "stdafx.cpp"), Sort-Object FullName |
(Join-Path $SourceDir "Tetris.cpp"), Select-Object -ExpandProperty FullName
(Join-Path $SourceDir "TetrisLogic.cpp"),
(Join-Path $SourceDir "TetrisLogicInnovation.cpp"),
(Join-Path $SourceDir "TetrisRogue.cpp"),
(Join-Path $SourceDir "TetrisRender.cpp")
)
$LinkInputs = @() $LinkInputs = @()
+274
View File
@@ -1,5 +1,10 @@
#pragma once #pragma once
/**
* @file Tetris.h
* @brief 定义俄罗斯方块项目的全局常量、结构体、枚举、全局状态和公开函数接口。
*/
#include "resource.h" #include "resource.h"
#include "stdafx.h" #include "stdafx.h"
#include <mmsystem.h> #include <mmsystem.h>
@@ -264,60 +269,329 @@ extern bool currentPieceIsRainbow;
extern int bricks[7][4][4][4]; extern int bricks[7][4][4][4];
extern COLORREF BrickColor[7]; extern COLORREF BrickColor[7];
/**
* @brief 判断当前活动方块是否还能向下移动一格。
* @return 可以下落返回 true,否则返回 false。
*/
bool CanMoveDown(); bool CanMoveDown();
/**
* @brief 判断当前活动方块是否还能向左移动一格。
* @return 可以左移返回 true,否则返回 false。
*/
bool CanMoveLeft(); bool CanMoveLeft();
/**
* @brief 判断当前活动方块是否还能向右移动一格。
* @return 可以右移返回 true,否则返回 false。
*/
bool CanMoveRight(); bool CanMoveRight();
/**
* @brief 将当前活动方块向下移动一格。
*/
void MoveDown(); void MoveDown();
/**
* @brief 将当前活动方块向左移动一格。
*/
void MoveLeft(); void MoveLeft();
/**
* @brief 将当前活动方块向右移动一格。
*/
void MoveRight(); void MoveRight();
/**
* @brief 尝试旋转当前活动方块,Rogue 完美旋转会额外尝试左右偏移。
*/
void Rotate(); void Rotate();
/**
* @brief 将当前活动方块直接下落到预测落点。
*/
void DropDown(); void DropDown();
/**
* @brief 将当前活动方块固定到棋盘并生成下一块。
*/
void Fixing(); void Fixing();
/**
* @brief 删除指定行并让上方棋盘整体下落。
* @param number 要删除的棋盘行号。
*/
void DeleteOneLine(int number); void DeleteOneLine(int number);
/**
* @brief 扫描棋盘、删除所有满行并触发消行结算。
* @return 本次删除的行数。
*/
int DeleteLines(); int DeleteLines();
/**
* @brief 计算当前活动方块的预测落点。
*/
void ComputeTarget(); void ComputeTarget();
/**
* @brief 重置棋盘、方块、统计和视觉状态,开始一局新游戏。
*/
void Restart(); void Restart();
/**
* @brief 按指定模式开始新游戏。
* @param mode 游戏模式,取值来自 GameMode。
*/
void StartGameWithMode(int mode); void StartGameWithMode(int mode);
/**
* @brief 返回主菜单并清理临时玩法与界面状态。
*/
void ReturnToMainMenu(); void ReturnToMainMenu();
/**
* @brief 复活视频播放成功后恢复游戏并清理顶部空间。
*/
void ReviveAfterVideo(); void ReviveAfterVideo();
/**
* @brief 从帮助页进入 Rogue 技能演示的第一项。
*/
void StartRogueSkillDemo(); void StartRogueSkillDemo();
/**
* @brief 从帮助页进入指定 Rogue 技能演示。
* @param demoIndex 技能演示序号。
*/
void StartRogueSkillDemoAt(int demoIndex); void StartRogueSkillDemoAt(int demoIndex);
/**
* @brief 重新开始当前 Rogue 技能演示场景。
*/
void RestartCurrentRogueSkillDemo(); void RestartCurrentRogueSkillDemo();
/**
* @brief 判断当前是否处于 Rogue 技能演示模式。
* @return 演示模式中返回 true,否则返回 false。
*/
bool IsRogueSkillDemoMode(); bool IsRogueSkillDemoMode();
/**
* @brief 推进 Rogue 技能演示计时。
* @return 演示模式正在运行返回 true,否则返回 false。
*/
bool TickRogueSkillDemo(); bool TickRogueSkillDemo();
/**
* @brief 切换到下一项 Rogue 技能演示。
*/
void AdvanceRogueSkillDemo(); void AdvanceRogueSkillDemo();
/**
* @brief 获取 Rogue 技能演示条目数量。
* @return 可选择的演示条目总数。
*/
int GetRogueSkillDemoCount(); int GetRogueSkillDemoCount();
/**
* @brief 获取指定 Rogue 技能演示名称。
* @param demoIndex 技能演示序号。
* @return 名称字符串,越界时返回空字符串。
*/
const TCHAR* GetRogueSkillDemoName(int demoIndex); const TCHAR* GetRogueSkillDemoName(int demoIndex);
/**
* @brief 获取指定 Rogue 技能演示说明。
* @param demoIndex 技能演示序号。
* @return 说明字符串,越界时返回空字符串。
*/
const TCHAR* GetRogueSkillDemoDetail(int demoIndex); const TCHAR* GetRogueSkillDemoDetail(int demoIndex);
/**
* @brief 获取当前 Rogue 技能演示名称。
* @return 当前名称,非演示模式返回空字符串。
*/
const TCHAR* GetCurrentRogueSkillDemoName(); const TCHAR* GetCurrentRogueSkillDemoName();
/**
* @brief 设置右侧战斗日志反馈信息。
* @param title 反馈标题。
* @param detail 反馈详情。
* @param ticks 保持显示的游戏计时次数。
*/
void SetFeedbackMessage(const TCHAR* title, const TCHAR* detail, int ticks); void SetFeedbackMessage(const TCHAR* title, const TCHAR* detail, int ticks);
/**
* @brief 打开帮助首页。
*/
void OpenRulesScreen(); void OpenRulesScreen();
/**
* @brief 打开 Rogue 技能演示选择页。
*/
void OpenSkillDemoScreen(); void OpenSkillDemoScreen();
/**
* @brief 打开致谢页。
*/
void OpenCreditScreen(); void OpenCreditScreen();
/**
* @brief 切换致谢页图片。
* @param direction 小于 0 向前切换,大于 0 向后切换。
*/
void ChangeCreditPage(int direction); void ChangeCreditPage(int direction);
/**
* @brief 打开 Rogue 升级选择界面。
*/
void OpenUpgradeMenu(); void OpenUpgradeMenu();
/**
* @brief 确认当前升级选择并恢复游戏流程。
*/
void ConfirmUpgradeSelection(); void ConfirmUpgradeSelection();
/**
* @brief 重置升级选择界面状态。
*/
void ResetUpgradeUiState(); void ResetUpgradeUiState();
/**
* @brief 使用或解锁后处理 Hold 备用仓逻辑。
*/
void HoldCurrentPiece(); void HoldCurrentPiece();
/**
* @brief 使用清屏炸弹主动技能。
*/
void UseScreenBomb(); void UseScreenBomb();
/**
* @brief 使用黑洞主动技能。
*/
void UseBlackHole(); void UseBlackHole();
/**
* @brief 使用空中换形主动技能。
*/
void UseAirReshape(); void UseAirReshape();
/**
* @brief 重置 Rogue 待播放视觉事件。
*/
void ResetPendingRogueVisualEvents(); void ResetPendingRogueVisualEvents();
/**
* @brief 清空所有视觉效果状态。
*/
void ResetVisualEffects(); void ResetVisualEffects();
/**
* @brief 推进视觉效果动画。
* @return 仍有动画需要刷新返回 true,否则返回 false。
*/
bool TickVisualEffects(); bool TickVisualEffects();
/**
* @brief 推进致谢页切换动画。
* @return 需要刷新界面返回 true,否则返回 false。
*/
bool TickCreditAnimation(); bool TickCreditAnimation();
/**
* @brief 触发标准消行动画。
* @param rows 被消除的行号数组。
* @param rowCount 行号数量。
* @param linesCleared 实际消除行数。
*/
void TriggerLineClearEffect(const int* rows, int rowCount, int linesCleared); void TriggerLineClearEffect(const int* rows, int rowCount, int linesCleared);
/**
* @brief 播放之前因升级界面暂存的消行动画。
*/
void PlayPendingLineClearEffect(); void PlayPendingLineClearEffect();
/**
* @brief 触发指定棋盘格的默认清除特效。
* @param cells 被清除格子数组。
* @param cellCount 格子数量。
* @param strongBurst 是否使用更强的爆裂粒子。
*/
void TriggerCellClearEffect(const Point* cells, int cellCount, bool strongBurst); void TriggerCellClearEffect(const Point* cells, int cellCount, bool strongBurst);
/**
* @brief 触发指定棋盘格的自定义颜色清除特效。
* @param cells 被清除格子数组。
* @param cellCount 格子数量。
* @param flashColor 高亮颜色。
* @param strongBurst 是否使用更强的爆裂粒子。
*/
void TriggerColoredCellClearEffect(const Point* cells, int cellCount, COLORREF flashColor, bool strongBurst); void TriggerColoredCellClearEffect(const Point* cells, int cellCount, COLORREF flashColor, bool strongBurst);
/**
* @brief 记录一个固定方块受重力下落的轨迹。
* @param x 棋盘列号。
* @param fromY 下落起始行号。
* @param toY 下落目标行号。
* @param cellValue 方块格子数值。
*/
void TriggerGravityFallEffect(int x, int fromY, int toY, int cellValue); void TriggerGravityFallEffect(int x, int fromY, int toY, int cellValue);
/**
* @brief 为 Rogue 主动或特殊技能清除格子发放奖励。
* @param clearedCells 清除格子数。
* @param scoreGain 返回本次得分增量。
* @param expGain 返回本次经验增量。
* @param allowLevelProgress 是否允许本次奖励触发升级流程。
*/
void AwardRogueSkillClearRewards(int clearedCells, int& scoreGain, int& expGain, bool allowLevelProgress); void AwardRogueSkillClearRewards(int clearedCells, int& scoreGain, int& expGain, bool allowLevelProgress);
/**
* @brief 检查 Rogue 经验是否达到升级条件。
*/
void CheckRogueLevelProgress(); void CheckRogueLevelProgress();
/**
* @brief 对棋盘固定方块应用重力下落。
*/
void ApplyBoardGravity(); void ApplyBoardGravity();
/**
* @brief 计算当前 Rogue 模式下落间隔。
* @return 下落计时器间隔,单位毫秒。
*/
int GetRogueFallInterval(); int GetRogueFallInterval();
/**
* @brief 获取 Rogue 当前可操作棋盘高度。
* @return 未被底部封锁占用的行数。
*/
int GetRoguePlayableHeight(); int GetRoguePlayableHeight();
/**
* @brief 获取 Rogue 难度系统当前封锁的底部行数。
* @return 封锁行数。
*/
int GetRogueLockedRows(); int GetRogueLockedRows();
/**
* @brief 按经过时间推进 Rogue 难度。
* @param elapsedMs 本次推进的时间,单位毫秒。
*/
void AdvanceRogueDifficulty(int elapsedMs); void AdvanceRogueDifficulty(int elapsedMs);
/**
* @brief 获取进化强化的合成路线文本。
* @param upgradeId 强化编号。
* @return 路线文本;普通强化返回空或空指针。
*/
const TCHAR* GetUpgradeSynthesisPath(int upgradeId); const TCHAR* GetUpgradeSynthesisPath(int upgradeId);
/**
* @brief 绘制当前窗口中的完整游戏界面。
* @param hdc 目标绘图设备上下文。
* @param hWnd 当前窗口句柄,用于读取客户区大小。
*/
void TDrawScreen(HDC hdc, HWND hWnd); void TDrawScreen(HDC hdc, HWND hWnd);
+227
View File
@@ -0,0 +1,227 @@
#pragma once
/**
* @file TetrisAppInternal.h
* @brief 声明窗口布局、输入、计时器和媒体播放等应用层内部接口。
*/
#include "Tetris.h"
constexpr int GAME_TIMER_ID = 1;
constexpr int EFFECT_TIMER_ID = 2;
constexpr int CREDIT_TIMER_ID = 3;
constexpr int WM_CREDIT_TICK = WM_APP + 1;
constexpr int GAME_TIMER_INTERVAL = 500;
constexpr int EFFECT_TIMER_INTERVAL = 16;
constexpr int CREDIT_TIMER_INTERVAL = 5;
struct LayoutMetrics
{
int scale;
int offsetX;
int offsetY;
int layoutWidth;
int layoutHeight;
int grid;
};
/**
* @brief 根据当前窗口大小计算整体界面缩放与偏移。
* @param hWnd 当前窗口句柄。
* @return 布局缩放、偏移和网格尺寸。
*/
LayoutMetrics GetLayoutMetrics(HWND hWnd);
/**
* @brief 按当前布局比例缩放一个尺寸值。
* @param metrics 当前布局参数。
* @param value 设计稿尺寸值。
* @return 缩放后的像素尺寸。
*/
int ScaleValue(const LayoutMetrics& metrics, int value);
/**
* @brief 按当前布局比例缩放横坐标并叠加窗口偏移。
* @param metrics 当前布局参数。
* @param value 设计稿横坐标。
* @return 实际窗口横坐标。
*/
int ScaleXValue(const LayoutMetrics& metrics, int value);
/**
* @brief 按当前布局比例缩放纵坐标并叠加窗口偏移。
* @param metrics 当前布局参数。
* @param value 设计稿纵坐标。
* @return 实际窗口纵坐标。
*/
int ScaleYValue(const LayoutMetrics& metrics, int value);
/**
* @brief 获取主菜单选项的点击区域。
* @param hWnd 当前窗口句柄。
* @param index 菜单选项序号。
* @return 选项在窗口中的矩形区域。
*/
RECT GetMenuOptionRect(HWND hWnd, int index);
/**
* @brief 获取帮助页选项的点击区域。
* @param hWnd 当前窗口句柄。
* @param index 帮助首页选项序号。
* @return 选项在窗口中的矩形区域。
*/
RECT GetHelpOptionRect(HWND hWnd, int index);
/**
* @brief 获取技能演示列表项的点击区域。
* @param hWnd 当前窗口句柄。
* @param index 技能演示条目序号。
* @return 条目在窗口中的矩形区域。
*/
RECT GetHelpSkillDemoItemRect(HWND hWnd, int index);
/**
* @brief 获取帮助页底部返回提示的点击区域。
* @param hWnd 当前窗口句柄。
* @return 返回提示在窗口中的矩形区域。
*/
RECT GetHelpBackHintRect(HWND hWnd);
/**
* @brief 获取致谢页左右切换按钮的点击区域。
* @param hWnd 当前窗口句柄。
* @param direction 小于 0 表示左箭头,大于 0 表示右箭头。
* @return 切换按钮在窗口中的矩形区域。
*/
RECT GetCreditArrowRect(HWND hWnd, int direction);
/**
* @brief 获取升级选择卡片的点击区域。
* @param hWnd 当前窗口句柄。
* @param index 强化卡片序号。
* @return 卡片在窗口中的矩形区域。
*/
RECT GetUpgradeCardRect(HWND hWnd, int index);
/**
* @brief 获取暂停或结束覆盖层按钮的点击区域。
* @param hWnd 当前窗口句柄。
* @param index 按钮序号。
* @param buttonCount 覆盖层当前按钮总数。
* @return 按钮在窗口中的矩形区域。
*/
RECT GetOverlayButtonRect(HWND hWnd, int index, int buttonCount);
/**
* @brief 获取左上角返回按钮的点击区域。
* @param hWnd 当前窗口句柄。
* @return 返回按钮在窗口中的矩形区域。
*/
RECT GetBackButtonRect(HWND hWnd);
/**
* @brief 获取右下角音乐按钮的点击区域。
* @param hWnd 当前窗口句柄。
* @return 音乐按钮在窗口中的矩形区域。
*/
RECT GetMusicButtonRect(HWND hWnd);
/**
* @brief 判断点坐标是否落在矩形内部。
* @param rect 待判断矩形。
* @param x 点的横坐标。
* @param y 点的纵坐标。
* @return 点在矩形内返回 true,否则返回 false。
*/
bool IsPointInRect(const RECT& rect, int x, int y);
/**
* @brief 将滚动偏移按步长调整并限制在有效范围内。
* @param scrollOffset 需要修改的滚动偏移。
* @param delta 本次滚动增量。
*/
void AdjustScrollOffset(int& scrollOffset, int delta);
/**
* @brief 获取适配当前窗口缩放的一次滚动步长。
* @param hWnd 当前窗口句柄。
* @param baseStep 设计稿中的基础滚动步长。
* @return 缩放后的滚动步长。
*/
int GetScrollStep(HWND hWnd, int baseStep);
/**
* @brief 重置主下落定时器。
* @param hWnd 当前窗口句柄。
*/
void ResetGameTimer(HWND hWnd);
/**
* @brief 启动游戏、特效和致谢页动画定时器。
* @param hWnd 当前窗口句柄。
*/
void StartAppTimers(HWND hWnd);
/**
* @brief 停止游戏、特效和致谢页动画定时器。
* @param hWnd 当前窗口句柄。
*/
void StopAppTimers(HWND hWnd);
/**
* @brief 处理致谢页高频动画刷新消息。
* @param hWnd 当前窗口句柄。
*/
void HandleCreditTick(HWND hWnd);
/**
* @brief 处理窗口定时器消息。
* @param hWnd 当前窗口句柄。
* @param timerId 触发的定时器编号。
*/
void HandleTimerMessage(HWND hWnd, WPARAM timerId);
/**
* @brief 启动背景音乐。
*/
void StartBackgroundMusic();
/**
* @brief 停止背景音乐。
*/
void StopBackgroundMusic();
/**
* @brief 切换背景音乐开关并刷新窗口。
* @param hWnd 当前窗口句柄。
*/
void ToggleBackgroundMusic(HWND hWnd);
/**
* @brief 播放复活视频,播放成功返回 true。
* @param hWnd 当前窗口句柄,用于 MCI 播放和父窗口绑定。
* @return 播放成功返回 true,否则返回 false。
*/
bool PlayReviveVideo(HWND hWnd);
/**
* @brief 处理鼠标左键释放事件,返回是否已处理。
* @param hWnd 当前窗口句柄。
* @param lParam 鼠标消息坐标参数。
* @return 事件已被界面逻辑消费返回 true,否则返回 false。
*/
bool HandleMouseClick(HWND hWnd, LPARAM lParam);
/**
* @brief 处理鼠标滚轮事件。
* @param hWnd 当前窗口句柄。
* @param wParam 鼠标滚轮消息参数。
*/
void HandleMouseWheel(HWND hWnd, WPARAM wParam);
/**
* @brief 处理键盘按键事件。
* @param hWnd 当前窗口句柄。
* @param wParam 按键虚拟键码。
*/
void HandleKeyDown(HWND hWnd, WPARAM wParam);
+30
View File
@@ -0,0 +1,30 @@
#pragma once
/**
* @file TetrisAssets.h
* @brief 声明资源路径拼接和文件存在性检查工具函数。
*/
#include "stdafx.h"
#include <string>
/**
* @brief 根据程序所在目录拼出项目资源文件的绝对路径。
* @param relativePath 相对于项目根目录的资源路径。
* @return 规范化后的绝对路径;解析失败时返回拼接路径。
*/
std::wstring BuildAssetPath(const wchar_t* relativePath);
/**
* @brief 根据当前工作目录拼出项目资源文件的绝对路径。
* @param relativePath 相对于当前工作目录的资源路径。
* @return 规范化后的绝对路径;解析失败时返回拼接路径。
*/
std::wstring BuildWorkingDirAssetPath(const wchar_t* relativePath);
/**
* @brief 判断指定路径是否存在且不是目录。
* @param path 待检查的文件路径。
* @return 文件存在且不是目录返回 true,否则返回 false。
*/
bool FileExists(const std::wstring& path);
+67
View File
@@ -1,5 +1,10 @@
#pragma once #pragma once
/**
* @file TetrisLogicInternal.h
* @brief 声明棋盘逻辑、Rogue 结算和特殊方块效果使用的内部接口。
*/
#include "Tetris.h" #include "Tetris.h"
extern Point pendingChainBombCenter; extern Point pendingChainBombCenter;
@@ -11,86 +16,129 @@ extern int pendingLineClearEffectLineCount;
/** /**
* @brief 计算指定方块在棋盘顶部的统一生成位置。 * @brief 计算指定方块在棋盘顶部的统一生成位置。
* @param brickType 方块类型编号。
* @return 生成坐标,可能位于可视区域上方。
*/ */
Point GetSpawnPoint(int brickType); Point GetSpawnPoint(int brickType);
/** /**
* @brief 重置经典或 Rogue 模式使用的玩家统计数据。 * @brief 重置经典或 Rogue 模式使用的玩家统计数据。
* @param stats 需要重置的统计结构。
* @param useRogueRules 是否按 Rogue 模式设置初始经验需求。
*/ */
void ResetPlayerStats(PlayerStats& stats, bool useRogueRules); void ResetPlayerStats(PlayerStats& stats, bool useRogueRules);
/** /**
* @brief 设置界面右侧显示的即时反馈标题、内容和持续时间。 * @brief 设置界面右侧显示的即时反馈标题、内容和持续时间。
* @param title 反馈标题。
* @param detail 反馈详情。
* @param ticks 显示持续的游戏计时次数。
*/ */
void SetFeedbackMessage(const TCHAR* title, const TCHAR* detail, int ticks); void SetFeedbackMessage(const TCHAR* title, const TCHAR* detail, int ticks);
/** /**
* @brief 判断指定方块、旋转状态和位置是否可以合法放置。 * @brief 判断指定方块、旋转状态和位置是否可以合法放置。
* @param pieceType 方块类型编号。
* @param pieceState 方块旋转状态。
* @param position 待检测的左上角坐标。
* @return 可以放置返回 true,否则返回 false。
*/ */
bool IsPiecePlacementValid(int pieceType, int pieceState, Point position); bool IsPiecePlacementValid(int pieceType, int pieceState, Point position);
/** /**
* @brief 判断棋盘格是否为彩虹特殊方块。 * @brief 判断棋盘格是否为彩虹特殊方块。
* @param cellValue 棋盘格存储值。
* @return 彩虹方块返回 true,否则返回 false。
*/ */
bool IsRainbowBoardCell(int cellValue); bool IsRainbowBoardCell(int cellValue);
/** /**
* @brief 触发小型黑洞并返回被清除的固定方块数量。 * @brief 触发小型黑洞并返回被清除的固定方块数量。
* @param maxCellsToClear 最多清除的格子数。
* @return 实际清除格子数。
*/ */
int TriggerMiniBlackHole(int maxCellsToClear); int TriggerMiniBlackHole(int maxCellsToClear);
/** /**
* @brief 触发彩虹方块行清除与覆盖行染色效果。 * @brief 触发彩虹方块行清除与覆盖行染色效果。
* @param anchorRow 作为主色判断的中心行。
* @param minRow 允许染色范围的最小行。
* @param maxRow 允许染色范围的最大行。
* @param recoloredCount 返回被染色的格子数。
* @return 被清除的主色格子数。
*/ */
int TriggerRainbowColorShift(int anchorRow, int minRow, int maxRow, int& recoloredCount); int TriggerRainbowColorShift(int anchorRow, int minRow, int maxRow, int& recoloredCount);
/** /**
* @brief 引爆清屏炸弹并返回清除格数。 * @brief 引爆清屏炸弹并返回清除格数。
* @return 实际清除格子数。
*/ */
int TriggerScreenBomb(); int TriggerScreenBomb();
/** /**
* @brief 清除指定中心点周围的爆破范围并返回清除格数。 * @brief 清除指定中心点周围的爆破范围并返回清除格数。
* @param centerY 爆破中心行。
* @param centerX 爆破中心列。
* @return 实际清除格子数。
*/ */
int ClearExplosiveAreaAt(int centerY, int centerX); int ClearExplosiveAreaAt(int centerY, int centerX);
/** /**
* @brief 清除指定列并返回清除格数。 * @brief 清除指定列并返回清除格数。
* @param column 目标列号。
* @return 实际清除格子数。
*/ */
int ClearColumnAt(int column); int ClearColumnAt(int column);
/** /**
* @brief 使用指定颜色特效清除指定列并返回清除格数。 * @brief 使用指定颜色特效清除指定列并返回清除格数。
* @param column 目标列号。
* @param flashColor 清除高亮颜色。
* @return 实际清除格子数。
*/ */
int ClearColumnAtWithColor(int column, COLORREF flashColor); int ClearColumnAtWithColor(int column, COLORREF flashColor);
/** /**
* @brief 清除指定行并返回清除格数。 * @brief 清除指定行并返回清除格数。
* @param row 目标行号。
* @return 实际清除格子数。
*/ */
int ClearRowAt(int row); int ClearRowAt(int row);
/** /**
* @brief 尝试填补局部空洞以稳定棋盘结构。 * @brief 尝试填补局部空洞以稳定棋盘结构。
* @return 实际填补格子数。
*/ */
int TryStabilizeBoard(); int TryStabilizeBoard();
/** /**
* @brief 为当前方块刷新 Rogue 特殊方块标记。 * @brief 为当前方块刷新 Rogue 特殊方块标记。
* @param allowRandomSpecials 是否允许按强化概率随机生成特殊方块。
*/ */
void RollCurrentPieceSpecialFlags(bool allowRandomSpecials); void RollCurrentPieceSpecialFlags(bool allowRandomSpecials);
/** /**
* @brief 暂存消行动画,等待升级选择结束后再播放。 * @brief 暂存消行动画,等待升级选择结束后再播放。
* @param rows 被消除的行号数组。
* @param rowCount 行号数量。
* @param linesCleared 实际消除行数。
*/ */
void QueueLineClearEffect(const int* rows, int rowCount, int linesCleared); void QueueLineClearEffect(const int* rows, int rowCount, int linesCleared);
/** /**
* @brief 记录固定方块受重力下落的轨迹,用于播放纵向残影特效。 * @brief 记录固定方块受重力下落的轨迹,用于播放纵向残影特效。
* @param x 棋盘列号。
* @param fromY 起始行号。
* @param toY 目标行号。
* @param cellValue 方块格子值。
*/ */
void TriggerGravityFallEffect(int x, int fromY, int toY, int cellValue); void TriggerGravityFallEffect(int x, int fromY, int toY, int cellValue);
/** /**
* @brief 尝试把旋转后的方块横向偏移指定格数后放置。 * @brief 尝试把旋转后的方块横向偏移指定格数后放置。
* @param nextState 旋转后的状态编号。
* @param offsetX 横向试探偏移。
* @return 偏移后可以放置返回 true,否则返回 false。
*/ */
bool TryRotateWithOffset(int nextState, int offsetX); bool TryRotateWithOffset(int nextState, int offsetX);
@@ -101,10 +149,29 @@ void ResetNextQueue();
/** /**
* @brief 消费队首下一方块并补充新的预览方块。 * @brief 消费队首下一方块并补充新的预览方块。
* @return 新的当前方块类型编号。
*/ */
int ConsumeNextType(); int ConsumeNextType();
/** /**
* @brief 结算一次标准消行带来的 Rogue 玩法效果。 * @brief 结算一次标准消行带来的 Rogue 玩法效果。
* @param linesCleared 本次标准消行数量。
*/ */
void ApplyLineClearResult(int linesCleared); void ApplyLineClearResult(int linesCleared);
/**
* @brief 结算彩虹方块固定后的染色和清除效果。
* @param overflowTop 固定时是否已经越过顶部。
* @param fixedCells 当前方块写入棋盘的格子数组。
* @param fixedCellCount 写入棋盘的格子数量。
*/
void ApplyRainbowLandingEffect(bool overflowTop, const Point* fixedCells, int fixedCellCount);
/**
* @brief 结算爆破、激光、十字和稳定结构等特殊落地效果。
* @param fixedCells 当前方块写入棋盘的格子数组。
* @param fixedCellCount 写入棋盘的格子数量。
* @param explosiveCells 爆破方块写入棋盘的格子数组。
* @param explosiveCellCount 爆破格子数量。
*/
void ApplySpecialLandingEffects(const Point* fixedCells, int fixedCellCount, const Point* explosiveCells, int explosiveCellCount);
+23
View File
@@ -0,0 +1,23 @@
#pragma once
/**
* @file TetrisRenderInternal.h
* @brief 声明渲染模块内部使用的 GDI+ 图片加载接口。
*/
#include "Tetris.h"
#include <objidl.h>
#include <gdiplus.h>
/**
* @brief 加载并缓存主背景图片。
* @return 成功时返回缓存位图指针,失败时返回 nullptr。
*/
Gdiplus::Bitmap* LoadBackgroundImage();
/**
* @brief 按序号加载并缓存致谢页图片。
* @param index 致谢页图片序号。
* @return 成功时返回缓存位图指针,失败或越界时返回 nullptr。
*/
Gdiplus::Bitmap* LoadCreditImage(int index);
+5
View File
@@ -1,5 +1,10 @@
#pragma once #pragma once
/**
* @file resource.h
* @brief 定义菜单、图标、对话框和命令等 Windows 资源编号。
*/
//{{NO_DEPENDENCIES}} //{{NO_DEPENDENCIES}}
// Microsoft Visual C++ 生成的包含文件。 // Microsoft Visual C++ 生成的包含文件。
// 供 Tetris.rc 使用 // 供 Tetris.rc 使用
+4 -4
View File
@@ -1,7 +1,7 @@
// stdafx.h : 标准系统包含文件的包含文件, /**
// 或是经常使用但不常更改的 * @file stdafx.h
// 特定于项目的包含文件 * @brief 集中包含 Windows、C 运行时和项目常用基础头文件。
// */
#pragma once #pragma once
+5
View File
@@ -1,5 +1,10 @@
#pragma once #pragma once
/**
* @file targetver.h
* @brief 设置 Windows SDK 目标平台版本,供 Win32 头文件选择可用 API。
*/
// 包括 SDKDDKVer.h 将定义可用的最高版本的 Windows 平台。 // 包括 SDKDDKVer.h 将定义可用的最高版本的 Windows 平台。
// 如果要为以前的 Windows 平台生成应用程序,请包括 WinSDKVer.h,并将 // 如果要为以前的 Windows 平台生成应用程序,请包括 WinSDKVer.h,并将
+84 -1526
View File
File diff suppressed because it is too large Load Diff
+14 -185
View File
@@ -1,4 +1,9 @@
#include "stdafx.h" #include "stdafx.h"
/**
* @file TetrisLogic.cpp
* @brief 实现基础俄罗斯方块的移动、旋转、固定、消行、落点计算和重开逻辑。
*/
#include "Tetris.h" #include "Tetris.h"
#include "TetrisLogicInternal.h" #include "TetrisLogicInternal.h"
@@ -332,7 +337,7 @@ void MoveRight()
* *
* 游戏中的每种方块都预置了 4 种旋转状态,该函数会先尝试切换到下一状态, * 游戏中的每种方块都预置了 4 种旋转状态,该函数会先尝试切换到下一状态,
* 然后检查旋转后的方块是否越界或与固定方块重叠。 * 然后检查旋转后的方块是否越界或与固定方块重叠。
* 如果旋转后的状态非法,则恢复到旋转前的状态 * 如果旋转后的状态非法,Rogue 的完美旋转会继续尝试左右各偏移一格
*/ */
void Rotate() void Rotate()
{ {
@@ -431,66 +436,7 @@ void Fixing()
} }
} }
if (!overflowTop && currentPieceIsRainbow) ApplyRainbowLandingEffect(overflowTop, fixedCells, fixedCellCount);
{
int rainbowAnchorRow = point.y + 1;
if (fixedCellCount > 0)
{
int ySum = 0;
for (int i = 0; i < fixedCellCount; i++)
{
ySum += fixedCells[i].y;
}
rainbowAnchorRow = (ySum + fixedCellCount / 2) / fixedCellCount;
}
if (rainbowAnchorRow < 0)
{
rainbowAnchorRow = 0;
}
if (rainbowAnchorRow >= GetRoguePlayableHeight())
{
rainbowAnchorRow = GetRoguePlayableHeight() - 1;
}
int rainbowRecoloredCount = 0;
int rainbowClearedCount = TriggerRainbowColorShift(rainbowAnchorRow, point.y, point.y + 3, rainbowRecoloredCount);
int rainbowScore = 0;
int rainbowExp = 0;
int voidClearedCount = 0;
int voidScore = 0;
int voidExp = 0;
if (currentMode == MODE_ROGUE && rainbowClearedCount > 0)
{
AwardRogueSkillClearRewards(rainbowClearedCount, rainbowScore, rainbowExp, false);
if (rogueStats.voidCoreLevel > 0)
{
voidClearedCount = TriggerMiniBlackHole(5);
AwardRogueSkillClearRewards(voidClearedCount, voidScore, voidExp, false);
}
}
TCHAR rainbowDetail[128];
if (voidClearedCount > 0)
{
_stprintf_s(
rainbowDetail,
_T("第 %d 行清 %d 格,染色 %d 格,虚空追加 %d 格"),
rainbowAnchorRow + 1,
rainbowClearedCount,
rainbowRecoloredCount,
voidClearedCount);
}
else
{
_stprintf_s(
rainbowDetail,
_T("第 %d 行清除主色 %d 格,覆盖行染色 %d 格。"),
rainbowAnchorRow + 1,
rainbowClearedCount,
rainbowRecoloredCount);
}
SetFeedbackMessage(_T("彩虹方块"), rainbowDetail, 10);
}
if (overflowTop) if (overflowTop)
{ {
@@ -531,129 +477,7 @@ void Fixing()
} }
} }
if (currentPieceIsExplosive) ApplySpecialLandingEffects(fixedCells, fixedCellCount, explosiveCells, explosiveCellCount);
{
int explosiveCellsCleared = 0;
for (int i = 0; i < explosiveCellCount; i++)
{
explosiveCellsCleared += ClearExplosiveAreaAt(explosiveCells[i].y, explosiveCells[i].x);
}
int explosiveScoreGain = 0;
int explosiveExpGain = 0;
if (currentMode == MODE_ROGUE && explosiveCellsCleared > 0)
{
AwardRogueSkillClearRewards(explosiveCellsCleared, explosiveScoreGain, explosiveExpGain, false);
}
TCHAR explosiveDetail[128];
_stprintf_s(
explosiveDetail,
_T("爆破清除 %d 格 +%d 分 +%d EXP"),
explosiveCellsCleared,
explosiveScoreGain,
explosiveExpGain);
SetFeedbackMessage(_T("爆破核心"), explosiveDetail, 12);
if (rogueStats.chainBombLevel > 0 && explosiveCellCount > 0)
{
pendingChainBombCenter = explosiveCells[0];
pendingChainBombFollowup = true;
}
}
if (currentPieceIsLaser)
{
int laserColumn = point.x + 1;
if (fixedCellCount > 0)
{
int xSum = 0;
for (int i = 0; i < fixedCellCount; i++)
{
xSum += fixedCells[i].x;
}
laserColumn = (xSum + fixedCellCount / 2) / fixedCellCount;
}
if (laserColumn < 0)
{
laserColumn = 0;
}
if (laserColumn >= nGameWidth)
{
laserColumn = nGameWidth - 1;
}
int laserCellsCleared = ClearColumnAt(laserColumn);
if (currentMode == MODE_ROGUE && laserCellsCleared > 0)
{
int laserScore = 0;
int laserExp = 0;
AwardRogueSkillClearRewards(laserCellsCleared, laserScore, laserExp, false);
TCHAR laserDetail[128];
_stprintf_s(laserDetail, _T("激光贯穿第 %d 列,清除 %d 格 +%d 分 +%d EXP"), laserColumn + 1, laserCellsCleared, laserScore, laserExp);
SetFeedbackMessage(_T("棱镜激光"), laserDetail, 12);
}
}
if (currentPieceIsCross)
{
int crossRow = point.y + 1;
int crossColumn = point.x + 1;
if (fixedCellCount > 0)
{
int xSum = 0;
int ySum = 0;
for (int i = 0; i < fixedCellCount; i++)
{
xSum += fixedCells[i].x;
ySum += fixedCells[i].y;
}
crossColumn = (xSum + fixedCellCount / 2) / fixedCellCount;
crossRow = (ySum + fixedCellCount / 2) / fixedCellCount;
}
if (crossRow < 0)
{
crossRow = 0;
}
if (crossRow >= GetRoguePlayableHeight())
{
crossRow = GetRoguePlayableHeight() - 1;
}
if (crossColumn < 0)
{
crossColumn = 0;
}
if (crossColumn >= nGameWidth)
{
crossColumn = nGameWidth - 1;
}
int crossCellsCleared = ClearRowAt(crossRow);
int columnCellsCleared = ClearColumnAtWithColor(crossColumn, RGB(196, 255, 132));
if (workRegion[crossRow][crossColumn] == 0 && columnCellsCleared > 0)
{
// center cell may already be counted by row clear
}
int totalCrossCleared = crossCellsCleared + columnCellsCleared;
if (currentMode == MODE_ROGUE && totalCrossCleared > 0)
{
int crossScore = 0;
int crossExp = 0;
AwardRogueSkillClearRewards(totalCrossCleared, crossScore, crossExp, false);
TCHAR crossDetail[128];
_stprintf_s(crossDetail, _T("十字冲击第 %d 行 / 第 %d 列,清除 %d 格 +%d 分 +%d EXP"), crossRow + 1, crossColumn + 1, totalCrossCleared, crossScore, crossExp);
SetFeedbackMessage(_T("十字方块"), crossDetail, 12);
}
}
if (!currentPieceIsRainbow && TryStabilizeBoard() > 0)
{
SetFeedbackMessage(_T("稳定结构"), _T("附近空洞被自动填补,阵型更加稳固。"), 10);
}
if (currentMode == MODE_ROGUE) if (currentMode == MODE_ROGUE)
{ {
@@ -703,6 +527,8 @@ void DeleteOneLine(int number)
* 如果某一行全部非 0,则调用 DeleteOneLine 删除该行, * 如果某一行全部非 0,则调用 DeleteOneLine 删除该行,
* 并将该行上方的内容整体下移。为了避免连续满行被漏检, * 并将该行上方的内容整体下移。为了避免连续满行被漏检,
* 删除后会继续检查当前行号。每成功消除 1 行,当前得分增加 100 分。 * 删除后会继续检查当前行号。每成功消除 1 行,当前得分增加 100 分。
*
* @return 本次实际消除的行数。
*/ */
int DeleteLines() int DeleteLines()
{ {
@@ -738,6 +564,7 @@ int DeleteLines()
} }
} }
// 消行数量先进入玩法结算,再根据是否正在升级决定动画立即播放还是暂存。
ApplyLineClearResult(clearedLines); ApplyLineClearResult(clearedLines);
if (currentScreen == SCREEN_UPGRADE) if (currentScreen == SCREEN_UPGRADE)
{ {
@@ -748,6 +575,7 @@ int DeleteLines()
TriggerLineClearEffect(clearedRows, clearedRowCount, clearedLines); TriggerLineClearEffect(clearedRows, clearedRowCount, clearedLines);
} }
// 连环炸弹的追加爆破只在爆破方块导致后续消行时触发一次。
if (pendingChainBombFollowup && clearedLines > 0) if (pendingChainBombFollowup && clearedLines > 0)
{ {
pendingChainBombFollowup = false; pendingChainBombFollowup = false;
@@ -876,3 +704,4 @@ void Restart()
ComputeTarget(); ComputeTarget();
} }
+29 -163
View File
@@ -1,13 +1,23 @@
#include "stdafx.h" #include "stdafx.h"
/**
* @file TetrisRender.cpp
* @brief 实现主菜单、帮助页、游戏棋盘、侧栏、覆盖层和升级界面的完整绘制逻辑。
*/
#include "Tetris.h" #include "Tetris.h"
#include "TetrisRenderInternal.h"
#include <objidl.h> #include <objidl.h>
#include <gdiplus.h> #include <gdiplus.h>
#include <string>
#pragma comment(lib, "gdiplus.lib") #pragma comment(lib, "gdiplus.lib")
using namespace Gdiplus; using namespace Gdiplus;
/**
* @brief 按颜色缓存粒子画刷,减少动画绘制时重复创建 GDI 对象。
* @param color 画刷颜色。
* @return 可复用的实心画刷句柄;缓存满时返回临时新建画刷。
*/
static HBRUSH GetCachedParticleBrush(COLORREF color) static HBRUSH GetCachedParticleBrush(COLORREF color)
{ {
static COLORREF cachedColors[16] = {}; static COLORREF cachedColors[16] = {};
@@ -34,174 +44,22 @@ static HBRUSH GetCachedParticleBrush(COLORREF color)
return CreateSolidBrush(color); return CreateSolidBrush(color);
} }
static 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)
{
basePath.resize(lastSlash);
}
std::wstring projectRelative = basePath + L"\\..\\..\\" + relativePath;
wchar_t fullPath[MAX_PATH] = {};
DWORD result = GetFullPathNameW(projectRelative.c_str(), MAX_PATH, fullPath, nullptr);
if (result > 0 && result < MAX_PATH)
{
return fullPath;
}
return projectRelative;
}
static std::wstring BuildWorkingDirAssetPath(const wchar_t* relativePath)
{
wchar_t currentDirectory[MAX_PATH] = {};
DWORD length = GetCurrentDirectoryW(MAX_PATH, currentDirectory);
if (length == 0 || length >= MAX_PATH)
{
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);
if (result > 0 && result < MAX_PATH)
{
return fullPath;
}
return candidate;
}
static Bitmap* LoadBackgroundImage()
{
static ULONG_PTR gdiplusToken = 0;
static Bitmap* backgroundImage = nullptr;
static bool attempted = false;
if (!attempted)
{
attempted = true;
GdiplusStartupInput startupInput;
if (GdiplusStartup(&gdiplusToken, &startupInput, nullptr) == Ok)
{
const std::wstring candidates[] =
{
BuildAssetPath(L"assets\\images\\background.png"),
BuildWorkingDirAssetPath(L"assets\\images\\background.png"),
BuildAssetPath(L"assets\\images\\background.bmp"),
BuildWorkingDirAssetPath(L"assets\\images\\background.bmp")
};
for (const std::wstring& candidate : candidates)
{
if (candidate.empty())
{
continue;
}
DWORD attributes = GetFileAttributesW(candidate.c_str());
if (attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0)
{
continue;
}
Bitmap* loadedImage = Bitmap::FromFile(candidate.c_str(), FALSE);
if (loadedImage != nullptr && loadedImage->GetLastStatus() == Ok)
{
backgroundImage = loadedImage;
break;
}
delete loadedImage;
}
}
}
return backgroundImage;
}
/** /**
* @brief 按序号加载致谢页图片资源 * @brief 绘制当前游戏窗口的完整界面
*
* 函数按当前屏幕状态绘制主菜单、帮助页、游戏棋盘、侧栏、覆盖层和升级选择。
* 由于大量绘图辅助逻辑共享当前缩放、字体和颜色,保持在同一函数内集中管理,
* 避免拆分时改变 GDI 对象的选择和释放顺序。
*
* @param hdc 目标绘图设备上下文。
* @param hWnd 当前窗口句柄,用于读取客户区大小。
*/ */
static Bitmap* LoadCreditImage(int index)
{
constexpr int creditPageCount = 4;
static ULONG_PTR gdiplusToken = 0;
static Bitmap* creditImages[creditPageCount] = {};
static bool attempted[creditPageCount] = {};
if (index < 0 || index >= creditPageCount)
{
return nullptr;
}
if (!attempted[index])
{
attempted[index] = true;
GdiplusStartupInput startupInput;
if (GdiplusStartup(&gdiplusToken, &startupInput, nullptr) == Ok)
{
const wchar_t* imageNames[creditPageCount] =
{
L"assets\\images\\qls.jpg",
L"assets\\images\\wyk.jpg",
L"assets\\images\\swj.jpg",
L"assets\\images\\qhy.jpg"
};
const std::wstring creditExtraCandidates[] =
{
BuildAssetPath(imageNames[index]),
BuildWorkingDirAssetPath(imageNames[index]),
BuildAssetPath(L"assets\\images\\qhy.png"),
BuildWorkingDirAssetPath(L"assets\\images\\qhy.png"),
BuildAssetPath(L"assets\\images\\qhy.jpeg"),
BuildWorkingDirAssetPath(L"assets\\images\\qhy.jpeg"),
BuildAssetPath(L"assets\\images\\qhy.bmp"),
BuildWorkingDirAssetPath(L"assets\\images\\qhy.bmp")
};
int candidateCount = (index == 3) ? 8 : 2;
for (int i = 0; i < candidateCount; i++)
{
const std::wstring& candidate = creditExtraCandidates[i];
if (candidate.empty())
{
continue;
}
DWORD attributes = GetFileAttributesW(candidate.c_str());
if (attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0)
{
continue;
}
Bitmap* loadedImage = Bitmap::FromFile(candidate.c_str(), FALSE);
if (loadedImage != nullptr && loadedImage->GetLastStatus() == Ok)
{
creditImages[index] = loadedImage;
break;
}
delete loadedImage;
}
}
}
return creditImages[index];
}
void TDrawScreen(HDC hdc, HWND hWnd) void TDrawScreen(HDC hdc, HWND hWnd)
{ {
RECT clientRect; RECT clientRect;
GetClientRect(hWnd, &clientRect); GetClientRect(hWnd, &clientRect);
// 根据窗口大小计算统一缩放比例,所有坐标都从设计稿尺寸映射到实际窗口。
int clientWidth = clientRect.right - clientRect.left; int clientWidth = clientRect.right - clientRect.left;
int clientHeight = clientRect.bottom - clientRect.top; int clientHeight = clientRect.bottom - clientRect.top;
int scaleX = MulDiv(clientWidth, 1000, WINDOW_CLIENT_WIDTH); int scaleX = MulDiv(clientWidth, 1000, WINDOW_CLIENT_WIDTH);
@@ -282,6 +140,7 @@ void TDrawScreen(HDC hdc, HWND hWnd)
const BYTE panelNestedAlpha = 128; const BYTE panelNestedAlpha = 128;
const BYTE panelStrongAlpha = 168; const BYTE panelStrongAlpha = 168;
// 背景图片存在时优先绘制图片并叠加浅色遮罩,否则使用纯色和装饰形状。
Bitmap* backgroundImage = LoadBackgroundImage(); Bitmap* backgroundImage = LoadBackgroundImage();
if (backgroundImage != nullptr) if (backgroundImage != nullptr)
{ {
@@ -318,6 +177,7 @@ void TDrawScreen(HDC hdc, HWND hWnd)
DeleteObject(blobBrushB); DeleteObject(blobBrushB);
} }
// 本函数集中创建字体,所有提前 return 分支都要在返回前释放这些 GDI 对象。
HFONT titleFont = CreateFont( HFONT titleFont = CreateFont(
-SS(36), 0, 0, 0, FW_BOLD, FALSE, FALSE, FALSE, -SS(36), 0, 0, 0, FW_BOLD, FALSE, FALSE, FALSE,
DEFAULT_CHARSET, OUT_OUTLINE_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_NATURAL_QUALITY, DEFAULT_CHARSET, OUT_OUTLINE_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_NATURAL_QUALITY,
@@ -341,6 +201,7 @@ void TDrawScreen(HDC hdc, HWND hWnd)
SetBkMode(hdc, TRANSPARENT); SetBkMode(hdc, TRANSPARENT);
SetTextColor(hdc, textColor); SetTextColor(hdc, textColor);
// 以下局部绘图函数共享 hdc、缩放函数和颜色,避免每个小控件重复计算上下文。
auto DrawPanelCard = [&](const RECT& rect, COLORREF fillColor, COLORREF borderColor, int radius) auto DrawPanelCard = [&](const RECT& rect, COLORREF fillColor, COLORREF borderColor, int radius)
{ {
HBRUSH cardBrush = CreateSolidBrush(fillColor); HBRUSH cardBrush = CreateSolidBrush(fillColor);
@@ -475,6 +336,7 @@ void TDrawScreen(HDC hdc, HWND hWnd)
DeleteObject(backPen); DeleteObject(backPen);
}; };
// 主菜单独立绘制并提前返回,避免后续游戏棋盘和侧栏在菜单后面继续绘制。
if (currentScreen == SCREEN_MENU) if (currentScreen == SCREEN_MENU)
{ {
RECT menuCard = RECT menuCard =
@@ -602,6 +464,7 @@ void TDrawScreen(HDC hdc, HWND hWnd)
return; return;
} }
// 帮助、规则、图鉴、致谢和技能演示入口共用规则页卡片框架。
if (currentScreen == SCREEN_RULES) if (currentScreen == SCREEN_RULES)
{ {
RECT rulesCard = RECT rulesCard =
@@ -2463,6 +2326,7 @@ void TDrawScreen(HDC hdc, HWND hWnd)
DT_LEFT | DT_TOP | DT_WORDBREAK); DT_LEFT | DT_TOP | DT_WORDBREAK);
} }
// 暂停和结束覆盖层只盖住棋盘区域,让两侧战斗信息仍然可见。
if (suspendFlag || gameOverFlag) if (suspendFlag || gameOverFlag)
{ {
RECT overlayRect = RECT overlayRect =
@@ -2590,6 +2454,7 @@ void TDrawScreen(HDC hdc, HWND hWnd)
} }
} }
// 升级选择界面在当前战局上方绘制半透明遮罩,保留背景局势作为上下文。
if (currentScreen == SCREEN_UPGRADE) if (currentScreen == SCREEN_UPGRADE)
{ {
RECT dimRect = RECT dimRect =
@@ -2855,3 +2720,4 @@ void TDrawScreen(HDC hdc, HWND hWnd)
DeleteObject(bodyFont); DeleteObject(bodyFont);
DeleteObject(smallFont); DeleteObject(smallFont);
} }
+842
View File
@@ -0,0 +1,842 @@
#include "stdafx.h"
/**
* @file TetrisInput.cpp
* @brief
*/
#include "TetrisAppInternal.h"
/**
* @brief
* @param hWnd
*/
static void ActivateMenuSelection(HWND hWnd)
{
if (menuState.selectedIndex == 0)
{
StartGameWithMode(MODE_CLASSIC);
}
else if (menuState.selectedIndex == 1)
{
StartGameWithMode(MODE_ROGUE);
}
else if (menuState.selectedIndex == 2)
{
OpenRulesScreen();
}
else
{
OpenCreditScreen();
}
ResetGameTimer(hWnd);
InvalidateRect(hWnd, nullptr, FALSE);
}
/**
* @brief
* @param hWnd
*/
static void HandleBackButtonClick(HWND hWnd)
{
if (currentScreen == SCREEN_PLAYING && IsRogueSkillDemoMode())
{
OpenSkillDemoScreen();
}
else if (currentScreen == SCREEN_RULES && helpState.currentPage != 0)
{
helpState.selectedIndex = (helpState.currentPage == 5) ? 3 : helpState.currentPage - 1;
helpState.currentPage = 0;
helpScrollOffset = 0;
}
else
{
ReturnToMainMenu();
}
InvalidateRect(hWnd, nullptr, FALSE);
}
/**
* @brief
* @param hWnd
* @param mouseX
* @param mouseY
* @return true false
*/
static bool HandleMenuClick(HWND hWnd, int mouseX, int mouseY)
{
if (currentScreen != SCREEN_MENU)
{
return false;
}
for (int i = 0; i < menuState.optionCount; i++)
{
if (!IsPointInRect(GetMenuOptionRect(hWnd, i), mouseX, mouseY))
{
continue;
}
menuState.selectedIndex = i;
ActivateMenuSelection(hWnd);
return true;
}
return true;
}
/**
* @brief
* @param hWnd
* @param mouseX
* @param mouseY
* @return true false
*/
static bool HandleRulesClick(HWND hWnd, int mouseX, int mouseY)
{
if (currentScreen != SCREEN_RULES)
{
return false;
}
if (helpState.currentPage == 0)
{
// 帮助首页的四个入口分别进入介绍、操作、图鉴和技能演示页。
for (int i = 0; i < helpState.optionCount; i++)
{
if (IsPointInRect(GetHelpOptionRect(hWnd, i), mouseX, mouseY))
{
helpState.selectedIndex = i;
if (i == 3)
{
helpState.currentPage = 5;
helpState.selectedIndex = 0;
helpScrollOffset = 0;
}
else
{
helpState.currentPage = i + 1;
helpScrollOffset = 0;
}
InvalidateRect(hWnd, nullptr, FALSE);
return true;
}
}
if (IsPointInRect(GetHelpBackHintRect(hWnd), mouseX, mouseY))
{
ReturnToMainMenu();
InvalidateRect(hWnd, nullptr, FALSE);
}
return true;
}
if (helpState.currentPage == 5)
{
if (IsPointInRect(GetHelpBackHintRect(hWnd), mouseX, mouseY))
{
helpState.currentPage = 0;
helpState.selectedIndex = 3;
helpScrollOffset = 0;
InvalidateRect(hWnd, nullptr, FALSE);
return true;
}
// 技能演示页的列表项直接启动对应预设棋盘。
int demoCount = GetRogueSkillDemoCount();
for (int i = 0; i < demoCount; i++)
{
if (IsPointInRect(GetHelpSkillDemoItemRect(hWnd, i), mouseX, mouseY))
{
helpState.selectedIndex = i;
StartRogueSkillDemoAt(i);
ResetGameTimer(hWnd);
InvalidateRect(hWnd, nullptr, FALSE);
return true;
}
}
return true;
}
if (IsPointInRect(GetHelpBackHintRect(hWnd), mouseX, mouseY))
{
helpState.selectedIndex = (helpState.currentPage == 5) ? 3 : helpState.currentPage - 1;
helpState.currentPage = 0;
helpScrollOffset = 0;
InvalidateRect(hWnd, nullptr, FALSE);
}
else if (helpState.currentPage == 4 && IsPointInRect(GetCreditArrowRect(hWnd, -1), mouseX, mouseY))
{
ChangeCreditPage(-1);
InvalidateRect(hWnd, nullptr, FALSE);
}
else if (helpState.currentPage == 4 && IsPointInRect(GetCreditArrowRect(hWnd, 1), mouseX, mouseY))
{
ChangeCreditPage(1);
InvalidateRect(hWnd, nullptr, FALSE);
}
return true;
}
/**
* @brief
* @param hWnd
* @param mouseX
* @param mouseY
* @return true false
*/
static bool HandleUpgradeClick(HWND hWnd, int mouseX, int mouseY)
{
if (currentScreen != SCREEN_UPGRADE)
{
return false;
}
for (int i = 0; i < upgradeUiState.optionCount; i++)
{
if (!IsPointInRect(GetUpgradeCardRect(hWnd, i), mouseX, mouseY))
{
continue;
}
upgradeUiState.selectedIndex = i;
// 多选强化先标记卡片,达到本次可选数量后再统一确认。
if (upgradeUiState.picksRemaining > 1)
{
bool currentlyMarked = upgradeUiState.marked[i];
if (currentlyMarked)
{
upgradeUiState.marked[i] = false;
if (upgradeUiState.markedCount > 0)
{
upgradeUiState.markedCount--;
}
}
else if (upgradeUiState.markedCount < upgradeUiState.picksRemaining)
{
upgradeUiState.marked[i] = true;
upgradeUiState.markedCount++;
}
if (upgradeUiState.markedCount == upgradeUiState.picksRemaining)
{
ConfirmUpgradeSelection();
ResetGameTimer(hWnd);
}
}
else
{
ConfirmUpgradeSelection();
ResetGameTimer(hWnd);
}
InvalidateRect(hWnd, nullptr, FALSE);
return true;
}
return true;
}
/**
* @brief
* @param hWnd
* @param mouseX
* @param mouseY
* @return true false
*/
static bool HandleOverlayClick(HWND hWnd, int mouseX, int mouseY)
{
if (currentScreen == SCREEN_PLAYING && suspendFlag)
{
if (IsPointInRect(GetOverlayButtonRect(hWnd, 0, 2), mouseX, mouseY))
{
suspendFlag = false;
InvalidateRect(hWnd, nullptr, FALSE);
return true;
}
if (IsPointInRect(GetOverlayButtonRect(hWnd, 1, 2), mouseX, mouseY))
{
ReturnToMainMenu();
InvalidateRect(hWnd, nullptr, FALSE);
return true;
}
}
if (currentScreen == SCREEN_PLAYING && gameOverFlag)
{
if (reviveAvailable)
{
if (IsPointInRect(GetOverlayButtonRect(hWnd, 0, 3), mouseX, mouseY))
{
if (PlayReviveVideo(hWnd))
{
ReviveAfterVideo();
ResetGameTimer(hWnd);
}
InvalidateRect(hWnd, nullptr, FALSE);
return true;
}
if (IsPointInRect(GetOverlayButtonRect(hWnd, 1, 3), mouseX, mouseY))
{
StartGameWithMode(currentMode);
ResetGameTimer(hWnd);
InvalidateRect(hWnd, nullptr, FALSE);
return true;
}
if (IsPointInRect(GetOverlayButtonRect(hWnd, 2, 3), mouseX, mouseY))
{
ReturnToMainMenu();
InvalidateRect(hWnd, nullptr, FALSE);
return true;
}
}
else
{
if (IsPointInRect(GetOverlayButtonRect(hWnd, 0, 2), mouseX, mouseY))
{
StartGameWithMode(currentMode);
ResetGameTimer(hWnd);
InvalidateRect(hWnd, nullptr, FALSE);
return true;
}
if (IsPointInRect(GetOverlayButtonRect(hWnd, 1, 2), mouseX, mouseY))
{
ReturnToMainMenu();
InvalidateRect(hWnd, nullptr, FALSE);
return true;
}
}
}
return false;
}
/**
* @brief
* @param hWnd
* @param lParam
* @return true false
*/
bool HandleMouseClick(HWND hWnd, LPARAM lParam)
{
int mouseX = static_cast<short>(LOWORD(lParam));
int mouseY = static_cast<short>(HIWORD(lParam));
if (IsPointInRect(GetMusicButtonRect(hWnd), mouseX, mouseY))
{
ToggleBackgroundMusic(hWnd);
return true;
}
if (currentScreen != SCREEN_MENU && IsPointInRect(GetBackButtonRect(hWnd), mouseX, mouseY))
{
HandleBackButtonClick(hWnd);
return true;
}
if (HandleMenuClick(hWnd, mouseX, mouseY) ||
HandleRulesClick(hWnd, mouseX, mouseY) ||
HandleUpgradeClick(hWnd, mouseX, mouseY) ||
HandleOverlayClick(hWnd, mouseX, mouseY))
{
return true;
}
return false;
}
/**
* @brief
* @param hWnd
* @param wParam
*/
void HandleMouseWheel(HWND hWnd, WPARAM wParam)
{
int wheelDelta = GET_WHEEL_DELTA_WPARAM(wParam);
int direction = (wheelDelta > 0) ? -1 : 1;
int scrollStep = GetScrollStep(hWnd, 64);
if (currentScreen == SCREEN_RULES && helpState.currentPage != 0)
{
AdjustScrollOffset(helpScrollOffset, direction * scrollStep);
InvalidateRect(hWnd, nullptr, FALSE);
return;
}
if (currentScreen == SCREEN_PLAYING && currentMode == MODE_ROGUE)
{
AdjustScrollOffset(upgradeListScrollOffset, direction * scrollStep);
InvalidateRect(hWnd, nullptr, FALSE);
}
}
/**
* @brief
* @param hWnd
* @param key
* @return true false
*/
static bool HandleMenuKey(HWND hWnd, WPARAM key)
{
if (currentScreen != SCREEN_MENU)
{
return false;
}
switch (key)
{
case VK_UP:
case VK_LEFT:
case 'W':
case 'A':
menuState.selectedIndex--;
if (menuState.selectedIndex < 0)
{
menuState.selectedIndex = menuState.optionCount - 1;
}
InvalidateRect(hWnd, nullptr, FALSE);
break;
case VK_DOWN:
case VK_RIGHT:
case 'S':
case 'D':
menuState.selectedIndex++;
if (menuState.selectedIndex >= menuState.optionCount)
{
menuState.selectedIndex = 0;
}
InvalidateRect(hWnd, nullptr, FALSE);
break;
case VK_RETURN:
case VK_SPACE:
ActivateMenuSelection(hWnd);
break;
case VK_ESCAPE:
DestroyWindow(hWnd);
break;
default:
break;
}
return true;
}
/**
* @brief
* @param hWnd
* @param key
* @return true false
*/
static bool HandleRulesKey(HWND hWnd, WPARAM key)
{
if (currentScreen != SCREEN_RULES)
{
return false;
}
switch (key)
{
case VK_UP:
case VK_LEFT:
case 'W':
case 'A':
if (helpState.currentPage == 0)
{
helpState.selectedIndex--;
if (helpState.selectedIndex < 0)
{
helpState.selectedIndex = helpState.optionCount - 1;
}
InvalidateRect(hWnd, nullptr, FALSE);
}
else if (helpState.currentPage == 4)
{
ChangeCreditPage(-1);
InvalidateRect(hWnd, nullptr, FALSE);
}
else if (helpState.currentPage == 5)
{
helpState.selectedIndex--;
if (helpState.selectedIndex < 0)
{
helpState.selectedIndex = GetRogueSkillDemoCount() - 1;
}
if (helpState.selectedIndex * 68 < helpScrollOffset)
{
helpScrollOffset = helpState.selectedIndex * 68;
}
InvalidateRect(hWnd, nullptr, FALSE);
}
break;
case VK_DOWN:
case VK_RIGHT:
case 'S':
case 'D':
if (helpState.currentPage == 0)
{
helpState.selectedIndex++;
if (helpState.selectedIndex >= helpState.optionCount)
{
helpState.selectedIndex = 0;
}
InvalidateRect(hWnd, nullptr, FALSE);
}
else if (helpState.currentPage == 4)
{
ChangeCreditPage(1);
InvalidateRect(hWnd, nullptr, FALSE);
}
else if (helpState.currentPage == 5)
{
helpState.selectedIndex++;
if (helpState.selectedIndex >= GetRogueSkillDemoCount())
{
helpState.selectedIndex = 0;
helpScrollOffset = 0;
}
else if (helpState.selectedIndex * 68 > helpScrollOffset + 360)
{
helpScrollOffset = helpState.selectedIndex * 68 - 360;
}
InvalidateRect(hWnd, nullptr, FALSE);
}
break;
case VK_RETURN:
case VK_SPACE:
if (helpState.currentPage == 0)
{
if (helpState.selectedIndex == 3)
{
helpState.currentPage = 5;
helpState.selectedIndex = 0;
helpScrollOffset = 0;
}
else
{
helpState.currentPage = helpState.selectedIndex + 1;
helpScrollOffset = 0;
}
InvalidateRect(hWnd, nullptr, FALSE);
}
else if (helpState.currentPage == 5)
{
StartRogueSkillDemoAt(helpState.selectedIndex);
ResetGameTimer(hWnd);
InvalidateRect(hWnd, nullptr, FALSE);
}
break;
case VK_ESCAPE:
case VK_BACK:
case 'M':
{
int previousPage = helpState.currentPage;
if (helpState.currentPage == 0)
{
ReturnToMainMenu();
}
else
{
helpState.currentPage = 0;
if (previousPage == 4 || previousPage == 5)
{
helpState.selectedIndex = 3;
}
helpScrollOffset = 0;
}
InvalidateRect(hWnd, nullptr, FALSE);
break;
}
default:
break;
}
return true;
}
/**
* @brief
* @param hWnd
* @param key
* @return true false
*/
static bool HandleUpgradeKey(HWND hWnd, WPARAM key)
{
if (currentScreen != SCREEN_UPGRADE)
{
return false;
}
int upgradeColumnCount = upgradeUiState.optionCount <= 3 ? upgradeUiState.optionCount : 3;
if (upgradeColumnCount < 1)
{
upgradeColumnCount = 1;
}
switch (key)
{
case VK_LEFT:
case 'A':
if (upgradeUiState.optionCount > 1)
{
int rowStart = upgradeUiState.selectedIndex - (upgradeUiState.selectedIndex % upgradeColumnCount);
if (upgradeUiState.selectedIndex > rowStart)
{
upgradeUiState.selectedIndex--;
}
else
{
int rowEnd = rowStart + upgradeColumnCount - 1;
if (rowEnd >= upgradeUiState.optionCount)
{
rowEnd = upgradeUiState.optionCount - 1;
}
upgradeUiState.selectedIndex = rowEnd;
}
}
InvalidateRect(hWnd, nullptr, FALSE);
break;
case VK_RIGHT:
case 'D':
if (upgradeUiState.optionCount > 1)
{
int rowStart = upgradeUiState.selectedIndex - (upgradeUiState.selectedIndex % upgradeColumnCount);
int rowEnd = rowStart + upgradeColumnCount - 1;
if (rowEnd >= upgradeUiState.optionCount)
{
rowEnd = upgradeUiState.optionCount - 1;
}
upgradeUiState.selectedIndex = (upgradeUiState.selectedIndex < rowEnd) ? upgradeUiState.selectedIndex + 1 : rowStart;
}
InvalidateRect(hWnd, nullptr, FALSE);
break;
case VK_UP:
case 'W':
if (upgradeUiState.selectedIndex >= upgradeColumnCount)
{
upgradeUiState.selectedIndex -= upgradeColumnCount;
}
InvalidateRect(hWnd, nullptr, FALSE);
break;
case VK_DOWN:
case 'S':
if (upgradeUiState.selectedIndex + upgradeColumnCount < upgradeUiState.optionCount)
{
upgradeUiState.selectedIndex += upgradeColumnCount;
}
InvalidateRect(hWnd, nullptr, FALSE);
break;
case VK_RETURN:
ConfirmUpgradeSelection();
ResetGameTimer(hWnd);
InvalidateRect(hWnd, nullptr, FALSE);
break;
case VK_SPACE:
if (upgradeUiState.picksRemaining > 1 && upgradeUiState.optionCount > 0)
{
bool currentlyMarked = upgradeUiState.marked[upgradeUiState.selectedIndex];
if (currentlyMarked)
{
upgradeUiState.marked[upgradeUiState.selectedIndex] = false;
if (upgradeUiState.markedCount > 0)
{
upgradeUiState.markedCount--;
}
}
else if (upgradeUiState.markedCount < upgradeUiState.picksRemaining)
{
upgradeUiState.marked[upgradeUiState.selectedIndex] = true;
upgradeUiState.markedCount++;
}
InvalidateRect(hWnd, nullptr, FALSE);
}
else
{
ConfirmUpgradeSelection();
ResetGameTimer(hWnd);
InvalidateRect(hWnd, nullptr, FALSE);
}
break;
case 'M':
ReturnToMainMenu();
InvalidateRect(hWnd, nullptr, FALSE);
break;
default:
break;
}
return true;
}
/**
* @brief
* @param hWnd
* @param key
*/
static void HandlePlayingKey(HWND hWnd, WPARAM key)
{
if (IsRogueSkillDemoMode())
{
if (key == 'N')
{
AdvanceRogueSkillDemo();
InvalidateRect(hWnd, nullptr, FALSE);
return;
}
if (key == 'R')
{
RestartCurrentRogueSkillDemo();
ResetGameTimer(hWnd);
InvalidateRect(hWnd, nullptr, FALSE);
return;
}
if (key == VK_ESCAPE || key == VK_BACK || key == 'M')
{
OpenSkillDemoScreen();
InvalidateRect(hWnd, nullptr, FALSE);
return;
}
}
if (!IsRogueSkillDemoMode() && key == 'M')
{
ReturnToMainMenu();
InvalidateRect(hWnd, nullptr, FALSE);
return;
}
if (!IsRogueSkillDemoMode() && key == 'R')
{
StartGameWithMode(currentMode);
ResetGameTimer(hWnd);
InvalidateRect(hWnd, nullptr, FALSE);
return;
}
if (!IsRogueSkillDemoMode() && key == 'P')
{
suspendFlag = !suspendFlag;
InvalidateRect(hWnd, nullptr, FALSE);
return;
}
if (key == 'G')
{
targetFlag = !targetFlag;
InvalidateRect(hWnd, nullptr, FALSE);
return;
}
if (gameOverFlag && reviveAvailable && key == 'V')
{
if (PlayReviveVideo(hWnd))
{
ReviveAfterVideo();
ResetGameTimer(hWnd);
}
else
{
SetFeedbackMessage(_T("视频播放失败"), _T("无法打开复活视频,复活机会未消耗。"), 14);
}
InvalidateRect(hWnd, nullptr, FALSE);
return;
}
if (gameOverFlag || suspendFlag)
{
return;
}
if (currentMode == MODE_ROGUE && (key == 'J' || key == 'K'))
{
int direction = (key == 'J') ? 1 : -1;
AdjustScrollOffset(upgradeListScrollOffset, direction * GetScrollStep(hWnd, 52));
InvalidateRect(hWnd, nullptr, FALSE);
return;
}
// 正常游玩按键先改变方块或触发技能,再统一刷新预测落点和界面。
switch (key)
{
case VK_LEFT:
case 'A':
if (CanMoveLeft())
{
MoveLeft();
}
break;
case VK_RIGHT:
case 'D':
if (CanMoveRight())
{
MoveRight();
}
break;
case VK_DOWN:
case 'S':
if (CanMoveDown())
{
MoveDown();
}
else
{
Fixing();
if (!gameOverFlag)
{
DeleteLines();
CheckRogueLevelProgress();
}
}
break;
case VK_UP:
case 'W':
Rotate();
break;
case VK_SPACE:
DropDown();
Fixing();
if (!gameOverFlag)
{
DeleteLines();
CheckRogueLevelProgress();
}
break;
case 'C':
case VK_SHIFT:
case VK_LSHIFT:
case VK_RSHIFT:
HoldCurrentPiece();
break;
case 'Z':
UseBlackHole();
break;
case 'X':
UseScreenBomb();
break;
case 'V':
UseAirReshape();
break;
default:
break;
}
if (!gameOverFlag)
{
ComputeTarget();
}
InvalidateRect(hWnd, nullptr, FALSE);
}
/**
* @brief
* @param hWnd
* @param wParam
*/
void HandleKeyDown(HWND hWnd, WPARAM wParam)
{
if (HandleMenuKey(hWnd, wParam) ||
HandleRulesKey(hWnd, wParam) ||
HandleUpgradeKey(hWnd, wParam))
{
return;
}
HandlePlayingKey(hWnd, wParam);
}
+422
View File
@@ -0,0 +1,422 @@
#include "stdafx.h"
/**
* @file TetrisLayout.cpp
* @brief
*/
#include "TetrisAppInternal.h"
/**
* @brief
* @param scrollOffset
* @param delta
*/
void AdjustScrollOffset(int& scrollOffset, int delta)
{
scrollOffset += delta;
if (scrollOffset < 0)
{
scrollOffset = 0;
}
if (scrollOffset > 2400)
{
scrollOffset = 2400;
}
}
/**
* @brief
* @param hWnd
* @param baseStep 稿
* @return
*/
int GetScrollStep(HWND hWnd, int baseStep)
{
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
return MulDiv(baseStep, metrics.scale, 1000);
}
/**
* @brief
* @param hWnd
* @return
*/
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);
int scaleY = MulDiv(clientHeight, 1000, WINDOW_CLIENT_HEIGHT);
int scale = (scaleX < scaleY) ? scaleX : scaleY;
if (scale < 500)
{
scale = 500;
}
LayoutMetrics metrics = {};
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);
return metrics;
}
/**
* @brief
* @param metrics
* @param value 稿
* @return
*/
int ScaleValue(const LayoutMetrics& metrics, int value)
{
return MulDiv(value, metrics.scale, 1000);
}
/**
* @brief
* @param metrics
* @param value 稿
* @return
*/
int ScaleXValue(const LayoutMetrics& metrics, int value)
{
return metrics.offsetX + MulDiv(value, metrics.scale, 1000);
}
/**
* @brief
* @param metrics
* @param value 稿
* @return
*/
int ScaleYValue(const LayoutMetrics& metrics, int value)
{
return metrics.offsetY + MulDiv(value, metrics.scale, 1000);
}
/**
* @brief
* @param hWnd
* @return
*/
static RECT GetMenuCardRect(HWND hWnd)
{
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
RECT rect =
{
ScaleXValue(metrics, 110),
ScaleYValue(metrics, 70),
ScaleXValue(metrics, WINDOW_CLIENT_WIDTH - 110),
ScaleYValue(metrics, WINDOW_CLIENT_HEIGHT - 70)
};
return rect;
}
/**
* @brief
* @param hWnd
* @param index
* @return
*/
RECT GetMenuOptionRect(HWND hWnd, int index)
{
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
RECT menuCard = GetMenuCardRect(hWnd);
int top = menuCard.top + ScaleValue(metrics, 140) + index * ScaleValue(metrics, 130);
RECT rect =
{
menuCard.left + ScaleValue(metrics, 36),
top,
menuCard.right - ScaleValue(metrics, 36),
top + ScaleValue(metrics, 104)
};
return rect;
}
/**
* @brief
* @param hWnd
* @return
*/
static RECT GetRulesCardRect(HWND hWnd)
{
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
RECT rect =
{
ScaleXValue(metrics, 76),
ScaleYValue(metrics, 54),
ScaleXValue(metrics, WINDOW_CLIENT_WIDTH - 76),
ScaleYValue(metrics, WINDOW_CLIENT_HEIGHT - 54)
};
return rect;
}
/**
* @brief
* @param hWnd
* @param index
* @return
*/
RECT GetHelpOptionRect(HWND hWnd, int index)
{
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
RECT rulesCard = GetRulesCardRect(hWnd);
RECT contentRect =
{
rulesCard.left + ScaleValue(metrics, 36),
rulesCard.top + ScaleValue(metrics, 126),
rulesCard.right - ScaleValue(metrics, 36),
rulesCard.bottom - ScaleValue(metrics, 86)
};
int optionHeight = ScaleValue(metrics, 100);
int optionGap = ScaleValue(metrics, 22);
int optionTop = contentRect.top + ScaleValue(metrics, 18);
RECT rect =
{
contentRect.left,
optionTop + index * (optionHeight + optionGap),
contentRect.right,
optionTop + index * (optionHeight + optionGap) + optionHeight
};
return rect;
}
/**
* @brief
* @param hWnd
* @param index
* @return
*/
RECT GetHelpSkillDemoItemRect(HWND hWnd, int index)
{
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
RECT rulesCard = GetRulesCardRect(hWnd);
RECT contentRect =
{
rulesCard.left + ScaleValue(metrics, 36),
rulesCard.top + ScaleValue(metrics, 126),
rulesCard.right - ScaleValue(metrics, 36),
rulesCard.bottom - ScaleValue(metrics, 86)
};
int itemHeight = ScaleValue(metrics, 58);
int itemGap = ScaleValue(metrics, 10);
int itemTop = contentRect.top + ScaleValue(metrics, 8) - helpScrollOffset;
RECT rect =
{
contentRect.left,
itemTop + index * (itemHeight + itemGap),
contentRect.right,
itemTop + index * (itemHeight + itemGap) + itemHeight
};
return rect;
}
/**
* @brief
* @param hWnd
* @return
*/
RECT GetHelpBackHintRect(HWND hWnd)
{
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
RECT rulesCard = GetRulesCardRect(hWnd);
RECT rect =
{
rulesCard.left + ScaleValue(metrics, 36),
rulesCard.bottom - ScaleValue(metrics, 58),
rulesCard.right - ScaleValue(metrics, 36),
rulesCard.bottom - ScaleValue(metrics, 24)
};
return rect;
}
/**
* @brief
* @param hWnd
* @param direction 0 0
* @return
*/
RECT GetCreditArrowRect(HWND hWnd, int direction)
{
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
RECT rulesCard = GetRulesCardRect(hWnd);
int size = ScaleValue(metrics, 54);
int centerY = (rulesCard.top + rulesCard.bottom) / 2;
int left = direction < 0
? rulesCard.left + ScaleValue(metrics, 52)
: rulesCard.right - ScaleValue(metrics, 52) - size;
RECT rect =
{
left,
centerY - size / 2,
left + size,
centerY + size / 2
};
return rect;
}
/**
* @brief
* @param hWnd
* @return
*/
static RECT GetUpgradeOverlayRect(HWND hWnd)
{
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
RECT rect =
{
ScaleXValue(metrics, 60),
ScaleYValue(metrics, 80),
ScaleXValue(metrics, WINDOW_CLIENT_WIDTH - 60),
ScaleYValue(metrics, WINDOW_CLIENT_HEIGHT - 80)
};
return rect;
}
/**
* @brief
* @param hWnd
* @param index
* @return
*/
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);
int columnCount = upgradeUiState.optionCount <= 3 ? upgradeUiState.optionCount : 3;
if (columnCount < 1)
{
columnCount = 1;
}
int rowCount = (upgradeUiState.optionCount + columnCount - 1) / columnCount;
if (rowCount < 1)
{
rowCount = 1;
}
int cardWidth = (overlayRect.right - overlayRect.left - horizontalPadding * 2 - gap * (columnCount - 1)) / columnCount;
int availableHeight = overlayRect.bottom - verticalTop - ScaleValue(metrics, 72) - (rowCount - 1) * gap;
int cardHeight = availableHeight / rowCount;
int column = index % columnCount;
int row = index / columnCount;
int left = overlayRect.left + horizontalPadding + column * (cardWidth + gap);
int top = verticalTop + row * (cardHeight + gap);
RECT rect = { left, top, left + cardWidth, top + cardHeight };
return rect;
}
/**
* @brief
* @param hWnd
* @return
*/
static RECT GetGameOverlayRect(HWND hWnd)
{
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
int panelGap = ScaleValue(metrics, SIDE_PANEL_GAP);
int panelWidth = ScaleValue(metrics, SIDE_PANEL_WIDTH);
int boardLeft = ScaleXValue(metrics, WINDOW_PADDING) + panelWidth + panelGap;
int boardTop = ScaleYValue(metrics, WINDOW_PADDING);
int boardWidth = nGameWidth * metrics.grid;
RECT rect =
{
boardLeft + ScaleValue(metrics, 28),
boardTop + metrics.grid * 6 + ScaleValue(metrics, 10),
boardLeft + boardWidth - ScaleValue(metrics, 28),
boardTop + metrics.grid * 10 + ScaleValue(metrics, 30)
};
return rect;
}
/**
* @brief
* @param hWnd
* @param index
* @param buttonCount
* @return
*/
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;
int height = ScaleValue(metrics, 44);
int left = overlayRect.left + sidePadding + index * (width + gap);
int top = overlayRect.top + ScaleValue(metrics, 94);
RECT rect = { left, top, left + width, top + height };
return rect;
}
/**
* @brief
* @param hWnd
* @return
*/
RECT GetBackButtonRect(HWND hWnd)
{
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
RECT rect =
{
ScaleXValue(metrics, 6),
ScaleYValue(metrics, 6),
ScaleXValue(metrics, 34),
ScaleYValue(metrics, 34)
};
return rect;
}
/**
* @brief
* @param hWnd
* @return
*/
RECT GetMusicButtonRect(HWND hWnd)
{
LayoutMetrics metrics = GetLayoutMetrics(hWnd);
int size = ScaleValue(metrics, 28);
if (size < 22)
{
size = 22;
}
int marginRight = ScaleValue(metrics, 12);
if (marginRight < 6)
{
marginRight = 6;
}
int marginBottom = ScaleValue(metrics, 12);
if (marginBottom < 6)
{
marginBottom = 6;
}
RECT buttonRect =
{
metrics.offsetX + metrics.layoutWidth - marginRight - size,
metrics.offsetY + metrics.layoutHeight - marginBottom - size,
metrics.offsetX + metrics.layoutWidth - marginRight,
metrics.offsetY + metrics.layoutHeight - marginBottom
};
return buttonRect;
}
/**
* @brief
* @param rect
* @param x
* @param y
* @return true false
*/
bool IsPointInRect(const RECT& rect, int x, int y)
{
return x >= rect.left && x < rect.right && y >= rect.top && y < rect.bottom;
}
+242
View File
@@ -0,0 +1,242 @@
#include "stdafx.h"
/**
* @file TetrisMedia.cpp
* @brief
*/
#include "Tetris.h"
#include "TetrisAppInternal.h"
#include "TetrisAssets.h"
#include <shellapi.h>
#include <string>
static bool bgmPlaying = false;
static bool bgmUsingMci = false;
static constexpr const wchar_t* kBgmAlias = L"TereisBgm";
static constexpr const wchar_t* kReviveVideoAlias = L"TereisReviveVideo";
/**
* @brief MCI
* @param path
* @param forceMpegVideo mpegvideo
* @return true false
*/
static bool TryPlayMciLoop(const std::wstring& path, bool forceMpegVideo)
{
if (!FileExists(path))
{
return false;
}
mciSendStringW((std::wstring(L"close ") + kBgmAlias).c_str(), nullptr, 0, nullptr);
// MCI 对部分 OGG/视频容器识别不稳定,调用方会按不同类型尝试。
std::wstring openCommand = L"open \"" + path + L"\" ";
if (forceMpegVideo)
{
openCommand += L"type mpegvideo ";
}
openCommand += L"alias ";
openCommand += kBgmAlias;
if (mciSendStringW(openCommand.c_str(), nullptr, 0, nullptr) != 0)
{
return false;
}
std::wstring playCommand = std::wstring(L"play ") + kBgmAlias + L" repeat";
if (mciSendStringW(playCommand.c_str(), nullptr, 0, nullptr) != 0)
{
mciSendStringW((std::wstring(L"close ") + kBgmAlias).c_str(), nullptr, 0, nullptr);
return false;
}
bgmPlaying = true;
bgmUsingMci = true;
return true;
}
/**
* @brief 使
*/
void StopBackgroundMusic()
{
if (bgmUsingMci)
{
mciSendStringW((std::wstring(L"stop ") + kBgmAlias).c_str(), nullptr, 0, nullptr);
mciSendStringW((std::wstring(L"close ") + kBgmAlias).c_str(), nullptr, 0, nullptr);
}
else
{
PlaySoundW(nullptr, nullptr, 0);
}
bgmPlaying = false;
bgmUsingMci = false;
}
/**
* @brief
*/
void StartBackgroundMusic()
{
if (!bgmEnabled || bgmPlaying)
{
return;
}
const wchar_t* bgmWavRelativePath = L"assets\\audio\\bgm.wav";
const std::wstring bgmWavCandidates[] =
{
BuildAssetPath(bgmWavRelativePath),
BuildWorkingDirAssetPath(bgmWavRelativePath)
};
for (const std::wstring& candidate : bgmWavCandidates)
{
if (FileExists(candidate) &&
PlaySoundW(candidate.c_str(), nullptr, SND_FILENAME | SND_ASYNC | SND_LOOP))
{
bgmPlaying = true;
bgmUsingMci = false;
return;
}
}
const wchar_t* oggRelativePath = L"assets\\audio\\bgm.ogg";
const std::wstring oggCandidates[] =
{
BuildAssetPath(oggRelativePath),
BuildWorkingDirAssetPath(oggRelativePath)
};
for (const std::wstring& candidate : oggCandidates)
{
if (TryPlayMciLoop(candidate, false) || TryPlayMciLoop(candidate, true))
{
return;
}
}
const wchar_t* fallbackWavRelativePath = L"assets\\audio\\background.wav";
const std::wstring fallbackWavCandidates[] =
{
BuildAssetPath(fallbackWavRelativePath),
BuildWorkingDirAssetPath(fallbackWavRelativePath)
};
for (const std::wstring& candidate : fallbackWavCandidates)
{
if (FileExists(candidate) &&
PlaySoundW(candidate.c_str(), nullptr, SND_FILENAME | SND_ASYNC | SND_LOOP))
{
bgmPlaying = true;
bgmUsingMci = false;
return;
}
}
bgmEnabled = false;
}
/**
* @brief
* @param hWnd
*/
void ToggleBackgroundMusic(HWND hWnd)
{
bgmEnabled = !bgmEnabled;
if (bgmEnabled)
{
StartBackgroundMusic();
}
else
{
StopBackgroundMusic();
}
InvalidateRect(hWnd, nullptr, FALSE);
}
/**
* @brief MCI退
* @param hWnd MCI ShellExecute
* @return true false
*/
bool PlayReviveVideo(HWND hWnd)
{
std::wstring videoPath = BuildAssetPath(L"assets\\video\\video.avi");
if (!FileExists(videoPath))
{
videoPath = BuildWorkingDirAssetPath(L"assets\\video\\video.avi");
}
if (!FileExists(videoPath))
{
videoPath = BuildAssetPath(L"assets\\video\\video.mp4");
}
if (!FileExists(videoPath))
{
videoPath = BuildWorkingDirAssetPath(L"assets\\video\\video.mp4");
}
if (!FileExists(videoPath))
{
return false;
}
bool shouldResumeBgm = bgmEnabled;
if (bgmPlaying)
{
StopBackgroundMusic();
}
// 先用 MCI 全屏同步播放;失败时再交给系统默认播放器。
bool played = false;
for (int attempt = 0; attempt < 2 && !played; attempt++)
{
bool forceMpegVideo = attempt == 0;
mciSendStringW((std::wstring(L"close ") + kReviveVideoAlias).c_str(), nullptr, 0, nullptr);
std::wstring openCommand = L"open \"" + videoPath + L"\" ";
if (forceMpegVideo)
{
openCommand += L"type mpegvideo ";
}
openCommand += L"alias ";
openCommand += kReviveVideoAlias;
if (mciSendStringW(openCommand.c_str(), nullptr, 0, hWnd) == 0)
{
std::wstring playCommand = std::wstring(L"play ") + kReviveVideoAlias + L" fullscreen wait";
MCIERROR playResult = mciSendStringW(playCommand.c_str(), nullptr, 0, hWnd);
mciSendStringW((std::wstring(L"close ") + kReviveVideoAlias).c_str(), nullptr, 0, nullptr);
played = playResult == 0;
}
}
if (!played)
{
SHELLEXECUTEINFOW shellInfo = {};
shellInfo.cbSize = sizeof(shellInfo);
shellInfo.fMask = SEE_MASK_NOCLOSEPROCESS;
shellInfo.hwnd = hWnd;
shellInfo.lpVerb = L"open";
shellInfo.lpFile = videoPath.c_str();
shellInfo.nShow = SW_SHOWNORMAL;
if (ShellExecuteExW(&shellInfo))
{
if (shellInfo.hProcess != nullptr)
{
WaitForSingleObject(shellInfo.hProcess, INFINITE);
CloseHandle(shellInfo.hProcess);
}
played = true;
}
}
if (shouldResumeBgm)
{
StartBackgroundMusic();
}
return played;
}
+324
View File
@@ -0,0 +1,324 @@
#include "stdafx.h"
/**
* @file TetrisTimers.cpp
* @brief Rogue
*/
#include "TetrisAppInternal.h"
static MMRESULT creditTimerHandle = 0;
/**
* @brief
* @param userData
*/
static void CALLBACK CreditTimerCallback(UINT, UINT, DWORD_PTR userData, DWORD_PTR, DWORD_PTR)
{
HWND hWnd = reinterpret_cast<HWND>(userData);
if (hWnd != nullptr)
{
PostMessage(hWnd, WM_CREDIT_TICK, 0, 0);
}
}
/**
* @brief
* @param hWnd
*/
void ResetGameTimer(HWND hWnd)
{
KillTimer(hWnd, GAME_TIMER_ID);
SetTimer(hWnd, GAME_TIMER_ID, currentFallInterval > 0 ? currentFallInterval : GAME_TIMER_INTERVAL, nullptr);
}
/**
* @brief
* @param hWnd
*/
void StartAppTimers(HWND hWnd)
{
ResetGameTimer(hWnd);
SetTimer(hWnd, EFFECT_TIMER_ID, EFFECT_TIMER_INTERVAL, nullptr);
creditTimerHandle = timeSetEvent(
CREDIT_TIMER_INTERVAL,
1,
CreditTimerCallback,
reinterpret_cast<DWORD_PTR>(hWnd),
TIME_PERIODIC | TIME_CALLBACK_FUNCTION);
if (creditTimerHandle == 0)
{
// 多媒体定时器不可用时退回普通窗口定时器,保证致谢页仍可动画。
SetTimer(hWnd, CREDIT_TIMER_ID, CREDIT_TIMER_INTERVAL, nullptr);
}
}
/**
* @brief
* @param hWnd
*/
void StopAppTimers(HWND hWnd)
{
KillTimer(hWnd, GAME_TIMER_ID);
KillTimer(hWnd, EFFECT_TIMER_ID);
if (creditTimerHandle != 0)
{
timeKillEvent(creditTimerHandle);
creditTimerHandle = 0;
}
else
{
KillTimer(hWnd, CREDIT_TIMER_ID);
}
}
/**
* @brief
* @param hWnd
*/
void HandleCreditTick(HWND hWnd)
{
if (currentScreen == SCREEN_RULES && helpState.currentPage == 4 && TickCreditAnimation())
{
InvalidateRect(hWnd, nullptr, FALSE);
}
}
/**
* @brief Rogue
* @param hWnd
* @return true
*/
static bool TickRogueTimedStates(HWND hWnd)
{
bool shouldRefresh = false;
if (currentMode == MODE_ROGUE && !IsRogueSkillDemoMode() && rogueStats.feverTicks > 0)
{
rogueStats.feverTicks--;
currentFallInterval = GetRogueFallInterval();
ResetGameTimer(hWnd);
shouldRefresh = true;
}
if (currentMode == MODE_ROGUE &&
!IsRogueSkillDemoMode() &&
rogueStats.timeDilationTicks > 0 &&
currentScreen == SCREEN_PLAYING &&
!suspendFlag &&
!gameOverFlag)
{
rogueStats.timeDilationTicks--;
currentFallInterval = GetRogueFallInterval();
ResetGameTimer(hWnd);
shouldRefresh = true;
}
if (currentMode == MODE_ROGUE && !IsRogueSkillDemoMode() && rogueStats.extremeSlowTicks > 0)
{
rogueStats.extremeSlowTicks--;
currentFallInterval = GetRogueFallInterval();
ResetGameTimer(hWnd);
shouldRefresh = true;
}
if (currentMode == MODE_ROGUE && !IsRogueSkillDemoMode() && rogueStats.holdSlowTicks > 0)
{
rogueStats.holdSlowTicks--;
currentFallInterval = GetRogueFallInterval();
ResetGameTimer(hWnd);
shouldRefresh = true;
}
return shouldRefresh;
}
/**
* @brief
* @param hWnd
* @return true
*/
static bool TickExtremeDanger(HWND hWnd)
{
if (currentMode != MODE_ROGUE ||
IsRogueSkillDemoMode() ||
rogueStats.extremePlayerLevel <= 0 ||
currentScreen != SCREEN_PLAYING ||
suspendFlag ||
gameOverFlag)
{
return false;
}
if (rogueStats.extremeDangerTicks > 0)
{
rogueStats.extremeDangerTicks--;
return false;
}
rogueStats.extremeDangerTicks = 30;
if (rogueStats.extremeDangerLevel < 5)
{
rogueStats.extremeDangerLevel++;
}
currentFallInterval = GetRogueFallInterval();
ResetGameTimer(hWnd);
SetFeedbackMessage(
_T("极限压力升高"),
_T("30 秒内没有完成四消,危险等级提升,下落速度进一步加快。"),
10);
return true;
}
/**
* @brief
* @param hWnd
* @return true
*/
static bool TryStartTimeDilation(HWND hWnd)
{
if (currentMode != MODE_ROGUE ||
IsRogueSkillDemoMode() ||
rogueStats.timeDilationLevel <= 0 ||
rogueStats.timeDilationTicks > 0)
{
return false;
}
int occupiedHeight = 0;
int playableHeight = GetRoguePlayableHeight();
for (int y = 0; y < playableHeight; y++)
{
bool hasCell = false;
for (int x = 0; x < nGameWidth; x++)
{
if (workRegion[y][x] != 0)
{
hasCell = true;
break;
}
}
if (hasCell)
{
occupiedHeight = playableHeight - y;
break;
}
}
if (occupiedHeight <= 15)
{
return false;
}
rogueStats.timeDilationTicks = 8;
currentFallInterval = GetRogueFallInterval();
ResetGameTimer(hWnd);
SetFeedbackMessage(
_T("时间缓流"),
_T("堆叠高度超过 15 行,接下来 8 秒下落速度降低 30%。"),
10);
return true;
}
/**
* @brief
* @param hWnd
* @return true
*/
static bool TickGameFall(HWND hWnd)
{
if (currentScreen != SCREEN_PLAYING || suspendFlag || gameOverFlag)
{
return false;
}
if (currentMode == MODE_ROGUE && !IsRogueSkillDemoMode())
{
int previousFallInterval = currentFallInterval;
AdvanceRogueDifficulty(currentFallInterval > 0 ? currentFallInterval : GAME_TIMER_INTERVAL);
if (currentFallInterval != previousFallInterval)
{
ResetGameTimer(hWnd);
}
}
TryStartTimeDilation(hWnd);
if (CanMoveDown())
{
MoveDown();
}
else
{
Fixing();
if (!gameOverFlag)
{
DeleteLines();
CheckRogueLevelProgress();
}
}
if (!gameOverFlag)
{
ComputeTarget();
}
return true;
}
/**
* @brief
* @param hWnd
* @param timerId
*/
void HandleTimerMessage(HWND hWnd, WPARAM timerId)
{
if (timerId == EFFECT_TIMER_ID)
{
if (TickVisualEffects())
{
InvalidateRect(hWnd, nullptr, FALSE);
}
return;
}
if (timerId == CREDIT_TIMER_ID && creditTimerHandle == 0)
{
HandleCreditTick(hWnd);
return;
}
if (timerId != GAME_TIMER_ID)
{
return;
}
bool shouldRefresh = false;
if (feedbackState.visibleTicks > 0)
{
feedbackState.visibleTicks--;
shouldRefresh = true;
}
if (IsRogueSkillDemoMode() && TickRogueSkillDemo())
{
shouldRefresh = true;
}
if (TickRogueTimedStates(hWnd))
{
shouldRefresh = true;
}
if (TickExtremeDanger(hWnd))
{
shouldRefresh = true;
}
if (TickGameFall(hWnd))
{
shouldRefresh = true;
}
if (shouldRefresh)
{
InvalidateRect(hWnd, nullptr, FALSE);
}
}
+79
View File
@@ -0,0 +1,79 @@
#include "stdafx.h"
/**
* @file TetrisAssets.cpp
* @brief
*/
#include "TetrisAssets.h"
/**
* @brief
*
*
* assets
*
* @param relativePath
* @return
*/
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)
{
basePath.resize(lastSlash);
}
// 可执行文件位于构建目录,向上两级回到项目根目录。
std::wstring projectRelative = basePath + L"\\..\\..\\" + relativePath;
wchar_t fullPath[MAX_PATH] = {};
DWORD result = GetFullPathNameW(projectRelative.c_str(), MAX_PATH, fullPath, nullptr);
if (result > 0 && result < MAX_PATH)
{
return fullPath;
}
return projectRelative;
}
/**
* @brief
*
* IDE
*
* @param relativePath
* @return
*/
std::wstring BuildWorkingDirAssetPath(const wchar_t* relativePath)
{
wchar_t currentDirectory[MAX_PATH] = {};
DWORD length = GetCurrentDirectoryW(MAX_PATH, currentDirectory);
if (length == 0 || length >= MAX_PATH)
{
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);
if (result > 0 && result < MAX_PATH)
{
return fullPath;
}
return candidate;
}
/**
* @brief
* @param path
* @return true false
*/
bool FileExists(const std::wstring& path)
{
DWORD attributes = GetFileAttributesW(path.c_str());
return attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_DIRECTORY) == 0;
}
@@ -1,4 +1,9 @@
#include "stdafx.h" #include "stdafx.h"
/**
* @file TetrisGameExtensions.cpp
* @brief /
*/
#include "TetrisLogicInternal.h" #include "TetrisLogicInternal.h"
int pendingLineClearEffectTicks = 0; int pendingLineClearEffectTicks = 0;
@@ -8,6 +13,8 @@ int pendingLineClearEffectLineCount = 0;
/** /**
* @brief Rogue 使 * @brief Rogue 使
* @param stats
* @param useRogueRules Rogue
*/ */
void ResetPlayerStats(PlayerStats& stats, bool useRogueRules) void ResetPlayerStats(PlayerStats& stats, bool useRogueRules)
{ {
@@ -85,6 +92,9 @@ void ResetPlayerStats(PlayerStats& stats, bool useRogueRules)
/** /**
* @brief * @brief
* @param title
* @param detail
* @param ticks
*/ */
void SetFeedbackMessage(const TCHAR* title, const TCHAR* detail, int ticks) void SetFeedbackMessage(const TCHAR* title, const TCHAR* detail, int ticks)
{ {
@@ -125,6 +135,7 @@ void ResetVisualEffects()
/** /**
* @brief * @brief
* @return true false
*/ */
bool TickVisualEffects() bool TickVisualEffects()
{ {
@@ -177,6 +188,7 @@ bool TickVisualEffects()
/** /**
* @brief * @brief
* @return true false
*/ */
bool TickCreditAnimation() bool TickCreditAnimation()
{ {
@@ -191,6 +203,10 @@ bool TickCreditAnimation()
/** /**
* @brief * @brief
* @param boardX 使 100
* @param boardY 使 100
* @param text
* @param color
*/ */
static void AddFloatingText(int boardX, int boardY, const TCHAR* text, COLORREF color) static void AddFloatingText(int boardX, int boardY, const TCHAR* text, COLORREF color)
{ {
@@ -211,6 +227,12 @@ static void AddFloatingText(int boardX, int boardY, const TCHAR* text, COLORREF
/** /**
* @brief * @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) static void AddParticle(int boardX, int boardY, int velocityX, int velocityY, int size, COLORREF color)
{ {
@@ -233,6 +255,10 @@ static void AddParticle(int boardX, int boardY, int velocityX, int velocityY, in
/** /**
* @brief * @brief
* @param boardX
* @param boardY
* @param baseColor
* @param strongBurst 使
*/ */
static void AddBurstParticles(int boardX, int boardY, COLORREF baseColor, bool strongBurst) static void AddBurstParticles(int boardX, int boardY, COLORREF baseColor, bool strongBurst)
{ {
@@ -296,6 +322,10 @@ static void AddBurstParticles(int boardX, int boardY, COLORREF baseColor, bool s
/** /**
* @brief * @brief
* @param x
* @param y
* @param color
* @param strongFlash 使
*/ */
static void AddCellFlash(int x, int y, COLORREF color, bool strongFlash) static void AddCellFlash(int x, int y, COLORREF color, bool strongFlash)
{ {
@@ -315,6 +345,9 @@ static void AddCellFlash(int x, int y, COLORREF color, bool strongFlash)
/** /**
* @brief * @brief
* @param rows
* @param rowCount
* @param linesCleared
*/ */
void QueueLineClearEffect(const int* rows, int rowCount, int linesCleared) void QueueLineClearEffect(const int* rows, int rowCount, int linesCleared)
{ {
@@ -358,6 +391,9 @@ void PlayPendingLineClearEffect()
/** /**
* @brief * @brief
* @param rows
* @param rowCount
* @param linesCleared
*/ */
void TriggerLineClearEffect(const int* rows, int rowCount, int linesCleared) void TriggerLineClearEffect(const int* rows, int rowCount, int linesCleared)
{ {
@@ -414,6 +450,9 @@ void TriggerLineClearEffect(const int* rows, int rowCount, int linesCleared)
/** /**
* @brief * @brief
* @param cells
* @param cellCount
* @param strongBurst 使
*/ */
void TriggerCellClearEffect(const Point* cells, int cellCount, bool strongBurst) void TriggerCellClearEffect(const Point* cells, int cellCount, bool strongBurst)
{ {
@@ -422,6 +461,10 @@ void TriggerCellClearEffect(const Point* cells, int cellCount, bool strongBurst)
/** /**
* @brief * @brief
* @param cells
* @param cellCount
* @param flashColor
* @param strongBurst 使
*/ */
void TriggerColoredCellClearEffect(const Point* cells, int cellCount, COLORREF flashColor, bool strongBurst) void TriggerColoredCellClearEffect(const Point* cells, int cellCount, COLORREF flashColor, bool strongBurst)
{ {
@@ -445,6 +488,10 @@ void TriggerColoredCellClearEffect(const Point* cells, int cellCount, COLORREF f
/** /**
* @brief * @brief
* @param x
* @param fromY
* @param toY
* @param cellValue
*/ */
void TriggerGravityFallEffect(int x, int fromY, int toY, int cellValue) void TriggerGravityFallEffect(int x, int fromY, int toY, int cellValue)
{ {
@@ -490,6 +537,10 @@ void TriggerGravityFallEffect(int x, int fromY, int toY, int cellValue)
/** /**
* @brief * @brief
* @param pieceType
* @param pieceState
* @param position
* @return true false
*/ */
bool IsPiecePlacementValid(int pieceType, int pieceState, Point position) bool IsPiecePlacementValid(int pieceType, int pieceState, Point position)
{ {
@@ -522,6 +573,9 @@ bool IsPiecePlacementValid(int pieceType, int pieceState, Point position)
/** /**
* @brief * @brief
* @param nextState
* @param offsetX
* @return true false
*/ */
bool TryRotateWithOffset(int nextState, int offsetX) bool TryRotateWithOffset(int nextState, int offsetX)
{ {
@@ -574,6 +628,7 @@ void ReviveAfterVideo()
/** /**
* @brief * @brief
* @param mode GameMode
*/ */
void StartGameWithMode(int mode) void StartGameWithMode(int mode)
{ {
@@ -632,7 +687,7 @@ void OpenRulesScreen()
} }
/** /**
* @brief * @brief Rogue
*/ */
void OpenSkillDemoScreen() void OpenSkillDemoScreen()
{ {
@@ -648,6 +703,9 @@ void OpenSkillDemoScreen()
creditAnimationDirection = 0; creditAnimationDirection = 0;
} }
/**
* @brief
*/
void OpenCreditScreen() void OpenCreditScreen()
{ {
rogueDemoMode = false; rogueDemoMode = false;
@@ -664,6 +722,7 @@ void OpenCreditScreen()
/** /**
* @brief * @brief
* @param direction 0 0
*/ */
void ChangeCreditPage(int direction) void ChangeCreditPage(int direction)
{ {
+255
View File
@@ -0,0 +1,255 @@
#include "stdafx.h"
/**
* @file TetrisPieceEffects.cpp
* @brief
*/
#include "TetrisLogicInternal.h"
/**
* @brief
* @param overflowTop
* @param fixedCells
* @param fixedCellCount
*/
void ApplyRainbowLandingEffect(bool overflowTop, const Point* fixedCells, int fixedCellCount)
{
if (overflowTop || !currentPieceIsRainbow)
{
return;
}
// 优先使用实际固定格子的平均行作为主色行,避免旋转形状偏移导致判定不自然。
int rainbowAnchorRow = point.y + 1;
if (fixedCellCount > 0)
{
int ySum = 0;
for (int i = 0; i < fixedCellCount; i++)
{
ySum += fixedCells[i].y;
}
rainbowAnchorRow = (ySum + fixedCellCount / 2) / fixedCellCount;
}
if (rainbowAnchorRow < 0)
{
rainbowAnchorRow = 0;
}
if (rainbowAnchorRow >= GetRoguePlayableHeight())
{
rainbowAnchorRow = GetRoguePlayableHeight() - 1;
}
int rainbowRecoloredCount = 0;
int rainbowClearedCount = TriggerRainbowColorShift(rainbowAnchorRow, point.y, point.y + 3, rainbowRecoloredCount);
int rainbowScore = 0;
int rainbowExp = 0;
int voidClearedCount = 0;
int voidScore = 0;
int voidExp = 0;
if (currentMode == MODE_ROGUE && rainbowClearedCount > 0)
{
AwardRogueSkillClearRewards(rainbowClearedCount, rainbowScore, rainbowExp, false);
if (rogueStats.voidCoreLevel > 0)
{
voidClearedCount = TriggerMiniBlackHole(5);
AwardRogueSkillClearRewards(voidClearedCount, voidScore, voidExp, false);
}
}
TCHAR rainbowDetail[128];
if (voidClearedCount > 0)
{
_stprintf_s(
rainbowDetail,
_T("第 %d 行清 %d 格,染色 %d 格,虚空追加 %d 格"),
rainbowAnchorRow + 1,
rainbowClearedCount,
rainbowRecoloredCount,
voidClearedCount);
}
else
{
_stprintf_s(
rainbowDetail,
_T("第 %d 行清除主色 %d 格,覆盖行染色 %d 格。"),
rainbowAnchorRow + 1,
rainbowClearedCount,
rainbowRecoloredCount);
}
SetFeedbackMessage(_T("彩虹方块"), rainbowDetail, 10);
}
/**
* @brief
* @param explosiveCells
* @param explosiveCellCount
*/
static void ApplyExplosiveLandingEffect(const Point* explosiveCells, int explosiveCellCount)
{
if (!currentPieceIsExplosive)
{
return;
}
int explosiveCellsCleared = 0;
for (int i = 0; i < explosiveCellCount; i++)
{
explosiveCellsCleared += ClearExplosiveAreaAt(explosiveCells[i].y, explosiveCells[i].x);
}
int explosiveScoreGain = 0;
int explosiveExpGain = 0;
if (currentMode == MODE_ROGUE && explosiveCellsCleared > 0)
{
AwardRogueSkillClearRewards(explosiveCellsCleared, explosiveScoreGain, explosiveExpGain, false);
}
TCHAR explosiveDetail[128];
_stprintf_s(
explosiveDetail,
_T("爆破清除 %d 格 +%d 分 +%d EXP"),
explosiveCellsCleared,
explosiveScoreGain,
explosiveExpGain);
SetFeedbackMessage(_T("爆破核心"), explosiveDetail, 12);
// 连环炸弹需要等标准消行判断完成后,再决定是否追加一次小爆炸。
if (rogueStats.chainBombLevel > 0 && explosiveCellCount > 0)
{
pendingChainBombCenter = explosiveCells[0];
pendingChainBombFollowup = true;
}
}
/**
* @brief
* @param fixedCells
* @param fixedCellCount
*/
static void ApplyLaserLandingEffect(const Point* fixedCells, int fixedCellCount)
{
if (!currentPieceIsLaser)
{
return;
}
int laserColumn = point.x + 1;
if (fixedCellCount > 0)
{
int xSum = 0;
for (int i = 0; i < fixedCellCount; i++)
{
xSum += fixedCells[i].x;
}
laserColumn = (xSum + fixedCellCount / 2) / fixedCellCount;
}
if (laserColumn < 0)
{
laserColumn = 0;
}
if (laserColumn >= nGameWidth)
{
laserColumn = nGameWidth - 1;
}
int laserCellsCleared = ClearColumnAt(laserColumn);
if (currentMode == MODE_ROGUE && laserCellsCleared > 0)
{
int laserScore = 0;
int laserExp = 0;
AwardRogueSkillClearRewards(laserCellsCleared, laserScore, laserExp, false);
TCHAR laserDetail[128];
_stprintf_s(laserDetail, _T("激光贯穿第 %d 列,清除 %d 格 +%d 分 +%d EXP"), laserColumn + 1, laserCellsCleared, laserScore, laserExp);
SetFeedbackMessage(_T("棱镜激光"), laserDetail, 12);
}
}
/**
* @brief
* @param fixedCells
* @param fixedCellCount
*/
static void ApplyCrossLandingEffect(const Point* fixedCells, int fixedCellCount)
{
if (!currentPieceIsCross)
{
return;
}
int crossRow = point.y + 1;
int crossColumn = point.x + 1;
if (fixedCellCount > 0)
{
int xSum = 0;
int ySum = 0;
for (int i = 0; i < fixedCellCount; i++)
{
xSum += fixedCells[i].x;
ySum += fixedCells[i].y;
}
crossColumn = (xSum + fixedCellCount / 2) / fixedCellCount;
crossRow = (ySum + fixedCellCount / 2) / fixedCellCount;
}
if (crossRow < 0)
{
crossRow = 0;
}
if (crossRow >= GetRoguePlayableHeight())
{
crossRow = GetRoguePlayableHeight() - 1;
}
if (crossColumn < 0)
{
crossColumn = 0;
}
if (crossColumn >= nGameWidth)
{
crossColumn = nGameWidth - 1;
}
int crossCellsCleared = ClearRowAt(crossRow);
int columnCellsCleared = ClearColumnAtWithColor(crossColumn, RGB(196, 255, 132));
if (workRegion[crossRow][crossColumn] == 0 && columnCellsCleared > 0)
{
// 中心格可能已经在行清除时被计数,这里保持原有结算方式。
}
int totalCrossCleared = crossCellsCleared + columnCellsCleared;
if (currentMode == MODE_ROGUE && totalCrossCleared > 0)
{
int crossScore = 0;
int crossExp = 0;
AwardRogueSkillClearRewards(totalCrossCleared, crossScore, crossExp, false);
TCHAR crossDetail[128];
_stprintf_s(crossDetail, _T("十字冲击第 %d 行 / 第 %d 列,清除 %d 格 +%d 分 +%d EXP"), crossRow + 1, crossColumn + 1, totalCrossCleared, crossScore, crossExp);
SetFeedbackMessage(_T("十字方块"), crossDetail, 12);
}
}
/**
* @brief
*/
static void ApplyStableStructureEffect()
{
if (!currentPieceIsRainbow && TryStabilizeBoard() > 0)
{
SetFeedbackMessage(_T("稳定结构"), _T("附近空洞被自动填补,阵型更加稳固。"), 10);
}
}
/**
* @brief
* @param fixedCells
* @param fixedCellCount
* @param explosiveCells
* @param explosiveCellCount
*/
void ApplySpecialLandingEffects(const Point* fixedCells, int fixedCellCount, const Point* explosiveCells, int explosiveCellCount)
{
ApplyExplosiveLandingEffect(explosiveCells, explosiveCellCount);
ApplyLaserLandingEffect(fixedCells, fixedCellCount);
ApplyCrossLandingEffect(fixedCells, fixedCellCount);
ApplyStableStructureEffect();
}
+152
View File
@@ -0,0 +1,152 @@
#include "stdafx.h"
/**
* @file TetrisRenderAssets.cpp
* @brief GDI+
*/
#include "TetrisRenderInternal.h"
#include "TetrisAssets.h"
#include <objidl.h>
#include <string>
#pragma comment(lib, "gdiplus.lib")
using namespace Gdiplus;
/**
* @brief GDI+
* @param path
* @return nullptr
*/
static Bitmap* TryLoadBitmap(const std::wstring& path)
{
if (path.empty() || !FileExists(path))
{
return nullptr;
}
Bitmap* loadedImage = Bitmap::FromFile(path.c_str(), FALSE);
if (loadedImage != nullptr && loadedImage->GetLastStatus() == Ok)
{
return loadedImage;
}
delete loadedImage;
return nullptr;
}
/**
* @brief GDI+
* @return GDI+ true false
*/
static bool EnsureGdiplusStarted()
{
static ULONG_PTR gdiplusToken = 0;
static bool attempted = false;
static bool started = false;
if (!attempted)
{
// GDI+ 只需要初始化一次,静态标记避免重复启动。
attempted = true;
GdiplusStartupInput startupInput;
started = GdiplusStartup(&gdiplusToken, &startupInput, nullptr) == Ok;
}
return started;
}
/**
* @brief
* @return nullptr
*/
Bitmap* LoadBackgroundImage()
{
static Bitmap* backgroundImage = nullptr;
static bool attempted = false;
if (!attempted)
{
attempted = true;
if (EnsureGdiplusStarted())
{
const std::wstring candidates[] =
{
BuildAssetPath(L"assets\\images\\background.png"),
BuildWorkingDirAssetPath(L"assets\\images\\background.png"),
BuildAssetPath(L"assets\\images\\background.bmp"),
BuildWorkingDirAssetPath(L"assets\\images\\background.bmp")
};
// 同时支持构建目录运行和项目根目录运行两种启动方式。
for (const std::wstring& candidate : candidates)
{
backgroundImage = TryLoadBitmap(candidate);
if (backgroundImage != nullptr)
{
break;
}
}
}
}
return backgroundImage;
}
/**
* @brief
* @param index
* @return nullptr
*/
Bitmap* LoadCreditImage(int index)
{
constexpr int creditPageCount = 4;
static Bitmap* creditImages[creditPageCount] = {};
static bool attempted[creditPageCount] = {};
if (index < 0 || index >= creditPageCount)
{
return nullptr;
}
if (!attempted[index])
{
attempted[index] = true;
if (EnsureGdiplusStarted())
{
const wchar_t* imageNames[creditPageCount] =
{
L"assets\\images\\qls.jpg",
L"assets\\images\\wyk.jpg",
L"assets\\images\\swj.jpg",
L"assets\\images\\qhy.jpg"
};
const std::wstring creditExtraCandidates[] =
{
BuildAssetPath(imageNames[index]),
BuildWorkingDirAssetPath(imageNames[index]),
BuildAssetPath(L"assets\\images\\qhy.png"),
BuildWorkingDirAssetPath(L"assets\\images\\qhy.png"),
BuildAssetPath(L"assets\\images\\qhy.jpeg"),
BuildWorkingDirAssetPath(L"assets\\images\\qhy.jpeg"),
BuildAssetPath(L"assets\\images\\qhy.bmp"),
BuildWorkingDirAssetPath(L"assets\\images\\qhy.bmp")
};
int candidateCount = (index == 3) ? 8 : 2;
// 第四张致谢图历史上有多种扩展名,这里保留兼容查找。
for (int i = 0; i < candidateCount; i++)
{
creditImages[index] = TryLoadBitmap(creditExtraCandidates[i]);
if (creditImages[index] != nullptr)
{
break;
}
}
}
}
return creditImages[index];
}
@@ -1,4 +1,9 @@
#include "stdafx.h" #include "stdafx.h"
/**
* @file TetrisRogue.cpp
* @brief Rogue
*/
#include "TetrisLogicInternal.h" #include "TetrisLogicInternal.h"
/** /**
@@ -1513,6 +1518,8 @@ static int GetRogueExpByLines(int linesCleared)
/** /**
* @brief * @brief
* @param stats
* @return
*/ */
static int ApplyLevelProgress(PlayerStats& stats) static int ApplyLevelProgress(PlayerStats& stats)
{ {
@@ -1531,6 +1538,8 @@ static int ApplyLevelProgress(PlayerStats& stats)
/** /**
* @brief * @brief
* @param rowsToClear
* @return
*/ */
static int TriggerUpgradeShockwave(int rowsToClear) static int TriggerUpgradeShockwave(int rowsToClear)
{ {
@@ -1547,6 +1556,9 @@ static int TriggerUpgradeShockwave(int rowsToClear)
/** /**
* @brief * @brief
*
* UI
*
*/ */
static void FillUpgradeOptions() static void FillUpgradeOptions()
{ {
@@ -1554,6 +1566,7 @@ static void FillUpgradeOptions()
int selectableWeights[kUpgradePoolSize] = { 0 }; int selectableWeights[kUpgradePoolSize] = { 0 };
int selectableCount = 0; int selectableCount = 0;
// 第一段:筛出当前真正可选的强化,并记录它们的动态权重。
for (int i = 0; i < kUpgradePoolSize; i++) for (int i = 0; i < kUpgradePoolSize; i++)
{ {
if (IsUpgradeSelectable(kUpgradePool[i])) if (IsUpgradeSelectable(kUpgradePool[i]))
@@ -1564,6 +1577,7 @@ static void FillUpgradeOptions()
} }
} }
// 命运轮盘把候选扩到 6 个;双重抉择和命运轮盘都会允许本轮选 2 个。
int optionLimit = (rogueStats.destinyWheelLevel > 0) ? 6 : 3; int optionLimit = (rogueStats.destinyWheelLevel > 0) ? 6 : 3;
int optionCount = selectableCount < optionLimit ? selectableCount : optionLimit; int optionCount = selectableCount < optionLimit ? selectableCount : optionLimit;
upgradeUiState.optionCount = optionCount; upgradeUiState.optionCount = optionCount;
@@ -1575,6 +1589,7 @@ static void FillUpgradeOptions()
upgradeUiState.marked[i] = false; upgradeUiState.marked[i] = false;
} }
// 第二段:按权重不放回抽取候选,抽中后用末尾元素覆盖当前槽位。
for (int i = 0; i < optionCount; i++) for (int i = 0; i < optionCount; i++)
{ {
int totalWeight = 0; int totalWeight = 0;
@@ -1612,6 +1627,7 @@ static void FillUpgradeOptions()
upgradeUiState.options[i].category = pickedEntry.category; upgradeUiState.options[i].category = pickedEntry.category;
upgradeUiState.options[i].description = pickedEntry.description; upgradeUiState.options[i].description = pickedEntry.description;
// 方块改造目前固定展示 I 块概率提升,说明文字需要运行时拼接。
if (pickedEntry.id == UPGRADE_PIECE_TUNING) if (pickedEntry.id == UPGRADE_PIECE_TUNING)
{ {
int targetPieceType = 0; int targetPieceType = 0;
@@ -1632,6 +1648,7 @@ static void FillUpgradeOptions()
selectableCount--; selectableCount--;
} }
// 命运轮盘在候选中随机附加一个诅咒,确认时才提高下一次升级需求。
if (rogueStats.destinyWheelLevel > 0 && optionCount > 0) if (rogueStats.destinyWheelLevel > 0 && optionCount > 0)
{ {
int cursedIndex = rand() % optionCount; int cursedIndex = rand() % optionCount;
@@ -1641,6 +1658,7 @@ static void FillUpgradeOptions()
/** /**
* @brief Rogue * @brief Rogue
* @return 使
*/ */
int GetRogueFallInterval() int GetRogueFallInterval()
{ {
@@ -1692,14 +1710,24 @@ int GetRogueFallInterval()
/** /**
* @brief Rogue * @brief Rogue
*
* Rogue
*
*
* @param upgradeId
* @param targetPieceType 使 I
* @param applyCount 0
*/ */
static void ApplyUpgradeById(int upgradeId, int targetPieceType, int applyCount) static void ApplyUpgradeById(int upgradeId, int targetPieceType, int applyCount)
{ {
(void)targetPieceType;
if (applyCount <= 0) if (applyCount <= 0)
{ {
return; return;
} }
// 基础成长类强化直接叠加倍率或层数。
switch (upgradeId) switch (upgradeId)
{ {
case UPGRADE_SCORE_MULTIPLIER: case UPGRADE_SCORE_MULTIPLIER:
@@ -1738,6 +1766,7 @@ static void ApplyUpgradeById(int upgradeId, int targetPieceType, int applyCount)
break; break;
case UPGRADE_PRESSURE_RELIEF: case UPGRADE_PRESSURE_RELIEF:
{ {
// 卸压清场立即从最高占用行开始删除,直接改善当前局面。
rogueStats.pressureReliefLevel += applyCount; rogueStats.pressureReliefLevel += applyCount;
for (int i = 0; i < applyCount; i++) for (int i = 0; i < applyCount; i++)
{ {
@@ -1838,6 +1867,7 @@ static void ApplyUpgradeById(int upgradeId, int targetPieceType, int applyCount)
rogueStats.previewUpgradeLevel = rogueStats.previewCount - 1; rogueStats.previewUpgradeLevel = rogueStats.previewCount - 1;
break; break;
case UPGRADE_BLOCK_STORM: case UPGRADE_BLOCK_STORM:
// 方块风暴会同时改写预览队列,确保玩家马上看到连续 I 块。
rogueStats.blockStormLevel = 1; rogueStats.blockStormLevel = 1;
rogueStats.blockStormPiecesRemaining = 5; rogueStats.blockStormPiecesRemaining = 5;
nextTypes[0] = 0; nextTypes[0] = 0;
@@ -1886,6 +1916,10 @@ static void ApplyUpgradeById(int upgradeId, int targetPieceType, int applyCount)
/** /**
* @brief * @brief
* @param clearedCells
* @param scoreGain
* @param expGain
* @param allowLevelProgress
*/ */
void AwardRogueSkillClearRewards(int clearedCells, int& scoreGain, int& expGain, bool allowLevelProgress) void AwardRogueSkillClearRewards(int clearedCells, int& scoreGain, int& expGain, bool allowLevelProgress)
{ {
@@ -1968,6 +2002,7 @@ void CheckRogueLevelProgress()
upgradeUiState.pendingCount += levelUps; upgradeUiState.pendingCount += levelUps;
// 升级冲击波和进化冲击延后到升级菜单关闭后播放,避免菜单遮挡反馈。
int shockwaveRows = 0; int shockwaveRows = 0;
if (rogueStats.evolutionImpactLevel > 0) if (rogueStats.evolutionImpactLevel > 0)
{ {
@@ -2033,6 +2068,7 @@ void ApplyBoardGravity()
/** /**
* @brief Rogue * @brief Rogue
* @param linesCleared
*/ */
void ApplyLineClearResult(int linesCleared) void ApplyLineClearResult(int linesCleared)
{ {
@@ -2053,6 +2089,7 @@ void ApplyLineClearResult(int linesCleared)
return; return;
} }
// 基础收益先计算,再依次套用风险、成长、狂热、赌徒和连击类修正。
int scoreGain = GetRogueScoreByLines(linesCleared); int scoreGain = GetRogueScoreByLines(linesCleared);
scoreGain = scoreGain * rogueStats.scoreMultiplierPercent / 100; scoreGain = scoreGain * rogueStats.scoreMultiplierPercent / 100;
@@ -2438,6 +2475,9 @@ void OpenUpgradeMenu()
/** /**
* @brief * @brief
*
*
*
*/ */
void ConfirmUpgradeSelection() void ConfirmUpgradeSelection()
{ {
@@ -2446,6 +2486,7 @@ void ConfirmUpgradeSelection()
return; return;
} }
// 命运轮盘或双重抉择使用多选分支,避免选完第一张后候选池被立即刷新。
if (upgradeUiState.picksRemaining > 1) if (upgradeUiState.picksRemaining > 1)
{ {
if (upgradeUiState.markedCount != upgradeUiState.picksRemaining) if (upgradeUiState.markedCount != upgradeUiState.picksRemaining)
@@ -2522,6 +2563,7 @@ void ConfirmUpgradeSelection()
return; return;
} }
// 普通升级只应用当前高亮卡片,并把剩余卡片前移,支持后续 picksRemaining。
UpgradeOption selectedOption = upgradeUiState.options[upgradeUiState.selectedIndex]; UpgradeOption selectedOption = upgradeUiState.options[upgradeUiState.selectedIndex];
TCHAR gamblerSuffix[64] = _T(""); TCHAR gamblerSuffix[64] = _T("");
int applyCount = RollGamblerApplyCount(gamblerSuffix, 64, false); int applyCount = RollGamblerApplyCount(gamblerSuffix, 64, false);
@@ -2580,6 +2622,7 @@ void ConfirmUpgradeSelection()
return; return;
} }
// 连续升级时重新生成下一轮强化;全部完成后回到游戏并播放延迟效果。
if (upgradeUiState.pendingCount > 0) if (upgradeUiState.pendingCount > 0)
{ {
upgradeUiState.pendingCount--; upgradeUiState.pendingCount--;
+4 -3
View File
@@ -1,6 +1,7 @@
// stdafx.cpp : 只包括标准包含文件的源文件 /**
// Tetris.pch 将作为预编译头 * @file stdafx.cpp
// stdafx.obj 将包含预编译类型信息 * @brief stdafx.h
*/
#include "stdafx.h" #include "stdafx.h"