feat: Add WorldStateFlagAttribute and custom property drawer for enhanced dialogue management
- Implemented WorldStateFlagAttribute to mark string fields as world state flags. - Created NarrativeNPCEditor for custom inspector to visualize dialogue version activation states. - Developed WorldStateFlagDrawer to provide dropdown menu for known flags in the inspector. - Introduced ActorModule for managing DialogueActorSO assets, including viewing, creating, and deleting actors. - Added DialogueModule for managing DialogueSequenceSO assets with detailed previews and action bars. - Established QuestModule for managing QuestSO assets, including objectives and branches. - Implemented QuestManagerPostprocessor to automatically refresh QuestManager's quest list on asset changes.
This commit is contained in:
@@ -27,7 +27,7 @@ namespace BaseGames.Quest
|
||||
protected override void Interact_Internal(Transform player)
|
||||
{
|
||||
var qm = SL.GetOrDefault<IQuestManager>();
|
||||
var quest = GetCurrentQuest(qm);
|
||||
var quest = GetCurrentOrCompletedQuest(qm);
|
||||
if (quest == null || qm == null) return;
|
||||
|
||||
var state = qm.GetState(quest.questId);
|
||||
@@ -36,7 +36,7 @@ namespace BaseGames.Quest
|
||||
{
|
||||
qm.AcceptQuest(quest.questId);
|
||||
}
|
||||
else if (qm.IsReadyToComplete(quest.questId))
|
||||
else if (state == QuestStateEnum.Active && qm.IsReadyToComplete(quest.questId))
|
||||
{
|
||||
// 直接从 player 获取 PlayerStats,避免对 PlayerController 的程序集依赖
|
||||
var stats = player.GetComponentInParent<PlayerStats>();
|
||||
@@ -47,7 +47,7 @@ namespace BaseGames.Quest
|
||||
protected override DialogueSequenceSO GetCurrentDialogue()
|
||||
{
|
||||
var qm = SL.GetOrDefault<IQuestManager>();
|
||||
var quest = GetCurrentQuest(qm);
|
||||
var quest = GetCurrentOrCompletedQuest(qm);
|
||||
if (quest == null || qm == null) return base.GetCurrentDialogue();
|
||||
|
||||
var state = qm.GetState(quest.questId);
|
||||
@@ -64,19 +64,25 @@ namespace BaseGames.Quest
|
||||
|
||||
// ── 私有辅助 ─────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>返回当前处于 Available 或 Active 状态的第一个任务。</summary>
|
||||
private QuestSO GetCurrentQuest(IQuestManager qm = null)
|
||||
/// <summary>
|
||||
/// 返回当前处于 Available 或 Active 状态的第一个任务;
|
||||
/// 若全部已完成,返回最后一个已完成任务(用于显示 completedDialogue)。
|
||||
/// </summary>
|
||||
private QuestSO GetCurrentOrCompletedQuest(IQuestManager qm = null)
|
||||
{
|
||||
if (_offeredQuests == null) return null;
|
||||
qm ??= SL.GetOrDefault<IQuestManager>();
|
||||
if (qm == null) return null;
|
||||
|
||||
QuestSO lastCompleted = null;
|
||||
foreach (var q in _offeredQuests)
|
||||
{
|
||||
if (q == null) continue;
|
||||
var s = qm.GetState(q.questId);
|
||||
if (s == QuestStateEnum.Available || s == QuestStateEnum.Active) return q;
|
||||
if (s == QuestStateEnum.Completed) lastCompleted = q;
|
||||
}
|
||||
return null;
|
||||
return lastCompleted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,17 +11,23 @@ namespace BaseGames.Quest
|
||||
/// 挂在 Persistent 场景 [GameManagers] 下。
|
||||
/// 事件驱动追踪目标进度,不主动轮询。
|
||||
/// 实现 ISaveable,通过 SaveManager 持久化任务状态。
|
||||
///
|
||||
/// _allQuests 由编辑器 OnValidate / "刷新任务列表" 右键菜单自动填充,
|
||||
/// 无需策划人员手动拖入 ScriptableObject。
|
||||
/// </summary>
|
||||
public class QuestManager : MonoBehaviour, ISaveable, IQuestManager
|
||||
{
|
||||
// ── Inspector ────────────────────────────────────────────────────────
|
||||
[SerializeField] private QuestSO[] _allQuests;
|
||||
|
||||
[Tooltip("所有 QuestSO 资产。编辑器会自动同步,无需手动维护。")]
|
||||
[SerializeField] private QuestSO[] _allQuests;
|
||||
|
||||
[Header("Event Channels(监听)")]
|
||||
[SerializeField] private StringEventChannelSO _onEnemyDied; // EVT_EnemyDied(enemyId)
|
||||
[SerializeField] private StringEventChannelSO _onCollectiblePickup; // EVT_CollectiblePickup(itemId)
|
||||
[SerializeField] private StringEventChannelSO _onSceneLoaded; // EVT_SceneLoaded(sceneName)
|
||||
[SerializeField] private StringEventChannelSO _onNpcDialogueCompleted; // EVT_NpcDialogueCompleted(npcId)
|
||||
[SerializeField] private StringEventChannelSO _onCollectiblePickup; // EVT_CollectiblePickup(itemId)
|
||||
[SerializeField] private StringEventChannelSO _onSceneLoaded; // EVT_SceneLoaded(sceneName)
|
||||
[SerializeField] private StringEventChannelSO _onNpcDialogueCompleted; // EVT_NpcDialogueCompleted(npcId)
|
||||
[SerializeField] private StringEventChannelSO _onSkillUsed; // EVT_SkillUsed(abilityType.ToString())
|
||||
|
||||
[Header("Event Channels(广播)")]
|
||||
[SerializeField] private StringEventChannelSO _onQuestStarted; // questId
|
||||
@@ -52,14 +58,35 @@ namespace BaseGames.Quest
|
||||
foreach (var q in _allQuests)
|
||||
if (q != null && !string.IsNullOrEmpty(q.questId))
|
||||
_questIndex[q.questId] = q;
|
||||
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||
ValidateQuestIds();
|
||||
#endif
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||
private void ValidateQuestIds()
|
||||
{
|
||||
if (_allQuests == null) return;
|
||||
var seen = new HashSet<string>(System.StringComparer.Ordinal);
|
||||
foreach (var q in _allQuests)
|
||||
{
|
||||
if (q == null) continue;
|
||||
if (string.IsNullOrEmpty(q.questId))
|
||||
Debug.LogError($"[QuestManager] QuestSO '{q.name}' 缺少 questId,此任务将无法被引用。", q);
|
||||
else if (!seen.Add(q.questId))
|
||||
Debug.LogError($"[QuestManager] 重复的 questId '{q.questId}'(资产:{q.name}),将导致任务系统异常。", q);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
_onEnemyDied?.Subscribe(HandleEnemyDefeated).AddTo(_subs);
|
||||
_onCollectiblePickup?.Subscribe(HandleItemCollected).AddTo(_subs);
|
||||
_onSceneLoaded?.Subscribe(HandleSceneLoaded).AddTo(_subs);
|
||||
_onNpcDialogueCompleted?.Subscribe(HandleNpcDialogue).AddTo(_subs);
|
||||
_onSkillUsed?.Subscribe(HandleSkillUsed).AddTo(_subs);
|
||||
BaseGames.Core.ServiceLocator.GetOrDefault<BaseGames.Core.Save.ISaveableRegistry>()?.Register(this);
|
||||
}
|
||||
|
||||
@@ -94,16 +121,17 @@ namespace BaseGames.Quest
|
||||
_onQuestCompleted?.Raise(questId);
|
||||
|
||||
// 解锁后续任务(分支)
|
||||
// conditionQuest == null 表示默认分支,conditionQuest != null 则要求该任务已完成。
|
||||
// 不 break —— 允许同时解锁多个后续任务(如完成任务后同时开放多条支线)。
|
||||
if (quest.branches != null)
|
||||
{
|
||||
foreach (var branch in quest.branches)
|
||||
{
|
||||
if (string.IsNullOrEmpty(branch.conditionQuestId) ||
|
||||
GetState(branch.conditionQuestId) == QuestStateEnum.Completed)
|
||||
if (branch.conditionQuest == null ||
|
||||
GetState(branch.conditionQuest.questId) == QuestStateEnum.Completed)
|
||||
{
|
||||
if (branch.nextQuest != null)
|
||||
_questStates[branch.nextQuest.questId] = QuestStateEnum.Available;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,9 +161,8 @@ namespace BaseGames.Quest
|
||||
{
|
||||
data.Quests.QuestStates[id] = new BaseGames.Core.Save.QuestState
|
||||
{
|
||||
Status = state.ToString(),
|
||||
ObjectiveIndex = 0,
|
||||
ProgressCounts = BuildProgressList(id),
|
||||
Status = state.ToString(),
|
||||
ProgressCounts = BuildProgressList(id),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -171,24 +198,36 @@ namespace BaseGames.Quest
|
||||
{
|
||||
if (GetState(questId) != QuestStateEnum.Available) return false;
|
||||
var quest = GetQuestSO(questId);
|
||||
if (quest?.prerequisiteQuestIds == null) return true;
|
||||
foreach (var pre in quest.prerequisiteQuestIds)
|
||||
if (GetState(pre) != QuestStateEnum.Completed) return false;
|
||||
if (quest?.prerequisiteQuests == null) return true;
|
||||
foreach (var pre in quest.prerequisiteQuests)
|
||||
{
|
||||
if (pre == null) continue;
|
||||
if (GetState(pre.questId) != QuestStateEnum.Completed) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool IsObjectiveComplete(QuestObjectiveSO obj)
|
||||
{
|
||||
_objectiveStates.TryGetValue(obj.objectiveId, out var s);
|
||||
s ??= new QuestObjectiveState();
|
||||
return obj.EvaluateCompletion(s);
|
||||
if (!_objectiveStates.TryGetValue(obj.objectiveId, out var s))
|
||||
s = new QuestObjectiveState();
|
||||
|
||||
bool result = obj.EvaluateCompletion(s);
|
||||
|
||||
// 首次达成时写回 completed 标志,避免 s 是本地临时对象时标志丢失
|
||||
if (result && !s.completed)
|
||||
{
|
||||
s.completed = true;
|
||||
_objectiveStates[obj.objectiveId] = s;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<int> BuildProgressList(string questId)
|
||||
{
|
||||
var list = new List<int>();
|
||||
var quest = GetQuestSO(questId);
|
||||
if (quest?.objectives == null) return list;
|
||||
if (quest?.objectives == null) return new List<int>(0);
|
||||
var list = new List<int>(quest.objectives.Length);
|
||||
foreach (var obj in quest.objectives)
|
||||
{
|
||||
_objectiveStates.TryGetValue(obj?.objectiveId ?? string.Empty, out var os);
|
||||
@@ -235,6 +274,17 @@ namespace BaseGames.Quest
|
||||
});
|
||||
}
|
||||
|
||||
private void HandleSkillUsed(string abilityTypeName)
|
||||
{
|
||||
// 用 Enum.TryParse 避免大小写/ToString 格式差异导致静默失败
|
||||
if (!System.Enum.TryParse<AbilityType>(abilityTypeName, ignoreCase: true, out var parsed)) return;
|
||||
ForEachActiveObjective<UseSkillObjective>(obj =>
|
||||
{
|
||||
if (obj.requiredAbility == parsed)
|
||||
IncrementProgress(obj.objectiveId);
|
||||
});
|
||||
}
|
||||
|
||||
private void ForEachActiveObjective<T>(System.Action<T> action) where T : QuestObjectiveSO
|
||||
{
|
||||
foreach (var (qid, state) in _questStates)
|
||||
@@ -263,5 +313,94 @@ namespace BaseGames.Quest
|
||||
|
||||
private QuestSO GetQuestSO(string id)
|
||||
=> _questIndex != null && _questIndex.TryGetValue(id, out var q) ? q : null;
|
||||
|
||||
// ── 编辑器自动维护 ────────────────────────────────────────────────────
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>
|
||||
/// 编辑器中每次属性变更时自动同步项目内所有 QuestSO,并校验前置任务是否存在循环引用。
|
||||
/// </summary>
|
||||
private void OnValidate()
|
||||
{
|
||||
EditorRefreshQuestList();
|
||||
ValidatePrerequisites();
|
||||
}
|
||||
|
||||
[ContextMenu("刷新任务列表")]
|
||||
private void EditorRefreshQuestList()
|
||||
{
|
||||
string[] guids = UnityEditor.AssetDatabase.FindAssets("t:QuestSO");
|
||||
var list = new List<QuestSO>(guids.Length);
|
||||
foreach (var guid in guids)
|
||||
{
|
||||
string path = UnityEditor.AssetDatabase.GUIDToAssetPath(guid);
|
||||
var q = UnityEditor.AssetDatabase.LoadAssetAtPath<QuestSO>(path);
|
||||
if (q != null) list.Add(q);
|
||||
}
|
||||
_allQuests = list.ToArray();
|
||||
UnityEditor.EditorUtility.SetDirty(this);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||
/// <summary>
|
||||
/// 通过 DFS 后序遍历检测 prerequisiteQuests 中是否存在循环引用。
|
||||
/// 在编辑器 OnValidate 及开发构建 Awake 时调用,发现问题立即打 LogError。
|
||||
/// </summary>
|
||||
[System.Diagnostics.Conditional("UNITY_EDITOR")] // ContextMenu 只在编辑器生效
|
||||
[UnityEngine.ContextMenu("校验前置任务循环引用")]
|
||||
private void ValidatePrerequisites()
|
||||
{
|
||||
if (_allQuests == null) return;
|
||||
|
||||
// 先建立 id → SO 快速查找表
|
||||
var index = new Dictionary<string, QuestSO>(_allQuests.Length);
|
||||
foreach (var q in _allQuests)
|
||||
{
|
||||
if (q == null || string.IsNullOrEmpty(q.questId)) continue;
|
||||
index[q.questId] = q;
|
||||
}
|
||||
|
||||
// 三色 DFS:0=未访问 1=灰(栈中)2=黑(已完成)
|
||||
var color = new Dictionary<string, int>(index.Count);
|
||||
|
||||
bool HasCycle(string startId, List<string> path)
|
||||
{
|
||||
if (!index.ContainsKey(startId)) return false;
|
||||
if (color.TryGetValue(startId, out int c))
|
||||
{
|
||||
if (c == 1)
|
||||
{
|
||||
path.Add(startId);
|
||||
Debug.LogError(
|
||||
$"[QuestManager] 前置任务循环引用: {string.Join(" → ", path)}",
|
||||
this);
|
||||
return true;
|
||||
}
|
||||
return false; // 已完成
|
||||
}
|
||||
|
||||
color[startId] = 1;
|
||||
path.Add(startId);
|
||||
|
||||
var prereqs = index[startId].prerequisiteQuests;
|
||||
if (prereqs != null)
|
||||
{
|
||||
foreach (var pre in prereqs)
|
||||
{
|
||||
if (pre == null || string.IsNullOrEmpty(pre.questId)) continue;
|
||||
if (HasCycle(pre.questId, path)) return true;
|
||||
}
|
||||
}
|
||||
|
||||
path.RemoveAt(path.Count - 1);
|
||||
color[startId] = 2;
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var questId in index.Keys)
|
||||
HasCycle(questId, new List<string>());
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,13 +11,32 @@ namespace BaseGames.Quest
|
||||
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; // 本地化 key(通过 LocalizationManager.Get(displayTextKey, "Quest") 显示)
|
||||
public bool IsOptional; // 可选目标(完成加奖励但不阻塞任务)
|
||||
public string displayTextKey;
|
||||
[Tooltip("勾选后此目标为可选项:完成可获奖励,但不阻塞任务交接。")]
|
||||
public bool IsOptional;
|
||||
|
||||
/// <summary>根据当前进度判断目标是否完成。</summary>
|
||||
public abstract bool EvaluateCompletion(QuestObjectiveState state);
|
||||
|
||||
/// <summary>
|
||||
/// 在 DataHub / 编辑器工具中显示的类型徽章文字。
|
||||
/// 子类应 override 返回简洁中文标签(如 "[对话]")。
|
||||
/// 避免工具代码中使用 type-switch 维护列表。
|
||||
/// </summary>
|
||||
public virtual string BadgeLabel => "[目标]";
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private void OnValidate()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(objectiveId)) return;
|
||||
objectiveId = name;
|
||||
UnityEditor.EditorUtility.SetDirty(this);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// ── 运行时目标进度状态(由 QuestManager 管理,不继承 SO)────────────────
|
||||
@@ -34,8 +53,9 @@ namespace BaseGames.Quest
|
||||
[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;
|
||||
}
|
||||
|
||||
@@ -43,9 +63,11 @@ namespace BaseGames.Quest
|
||||
[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 bool EvaluateCompletion(QuestObjectiveState s) => s.progressCount >= defeatCount;
|
||||
}
|
||||
|
||||
@@ -53,9 +75,11 @@ namespace BaseGames.Quest
|
||||
[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 bool EvaluateCompletion(QuestObjectiveState s) => s.progressCount >= collectCount;
|
||||
}
|
||||
|
||||
@@ -63,9 +87,11 @@ namespace BaseGames.Quest
|
||||
[CreateAssetMenu(menuName = "BaseGames/Quest/Objective/Reach")]
|
||||
public class ReachAreaObjective : QuestObjectiveSO
|
||||
{
|
||||
public string sceneName; // 需到达的场景
|
||||
public string markerTag; // 场景内的目标标记 Tag(预留)
|
||||
|
||||
[Tooltip("需到达的场景名(Unity Build Settings 中的场景名称)。")]
|
||||
public string sceneName;
|
||||
[Tooltip("场景内的目标标记 Tag(预留字段,当前未启用)。")]
|
||||
public string markerTag;
|
||||
public override string BadgeLabel => "[到达]";
|
||||
public override bool EvaluateCompletion(QuestObjectiveState s) => s.progressCount >= 1;
|
||||
}
|
||||
|
||||
@@ -73,9 +99,11 @@ namespace BaseGames.Quest
|
||||
[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 bool EvaluateCompletion(QuestObjectiveState s) => s.progressCount >= useCount;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,17 +13,19 @@ namespace BaseGames.Quest
|
||||
{
|
||||
[Header("标识")]
|
||||
public string questId; // 唯一 ID,如 "Quest_FindMushroom"
|
||||
public string displayName;
|
||||
[TextArea(2, 6)]
|
||||
public string description;
|
||||
|
||||
[Tooltip("本地化 Key,格式如 \"Quest_FindMushroom_Name\"。通过 LocalizationManager.Get(displayNameKey, \"Quest\") 显示。")]
|
||||
public string displayNameKey;
|
||||
[Tooltip("本地化 Key,格式如 \"Quest_FindMushroom_Desc\"。通过 LocalizationManager.Get(descriptionKey, \"Quest\") 显示。")]
|
||||
public string descriptionKey;
|
||||
public Sprite icon;
|
||||
|
||||
[Header("目标链")]
|
||||
public QuestObjectiveSO[] objectives; // 按顺序完成,全部完成 = 可交完
|
||||
|
||||
[Header("前置条件")]
|
||||
public string[] prerequisiteQuestIds; // 所有前置任务 Completed 后才可接
|
||||
public int minAffinityToAccept; // NPC 好感度门槛(0 = 无限制)
|
||||
public QuestSO[] prerequisiteQuests; // 所有前置任务 Completed 后才可接
|
||||
public int minAffinityToAccept; // NPC 好感度门槛(0 = 无限制)
|
||||
|
||||
[Header("奖励")]
|
||||
public RewardSO reward;
|
||||
@@ -34,13 +36,91 @@ namespace BaseGames.Quest
|
||||
|
||||
[Header("完成后续任务(分支)")]
|
||||
public QuestBranch[] branches;
|
||||
|
||||
// ── 编辑器校验 ────────────────────────────────────────────────────────
|
||||
#if UNITY_EDITOR
|
||||
// questId → 资产路径,5 秒 TTL,跨所有 QuestSO.OnValidate 共用。
|
||||
// 重复检测时只需将缓存路径与自身路径比对(O(1)),无需全量扫描。
|
||||
private static System.Collections.Generic.Dictionary<string, string> s_questIdToPath;
|
||||
private static double s_questIdsCacheTime = -10.0;
|
||||
|
||||
private static System.Collections.Generic.Dictionary<string, string> GetQuestIdCache()
|
||||
{
|
||||
double now = UnityEditor.EditorApplication.timeSinceStartup;
|
||||
if (s_questIdToPath != null && now - s_questIdsCacheTime < 5.0)
|
||||
return s_questIdToPath;
|
||||
|
||||
s_questIdToPath = new System.Collections.Generic.Dictionary<string, string>(System.StringComparer.Ordinal);
|
||||
string[] guids = UnityEditor.AssetDatabase.FindAssets("t:QuestSO");
|
||||
foreach (var guid in guids)
|
||||
{
|
||||
var path = UnityEditor.AssetDatabase.GUIDToAssetPath(guid);
|
||||
var q = UnityEditor.AssetDatabase.LoadAssetAtPath<QuestSO>(path);
|
||||
if (q != null && !string.IsNullOrEmpty(q.questId) && !s_questIdToPath.ContainsKey(q.questId))
|
||||
s_questIdToPath[q.questId] = path;
|
||||
}
|
||||
s_questIdsCacheTime = now;
|
||||
return s_questIdToPath;
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(questId))
|
||||
{
|
||||
Debug.LogWarning($"[QuestSO] '{name}' 缺少 questId,保存前请填写。", this);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检测重复 questId:缓存路径 vs 自身路径比对(O(1)),5 秒内无需重扫。
|
||||
var cache = GetQuestIdCache();
|
||||
string myPath = UnityEditor.AssetDatabase.GetAssetPath(this);
|
||||
if (!string.IsNullOrEmpty(myPath) &&
|
||||
cache.TryGetValue(questId, out var existingPath) &&
|
||||
existingPath != myPath)
|
||||
{
|
||||
Debug.LogError(
|
||||
$"[QuestSO] questId '{questId}' 与 " +
|
||||
$"'{System.IO.Path.GetFileNameWithoutExtension(existingPath)}' 重复!请修改其中一个。", this);
|
||||
s_questIdsCacheTime = -10.0;
|
||||
}
|
||||
|
||||
ValidateBranchDialogueKeys();
|
||||
}
|
||||
|
||||
private void ValidateBranchDialogueKeys()
|
||||
{
|
||||
if (branches == null || branches.Length == 0) return;
|
||||
|
||||
foreach (var branch in branches)
|
||||
{
|
||||
if (branch == null) continue;
|
||||
|
||||
// npcDialogueSequence 是 SO 直接引用,无需字符串校验。
|
||||
// 旧字段 npcDialogueKey(Obsolete)有值时提示迁移。
|
||||
#pragma warning disable CS0618
|
||||
if (!string.IsNullOrEmpty(branch.npcDialogueKey) && branch.npcDialogueSequence == null)
|
||||
{
|
||||
Debug.LogWarning(
|
||||
$"[QuestSO] '{name}' 分支仍使用旧字段 npcDialogueKey='{branch.npcDialogueKey}'," +
|
||||
"请迁移至 npcDialogueSequence(直接拖入 DialogueSequenceSO)。", this);
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class QuestBranch
|
||||
{
|
||||
public string conditionQuestId; // 若此任务已完成 → 走本分支(空 = 默认)
|
||||
public QuestSO nextQuest;
|
||||
public string npcDialogueKey; // 触发 NPC 对话 key
|
||||
/// <summary>若此前置任务已完成 → 走本分支(null = 默认分支)。</summary>
|
||||
public QuestSO conditionQuest;
|
||||
public QuestSO nextQuest;
|
||||
/// <summary>完成后触发的 NPC 对话序列(直接引用,避免手写 sequenceId 字符串出错)。</summary>
|
||||
public DialogueSequenceSO npcDialogueSequence;
|
||||
|
||||
[System.Obsolete("已废弃,请改用 npcDialogueSequence(直接 SO 引用)。保留字段以兼容现有资产序列化。")]
|
||||
[HideInInspector]
|
||||
public string npcDialogueKey;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user