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>
This commit is contained in:
@@ -23,6 +23,7 @@ namespace BaseGames.Editor.Dialogue
|
||||
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);
|
||||
@@ -106,6 +107,20 @@ namespace BaseGames.Editor.Dialogue
|
||||
_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();
|
||||
}
|
||||
|
||||
@@ -364,6 +379,119 @@ namespace BaseGames.Editor.Dialogue
|
||||
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()
|
||||
|
||||
@@ -431,10 +431,10 @@ namespace BaseGames.Editor.Modules
|
||||
var capturedC = c;
|
||||
var forceBtn = new Button(() =>
|
||||
{
|
||||
var mgr = UnityEngine.Object.FindObjectOfType<EventChainManager>();
|
||||
var mgr = BaseGames.Core.ServiceLocator.GetOrDefault<EventChainManager>();
|
||||
if (mgr == null)
|
||||
{
|
||||
Debug.LogWarning("[EventChainModule] 场景中未找到 EventChainManager,无法强制触发。");
|
||||
Debug.LogWarning("[EventChainModule] ServiceLocator 中未找到 EventChainManager,无法强制触发。请确认场景中已挂载并注册。");
|
||||
return;
|
||||
}
|
||||
Debug.Log($"[EventChainModule] 强制触发链:{capturedC.chainId}");
|
||||
@@ -519,6 +519,48 @@ namespace BaseGames.Editor.Modules
|
||||
AddError($"{c.chainId}: actions[{i}] 为 null,运行时将触发 NullReferenceException。", c);
|
||||
}
|
||||
|
||||
// ⑥ 自触发检测:某条件检查的标志由同一链的 Action 写入(可能造成链被自身条件阻断或无限反复触发)
|
||||
var flagFieldFlags = System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance;
|
||||
foreach (var c in allChains)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(c.chainId)) continue;
|
||||
var writtenFlags = new System.Collections.Generic.HashSet<string>(StringComparer.Ordinal);
|
||||
var readFlags = new System.Collections.Generic.HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
if (c.actions != null)
|
||||
foreach (var action in c.actions)
|
||||
{
|
||||
if (action == null) continue;
|
||||
foreach (var field in action.GetType().GetFields(flagFieldFlags))
|
||||
if ((field.Name.Contains("flag", System.StringComparison.OrdinalIgnoreCase) ||
|
||||
field.Name.Contains("Flag", System.StringComparison.OrdinalIgnoreCase))
|
||||
&& field.FieldType == typeof(string))
|
||||
{
|
||||
var val = field.GetValue(action) as string;
|
||||
if (!string.IsNullOrEmpty(val)) writtenFlags.Add(val);
|
||||
}
|
||||
}
|
||||
|
||||
if (c.conditions != null)
|
||||
foreach (var cond in c.conditions)
|
||||
{
|
||||
if (cond == null) continue;
|
||||
foreach (var field in cond.GetType().GetFields(flagFieldFlags))
|
||||
if ((field.Name.Contains("flag", System.StringComparison.OrdinalIgnoreCase) ||
|
||||
field.Name.Contains("Flag", System.StringComparison.OrdinalIgnoreCase))
|
||||
&& field.FieldType == typeof(string))
|
||||
{
|
||||
var val = field.GetValue(cond) as string;
|
||||
if (!string.IsNullOrEmpty(val)) readFlags.Add(val);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var flagId in readFlags)
|
||||
if (writtenFlags.Contains(flagId))
|
||||
AddWarn($"{c.chainId}: 条件读取的标志 '{flagId}' 同时被本链的 Action 写入。" +
|
||||
"若该标志在触发前已被设置,链将永远无法执行或会产生意外的循环行为。", c);
|
||||
}
|
||||
|
||||
Debug.Log($"[EventChainModule] 验证完成:{allChains.Count} 条事件链,{errorCount} 个错误,{warnCount} 个警告。");
|
||||
QuestValidationResultWindow.Show(issues, errorCount, warnCount, allChains.Count, "事件链批量验证结果", "事件链");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user