Files
zeling_v2/Docs/Architecture/22_QuestChallengeModule.md
2026-05-12 15:34:08 +08:00

752 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
{
/// <summary>
/// 任务目标基类(抽象)。所有具体目标类型均继承此类。
/// 策划通过各子类的 CreateAssetMenu 创建具体目标资产。
/// </summary>
public abstract class QuestObjectiveSO : ScriptableObject
{
[Header("标识")]
public string objectiveId;
[TextArea(1, 4)]
public string displayText; // 任务日志中显示的文本
public bool IsOptional; // 可选目标(完成加奖励但不阻塞任务)
/// <summary>注册监听(由 QuestManager 在任务激活时调用)。</summary>
public abstract void RegisterListeners(IQuestObjectiveListener listener);
/// <summary>注销监听(由 QuestManager 在任务完成/失败时调用)。</summary>
public abstract void UnregisterListeners(IQuestObjectiveListener listener);
/// <summary>根据当前进度判断目标是否完成。</summary>
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;
/// <summary>将奖励应用到游戏状态(由 QuestManager.CompleteQuest 调用)。</summary>
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
{
/// <summary>
/// 运行时任务管理器,挂在 Persistent 场景 [GameManagers] 下。
/// 通过事件频道追踪目标进度,不主动轮询。
/// </summary>
public class QuestManager : MonoBehaviour
{
// ── Inspector ────────────────────────────────────────
[SerializeField] QuestSO[] _allQuests; // 所有任务 SO
[SerializeField] TransformEventChannelSO _onEnemyDied; // EVT_EnemyDied → 通过 Transform.GetComponent 获取敌人类型
[SerializeField] StringEventChannelSO _onCollectiblePickup; // EVT_CollectiblePickupitemId
[SerializeField] StringEventChannelSO _onSceneLoaded; // EVT_SceneLoadedsceneName
[SerializeField] StringEventChannelSO _onNpcDialogueCompleted; // EVT_NpcDialogueCompletednpcId
// 分拆为粒度更细的事件频道(替代旧 _onQuestStateChanged 单频道)
[SerializeField] StringEventChannelSO _onQuestStarted; // RaisequestId
[SerializeField] StringEventChannelSO _onQuestCompleted; // RaisequestId
[SerializeField] StringEventChannelSO _onQuestFailed; // RaisequestId
[SerializeField] QuestObjectiveEventChannelSO _onObjectiveUpdated; // RaiseobjectiveId + progress
// ── Runtime State ────────────────────────────────────
readonly Dictionary<string, QuestState> _questStates = new();
readonly Dictionary<string, QuestObjectiveState> _objectiveStates = new(); // objectiveId → 状态
public static QuestManager Instance { get; private set; }
/// <summary>向后兼容:保留任务开始事件频道公开属性(供 QuestGiver/QuestLogUI 订阅)。</summary>
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 ──────────────────────────────────────────
/// <summary>NPC 接受任务时调用。</summary>
public void AcceptQuest(string questId)
{
if (!CanAccept(questId)) return;
_questStates[questId] = QuestState.Active;
_onQuestStarted.Raise(questId);
}
/// <summary>NPC 完成任务时调用。</summary>
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 获取敌人 IDEnemyBase.EnemyId 属性)
var enemyBase = enemyTransform.GetComponent<EnemyBase>();
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 }
/// <summary>记录单个目标的运行时进度。</summary>
public class QuestObjectiveState
{
public bool completed = false;
public int progressCount = 0;
}
/// <summary>
/// 实现此接口的 MonoBehaviour 可自行注册/注销目标监听,
/// 由 QuestManager.RegisterObjectiveListeners() 自动调用。
/// </summary>
public interface IQuestObjectiveListener
{
void RegisterListeners(QuestManager manager);
void UnregisterListeners(QuestManager manager);
}
}
```
---
## 6. QuestGiver
```csharp
namespace BaseGames.Quest
{
/// <summary>
/// 继承 InteractableNPC负责发布/完成任务并根据任务状态切换对话版本。
/// 不依赖 [RequireComponent],直接通过继承获得 Interact 流程。
/// </summary>
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<PlayerController>()?.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 RushBossRush 类型专用)")]
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
{
/// <summary>
/// 挑战房间流程管理器,挂在挑战房间场景的 [ChallengeManager] GameObject 上。
/// 场景加载时自动启动挑战。
/// </summary>
public class ChallengeRoomManager : MonoBehaviour
{
[SerializeField] ChallengeRoomSO _challengeData;
[SerializeField] StringEventChannelSO _onChallengeCompleted; // → EVT_ChallengeCompletedchallengeId
[SerializeField] StringEventChannelSO _onChallengeFailed; // → EVT_ChallengeFailedchallengeId
// ⚠️ PlayerController 无 InstanceArchitecture 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<EnemyBase>(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
{
/// <summary>
/// 放置在挑战房间入口处,玩家交互后加载挑战场景。
/// </summary>
[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` |