Files
zeling_v2/Assets/_Game/Scripts/World/WorldStateRegistry.cs
Joywayer 6eaa83dc71 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>
2026-05-25 00:05:15 +08:00

157 lines
7.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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] 注入各组件。
/// WorldStateRegistrySaverISaveable负责将本对象与存档管道连接
/// 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();
}
}