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,11 @@
fileFormatVersion: 2
guid: 832e6ad6d64454d4286183c6d7cfdc3e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,118 @@
using UnityEditor;
using UnityEngine;
using BaseGames.Quest;
using BaseGames.Core;
namespace BaseGames.Editor.Quest
{
/// <summary>
/// QuestSO 自定义 Inspector。
/// 在检测到旧版前置字段prerequisiteQuests / prerequisiteFlags有数据时
/// 显示迁移提示框和一键迁移按钮,引导策划将数据迁移到 QuestPrerequisite 统一结构。
/// </summary>
[CustomEditor(typeof(QuestSO))]
public class QuestSOEditor : UnityEditor.Editor
{
private bool _showMigrationBox = true;
public override void OnInspectorGUI()
{
serializedObject.Update();
var quest = (QuestSO)target;
// ── 旧版字段迁移提示 ──────────────────────────────────────────────
#pragma warning disable CS0618
bool hasLegacyQuests = quest.prerequisiteQuests != null && quest.prerequisiteQuests.Length > 0;
bool hasLegacyFlags = quest.prerequisiteFlags != null && quest.prerequisiteFlags.Length > 0;
#pragma warning restore CS0618
if (hasLegacyQuests || hasLegacyFlags)
{
_showMigrationBox = EditorGUILayout.BeginFoldoutHeaderGroup(_showMigrationBox, "⚠ 旧版前置字段迁移");
if (_showMigrationBox)
{
EditorGUILayout.HelpBox(
"检测到旧版前置字段有数据:\n" +
(hasLegacyQuests ? $" • prerequisiteQuests{quest.prerequisiteQuests.Length} 项\n" : "") +
(hasLegacyFlags ? $" • prerequisiteFlags{quest.prerequisiteFlags.Length} 项\n" : "") +
"\n新版 'prerequisites'QuestPrerequisite字段已支持更完整的前置配置。\n" +
"点击下方按钮可将旧版数据自动迁移至新字段,迁移后旧字段将被清空。\n" +
"迁移操作可撤销Ctrl+Z。",
MessageType.Warning);
bool hasNewData = quest.prerequisites.HasAny;
if (hasNewData)
EditorGUILayout.HelpBox(
"新版 prerequisites 字段已有数据。点击迁移将与旧版数据合并(去重),不会覆盖现有配置。",
MessageType.Info);
if (GUILayout.Button("一键迁移旧版前置字段 → prerequisites"))
{
MigrateLegacyPrerequisites(quest);
}
}
EditorGUILayout.EndFoldoutHeaderGroup();
EditorGUILayout.Space(4);
}
// ── 默认 Inspector ────────────────────────────────────────────────
DrawDefaultInspector();
serializedObject.ApplyModifiedProperties();
}
private static void MigrateLegacyPrerequisites(QuestSO quest)
{
Undo.RecordObject(quest, "迁移 QuestSO 旧版前置字段");
#pragma warning disable CS0618
int legacyQuestCount = quest.prerequisiteQuests?.Length ?? 0;
int legacyFlagCount = quest.prerequisiteFlags?.Length ?? 0;
// 迁移 prerequisiteQuests → prerequisites.questDependencies合并去重
if (quest.prerequisiteQuests != null && quest.prerequisiteQuests.Length > 0)
{
var existing = quest.prerequisites.questDependencies ?? System.Array.Empty<QuestSO>();
var merged = new System.Collections.Generic.HashSet<QuestSO>(existing);
foreach (var q in quest.prerequisiteQuests)
if (q != null) merged.Add(q);
quest.prerequisites.questDependencies = new QuestSO[merged.Count];
merged.CopyTo(quest.prerequisites.questDependencies);
quest.prerequisiteQuests = System.Array.Empty<QuestSO>();
}
// 迁移 prerequisiteFlags → prerequisites.flagCondition合并去重
if (quest.prerequisiteFlags != null && quest.prerequisiteFlags.Length > 0)
{
var existing = quest.prerequisites.flagCondition.flags ?? System.Array.Empty<string>();
var merged = new System.Collections.Generic.HashSet<string>(
existing, System.StringComparer.Ordinal);
foreach (var f in quest.prerequisiteFlags)
if (!string.IsNullOrEmpty(f)) merged.Add(f);
quest.prerequisites.flagCondition.flags = new string[merged.Count];
merged.CopyTo(quest.prerequisites.flagCondition.flags);
// 迁移逻辑模式(旧字段覆盖新字段,以旧配置为准)
quest.prerequisites.flagCondition.logic = quest.prerequisiteFlagsLogic;
quest.prerequisiteFlags = System.Array.Empty<string>();
quest.prerequisiteFlagsLogic = WorldStateFlagLogic.And;
}
#pragma warning restore CS0618
EditorUtility.SetDirty(quest);
AssetDatabase.SaveAssets();
Debug.Log($"[QuestSOEditor] '{quest.name}' 旧版前置字段迁移完成(任务:{legacyQuestCount} 项,标志:{legacyFlagCount} 项)。", quest);
EditorUtility.DisplayDialog(
"迁移完成",
$"任务 \"{quest.name}\" 旧版前置字段已成功迁移:\n\n" +
$" 前置任务:{legacyQuestCount} 项 → prerequisites.questDependencies\n" +
$" 前置标志:{legacyFlagCount} 项 → prerequisites.flagCondition.flags\n\n" +
"旧版字段已清空。操作可通过 Ctrl+Z 撤销。",
"确定");
}
}
}