Files
zeling_v2/Assets/_Game/Scripts/Editor/Dialogue/DialogueVariantPreviewWindow.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

380 lines
14 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.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using BaseGames.Dialogue;
namespace BaseGames.Editor.Dialogue
{
/// <summary>
/// 对话变体预览窗口。
/// 给定一个 DialogueSequenceSO模拟世界状态标志的开关组合
/// 实时显示各条件变体是否满足,并高亮胜出的变体。
/// 菜单BaseGames/Dialogue/Variant Preview
/// </summary>
public class DialogueVariantPreviewWindow : EditorWindow
{
private DialogueSequenceSO _target;
private readonly HashSet<string> _enabledFlags = new(System.StringComparer.Ordinal);
private readonly List<string> _allFlags = new();
private ObjectField _targetField;
private VisualElement _flagContainer;
private VisualElement _resultContainer;
private static readonly Color ColWin = new(0.20f, 0.75f, 0.35f, 1f);
private static readonly Color ColFail = new(0.55f, 0.55f, 0.55f, 1f);
private static readonly Color ColOverride = new(0.70f, 0.70f, 0.25f, 1f);
private static readonly Color ColBlocked = new(0.85f, 0.35f, 0.30f, 1f);
[MenuItem("BaseGames/Dialogue/Variant Preview")]
public static void Open()
{
var win = GetWindow<DialogueVariantPreviewWindow>("对话变体预览");
win.minSize = new Vector2(480, 400);
}
/// <summary>从外部打开并预填目标 SO。</summary>
public static void OpenWith(DialogueSequenceSO target)
{
var win = GetWindow<DialogueVariantPreviewWindow>("对话变体预览");
win.minSize = new Vector2(480, 400);
win.SetTarget(target);
}
private void CreateGUI()
{
_mockReader = new MockFlagReader(_enabledFlags);
rootVisualElement.style.paddingLeft = 10;
rootVisualElement.style.paddingRight = 10;
rootVisualElement.style.paddingTop = 10;
rootVisualElement.style.paddingBottom = 10;
// ── 标题栏 ──
var header = new Label("对话变体预览工具");
header.style.fontSize = 14;
header.style.unityFontStyleAndWeight = FontStyle.Bold;
header.style.marginBottom = 8;
rootVisualElement.Add(header);
var desc = new Label("在模拟的世界状态标志组合下,预览哪个条件变体会被选中。");
desc.style.fontSize = 11;
desc.style.opacity = 0.6f;
desc.style.marginBottom = 10;
rootVisualElement.Add(desc);
// ── 目标选择器 ──
_targetField = new ObjectField("对话序列 SO")
{
objectType = typeof(DialogueSequenceSO),
allowSceneObjects = false
};
_targetField.value = _target;
_targetField.RegisterValueChangedCallback(evt =>
{
SetTarget(evt.newValue as DialogueSequenceSO);
});
rootVisualElement.Add(_targetField);
rootVisualElement.Add(MakeDivider());
// ── 标志模拟区 ──
var flagHeader = new Label("模拟世界状态标志");
flagHeader.style.fontSize = 12;
flagHeader.style.unityFontStyleAndWeight = FontStyle.Bold;
flagHeader.style.marginBottom = 4;
rootVisualElement.Add(flagHeader);
_flagContainer = new VisualElement();
rootVisualElement.Add(_flagContainer);
rootVisualElement.Add(MakeDivider());
// ── 变体结果区 ──
var resultHeader = new Label("变体求值结果");
resultHeader.style.fontSize = 12;
resultHeader.style.unityFontStyleAndWeight = FontStyle.Bold;
resultHeader.style.marginBottom = 4;
rootVisualElement.Add(resultHeader);
var scrollView = new ScrollView(ScrollViewMode.Vertical);
scrollView.style.flexGrow = 1;
rootVisualElement.Add(scrollView);
_resultContainer = new VisualElement();
scrollView.Add(_resultContainer);
Rebuild();
}
private void SetTarget(DialogueSequenceSO target)
{
_target = target;
_enabledFlags.Clear();
if (_targetField != null && _targetField.value != target)
_targetField.SetValueWithoutNotify(target);
Rebuild();
}
// ── 重建 ─────────────────────────────────────────────────────────────
private void Rebuild()
{
RebuildFlagToggles();
RebuildResults();
}
private void RebuildFlagToggles()
{
if (_flagContainer == null) return;
_flagContainer.Clear();
_allFlags.Clear();
if (_target == null || _target.variants == null || _target.variants.Length == 0)
{
var empty = new Label(_target == null
? "(请选择一个 DialogueSequenceSO"
: "(该序列无条件变体,无需模拟)");
empty.style.opacity = 0.5f;
empty.style.fontSize = 11;
_flagContainer.Add(empty);
return;
}
// 收集所有变体中涉及的 Flag
var flagSet = new HashSet<string>(System.StringComparer.Ordinal);
foreach (var v in _target.variants)
{
if (v.requiredFlags != null)
foreach (var f in v.requiredFlags)
if (!string.IsNullOrEmpty(f)) flagSet.Add(f);
}
_allFlags.AddRange(flagSet.OrderBy(x => x));
if (_allFlags.Count == 0)
{
var empty = new Label("(变体未使用任何 requiredFlags");
empty.style.opacity = 0.5f;
empty.style.fontSize = 11;
_flagContainer.Add(empty);
return;
}
// 全选 / 全不选 快速按钮
var btnRow = new VisualElement();
btnRow.style.flexDirection = FlexDirection.Row;
btnRow.style.marginBottom = 4;
var btnAll = new Button(() =>
{
foreach (var f in _allFlags) _enabledFlags.Add(f);
Rebuild();
}) { text = "全选" };
btnAll.style.fontSize = 10;
btnAll.style.height = 18;
btnRow.Add(btnAll);
var btnNone = new Button(() =>
{
_enabledFlags.Clear();
Rebuild();
}) { text = "全不选" };
btnNone.style.fontSize = 10;
btnNone.style.height = 18;
btnRow.Add(btnNone);
_flagContainer.Add(btnRow);
// 每个 Flag 对应一个 Toggle
foreach (var flag in _allFlags)
{
bool isOn = _enabledFlags.Contains(flag);
var toggle = new Toggle(flag) { value = isOn };
toggle.style.fontSize = 11;
toggle.RegisterValueChangedCallback(evt =>
{
if (evt.newValue) _enabledFlags.Add(flag);
else _enabledFlags.Remove(flag);
RebuildResults();
});
_flagContainer.Add(toggle);
}
}
private void RebuildResults()
{
if (_resultContainer == null) return;
_resultContainer.Clear();
if (_target == null)
return;
if (_target.variants == null || _target.variants.Length == 0)
{
var msg = new Label("(序列无条件变体,直接使用本序列默认台词)");
msg.style.opacity = 0.5f;
msg.style.fontSize = 11;
_resultContainer.Add(msg);
return;
}
bool winnerFound = false;
for (int i = 0; i < _target.variants.Length; i++)
{
var variant = _target.variants[i];
var row = BuildVariantRow(i, variant, winnerFound);
_resultContainer.Add(row);
if (!winnerFound && EvaluateVariant(variant))
winnerFound = true;
}
// 若无变体胜出,提示将回退到本序列默认台词
if (!winnerFound)
{
var fallback = new Label("↳ 无变体满足,将使用本序列默认台词(无变体覆盖)");
fallback.style.fontSize = 11;
fallback.style.opacity = 0.6f;
fallback.style.marginTop = 4;
_resultContainer.Add(fallback);
}
}
private VisualElement BuildVariantRow(int index, DialogueSequenceSO.ConditionalVariant variant, bool higherWon)
{
bool condMet = EvaluateVariant(variant);
bool isWinner = condMet && !higherWon;
var card = new VisualElement();
card.style.borderLeftWidth = 3;
card.style.paddingLeft = 8;
card.style.paddingRight = 8;
card.style.paddingTop = 5;
card.style.paddingBottom = 5;
card.style.marginBottom = 4;
card.style.backgroundColor = new StyleColor(new Color(0.18f, 0.18f, 0.18f, 1f));
Color borderColor;
string statusText;
Color statusColor;
if (isWinner)
{
borderColor = ColWin;
statusText = "✓ 胜出";
statusColor = ColWin;
}
else if (condMet)
{
borderColor = ColOverride;
statusText = "⏩ 被更高优先级覆盖";
statusColor = ColOverride;
}
else
{
borderColor = ColFail;
statusText = "✗ 条件不满足";
statusColor = ColFail;
}
card.style.borderLeftColor = new StyleColor(borderColor);
// 标题行
var titleRow = new VisualElement();
titleRow.style.flexDirection = FlexDirection.Row;
titleRow.style.alignItems = Align.Center;
titleRow.style.marginBottom = 3;
var idxLabel = new Label($"变体 {index}");
idxLabel.style.fontSize = 11;
idxLabel.style.flexGrow = 1;
idxLabel.style.unityFontStyleAndWeight = isWinner ? FontStyle.Bold : FontStyle.Normal;
titleRow.Add(idxLabel);
var seqName = new Label(variant.sequence != null ? variant.sequence.name : "(未设置序列)");
seqName.style.fontSize = 10;
seqName.style.opacity = 0.6f;
seqName.style.width = 160;
titleRow.Add(seqName);
var statusLabel = new Label(statusText);
statusLabel.style.fontSize = 10;
statusLabel.style.color = new StyleColor(statusColor);
statusLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
titleRow.Add(statusLabel);
card.Add(titleRow);
// 逻辑类型
var logicLabel = new Label($"逻辑:{variant.logic}");
logicLabel.style.fontSize = 10;
logicLabel.style.opacity = 0.5f;
card.Add(logicLabel);
// 条件详情
if (variant.requiredFlags != null && variant.requiredFlags.Length > 0)
{
foreach (var flag in variant.requiredFlags)
{
if (string.IsNullOrEmpty(flag)) continue;
bool flagOn = _enabledFlags.Contains(flag);
var flagRow = new VisualElement();
flagRow.style.flexDirection = FlexDirection.Row;
flagRow.style.alignItems = Align.Center;
flagRow.style.marginTop = 1;
var icon = new Label(flagOn ? "✓" : "✗");
icon.style.fontSize = 10;
icon.style.color = new StyleColor(flagOn ? ColWin : ColBlocked);
icon.style.width = 16;
flagRow.Add(icon);
var flagLabel = new Label(flag);
flagLabel.style.fontSize = 10;
flagRow.Add(flagLabel);
card.Add(flagRow);
}
}
else
{
var noFlags = new Label("(无 requiredFlags — 无条件激活)");
noFlags.style.fontSize = 10;
noFlags.style.opacity = 0.5f;
card.Add(noFlags);
}
return card;
}
private bool EvaluateVariant(DialogueSequenceSO.ConditionalVariant variant)
{
// 使用 DialogueSequenceSO.CheckVariant 统一变体求值逻辑,避免重复实现
return _target != null && _target.CheckVariant(variant, _mockReader);
}
/// <summary>将 _enabledFlags 包装为 IWorldStateReader供 CheckVariant 调用。</summary>
private MockFlagReader _mockReader;
private sealed class MockFlagReader : BaseGames.Core.IWorldStateReader
{
private readonly System.Collections.Generic.HashSet<string> _flags;
public MockFlagReader(System.Collections.Generic.HashSet<string> flags) => _flags = flags;
public bool HasFlag(string key) => _flags.Contains(key);
}
// ── 辅助 ─────────────────────────────────────────────────────────────
private static VisualElement MakeDivider()
{
var d = new VisualElement();
d.style.height = 1;
d.style.backgroundColor = new StyleColor(new Color(0.35f, 0.35f, 0.35f, 0.5f));
d.style.marginTop = 6;
d.style.marginBottom = 6;
return d;
}
}
}