using System; using System.Collections.Generic; using Sirenix.OdinInspector; using UnityEngine; namespace Ichni.Story { /// /// 通用剧情条件树的节点基类。通过多态组合出“与 / 或 / 非 + 叶子条件”的复合表达式。 /// 由 借助 [SerializeReference] 序列化持有, /// Odin 会以类型下拉框展示可选的节点类型。 /// [Serializable] public abstract class StoryConditionNode { /// /// 对该节点求值。 /// /// 按变量名返回其整型值的委托。 /// 按 blockId 判断该 Block 是否已完成的委托。 public abstract bool Evaluate(Func getVariable, Func isBlockCompleted); #if UNITY_EDITOR /// /// 返回 Inspector 中使用的紧凑摘要。条件编辑器只读取该文本,不影响运行时求值逻辑。 /// public virtual string GetEditorSummary() => GetType().Name; #endif } /// /// 复合节点(逻辑与 AND):所有子条件均满足时才满足。子列表为空视为满足。 /// [Serializable] [LabelText("All Of (AND)")] public class AllOfCondition : StoryConditionNode { [HideLabel] [SerializeReference] [ListDrawerSettings(ShowFoldout = true, DefaultExpandedState = true)] public List conditions = new List(); public override bool Evaluate(Func getVariable, Func 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 } /// /// 复合节点(逻辑或 OR):任一子条件满足即满足。子列表为空视为不满足。 /// [Serializable] [LabelText("Any Of (OR)")] public class AnyOfCondition : StoryConditionNode { [HideLabel] [SerializeReference] [ListDrawerSettings(ShowFoldout = true, DefaultExpandedState = true)] public List conditions = new List(); public override bool Evaluate(Func getVariable, Func 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 } /// /// 复合节点(逻辑非 NOT):对子条件取反。子条件为空视为满足(等价于“非 假”)。 /// [Serializable] [LabelText("Not")] public class NotCondition : StoryConditionNode { [HideLabel] [SerializeReference] public StoryConditionNode condition; public override bool Evaluate(Func getVariable, Func isBlockCompleted) { return condition == null || !condition.Evaluate(getVariable, isBlockCompleted); } #if UNITY_EDITOR public override string GetEditorSummary() => $"Not ({condition?.GetEditorSummary() ?? "None"})"; #endif } /// /// 叶子节点:指定 Block 已完成时满足。 /// [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 getVariable, Func isBlockCompleted) { return !string.IsNullOrEmpty(blockId) && isBlockCompleted(blockId); } #if UNITY_EDITOR public override string GetEditorSummary() => string.IsNullOrWhiteSpace(blockId) ? "Block Completed: " : $"Block Completed: {blockId}"; #endif } /// /// 叶子节点:将指定变量的当前值与目标值按比较方式求值。 /// 既可用于解锁条件,也可用于 Forbidden Condition;例如当路线变量等于另一条路线的值时禁用 Block。 /// [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 getVariable, Func 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) ? "" : 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 { /// /// 通用 StoryCondition 的 Odin Inspector Drawer。 /// Unlock、Forbidden 与 Helper Availability 共用同一条件模型;此 Drawer 只改变编辑器中的 /// 折叠呈现方式,不修改条件树的序列化结构或运行时求值结果。 /// 默认仅显示可读摘要。需要调整条件时再展开,避免 SerializeReference 条件树长期占据 Block 的编辑空间。 /// public class StoryConditionDrawer : Sirenix.OdinInspector.Editor.OdinValueDrawer { 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