forked from Shardstone/trail-into-darkness
added encounter system
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
using Jovian.EncounterSystem;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.EncounterSystem.Editor {
|
||||
/// <summary>Id dropdown (library inherited from the owning asset's <c>library</c> field) + inline fallback + resolved preview.</summary>
|
||||
[CustomPropertyDrawer(typeof(DialogLineRef))]
|
||||
public class DialogLineRefDrawer : PropertyDrawer {
|
||||
private const string NonePlaceholder = "<none>";
|
||||
private const string EmptyLibraryPlaceholder = "<set library on the parent asset>";
|
||||
private const float PreviewHeight = 32f;
|
||||
|
||||
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
|
||||
var idProp = property.FindPropertyRelative("id");
|
||||
var inlineProp = property.FindPropertyRelative("inlineText");
|
||||
var library = ResolveLibrary(property);
|
||||
|
||||
EditorGUI.BeginProperty(position, label, property);
|
||||
|
||||
var lineHeight = EditorGUIUtility.singleLineHeight;
|
||||
var spacing = EditorGUIUtility.standardVerticalSpacing;
|
||||
|
||||
var idRect = new Rect(position.x, position.y, position.width, lineHeight);
|
||||
DrawIdPicker(idRect, library, idProp, label);
|
||||
|
||||
var inlineRect = new Rect(
|
||||
position.x,
|
||||
position.y + lineHeight + spacing,
|
||||
position.width,
|
||||
lineHeight * 2);
|
||||
EditorGUI.PropertyField(inlineRect, inlineProp, new GUIContent("Inline"));
|
||||
|
||||
var previewRect = new Rect(
|
||||
position.x,
|
||||
position.y + lineHeight + spacing + lineHeight * 2 + spacing,
|
||||
position.width,
|
||||
PreviewHeight);
|
||||
DrawPreview(previewRect, library, idProp, inlineProp);
|
||||
|
||||
EditorGUI.EndProperty();
|
||||
}
|
||||
|
||||
public override float GetPropertyHeight(SerializedProperty property, GUIContent label) {
|
||||
var lineHeight = EditorGUIUtility.singleLineHeight;
|
||||
var spacing = EditorGUIUtility.standardVerticalSpacing;
|
||||
// id + (inline 2 lines) + preview
|
||||
return lineHeight + spacing + lineHeight * 2 + spacing + PreviewHeight;
|
||||
}
|
||||
|
||||
private static DialogLineLibrary ResolveLibrary(SerializedProperty property) {
|
||||
var libraryProp = property.serializedObject.FindProperty("library");
|
||||
return libraryProp?.objectReferenceValue as DialogLineLibrary;
|
||||
}
|
||||
|
||||
private static void DrawIdPicker(Rect rect, DialogLineLibrary library, SerializedProperty idProp, GUIContent label) {
|
||||
if(library == null || library.lines == null || library.lines.Count == 0) {
|
||||
using(new EditorGUI.DisabledScope(true)) {
|
||||
EditorGUI.Popup(rect, label.text, 0, new[] { EmptyLibraryPlaceholder });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var count = library.lines.Count;
|
||||
var ids = new string[count + 1];
|
||||
var names = new string[count + 1];
|
||||
ids[0] = string.Empty;
|
||||
names[0] = NonePlaceholder;
|
||||
|
||||
var currentIndex = 0;
|
||||
for(int i = 0; i < count; i++) {
|
||||
var line = library.lines[i];
|
||||
var id = line?.id ?? string.Empty;
|
||||
ids[i + 1] = id;
|
||||
names[i + 1] = string.IsNullOrEmpty(id) ? $"<unnamed {i}>" : id;
|
||||
if(id == idProp.stringValue && !string.IsNullOrEmpty(id)) {
|
||||
currentIndex = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
var newIndex = EditorGUI.Popup(rect, label.text, currentIndex, names);
|
||||
if(newIndex != currentIndex) {
|
||||
idProp.stringValue = ids[newIndex];
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawPreview(Rect rect, DialogLineLibrary library, SerializedProperty idProp, SerializedProperty inlineProp) {
|
||||
var id = idProp.stringValue;
|
||||
var inline = inlineProp.stringValue;
|
||||
|
||||
string resolved = null;
|
||||
if(library != null && !string.IsNullOrEmpty(id)) {
|
||||
resolved = library.Resolve(id);
|
||||
}
|
||||
if(string.IsNullOrEmpty(resolved)) {
|
||||
resolved = inline;
|
||||
}
|
||||
|
||||
var style = new GUIStyle(EditorStyles.helpBox) {
|
||||
fontStyle = FontStyle.Italic,
|
||||
wordWrap = true
|
||||
};
|
||||
var label = string.IsNullOrEmpty(resolved) ? "<no text will display>" : resolved;
|
||||
EditorGUI.LabelField(rect, new GUIContent("Preview: " + label), style);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a55b866cc153f5749a71928930faeb62
|
||||
@@ -0,0 +1,394 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Jovian.EncounterSystem;
|
||||
using UnityEditor;
|
||||
using UnityEditor.UIElements;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
namespace Jovian.EncounterSystem.Editor {
|
||||
/// <summary>Browser for every encounter across all tables. Search + kind filter + detail pane. Quest chains render as a tree rooted at the first step.</summary>
|
||||
public class EncounterBrowserWindow : EditorWindow {
|
||||
private const string AllKinds = "All";
|
||||
|
||||
private class Record {
|
||||
public EncounterTable table;
|
||||
public int index;
|
||||
public IEncounter encounter;
|
||||
}
|
||||
|
||||
private readonly List<Record> allRecords = new();
|
||||
private string searchText = string.Empty;
|
||||
private string kindFilter = AllKinds;
|
||||
|
||||
private readonly Dictionary<IEncounter, List<ValidationIssue>> issuesByEncounter = new();
|
||||
|
||||
private TreeView treeView;
|
||||
private VisualElement detailPane;
|
||||
private ToolbarMenu kindDropdown;
|
||||
|
||||
[MenuItem("Jovian/Encounters/Encounter Browser")]
|
||||
public static void Open() {
|
||||
var window = GetWindow<EncounterBrowserWindow>("Encounters");
|
||||
window.minSize = new Vector2(640, 360);
|
||||
}
|
||||
|
||||
private void CreateGUI() {
|
||||
BuildToolbar();
|
||||
BuildSplit();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private void BuildToolbar() {
|
||||
var toolbar = new Toolbar();
|
||||
|
||||
var search = new ToolbarSearchField();
|
||||
search.style.flexGrow = 1f;
|
||||
search.RegisterValueChangedCallback(evt => {
|
||||
searchText = evt.newValue ?? string.Empty;
|
||||
ApplyFilter();
|
||||
});
|
||||
toolbar.Add(search);
|
||||
|
||||
kindDropdown = new ToolbarMenu { text = $"Kind: {AllKinds}" };
|
||||
foreach(var choice in GetKindChoices()) {
|
||||
var captured = choice;
|
||||
kindDropdown.menu.AppendAction(captured, _ => {
|
||||
kindFilter = captured;
|
||||
kindDropdown.text = $"Kind: {captured}";
|
||||
ApplyFilter();
|
||||
});
|
||||
}
|
||||
toolbar.Add(kindDropdown);
|
||||
|
||||
var refreshButton = new ToolbarButton(Refresh) { text = "Refresh" };
|
||||
toolbar.Add(refreshButton);
|
||||
|
||||
rootVisualElement.Add(toolbar);
|
||||
}
|
||||
|
||||
private void BuildSplit() {
|
||||
var split = new TwoPaneSplitView(0, 280, TwoPaneSplitViewOrientation.Horizontal);
|
||||
split.style.flexGrow = 1f;
|
||||
rootVisualElement.Add(split);
|
||||
|
||||
treeView = new TreeView {
|
||||
makeItem = MakeRow,
|
||||
bindItem = BindRow,
|
||||
fixedItemHeight = 22,
|
||||
selectionType = SelectionType.Single
|
||||
};
|
||||
treeView.selectionChanged += OnSelectionChanged;
|
||||
treeView.style.flexGrow = 1f;
|
||||
split.Add(treeView);
|
||||
|
||||
detailPane = new ScrollView(ScrollViewMode.Vertical) {
|
||||
style = { paddingLeft = 8, paddingTop = 8, paddingRight = 8, flexGrow = 1f }
|
||||
};
|
||||
ShowEmptyDetail();
|
||||
split.Add(detailPane);
|
||||
}
|
||||
|
||||
private static VisualElement MakeRow() {
|
||||
var row = new VisualElement {
|
||||
style = {
|
||||
flexDirection = FlexDirection.Row,
|
||||
alignItems = Align.Center,
|
||||
paddingLeft = 6,
|
||||
paddingRight = 6,
|
||||
height = 22
|
||||
}
|
||||
};
|
||||
|
||||
var badge = new VisualElement {
|
||||
name = "issue-badge",
|
||||
style = {
|
||||
width = 8,
|
||||
height = 8,
|
||||
marginRight = 6,
|
||||
borderTopLeftRadius = 4,
|
||||
borderTopRightRadius = 4,
|
||||
borderBottomLeftRadius = 4,
|
||||
borderBottomRightRadius = 4,
|
||||
visibility = Visibility.Hidden
|
||||
}
|
||||
};
|
||||
row.Add(badge);
|
||||
|
||||
var label = new Label {
|
||||
name = "row-label",
|
||||
style = {
|
||||
flexGrow = 1f,
|
||||
unityTextAlign = TextAnchor.MiddleLeft
|
||||
}
|
||||
};
|
||||
row.Add(label);
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
private void BindRow(VisualElement element, int index) {
|
||||
var record = treeView.GetItemDataForIndex<Record>(index);
|
||||
var label = element.Q<Label>("row-label");
|
||||
var badge = element.Q<VisualElement>("issue-badge");
|
||||
|
||||
var name = record.encounter?.EncounterDefinition?.name;
|
||||
var kind = record.encounter?.Kind?.GetType().Name ?? "—";
|
||||
label.text = string.IsNullOrEmpty(name)
|
||||
? $"<unnamed> [{kind}]"
|
||||
: $"{name} [{kind}]";
|
||||
|
||||
if(record.encounter != null && issuesByEncounter.TryGetValue(record.encounter, out var issues) && issues.Count > 0) {
|
||||
var hasError = issues.Exists(i => i.severity == ValidationSeverity.Error);
|
||||
badge.style.backgroundColor = new StyleColor(hasError ? new Color(0.85f, 0.25f, 0.25f) : new Color(0.95f, 0.75f, 0.1f));
|
||||
badge.style.visibility = Visibility.Visible;
|
||||
element.tooltip = BuildTooltip(issues);
|
||||
}
|
||||
else {
|
||||
badge.style.visibility = Visibility.Hidden;
|
||||
element.tooltip = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private static string BuildTooltip(List<ValidationIssue> issues) {
|
||||
var lines = new List<string>(issues.Count);
|
||||
foreach(var issue in issues) {
|
||||
var prefix = issue.severity == ValidationSeverity.Error ? "ERROR" : "WARN";
|
||||
lines.Add($"[{prefix}] {issue.path} — {issue.message}");
|
||||
}
|
||||
return string.Join("\n", lines);
|
||||
}
|
||||
|
||||
private static IEnumerable<string> GetKindChoices() {
|
||||
yield return AllKinds;
|
||||
|
||||
var kindTypes = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.SelectMany(assembly => {
|
||||
try { return assembly.GetTypes(); }
|
||||
catch { return Array.Empty<Type>(); }
|
||||
})
|
||||
.Where(type => typeof(IEncounterKind).IsAssignableFrom(type) && !type.IsAbstract && !type.IsInterface)
|
||||
.Select(type => type.Name)
|
||||
.OrderBy(name => name);
|
||||
|
||||
foreach(var name in kindTypes) {
|
||||
yield return name;
|
||||
}
|
||||
}
|
||||
|
||||
private void Refresh() {
|
||||
allRecords.Clear();
|
||||
|
||||
var guids = AssetDatabase.FindAssets("t:" + nameof(EncounterTable));
|
||||
foreach(var guid in guids) {
|
||||
var path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
var table = AssetDatabase.LoadAssetAtPath<EncounterTable>(path);
|
||||
if(table?.encounters == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for(int i = 0; i < table.encounters.Count; i++) {
|
||||
allRecords.Add(new Record {
|
||||
table = table,
|
||||
index = i,
|
||||
encounter = table.encounters[i]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
RebuildIssueIndex();
|
||||
ApplyFilter();
|
||||
}
|
||||
|
||||
private void RebuildIssueIndex() {
|
||||
issuesByEncounter.Clear();
|
||||
var issues = EncounterValidator.ValidateProject();
|
||||
foreach(var issue in issues) {
|
||||
if(issue.encounter == null) {
|
||||
continue;
|
||||
}
|
||||
if(!issuesByEncounter.TryGetValue(issue.encounter, out var list)) {
|
||||
list = new List<ValidationIssue>();
|
||||
issuesByEncounter[issue.encounter] = list;
|
||||
}
|
||||
list.Add(issue);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyFilter() {
|
||||
var filtered = allRecords.Where(Matches).ToList();
|
||||
var items = BuildTreeItems(filtered);
|
||||
if(treeView != null) {
|
||||
treeView.SetRootItems(items);
|
||||
treeView.Rebuild();
|
||||
treeView.ClearSelection();
|
||||
treeView.ExpandAll();
|
||||
}
|
||||
ShowEmptyDetail();
|
||||
}
|
||||
|
||||
private static List<TreeViewItemData<Record>> BuildTreeItems(List<Record> records) {
|
||||
var byEncounter = new Dictionary<IEncounter, Record>();
|
||||
foreach(var r in records) {
|
||||
if(r.encounter != null) {
|
||||
byEncounter[r.encounter] = r;
|
||||
}
|
||||
}
|
||||
|
||||
// Any encounter that is a `nextEncounter` target of another record in the filtered set
|
||||
// becomes a non-root (rendered as a child), not a top-level item.
|
||||
var nonRoot = new HashSet<IEncounter>();
|
||||
foreach(var r in records) {
|
||||
if(r.encounter?.Kind is not QuestKind quest) {
|
||||
continue;
|
||||
}
|
||||
var next = quest.nextEncounter.Resolve();
|
||||
if(next != null && byEncounter.ContainsKey(next)) {
|
||||
nonRoot.Add(next);
|
||||
}
|
||||
}
|
||||
|
||||
var result = new List<TreeViewItemData<Record>>();
|
||||
var uid = 0;
|
||||
foreach(var r in records) {
|
||||
if(r.encounter != null && nonRoot.Contains(r.encounter)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var children = BuildChainChildren(r, byEncounter, ref uid);
|
||||
result.Add(new TreeViewItemData<Record>(uid++, r, children));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static List<TreeViewItemData<Record>> BuildChainChildren(Record root, Dictionary<IEncounter, Record> byEncounter, ref int uid) {
|
||||
var children = new List<TreeViewItemData<Record>>();
|
||||
if(root.encounter?.Kind is not QuestKind) {
|
||||
return children;
|
||||
}
|
||||
|
||||
var current = root.encounter;
|
||||
var visited = new HashSet<IEncounter> { current };
|
||||
while(current?.Kind is QuestKind quest) {
|
||||
var next = quest.nextEncounter.Resolve();
|
||||
if(next == null || !visited.Add(next)) {
|
||||
break;
|
||||
}
|
||||
if(!byEncounter.TryGetValue(next, out var nextRecord)) {
|
||||
break;
|
||||
}
|
||||
children.Add(new TreeViewItemData<Record>(uid++, nextRecord));
|
||||
current = next;
|
||||
}
|
||||
return children;
|
||||
}
|
||||
|
||||
private bool Matches(Record record) {
|
||||
var kindName = record.encounter?.Kind?.GetType().Name ?? string.Empty;
|
||||
if(kindFilter != AllKinds && kindName != kindFilter) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(string.IsNullOrEmpty(searchText)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var needle = searchText.ToLowerInvariant();
|
||||
var definition = record.encounter?.EncounterDefinition;
|
||||
return Contains(definition?.id, needle)
|
||||
|| Contains(definition?.name, needle)
|
||||
|| Contains(definition?.description, needle);
|
||||
}
|
||||
|
||||
private static bool Contains(string source, string needle) {
|
||||
return !string.IsNullOrEmpty(source) && source.ToLowerInvariant().Contains(needle);
|
||||
}
|
||||
|
||||
private void OnSelectionChanged(IEnumerable<object> selection) {
|
||||
var record = selection.OfType<Record>().FirstOrDefault();
|
||||
if(record == null) {
|
||||
ShowEmptyDetail();
|
||||
return;
|
||||
}
|
||||
|
||||
detailPane.Clear();
|
||||
|
||||
var serializedObject = new SerializedObject(record.table);
|
||||
var encountersProp = serializedObject.FindProperty(nameof(EncounterTable.encounters));
|
||||
var elementProp = encountersProp.GetArrayElementAtIndex(record.index);
|
||||
|
||||
var header = new Label($"{record.table.name} → [{record.index}]") {
|
||||
style = {
|
||||
unityFontStyleAndWeight = FontStyle.Bold,
|
||||
marginBottom = 6,
|
||||
color = new StyleColor(new Color(0.75f, 0.75f, 0.75f))
|
||||
}
|
||||
};
|
||||
detailPane.Add(header);
|
||||
|
||||
elementProp.isExpanded = true;
|
||||
var field = new PropertyField(elementProp);
|
||||
field.Bind(serializedObject);
|
||||
detailPane.Add(field);
|
||||
|
||||
AddChainPreviewIfQuest(record);
|
||||
}
|
||||
|
||||
private void AddChainPreviewIfQuest(Record record) {
|
||||
if(record.encounter?.Kind is not QuestKind questKind) {
|
||||
return;
|
||||
}
|
||||
|
||||
var predecessor = FindPredecessor(record.encounter.EncounterDefinition?.internalId);
|
||||
var next = questKind.nextEncounter.Resolve();
|
||||
|
||||
var predName = predecessor?.encounter.EncounterDefinition?.name ?? "—";
|
||||
var currName = record.encounter.EncounterDefinition?.name ?? "<unnamed>";
|
||||
var nextName = next?.EncounterDefinition?.name ?? "—";
|
||||
|
||||
var chainLabel = new Label($"Chain: {predName} ← {currName} → {nextName}") {
|
||||
style = {
|
||||
marginTop = 12,
|
||||
paddingTop = 6,
|
||||
paddingBottom = 6,
|
||||
paddingLeft = 6,
|
||||
paddingRight = 6,
|
||||
backgroundColor = new StyleColor(new Color(0.2f, 0.2f, 0.2f, 0.4f)),
|
||||
unityFontStyleAndWeight = FontStyle.Bold
|
||||
}
|
||||
};
|
||||
detailPane.Add(chainLabel);
|
||||
}
|
||||
|
||||
private Record FindPredecessor(string targetId) {
|
||||
if(string.IsNullOrEmpty(targetId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach(var record in allRecords) {
|
||||
if(record.encounter?.Kind is not QuestKind kind) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var next = kind.nextEncounter.Resolve();
|
||||
if(next?.EncounterDefinition?.internalId == targetId) {
|
||||
return record;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void ShowEmptyDetail() {
|
||||
if(detailPane == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
detailPane.Clear();
|
||||
var empty = new Label("Select an encounter to edit.") {
|
||||
style = { color = new StyleColor(Color.gray), marginTop = 8 }
|
||||
};
|
||||
detailPane.Add(empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0312d015c183e2f4582358d17867585c
|
||||
@@ -0,0 +1,76 @@
|
||||
using Jovian.EncounterSystem;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.EncounterSystem.Editor {
|
||||
/// <summary>Label list elements with the option's text id (fallback: inline text preview, then default).</summary>
|
||||
[CustomPropertyDrawer(typeof(EncounterDialogOption))]
|
||||
public class EncounterDialogOptionDrawer : PropertyDrawer {
|
||||
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
|
||||
var displayLabel = ResolveLabel(property, label);
|
||||
|
||||
var foldoutRect = new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight);
|
||||
property.isExpanded = EditorGUI.Foldout(foldoutRect, property.isExpanded, displayLabel, true);
|
||||
|
||||
if(!property.isExpanded) {
|
||||
return;
|
||||
}
|
||||
|
||||
EditorGUI.indentLevel++;
|
||||
var y = position.y + EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
|
||||
var iterator = property.Copy();
|
||||
var end = iterator.GetEndProperty();
|
||||
if(iterator.NextVisible(true)) {
|
||||
while(!SerializedProperty.EqualContents(iterator, end)) {
|
||||
var h = EditorGUI.GetPropertyHeight(iterator, true);
|
||||
var r = new Rect(position.x, y, position.width, h);
|
||||
EditorGUI.PropertyField(r, iterator, true);
|
||||
y += h + EditorGUIUtility.standardVerticalSpacing;
|
||||
if(!iterator.NextVisible(false)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
|
||||
public override float GetPropertyHeight(SerializedProperty property, GUIContent label) {
|
||||
var height = EditorGUIUtility.singleLineHeight;
|
||||
if(!property.isExpanded) {
|
||||
return height;
|
||||
}
|
||||
|
||||
var iterator = property.Copy();
|
||||
var end = iterator.GetEndProperty();
|
||||
if(iterator.NextVisible(true)) {
|
||||
while(!SerializedProperty.EqualContents(iterator, end)) {
|
||||
height += EditorGUI.GetPropertyHeight(iterator, true) + EditorGUIUtility.standardVerticalSpacing;
|
||||
if(!iterator.NextVisible(false)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return height;
|
||||
}
|
||||
|
||||
private static GUIContent ResolveLabel(SerializedProperty property, GUIContent fallback) {
|
||||
var textProp = property.FindPropertyRelative("text");
|
||||
if(textProp == null) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
var id = textProp.FindPropertyRelative("id")?.stringValue;
|
||||
if(!string.IsNullOrEmpty(id)) {
|
||||
return new GUIContent(id);
|
||||
}
|
||||
|
||||
var inline = textProp.FindPropertyRelative("inlineText")?.stringValue;
|
||||
if(!string.IsNullOrEmpty(inline)) {
|
||||
var preview = inline.Length > 40 ? inline.Substring(0, 40) + "…" : inline;
|
||||
return new GUIContent(preview);
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5636dc9f15ce02c4ca189c3d5f46eb34
|
||||
@@ -0,0 +1,77 @@
|
||||
using Jovian.EncounterSystem;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.EncounterSystem.Editor {
|
||||
/// <summary>Label list elements with the encounter id (fallback: name, then default).</summary>
|
||||
[CustomPropertyDrawer(typeof(Encounter))]
|
||||
public class EncounterDrawer : PropertyDrawer {
|
||||
private const string DefinitionBackingField = "<EncounterDefinition>k__BackingField";
|
||||
|
||||
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
|
||||
var displayLabel = ResolveLabel(property, label);
|
||||
|
||||
var foldoutRect = new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight);
|
||||
property.isExpanded = EditorGUI.Foldout(foldoutRect, property.isExpanded, displayLabel, true);
|
||||
|
||||
if(!property.isExpanded) {
|
||||
return;
|
||||
}
|
||||
|
||||
EditorGUI.indentLevel++;
|
||||
var y = position.y + EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
|
||||
var iterator = property.Copy();
|
||||
var end = iterator.GetEndProperty();
|
||||
if(iterator.NextVisible(true)) {
|
||||
while(!SerializedProperty.EqualContents(iterator, end)) {
|
||||
var h = EditorGUI.GetPropertyHeight(iterator, true);
|
||||
var r = new Rect(position.x, y, position.width, h);
|
||||
EditorGUI.PropertyField(r, iterator, true);
|
||||
y += h + EditorGUIUtility.standardVerticalSpacing;
|
||||
if(!iterator.NextVisible(false)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
|
||||
public override float GetPropertyHeight(SerializedProperty property, GUIContent label) {
|
||||
var height = EditorGUIUtility.singleLineHeight;
|
||||
if(!property.isExpanded) {
|
||||
return height;
|
||||
}
|
||||
|
||||
var iterator = property.Copy();
|
||||
var end = iterator.GetEndProperty();
|
||||
if(iterator.NextVisible(true)) {
|
||||
while(!SerializedProperty.EqualContents(iterator, end)) {
|
||||
height += EditorGUI.GetPropertyHeight(iterator, true) + EditorGUIUtility.standardVerticalSpacing;
|
||||
if(!iterator.NextVisible(false)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return height;
|
||||
}
|
||||
|
||||
private static GUIContent ResolveLabel(SerializedProperty property, GUIContent fallback) {
|
||||
var definition = property.FindPropertyRelative(DefinitionBackingField);
|
||||
if(definition == null) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
var id = definition.FindPropertyRelative("id")?.stringValue;
|
||||
if(!string.IsNullOrEmpty(id)) {
|
||||
return new GUIContent(id);
|
||||
}
|
||||
|
||||
var name = definition.FindPropertyRelative("name")?.stringValue;
|
||||
if(!string.IsNullOrEmpty(name)) {
|
||||
return new GUIContent(name);
|
||||
}
|
||||
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0ee5ffe2ea6903547ac75470249af31f
|
||||
@@ -0,0 +1,82 @@
|
||||
using Jovian.EncounterSystem;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.EncounterSystem.Editor {
|
||||
/// <summary>Table object-field + encounter dropdown picker. Changing tables clears the id.</summary>
|
||||
[CustomPropertyDrawer(typeof(EncounterLink))]
|
||||
public class EncounterLinkDrawer : PropertyDrawer {
|
||||
private const string NonePlaceholder = "<none>";
|
||||
private const string EmptyTablePlaceholder = "<select a table first>";
|
||||
|
||||
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
|
||||
var tableProp = property.FindPropertyRelative("table");
|
||||
var idProp = property.FindPropertyRelative("internalId");
|
||||
|
||||
EditorGUI.BeginProperty(position, label, property);
|
||||
|
||||
var lineHeight = EditorGUIUtility.singleLineHeight;
|
||||
var spacing = EditorGUIUtility.standardVerticalSpacing;
|
||||
|
||||
var tableRect = new Rect(position.x, position.y, position.width, lineHeight);
|
||||
var encounterRect = new Rect(position.x, position.y + lineHeight + spacing, position.width, lineHeight);
|
||||
|
||||
EditorGUI.BeginChangeCheck();
|
||||
EditorGUI.PropertyField(tableRect, tableProp, label);
|
||||
var tableChanged = EditorGUI.EndChangeCheck();
|
||||
|
||||
using(new EditorGUI.IndentLevelScope()) {
|
||||
var table = tableProp.objectReferenceValue as EncounterTable;
|
||||
if(table == null || table.encounters == null || table.encounters.Count == 0) {
|
||||
using(new EditorGUI.DisabledScope(true)) {
|
||||
EditorGUI.Popup(encounterRect, "Encounter", 0, new[] { EmptyTablePlaceholder });
|
||||
}
|
||||
if(tableChanged) {
|
||||
idProp.stringValue = string.Empty;
|
||||
}
|
||||
}
|
||||
else {
|
||||
DrawEncounterPicker(encounterRect, table, idProp, tableChanged);
|
||||
}
|
||||
}
|
||||
|
||||
EditorGUI.EndProperty();
|
||||
}
|
||||
|
||||
public override float GetPropertyHeight(SerializedProperty property, GUIContent label) {
|
||||
return EditorGUIUtility.singleLineHeight * 2 + EditorGUIUtility.standardVerticalSpacing;
|
||||
}
|
||||
|
||||
private static void DrawEncounterPicker(Rect rect, EncounterTable table, SerializedProperty idProp, bool tableChanged) {
|
||||
var count = table.encounters.Count;
|
||||
var ids = new string[count + 1];
|
||||
var names = new string[count + 1];
|
||||
ids[0] = string.Empty;
|
||||
names[0] = NonePlaceholder;
|
||||
|
||||
var currentIndex = 0;
|
||||
for(int i = 0; i < count; i++) {
|
||||
var encounter = table.encounters[i];
|
||||
var id = encounter?.EncounterDefinition?.internalId ?? string.Empty;
|
||||
var name = encounter?.EncounterDefinition?.name;
|
||||
ids[i + 1] = id;
|
||||
names[i + 1] = string.IsNullOrEmpty(name)
|
||||
? $"<unnamed> ({encounter?.GetType().Name ?? "null"})"
|
||||
: name;
|
||||
if(!tableChanged && id == idProp.stringValue && !string.IsNullOrEmpty(id)) {
|
||||
currentIndex = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if(tableChanged) {
|
||||
idProp.stringValue = string.Empty;
|
||||
currentIndex = 0;
|
||||
}
|
||||
|
||||
var newIndex = EditorGUI.Popup(rect, "Encounter", currentIndex, names);
|
||||
if(newIndex != currentIndex) {
|
||||
idProp.stringValue = ids[newIndex];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 385fe4b7b2663e54aa3f520a809a33bc
|
||||
@@ -0,0 +1,296 @@
|
||||
using System.Collections.Generic;
|
||||
using Jovian.EncounterSystem;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.EncounterSystem.Editor {
|
||||
public enum ValidationSeverity {
|
||||
Warning,
|
||||
Error
|
||||
}
|
||||
|
||||
public class ValidationIssue {
|
||||
public Object asset;
|
||||
public Encounter encounter;
|
||||
public string path;
|
||||
public ValidationSeverity severity;
|
||||
public string message;
|
||||
}
|
||||
|
||||
/// <summary>Project-wide scan of encounter tables and rewards. Runs on demand, no caching.</summary>
|
||||
public static class EncounterValidator {
|
||||
public static List<ValidationIssue> ValidateProject() {
|
||||
var issues = new List<ValidationIssue>();
|
||||
|
||||
var tables = FindAssetsOfType<EncounterTable>();
|
||||
var idIndex = new Dictionary<string, (Encounter encounter, EncounterTable table)>();
|
||||
|
||||
foreach(var table in tables) {
|
||||
ValidateTable(table, issues, idIndex);
|
||||
}
|
||||
|
||||
foreach(var reward in FindAssetsOfType<Reward>()) {
|
||||
ValidateReward(reward, issues);
|
||||
}
|
||||
|
||||
return issues;
|
||||
}
|
||||
|
||||
public static List<ValidationIssue> ValidateEncounter(EncounterTable table, int index) {
|
||||
var issues = new List<ValidationIssue>();
|
||||
if(table?.encounters == null || index < 0 || index >= table.encounters.Count) {
|
||||
return issues;
|
||||
}
|
||||
|
||||
var encounter = table.encounters[index];
|
||||
ValidateEncounterEntry(table, index, encounter, issues, duplicateCheck: null);
|
||||
return issues;
|
||||
}
|
||||
|
||||
private static void ValidateTable(EncounterTable table, List<ValidationIssue> issues, Dictionary<string, (Encounter, EncounterTable)> idIndex) {
|
||||
if(table?.encounters == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for(int i = 0; i < table.encounters.Count; i++) {
|
||||
ValidateEncounterEntry(table, i, table.encounters[i], issues, idIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateEncounterEntry(EncounterTable table, int index, Encounter encounter, List<ValidationIssue> issues, Dictionary<string, (Encounter, EncounterTable)> duplicateCheck) {
|
||||
var pathPrefix = $"encounters[{index}]";
|
||||
|
||||
if(encounter == null) {
|
||||
issues.Add(new ValidationIssue {
|
||||
asset = table,
|
||||
path = pathPrefix,
|
||||
severity = ValidationSeverity.Error,
|
||||
message = "Null encounter entry."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var definition = encounter.EncounterDefinition;
|
||||
if(definition == null || string.IsNullOrEmpty(definition.internalId)) {
|
||||
issues.Add(new ValidationIssue {
|
||||
asset = table,
|
||||
encounter = encounter,
|
||||
path = $"{pathPrefix}.EncounterDefinition.internalId",
|
||||
severity = ValidationSeverity.Error,
|
||||
message = "Missing internalId (cannot be referenced by EncounterLink)."
|
||||
});
|
||||
}
|
||||
else if(duplicateCheck != null) {
|
||||
if(duplicateCheck.TryGetValue(definition.internalId, out var prior)) {
|
||||
issues.Add(new ValidationIssue {
|
||||
asset = table,
|
||||
encounter = encounter,
|
||||
path = $"{pathPrefix}.EncounterDefinition.internalId",
|
||||
severity = ValidationSeverity.Error,
|
||||
message = $"Duplicate internalId '{definition.internalId}' (also in table '{prior.Item2.name}')."
|
||||
});
|
||||
}
|
||||
else {
|
||||
duplicateCheck[definition.internalId] = (encounter, table);
|
||||
}
|
||||
}
|
||||
|
||||
if(encounter.Kind == null) {
|
||||
issues.Add(new ValidationIssue {
|
||||
asset = table,
|
||||
encounter = encounter,
|
||||
path = $"{pathPrefix}.Kind",
|
||||
severity = ValidationSeverity.Warning,
|
||||
message = "Encounter.Kind is null — pick a kind in the inspector."
|
||||
});
|
||||
}
|
||||
|
||||
if(encounter.Kind is QuestKind questKind) {
|
||||
ValidateEncounterLink(table, pathPrefix + ".Kind.nextEncounter", encounter, questKind.nextEncounter, issues);
|
||||
}
|
||||
|
||||
ValidateDialogEvents(table, index, encounter, issues);
|
||||
}
|
||||
|
||||
private static void ValidateEncounterLink(EncounterTable owningTable, string path, Encounter encounter, EncounterLink link, List<ValidationIssue> issues) {
|
||||
if(link.table == null && string.IsNullOrEmpty(link.internalId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(link.table == null) {
|
||||
issues.Add(new ValidationIssue {
|
||||
asset = owningTable,
|
||||
encounter = encounter,
|
||||
path = path,
|
||||
severity = ValidationSeverity.Error,
|
||||
message = "EncounterLink has internalId set but no table assigned."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if(link.Resolve() == null) {
|
||||
issues.Add(new ValidationIssue {
|
||||
asset = owningTable,
|
||||
encounter = encounter,
|
||||
path = path,
|
||||
severity = ValidationSeverity.Error,
|
||||
message = $"Broken EncounterLink — table '{link.table.name}' has no encounter with internalId '{link.internalId}'."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateDialogEvents(EncounterTable table, int index, Encounter encounter, List<ValidationIssue> issues) {
|
||||
var optionSet = encounter.EncounterDialogOptionSet;
|
||||
if(optionSet?.options == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for(int o = 0; o < optionSet.options.Count; o++) {
|
||||
var option = optionSet.options[o];
|
||||
if(option == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ValidateDialogLineRef(optionSet, encounter, $"options[{o}].text", option.text, issues);
|
||||
|
||||
if(option.events == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for(int e = 0; e < option.events.Count; e++) {
|
||||
var evt = option.events[e];
|
||||
if(evt == null) {
|
||||
issues.Add(new ValidationIssue {
|
||||
asset = optionSet,
|
||||
encounter = encounter,
|
||||
path = $"options[{o}].events[{e}]",
|
||||
severity = ValidationSeverity.Warning,
|
||||
message = "Null event entry (type may have been renamed/deleted)."
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if(evt is GiveRewardEvent give && give.reward == null) {
|
||||
issues.Add(new ValidationIssue {
|
||||
asset = optionSet,
|
||||
encounter = encounter,
|
||||
path = $"options[{o}].events[{e}].reward",
|
||||
severity = ValidationSeverity.Warning,
|
||||
message = "GiveRewardEvent has no Reward asset assigned."
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateDialogLineRef(EncounterDialogOptionSet optionSet, Encounter encounter, string path, DialogLineRef lineRef, List<ValidationIssue> issues) {
|
||||
var library = optionSet.library;
|
||||
var hasId = !string.IsNullOrEmpty(lineRef.id);
|
||||
var hasInline = !string.IsNullOrEmpty(lineRef.inlineText);
|
||||
|
||||
if(!hasId && !hasInline) {
|
||||
issues.Add(new ValidationIssue {
|
||||
asset = optionSet,
|
||||
encounter = encounter,
|
||||
path = path,
|
||||
severity = ValidationSeverity.Warning,
|
||||
message = "Dialog line is empty (no id and no inline text)."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if(hasId && library == null) {
|
||||
issues.Add(new ValidationIssue {
|
||||
asset = optionSet,
|
||||
encounter = encounter,
|
||||
path = path,
|
||||
severity = ValidationSeverity.Error,
|
||||
message = $"DialogLineRef references id '{lineRef.id}' but the option set has no library assigned."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if(hasId && library != null && string.IsNullOrEmpty(library.Resolve(lineRef.id))) {
|
||||
issues.Add(new ValidationIssue {
|
||||
asset = optionSet,
|
||||
encounter = encounter,
|
||||
path = path,
|
||||
severity = ValidationSeverity.Error,
|
||||
message = $"DialogLineRef id '{lineRef.id}' not found in library '{library.name}'."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateReward(Reward reward, List<ValidationIssue> issues) {
|
||||
if(reward == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(reward.kind == null) {
|
||||
issues.Add(new ValidationIssue {
|
||||
asset = reward,
|
||||
path = "kind",
|
||||
severity = ValidationSeverity.Warning,
|
||||
message = "Reward.kind is null — pick a reward kind in the inspector."
|
||||
});
|
||||
}
|
||||
|
||||
if(string.IsNullOrEmpty(reward.id)) {
|
||||
issues.Add(new ValidationIssue {
|
||||
asset = reward,
|
||||
path = "id",
|
||||
severity = ValidationSeverity.Warning,
|
||||
message = "Reward has no id."
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static List<T> FindAssetsOfType<T>() where T : Object {
|
||||
var result = new List<T>();
|
||||
var guids = AssetDatabase.FindAssets("t:" + typeof(T).Name);
|
||||
foreach(var guid in guids) {
|
||||
var path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
var asset = AssetDatabase.LoadAssetAtPath<T>(path);
|
||||
if(asset != null) {
|
||||
result.Add(asset);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public static class EncounterValidatorMenu {
|
||||
[MenuItem("Jovian/Encounters/Validate All")]
|
||||
public static void ValidateAll() {
|
||||
var issues = EncounterValidator.ValidateProject();
|
||||
if(issues.Count == 0) {
|
||||
Debug.Log("[EncounterValidator] No issues found.");
|
||||
return;
|
||||
}
|
||||
|
||||
int errors = 0;
|
||||
int warnings = 0;
|
||||
foreach(var issue in issues) {
|
||||
if(issue.severity == ValidationSeverity.Error) {
|
||||
errors++;
|
||||
}
|
||||
else {
|
||||
warnings++;
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log($"[EncounterValidator] {issues.Count} issue(s) found — {errors} error(s), {warnings} warning(s). Click any log row to ping the offending asset.");
|
||||
|
||||
foreach(var issue in issues) {
|
||||
var assetName = issue.asset != null ? issue.asset.name : "<null>";
|
||||
var message = $"[EncounterValidator] {assetName} · {issue.path} — {issue.message}";
|
||||
if(issue.severity == ValidationSeverity.Error) {
|
||||
Debug.LogError(message, issue.asset);
|
||||
}
|
||||
else {
|
||||
Debug.LogWarning(message, issue.asset);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bab1779bc53230a4291cd3ff35774558
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "Jovian.EncounterSystem.Editor",
|
||||
"rootNamespace": "Jovian.EncounterSystem.Editor",
|
||||
"references": [
|
||||
"Jovian.EncounterSystem"
|
||||
],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fa750d6ecf6f41540ab10d47c136fc33
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,148 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Jovian.EncounterSystem;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Jovian.EncounterSystem.Editor {
|
||||
/// <summary>Concrete-type dropdown for <c>[SerializeReference]</c> fields, arrays, and list elements.</summary>
|
||||
[CustomPropertyDrawer(typeof(SubclassSelectorAttribute))]
|
||||
public class SubclassSelectorDrawer : PropertyDrawer {
|
||||
private static readonly Dictionary<Type, Type[]> TypeCache = new();
|
||||
|
||||
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) {
|
||||
if(property.propertyType != SerializedPropertyType.ManagedReference) {
|
||||
EditorGUI.LabelField(position, label.text, "[SubclassSelector] requires [SerializeReference]");
|
||||
return;
|
||||
}
|
||||
|
||||
var baseType = ResolveBaseType(fieldInfo.FieldType);
|
||||
var concreteTypes = GetConcreteTypes(baseType);
|
||||
var currentType = GetCurrentType(property);
|
||||
var currentIndex = Array.IndexOf(concreteTypes, currentType);
|
||||
|
||||
var names = new string[concreteTypes.Length + 1];
|
||||
names[0] = "<None>";
|
||||
for(int i = 0; i < concreteTypes.Length; i++) {
|
||||
names[i + 1] = concreteTypes[i].Name;
|
||||
}
|
||||
|
||||
var displayLabel = ResolveDisplayLabel(property, label);
|
||||
var headerRect = new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight);
|
||||
var labelRect = new Rect(headerRect.x, headerRect.y, EditorGUIUtility.labelWidth, headerRect.height);
|
||||
var popupRect = new Rect(headerRect.x + EditorGUIUtility.labelWidth, headerRect.y, headerRect.width - EditorGUIUtility.labelWidth, headerRect.height);
|
||||
|
||||
EditorGUI.LabelField(labelRect, displayLabel);
|
||||
var newIndex = EditorGUI.Popup(popupRect, currentIndex + 1, names);
|
||||
if(newIndex != currentIndex + 1) {
|
||||
property.managedReferenceValue = newIndex == 0 ? null : Activator.CreateInstance(concreteTypes[newIndex - 1]);
|
||||
property.serializedObject.ApplyModifiedProperties();
|
||||
}
|
||||
|
||||
if(property.managedReferenceValue == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
EditorGUI.indentLevel++;
|
||||
var y = position.y + EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
|
||||
var iterator = property.Copy();
|
||||
var end = iterator.GetEndProperty();
|
||||
if(iterator.NextVisible(true)) {
|
||||
while(!SerializedProperty.EqualContents(iterator, end)) {
|
||||
var h = EditorGUI.GetPropertyHeight(iterator, true);
|
||||
var r = new Rect(position.x, y, position.width, h);
|
||||
EditorGUI.PropertyField(r, iterator, true);
|
||||
y += h + EditorGUIUtility.standardVerticalSpacing;
|
||||
if(!iterator.NextVisible(false)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
|
||||
public override float GetPropertyHeight(SerializedProperty property, GUIContent label) {
|
||||
var height = EditorGUIUtility.singleLineHeight;
|
||||
if(property.propertyType != SerializedPropertyType.ManagedReference || property.managedReferenceValue == null) {
|
||||
return height;
|
||||
}
|
||||
|
||||
var iterator = property.Copy();
|
||||
var end = iterator.GetEndProperty();
|
||||
if(iterator.NextVisible(true)) {
|
||||
while(!SerializedProperty.EqualContents(iterator, end)) {
|
||||
height += EditorGUI.GetPropertyHeight(iterator, true) + EditorGUIUtility.standardVerticalSpacing;
|
||||
if(!iterator.NextVisible(false)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return height;
|
||||
}
|
||||
|
||||
private static Type ResolveBaseType(Type fieldType) {
|
||||
if(fieldType.IsArray) {
|
||||
return fieldType.GetElementType();
|
||||
}
|
||||
if(fieldType.IsGenericType && fieldType.GetGenericTypeDefinition() == typeof(List<>)) {
|
||||
return fieldType.GetGenericArguments()[0];
|
||||
}
|
||||
return fieldType;
|
||||
}
|
||||
|
||||
private static Type[] GetConcreteTypes(Type baseType) {
|
||||
if(TypeCache.TryGetValue(baseType, out var cached)) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
var types = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.SelectMany(assembly => {
|
||||
try {
|
||||
return assembly.GetTypes();
|
||||
}
|
||||
catch {
|
||||
return Array.Empty<Type>();
|
||||
}
|
||||
})
|
||||
.Where(type => baseType.IsAssignableFrom(type)
|
||||
&& !type.IsAbstract
|
||||
&& !type.IsInterface
|
||||
&& !typeof(UnityEngine.Object).IsAssignableFrom(type)
|
||||
&& type.GetConstructor(Type.EmptyTypes) != null)
|
||||
.OrderBy(type => type.Name)
|
||||
.ToArray();
|
||||
TypeCache[baseType] = types;
|
||||
return types;
|
||||
}
|
||||
|
||||
private static GUIContent ResolveDisplayLabel(SerializedProperty property, GUIContent fallback) {
|
||||
var value = property.managedReferenceValue;
|
||||
if(value is not IEncounter) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
var definitionProp = value.GetType().GetProperty("EncounterDefinition");
|
||||
var definition = definitionProp?.GetValue(value);
|
||||
var nameField = definition?.GetType().GetField("name");
|
||||
var name = nameField?.GetValue(definition) as string;
|
||||
return string.IsNullOrEmpty(name) ? fallback : new GUIContent(name);
|
||||
}
|
||||
|
||||
private static Type GetCurrentType(SerializedProperty property) {
|
||||
var full = property.managedReferenceFullTypename;
|
||||
if(string.IsNullOrEmpty(full)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var space = full.IndexOf(' ');
|
||||
if(space < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var assembly = full.Substring(0, space);
|
||||
var typeName = full.Substring(space + 1);
|
||||
return Type.GetType($"{typeName}, {assembly}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5e00ec3e6bdfd024aa73689289578192
|
||||
Reference in New Issue
Block a user