239 lines
9.1 KiB
C#
239 lines
9.1 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using Sirenix.OdinInspector;
|
||
using UnityEngine;
|
||
|
||
namespace Ichni.Story
|
||
{
|
||
/// <summary>
|
||
/// 通用剧情条件树的节点基类。通过多态组合出“与 / 或 / 非 + 叶子条件”的复合表达式。
|
||
/// 由 <see cref="StoryCondition"/> 借助 <c>[SerializeReference]</c> 序列化持有,
|
||
/// Odin 会以类型下拉框展示可选的节点类型。
|
||
/// </summary>
|
||
[Serializable]
|
||
public abstract class StoryConditionNode
|
||
{
|
||
/// <summary>
|
||
/// 对该节点求值。
|
||
/// </summary>
|
||
/// <param name="getVariable">按变量名返回其整型值的委托。</param>
|
||
/// <param name="isBlockCompleted">按 blockId 判断该 Block 是否已完成的委托。</param>
|
||
public abstract bool Evaluate(Func<string, int> getVariable, Func<string, bool> isBlockCompleted);
|
||
|
||
#if UNITY_EDITOR
|
||
/// <summary>
|
||
/// 返回 Inspector 中使用的紧凑摘要。条件编辑器只读取该文本,不影响运行时求值逻辑。
|
||
/// </summary>
|
||
public virtual string GetEditorSummary() => GetType().Name;
|
||
#endif
|
||
}
|
||
|
||
/// <summary>
|
||
/// 复合节点(逻辑与 AND):所有子条件均满足时才满足。子列表为空视为满足。
|
||
/// </summary>
|
||
[Serializable]
|
||
[LabelText("All Of (AND)")]
|
||
public class AllOfCondition : StoryConditionNode
|
||
{
|
||
[HideLabel]
|
||
[SerializeReference]
|
||
[ListDrawerSettings(ShowFoldout = true, DefaultExpandedState = true)]
|
||
public List<StoryConditionNode> conditions = new List<StoryConditionNode>();
|
||
|
||
public override bool Evaluate(Func<string, int> getVariable, Func<string, bool> isBlockCompleted)
|
||
{
|
||
foreach (StoryConditionNode child in conditions)
|
||
{
|
||
if (child != null && !child.Evaluate(getVariable, isBlockCompleted))
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
#if UNITY_EDITOR
|
||
public override string GetEditorSummary() => $"All Of ({conditions?.Count ?? 0})";
|
||
#endif
|
||
}
|
||
|
||
/// <summary>
|
||
/// 复合节点(逻辑或 OR):任一子条件满足即满足。子列表为空视为不满足。
|
||
/// </summary>
|
||
[Serializable]
|
||
[LabelText("Any Of (OR)")]
|
||
public class AnyOfCondition : StoryConditionNode
|
||
{
|
||
[HideLabel]
|
||
[SerializeReference]
|
||
[ListDrawerSettings(ShowFoldout = true, DefaultExpandedState = true)]
|
||
public List<StoryConditionNode> conditions = new List<StoryConditionNode>();
|
||
|
||
public override bool Evaluate(Func<string, int> getVariable, Func<string, bool> isBlockCompleted)
|
||
{
|
||
if (conditions.Count == 0)
|
||
return false;
|
||
|
||
foreach (StoryConditionNode child in conditions)
|
||
{
|
||
if (child != null && child.Evaluate(getVariable, isBlockCompleted))
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
#if UNITY_EDITOR
|
||
public override string GetEditorSummary() => $"Any Of ({conditions?.Count ?? 0})";
|
||
#endif
|
||
}
|
||
|
||
/// <summary>
|
||
/// 复合节点(逻辑非 NOT):对子条件取反。子条件为空视为满足(等价于“非 假”)。
|
||
/// </summary>
|
||
[Serializable]
|
||
[LabelText("Not")]
|
||
public class NotCondition : StoryConditionNode
|
||
{
|
||
[HideLabel]
|
||
[SerializeReference]
|
||
public StoryConditionNode condition;
|
||
|
||
public override bool Evaluate(Func<string, int> getVariable, Func<string, bool> isBlockCompleted)
|
||
{
|
||
return condition == null || !condition.Evaluate(getVariable, isBlockCompleted);
|
||
}
|
||
|
||
#if UNITY_EDITOR
|
||
public override string GetEditorSummary() => $"Not ({condition?.GetEditorSummary() ?? "None"})";
|
||
#endif
|
||
}
|
||
|
||
/// <summary>
|
||
/// 叶子节点:指定 Block 已完成时满足。
|
||
/// </summary>
|
||
[Serializable]
|
||
[LabelText("Block Completed")]
|
||
public class BlockCompletedCondition : StoryConditionNode
|
||
{
|
||
[LabelText("Block ID")]
|
||
[LabelWidth(50)]
|
||
[Tooltip("必须已完成的 StoryBlock ID。可用于表达显式前置条件;通常连接关系已由 Connected Rule 自动处理。")]
|
||
public string blockId;
|
||
|
||
public override bool Evaluate(Func<string, int> getVariable, Func<string, bool> isBlockCompleted)
|
||
{
|
||
return !string.IsNullOrEmpty(blockId) && isBlockCompleted(blockId);
|
||
}
|
||
|
||
#if UNITY_EDITOR
|
||
public override string GetEditorSummary() => string.IsNullOrWhiteSpace(blockId)
|
||
? "Block Completed: <missing ID>"
|
||
: $"Block Completed: {blockId}";
|
||
#endif
|
||
}
|
||
|
||
/// <summary>
|
||
/// 叶子节点:将指定变量的当前值与目标值按比较方式求值。
|
||
/// 既可用于解锁条件,也可用于 Forbidden Condition;例如当路线变量等于另一条路线的值时禁用 Block。
|
||
/// </summary>
|
||
[Serializable]
|
||
[LabelText("Variable Compare")]
|
||
public class VariableCondition : StoryConditionNode
|
||
{
|
||
[HorizontalGroup("VariableComparison", 0.50f)]
|
||
[LabelText("Var")]
|
||
[LabelWidth(24)]
|
||
[Tooltip("章节剧情变量名。使用小写英文、数字和下划线;可由 Yarn 的 set_* 指令写入。")]
|
||
public string variableName;
|
||
|
||
[HorizontalGroup("VariableComparison", 0.22f)]
|
||
[LabelText("Op")]
|
||
[LabelWidth(20)]
|
||
[Tooltip("变量值与目标值之间的比较方式。")]
|
||
public VariableComparison comparison;
|
||
|
||
[HorizontalGroup("VariableComparison")]
|
||
[LabelText("Value")]
|
||
[LabelWidth(35)]
|
||
[Tooltip("用于比较的整数目标值。Bool 条件使用 0 / 1。")]
|
||
public int value;
|
||
|
||
public override bool Evaluate(Func<string, int> getVariable, Func<string, bool> isBlockCompleted)
|
||
{
|
||
int actual = getVariable(variableName);
|
||
return comparison switch
|
||
{
|
||
VariableComparison.Equal => actual == value,
|
||
VariableComparison.NotEqual => actual != value,
|
||
VariableComparison.Greater => actual > value,
|
||
VariableComparison.GreaterOrEqual => actual >= value,
|
||
VariableComparison.Less => actual < value,
|
||
VariableComparison.LessOrEqual => actual <= value,
|
||
_ => false
|
||
};
|
||
}
|
||
|
||
#if UNITY_EDITOR
|
||
public override string GetEditorSummary()
|
||
{
|
||
string variable = string.IsNullOrWhiteSpace(variableName) ? "<missing variable>" : variableName;
|
||
string operation = comparison switch
|
||
{
|
||
VariableComparison.Equal => "==",
|
||
VariableComparison.NotEqual => "!=",
|
||
VariableComparison.Greater => ">",
|
||
VariableComparison.GreaterOrEqual => ">=",
|
||
VariableComparison.Less => "<",
|
||
VariableComparison.LessOrEqual => "<=",
|
||
_ => "?"
|
||
};
|
||
return $"{variable} {operation} {value}";
|
||
}
|
||
#endif
|
||
}
|
||
}
|
||
|
||
#if UNITY_EDITOR
|
||
namespace Ichni.Story.Editor
|
||
{
|
||
/// <summary>
|
||
/// 通用 StoryCondition 的 Odin Inspector Drawer。
|
||
/// <para>Unlock、Forbidden 与 Helper Availability 共用同一条件模型;此 Drawer 只改变编辑器中的
|
||
/// 折叠呈现方式,不修改条件树的序列化结构或运行时求值结果。</para>
|
||
/// <para>默认仅显示可读摘要。需要调整条件时再展开,避免 SerializeReference 条件树长期占据 Block 的编辑空间。</para>
|
||
/// </summary>
|
||
public class StoryConditionDrawer : Sirenix.OdinInspector.Editor.OdinValueDrawer<StoryCondition>
|
||
{
|
||
private bool _isExpanded;
|
||
|
||
protected override void DrawPropertyLayout(UnityEngine.GUIContent label)
|
||
{
|
||
StoryCondition condition = ValueEntry.SmartValue;
|
||
string summary = condition == null ? "None" : condition.GetEditorSummary();
|
||
|
||
// 父属性若保留了简短标签(例如 Helper 的 Availability),则与摘要合并显示;
|
||
// StoryBlock 中使用 HideLabel 时只保留摘要,避免重复占用左侧标签列。
|
||
string prefix = label == null || string.IsNullOrWhiteSpace(label.text)
|
||
? string.Empty
|
||
: $"{label.text}: ";
|
||
|
||
UnityEngine.GUIContent foldoutLabel = new UnityEngine.GUIContent(
|
||
prefix + summary,
|
||
"留空表示未配置条件。展开后可选择 All Of (AND)、Any Of (OR)、Not、Block Completed 或 Variable Compare。"
|
||
);
|
||
|
||
_isExpanded = UnityEditor.EditorGUILayout.Foldout(_isExpanded, foldoutLabel, true);
|
||
if (!_isExpanded)
|
||
{
|
||
return;
|
||
}
|
||
|
||
UnityEditor.EditorGUI.indentLevel++;
|
||
// 继续调用 Odin 的默认 SerializeReference 绘制链,使类型选择、嵌套条件和 Undo 行为保持原样。
|
||
CallNextDrawer(UnityEngine.GUIContent.none);
|
||
UnityEditor.EditorGUI.indentLevel--;
|
||
}
|
||
}
|
||
}
|
||
#endif
|