Files
zeling_v2/Assets/_Game/Scripts/Editor/Dialogue/DialogueVariantPreviewWindow.cs
Joywayer 3c3ea1ead6 feat: Round 49 narrative systems improvements
QuestManager: extract CheckQuestDepsAndFlags shared method, simplify GetQuestLockInfo/CanAccept/MeetsPrerequisites; add GetQuestsInState+FilterQuests implementations; fix extra brace compile bug; add _pauseTimestamps logging; use actualDelta in ApplyAffinity event.

QuestSO: add depth>32 guard to HasPrerequisiteCycle and HasBranchCycle to prevent editor freeze on deep chains.

EventChainModule: replace FindObjectOfType with ServiceLocator.GetOrDefault in ForceExecute; add self-trigger flag detection (check 6) in ValidateAllChains using reflection.

DialogueVariantPreviewWindow: add matrix analysis section enumerating all 2^N flag combinations (N<=10) with table showing winning variant per combination.

WorldStateRegistry: LoadFromSave null guard on data.World sub-collections (P0 fix).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-25 00:17:27 +08:00

508 lines
20 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 VisualElement _matrixContainer;
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);
rootVisualElement.Add(MakeDivider());
// ── 矩阵分析区 ──
var matrixFoldout = new Foldout { text = "矩阵分析(所有标志组合 → 胜出变体)", value = false };
matrixFoldout.style.marginTop = 4;
rootVisualElement.Add(matrixFoldout);
_matrixContainer = new VisualElement();
matrixFoldout.Add(_matrixContainer);
var matrixBtn = new Button(() => RebuildMatrix()) { text = "矩阵分析" };
matrixBtn.style.marginBottom = 4;
matrixFoldout.Add(matrixBtn);
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);
}
// ── 矩阵分析 ─────────────────────────────────────────────────────────
/// <summary>
/// 枚举全部 2^N 标志组合N ≤ 10以表格形式展示每种组合下胜出的变体索引。
/// N > 10 时显示提示,建议手动筛选标志后分析。
/// </summary>
private void RebuildMatrix()
{
if (_matrixContainer == null) return;
_matrixContainer.Clear();
if (_target == null || _target.variants == null || _target.variants.Length == 0)
{
_matrixContainer.Add(new Label("(无可分析的变体)") { style = { opacity = 0.5f, fontSize = 11 } });
return;
}
var matrixFlags = _allFlags.Count > 0 ? _allFlags : new List<string>();
if (matrixFlags.Count == 0)
{
_matrixContainer.Add(new Label("(变体不使用任何 requiredFlags无需矩阵分析") { style = { opacity = 0.5f, fontSize = 11 } });
return;
}
const int MaxFlags = 10;
if (matrixFlags.Count > MaxFlags)
{
var warn = new Label($"⚠ 标志数量 ({matrixFlags.Count}) 超过 {MaxFlags},枚举 2^N 组合代价过高。请在上方取消勾选不关心的标志后重新点击「矩阵分析」。");
warn.style.fontSize = 11;
warn.style.color = new StyleColor(new Color(0.9f, 0.7f, 0.2f));
warn.style.whiteSpace = WhiteSpace.Normal;
_matrixContainer.Add(warn);
return;
}
int n = matrixFlags.Count;
int combos = 1 << n; // 2^n
// ── 表头 ──
var headerRow = MakeMatrixRow(isHeader: true);
for (int ci = 0; ci < n; ci++)
{
var cell = MakeMatrixCell(matrixFlags[ci], isHeader: true);
cell.style.minWidth = 90;
headerRow.Add(cell);
}
headerRow.Add(MakeMatrixCell("胜出变体", isHeader: true));
_matrixContainer.Add(headerRow);
// ── 数据行 ──
for (int mask = 0; mask < combos; mask++)
{
var combo = new HashSet<string>(System.StringComparer.Ordinal);
for (int bit = 0; bit < n; bit++)
if ((mask & (1 << bit)) != 0) combo.Add(matrixFlags[bit]);
// 求胜出变体
var mockReader = new MockFlagReader(combo);
int winner = -1;
for (int vi = 0; vi < _target.variants.Length; vi++)
if (_target.CheckVariant(_target.variants[vi], mockReader)) { winner = vi; break; }
string winnerText = winner >= 0
? $"变体 {winner}" +
(_target.variants[winner].sequence != null
? $"\n({_target.variants[winner].sequence.name})"
: "(无序列)")
: "默认台词";
var dataRow = MakeMatrixRow(isHeader: false);
// 标志列
for (int ci = 0; ci < n; ci++)
{
bool on = (mask & (1 << ci)) != 0;
var cell = MakeMatrixCell(on ? "✓" : "", isHeader: false);
cell.style.color = new StyleColor(on ? ColWin : ColFail);
cell.style.minWidth = 90;
dataRow.Add(cell);
}
// 胜出列
var winCell = MakeMatrixCell(winnerText, isHeader: false);
winCell.style.color = new StyleColor(winner >= 0 ? ColWin : new Color(0.5f, 0.5f, 0.5f));
dataRow.Add(winCell);
_matrixContainer.Add(dataRow);
}
}
private static VisualElement MakeMatrixRow(bool isHeader)
{
var row = new VisualElement();
row.style.flexDirection = FlexDirection.Row;
row.style.borderBottomWidth = 1;
row.style.borderBottomColor = new StyleColor(new Color(0.3f, 0.3f, 0.3f, 0.5f));
if (isHeader)
row.style.backgroundColor = new StyleColor(new Color(0.22f, 0.22f, 0.28f, 1f));
return row;
}
private static Label MakeMatrixCell(string text, bool isHeader)
{
var lbl = new Label(text);
lbl.style.fontSize = isHeader ? 10 : 10;
lbl.style.unityFontStyleAndWeight = isHeader ? FontStyle.Bold : FontStyle.Normal;
lbl.style.paddingLeft = 4;
lbl.style.paddingRight = 4;
lbl.style.paddingTop = 3;
lbl.style.paddingBottom = 3;
lbl.style.whiteSpace = WhiteSpace.Normal;
lbl.style.width = 80;
return lbl;
}
// ── 辅助 ─────────────────────────────────────────────────────────────
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;
}
}
}