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:
2026-05-25 00:05:15 +08:00
parent 446fd5dcd0
commit 6eaa83dc71
72 changed files with 7080 additions and 373 deletions

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using BaseGames.Core;
using BaseGames.Core.Save;
using UnityEngine;
@@ -22,16 +23,23 @@ namespace BaseGames.World
/// LoadAsync → WorldStateRegistrySaver.OnLoad → 调用 LoadFromSave(data) 恢复缓存。
/// </summary>
[CreateAssetMenu(menuName = "BaseGames/World/WorldStateRegistry")]
public class WorldStateRegistry : ScriptableObject
public class WorldStateRegistry : ScriptableObject, IWorldStateReader
{
// ── 统一状态字典 ─────────────────────────────────────────────────────
private readonly Dictionary<WorldObjectCategory, HashSet<string>> _states = new();
/// <summary>
/// 状态变更时广播:(类别, id)。UI / 测试代码可订阅此事件做响应式刷新。
/// 若需批量写入多个 ID推荐使用 <see cref="BatchMark"/> 避免同帧多次重绘。
/// </summary>
public event Action<WorldObjectCategory, string> OnStateChanged;
/// <summary>
/// 批次状态变更时广播:(类别, 新增标记的 ID 数组)。
/// 由 <see cref="BatchMark"/> 触发,一次性广播所有新增 ID避免 UI 同帧重绘 N 次。
/// </summary>
public event Action<WorldObjectCategory, string[]> OnBatchStateChanged;
/// <summary>
/// Editor 重新进入 Play Mode 时 ScriptableObject 保留上一次运行的状态,
/// OnEnable 在域重载Domain Reload和每次 Play 开始时都会调用,确保状态干净。
@@ -70,10 +78,39 @@ namespace BaseGames.World
public bool HasFlag(string key) => IsMarked(WorldObjectCategory.Flag, key);
public void SetFlag(string key) => Mark(WorldObjectCategory.Flag, key);
public void ClearFlag(string key)
public void ClearFlag(string key) => Clear(WorldObjectCategory.Flag, key);
/// <summary>
/// 通用清除接口:移除指定类别中 id 的标记状态(幂等)。
/// 用于调试重置、测试、或撤销错误标记。
/// </summary>
public void Clear(WorldObjectCategory category, string id)
{
if (_states.TryGetValue(WorldObjectCategory.Flag, out var set) && set.Remove(key))
OnStateChanged?.Invoke(WorldObjectCategory.Flag, key);
if (_states.TryGetValue(category, out var set) && set.Remove(id))
OnStateChanged?.Invoke(category, id);
}
/// <summary>
/// 一次性标记多个 ID批次写入。已标记的 ID 被幂等跳过;
/// 全部写入后触发单次 <see cref="OnBatchStateChanged"/>,而非逐个触发 <see cref="OnStateChanged"/>
/// 适合 EventChain 同帧连续设置多个标志时使用以避免 UI 同帧重绘 N 次。
/// </summary>
/// <returns>实际新增标记的 ID 数量。</returns>
public int BatchMark(WorldObjectCategory category, System.Collections.Generic.IEnumerable<string> ids)
{
if (ids == null) return 0;
if (!_states.TryGetValue(category, out var set))
{
set = new HashSet<string>();
_states[category] = set;
}
var added = new System.Collections.Generic.List<string>();
foreach (var id in ids)
if (!string.IsNullOrEmpty(id) && set.Add(id))
added.Add(id);
if (added.Count > 0)
OnBatchStateChanged?.Invoke(category, added.ToArray());
return added.Count;
}
// ── Persistence ───────────────────────────────────────────────────────
@@ -98,14 +135,18 @@ namespace BaseGames.World
}
/// <summary>
/// 返回指定分类中所有已标记 ID(只读视图,非副本)。
/// 返回指定分类中所有已标记 ID 的快照副本(数组)。
/// 由 WorldStateRegistrySaver.OnSave 调用,将运行时状态写入 SaveData。
/// 注意:不保证跨帧稳定性,调用方若需持久快照请自行 ToArray()
/// 返回数组副本而非内部集合引用,防止调用方意外修改内部状态
/// </summary>
public IReadOnlyCollection<string> GetAllIds(WorldObjectCategory category)
{
if (_states.TryGetValue(category, out var set))
return set;
if (_states.TryGetValue(category, out var set) && set.Count > 0)
{
var copy = new string[set.Count];
set.CopyTo(copy);
return copy;
}
return System.Array.Empty<string>();
}