diff --git a/Components/Default/ChangedChoice.cs b/Components/Default/ChangedChoice.cs index 8b1fa77..6bcdcf0 100644 --- a/Components/Default/ChangedChoice.cs +++ b/Components/Default/ChangedChoice.cs @@ -10,7 +10,7 @@ namespace Descant.Components [Serializable, MaxQuantity(float.PositiveInfinity), NodeType(DescantNodeType.Choice)] public class ChangedChoice : DescantComponent { - [Inline] public string ActorName; + [Inline] public DescantActor Actor; [ParameterGroup("Index of the choice to change (base 1)")] public int ChoiceNumber; @@ -24,17 +24,13 @@ public class ChangedChoice : DescantComponent public override DescantNodeInvokeResult Invoke(DescantNodeInvokeResult result) { - DescantActor actor = DescantComponentUtilities.GetActor(this, result.Actors, ActorName); - - if (actor == null) return result; - - if ((VariableType.Equals(VariableType.Statistic) && CompareVariable( - actor.StatisticValues[actor.StatisticKeys.IndexOf(VariableName)], Comparison, ComparisonType)) || - (VariableType.Equals(VariableType.Topic) && actor.Topics.Contains(VariableName)) || - (VariableType.Equals(VariableType.Relationship) && CompareVariable( - actor.RelationshipValues[actor.RelationshipKeys.IndexOf(VariableName)], Comparison, ComparisonType)) || - (VariableType.Equals(VariableType.DialogueAttempts) && CompareVariable( - actor.DialogueAttempts, Comparison, ComparisonType))) + if ((VariableType.Equals(VariableType.Statistic) && DescantComponentUtilities.CompareVariable( + Actor.Statistics[VariableName], Comparison, ComparisonType)) || + (VariableType.Equals(VariableType.Topic) && Actor.Topics.Contains(VariableName)) || + (VariableType.Equals(VariableType.Relationship) && DescantComponentUtilities.CompareVariable( + Actor.Relationships[VariableName], Comparison, ComparisonType)) || + (VariableType.Equals(VariableType.DialogueAttempts) && DescantComponentUtilities.CompareVariable( + Actor.DialogueAttempts, Comparison, ComparisonType))) { result.Text[ChoiceNumber - 1] = new KeyValuePair( result.Text[ChoiceNumber - 1].Key, diff --git a/Components/Default/ChangedResponse.cs b/Components/Default/ChangedResponse.cs index 97706bc..6fa64d1 100644 --- a/Components/Default/ChangedResponse.cs +++ b/Components/Default/ChangedResponse.cs @@ -10,7 +10,7 @@ namespace Descant.Components [Serializable, MaxQuantity(float.PositiveInfinity), NodeType(DescantNodeType.Response)] public class ChangedResponse : DescantComponent { - [Inline] public string ActorName; + [Inline] public DescantActor Actor; [ParameterGroup("Variable to check")] public VariableType VariableType; [ParameterGroup("Variable to check")] public string VariableName; @@ -22,17 +22,13 @@ public class ChangedResponse : DescantComponent public override DescantNodeInvokeResult Invoke(DescantNodeInvokeResult result) { - DescantActor actor = DescantComponentUtilities.GetActor(this, result.Actors, ActorName); - - if (actor == null) return result; - - if ((VariableType.Equals(VariableType.Statistic) && CompareVariable( - actor.StatisticValues[actor.StatisticKeys.IndexOf(VariableName)], Comparison, ComparisonType)) || - (VariableType.Equals(VariableType.Topic) && actor.Topics.Contains(VariableName)) || - (VariableType.Equals(VariableType.Relationship) && CompareVariable( - actor.RelationshipValues[actor.RelationshipKeys.IndexOf(VariableName)], Comparison, ComparisonType)) || - (VariableType.Equals(VariableType.DialogueAttempts) && CompareVariable( - actor.DialogueAttempts, Comparison, ComparisonType))) + if ((VariableType.Equals(VariableType.Statistic) && DescantComponentUtilities.CompareVariable( + Actor.Statistics[VariableName], Comparison, ComparisonType)) || + (VariableType.Equals(VariableType.Topic) && Actor.Topics.Contains(VariableName)) || + (VariableType.Equals(VariableType.Relationship) && DescantComponentUtilities.CompareVariable( + Actor.Relationships[VariableName], Comparison, ComparisonType)) || + (VariableType.Equals(VariableType.DialogueAttempts) && DescantComponentUtilities.CompareVariable( + Actor.DialogueAttempts, Comparison, ComparisonType))) { result.Text[0] = new KeyValuePair(result.Text[0].Key, ChangeTo); } diff --git a/Components/Default/LockedChoice.cs b/Components/Default/LockedChoice.cs index 7ad5e27..a972b7a 100644 --- a/Components/Default/LockedChoice.cs +++ b/Components/Default/LockedChoice.cs @@ -8,7 +8,7 @@ namespace Descant.Components [Serializable, MaxQuantity(Single.PositiveInfinity), NodeType(DescantNodeType.Choice)] public class LockedChoice : DescantComponent { - [Inline] public string ActorName; + [Inline] public DescantActor Actor; [ParameterGroup("Index of the choice to change (base 1)")] public int ChoiceNumber; @@ -20,17 +20,13 @@ public class LockedChoice : DescantComponent public override DescantNodeInvokeResult Invoke(DescantNodeInvokeResult result) { - DescantActor actor = DescantComponentUtilities.GetActor(this, result.Actors, ActorName); - - if (actor == null) return result; - - if ((VariableType.Equals(VariableType.Statistic) && CompareVariable( - actor.StatisticValues[actor.StatisticKeys.IndexOf(VariableName)], Comparison, ComparisonType)) || - (VariableType.Equals(VariableType.Topic) && actor.Topics.Contains(VariableName)) || - (VariableType.Equals(VariableType.Relationship) && CompareVariable( - actor.RelationshipValues[actor.RelationshipKeys.IndexOf(VariableName)], Comparison, ComparisonType)) || - (VariableType.Equals(VariableType.DialogueAttempts) && CompareVariable( - actor.DialogueAttempts, Comparison, ComparisonType))) + if ((VariableType.Equals(VariableType.Statistic) && DescantComponentUtilities.CompareVariable( + Actor.Statistics[VariableName], Comparison, ComparisonType)) || + (VariableType.Equals(VariableType.Topic) && Actor.Topics.Contains(VariableName)) || + (VariableType.Equals(VariableType.Relationship) && DescantComponentUtilities.CompareVariable( + Actor.Relationships[VariableName], Comparison, ComparisonType)) || + (VariableType.Equals(VariableType.DialogueAttempts) && DescantComponentUtilities.CompareVariable( + Actor.DialogueAttempts, Comparison, ComparisonType))) { result.Text.RemoveAt(ChoiceNumber - 1); } diff --git a/Components/Default/PortraitChange.cs b/Components/Default/PortraitChange.cs index 9074f03..2662ae5 100644 --- a/Components/Default/PortraitChange.cs +++ b/Components/Default/PortraitChange.cs @@ -8,29 +8,25 @@ namespace Descant.Components [Serializable, MaxQuantity(Single.PositiveInfinity), NodeType(DescantNodeType.Any)] public class PortraitChange : DescantComponent { - [Inline] public string ActorName; + [Inline] public DescantActor Actor; [ParameterGroup("Change to perform")] public PortraitChangeType ChangeType; [ParameterGroup("Change to perform")] public int PortraitIndex; public override DescantNodeInvokeResult Invoke(DescantNodeInvokeResult result) { - DescantActor actor = DescantComponentUtilities.GetActor(this, result.Actors, ActorName); - - if (actor == null) return result; - switch (ChangeType) { case PortraitChangeType.Set: - actor.Portrait = actor.Portraits[PortraitIndex]; + Actor.Portrait = Actor.Portraits[PortraitIndex]; break; case PortraitChangeType.Enable: - actor.PortraitEnabled = true; + Actor.PortraitEnabled = true; break; case PortraitChangeType.Disable: - actor.PortraitEnabled = false; + Actor.PortraitEnabled = false; break; } diff --git a/Components/Default/RandomizedChoice.cs b/Components/Default/RandomizedChoice.cs index e7107e7..0052a8b 100644 --- a/Components/Default/RandomizedChoice.cs +++ b/Components/Default/RandomizedChoice.cs @@ -8,7 +8,7 @@ namespace Descant.Components { [Serializable, MaxQuantity(1), NodeType(DescantNodeType.Choice)] - public class RandomizedChoice : DescantComponent // TODO: make this always last + public class RandomizedChoice : DescantComponent { public override DescantNodeInvokeResult Invoke(DescantNodeInvokeResult result) { diff --git a/Components/Default/RelationshipChange.cs b/Components/Default/RelationshipChange.cs index c0da127..12df011 100644 --- a/Components/Default/RelationshipChange.cs +++ b/Components/Default/RelationshipChange.cs @@ -1,6 +1,7 @@ // Please see https://omch.tech/descant/#relationshipchange for documentation using System; +using System.Linq; using Descant.Utilities; namespace Descant.Components @@ -8,36 +9,29 @@ namespace Descant.Components [Serializable, MaxQuantity(Single.PositiveInfinity), NodeType(DescantNodeType.Any)] public class RelationshipChange : DescantComponent { - [ParameterGroup("Actors")] public string FirstActorName; - [ParameterGroup("Actors")] public string SecondActorName; + [ParameterGroup("Actors")] public DescantActor FirstActor; + [ParameterGroup("Actors")] public DescantActor SecondActor; [ParameterGroup("Operation to perform")] public OperationType OperationType; [ParameterGroup("Operation to perform")] public float OperationValue; public override DescantNodeInvokeResult Invoke(DescantNodeInvokeResult result) { - DescantActor actor = DescantComponentUtilities.GetActor(this, result.Actors, FirstActorName); - - if (actor == null) return result; - - if (!actor.RelationshipKeys.Contains(SecondActorName)) - { - actor.RelationshipKeys.Add(SecondActorName); - actor.RelationshipValues.Add(0); - } + if (!FirstActor.Relationships.Keys.Contains(SecondActor.name)) + FirstActor.Relationships.Add(SecondActor.name, 0); switch (OperationType) { case OperationType.IncreaseBy: - actor.RelationshipValues[actor.RelationshipKeys.IndexOf(SecondActorName)] += OperationValue; + FirstActor.Relationships[SecondActor.name] += OperationValue; break; case OperationType.DecreaseBy: - actor.RelationshipValues[actor.RelationshipKeys.IndexOf(SecondActorName)] -= OperationValue; + FirstActor.Relationships[SecondActor.name] -= OperationValue; break; case OperationType.Set: - actor.RelationshipValues[actor.RelationshipKeys.IndexOf(SecondActorName)] = OperationValue; + FirstActor.Relationships[SecondActor.name] = OperationValue; break; } diff --git a/Components/Default/StatisticChange.cs b/Components/Default/StatisticChange.cs index 5b93fc2..24d1331 100644 --- a/Components/Default/StatisticChange.cs +++ b/Components/Default/StatisticChange.cs @@ -8,7 +8,7 @@ namespace Descant.Components [Serializable, MaxQuantity(Single.PositiveInfinity), NodeType(DescantNodeType.Any)] public class StatisticChange : DescantComponent { - [Inline] public string ActorName; + [Inline] public DescantActor Actor; [ParameterGroup("Statistic to change")] public string StatisticName; @@ -17,22 +17,18 @@ public class StatisticChange : DescantComponent public override DescantNodeInvokeResult Invoke(DescantNodeInvokeResult result) { - DescantActor actor = DescantComponentUtilities.GetActor(this, result.Actors, ActorName); - - if (actor == null) return result; - switch (OperationType) { case OperationType.IncreaseBy: - actor.StatisticValues[actor.StatisticKeys.IndexOf(StatisticName)] += OperationValue; + Actor.Statistics[StatisticName] += OperationValue; break; case OperationType.DecreaseBy: - actor.StatisticValues[actor.StatisticKeys.IndexOf(StatisticName)] -= OperationValue; + Actor.Statistics[StatisticName] -= OperationValue; break; case OperationType.Set: - actor.StatisticValues[actor.StatisticKeys.IndexOf(StatisticName)] = OperationValue; + Actor.Statistics[StatisticName] = OperationValue; break; } diff --git a/Components/Default/StatisticReveal.cs b/Components/Default/StatisticReveal.cs index 6983b65..01a3bdc 100644 --- a/Components/Default/StatisticReveal.cs +++ b/Components/Default/StatisticReveal.cs @@ -9,7 +9,7 @@ namespace Descant.Components [Serializable, MaxQuantity(Single.PositiveInfinity), NodeType(DescantNodeType.Any)] public class StatisticReveal : DescantComponent { - [Inline] public string ActorName; + [Inline] public DescantActor Actor; [ParameterGroup("Statistic to reveal")] public string StatisticName; @@ -19,16 +19,12 @@ public class StatisticReveal : DescantComponent public override DescantNodeInvokeResult Invoke(DescantNodeInvokeResult result) { - DescantActor actor = DescantComponentUtilities.GetActor(this, result.Actors, ActorName); - - if (actor == null || ScriptName == "" || MethodName == "") return result; - if (DescantComponentUtilities.InvokeFromObjectOrScript( this, ObjectTag, ScriptName, MethodName, - actor.StatisticValues[actor.StatisticKeys.IndexOf(StatisticName)].ToString() + Actor.Statistics[StatisticName].ToString() )) return result; DescantComponentUtilities.MissingMethodError(this, ScriptName, MethodName); diff --git a/Components/Default/TopicChange.cs b/Components/Default/TopicChange.cs index 8b683a1..01a03a2 100644 --- a/Components/Default/TopicChange.cs +++ b/Components/Default/TopicChange.cs @@ -8,7 +8,7 @@ namespace Descant.Components [Serializable, MaxQuantity(Single.PositiveInfinity), NodeType(DescantNodeType.Response)] public class TopicChange : DescantComponent { - [Inline] public string ActorName; + [Inline] public DescantActor Actor; [ParameterGroup("Topic to change")] public string TopicName; @@ -16,17 +16,13 @@ public class TopicChange : DescantComponent public override DescantNodeInvokeResult Invoke(DescantNodeInvokeResult result) { - DescantActor actor = DescantComponentUtilities.GetActor(this, result.Actors, ActorName); - - if (actor == null) return result; - switch (ChangeType) { case ListChangeType.Add: - if (!actor.Topics.Contains(TopicName)) actor.Topics.Add(TopicName); + if (!Actor.Topics.Contains(TopicName)) Actor.Topics.Add(TopicName); break; case ListChangeType.Remove: - if (actor.Topics.Contains(TopicName)) actor.Topics.Remove(TopicName); + if (Actor.Topics.Contains(TopicName)) Actor.Topics.Remove(TopicName); break; } diff --git a/Components/DescantActor.cs b/Components/DescantActor.cs index 94d067c..c0761b8 100644 --- a/Components/DescantActor.cs +++ b/Components/DescantActor.cs @@ -24,35 +24,21 @@ public class DescantActor : ScriptableObject /// All the possible portraits that this Actor can switch between (the first Sprite is used by default) /// public Sprite[] Portraits; - - /// - /// The actor's statistics dictionary keys - /// (C# Dictionaries can't be [Serialized], so we use two Lists instead) - /// - public List StatisticKeys; - + /// - /// The actor's statistics dictionary values - /// (C# Dictionaries can't be [Serialized], so we use two Lists instead) + /// The actor's statistics /// - public List StatisticValues; + public SerializableDictionary Statistics; /// /// The actor's topics list /// public List Topics; - - /// - /// The actor's relationships dictionary keys - /// (C# Dictionaries can't be [Serialized], so we use two Lists instead) - /// - public List RelationshipKeys; - + /// - /// The actor's relationships dictionary values - /// (C# Dictionaries can't be [Serialized], so we use two Lists instead) + /// The actor's relationship values /// - public List RelationshipValues; + public SerializableDictionary Relationships; /// /// The number of times that the player has attempted to start a dialogue with the actor @@ -66,8 +52,8 @@ public override string ToString() { string statistics = ""; - for (int i = 0; i < StatisticKeys.Count; i++) - statistics += " (" + StatisticKeys[i] + " " + StatisticValues[i] + ")"; + foreach (var i in Statistics) + statistics += "(" + i.Key + " : " + i.Value + ")"; string topics = ""; @@ -75,9 +61,9 @@ public override string ToString() topics += " " + j; string relationships = ""; - - for (int i = 0; i < RelationshipKeys.Count; i++) - relationships += " (" + RelationshipKeys[i] + " " + RelationshipValues[i] + ")"; + + foreach (var k in Relationships) + statistics += "(" + k.Key + " : " + k.Value + ")"; return GetType() + " (" + (statistics.Length > 0 ? statistics.Substring(1) : "") + ")" + diff --git a/Components/DescantComponent.cs b/Components/DescantComponent.cs index c2cf022..778e30b 100644 --- a/Components/DescantComponent.cs +++ b/Components/DescantComponent.cs @@ -42,39 +42,6 @@ public virtual bool Update() { return true; } - - /// - /// Quickly compares two float values based on some comparison type - /// - /// The main variable in question - /// The value itr is being compared against - /// How the values are being compared - /// Whether the comparison succeeds or not - protected static bool CompareVariable(float variable, float comparison, ComparisonType comparisonType) - { - switch (comparisonType) - { - case ComparisonType.LessThan: - return variable < comparison; - - case ComparisonType.LessThanOrEqualTo: - return variable <= comparison; - - case ComparisonType.EqualTo: - return variable == comparison; - - case ComparisonType.GreaterThanOrEqualTo: - return variable >= comparison; - - case ComparisonType.GreaterThan: - return variable > comparison; - - case ComparisonType.NotEqualTo: - return variable != comparison; - - default: return false; - } - } /// /// Overridden Equals method @@ -95,6 +62,11 @@ public override bool Equals(object other) return Equals((DescantComponent)other); } + public override int GetHashCode() + { + return Collapsed.GetHashCode(); + } + /// /// Custom Equals method /// diff --git a/Components/DescantComponentUtilities.cs b/Components/DescantComponentUtilities.cs index 230a184..c6e971b 100644 --- a/Components/DescantComponentUtilities.cs +++ b/Components/DescantComponentUtilities.cs @@ -207,6 +207,39 @@ public static void MissingMethodError(DescantComponent component, string scriptN } #endregion + + /// + /// Quickly compares two float values based on some comparison type + /// + /// The main variable in question + /// The value itr is being compared against + /// How the values are being compared + /// Whether the comparison succeeds or not + public static bool CompareVariable(float variable, float comparison, ComparisonType comparisonType) + { + switch (comparisonType) + { + case ComparisonType.LessThan: + return variable < comparison; + + case ComparisonType.LessThanOrEqualTo: + return variable <= comparison; + + case ComparisonType.EqualTo: + return variable == comparison; + + case ComparisonType.GreaterThanOrEqualTo: + return variable >= comparison; + + case ComparisonType.GreaterThan: + return variable > comparison; + + case ComparisonType.NotEqualTo: + return variable != comparison; + + default: return false; + } + } /// /// Gets an actor with a given name from the a list of actors @@ -247,14 +280,9 @@ public static T ParseEnum(string value) where T : Enum /// The actor to be assigned from public static void AssignActor(DescantActor data, DescantActor actor) { - data.StatisticKeys = actor.StatisticKeys; - data.StatisticValues = actor.StatisticValues; - + data.Statistics = actor.Statistics; data.Topics = actor.Topics; - - data.RelationshipKeys = actor.RelationshipKeys; - data.RelationshipValues = actor.RelationshipValues; - + data.Relationships = actor.Relationships; data.DialogueAttempts = actor.DialogueAttempts; } } diff --git a/Components/SerializableDictionary.meta b/Components/SerializableDictionary.meta new file mode 100644 index 0000000..58c0d8d --- /dev/null +++ b/Components/SerializableDictionary.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 92457a38d7647bf43aac9ee7db913593 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Components/SerializableDictionary/SerializableDictionary.cs b/Components/SerializableDictionary/SerializableDictionary.cs new file mode 100644 index 0000000..8febb0f --- /dev/null +++ b/Components/SerializableDictionary/SerializableDictionary.cs @@ -0,0 +1,113 @@ +// This code copied and modified from: https://github.com/EduardMalkhasyan/Serializable-Dictionary-Unity + +using System.Linq; +using System.Collections.Generic; +using UnityEngine; + +namespace Descant.Components +{ + [System.Serializable] + public class SerializableDictionary : Dictionary, ISerializationCallbackReceiver + { + [SerializeField] private List> dictionaryList = new(); + + void ISerializationCallbackReceiver.OnBeforeSerialize() + { + foreach (var kVP in this) + { + if (dictionaryList.FirstOrDefault(value => this.Comparer.Equals(value.Key, kVP.Key)) + is SerializedDictionaryKVPProps serializedKVP) + { + serializedKVP.Value = kVP.Value; + } + else + { + dictionaryList.Add(kVP); + } + } + + dictionaryList.RemoveAll(value => ContainsKey(value.Key) == false); + + for (int i = 0; i < dictionaryList.Count; i++) + { + dictionaryList[i].index = i; + } + } + + void ISerializationCallbackReceiver.OnAfterDeserialize() + { + Clear(); + + dictionaryList.RemoveAll(r => r.Key == null); + + foreach (var serializedKVP in dictionaryList) + { + if (!(serializedKVP.isKeyDuplicated = ContainsKey(serializedKVP.Key))) + { + Add(serializedKVP.Key, serializedKVP.Value); + } + } + } + + public new TValue this[TKey key] + { + get + { +#if UNITY_EDITOR + if (ContainsKey(key)) + { + var duplicateKeysWithCount = dictionaryList.GroupBy(item => item.Key) + .Where(group => group.Count() > 1) + .Select(group => new { Key = group.Key, Count = group.Count() }); + + foreach (var duplicatedKey in duplicateKeysWithCount) + { + Debug.LogError($"Key '{duplicatedKey.Key}' is duplicated {duplicatedKey.Count} times in the dictionary."); + } + + return base[key]; + } + else + { + Debug.LogError($"Key '{key}' not found in dictionary."); + return default(TValue); + } +#else + return base[key]; +#endif + } + + set + { + var keys = dictionaryList; + + foreach (var i in keys) + { + if (i.Key.Equals(key)) + { + i.Value = value; + break; + } + } + } + } + + [System.Serializable] + public class SerializedDictionaryKVPProps + { + public TypeKey Key; + public TypeValue Value; + + public int index; + public bool isKeyDuplicated; + + public SerializedDictionaryKVPProps(TypeKey key, TypeValue value) { this.Key = key; this.Value = value; } + + public static implicit operator SerializedDictionaryKVPProps(KeyValuePair kvp) + => new SerializedDictionaryKVPProps(kvp.Key, kvp.Value); + public static implicit operator KeyValuePair(SerializedDictionaryKVPProps kvp) + => new KeyValuePair(kvp.Key, kvp.Value); + } + } + +} \ No newline at end of file diff --git a/Components/SerializableDictionary/SerializableDictionary.cs.meta b/Components/SerializableDictionary/SerializableDictionary.cs.meta new file mode 100644 index 0000000..0b7f46b --- /dev/null +++ b/Components/SerializableDictionary/SerializableDictionary.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 08185d6eb814648ce9cdfca048e1611b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Components/SerializableDictionary/SerializableDictionaryDrawer.cs b/Components/SerializableDictionary/SerializableDictionaryDrawer.cs new file mode 100644 index 0000000..bd2e8dc --- /dev/null +++ b/Components/SerializableDictionary/SerializableDictionaryDrawer.cs @@ -0,0 +1,354 @@ +// This code copied and modified from: https://github.com/EduardMalkhasyan/Serializable-Dictionary-Unity + +#if UNITY_EDITOR +using UnityEngine; +using System.Collections.Generic; +using UnityEditor; +using UnityEditorInternal; + +namespace Descant.Components +{ + [CustomPropertyDrawer(typeof(SerializableDictionary<,>), true)] + public class SerializableDictionaryDrawer : PropertyDrawer + { + public override void OnGUI(Rect rect, SerializedProperty prop, GUIContent label) + { + var indentedRect = EditorGUI.IndentedRect(rect); + + void Head() + { + var headerRect = indentedRect; + headerRect.height = EditorGUIUtility.singleLineHeight; + + void ExpandablePanel() + { + var fullHeaderRect = new Rect(headerRect); + fullHeaderRect.x -= 17; + fullHeaderRect.width += 34; + + if (UnityEngine.Event.current != null && fullHeaderRect.Contains(UnityEngine.Event.current.mousePosition)) + { + Color transparentGrey = new Color(0.4f, 0.4f, 0.4f, 0.4f); + EditorGUI.DrawRect(fullHeaderRect, transparentGrey); + } + + GUI.color = Color.clear; + + if (GUI.Button(new Rect(fullHeaderRect.x, fullHeaderRect.y, fullHeaderRect.width - 40, + fullHeaderRect.height), "")) + { + prop.isExpanded = !prop.isExpanded; + } + + GUI.color = Color.white; + + var triangleRect = rect; + triangleRect.height = EditorGUIUtility.singleLineHeight; + triangleRect.x -= 15; + + EditorGUI.Foldout(triangleRect, prop.isExpanded, ""); + } + + void DisplayName() + { + GUI.color = Color.white; + GUI.Label(headerRect, prop.displayName); + GUI.color = Color.white; + GUI.skin.label.fontSize = 12; + GUI.skin.label.fontStyle = FontStyle.Normal; + GUI.skin.label.alignment = TextAnchor.MiddleLeft; + } + + void DuplicatedKeysWarning() + { + if (UnityEngine.Event.current != null && UnityEngine.Event.current.type != EventType.Repaint) + { + return; + } + + var hasRepeated = false; + var repeatedKeys = new List(); + + for (int i = 0; i < dictionaryList.arraySize; i++) + { + SerializedProperty isKeyRepeatedProperty = dictionaryList.GetArrayElementAtIndex(i) + .FindPropertyRelative("isKeyDuplicated"); + + if (isKeyRepeatedProperty.boolValue) + { + hasRepeated = true; + SerializedProperty keyProperty = dictionaryList.GetArrayElementAtIndex(i).FindPropertyRelative("Key"); + string keyString = GetSerializedPropertyValueAsString(keyProperty); + repeatedKeys.Add(keyString); + } + } + + if (!hasRepeated) + { + return; + } + + float with = GUI.skin.label.CalcSize(new GUIContent(prop.displayName)).x; + headerRect.x += with + 24f; + var warningRect = headerRect; + Rect warningRectIcon = new Rect(headerRect.x - 18, headerRect.y, headerRect.width, headerRect.height); + GUI.color = Color.white; + GUI.Label(warningRectIcon, EditorGUIUtility.IconContent("console.erroricon")); + GUI.color = new Color(1.0f, 0.443f, 0.443f); + GUI.skin.label.fontStyle = FontStyle.Bold; + GUI.Label(warningRect, "Duplicated keys: " + string.Join(", ", repeatedKeys)); + GUI.color = Color.white; + GUI.skin.label.fontStyle = FontStyle.Normal; + } + + string GetSerializedPropertyValueAsString(SerializedProperty property) + { + switch (property.propertyType) + { + case SerializedPropertyType.Integer: + return property.intValue.ToString(); + case SerializedPropertyType.Boolean: + return property.boolValue.ToString(); + case SerializedPropertyType.Float: + return property.floatValue.ToString(); + case SerializedPropertyType.String: + return property.stringValue; + default: + return "(Unsupported Type)"; + } + } + + ExpandablePanel(); + DisplayName(); + DuplicatedKeysWarning(); + } + + void List() + { + if (!prop.isExpanded) + { + return; + } + + SetupList(prop); + + float newHeight = indentedRect.height - EditorGUIUtility.singleLineHeight - 3; + indentedRect.y += indentedRect.height - newHeight; + indentedRect.height = newHeight; + + reorderableList.DoList(indentedRect); + } + + SetupProps(prop); + + Head(); + List(); + } + + public override float GetPropertyHeight(SerializedProperty prop, GUIContent label) + { + SetupProps(prop); + + var height = EditorGUIUtility.singleLineHeight; + + if (prop.isExpanded) + { + SetupList(prop); + height += reorderableList.GetHeight() + 5; + } + + return height; + } + + private float GetListElementHeight(int index) + { + var kvpProp = dictionaryList.GetArrayElementAtIndex(index); + var keyProp = kvpProp.FindPropertyRelative("Key"); + var valueProp = kvpProp.FindPropertyRelative("Value"); + + float GetPropertyHeight(SerializedProperty prop) + { + if (IsSingleLine(prop)) + { + return EditorGUI.GetPropertyHeight(prop); + } + + var height = 1f; + + foreach (var childProp in GetChildren(prop, false)) + { + height += EditorGUI.GetPropertyHeight(childProp) + 1; + } + + height += 10; + + return height; + } + + return Mathf.Max(GetPropertyHeight(keyProp), GetPropertyHeight(valueProp)); + } + + void DrawListElement(Rect rect, int index, bool isActive, bool isFocused) + { + Rect keyRect; + Rect valueRect; + Rect dividerRect; + + var kvpProp = dictionaryList.GetArrayElementAtIndex(index); + var keyProp = kvpProp.FindPropertyRelative("Key"); + var valueProp = kvpProp.FindPropertyRelative("Value"); + + void Draw(Rect rect, SerializedProperty prop) + { + if (IsSingleLine(prop)) + { + rect.height = EditorGUIUtility.singleLineHeight; + EditorGUI.PropertyField(rect, prop, GUIContent.none); + } + else + { + foreach (var childProp in GetChildren(prop, false)) + { + var childPropHeight = EditorGUI.GetPropertyHeight(childProp); + rect.height = childPropHeight; + EditorGUI.PropertyField(rect, childProp, true); + rect.y += childPropHeight + 2; + } + } + } + + void DrawRects() + { + var dividerWidh = IsSingleLine(valueProp) ? 6 : 16f; + var dividerPosition = 0.25f; + + var fullRect = rect; + fullRect.width -= 1; + fullRect.height -= 2; + + keyRect = fullRect; + keyRect.width *= dividerPosition; + keyRect.width -= dividerWidh / 2; + + valueRect = fullRect; + valueRect.x += fullRect.width * dividerPosition; + valueRect.width *= (1 - dividerPosition); + valueRect.width -= dividerWidh / 2; + + dividerRect = fullRect; + dividerRect.x += fullRect.width * dividerPosition - dividerWidh / 2; + dividerRect.width = dividerWidh; + } + + void Key() + { + Draw(keyRect, keyProp); + + if (kvpProp.FindPropertyRelative("isKeyDuplicated").boolValue) + { + GUI.Label(new Rect(keyRect.x + keyRect.width - 20, keyRect.y - 1, 20, 20), + EditorGUIUtility.IconContent("console.erroricon")); + } + } + + void Value() + { + Draw(valueRect, valueProp); + } + + void Divider() + { + EditorGUIUtility.AddCursorRect(dividerRect, MouseCursor.ResizeHorizontal); + + if (UnityEngine.Event.current == null || rect.Contains(UnityEngine.Event.current.mousePosition) == false) + { + return; + } + + if (UnityEngine.Event.current != null && dividerRect.Contains(UnityEngine.Event.current.mousePosition)) + { + if (UnityEngine.Event.current.type == EventType.MouseDown) + { + isDividerDragged = true; + } + else if (UnityEngine.Event.current.type == EventType.MouseUp + || UnityEngine.Event.current.type == EventType.MouseMove + || UnityEngine.Event.current.type == EventType.MouseLeaveWindow) + { + isDividerDragged = false; + } + } + + if (isDividerDragged && UnityEngine.Event.current != null && UnityEngine.Event.current.type == EventType.MouseDrag) + { + dividerPosProp.floatValue = Mathf.Clamp(dividerPosProp.floatValue + UnityEngine.Event.current.delta.x / rect.width, .2f, .8f); + } + } + + DrawRects(); + Key(); + Value(); + Divider(); + } + + private void ShowDictIsEmptyMessage(Rect rect) + { + GUI.Label(rect, "Empty"); + } + + private IEnumerable GetChildren(SerializedProperty prop, bool enterVisibleGrandchildren) + { + prop = prop.Copy(); + + var startPath = prop.propertyPath; + + var enterVisibleChildren = true; + + while (prop.NextVisible(enterVisibleChildren) && prop.propertyPath.StartsWith(startPath)) + { + yield return prop; + enterVisibleChildren = enterVisibleGrandchildren; + } + } + + private bool IsSingleLine(SerializedProperty prop) + { + return prop.propertyType != SerializedPropertyType.Generic || prop.hasVisibleChildren == false; + } + + private void SetupList(SerializedProperty prop) + { + if (reorderableList != null) + { + return; + } + + SetupProps(prop); + + this.reorderableList = new ReorderableList(dictionaryList.serializedObject, dictionaryList, true, false, true, true); + this.reorderableList.drawElementCallback = DrawListElement; + this.reorderableList.elementHeightCallback = GetListElementHeight; + this.reorderableList.drawNoneElementCallback = ShowDictIsEmptyMessage; + } + + private ReorderableList reorderableList; + private bool isDividerDragged; + + public void SetupProps(SerializedProperty prop) + { + if (this.property != null) + { + return; + } + + this.property = prop; + this.dictionaryList = prop.FindPropertyRelative("dictionaryList"); + this.dividerPosProp = prop.FindPropertyRelative("dividerPos"); + } + + private SerializedProperty property; + private SerializedProperty dictionaryList; + private SerializedProperty dividerPosProp; + } +} +#endif \ No newline at end of file diff --git a/Components/SerializableDictionary/SerializableDictionaryDrawer.cs.meta b/Components/SerializableDictionary/SerializableDictionaryDrawer.cs.meta new file mode 100644 index 0000000..c61d666 --- /dev/null +++ b/Components/SerializableDictionary/SerializableDictionaryDrawer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: af44f85b3a51e40cb8b1285fb308b2a7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Descant-1.1.4.zip b/Descant-1.1.4.zip deleted file mode 100644 index 2924e69..0000000 Binary files a/Descant-1.1.4.zip and /dev/null differ diff --git a/Descant-1.1.4.unitypackage.meta b/Descant-1.2.0.unitypackage.meta similarity index 74% rename from Descant-1.1.4.unitypackage.meta rename to Descant-1.2.0.unitypackage.meta index e042891..85c0b68 100644 --- a/Descant-1.1.4.unitypackage.meta +++ b/Descant-1.2.0.unitypackage.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: c88a4fc80fad880489035188a8a5e2f7 +guid: 2f6e602d0b00920449b42f922ec44a68 DefaultImporter: externalObjects: {} userData: diff --git a/Descant-1.2.0.zip b/Descant-1.2.0.zip new file mode 100644 index 0000000..d9b6335 Binary files /dev/null and b/Descant-1.2.0.zip differ diff --git a/Descant-1.1.4.zip.meta b/Descant-1.2.0.zip.meta similarity index 74% rename from Descant-1.1.4.zip.meta rename to Descant-1.2.0.zip.meta index 293955c..a55c393 100644 --- a/Descant-1.1.4.zip.meta +++ b/Descant-1.2.0.zip.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 6a871d2cf7690354099f65307129a742 +guid: 06b578885d4d03946897cce2a9c57ee3 DefaultImporter: externalObjects: {} userData: diff --git a/Documentation/todo.md b/Documentation/todo.md index e1448ca..7124c91 100644 --- a/Documentation/todo.md +++ b/Documentation/todo.md @@ -27,7 +27,7 @@ - ~~Make autosave better (removed temporarily)~~ - ~~Create log system~~ - ~~Create an online Component documentation website~~ -- Ctrl-S saving functionality for editors +- ~~Ctrl-S saving functionality for editors~~ - ~~Save panned position in `Descant Graph Editor`~~ @@ -37,7 +37,7 @@ - ~~Bug where changeportrait dropdown can't be changed~~ - ~~Allow Components to be rearranged~~ - Make it more obvious what data is being referenced by components - - Allow actors and other data to be dragged and dropped + - Allow ~~actors~~ and other data to be dragged and dropped - Show thumbnails - Indicate that scripts and methods are there when typing in the components - Add If Node diff --git a/Editor/Data/DescantGraph.cs b/Editor/Data/DescantGraph.cs index 5356a83..59b22e8 100644 --- a/Editor/Data/DescantGraph.cs +++ b/Editor/Data/DescantGraph.cs @@ -105,7 +105,7 @@ public class DescantGraph : ScriptableObject public DescantGraph() { Autosave = false; - Advanced = false; + Advanced = true; PannedPosition = Vector3.zero; ScrolledScale = Vector3.one; Typewriter = true; diff --git a/Editor/DescantEditorUtilities.cs b/Editor/DescantEditorUtilities.cs index b273098..4d0a2ca 100644 --- a/Editor/DescantEditorUtilities.cs +++ b/Editor/DescantEditorUtilities.cs @@ -7,6 +7,8 @@ namespace Descant.Editor { public static class DescantEditorUtilities { + public const string VERSION = "1.2.0"; + #if UNITY_EDITOR #region VisualElements @@ -146,13 +148,13 @@ public static void AddStyleSheet(VisualElementStyleSheetSet styleSheetSet, strin string[] paths = { "Packages/com.owmacohe.descant/Resources/", - "Packages/Descant-1.1.4/Resources/", + "Packages/Descant-" + VERSION + "/Resources/", "Assets/com.owmacohe.descant/Resources/", - "Assets/Descant-1.1.4/Resources/", + "Assets/Descant-" + VERSION + "/Resources/", "Assets/com.owmacohe.descant/Resources/", - "Assets/Packages/Descant-1.1.4/Resources/" + "Assets/Packages/Descant-" + VERSION + "/Resources/" }; StyleSheet styleSheet = null; diff --git a/Editor/DescantNodeComponentVisualElement.cs b/Editor/DescantNodeComponentVisualElement.cs index 1bdd5d7..eddecc8 100644 --- a/Editor/DescantNodeComponentVisualElement.cs +++ b/Editor/DescantNodeComponentVisualElement.cs @@ -5,6 +5,7 @@ using System.Reflection; using Descant.Components; using Descant.Utilities; +using UnityEditor.UIElements; using UnityEngine; using UnityEngine.UIElements; @@ -147,12 +148,16 @@ public void Draw() // Adding the name TextElement name = new TextElement(); + name.AddToClassList("node_component_name"); name.text = componentName; top_row_left.Add(name); // Adding the collapsible section collapsible = new VisualElement(); Add(collapsible); + + PropertyField test = new PropertyField(); + Add(test); // Creating the first (default) row ScrollView paramRow1 = new ScrollView(); @@ -198,18 +203,18 @@ public void Draw() var componentValue = i.GetValue(Component); // The current value of this parameter // If the parameter type is one that can just be filled in with a field... - if (i.FieldType == typeof(float) || i.FieldType == typeof(int) || i.FieldType == typeof(string)) + if (i.FieldType == typeof(string)) { // Adding a new field - TextField temp = new TextField(); - temp.label = label; - CheckAndAddParamLine(temp, i); + TextField field = new TextField(); + field.label = label; + CheckAndAddParamLine(field, i); // Setting the field's value - if (componentValue != null) temp.value = componentValue.ToString(); + if (componentValue != null) field.value = componentValue.ToString(); // When the field's value is changed... - temp.RegisterValueChangedCallback(callback => + field.RegisterValueChangedCallback(callback => { // Checking to see if the field should be filtered, and if so, doing that try @@ -221,7 +226,7 @@ public void Draw() } catch { - temp.value = DescantUtilities.FilterText(temp.value, i.FieldType != typeof(string)); + field.value = DescantUtilities.FilterText(field.value, i.FieldType != typeof(string)); } try @@ -240,18 +245,141 @@ public void Draw() graphView.Editor.CheckAndSave(); // Check for autosave }); } + // If the parameter type is a float... + else if (i.FieldType == typeof(float)) + { + FloatField field = new FloatField(); + field.label = label; + CheckAndAddParamLine(field, i); + + // Setting the field's value + if (componentValue != null) field.value = (float) componentValue; + + // When the field's value is changed... + field.RegisterValueChangedCallback(callback => + { + // Saving the change to the Component object + i.SetValue(Component, callback.newValue); + + graphView.Editor.CheckAndSave(); // Check for autosave + }); + + } + // If the parameter type is an int... + else if (i.FieldType == typeof(int)) + { + IntegerField field = new IntegerField(); + field.label = label; + CheckAndAddParamLine(field, i); + + // Setting the field's value + if (componentValue != null) field.value = (int) componentValue; + + // When the field's value is changed... + field.RegisterValueChangedCallback(callback => + { + // Saving the change to the Component object + i.SetValue(Component, callback.newValue); + + graphView.Editor.CheckAndSave(); // Check for autosave + }); + + } // If the parameter type is a boolean... else if (i.FieldType == typeof(bool)) { // Adding a new toggle - Toggle temp = new Toggle(); - temp.label = label; - CheckAndAddParamLine(temp, i); + Toggle field = new Toggle(); + field.label = label; + CheckAndAddParamLine(field, i); // Setting the toggle's value - if (componentValue != null) temp.value = (bool) componentValue; + if (componentValue != null) field.value = (bool) componentValue; + + // When the field's value is changed... + field.RegisterValueChangedCallback(callback => + { + // Saving the change to the Component object + i.SetValue(Component, callback.newValue); + + graphView.Editor.CheckAndSave(); // Check for autosave + }); + } + // If the parameter type is a Vector2... + else if (i.FieldType == typeof(Vector2)) + { + // Adding a new field + Vector2Field field = new Vector2Field(); + field.label = label; + CheckAndAddParamLine(field, i); + + // Setting the field's value + if (componentValue != null) field.value = (Vector2) componentValue; + + // When the field's value is changed... + field.RegisterValueChangedCallback(callback => + { + // Saving the change to the Component object + i.SetValue(Component, callback.newValue); + + graphView.Editor.CheckAndSave(); // Check for autosave + }); + } + // If the parameter type is a Vector3... + else if (i.FieldType == typeof(Vector3)) + { + // Adding a new field + Vector3Field field = new Vector3Field(); + field.label = label; + CheckAndAddParamLine(field, i); + + // Setting the field's value + if (componentValue != null) field.value = (Vector3) componentValue; + + // When the field's value is changed... + field.RegisterValueChangedCallback(callback => + { + // Saving the change to the Component object + i.SetValue(Component, callback.newValue); + + graphView.Editor.CheckAndSave(); // Check for autosave + }); + } + // If the parameter type is a Color... + else if (i.FieldType == typeof(Color)) + { + // Adding a new field + ColorField field = new ColorField(); + field.label = label; + CheckAndAddParamLine(field, i); + + // Setting the field's value + if (componentValue != null) field.value = (Color) componentValue; - temp.RegisterValueChangedCallback(callback => + // When the field's value is changed... + field.RegisterValueChangedCallback(callback => + { + // Saving the change to the Component object + i.SetValue(Component, callback.newValue); + + graphView.Editor.CheckAndSave(); // Check for autosave + }); + } + // If the parameter type is an Object... + else if (i.FieldType.IsSubclassOf(typeof(UnityEngine.Object))) + { + // Adding a new field + ObjectField field = new ObjectField(); + field.objectType = i.FieldType; + field.allowSceneObjects = false; + field.label = label; + CheckAndAddParamLine(field, i); + + // Setting the field's value + if (componentValue != null) field.value = (UnityEngine.Object) componentValue; + + // When the field's value is changed... + field.RegisterValueChangedCallback(callback => { // Saving the change to the Component object i.SetValue(Component, callback.newValue); @@ -267,44 +395,17 @@ public void Draw() // Getting the full list of the enum's options foreach (var j in i.FieldType.GetFields()) enumValues.Add(j.ToString().Substring(j.ToString().LastIndexOf(' ') + 1)); + + EnumField field = new EnumField(); + field.Init((Enum) componentValue); + field.label = label; + CheckAndAddParamLine(field, i); - // Adding a new dropdown - PopupField temp = new PopupField( - enumValues.GetRange(1, enumValues.Count - 1), 0); - temp.label = label; - CheckAndAddParamLine(temp, i); - - // Setting the dropdown's value - if (componentValue != null) temp.value = componentValue.ToString(); - - temp.RegisterValueChangedCallback(callback => + // When the field's value is changed... + field.RegisterValueChangedCallback(callback => { - string enumName = i.FieldType.Name; - // Saving the change to the Component object - // (we need to know what kind of enum it is so we can parse it accordingly) - switch (enumName.Substring(enumName.LastIndexOf('.') + 1)) - { - case "VariableType": - i.SetValue(Component, DescantComponentUtilities.ParseEnum(callback.newValue)); - break; - - case "ComparisonType": - i.SetValue(Component, DescantComponentUtilities.ParseEnum(callback.newValue)); - break; - - case "OperationType": - i.SetValue(Component, DescantComponentUtilities.ParseEnum(callback.newValue)); - break; - - case "ListChangeType": - i.SetValue(Component, DescantComponentUtilities.ParseEnum(callback.newValue)); - break; - - case "PortraitChangeType": - i.SetValue(Component, DescantComponentUtilities.ParseEnum(callback.newValue)); - break; - } + i.SetValue(Component, callback.newValue); graphView.Editor.CheckAndSave(); // Check for autosave }); diff --git a/Editor/Nodes/DescantEndNode.cs b/Editor/Nodes/DescantEndNode.cs index 9017641..81c1204 100644 --- a/Editor/Nodes/DescantEndNode.cs +++ b/Editor/Nodes/DescantEndNode.cs @@ -39,7 +39,7 @@ public DescantEndNode( GraphView.EndNodeID++; } - style.width = 350; + style.minWidth = 350; // Initializing the input port Port input = InstantiatePort(Orientation.Horizontal, Direction.Input, Port.Capacity.Multi, typeof(bool)); diff --git a/Editor/Nodes/DescantNode.cs b/Editor/Nodes/DescantNode.cs index accc369..324b791 100644 --- a/Editor/Nodes/DescantNode.cs +++ b/Editor/Nodes/DescantNode.cs @@ -239,7 +239,7 @@ public void RearrangeComponent(DescantNodeComponentVisualElement component, int VisualComponents.Remove(component); VisualComponents.Insert(index, component); - extensionContainer.Insert(2 + index, component); // Removing and inserting it in the Node + componentParent.Insert(index, component); // Removing and inserting it in the Node if (update) UpdateComponents(); } diff --git a/Editor/Nodes/DescantStartNode.cs b/Editor/Nodes/DescantStartNode.cs index 810cbd6..863d41a 100644 --- a/Editor/Nodes/DescantStartNode.cs +++ b/Editor/Nodes/DescantStartNode.cs @@ -32,7 +32,7 @@ public DescantStartNode( { base.Draw(); // Making sure that the parent has been drawn - style.width = 350; + style.minWidth = 350; // Initializing the output port Port output = InstantiatePort(Orientation.Horizontal, Direction.Output, Port.Capacity.Single, typeof(bool)); diff --git a/Editor/Window/DescantEditor.cs b/Editor/Window/DescantEditor.cs index 5fb2d27..906b77f 100644 --- a/Editor/Window/DescantEditor.cs +++ b/Editor/Window/DescantEditor.cs @@ -37,6 +37,10 @@ public class DescantEditor : EditorWindow bool GUICreated; // Whether a Descant graph is currently loaded into the editor bool rightCtrl, leftCtrl; // Whether or not the right and left control/command keys are being held down + + string lastSearch; // The last searched query in the search bar + List foundElements = new List(); // The last found elements that had a field that contained the query + int foundElementIndex; // The current index within the found elements that we have snapped to [MenuItem("Window/Descant/Graph Editor"), MenuItem("Descant/Graph Editor")] public static void Open() @@ -141,9 +145,14 @@ void AddToolbar() unsaved = new TextElement(); unsaved.AddToClassList("toolbar-unsaved"); unsaved.text = "*"; - unsaved.visible = false; + unsaved.style.display = new StyleEnum(DisplayStyle.None); toolbarTitle.Add(unsaved); + TextElement version = new TextElement(); + version.AddToClassList("version"); + version.text = "Descant v" + DescantEditorUtilities.VERSION; + toolbarTitle.Add(version); + // Initializing the save section VisualElement saveSection = new VisualElement(); saveSection.AddToClassList("save-section"); @@ -213,6 +222,8 @@ void AddToolbar() close.clicked += Unload; close.text = "Close"; saveSection.Add(close); + + lastSearch = ""; // Making sure that the lastSearch value is empty when the editor is reloaded // Initializing the searchbar search = new TextField("Search:"); @@ -223,8 +234,44 @@ void AddToolbar() search.RegisterCallback(evt => { // Clearing the search find if we find a match - if (evt.keyCode.Equals(KeyCode.Return) && !search.value.Equals("")) - if (Search(search.value)) search.value = ""; + if (evt.keyCode.Equals(KeyCode.Return)) + { + if (!search.value.Equals("")) + { + // If this is a new search query, we find all the field that contain it + if (!search.value.Equals(lastSearch)) + { + // Finding the fields and setting the label accordingly + foundElements = Search(search.value); + search.label = "(1/" + foundElements.Count + ") Search:"; + + // Snapping to the first result + if (foundElements.Count > 0) + { + lastSearch = search.value; + + SnapTo(foundElements[foundElementIndex]); + } + } + // If this is a continued query, we snap to the next one + else + { + foundElementIndex++; + if (foundElementIndex >= foundElements.Count) foundElementIndex = 0; + + // Updating the label accordingly + search.label = "(" + (foundElementIndex + 1) + "/" + foundElements.Count + ") Search:"; + + SnapTo(foundElements[foundElementIndex]); // Snapping to the next result + } + } + else search.label = "Search:"; + } + }); + + search.RegisterValueChangedCallback(callback => + { + if (callback.newValue.Equals("")) search.label = "Search:"; }); SetAdvanced(advanced.value); @@ -581,7 +628,7 @@ public void CheckAndSave() if (autoSave.value) Save(); // Getting a copy of the current data to compare it with the saved data else if (!data.Equals(GetData())) - unsaved.visible = true; + unsaved.style.display = new StyleEnum(DisplayStyle.Flex); } } @@ -599,7 +646,7 @@ void PlayCheckAndSave(PlayModeStateChange change) void Save() { DescantEditorUtilities.FindFirstElement(toolbar).text = data.name; - unsaved.visible = false; + unsaved.style.display = new StyleEnum(DisplayStyle.None); AssignData(); @@ -628,7 +675,7 @@ public void Load(DescantGraph graph) /// void Unload() { - Save(); + CheckAndSave(); data = null; @@ -645,15 +692,20 @@ void Unload() /// Default method to be called when searching for text in the graph /// /// The phrase that should be looked for - /// Whether or not a field somewhere in the graph was found that contained the phrase - bool Search(string query) + /// A list of all DescantNodes and DescantNodeGroups that have a field that contained the phrase + List Search(string query) { + List elements = new List(); + string formattedQuery = query.Trim().ToLower(); - if (SearchNodes(formattedQuery)) return true; - if (SearchGroups(formattedQuery)) return true; + foreach (var i in SearchNodes(formattedQuery)) + elements.Add(i); + + foreach (var j in SearchGroups(formattedQuery)) + elements.Add(j); - return false; + return elements; } /// @@ -661,13 +713,16 @@ bool Search(string query) /// (see default Search() method for more info) /// /// The formatted phrase that should be looked for - /// Whether or not a field somewhere in a DescantNode was found that contained the phrase - bool SearchNodes(string query) + /// A list of all DescantNodes that have a field that contained the phrase + List SearchNodes(string query) { + List nodes = new List(); + foreach (var i in DescantEditorUtilities.FindAllElements(graphView)) - if (SearchElement(i, query)) return true; + if (SearchElement(i, query)) + nodes.Add(i); - return false; + return nodes; } /// @@ -675,13 +730,16 @@ bool SearchNodes(string query) /// (see default Search() method for more info) /// /// The formatted phrase that should be looked for - /// Whether or not a field somewhere in a DescantNodeGroup was found that contained the phrase - bool SearchGroups(string query) + /// A list of all DescantNodeGroups that have a field that contained the phrase + List SearchGroups(string query) { + List groups = new List(); + foreach (var i in DescantEditorUtilities.FindAllElements(graphView)) - if (SearchElement(i, query)) return true; + if (SearchElement(i, query)) + groups.Add(i); - return false; + return groups; } /// @@ -698,10 +756,7 @@ bool SearchElement(GraphElement element, string query) string formattedValue = i.value.ToLower(); if (!formattedValue.Equals("") && formattedValue.Contains(query)) - { - SnapTo(element, i); // Snapping the view to the target element when its found return true; - } } return false; @@ -711,8 +766,7 @@ bool SearchElement(GraphElement element, string query) /// Quick method to snap the Descant Graph's view to a particular VisualElement within the graph /// /// The element to be snapped to - /// The TextField that the search query was found in - void SnapTo(GraphElement target, TextField field) + void SnapTo(GraphElement target) { Rect temp = target.GetPosition(); diff --git a/Examples/Test_Graph.asset b/Examples/Test_Graph.asset index 1098bb1..ee04803 100644 --- a/Examples/Test_Graph.asset +++ b/Examples/Test_Graph.asset @@ -14,8 +14,8 @@ MonoBehaviour: m_EditorClassIdentifier: Autosave: 0 Advanced: 1 - PannedPosition: {x: 85, y: 301, z: 0} - ScrolledScale: {x: 0.25, y: 0.25, z: 1} + PannedPosition: {x: -345, y: 72, z: 0} + ScrolledScale: {x: 0.7561437, y: 0.7561437, z: 1} Typewriter: 1 TypewriterSpeed: 1 ChoiceNodeID: 6 @@ -29,6 +29,7 @@ MonoBehaviour: Position: {x: 973, y: -15} NodeComponents: - rid: 7632772672291864709 + - rid: 2479963032036049265 - rid: 7632772672291864714 Comments: Choices: @@ -59,8 +60,8 @@ MonoBehaviour: Position: {x: 400, y: -15} NodeComponents: [] Comments: - Response: 'Welcome to Descant! How can I help you? Your - health is: {Test_Player:Health}.' + Response: Welcome to Descant! How can I help you? Your + health is {Test_Player:Health}, by the way. - Name: ResponseNode Type: Response ID: 1 @@ -123,7 +124,7 @@ MonoBehaviour: Groups: - Name: Answers ID: 0 - Position: {x: 1606.9999, y: -350} + Position: {x: 1607, y: -350} Nodes: - Response - Response @@ -133,7 +134,7 @@ MonoBehaviour: Comments: - Name: Start ID: 1 - Position: {x: -53.999977, y: -74} + Position: {x: -54.00003, y: -74.00001} Nodes: - Response - Start @@ -142,7 +143,7 @@ MonoBehaviour: Comments: - Name: QuickEnding ID: 2 - Position: {x: 2288, y: -209.99991} + Position: {x: 2288.0002, y: -210} Nodes: - Choice - End @@ -150,7 +151,7 @@ MonoBehaviour: Comments: - Name: LongEnding ID: 3 - Position: {x: 2288, y: 228.00002} + Position: {x: 2288.0002, y: 228.00003} Nodes: - Choice - Response @@ -221,6 +222,16 @@ MonoBehaviour: references: version: 2 RefIds: + - rid: 2479963032036049265 + type: {class: LockedChoice, ns: Descant.Components, asm: omch.descant.components} + data: + Collapsed: 0 + Actor: {fileID: 11400000, guid: aa50e7fd76a0a8c4ba679f3a8ac2fe0e, type: 2} + ChoiceNumber: 4 + VariableType: 0 + VariableName: WantsHelp + ComparisonType: 3 + Comparison: 1 - rid: 7632772672291864709 type: {class: Log, ns: Descant.Components, asm: omch.descant.components} data: diff --git a/Examples/Test_NPC.asset b/Examples/Test_NPC.asset index 905774b..1d3735c 100644 --- a/Examples/Test_NPC.asset +++ b/Examples/Test_NPC.asset @@ -16,13 +16,17 @@ MonoBehaviour: Portrait: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} Portraits: - {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} - StatisticKeys: - - Money - StatisticValues: - - 5 + Statistics: + dictionaryList: + - Key: Money + Value: 5 + index: 0 + isKeyDuplicated: 0 Topics: [] - RelationshipKeys: - - Test_Player - RelationshipValues: - - 0.5 - DialogueAttempts: 0 + Relationships: + dictionaryList: + - Key: Test_Player + Value: 0.5 + index: 0 + isKeyDuplicated: 0 + DialogueAttempts: 11 diff --git a/Examples/Test_Player.asset b/Examples/Test_Player.asset index 9e2597b..1137ec4 100644 --- a/Examples/Test_Player.asset +++ b/Examples/Test_Player.asset @@ -17,14 +17,22 @@ MonoBehaviour: Portraits: - {fileID: 10913, guid: 0000000000000000f000000000000000, type: 0} - {fileID: 10901, guid: 0000000000000000f000000000000000, type: 0} - StatisticKeys: - - Health - StatisticValues: - - 20 + Statistics: + dictionaryList: + - Key: Health + Value: 20 + index: 0 + isKeyDuplicated: 0 + - Key: WantsHelp + Value: 1 + index: 1 + isKeyDuplicated: 0 Topics: - Descant - RelationshipKeys: - - Test_NPC - RelationshipValues: - - 1 + Relationships: + dictionaryList: + - Key: Test_NPC + Value: 0.5 + index: 0 + isKeyDuplicated: 0 DialogueAttempts: 0 diff --git a/Media/Screenshots/actor.png b/Media/Screenshots/actor.png index f3d8653..6777213 100644 Binary files a/Media/Screenshots/actor.png and b/Media/Screenshots/actor.png differ diff --git a/Media/Screenshots/editor.png b/Media/Screenshots/editor.png index 240ed78..0ce28fa 100644 Binary files a/Media/Screenshots/editor.png and b/Media/Screenshots/editor.png differ diff --git a/Media/Screenshots/game.png b/Media/Screenshots/game.png index 1a6a724..d01c10a 100644 Binary files a/Media/Screenshots/game.png and b/Media/Screenshots/game.png differ diff --git a/README.md b/README.md index a8bc676..e636e67 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,7 @@ Descant Dialogue Editor - -
- Descant Actor - Sample UI implementation -
+Sample UI implementation @@ -31,9 +27,9 @@ ## Installation 1. Install the latest release from the [GitHub repository](https://github.com/Owmacohe/Descant/releases), unzip it, and place the folder into your Unity project's `Packages` folder. -2. return to Unity, and the package should automatically be recognized and visible in the **Package Manager**. +2. Return to Unity, and the package should automatically be recognized and visible in the **Package Manager**. 3. A sample scene can be found at: `Descant/Examples/Test_Scene.unity`. -4. Opening this scene may prompt you to install **Text Mesh Pro**. Simple click on **Import TMP Essentials** to do so. +4. Opening this scene may prompt you to install **Text Mesh Pro**. Simply click on **Import TMP Essentials** to do so. @@ -44,35 +40,34 @@ - **Descant Graphs** can be edited by right clicking on a `Descant Graph` file, and selecting `Edit Descant Graph`. - **Descant Actors** - **Descant Actors** can be created by right clicking, and selecting `Create/Descant Actor`. - - **Descant Actors** can be edited by right clicking on a `Descant Actor` file, and selecting `Edit Descant Actor`. + - **Descant Actors** can be edited through the **Inspector**. - **Descant Logs** don't need to be created or modified manually by the player at all. A single `DescantLogData` file exists in the `Descant/Resources` folder. Please don't delete it! ## Usage - **Descant Graphs** - 1. The **Descant Graph Editor** can be opened with `Tools/Descant/Graph Editor` or by creating/editing a **Descant Graph** file. + 1. The **Descant Graph Editor** can be opened from the toolbar with `Descant/Graph Editor` or by creating/editing a **Descant Graph** file. 2. Use middle-click to pan around in the editor. 3. New nodes can be created by right-clicking within the grid. 4. Connections between nodes can be created by left-clicking on nodes’ ports, and dragging to create a connection line to another port. 5. `ChoiceNode`s represent player choices at certain moments within the dialogue, and `ResponseNode`s represent the NPC’s responses or statements. - 6. If you want to have a `DescantActor`’s statistic show up within a `ChoiceNode` or `ResponseNode`'s text, write `{actor_name:statistic_name}`, and **Descant** will inject the statistic right into it. *(see the below for more info on `DescantActor`s)*. + 6. If you want to have a `DescantActor`’s property (statistic, topic, or relationship value) to show up within a `ChoiceNode` or `ResponseNode`'s text, write `{actor_name:property_name}`, and **Descant** will inject the first property it finds with the name `property_name` right into it. *(see the below for more info on `DescantActor`s)*. 7. The `StartNode` represents the place where a given dialogue begins, and `EndNode`s represent where it can end. 8. More complex functionality can be added to nodes by adding `Components` (see the [Component documentation](https://omch.tech/descant) for more info on each default component, as well as how to write your own). - **Descant Actors** - 1. The **Descant Actor Editor** can be opened with `Tools/Descant/Descant Actor Editor` or by creating/editing a **Descant Actor** file. - 2. New **Statistics**, **Topics**, and **Relationships** can be added with their respective **Add** buttons. + 1. **Descant Actor**s represent the player and any NPCs that they might talk to. **Descant Actor**s are not necessary when using **Descant**. + 2. New **Statistics**, **Topics**, and **Relationships** can be added in the **Inspector** for each **Descant Actor**. - **Statistics** are variables that pertain to actors *(e.g. health, level, stamina, etc.)* - **Topics** are concepts that the actors may learn during dialogue *(e.g. names of characters, locations, events, etc.)* - **Relationships** are values quantifiably that represent how actors feel about each other. - 3. The **Dialogue attempts** represents the number of times that this actor has been talked to by the player. - **Runtime** 1. Drag the `ConversationUI` prefab from the `Descant/Assets` folder into your Unity scene *(you may have as many `ConversationUI`s as you want in the same scene, and you may modify their text and UI styles as much as you want, so long as the `DescantConversationUI`'s inspector assignments don’t get broken)*. 2. Add an `Event System` object to your scene *(`Create/UI/Event System`)*, if you don't already have one. 3. Add a `DescantDialogueTrigger` script to a GameObject of your choice, and assign its fields *(hover over each field to see a popup of its description) (you may have as many `DescantDialogueTrigger`s as you want in the same scene)*. 4. At some point while the game is running, call the `Display()` method in the `DescantDialogueTrigger` script to begin the dialogue *(e.g. when the player presses `[E]`, when a `Button` is clicked, etc.)*. - **Logs** - 1. The **Descant Log** can be opened with `Tools/Descant/Log`. + 1. The **Descant Log** can be opened from the toolbar with `Descant/Log`. 2. Turn the `Capture` toggle on to start capturing events as they happen at runtime. 3. The log only saves the last played dialogue. 4. Please don't delete the log file located at `Descant/Resources`! @@ -98,3 +93,9 @@ - [Emily Short's Interactive Storytelling](https://emshort.blog/how-to-play/writing-if/my-articles/conversation) - [A Gossip Virtual Social Network for Non Playable Characters in Role Play Games](https://ieeexplore.ieee.org/document/6680108?part=1) - [NiEngine](https://github.com/StephanieRct/NiEngine) + + + +## Acknowledgements +- Unity's [UI Toolkit framework](https://docs.unity3d.com/Manual/UIElements.html) +- Eduard Malkhasyan's [Serializable Dictionary in Unity](https://github.com/EduardMalkhasyan/Serializable-Dictionary-Unity) diff --git a/Resources/DescantGraphEditorStyleSheet.uss b/Resources/DescantGraphEditorStyleSheet.uss index cfe05b2..2602836 100644 --- a/Resources/DescantGraphEditorStyleSheet.uss +++ b/Resources/DescantGraphEditorStyleSheet.uss @@ -47,6 +47,13 @@ Toolbar .toolbar-title { height: 30px; } +.version { + margin-left: 15px; + + color: var(--descant-color-bright); + font-size: 12px; +} + Toolbar .toolbar-filename { font-size: 18px; } @@ -133,8 +140,12 @@ DescantEndNode .node_component_dropdown VisualElement { } DescantChoiceNode .add_choice { - background-color: var(--descant-color-light); + margin: 5px; + padding: 3px; + color: #d2d2d2; + + background-color: var(--descant-color-light); } DescantResponseNode .node_component_dropdown { @@ -288,6 +299,10 @@ DescantResponseNode #contents .response TextElement { margin-bottom: 15px; } +.node_component .node_component_name { + min-width: 40px; +} + .node_component .node_component_group { flex-direction: column; @@ -327,12 +342,55 @@ DescantResponseNode #contents .response TextElement { align-self: center; } -.node_component TextField, -.node_component PopupField, .node_component Toggle { - background-color: var(--descant-color-background); +.node_component TextField { + height: 35px; + padding: 5px; + background-color: var(--descant-color-background); +} + +.node_component FloatField, +.node_component IntegerField, +.node_component Toggle, +.node_component Vector2Field, +.node_component Vector3Field, +.node_component ColorField, +.node_component ObjectField, +.node_component EnumField { height: 35px; + width: auto; + + padding: 5px; + + background-color: var(--descant-color-background); +} + +.node_component IntegerField, +.node_component Toggle, +.node_component ColorField, +.node_component ObjectField, +.node_component EnumField { + align-items: center; +} + +.node_component Vector2Field FloatField, +.node_component Vector3Field FloatField { + height: 30px; + + min-width: 50px; +} + +.node_component ColorField IMGUIContainer { + width: 150px; +} + +.node_component ObjectField ObjectFieldDisplay { + max-width: 150px; +} + +.node_component EnumField VisualElement { + background-color: var(--descant-color-light); } .node_component TextElement { @@ -346,7 +404,7 @@ DescantResponseNode #contents .response TextElement { } .node_component TextField TextElement { - font-size: 12px; + font-size: 15px; } .node_component Label { diff --git a/Runtime/DescantDialogueController.cs b/Runtime/DescantDialogueController.cs index 5f57653..93016a7 100644 --- a/Runtime/DescantDialogueController.cs +++ b/Runtime/DescantDialogueController.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using Descant.Components; using Descant.Editor; using Descant.Utilities; @@ -343,14 +344,20 @@ string CheckForActorProperties(string text) if (text[i] == '{' && text.Substring(i).Contains('}')) { // Getting the injection text (minus the brackets), - // and splitting it into actor name and statistic name + // and splitting it into actor name and property name string injection = text.Substring(i + 1, text.Substring(i).IndexOf('}') - 1); var split = injection.Split(':'); - // Appending the statistic value + // Appending the property value foreach (var j in Actors) + { if (j.name == split[0]) - temp += j.StatisticValues[j.StatisticKeys.IndexOf(split[1])]; + { + if (j.Statistics.Keys.Contains(split[1])) temp += j.Statistics[split[1]]; + else if (j.Topics.Contains(split[1])) temp += j.Topics[j.Topics.IndexOf(split[1])]; + else if (j.Relationships.Keys.Contains(split[1])) temp += j.Relationships[split[1]]; + } + } i += injection.Length + 1; // Skipping ahead to avoid adding any of the injection text } diff --git a/package-lock.json b/package-lock.json index 5feda8f..6ea8531 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "com.owmacohe.descant", - "version": "1.1.4", + "version": "1.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index b39b30b..c150011 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "com.owmacohe.descant", "displayName": "Descant", - "version": "1.1.4", + "version": "1.2.0", "description": "An enhanced and and user-friendly Unity dialogue system plugin.\n\nDescant aims to hit the sweet spot between quality UI, powerful features, and easy-to-lean functionality, while also addressing many of the game-specific consequences of the standard dialogue manager setup.\n\nPlease view the README before beginning development.", "unity": "2022.3", "unityRelease": "7f1",