# 22 · 任务与挑战房间模块(Quest & Challenge Module)
> **命名空间** `BaseGames.Quest` / `BaseGames.Challenge`
> **程序集** `BaseGames.Quest`(`Assets/Scripts/Quest/`)
> **依赖** `BaseGames.Core.Events` · `BaseGames.Dialogue`(InteractableNPC)· `BaseGames.World`(SaveManager · SceneLoader)· `BaseGames.Player`(PlayerStats)
> **Design 来源** [38_QuestSystem](../Design/38_QuestSystem.md) · [39_ChallengeRoomSystem](../Design/39_ChallengeRoomSystem.md)
---
## 目录
### Part A — 任务系统
1. [任务系统职责](#1-任务系统职责)
2. [QuestSO](#2-questso)
3. [QuestObjectiveSO](#3-questobjectiveso)
4. [RewardSO](#4-rewardso)
5. [QuestManager](#5-questmanager)
6. [QuestGiver](#6-questgiver)
7. [SaveData 集成(任务)](#7-savedata-集成任务)
### Part B — 挑战房间
8. [挑战房间系统职责](#8-挑战房间系统职责)
9. [ChallengeRoomSO](#9-challengeroomso)
10. [ChallengeEncounterSO](#10-challengeencounterso)
11. [BossRushSequenceSO](#11-bossrushsequenceso)
12. [ChallengeRoomManager](#12-challengeroommanager)
13. [ChallengeRoomTrigger](#13-challengeroomtrigger)
14. [事件频道](#14-事件频道)
---
## Part A — 任务系统
---
## 1. 任务系统职责
```
任务系统职责:
├─ QuestSO → 任务定义(目标链/奖励/分支/前置条件)
├─ QuestObjectiveSO → 单步目标(对话/击败/收集/到达)
├─ RewardSO → 奖励配置(Geo/物品/好感度/能力)
├─ QuestManager → 运行时追踪所有任务状态,事件驱动推进进度
└─ QuestGiver → 扩展 InteractableNPC,发布/完成任务,切换对话
```
**状态机**:`Unavailable → Available → Active → Completed / Failed`(只进不退)
**事件驱动**:`QuestManager` 只订阅事件频道(击败敌人/收集物品/到达地点),不主动轮询。
---
## 2. QuestSO
```csharp
namespace BaseGames.Quest
{
[CreateAssetMenu(menuName = "Quest/Quest")]
public class QuestSO : ScriptableObject
{
[Header("标识")]
public string questId; // 唯一 ID,如 "Quest_FindMushroom"
public string displayName;
[TextArea(2, 6)]
public string description;
public Sprite icon;
[Header("目标链")]
public QuestObjectiveSO[] objectives; // 按顺序完成,全部完成 = 可交完
[Header("前置条件")]
public string[] prerequisiteQuestIds; // 所有前置任务 Completed 后才可接
public int minAffinityToAccept; // NPC 好感度门槛(0 = 无限制)
[Header("奖励")]
public RewardSO reward;
[Header("失败条件(可选)")]
public bool canFail;
public QuestObjectiveSO failCondition;
[Header("完成后续任务(分支)")]
public QuestBranch[] branches;
}
[Serializable]
public class QuestBranch
{
public string conditionQuestId; // 若此任务已完成 → 走本分支(空 = 默认)
public QuestSO nextQuest;
public string npcDialogueKey; // 触发 NPC 对话 key
}
}
```
---
## 3. QuestObjectiveSO(多态目标体系)
每种目标类型使用独立的子类 SO,便于策划在 Inspector 中配置且无需修改代码即可扩展:
```csharp
namespace BaseGames.Quest
{
///
/// 任务目标基类(抽象)。所有具体目标类型均继承此类。
/// 策划通过各子类的 CreateAssetMenu 创建具体目标资产。
///
public abstract class QuestObjectiveSO : ScriptableObject
{
[Header("标识")]
public string objectiveId;
[TextArea(1, 4)]
public string displayText; // 任务日志中显示的文本
public bool IsOptional; // 可选目标(完成加奖励但不阻塞任务)
/// 注册监听(由 QuestManager 在任务激活时调用)。
public abstract void RegisterListeners(IQuestObjectiveListener listener);
/// 注销监听(由 QuestManager 在任务完成/失败时调用)。
public abstract void UnregisterListeners(IQuestObjectiveListener listener);
/// 根据当前进度判断目标是否完成。
public abstract bool EvaluateCompletion(QuestObjectiveState state);
}
// ── 具体目标类型 ──────────────────────────────────────────────
[CreateAssetMenu(menuName = "Quest/Objective/TalkToNPC")]
public class TalkToNPCObjective : QuestObjectiveSO
{
public string targetNpcId;
public override void RegisterListeners(IQuestObjectiveListener l) => l.RegisterListeners(this);
public override void UnregisterListeners(IQuestObjectiveListener l) => l.UnregisterListeners(this);
public override bool EvaluateCompletion(QuestObjectiveState s) => s.progressCount >= 1;
}
[CreateAssetMenu(menuName = "Quest/Objective/Defeat")]
public class DefeatEnemyObjective : QuestObjectiveSO
{
public string targetEnemyId;
[Min(1)] public int defeatCount = 1;
public override void RegisterListeners(IQuestObjectiveListener l) => l.RegisterListeners(this);
public override void UnregisterListeners(IQuestObjectiveListener l) => l.UnregisterListeners(this);
public override bool EvaluateCompletion(QuestObjectiveState s) => s.progressCount >= defeatCount;
}
[CreateAssetMenu(menuName = "Quest/Objective/Collect")]
public class CollectItemObjective : QuestObjectiveSO
{
public string itemId;
[Min(1)] public int collectCount = 1;
public override void RegisterListeners(IQuestObjectiveListener l) => l.RegisterListeners(this);
public override void UnregisterListeners(IQuestObjectiveListener l) => l.UnregisterListeners(this);
public override bool EvaluateCompletion(QuestObjectiveState s) => s.progressCount >= collectCount;
}
[CreateAssetMenu(menuName = "Quest/Objective/Reach")]
public class ReachAreaObjective : QuestObjectiveSO
{
public string sceneName; // 需到达的场景
public string markerTag; // 场景内的目标标记 Tag
public override void RegisterListeners(IQuestObjectiveListener l) => l.RegisterListeners(this);
public override void UnregisterListeners(IQuestObjectiveListener l) => l.UnregisterListeners(this);
public override bool EvaluateCompletion(QuestObjectiveState s) => s.progressCount >= 1;
}
[CreateAssetMenu(menuName = "Quest/Objective/UseSkill")]
public class UseSkillObjective : QuestObjectiveSO
{
public AbilityType requiredAbility;
[Min(1)] public int useCount = 1;
public override void RegisterListeners(IQuestObjectiveListener l) => l.RegisterListeners(this);
public override void UnregisterListeners(IQuestObjectiveListener l) => l.UnregisterListeners(this);
public override bool EvaluateCompletion(QuestObjectiveState s) => s.progressCount >= useCount;
}
}
```
**扩展方式**:继承 `QuestObjectiveSO`,实现三个抽象方法,标注 `[CreateAssetMenu]`,无需修改 `QuestManager`。
---
## 4. RewardSO
```csharp
namespace BaseGames.Quest
{
[CreateAssetMenu(menuName = "Quest/Reward")]
public class RewardSO : ScriptableObject
{
public int geo; // Geo 奖励
public int soulBonus; // 灵魂槽扩展(+max)
public string[] itemIds; // 物品/护符 ID 列表
public int affinityBonus; // 对发布 NPC 的好感度增量
public string unlockDialogueKey; // 解锁 NPC 新台词集合 key
public bool unlocksAbility = false; // ⚠️ AbilityType 无 None 值,用 bool 标识是否解锁能力(架构 09 §1)
public AbilityType unlockedAbility; // 仅当 unlocksAbility == true 时有效
[Header("物品发放事件")]
[Tooltip("EVT_CollectiblePickup:向 QuestManager/EquipmentManager 广播 itemId")]
[SerializeField] private StringEventChannelSO _onCollectiblePickup;
/// 将奖励应用到游戏状态(由 QuestManager.CompleteQuest 调用)。
public void Apply(PlayerStats player)
{
if (geo > 0) player.AddGeo(geo);
if (soulBonus > 0) player.AddSoulPower(soulBonus);
if (unlocksAbility) // ⚠️ 替代 AbilityType.None 判断
player.UnlockAbility(unlockedAbility);
// 物品/护符通过 EVT_CollectiblePickup 事件频道广播(InventoryManager 不存在于本项目)
if (itemIds != null && _onCollectiblePickup != null)
foreach (var id in itemIds)
_onCollectiblePickup.Raise(id);
}
}
}
```
---
## 5. QuestManager
```csharp
namespace BaseGames.Quest
{
///
/// 运行时任务管理器,挂在 Persistent 场景 [GameManagers] 下。
/// 通过事件频道追踪目标进度,不主动轮询。
///
public class QuestManager : MonoBehaviour
{
// ── Inspector ────────────────────────────────────────
[SerializeField] QuestSO[] _allQuests; // 所有任务 SO
[SerializeField] TransformEventChannelSO _onEnemyDied; // EVT_EnemyDied → 通过 Transform.GetComponent 获取敌人类型
[SerializeField] StringEventChannelSO _onCollectiblePickup; // EVT_CollectiblePickup(itemId)
[SerializeField] StringEventChannelSO _onSceneLoaded; // EVT_SceneLoaded(sceneName)
[SerializeField] StringEventChannelSO _onNpcDialogueCompleted; // EVT_NpcDialogueCompleted(npcId)
// 分拆为粒度更细的事件频道(替代旧 _onQuestStateChanged 单频道)
[SerializeField] StringEventChannelSO _onQuestStarted; // Raise:questId
[SerializeField] StringEventChannelSO _onQuestCompleted; // Raise:questId
[SerializeField] StringEventChannelSO _onQuestFailed; // Raise:questId
[SerializeField] QuestObjectiveEventChannelSO _onObjectiveUpdated; // Raise:objectiveId + progress
// ── Runtime State ────────────────────────────────────
readonly Dictionary _questStates = new();
readonly Dictionary _objectiveStates = new(); // objectiveId → 状态
public static QuestManager Instance { get; private set; }
/// 向后兼容:保留任务开始事件频道公开属性(供 QuestGiver/QuestLogUI 订阅)。
public StringEventChannelSO OnQuestStarted => _onQuestStarted;
public StringEventChannelSO OnQuestCompleted => _onQuestCompleted;
void Awake() => Instance = this;
void OnEnable()
{
_onEnemyDied.OnEventRaised += HandleEnemyDefeated;
_onCollectiblePickup.OnEventRaised += HandleItemCollected;
_onSceneLoaded.OnEventRaised += HandleSceneLoaded;
_onNpcDialogueCompleted.OnEventRaised += HandleNpcDialogue;
}
void OnDisable()
{
_onEnemyDied.OnEventRaised -= HandleEnemyDefeated;
_onCollectiblePickup.OnEventRaised -= HandleItemCollected;
_onSceneLoaded.OnEventRaised -= HandleSceneLoaded;
_onNpcDialogueCompleted.OnEventRaised -= HandleNpcDialogue;
}
// ── 公共 API ──────────────────────────────────────────
/// NPC 接受任务时调用。
public void AcceptQuest(string questId)
{
if (!CanAccept(questId)) return;
_questStates[questId] = QuestState.Active;
_onQuestStarted.Raise(questId);
}
/// NPC 完成任务时调用。
public void CompleteQuest(string questId, PlayerStats player)
{
if (!IsReadyToComplete(questId)) return;
var quest = GetQuestSO(questId);
quest.reward?.Apply(player);
_questStates[questId] = QuestState.Completed;
_onQuestCompleted.Raise(questId);
// 解锁后续任务
foreach (var branch in quest.branches)
{
if (string.IsNullOrEmpty(branch.conditionQuestId) ||
GetState(branch.conditionQuestId) == QuestState.Completed)
{
if (branch.nextQuest != null)
_questStates[branch.nextQuest.questId] = QuestState.Available;
break;
}
}
}
public QuestState GetState(string questId)
=> _questStates.TryGetValue(questId, out var s) ? s : QuestState.Unavailable;
public bool IsReadyToComplete(string questId)
{
var quest = GetQuestSO(questId);
if (quest == null || GetState(questId) != QuestState.Active) return false;
foreach (var obj in quest.objectives)
{
if (!obj.IsOptional && !IsObjectiveComplete(obj)) return false;
}
return true;
}
// ── 私有 ─────────────────────────────────────────────
bool CanAccept(string questId)
{
if (GetState(questId) != QuestState.Available) return false;
var quest = GetQuestSO(questId);
foreach (var pre in quest.prerequisiteQuestIds)
if (GetState(pre) != QuestState.Completed) return false;
return true;
}
bool IsObjectiveComplete(QuestObjectiveSO obj)
{
_objectiveStates.TryGetValue(obj.objectiveId, out var s);
s ??= new QuestObjectiveState();
return obj.EvaluateCompletion(s); // 多态:各子类自行判断完成条件
}
void HandleEnemyDefeated(Transform enemyTransform)
{
// 通过 Transform 获取敌人 ID(EnemyBase.EnemyId 属性)
var enemyBase = enemyTransform.GetComponent();
if (enemyBase == null) return;
string enemyId = enemyBase.EnemyId;
foreach (var (qid, state) in _questStates)
{
if (state != QuestState.Active) continue;
var quest = GetQuestSO(qid);
foreach (var obj in quest.objectives)
{
if (obj.type == ObjectiveType.Defeat && obj.targetEnemyId == enemyId)
IncrementProgress(obj.objectiveId);
}
}
}
void HandleItemCollected(string itemId) { /* 同上,匹配 Collect 目标 */ }
void HandleNpcDialogue(string npcId) { /* 同上,匹配 TalkTo 目标 */ }
void HandleSceneLoaded(string sceneName) { /* 同上,匹配 Reach 目标 */ }
void IncrementProgress(string objectiveId)
{
if (!_objectiveStates.TryGetValue(objectiveId, out var s))
s = _objectiveStates[objectiveId] = new QuestObjectiveState();
s.progressCount++;
_onObjectiveUpdated.Raise(new QuestObjectiveEvent { ObjectiveId = objectiveId, Progress = s.progressCount });
}
QuestSO GetQuestSO(string id) => System.Array.Find(_allQuests, q => q.questId == id);
}
public enum QuestState { Unavailable, Available, Active, Completed, Failed }
/// 记录单个目标的运行时进度。
public class QuestObjectiveState
{
public bool completed = false;
public int progressCount = 0;
}
///
/// 实现此接口的 MonoBehaviour 可自行注册/注销目标监听,
/// 由 QuestManager.RegisterObjectiveListeners() 自动调用。
///
public interface IQuestObjectiveListener
{
void RegisterListeners(QuestManager manager);
void UnregisterListeners(QuestManager manager);
}
}
```
---
## 6. QuestGiver
```csharp
namespace BaseGames.Quest
{
///
/// 继承 InteractableNPC,负责发布/完成任务并根据任务状态切换对话版本。
/// 不依赖 [RequireComponent],直接通过继承获得 Interact 流程。
///
public class QuestGiver : InteractableNPC
{
[Header("任务")]
[SerializeField] QuestSO[] _offeredQuests; // 该 NPC 可提供的所有任务(按优先级排列)
[Header("对话版本(根据任务状态切换)")]
[SerializeField] DialogueSequenceSO _availableDialogue; // 任务可接时
[SerializeField] DialogueSequenceSO _activeDialogue; // 任务进行中
[SerializeField] DialogueSequenceSO _readyDialogue; // 完成条件满足时
[SerializeField] DialogueSequenceSO _completedDialogue; // 任务已完成后
// ── Interact_Internal 覆盖(在启动对话前处理任务逻辑)────────
protected override void Interact_Internal(Transform player)
{
var quest = GetCurrentQuest();
if (quest == null) return;
var state = QuestManager.Instance.GetState(quest.questId);
if (state == QuestState.Available)
QuestManager.Instance.AcceptQuest(quest.questId);
else if (QuestManager.Instance.IsReadyToComplete(quest.questId))
QuestManager.Instance.CompleteQuest(quest.questId,
player.GetComponent()?.Stats);
}
// ── 返回与当前最高优先级任务状态匹配的对话 SO ──────────────────
protected override DialogueSequenceSO GetCurrentDialogue()
{
var quest = GetCurrentQuest();
if (quest == null) return base.GetCurrentDialogue();
return QuestManager.Instance.GetState(quest.questId) switch
{
QuestState.Available => _availableDialogue,
QuestState.Active => QuestManager.Instance.IsReadyToComplete(quest.questId)
? _readyDialogue : _activeDialogue,
QuestState.Completed => _completedDialogue,
_ => base.GetCurrentDialogue(),
};
}
// 返回当前处于 Available 或 Active 状态的第一个任务
QuestSO GetCurrentQuest()
{
if (_offeredQuests == null) return null;
foreach (var q in _offeredQuests)
{
var s = QuestManager.Instance.GetState(q.questId);
if (s == QuestState.Available || s == QuestState.Active) return q;
}
return null;
}
}
}
```
---
## 7. SaveData 集成(任务)
`SaveData.Quests.QuestStates`(已在 `12_SaveModule.md` 中定义)读写:
```csharp
// 存档时(SaveManager.GatherSaveData)
foreach (var (id, state) in QuestManager.Instance.QuestStates)
saveData.Quests.QuestStates[id] = (int)state;
// 读档时(QuestManager.LoadFromSaveData)
public void LoadFromSaveData(QuestSaveData data)
{
_questStates.Clear();
foreach (var (id, stateInt) in data.QuestStates)
_questStates[id] = (QuestState)stateInt;
foreach (var (id, progress) in data.ObjectiveProgress)
{
if (!_objectiveStates.TryGetValue(id, out var s))
s = _objectiveStates[id] = new QuestObjectiveState();
s.progressCount = progress;
}
}
```
---
## Part B — 挑战房间
---
## 8. 挑战房间系统职责
```
挑战房间系统职责:
├─ ChallengeRoomSO → 挑战定义(波次/时限/奖励)
├─ ChallengeEncounterSO → 单波敌人配置
├─ BossRushSequenceSO → Boss Rush 序列数据
├─ ChallengeRoomManager → 运行时流程管理(开始/推进/成功/失败)
└─ ChallengeRoomTrigger → 场景组件,触发进入挑战
```
---
## 9. ChallengeRoomSO
```csharp
namespace BaseGames.Challenge
{
[CreateAssetMenu(menuName = "Challenge/ChallengeRoom")]
public class ChallengeRoomSO : ScriptableObject
{
[Header("标识")]
public string challengeId;
public string displayName;
public ChallengeType challengeType;
[Header("波次(非 BossRush)")]
public ChallengeEncounterSO[] encounters;
[Header("Boss Rush(BossRush 类型专用)")]
public BossRushSequenceSO bossRushSequence;
[Header("限制条件")]
public float timeLimit; // 0 = 无时限
public bool requireNoHit;
public int minComboRequired; // 0 = 无要求
[Header("奖励")]
public RewardSO firstClearReward; // 首次通关奖励
public RewardSO repeatedReward; // 重复通关奖励
[Header("解锁条件")]
public string[] prerequisiteBossIds; // 需击败的 Boss ID
}
public enum ChallengeType { Survival, TimeTrial, BossRush, NoHit }
}
```
---
## 10. ChallengeEncounterSO
```csharp
namespace BaseGames.Challenge
{
[CreateAssetMenu(menuName = "Challenge/Encounter")]
public class ChallengeEncounterSO : ScriptableObject
{
[Serializable]
public struct SpawnEntry
{
public string enemyAddressKey; // Addressables key
public Transform spawnPoint;
public int count;
}
public SpawnEntry[] enemies;
public float waveDelay; // 上波清空后等待多少秒生成本波
}
}
```
---
## 11. BossRushSequenceSO
```csharp
namespace BaseGames.Challenge
{
[CreateAssetMenu(menuName = "Challenge/BossRushSequence")]
public class BossRushSequenceSO : ScriptableObject
{
[Serializable]
public struct BossEntry
{
public string bossSceneName; // Boss 所在场景(Additive 加载)
public string bossId;
public float hpRestoreRatio; // 击败本 Boss 后玩家恢复 HP 比例(默认 0.3)
}
public BossEntry[] bosses;
}
}
```
---
## 12. ChallengeRoomManager
```csharp
namespace BaseGames.Challenge
{
///
/// 挑战房间流程管理器,挂在挑战房间场景的 [ChallengeManager] GameObject 上。
/// 场景加载时自动启动挑战。
///
public class ChallengeRoomManager : MonoBehaviour
{
[SerializeField] ChallengeRoomSO _challengeData;
[SerializeField] StringEventChannelSO _onChallengeCompleted; // → EVT_ChallengeCompleted(challengeId)
[SerializeField] StringEventChannelSO _onChallengeFailed; // → EVT_ChallengeFailed(challengeId)
// ⚠️ PlayerController 无 Instance(Architecture 05 §2);挑战房间场景持有引用
[SerializeField] PlayerController _player;
int _currentEncounterIndex;
int _remainingEnemies;
float _elapsedTime;
bool _isRunning;
bool _noHitViolated;
void Start() => StartChallenge();
void Update()
{
if (!_isRunning) return;
_elapsedTime += Time.deltaTime;
// 超时失败
if (_challengeData.timeLimit > 0 && _elapsedTime >= _challengeData.timeLimit)
FailChallenge();
}
void StartChallenge()
{
// 自动存档当前位置(挑战失败后读档)
SaveManager.Instance.QuickSave();
_isRunning = true;
_currentEncounterIndex = 0;
SpawnWave(_currentEncounterIndex);
}
void SpawnWave(int index)
{
var enc = _challengeData.encounters[index];
_remainingEnemies = 0;
foreach (var entry in enc.enemies)
{
for (int i = 0; i < entry.count; i++)
{
_remainingEnemies++;
// Addressables 加载并生成敌人
Addressables.InstantiateAsync(entry.enemyAddressKey, entry.spawnPoint.position, Quaternion.identity)
.Completed += handle =>
{
if (handle.Result.TryGetComponent(out var enemy))
enemy.OnDied += OnEnemyDefeated;
};
}
}
}
void OnEnemyDefeated()
{
_remainingEnemies--;
if (_remainingEnemies > 0) return;
_currentEncounterIndex++;
if (_currentEncounterIndex >= _challengeData.encounters.Length)
CompleteChallenge();
else
StartCoroutine(DelayedNextWave(_challengeData.encounters[_currentEncounterIndex].waveDelay));
}
IEnumerator DelayedNextWave(float delay)
{
yield return new WaitForSeconds(delay);
SpawnWave(_currentEncounterIndex);
}
void CompleteChallenge()
{
_isRunning = false;
var reward = SaveManager.Instance.IsFirstClear(_challengeData.challengeId)
? _challengeData.firstClearReward
: _challengeData.repeatedReward;
reward?.Apply(_player.Stats);
_onChallengeCompleted.Raise(_challengeData.challengeId);
}
void FailChallenge()
{
_isRunning = false;
_onChallengeFailed.Raise(_challengeData.challengeId);
// 自动读档回挑战入口
SaveManager.Instance.QuickLoad();
}
}
}
```
---
## 13. ChallengeRoomTrigger
```csharp
namespace BaseGames.Challenge
{
///
/// 放置在挑战房间入口处,玩家交互后加载挑战场景。
///
[RequireComponent(typeof(Collider2D))]
public class ChallengeRoomTrigger : MonoBehaviour, IInteractable
{
[SerializeField] ChallengeRoomSO _challengeData;
[SerializeField] string _challengeSceneName;
[SerializeField] SceneLoadRequestEventChannelSO _onSceneLoadRequest; // ⚠️ 通过事件频道触发加载(SceneLoader 无 Instance;架构 03 §3)
public string InteractPrompt => $"进入挑战:{_challengeData.displayName}";
public bool CanInteract => IsUnlocked();
public void Interact(Transform player)
{
if (!IsUnlocked()) return;
// ⚠️ 通过 EVT_SceneLoadRequest 频道触发(不直接调用 SceneLoader,架构 03 §3 接口为 RequestLoad)
_onSceneLoadRequest.Raise(new SceneLoadRequest
{
SceneName = _challengeSceneName,
EntryTransitionId = string.Empty,
ShowLoadingScreen = false,
IsRespawn = false,
});
}
public void OnPlayerEnterRange(Transform player) { }
public void OnPlayerExitRange() { }
bool IsUnlocked()
{
foreach (var bossId in _challengeData.prerequisiteBossIds)
if (!SaveManager.Instance.IsBossDefeated(bossId)) return false;
return true;
}
}
}
```
---
## 14. 事件频道
| 频道 SO | Payload | 发布者 | 订阅者 |
|--------|---------|--------|--------|
| `EVT_QuestStateChanged` | `(string questId, QuestState)` | `QuestManager` | `QuestGiver`(刷新对话)、`QuestLogUI`(刷新日志) |
| `EVT_ChallengeCompleted` | `string challengeId` | `ChallengeRoomManager` | `HUDController`(结算界面)、`AchievementManager` |
| `EVT_ChallengeFailed` | `string challengeId` | `ChallengeRoomManager` | `SaveManager`(触发读档)、`HUDController` |