using UnityEngine; using BaseGames.Core.Events; using BaseGames.Player; namespace BaseGames.Quest { /// /// 任务事件类型枚举,对应 QuestManager 订阅的各事件频道。 /// 新增事件类型时在此扩展,无需修改 QuestManager。 /// public enum QuestEventType { EnemyDefeated, ItemCollected, NpcDialogueCompleted, SceneLoaded, SkillUsed, /// /// 玩家进入场景内的具体区域标记(由 TriggerZone 广播)。 /// payload = TriggerZone.markerTag(string)。 /// 与 SceneLoaded(场景级)互补,实现精确的区域到达判定。 /// AreaReached, } /// /// 任务目标基类(抽象,架构 22_QuestChallengeModule §3)。 /// 所有具体目标类型均继承此类,通过多态实现零代码扩展。 /// /// 【自注册机制】子类通过 override 声明自己 /// 感兴趣的事件类型及匹配条件,QuestManager 统一路由,无需为每种目标类型 /// 硬编码处理器。新增目标类型只需: /// 1. 继承 QuestObjectiveSO /// 2. override TryHandleEvent /// 3. override EvaluateCompletion /// 4. 创建 CreateAssetMenu /// QuestManager 代码**无需任何修改**。 /// public abstract class QuestObjectiveSO : ScriptableObject { [Header("标识")] [Tooltip("目标唯一 ID,如 \"OBJ_TalkElder\"。空时由 OnValidate 自动以资产名填充。")] public string objectiveId; [Tooltip("本地化 Key,格式如 \"Quest_FindMushroom_Obj1\"。通过 LocalizationManager.Get(displayTextKey, \"Quest\") 显示给玩家。")] [TextArea(1, 4)] public string displayTextKey; [Tooltip("勾选后此目标为可选项:完成可获奖励,但不阻塞任务交接。")] public bool IsOptional; [Tooltip("前置目标 objectiveId(留空 = 无依赖,目标与其他目标并行激活)。\n" + "设置后,在指定目标的 completed 标志置为 true 之前,本目标不接收任何事件路由,即便事件满足匹配条件。\n" + "用于实现顺序解锁的目标链(如先对话再交物品)。")] public string prerequisiteObjectiveId; /// /// 目标所需完成数量。用于 UI 显示进度条分母(如 "3/5 击败")。 /// 子类应 override 返回相应计数字段(defeatCount、collectCount 等)。 /// 默认返回 1(表示"完成一次即可"的目标类型)。 /// public virtual int GetRequiredCount() => 1; /// 根据当前进度判断目标是否完成。 public abstract bool EvaluateCompletion(QuestObjectiveState state); /// /// 尝试处理一个运行时事件。 /// QuestManager 在每次事件到来时对所有活跃目标调用此方法。 /// 子类 override:若事件与自身条件匹配,递增 state.progressCount 并返回 true; /// 不匹配时返回 false(基类默认实现)。 /// /// 参数 含义由事件类型决定: /// /// EnemyDefeated → enemyId (string) /// ItemCollected → itemId (string) /// NpcDialogueCompleted → npcId (string) /// SceneLoaded → sceneName (string) /// SkillUsed → AbilityType.ToString() (string) /// /// public virtual bool TryHandleEvent(QuestEventType eventType, string payload, QuestObjectiveState state) => false; /// /// 强类型载荷重载()。 /// 默认实现委托给 string 版本(向后兼容),子类可 override 以直接消费结构化载荷,避免字符串解析。 /// QuestManager 优先调用此重载。 /// public virtual bool TryHandleEvent(QuestEventType eventType, IQuestEventPayload payload, QuestObjectiveState state) => TryHandleEvent(eventType, payload?.AsString(), state); /// /// 在 DataHub / 编辑器工具中显示的类型徽章文字。 /// 子类应 override 返回简洁中文标签(如 "[对话]")。 /// 避免工具代码中使用 type-switch 维护列表。 /// public virtual string BadgeLabel => "[目标]"; #if UNITY_EDITOR // objectiveId → 资产路径,5 秒 TTL,跨所有 QuestObjectiveSO 子类 OnValidate 共用。 // 与 QuestSO / DialogueSequenceSO / DialogueActorSO 保持一致的 O(1) 重复检测策略。 private static System.Collections.Generic.Dictionary s_objIdToPath; private static double s_objIdsCacheTime = -10.0; private static System.Collections.Generic.Dictionary GetObjectiveIdCache() { double now = UnityEditor.EditorApplication.timeSinceStartup; if (s_objIdToPath != null && now - s_objIdsCacheTime < 5.0) return s_objIdToPath; s_objIdToPath = new System.Collections.Generic.Dictionary(System.StringComparer.Ordinal); string[] guids = UnityEditor.AssetDatabase.FindAssets("t:QuestObjectiveSO"); foreach (var guid in guids) { var path = UnityEditor.AssetDatabase.GUIDToAssetPath(guid); var obj = UnityEditor.AssetDatabase.LoadAssetAtPath(path); if (obj != null && !string.IsNullOrEmpty(obj.objectiveId) && !s_objIdToPath.ContainsKey(obj.objectiveId)) s_objIdToPath[obj.objectiveId] = path; } s_objIdsCacheTime = now; return s_objIdToPath; } private void OnValidate() { // 若 objectiveId 为空,自动以资产文件名填充 if (string.IsNullOrEmpty(objectiveId)) { objectiveId = name; UnityEditor.EditorUtility.SetDirty(this); } // 检测重复:缓存路径 vs 自身路径比对(O(1)),5 秒内无需重扫 var cache = GetObjectiveIdCache(); string myPath = UnityEditor.AssetDatabase.GetAssetPath(this); if (!string.IsNullOrEmpty(myPath) && cache.TryGetValue(objectiveId, out var existingPath) && existingPath != myPath) { Debug.LogError( $"[QuestObjectiveSO] objectiveId '{objectiveId}' 与 " + $"'{System.IO.Path.GetFileNameWithoutExtension(existingPath)}' 重复!" + "重复 ID 会导致任务进度互相覆盖,请修改其中一个。", this); s_objIdsCacheTime = -10.0; } } #endif } // ── 运行时目标进度状态(由 QuestManager 管理,不继承 SO)──────────────── public class QuestObjectiveState { public bool completed = false; public int progressCount = 0; } // ── 具体目标类型 ──────────────────────────────────────────────────────── /// 与指定 NPC 对话后完成。 [CreateAssetMenu(menuName = "BaseGames/Quest/Objective/TalkToNPC")] public class TalkToNPCObjective : QuestObjectiveSO { [Tooltip("目标 NPC 的唯一 ID,需与 NPC 组件上的 npcId / InteractableNPC.npcId 保持一致。")] public string targetNpcId; public override string BadgeLabel => "[对话]"; public override bool EvaluateCompletion(QuestObjectiveState s) => s.progressCount >= 1; public override bool TryHandleEvent(QuestEventType eventType, string payload, QuestObjectiveState state) { if (eventType != QuestEventType.NpcDialogueCompleted) return false; if (payload != targetNpcId) return false; state.progressCount++; return true; } } /// 击败指定 ID 的敌人若干次。 [CreateAssetMenu(menuName = "BaseGames/Quest/Objective/Defeat")] public class DefeatEnemyObjective : QuestObjectiveSO { [Tooltip("目标敌人的唯一 ID,需与敌人 SO 或敌人组件上的 enemyId 保持一致。")] public string targetEnemyId; [Tooltip("需击败的次数,默认 1。")] [Min(1)] public int defeatCount = 1; public override string BadgeLabel => "[击败]"; public override int GetRequiredCount() => defeatCount; public override bool EvaluateCompletion(QuestObjectiveState s) => s.progressCount >= defeatCount; public override bool TryHandleEvent(QuestEventType eventType, string payload, QuestObjectiveState state) { if (eventType != QuestEventType.EnemyDefeated) return false; if (payload != targetEnemyId) return false; state.progressCount++; return true; } } /// 收集指定 ID 的物品若干件。 [CreateAssetMenu(menuName = "BaseGames/Quest/Objective/Collect")] public class CollectItemObjective : QuestObjectiveSO { [Tooltip("目标物品的唯一 ID,需与拾取事件广播的 itemId 保持一致。")] public string itemId; [Tooltip("需收集的数量,默认 1。")] [Min(1)] public int collectCount = 1; public override string BadgeLabel => "[收集]"; public override int GetRequiredCount() => collectCount; public override bool EvaluateCompletion(QuestObjectiveState s) => s.progressCount >= collectCount; public override bool TryHandleEvent(QuestEventType eventType, string payload, QuestObjectiveState state) { if (eventType != QuestEventType.ItemCollected) return false; if (payload != itemId) return false; state.progressCount++; return true; } } /// 到达指定场景/区域标记点后完成。 [CreateAssetMenu(menuName = "BaseGames/Quest/Objective/Reach")] public class ReachAreaObjective : QuestObjectiveSO { [Tooltip("需到达的场景名(Unity Build Settings 中的场景名称)。留空时仅依赖 markerTag 判定。")] public string sceneName; [Tooltip("场景内的区域标记 Tag(与 TriggerZone.markerTag 保持一致)。\n" + "非空时:玩家进入挂有对应 markerTag 的 TriggerZone 碰撞体即触发。\n" + "留空时:整个场景切换即触发(粗粒度,仅检查 sceneName)。")] public string markerTag; public override string BadgeLabel => "[到达]"; public override bool EvaluateCompletion(QuestObjectiveState s) => s.progressCount >= 1; public override bool TryHandleEvent(QuestEventType eventType, string payload, QuestObjectiveState state) { // 精确区域到达:TriggerZone 广播 AreaReached,payload = markerTag if (eventType == QuestEventType.AreaReached && !string.IsNullOrEmpty(markerTag) && payload == markerTag) { state.progressCount++; return true; } // 场景级到达:payload = sceneName,仅当 markerTag 为空时生效(否则等待精确触发) if (eventType == QuestEventType.SceneLoaded && !string.IsNullOrEmpty(sceneName) && payload == sceneName && string.IsNullOrEmpty(markerTag)) { state.progressCount++; return true; } return false; } } /// 使用指定能力若干次后完成。 [CreateAssetMenu(menuName = "BaseGames/Quest/Objective/UseSkill")] public class UseSkillObjective : QuestObjectiveSO { [Tooltip("目标能力类型。事件频道 EVT_SkillUsed 广播 AbilityType.ToString(),与此值匹配时计数。")] public AbilityType requiredAbility; [Tooltip("需使用的次数,默认 1。")] [Min(1)] public int useCount = 1; public override string BadgeLabel => "[使用]"; public override int GetRequiredCount() => useCount; public override bool EvaluateCompletion(QuestObjectiveState s) => s.progressCount >= useCount; public override bool TryHandleEvent(QuestEventType eventType, string payload, QuestObjectiveState state) { if (eventType != QuestEventType.SkillUsed) return false; // Enum.TryParse 避免大小写/格式差异导致静默失败 if (!System.Enum.TryParse(payload, ignoreCase: true, out var parsed)) return false; if (parsed != requiredAbility) return false; state.progressCount++; return true; } } // ── 扩展事件频道绑定(供 QuestManager Inspector 使用)──────────────────── /// /// 自定义事件频道与任务事件类型的绑定。 /// 在 QuestManager Inspector 的"扩展事件频道"数组中添加条目,即可不修改代码 /// 支持未来新增的 枚举值与 SO 频道的映射。 /// [System.Serializable] public struct QuestEventChannelBinding { [Tooltip("要监听的事件类型(需与 QuestObjectiveSO 子类中 TryHandleEvent 处理的类型一致)。")] public QuestEventType eventType; [Tooltip("该类型对应的 StringEventChannelSO 资产。由广播方(如战斗系统、场景系统)负责 Raise。")] public StringEventChannelSO channel; } }