- 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>
157 lines
7.5 KiB
C#
157 lines
7.5 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using BaseGames.Core;
|
||
using BaseGames.Core.Save;
|
||
using UnityEngine;
|
||
|
||
namespace BaseGames.World
|
||
{
|
||
/// <summary>世界对象的分类枚举,作为 WorldStateRegistry 的泛化 key。</summary>
|
||
public enum WorldObjectCategory
|
||
{
|
||
Collectible,
|
||
SavePoint,
|
||
Door,
|
||
Destroyed,
|
||
Flag,
|
||
}
|
||
|
||
/// <summary>
|
||
/// 运行时世界状态缓存。ScriptableObject,通过 [SerializeField] 注入各组件。
|
||
/// WorldStateRegistrySaver(ISaveable)负责将本对象与存档管道连接:
|
||
/// SaveAsync → WorldStateRegistrySaver.OnSave → 写出全部分类到 SaveData;
|
||
/// LoadAsync → WorldStateRegistrySaver.OnLoad → 调用 LoadFromSave(data) 恢复缓存。
|
||
/// </summary>
|
||
[CreateAssetMenu(menuName = "BaseGames/World/WorldStateRegistry")]
|
||
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 开始时都会调用,确保状态干净。
|
||
/// </summary>
|
||
private void OnEnable() => _states.Clear();
|
||
|
||
// ── 泛化 API ─────────────────────────────────────────────────────────
|
||
/// <summary>检查指定类别中 id 是否已标记。</summary>
|
||
public bool IsMarked(WorldObjectCategory category, string id)
|
||
=> _states.TryGetValue(category, out var set) && set.Contains(id);
|
||
|
||
/// <summary>标记指定类别中的 id(幂等)。</summary>
|
||
public void Mark(WorldObjectCategory category, string id)
|
||
{
|
||
if (!_states.TryGetValue(category, out var set))
|
||
{
|
||
set = new HashSet<string>();
|
||
_states[category] = set;
|
||
}
|
||
if (set.Add(id))
|
||
OnStateChanged?.Invoke(category, id);
|
||
}
|
||
|
||
// ── 语义化具名 API(泛化方法快捷方式)───────────────────────────────
|
||
public bool IsCollected(string id) => IsMarked(WorldObjectCategory.Collectible, id);
|
||
public void MarkCollected(string id) => Mark(WorldObjectCategory.Collectible, id);
|
||
|
||
public bool IsSavePointActivated(string id) => IsMarked(WorldObjectCategory.SavePoint, id);
|
||
public void MarkSavePointActivated(string id) => Mark(WorldObjectCategory.SavePoint, id);
|
||
|
||
public bool IsDestroyed(string id) => IsMarked(WorldObjectCategory.Destroyed, id);
|
||
public void MarkDestroyed(string id) => Mark(WorldObjectCategory.Destroyed, id);
|
||
|
||
public bool IsDoorOpened(string id) => IsMarked(WorldObjectCategory.Door, id);
|
||
public void MarkDoorOpened(string id) => Mark(WorldObjectCategory.Door, id);
|
||
|
||
public bool HasFlag(string key) => IsMarked(WorldObjectCategory.Flag, key);
|
||
public void SetFlag(string key) => Mark(WorldObjectCategory.Flag, key);
|
||
public void ClearFlag(string key) => Clear(WorldObjectCategory.Flag, key);
|
||
|
||
/// <summary>
|
||
/// 通用清除接口:移除指定类别中 id 的标记状态(幂等)。
|
||
/// 用于调试重置、测试、或撤销错误标记。
|
||
/// </summary>
|
||
public void Clear(WorldObjectCategory category, string id)
|
||
{
|
||
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 ───────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 从存档数据恢复全部状态。由 WorldStateRegistrySaver.OnLoad 调用。
|
||
/// </summary>
|
||
public void LoadFromSave(SaveData data)
|
||
{
|
||
_states.Clear();
|
||
|
||
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);
|
||
|
||
if (data.EventChains?.WorldFlags != null)
|
||
foreach (var kv in data.EventChains.WorldFlags)
|
||
if (kv.Value) Mark(WorldObjectCategory.Flag, kv.Key);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 返回指定分类中所有已标记 ID 的快照副本(数组)。
|
||
/// 由 WorldStateRegistrySaver.OnSave 调用,将运行时状态写入 SaveData。
|
||
/// 返回数组副本而非内部集合引用,防止调用方意外修改内部状态。
|
||
/// </summary>
|
||
public IReadOnlyCollection<string> GetAllIds(WorldObjectCategory category)
|
||
{
|
||
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>();
|
||
}
|
||
|
||
/// <summary>重置所有状态(开始新游戏时调用)。</summary>
|
||
public void Reset() => _states.Clear();
|
||
}
|
||
}
|