feat: Round 48 narrative systems improvements
- QuestSO: Add ValidateBranchCycles() DFS detection for branches[].nextQuest loop - QuestSO: Mark three legacy prerequisite fields with v2.0 removal warning in Tooltip - IQuestManager: Add QuestLockReason enum + QuestLockInfo struct (strongly-typed lock info) - IQuestManager: Add GetQuestLockInfo() method to interface; GetQuestLockReason() now delegates to it - IQuestEventSource: Add OnQuestStateChanged(questId, oldState, newState) unified event - QuestManager: Implement GetQuestLockInfo(); fire OnQuestStateChanged on all state transitions - DialogueManager: Add one-frame yield in HandleChoices before ShowChoices (skip-debounce fix) - DialogueManager: Increment _playbackId in ForceEnd() to invalidate residual choice callbacks - DialogueSequenceSO: Add UNITY_EDITOR debug log in TryGetActiveVariant on variant match - WorldStateRegistry: Add OnBatchStateChanged event + BatchMark() batch-write API - DialogueModule: List badge shows warning indicator for unconditional-shadowing variants - DialogueModule: BuildVariantsCard shows logic mode (AND/OR) alongside flag conditions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -18,6 +18,11 @@ namespace BaseGames.EventChain
|
||||
[Header("所有事件链")]
|
||||
[SerializeField] private EventChainSO[] _chains;
|
||||
|
||||
[Header("执行保护")]
|
||||
[Tooltip("单个 Action 的最长执行时长(秒,unscaled time)。超时后强制跳过并记录警告。0 = 不限时。")]
|
||||
[Min(0f)]
|
||||
[SerializeField] private float _maxActionTimeout = 30f;
|
||||
|
||||
[Header("事件频道(中继)")]
|
||||
[SerializeField] private StringEventChannelSO _onBossDefeated; // EVT_EnemyDied(bossId)
|
||||
[SerializeField] private StringEventChannelSO _onCollectiblePickedUp; // EVT_CollectiblePickup
|
||||
@@ -33,6 +38,39 @@ namespace BaseGames.EventChain
|
||||
public event Action<string> OnDialogueCompleted;
|
||||
/// <summary>链执行完成时广播 chainId(供 ChainCompletedCondition)。</summary>
|
||||
public event Action<string> OnChainCompleted;
|
||||
/// <summary>
|
||||
/// 世界标志变更事件(供 FlagSetCondition 事件驱动订阅,避免每帧轮询)。
|
||||
/// 由 SetFlagAction.ExecuteAsync 在写入标志后调用 NotifyFlagChanged 触发。
|
||||
/// </summary>
|
||||
public event Action<string> OnWorldFlagChanged;
|
||||
|
||||
/// <summary>通知所有 FlagSetCondition 指定标志已变更,并立即重评估标志相关链条件。</summary>
|
||||
public void NotifyFlagChanged(string flagId)
|
||||
{
|
||||
OnWorldFlagChanged?.Invoke(flagId);
|
||||
EvaluateForMask(ChainEventMask.FlagChanged);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 强制执行指定链,完全跳过条件检查。
|
||||
/// 用于调试、关卡测试,或 QA 快速验证后续事件逻辑。
|
||||
/// 仅在 Play Mode 中有效(链执行需要运行时环境)。
|
||||
/// </summary>
|
||||
public void ForceExecute(string chainId)
|
||||
{
|
||||
if (_chains == null)
|
||||
{
|
||||
Debug.LogWarning("[EventChainManager] ForceExecute: _chains 为空,没有可执行的事件链。", this);
|
||||
return;
|
||||
}
|
||||
foreach (var chain in _chains)
|
||||
{
|
||||
if (chain == null || chain.chainId != chainId) continue;
|
||||
StartCoroutine(ExecuteChain(chain));
|
||||
return;
|
||||
}
|
||||
Debug.LogWarning($"[EventChainManager] ForceExecute: 未找到 chainId='{chainId}' 的事件链。", this);
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>
|
||||
@@ -46,6 +84,15 @@ namespace BaseGames.EventChain
|
||||
private readonly HashSet<string> _completedChains = new();
|
||||
private readonly CompositeDisposable _subs = new();
|
||||
|
||||
// 每个链对应的事件掩码:OnEnable 后通过 BuildChainMasks 构建
|
||||
private List<ChainMaskEntry> _chainMasks;
|
||||
|
||||
private struct ChainMaskEntry
|
||||
{
|
||||
public EventChainSO chain;
|
||||
public ChainEventMask mask;
|
||||
}
|
||||
|
||||
// ── 生命周期 ──────────────────────────────────────────────────────
|
||||
|
||||
private void Awake()
|
||||
@@ -56,11 +103,11 @@ namespace BaseGames.EventChain
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
_onBossDefeated?.Subscribe(id => { OnBossDefeated?.Invoke(id); EvaluateAll(); }).AddTo(_subs);
|
||||
_onCollectiblePickedUp?.Subscribe(id => { OnCollectiblePickedUp?.Invoke(id); EvaluateAll(); }).AddTo(_subs);
|
||||
_onAbilityUnlocked?.Subscribe(id => { OnAbilityUnlocked?.Invoke(id); EvaluateAll(); }).AddTo(_subs);
|
||||
_onRoomEntered?.Subscribe(id => { OnRoomEntered?.Invoke(id); EvaluateAll(); }).AddTo(_subs);
|
||||
_onDialogueCompleted?.Subscribe(id => { OnDialogueCompleted?.Invoke(id); EvaluateAll(); }).AddTo(_subs);
|
||||
_onBossDefeated?.Subscribe(id => { OnBossDefeated?.Invoke(id); EvaluateForMask(ChainEventMask.Boss); }).AddTo(_subs);
|
||||
_onCollectiblePickedUp?.Subscribe(id => { OnCollectiblePickedUp?.Invoke(id); EvaluateForMask(ChainEventMask.Collectible); }).AddTo(_subs);
|
||||
_onAbilityUnlocked?.Subscribe(id => { OnAbilityUnlocked?.Invoke(id); EvaluateForMask(ChainEventMask.Ability); }).AddTo(_subs);
|
||||
_onRoomEntered?.Subscribe(id => { OnRoomEntered?.Invoke(id); EvaluateForMask(ChainEventMask.Room); }).AddTo(_subs);
|
||||
_onDialogueCompleted?.Subscribe(id => { OnDialogueCompleted?.Invoke(id); EvaluateForMask(ChainEventMask.Dialogue); }).AddTo(_subs);
|
||||
|
||||
ServiceLocator.GetOrDefault<ISaveableRegistry>()?.Register(this);
|
||||
|
||||
@@ -68,12 +115,26 @@ namespace BaseGames.EventChain
|
||||
// 先重置 SO 资产上的运行时状态,防止跨 PlayMode 会话或多场景加载时状态残留
|
||||
if (_chains == null) return;
|
||||
foreach (var chain in _chains)
|
||||
{
|
||||
// 旧版 conditions[] 字段
|
||||
if (chain?.conditions != null)
|
||||
foreach (var cond in chain.conditions)
|
||||
{
|
||||
cond?.ResetState();
|
||||
cond?.Register(this);
|
||||
}
|
||||
// 新版 conditionGroups[] 字段
|
||||
if (chain?.conditionGroups != null)
|
||||
foreach (var group in chain.conditionGroups)
|
||||
if (group?.conditions != null)
|
||||
foreach (var cond in group.conditions)
|
||||
{
|
||||
cond?.ResetState();
|
||||
cond?.Register(this);
|
||||
}
|
||||
}
|
||||
|
||||
BuildChainMasks();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
@@ -83,9 +144,16 @@ namespace BaseGames.EventChain
|
||||
|
||||
if (_chains == null) return;
|
||||
foreach (var chain in _chains)
|
||||
{
|
||||
if (chain?.conditions != null)
|
||||
foreach (var cond in chain.conditions)
|
||||
cond?.Unregister(this);
|
||||
if (chain?.conditionGroups != null)
|
||||
foreach (var group in chain.conditionGroups)
|
||||
if (group?.conditions != null)
|
||||
foreach (var cond in group.conditions)
|
||||
cond?.Unregister(this);
|
||||
}
|
||||
}
|
||||
|
||||
// ── ISaveable ─────────────────────────────────────────────────────────
|
||||
@@ -108,26 +176,98 @@ namespace BaseGames.EventChain
|
||||
}
|
||||
|
||||
// ── 评估逻辑 ──────────────────────────────────────────────────────
|
||||
/// <summary>收到新事件时立即评估所有链条件(无帧延迟)。</summary>
|
||||
private void EvaluateAll() => DoEvaluateAll();
|
||||
|
||||
private void DoEvaluateAll()
|
||||
/// <summary>
|
||||
/// 构建每条链的 ChainEventMask(链内所有条件 RelevantEvents 的并集)。
|
||||
/// OnEnable 在所有条件 Register 完毕后调用,之后事件触发只评估掩码相交的链。
|
||||
/// </summary>
|
||||
private void BuildChainMasks()
|
||||
{
|
||||
_chainMasks = new List<ChainMaskEntry>(_chains?.Length ?? 0);
|
||||
if (_chains == null) return;
|
||||
foreach (var chain in _chains)
|
||||
{
|
||||
if (chain == null) continue;
|
||||
ChainEventMask mask = ChainEventMask.None;
|
||||
|
||||
bool hasGroups = chain.conditionGroups != null && chain.conditionGroups.Length > 0;
|
||||
if (hasGroups)
|
||||
{
|
||||
foreach (var group in chain.conditionGroups)
|
||||
if (group?.conditions != null)
|
||||
foreach (var cond in group.conditions)
|
||||
if (cond != null) mask |= cond.RelevantEvents;
|
||||
}
|
||||
else if (chain.conditions != null)
|
||||
{
|
||||
foreach (var cond in chain.conditions)
|
||||
if (cond != null) mask |= cond.RelevantEvents;
|
||||
}
|
||||
|
||||
// 无条件链:任何事件均可触发(等同 Any)
|
||||
if (mask == ChainEventMask.None) mask = ChainEventMask.Any;
|
||||
_chainMasks.Add(new ChainMaskEntry { chain = chain, mask = mask });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 懒评估:仅评估 mask 与 <paramref name="triggerMask"/> 相交的链。
|
||||
/// ChainEventMask.Any(-1)与任何非零 mask 均相交,确保自定义条件链不被跳过。
|
||||
/// </summary>
|
||||
private void EvaluateForMask(ChainEventMask triggerMask)
|
||||
{
|
||||
if (_chainMasks == null) return;
|
||||
foreach (var entry in _chainMasks)
|
||||
{
|
||||
if ((entry.mask & triggerMask) == 0) continue;
|
||||
var chain = entry.chain;
|
||||
if (!chain.repeatable && _completedChains.Contains(chain.chainId)) continue;
|
||||
|
||||
bool allMet = true;
|
||||
if (chain.conditions != null)
|
||||
foreach (var cond in chain.conditions)
|
||||
if (cond != null && !cond.IsMet()) { allMet = false; break; }
|
||||
|
||||
if (allMet) StartCoroutine(ExecuteChain(chain));
|
||||
if (EvaluateChainConditions(chain)) StartCoroutine(ExecuteChain(chain));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 评估链的所有触发条件。
|
||||
/// 若链配置了 <see cref="EventChainSO.conditionGroups"/>,按组逻辑评估(组内 And/Or,组间 And);
|
||||
/// 否则回退到旧版 <see cref="EventChainSO.conditions"/> 隐式 And 逻辑。
|
||||
/// </summary>
|
||||
private static bool EvaluateChainConditions(EventChainSO chain)
|
||||
{
|
||||
bool hasGroups = chain.conditionGroups != null && chain.conditionGroups.Length > 0;
|
||||
if (hasGroups)
|
||||
{
|
||||
// 组间为 And:所有组均须通过
|
||||
foreach (var group in chain.conditionGroups)
|
||||
{
|
||||
if (group?.conditions == null || group.conditions.Length == 0) continue;
|
||||
if (!EvaluateConditionGroup(group)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// 旧版:conditions[] 隐式 And
|
||||
if (chain.conditions != null)
|
||||
foreach (var cond in chain.conditions)
|
||||
if (cond != null && !cond.IsMet()) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>按组内 logic(And/Or)评估单个条件组是否通过。</summary>
|
||||
private static bool EvaluateConditionGroup(ConditionGroup group)
|
||||
{
|
||||
if (group.logic == WorldStateFlagLogic.Or)
|
||||
{
|
||||
foreach (var cond in group.conditions)
|
||||
if (cond != null && cond.IsMet()) return true;
|
||||
return false;
|
||||
}
|
||||
// And(默认)
|
||||
foreach (var cond in group.conditions)
|
||||
if (cond != null && !cond.IsMet()) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private IEnumerator ExecuteChain(EventChainSO chain)
|
||||
{
|
||||
// 防重入:一次性链立即标记为已完成
|
||||
@@ -137,7 +277,62 @@ namespace BaseGames.EventChain
|
||||
foreach (var action in chain.actions)
|
||||
{
|
||||
if (action == null) continue;
|
||||
yield return action.ExecuteAsync(this);
|
||||
|
||||
bool actionFailed = false;
|
||||
if (_maxActionTimeout > 0f)
|
||||
{
|
||||
bool finished = false;
|
||||
Coroutine co = null;
|
||||
try
|
||||
{
|
||||
co = StartCoroutine(SetTrueOnFinish(action.ExecuteAsync(this), () => finished = true));
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogError(
|
||||
$"[EventChainManager] 链 '{chain.chainId}' 动作 '{action.GetType().Name}' 启动异常:{ex.Message}",
|
||||
this);
|
||||
actionFailed = true;
|
||||
}
|
||||
|
||||
if (!actionFailed)
|
||||
{
|
||||
// 用 realtimeSinceStartup 计时,确保 PlayMode 暂停时超时仍能触发
|
||||
float deadline = Time.realtimeSinceStartup + _maxActionTimeout;
|
||||
while (!finished)
|
||||
{
|
||||
if (Time.realtimeSinceStartup >= deadline)
|
||||
{
|
||||
if (co != null) StopCoroutine(co);
|
||||
Debug.LogWarning(
|
||||
$"[EventChainManager] 链 '{chain.chainId}' 的动作 '{action.GetType().Name}' " +
|
||||
$"执行超时({_maxActionTimeout}s),已强制跳过。");
|
||||
#if UNITY_EDITOR
|
||||
OnChainExecutedInEditor?.Invoke(chain.chainId,
|
||||
$"超时跳过:{action.GetType().Name}(>{_maxActionTimeout}s)");
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
yield return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
bool exceptionThrown = false;
|
||||
IEnumerator routine = null;
|
||||
try { routine = action.ExecuteAsync(this); }
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Debug.LogError(
|
||||
$"[EventChainManager] 链 '{chain.chainId}' 动作 '{action.GetType().Name}' 异常:{ex.Message}",
|
||||
this);
|
||||
exceptionThrown = true;
|
||||
}
|
||||
if (!exceptionThrown && routine != null)
|
||||
yield return routine;
|
||||
}
|
||||
|
||||
if (chain.actionDelay > 0f)
|
||||
yield return new WaitForSeconds(chain.actionDelay);
|
||||
}
|
||||
@@ -148,5 +343,15 @@ namespace BaseGames.EventChain
|
||||
OnChainExecutedInEditor?.Invoke(chain.chainId, "执行完成");
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 inner 协程执行完毕后触发 onFinish 回调,供超时保护使用。
|
||||
/// StopCoroutine 此协程时,内嵌的 inner(通过 yield return 直接展开)也会一并停止。
|
||||
/// </summary>
|
||||
private static IEnumerator SetTrueOnFinish(IEnumerator inner, Action onFinish)
|
||||
{
|
||||
yield return inner;
|
||||
onFinish();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user