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:
76
Assets/_Game/Scripts/World/TriggerZone.cs
Normal file
76
Assets/_Game/Scripts/World/TriggerZone.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using BaseGames.Core.Events;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.World
|
||||
{
|
||||
/// <summary>
|
||||
/// 区域到达触发器(架构 22_QuestChallengeModule §3 扩展)。
|
||||
/// 挂在场景中的 2D 触发碰撞体上,玩家进入时广播 EVT_AreaReached 事件,
|
||||
/// 驱动 ReachAreaObjective(markerTag 模式)的任务目标进度。
|
||||
///
|
||||
/// 使用方式:
|
||||
/// 1. 在目标区域创建空 GameObject,添加 Collider2D(勾选 IsTrigger)。
|
||||
/// 2. 挂上 TriggerZone,填写 markerTag(与 ReachAreaObjective.markerTag 保持一致)。
|
||||
/// 3. 将 EVT_AreaReached 事件频道资产拖入 _onAreaReached 字段。
|
||||
/// 4. 将 QuestManager 同一 _onAreaReached 频道字段也引用同一资产即可联通。
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(Collider2D))]
|
||||
public class TriggerZone : MonoBehaviour
|
||||
{
|
||||
[Header("区域标识")]
|
||||
[Tooltip("区域唯一标记(如 \"ZONE_ForestEntry\")。需与 ReachAreaObjective.markerTag 保持完全一致。")]
|
||||
[SerializeField] private string _markerTag;
|
||||
|
||||
[Tooltip("触发后是否只生效一次(推荐勾选,防止重复广播)。")]
|
||||
[SerializeField] private bool _triggerOnce = true;
|
||||
|
||||
[Header("玩家层级")]
|
||||
[Tooltip("玩家所在的物理层,用于过滤非玩家碰撞体进入。只有属于此层的碰撞体才触发广播。")]
|
||||
[SerializeField] private LayerMask _playerLayer;
|
||||
|
||||
[Header("事件频道")]
|
||||
[Tooltip("EVT_AreaReached 事件频道(StringEventChannelSO)。触发时以 markerTag 为 payload 广播。\n" +
|
||||
"需与 QuestManager._onAreaReached 字段引用同一资产。")]
|
||||
[SerializeField] private StringEventChannelSO _onAreaReached;
|
||||
|
||||
private bool _triggered;
|
||||
|
||||
/// <summary>区域唯一标记(只读)。供编辑器工具(QuestModule 批量验证)交叉比对使用。</summary>
|
||||
public string MarkerTag => _markerTag;
|
||||
|
||||
private void OnTriggerEnter2D(Collider2D other)
|
||||
{
|
||||
if (_triggered) return;
|
||||
if (string.IsNullOrEmpty(_markerTag)) return;
|
||||
if (((1 << other.gameObject.layer) & _playerLayer.value) == 0) return;
|
||||
|
||||
_onAreaReached?.Raise(_markerTag);
|
||||
|
||||
if (_triggerOnce) _triggered = true;
|
||||
}
|
||||
|
||||
/// <summary>重置触发状态(如读档/重新进入关卡时调用)。</summary>
|
||||
public void ResetTrigger() => _triggered = false;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
private void OnDrawGizmos()
|
||||
{
|
||||
if (string.IsNullOrEmpty(_markerTag)) return;
|
||||
Gizmos.color = new Color(0.2f, 0.8f, 0.4f, 0.25f);
|
||||
var col = GetComponent<Collider2D>();
|
||||
if (col != null) Gizmos.DrawWireSphere(col.bounds.center, 0.3f);
|
||||
}
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
var col = GetComponent<Collider2D>();
|
||||
if (col == null) return;
|
||||
Gizmos.color = new Color(0.2f, 0.9f, 0.4f, 0.5f);
|
||||
Gizmos.DrawWireCube(col.bounds.center, col.bounds.size);
|
||||
UnityEditor.Handles.Label(
|
||||
col.bounds.center + Vector3.up * (col.bounds.extents.y + 0.2f),
|
||||
string.IsNullOrEmpty(_markerTag) ? "(未设置 markerTag)" : _markerTag);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/World/TriggerZone.cs.meta
Normal file
11
Assets/_Game/Scripts/World/TriggerZone.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a77b179b1f28b6048bdf8aa9a92a1ea6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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>();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user