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

@@ -0,0 +1,76 @@
using BaseGames.Core.Events;
using UnityEngine;
namespace BaseGames.World
{
/// <summary>
/// 区域到达触发器(架构 22_QuestChallengeModule §3 扩展)。
/// 挂在场景中的 2D 触发碰撞体上,玩家进入时广播 EVT_AreaReached 事件,
/// 驱动 ReachAreaObjectivemarkerTag 模式)的任务目标进度。
///
/// 使用方式:
/// 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
}
}