feat: Round 49 narrative systems improvements
QuestManager: extract CheckQuestDepsAndFlags shared method, simplify GetQuestLockInfo/CanAccept/MeetsPrerequisites; add GetQuestsInState+FilterQuests implementations; fix extra brace compile bug; add _pauseTimestamps logging; use actualDelta in ApplyAffinity event. QuestSO: add depth>32 guard to HasPrerequisiteCycle and HasBranchCycle to prevent editor freeze on deep chains. EventChainModule: replace FindObjectOfType with ServiceLocator.GetOrDefault in ForceExecute; add self-trigger flag detection (check 6) in ValidateAllChains using reflection. DialogueVariantPreviewWindow: add matrix analysis section enumerating all 2^N flag combinations (N<=10) with table showing winning variant per combination. WorldStateRegistry: LoadFromSave null guard on data.World sub-collections (P0 fix). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -23,6 +23,7 @@ namespace BaseGames.Editor.Dialogue
|
||||
private ObjectField _targetField;
|
||||
private VisualElement _flagContainer;
|
||||
private VisualElement _resultContainer;
|
||||
private VisualElement _matrixContainer;
|
||||
|
||||
private static readonly Color ColWin = new(0.20f, 0.75f, 0.35f, 1f);
|
||||
private static readonly Color ColFail = new(0.55f, 0.55f, 0.55f, 1f);
|
||||
@@ -106,6 +107,20 @@ namespace BaseGames.Editor.Dialogue
|
||||
_resultContainer = new VisualElement();
|
||||
scrollView.Add(_resultContainer);
|
||||
|
||||
rootVisualElement.Add(MakeDivider());
|
||||
|
||||
// ── 矩阵分析区 ──
|
||||
var matrixFoldout = new Foldout { text = "矩阵分析(所有标志组合 → 胜出变体)", value = false };
|
||||
matrixFoldout.style.marginTop = 4;
|
||||
rootVisualElement.Add(matrixFoldout);
|
||||
|
||||
_matrixContainer = new VisualElement();
|
||||
matrixFoldout.Add(_matrixContainer);
|
||||
|
||||
var matrixBtn = new Button(() => RebuildMatrix()) { text = "矩阵分析" };
|
||||
matrixBtn.style.marginBottom = 4;
|
||||
matrixFoldout.Add(matrixBtn);
|
||||
|
||||
Rebuild();
|
||||
}
|
||||
|
||||
@@ -364,6 +379,119 @@ namespace BaseGames.Editor.Dialogue
|
||||
public bool HasFlag(string key) => _flags.Contains(key);
|
||||
}
|
||||
|
||||
// ── 矩阵分析 ─────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 枚举全部 2^N 标志组合(N ≤ 10),以表格形式展示每种组合下胜出的变体索引。
|
||||
/// N > 10 时显示提示,建议手动筛选标志后分析。
|
||||
/// </summary>
|
||||
private void RebuildMatrix()
|
||||
{
|
||||
if (_matrixContainer == null) return;
|
||||
_matrixContainer.Clear();
|
||||
|
||||
if (_target == null || _target.variants == null || _target.variants.Length == 0)
|
||||
{
|
||||
_matrixContainer.Add(new Label("(无可分析的变体)") { style = { opacity = 0.5f, fontSize = 11 } });
|
||||
return;
|
||||
}
|
||||
|
||||
var matrixFlags = _allFlags.Count > 0 ? _allFlags : new List<string>();
|
||||
if (matrixFlags.Count == 0)
|
||||
{
|
||||
_matrixContainer.Add(new Label("(变体不使用任何 requiredFlags,无需矩阵分析)") { style = { opacity = 0.5f, fontSize = 11 } });
|
||||
return;
|
||||
}
|
||||
|
||||
const int MaxFlags = 10;
|
||||
if (matrixFlags.Count > MaxFlags)
|
||||
{
|
||||
var warn = new Label($"⚠ 标志数量 ({matrixFlags.Count}) 超过 {MaxFlags},枚举 2^N 组合代价过高。请在上方取消勾选不关心的标志后重新点击「矩阵分析」。");
|
||||
warn.style.fontSize = 11;
|
||||
warn.style.color = new StyleColor(new Color(0.9f, 0.7f, 0.2f));
|
||||
warn.style.whiteSpace = WhiteSpace.Normal;
|
||||
_matrixContainer.Add(warn);
|
||||
return;
|
||||
}
|
||||
|
||||
int n = matrixFlags.Count;
|
||||
int combos = 1 << n; // 2^n
|
||||
|
||||
// ── 表头 ──
|
||||
var headerRow = MakeMatrixRow(isHeader: true);
|
||||
for (int ci = 0; ci < n; ci++)
|
||||
{
|
||||
var cell = MakeMatrixCell(matrixFlags[ci], isHeader: true);
|
||||
cell.style.minWidth = 90;
|
||||
headerRow.Add(cell);
|
||||
}
|
||||
headerRow.Add(MakeMatrixCell("胜出变体", isHeader: true));
|
||||
_matrixContainer.Add(headerRow);
|
||||
|
||||
// ── 数据行 ──
|
||||
for (int mask = 0; mask < combos; mask++)
|
||||
{
|
||||
var combo = new HashSet<string>(System.StringComparer.Ordinal);
|
||||
for (int bit = 0; bit < n; bit++)
|
||||
if ((mask & (1 << bit)) != 0) combo.Add(matrixFlags[bit]);
|
||||
|
||||
// 求胜出变体
|
||||
var mockReader = new MockFlagReader(combo);
|
||||
int winner = -1;
|
||||
for (int vi = 0; vi < _target.variants.Length; vi++)
|
||||
if (_target.CheckVariant(_target.variants[vi], mockReader)) { winner = vi; break; }
|
||||
|
||||
string winnerText = winner >= 0
|
||||
? $"变体 {winner}" +
|
||||
(_target.variants[winner].sequence != null
|
||||
? $"\n({_target.variants[winner].sequence.name})"
|
||||
: "(无序列)")
|
||||
: "默认台词";
|
||||
|
||||
var dataRow = MakeMatrixRow(isHeader: false);
|
||||
// 标志列
|
||||
for (int ci = 0; ci < n; ci++)
|
||||
{
|
||||
bool on = (mask & (1 << ci)) != 0;
|
||||
var cell = MakeMatrixCell(on ? "✓" : "–", isHeader: false);
|
||||
cell.style.color = new StyleColor(on ? ColWin : ColFail);
|
||||
cell.style.minWidth = 90;
|
||||
dataRow.Add(cell);
|
||||
}
|
||||
// 胜出列
|
||||
var winCell = MakeMatrixCell(winnerText, isHeader: false);
|
||||
winCell.style.color = new StyleColor(winner >= 0 ? ColWin : new Color(0.5f, 0.5f, 0.5f));
|
||||
dataRow.Add(winCell);
|
||||
|
||||
_matrixContainer.Add(dataRow);
|
||||
}
|
||||
}
|
||||
|
||||
private static VisualElement MakeMatrixRow(bool isHeader)
|
||||
{
|
||||
var row = new VisualElement();
|
||||
row.style.flexDirection = FlexDirection.Row;
|
||||
row.style.borderBottomWidth = 1;
|
||||
row.style.borderBottomColor = new StyleColor(new Color(0.3f, 0.3f, 0.3f, 0.5f));
|
||||
if (isHeader)
|
||||
row.style.backgroundColor = new StyleColor(new Color(0.22f, 0.22f, 0.28f, 1f));
|
||||
return row;
|
||||
}
|
||||
|
||||
private static Label MakeMatrixCell(string text, bool isHeader)
|
||||
{
|
||||
var lbl = new Label(text);
|
||||
lbl.style.fontSize = isHeader ? 10 : 10;
|
||||
lbl.style.unityFontStyleAndWeight = isHeader ? FontStyle.Bold : FontStyle.Normal;
|
||||
lbl.style.paddingLeft = 4;
|
||||
lbl.style.paddingRight = 4;
|
||||
lbl.style.paddingTop = 3;
|
||||
lbl.style.paddingBottom = 3;
|
||||
lbl.style.whiteSpace = WhiteSpace.Normal;
|
||||
lbl.style.width = 80;
|
||||
return lbl;
|
||||
}
|
||||
|
||||
// ── 辅助 ─────────────────────────────────────────────────────────────
|
||||
|
||||
private static VisualElement MakeDivider()
|
||||
|
||||
@@ -431,10 +431,10 @@ namespace BaseGames.Editor.Modules
|
||||
var capturedC = c;
|
||||
var forceBtn = new Button(() =>
|
||||
{
|
||||
var mgr = UnityEngine.Object.FindObjectOfType<EventChainManager>();
|
||||
var mgr = BaseGames.Core.ServiceLocator.GetOrDefault<EventChainManager>();
|
||||
if (mgr == null)
|
||||
{
|
||||
Debug.LogWarning("[EventChainModule] 场景中未找到 EventChainManager,无法强制触发。");
|
||||
Debug.LogWarning("[EventChainModule] ServiceLocator 中未找到 EventChainManager,无法强制触发。请确认场景中已挂载并注册。");
|
||||
return;
|
||||
}
|
||||
Debug.Log($"[EventChainModule] 强制触发链:{capturedC.chainId}");
|
||||
@@ -519,6 +519,48 @@ namespace BaseGames.Editor.Modules
|
||||
AddError($"{c.chainId}: actions[{i}] 为 null,运行时将触发 NullReferenceException。", c);
|
||||
}
|
||||
|
||||
// ⑥ 自触发检测:某条件检查的标志由同一链的 Action 写入(可能造成链被自身条件阻断或无限反复触发)
|
||||
var flagFieldFlags = System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance;
|
||||
foreach (var c in allChains)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(c.chainId)) continue;
|
||||
var writtenFlags = new System.Collections.Generic.HashSet<string>(StringComparer.Ordinal);
|
||||
var readFlags = new System.Collections.Generic.HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
if (c.actions != null)
|
||||
foreach (var action in c.actions)
|
||||
{
|
||||
if (action == null) continue;
|
||||
foreach (var field in action.GetType().GetFields(flagFieldFlags))
|
||||
if ((field.Name.Contains("flag", System.StringComparison.OrdinalIgnoreCase) ||
|
||||
field.Name.Contains("Flag", System.StringComparison.OrdinalIgnoreCase))
|
||||
&& field.FieldType == typeof(string))
|
||||
{
|
||||
var val = field.GetValue(action) as string;
|
||||
if (!string.IsNullOrEmpty(val)) writtenFlags.Add(val);
|
||||
}
|
||||
}
|
||||
|
||||
if (c.conditions != null)
|
||||
foreach (var cond in c.conditions)
|
||||
{
|
||||
if (cond == null) continue;
|
||||
foreach (var field in cond.GetType().GetFields(flagFieldFlags))
|
||||
if ((field.Name.Contains("flag", System.StringComparison.OrdinalIgnoreCase) ||
|
||||
field.Name.Contains("Flag", System.StringComparison.OrdinalIgnoreCase))
|
||||
&& field.FieldType == typeof(string))
|
||||
{
|
||||
var val = field.GetValue(cond) as string;
|
||||
if (!string.IsNullOrEmpty(val)) readFlags.Add(val);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var flagId in readFlags)
|
||||
if (writtenFlags.Contains(flagId))
|
||||
AddWarn($"{c.chainId}: 条件读取的标志 '{flagId}' 同时被本链的 Action 写入。" +
|
||||
"若该标志在触发前已被设置,链将永远无法执行或会产生意外的循环行为。", c);
|
||||
}
|
||||
|
||||
Debug.Log($"[EventChainModule] 验证完成:{allChains.Count} 条事件链,{errorCount} 个错误,{warnCount} 个警告。");
|
||||
QuestValidationResultWindow.Show(issues, errorCount, warnCount, allChains.Count, "事件链批量验证结果", "事件链");
|
||||
}
|
||||
|
||||
@@ -119,6 +119,18 @@ namespace BaseGames.Quest
|
||||
/// 若任务当前可以接取,返回 <see cref="QuestLockInfo.Reason"/> 为 <see cref="QuestLockReason.None"/> 的实例。
|
||||
/// </summary>
|
||||
QuestLockInfo GetQuestLockInfo(string questId);
|
||||
|
||||
/// <summary>
|
||||
/// 返回当前处于指定状态的所有任务 ID 快照列表。
|
||||
/// 适用于任务日志 UI 分组显示、成就系统批量统计等场景。
|
||||
/// </summary>
|
||||
System.Collections.Generic.IReadOnlyList<string> GetQuestsInState(QuestStateEnum state);
|
||||
|
||||
/// <summary>
|
||||
/// 对所有已注册任务执行谓词过滤,返回满足条件的任务 ID 快照列表。
|
||||
/// 适用于自定义筛选(如"活跃且含 NPC 亲密度门槛")等场景。
|
||||
/// </summary>
|
||||
System.Collections.Generic.IReadOnlyList<string> FilterQuests(System.Func<string, QuestStateEnum, bool> predicate);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -122,6 +122,10 @@ namespace BaseGames.Quest
|
||||
/// <summary>已广播过 EVT_QuestReadyToComplete 的任务 ID 集合(防重复通知)。
|
||||
/// 任务完成/失败时从集合移除,再次激活后可重新通知。</summary>
|
||||
private readonly HashSet<string> _notifiedReadyQuests = new();
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||
/// <summary>任务暂停时记录的 realtimeSinceStartup(供 ResumeQuest 计算暂停持续时长并日志输出)。</summary>
|
||||
private readonly Dictionary<string, float> _pauseTimestamps = new();
|
||||
#endif
|
||||
/// <summary>供需要直接订阅 SO 频道的系统使用(推荐使用 IQuestEventSource C# 事件)。</summary>
|
||||
public StringEventChannelSO QuestStartedChannel => _onQuestStarted;
|
||||
/// <summary>供需要直接订阅 SO 频道的系统使用(推荐使用 IQuestEventSource C# 事件)。</summary>
|
||||
@@ -313,6 +317,10 @@ namespace BaseGames.Quest
|
||||
if (GetState(questId) != QuestStateEnum.Active) return;
|
||||
_questStates[questId] = QuestStateEnum.Paused;
|
||||
OnQuestStateChanged?.Invoke(questId, QuestStateEnum.Active, QuestStateEnum.Paused);
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||
_pauseTimestamps[questId] = UnityEngine.Time.realtimeSinceStartup;
|
||||
Debug.Log($"[QuestManager] 任务 '{questId}' 已暂停(realtimeSinceStartup={UnityEngine.Time.realtimeSinceStartup:F2}s)。");
|
||||
#endif
|
||||
Chan_QuestPaused?.Raise(questId);
|
||||
OnQuestPaused?.Invoke(questId);
|
||||
}
|
||||
@@ -327,6 +335,14 @@ namespace BaseGames.Quest
|
||||
if (GetState(questId) != QuestStateEnum.Paused) return;
|
||||
_questStates[questId] = QuestStateEnum.Active;
|
||||
OnQuestStateChanged?.Invoke(questId, QuestStateEnum.Paused, QuestStateEnum.Active);
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||
if (_pauseTimestamps.TryGetValue(questId, out float pausedAt))
|
||||
{
|
||||
float duration = UnityEngine.Time.realtimeSinceStartup - pausedAt;
|
||||
_pauseTimestamps.Remove(questId);
|
||||
Debug.Log($"[QuestManager] 任务 '{questId}' 已恢复(暂停持续 {duration:F2}s)。");
|
||||
}
|
||||
#endif
|
||||
Chan_QuestResumed?.Raise(questId);
|
||||
OnQuestResumed?.Invoke(questId);
|
||||
}
|
||||
@@ -367,16 +383,18 @@ namespace BaseGames.Quest
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||
Debug.LogWarning(
|
||||
$"[QuestManager] 任务 '{quest.questId}' 好感度奖励 +{quest.reward.affinityBonus} " +
|
||||
$"将超出 NPC '{quest.GiverNpcId}' 的上限 {maxAffinity}(当前 {current}),已截断至 {maxAffinity}。");
|
||||
$"将超出 NPC '{quest.GiverNpcId}' 的上限 {maxAffinity}(当前 {current}),已截断至 {maxAffinity},实际增量为 {maxAffinity - current}。");
|
||||
#endif
|
||||
newTotal = maxAffinity;
|
||||
}
|
||||
|
||||
// 广播实际写入的 delta(截断后),而非请求值;UI 层显示 "+5" 而非因截断产生误导的 "+20"
|
||||
int actualDelta = newTotal - current;
|
||||
_npcRelations[quest.GiverNpcId] = newTotal;
|
||||
Chan_NpcAffinityChanged?.Raise(new NpcAffinityEvent
|
||||
{
|
||||
npcId = quest.GiverNpcId,
|
||||
delta = quest.reward.affinityBonus,
|
||||
delta = actualDelta,
|
||||
newTotal = newTotal
|
||||
});
|
||||
}
|
||||
@@ -503,7 +521,7 @@ namespace BaseGames.Quest
|
||||
var quest = GetQuestSO(questId);
|
||||
if (quest == null) return new QuestLockInfo { Reason = QuestLockReason.NotFound };
|
||||
|
||||
// 好感度门槛检查
|
||||
// 好感度门槛检查(仅 GetQuestLockInfo 关心,不影响 MeetsPrerequisites)
|
||||
if (quest.minAffinityToAccept > 0 && !string.IsNullOrEmpty(quest.GiverNpcId))
|
||||
{
|
||||
if (!_affinityInitialized) return new QuestLockInfo { Reason = QuestLockReason.DataNotLoaded };
|
||||
@@ -512,36 +530,8 @@ namespace BaseGames.Quest
|
||||
return new QuestLockInfo { Reason = QuestLockReason.InsufficientAffinity, Param = $"{affinity}/{quest.minAffinityToAccept}" };
|
||||
}
|
||||
|
||||
// 前置任务依赖检查(新版优先,回退旧版)
|
||||
#pragma warning disable CS0618
|
||||
var deps = quest.prerequisites.HasAny ? quest.prerequisites.questDependencies : quest.prerequisiteQuests;
|
||||
#pragma warning restore CS0618
|
||||
if (deps != null)
|
||||
{
|
||||
foreach (var dep in deps)
|
||||
{
|
||||
if (dep == null || string.IsNullOrEmpty(dep.questId)) continue;
|
||||
if (GetState(dep.questId) != QuestStateEnum.Completed)
|
||||
return new QuestLockInfo { Reason = QuestLockReason.RequiresQuest, Param = dep.questId };
|
||||
}
|
||||
}
|
||||
|
||||
// 世界标志条件检查
|
||||
var fc = quest.prerequisites.HasAny ? quest.prerequisites.flagCondition : default;
|
||||
#pragma warning disable CS0618
|
||||
if (!quest.prerequisites.HasAny && quest.prerequisiteFlags != null && quest.prerequisiteFlags.Length > 0)
|
||||
fc = new QuestPrerequisite.FlagCondition
|
||||
{ flags = quest.prerequisiteFlags, logic = quest.prerequisiteFlagsLogic };
|
||||
#pragma warning restore CS0618
|
||||
|
||||
if (fc.flags != null && fc.flags.Length > 0)
|
||||
{
|
||||
var svc = BaseGames.Core.ServiceLocator.GetOrDefault<ISaveService>();
|
||||
if (svc != null && !EvaluateFlagPrerequisites(fc.flags, fc.logic, svc))
|
||||
return new QuestLockInfo { Reason = QuestLockReason.FlagConditionNotMet };
|
||||
}
|
||||
|
||||
return new QuestLockInfo { Reason = QuestLockReason.None }; // 无锁定,任务可接取
|
||||
// 前置依赖 + 标志检查委托给 CheckQuestDepsAndFlags(单一权威实现)
|
||||
return CheckQuestDepsAndFlags(quest);
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||
@@ -687,67 +677,9 @@ namespace BaseGames.Quest
|
||||
|
||||
private bool CanAccept(string questId)
|
||||
{
|
||||
// 状态必须为 Available;其余门槛检查委托给 GetQuestLockInfo(单一权威实现)。
|
||||
if (GetState(questId) != QuestStateEnum.Available) return false;
|
||||
var quest = GetQuestSO(questId);
|
||||
if (quest == null) return false;
|
||||
|
||||
// 好感度门槛检查:_npcRelations 仅在 OnLoad 后有效
|
||||
if (quest.minAffinityToAccept > 0 && !string.IsNullOrEmpty(quest.GiverNpcId))
|
||||
{
|
||||
if (!_affinityInitialized)
|
||||
{
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||
Debug.LogWarning(
|
||||
$"[QuestManager] CanAccept: 好感度数据尚未从存档加载(OnLoad 未完成)," +
|
||||
$"任务 '{questId}' 的好感度门槛检查暂时拒绝接取。");
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
_npcRelations.TryGetValue(quest.GiverNpcId, out int affinity);
|
||||
if (affinity < quest.minAffinityToAccept) return false;
|
||||
}
|
||||
|
||||
// 前置条件检查:优先使用新版 prerequisites 结构,回退到旧版字段
|
||||
if (quest.prerequisites.HasAny)
|
||||
{
|
||||
if (quest.prerequisites.questDependencies != null)
|
||||
foreach (var dep in quest.prerequisites.questDependencies)
|
||||
{
|
||||
if (dep == null) continue;
|
||||
if (GetState(dep.questId) != QuestStateEnum.Completed) return false;
|
||||
}
|
||||
var fc = quest.prerequisites.flagCondition;
|
||||
if (fc.flags != null && fc.flags.Length > 0)
|
||||
{
|
||||
var svc = BaseGames.Core.ServiceLocator.GetOrDefault<ISaveService>();
|
||||
if (svc != null && !EvaluateFlagPrerequisites(fc.flags, fc.logic, svc)) return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 旧版字段回退(兼容现有资产)
|
||||
if (quest.prerequisiteQuests != null)
|
||||
foreach (var pre in quest.prerequisiteQuests)
|
||||
{
|
||||
if (pre == null) continue;
|
||||
if (GetState(pre.questId) != QuestStateEnum.Completed) return false;
|
||||
}
|
||||
|
||||
if (quest.prerequisiteFlags != null && quest.prerequisiteFlags.Length > 0)
|
||||
{
|
||||
var svc = BaseGames.Core.ServiceLocator.GetOrDefault<ISaveService>();
|
||||
if (svc != null)
|
||||
{
|
||||
if (!EvaluateFlagPrerequisites(quest.prerequisiteFlags, quest.prerequisiteFlagsLogic, svc)) return false;
|
||||
}
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||
else Debug.LogWarning(
|
||||
$"[QuestManager] CanAccept: 任务 '{questId}' 的 prerequisiteFlags 需要 ISaveService,但服务未注册,标志检查已跳过。");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return !GetQuestLockInfo(questId).IsLocked;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -784,17 +716,26 @@ namespace BaseGames.Quest
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查任务是否满足全部前置条件(不含状态检查),用于 InitializeAvailableQuests 初始化。
|
||||
/// 与 CanAccept 的区别:CanAccept 需要任务已经是 Available;此方法仅判断前置依赖是否达成。
|
||||
/// 优先读取新版 <see cref="QuestPrerequisite"/> 结构;若未配置则回退到旧版字段。
|
||||
/// 检查任务是否满足全部前置条件(不含状态和亲密度检查),用于 InitializeAvailableQuests 初始化。
|
||||
/// 与 CanAccept 的区别:CanAccept 需要任务已经是 Available 且包含亲密度检查;此方法仅判断前置依赖是否达成。
|
||||
/// 委托给 <see cref="CheckQuestDepsAndFlags"/> 实现,不再重复前置逻辑。
|
||||
/// </summary>
|
||||
private bool MeetsPrerequisites(QuestSO quest)
|
||||
{
|
||||
if (quest == null) return false;
|
||||
return CheckQuestDepsAndFlags(quest).Reason == QuestLockReason.None;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查任务的前置依赖(任务完成 + 世界标志),不含亲密度和状态检查。
|
||||
/// 是 <see cref="CanAccept"/>(经 GetQuestLockInfo 间接调用)、<see cref="MeetsPrerequisites"/>、
|
||||
/// <see cref="GetQuestLockInfo"/> 共享的单一权威实现,消除三处重复逻辑。
|
||||
/// </summary>
|
||||
private QuestLockInfo CheckQuestDepsAndFlags(QuestSO quest)
|
||||
{
|
||||
if (quest == null) return new QuestLockInfo { Reason = QuestLockReason.NotFound };
|
||||
|
||||
if (quest.prerequisites.HasAny)
|
||||
{
|
||||
// 新版前置结构:questDependencies + flagCondition
|
||||
if (quest.prerequisites.questDependencies != null)
|
||||
foreach (var dep in quest.prerequisites.questDependencies)
|
||||
{
|
||||
@@ -802,25 +743,27 @@ namespace BaseGames.Quest
|
||||
if (string.IsNullOrEmpty(dep.questId))
|
||||
{
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||
Debug.LogWarning(
|
||||
$"[QuestManager] 任务 '{quest.questId}' 的 prerequisites.questDependencies 含 questId 为空的条目,已跳过。");
|
||||
Debug.LogWarning($"[QuestManager] 任务 '{quest.questId}' 的 prerequisites.questDependencies 含 questId 为空的条目,已跳过。");
|
||||
#endif
|
||||
continue;
|
||||
}
|
||||
if (GetState(dep.questId) != QuestStateEnum.Completed) return false;
|
||||
if (GetState(dep.questId) != QuestStateEnum.Completed)
|
||||
return new QuestLockInfo { Reason = QuestLockReason.RequiresQuest, Param = dep.questId };
|
||||
}
|
||||
|
||||
var fc = quest.prerequisites.flagCondition;
|
||||
if (fc.flags != null && fc.flags.Length > 0)
|
||||
{
|
||||
var svc = BaseGames.Core.ServiceLocator.GetOrDefault<ISaveService>();
|
||||
if (svc != null && !EvaluateFlagPrerequisites(fc.flags, fc.logic, svc)) return false;
|
||||
// ISaveService 未就绪(Awake 阶段)→ 保守跳过;OnLoad 后重新评估
|
||||
if (svc != null && !EvaluateFlagPrerequisites(fc.flags, fc.logic, svc))
|
||||
return new QuestLockInfo { Reason = QuestLockReason.FlagConditionNotMet };
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 旧版字段回退(兼容现有资产)
|
||||
#pragma warning disable CS0618
|
||||
if (quest.prerequisiteQuests != null)
|
||||
foreach (var pre in quest.prerequisiteQuests)
|
||||
{
|
||||
@@ -828,12 +771,12 @@ namespace BaseGames.Quest
|
||||
if (string.IsNullOrEmpty(pre.questId))
|
||||
{
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||
Debug.LogWarning(
|
||||
$"[QuestManager] 任务 '{quest.questId}' 的 prerequisiteQuests 含 questId 为空的条目,已跳过该前置条件。");
|
||||
Debug.LogWarning($"[QuestManager] 任务 '{quest.questId}' 的 prerequisiteQuests 含 questId 为空的条目,已跳过该前置条件。");
|
||||
#endif
|
||||
continue;
|
||||
}
|
||||
if (GetState(pre.questId) != QuestStateEnum.Completed) return false;
|
||||
if (GetState(pre.questId) != QuestStateEnum.Completed)
|
||||
return new QuestLockInfo { Reason = QuestLockReason.RequiresQuest, Param = pre.questId };
|
||||
}
|
||||
|
||||
if (quest.prerequisiteFlags != null && quest.prerequisiteFlags.Length > 0)
|
||||
@@ -841,13 +784,36 @@ namespace BaseGames.Quest
|
||||
var svc = BaseGames.Core.ServiceLocator.GetOrDefault<ISaveService>();
|
||||
if (svc != null)
|
||||
{
|
||||
if (!EvaluateFlagPrerequisites(quest.prerequisiteFlags, quest.prerequisiteFlagsLogic, svc)) return false;
|
||||
if (!EvaluateFlagPrerequisites(quest.prerequisiteFlags, quest.prerequisiteFlagsLogic, svc))
|
||||
return new QuestLockInfo { Reason = QuestLockReason.FlagConditionNotMet };
|
||||
}
|
||||
// ISaveService 未就绪(Awake 阶段)→ 保守跳过;OnLoad 后 InitializeAvailableQuests 重新评估
|
||||
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
||||
else Debug.LogWarning($"[QuestManager] 任务 '{quest.questId}' 的 prerequisiteFlags 需要 ISaveService,但服务未注册,标志检查已跳过。");
|
||||
#endif
|
||||
}
|
||||
#pragma warning restore CS0618
|
||||
}
|
||||
|
||||
return true;
|
||||
return new QuestLockInfo { Reason = QuestLockReason.None };
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IQuestManager.GetQuestsInState"/>
|
||||
public System.Collections.Generic.IReadOnlyList<string> GetQuestsInState(QuestStateEnum state)
|
||||
{
|
||||
var result = new List<string>();
|
||||
foreach (var (id, s) in _questStates)
|
||||
if (s == state) result.Add(id);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IQuestManager.FilterQuests"/>
|
||||
public System.Collections.Generic.IReadOnlyList<string> FilterQuests(System.Func<string, QuestStateEnum, bool> predicate)
|
||||
{
|
||||
if (predicate == null) return System.Array.Empty<string>();
|
||||
var result = new List<string>();
|
||||
foreach (var (id, state) in _questStates)
|
||||
if (predicate(id, state)) result.Add(id);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -200,10 +200,16 @@ namespace BaseGames.Quest
|
||||
/// <summary>
|
||||
/// 深度优先遍历前置链,检测是否存在环路。
|
||||
/// <para>已访问节点集 <paramref name="visited"/> 在回溯时移除,保证同一链条中不误报平行分支。</para>
|
||||
/// <para><paramref name="depth"/> 超过 32 层时停止递归并输出警告,防止编辑器因超深嵌套卡顿。</para>
|
||||
/// </summary>
|
||||
private static bool HasPrerequisiteCycle(QuestSO quest,
|
||||
System.Collections.Generic.HashSet<string> visited)
|
||||
System.Collections.Generic.HashSet<string> visited, int depth = 0)
|
||||
{
|
||||
if (depth > 32)
|
||||
{
|
||||
Debug.LogWarning($"[QuestSO] 前置链深度超过 32 层(路径末端:'{quest.name}'),已停止检测。请减少任务链深度。");
|
||||
return false;
|
||||
}
|
||||
if (string.IsNullOrEmpty(quest.questId)) return false;
|
||||
if (!visited.Add(quest.questId)) return true; // 已在当前路径上 = 环路
|
||||
|
||||
@@ -215,7 +221,7 @@ namespace BaseGames.Quest
|
||||
{
|
||||
foreach (var dep in deps)
|
||||
{
|
||||
if (dep != null && HasPrerequisiteCycle(dep, visited))
|
||||
if (dep != null && HasPrerequisiteCycle(dep, visited, depth + 1))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -250,10 +256,16 @@ namespace BaseGames.Quest
|
||||
|
||||
/// <summary>
|
||||
/// 深度优先遍历 branches[].nextQuest 链,检测是否存在环路(DFS 回溯)。
|
||||
/// <para><paramref name="depth"/> 超过 32 层时停止递归并输出警告,防止编辑器因超深嵌套卡顿。</para>
|
||||
/// </summary>
|
||||
private static bool HasBranchCycle(QuestSO quest,
|
||||
System.Collections.Generic.HashSet<string> visited)
|
||||
System.Collections.Generic.HashSet<string> visited, int depth = 0)
|
||||
{
|
||||
if (depth > 32)
|
||||
{
|
||||
Debug.LogWarning($"[QuestSO] 分支链深度超过 32 层(路径末端:'{quest.name}'),已停止检测。请减少分支链深度。");
|
||||
return false;
|
||||
}
|
||||
if (string.IsNullOrEmpty(quest.questId)) return false;
|
||||
if (!visited.Add(quest.questId)) return true; // 已在路径上 = 环路
|
||||
|
||||
@@ -261,7 +273,7 @@ namespace BaseGames.Quest
|
||||
{
|
||||
foreach (var branch in quest.branches)
|
||||
{
|
||||
if (branch?.nextQuest != null && HasBranchCycle(branch.nextQuest, visited))
|
||||
if (branch?.nextQuest != null && HasBranchCycle(branch.nextQuest, visited, depth + 1))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,10 +124,11 @@ namespace BaseGames.World
|
||||
|
||||
if (data == null) return;
|
||||
|
||||
foreach (var id in data.World.CollectedIds) Mark(WorldObjectCategory.Collectible, id);
|
||||
foreach (var id in data.World.ActivatedSavePoints) Mark(WorldObjectCategory.SavePoint, id);
|
||||
foreach (var id in data.World.OpenedDoors) Mark(WorldObjectCategory.Door, id);
|
||||
foreach (var id in data.World.DestroyedObjectIds) Mark(WorldObjectCategory.Destroyed, id);
|
||||
var world = data.World;
|
||||
if (world?.CollectedIds != null) foreach (var id in world.CollectedIds) Mark(WorldObjectCategory.Collectible, id);
|
||||
if (world?.ActivatedSavePoints != null) foreach (var id in world.ActivatedSavePoints) Mark(WorldObjectCategory.SavePoint, id);
|
||||
if (world?.OpenedDoors != null) foreach (var id in world.OpenedDoors) Mark(WorldObjectCategory.Door, id);
|
||||
if (world?.DestroyedObjectIds != null) foreach (var id in world.DestroyedObjectIds) Mark(WorldObjectCategory.Destroyed, id);
|
||||
|
||||
if (data.EventChains?.WorldFlags != null)
|
||||
foreach (var kv in data.EventChains.WorldFlags)
|
||||
|
||||
Reference in New Issue
Block a user