fixed the encounter window
This commit is contained in:
@@ -16,8 +16,19 @@ namespace Jovian.EncounterSystem.Editor {
|
||||
public EncounterTable table;
|
||||
public int index;
|
||||
public IEncounter encounter;
|
||||
public int depth;
|
||||
|
||||
public bool IsTableHeader => encounter == null;
|
||||
}
|
||||
|
||||
private static readonly Color[] DepthColors = {
|
||||
new(0.35f, 0.55f, 0.85f), // depth 0 — table headers (blue)
|
||||
new(0.90f, 0.90f, 0.90f), // depth 1 — encounters under table (neutral)
|
||||
new(0.70f, 0.90f, 0.55f), // depth 2 — chain step 1 (green)
|
||||
new(0.95f, 0.80f, 0.45f), // depth 3 — chain step 2 (amber)
|
||||
new(0.85f, 0.65f, 0.90f) // depth 4+ — deeper chain (violet)
|
||||
};
|
||||
|
||||
private readonly List<Record> allRecords = new();
|
||||
private string searchText = string.Empty;
|
||||
private string kindFilter = AllKinds;
|
||||
@@ -27,6 +38,7 @@ namespace Jovian.EncounterSystem.Editor {
|
||||
private TreeView treeView;
|
||||
private VisualElement detailPane;
|
||||
private ToolbarMenu kindDropdown;
|
||||
private VisualElement statusBanner;
|
||||
|
||||
[MenuItem("Jovian/Encounters/Encounter Browser")]
|
||||
public static void Open() {
|
||||
@@ -69,8 +81,26 @@ namespace Jovian.EncounterSystem.Editor {
|
||||
}
|
||||
|
||||
private void BuildSplit() {
|
||||
var split = new TwoPaneSplitView(0, 280, TwoPaneSplitViewOrientation.Horizontal);
|
||||
split.style.flexGrow = 1f;
|
||||
statusBanner = new VisualElement {
|
||||
style = {
|
||||
paddingLeft = 10,
|
||||
paddingRight = 10,
|
||||
paddingTop = 8,
|
||||
paddingBottom = 8,
|
||||
marginBottom = 2,
|
||||
flexDirection = FlexDirection.Column,
|
||||
backgroundColor = new StyleColor(new Color(0.85f, 0.4f, 0.15f, 0.35f)),
|
||||
display = DisplayStyle.None
|
||||
}
|
||||
};
|
||||
rootVisualElement.Add(statusBanner);
|
||||
|
||||
var split = new VisualElement {
|
||||
style = {
|
||||
flexDirection = FlexDirection.Row,
|
||||
flexGrow = 1f
|
||||
}
|
||||
};
|
||||
rootVisualElement.Add(split);
|
||||
|
||||
treeView = new TreeView {
|
||||
@@ -80,7 +110,10 @@ namespace Jovian.EncounterSystem.Editor {
|
||||
selectionType = SelectionType.Single
|
||||
};
|
||||
treeView.selectionChanged += OnSelectionChanged;
|
||||
treeView.style.flexGrow = 1f;
|
||||
treeView.style.width = 280;
|
||||
treeView.style.flexShrink = 0f;
|
||||
treeView.style.borderRightWidth = 1;
|
||||
treeView.style.borderRightColor = new StyleColor(new Color(0f, 0f, 0f, 0.4f));
|
||||
split.Add(treeView);
|
||||
|
||||
detailPane = new ScrollView(ScrollViewMode.Vertical) {
|
||||
@@ -97,7 +130,9 @@ namespace Jovian.EncounterSystem.Editor {
|
||||
alignItems = Align.Center,
|
||||
paddingLeft = 6,
|
||||
paddingRight = 6,
|
||||
height = 22
|
||||
height = 22,
|
||||
flexGrow = 1f,
|
||||
flexShrink = 0f
|
||||
}
|
||||
};
|
||||
|
||||
@@ -125,6 +160,19 @@ namespace Jovian.EncounterSystem.Editor {
|
||||
};
|
||||
row.Add(label);
|
||||
|
||||
var selectButton = new Button {
|
||||
name = "select-button",
|
||||
text = "Select",
|
||||
style = {
|
||||
marginLeft = 4,
|
||||
marginRight = 0,
|
||||
paddingLeft = 6,
|
||||
paddingRight = 6,
|
||||
display = DisplayStyle.None
|
||||
}
|
||||
};
|
||||
row.Add(selectButton);
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
@@ -132,9 +180,34 @@ namespace Jovian.EncounterSystem.Editor {
|
||||
var record = treeView.GetItemDataForIndex<Record>(index);
|
||||
var label = element.Q<Label>("row-label");
|
||||
var badge = element.Q<VisualElement>("issue-badge");
|
||||
var selectButton = element.Q<Button>("select-button");
|
||||
|
||||
var depthIndex = Mathf.Clamp(record.depth, 0, DepthColors.Length - 1);
|
||||
label.style.color = new StyleColor(DepthColors[depthIndex]);
|
||||
|
||||
if(record.IsTableHeader) {
|
||||
label.text = record.table != null ? record.table.name : "<missing table>";
|
||||
label.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
badge.style.visibility = Visibility.Hidden;
|
||||
element.tooltip = $"EncounterTable asset: {record.table?.name}";
|
||||
|
||||
selectButton.style.display = DisplayStyle.Flex;
|
||||
var table = record.table;
|
||||
selectButton.clickable = new Clickable(() => {
|
||||
if(table == null) {
|
||||
return;
|
||||
}
|
||||
Selection.activeObject = table;
|
||||
EditorGUIUtility.PingObject(table);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
label.style.unityFontStyleAndWeight = FontStyle.Normal;
|
||||
selectButton.style.display = DisplayStyle.None;
|
||||
|
||||
var name = record.encounter?.EncounterDefinition?.name;
|
||||
var kind = record.encounter?.Kind?.GetType().Name ?? "—";
|
||||
var kind = record.encounter?.EncounterDefinition?.Kind?.GetType().Name ?? "—";
|
||||
label.text = string.IsNullOrEmpty(name)
|
||||
? $"<unnamed> [{kind}]"
|
||||
: $"{name} [{kind}]";
|
||||
@@ -180,20 +253,33 @@ namespace Jovian.EncounterSystem.Editor {
|
||||
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) {
|
||||
var registries = FindAssetsOfType<EncounterRegistry>().ToList();
|
||||
UpdateStatusBanner(registries);
|
||||
|
||||
var seenTables = new HashSet<EncounterTable>();
|
||||
foreach(var registry in registries) {
|
||||
if(registry?.encounterCollections == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for(int i = 0; i < table.encounters.Count; i++) {
|
||||
allRecords.Add(new Record {
|
||||
table = table,
|
||||
index = i,
|
||||
encounter = table.encounters[i]
|
||||
});
|
||||
foreach(var collection in registry.encounterCollections) {
|
||||
if(collection?.encounterTables == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach(var table in collection.encounterTables) {
|
||||
if(table?.encounters == null || !seenTables.Add(table)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for(var i = 0; i < table.encounters.Count; i++) {
|
||||
allRecords.Add(new Record {
|
||||
table = table,
|
||||
index = i,
|
||||
encounter = table.encounters[i]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,6 +287,72 @@ namespace Jovian.EncounterSystem.Editor {
|
||||
ApplyFilter();
|
||||
}
|
||||
|
||||
private void UpdateStatusBanner(List<EncounterRegistry> registries) {
|
||||
statusBanner.Clear();
|
||||
|
||||
if(registries.Count == 0) {
|
||||
statusBanner.style.display = DisplayStyle.Flex;
|
||||
statusBanner.Add(new Label("No EncounterRegistry asset found. The browser reads encounters from the registry — without one, it has nothing to show.") {
|
||||
style = { marginBottom = 6, whiteSpace = WhiteSpace.Normal }
|
||||
});
|
||||
statusBanner.Add(new Button(CreateRegistryInteractive) {
|
||||
text = "Create Registry…",
|
||||
style = { alignSelf = Align.FlexStart }
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
var emptyRegistries = registries.FindAll(r => r?.encounterCollections == null || r.encounterCollections.Length == 0);
|
||||
if(emptyRegistries.Count == registries.Count) {
|
||||
statusBanner.style.display = DisplayStyle.Flex;
|
||||
statusBanner.Add(new Label("Registry found but no EncountersCollection is assigned to its 'encounterCollections' array.") {
|
||||
style = { whiteSpace = WhiteSpace.Normal }
|
||||
});
|
||||
var pingButton = new Button(() => {
|
||||
Selection.activeObject = registries[0];
|
||||
EditorGUIUtility.PingObject(registries[0]);
|
||||
}) {
|
||||
text = "Open Registry",
|
||||
style = { alignSelf = Align.FlexStart, marginTop = 6 }
|
||||
};
|
||||
statusBanner.Add(pingButton);
|
||||
return;
|
||||
}
|
||||
|
||||
statusBanner.style.display = DisplayStyle.None;
|
||||
}
|
||||
|
||||
private void CreateRegistryInteractive() {
|
||||
var path = EditorUtility.SaveFilePanelInProject(
|
||||
"Create Encounter Registry",
|
||||
"EncounterRegistry",
|
||||
"asset",
|
||||
"Choose where to save the new EncounterRegistry asset.");
|
||||
if(string.IsNullOrEmpty(path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var registry = ScriptableObject.CreateInstance<EncounterRegistry>();
|
||||
AssetDatabase.CreateAsset(registry, path);
|
||||
AssetDatabase.SaveAssets();
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
Selection.activeObject = registry;
|
||||
EditorGUIUtility.PingObject(registry);
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private static IEnumerable<T> FindAssetsOfType<T>() where T : UnityEngine.Object {
|
||||
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) {
|
||||
yield return asset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RebuildIssueIndex() {
|
||||
issuesByEncounter.Clear();
|
||||
var issues = EncounterValidator.ValidateProject();
|
||||
@@ -219,16 +371,46 @@ namespace Jovian.EncounterSystem.Editor {
|
||||
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();
|
||||
if(treeView == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
treeView.ClearSelection();
|
||||
treeView.SetRootItems(items);
|
||||
treeView.Rebuild();
|
||||
// Expand after layout settles — ExpandAll immediately after Rebuild can no-op in some cases.
|
||||
treeView.schedule.Execute(() => treeView.ExpandAll()).ExecuteLater(0);
|
||||
ShowEmptyDetail();
|
||||
}
|
||||
|
||||
private static List<TreeViewItemData<Record>> BuildTreeItems(List<Record> records) {
|
||||
// Group by table: each table becomes a top-level row, its encounters nest under it.
|
||||
var byTable = new Dictionary<EncounterTable, List<Record>>();
|
||||
var tableOrder = new List<EncounterTable>();
|
||||
foreach(var r in records) {
|
||||
if(r.table == null) {
|
||||
continue;
|
||||
}
|
||||
if(!byTable.TryGetValue(r.table, out var list)) {
|
||||
list = new List<Record>();
|
||||
byTable[r.table] = list;
|
||||
tableOrder.Add(r.table);
|
||||
}
|
||||
list.Add(r);
|
||||
}
|
||||
|
||||
var result = new List<TreeViewItemData<Record>>();
|
||||
var uid = 0;
|
||||
foreach(var table in tableOrder) {
|
||||
var tableRecord = new Record { table = table, index = -1, encounter = null, depth = 0 };
|
||||
var tableId = uid++;
|
||||
var children = BuildEncounterNodes(byTable[table], 1, ref uid);
|
||||
result.Add(new TreeViewItemData<Record>(tableId, tableRecord, children));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static List<TreeViewItemData<Record>> BuildEncounterNodes(List<Record> records, int depth, ref int uid) {
|
||||
var byEncounter = new Dictionary<IEncounter, Record>();
|
||||
foreach(var r in records) {
|
||||
if(r.encounter != null) {
|
||||
@@ -236,41 +418,81 @@ namespace Jovian.EncounterSystem.Editor {
|
||||
}
|
||||
}
|
||||
|
||||
// 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>();
|
||||
// Chain nesting only applies to chains contained within this table. Cross-table chains
|
||||
// naturally appear flat (the target encounter lives under its own table header).
|
||||
var predecessorCount = new Dictionary<IEncounter, int>();
|
||||
foreach(var r in records) {
|
||||
if(r.encounter?.Kind is not QuestKind quest) {
|
||||
if(r.encounter?.EncounterDefinition?.Kind is not QuestKind quest) {
|
||||
continue;
|
||||
}
|
||||
var next = quest.nextEncounter.Resolve();
|
||||
if(next != null && byEncounter.ContainsKey(next)) {
|
||||
nonRoot.Add(next);
|
||||
predecessorCount.TryGetValue(next, out var count);
|
||||
predecessorCount[next] = count + 1;
|
||||
}
|
||||
}
|
||||
|
||||
var result = new List<TreeViewItemData<Record>>();
|
||||
var uid = 0;
|
||||
var parentOf = new Dictionary<IEncounter, IEncounter>();
|
||||
foreach(var r in records) {
|
||||
if(r.encounter != null && nonRoot.Contains(r.encounter)) {
|
||||
if(r.encounter?.EncounterDefinition?.Kind is not QuestKind quest) {
|
||||
continue;
|
||||
}
|
||||
var next = quest.nextEncounter.Resolve();
|
||||
if(next == null || !byEncounter.ContainsKey(next)) {
|
||||
continue;
|
||||
}
|
||||
if(predecessorCount[next] != 1) {
|
||||
continue;
|
||||
}
|
||||
if(ForwardReaches(next, r.encounter, byEncounter)) {
|
||||
continue;
|
||||
}
|
||||
parentOf[next] = r.encounter;
|
||||
}
|
||||
|
||||
var nodes = new List<TreeViewItemData<Record>>();
|
||||
foreach(var r in records) {
|
||||
if(r.encounter != null && parentOf.ContainsKey(r.encounter)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var children = BuildChainChildren(r, byEncounter, ref uid);
|
||||
result.Add(new TreeViewItemData<Record>(uid++, r, children));
|
||||
r.depth = depth;
|
||||
var rootId = uid++;
|
||||
var children = BuildChainChildren(r, byEncounter, parentOf, depth + 1, ref uid);
|
||||
nodes.Add(new TreeViewItemData<Record>(rootId, r, children));
|
||||
}
|
||||
return result;
|
||||
return nodes;
|
||||
}
|
||||
|
||||
private static List<TreeViewItemData<Record>> BuildChainChildren(Record root, Dictionary<IEncounter, Record> byEncounter, ref int uid) {
|
||||
private static bool ForwardReaches(IEncounter start, IEncounter target, Dictionary<IEncounter, Record> byEncounter) {
|
||||
var current = start;
|
||||
var visited = new HashSet<IEncounter>();
|
||||
while(current?.EncounterDefinition?.Kind is QuestKind quest) {
|
||||
var next = quest.nextEncounter.Resolve();
|
||||
if(next == null || !byEncounter.ContainsKey(next)) {
|
||||
return false;
|
||||
}
|
||||
if(next == target) {
|
||||
return true;
|
||||
}
|
||||
if(!visited.Add(next)) {
|
||||
return false;
|
||||
}
|
||||
current = next;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static List<TreeViewItemData<Record>> BuildChainChildren(Record root, Dictionary<IEncounter, Record> byEncounter, Dictionary<IEncounter, IEncounter> parentOf, int depth, ref int uid) {
|
||||
var children = new List<TreeViewItemData<Record>>();
|
||||
if(root.encounter?.Kind is not QuestKind) {
|
||||
if(root.encounter?.EncounterDefinition?.Kind is not QuestKind) {
|
||||
return children;
|
||||
}
|
||||
|
||||
var current = root.encounter;
|
||||
var visited = new HashSet<IEncounter> { current };
|
||||
while(current?.Kind is QuestKind quest) {
|
||||
var currentDepth = depth;
|
||||
while(current?.EncounterDefinition?.Kind is QuestKind quest) {
|
||||
var next = quest.nextEncounter.Resolve();
|
||||
if(next == null || !visited.Add(next)) {
|
||||
break;
|
||||
@@ -278,14 +500,19 @@ namespace Jovian.EncounterSystem.Editor {
|
||||
if(!byEncounter.TryGetValue(next, out var nextRecord)) {
|
||||
break;
|
||||
}
|
||||
if(!parentOf.TryGetValue(next, out var parent) || parent != current) {
|
||||
break;
|
||||
}
|
||||
nextRecord.depth = currentDepth;
|
||||
children.Add(new TreeViewItemData<Record>(uid++, nextRecord));
|
||||
current = next;
|
||||
currentDepth++;
|
||||
}
|
||||
return children;
|
||||
}
|
||||
|
||||
private bool Matches(Record record) {
|
||||
var kindName = record.encounter?.Kind?.GetType().Name ?? string.Empty;
|
||||
var kindName = record.encounter?.EncounterDefinition?.Kind?.GetType().Name ?? string.Empty;
|
||||
if(kindFilter != AllKinds && kindName != kindFilter) {
|
||||
return false;
|
||||
}
|
||||
@@ -312,20 +539,40 @@ namespace Jovian.EncounterSystem.Editor {
|
||||
return;
|
||||
}
|
||||
|
||||
if(record.IsTableHeader) {
|
||||
ShowTableHeaderDetail(record.table);
|
||||
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}]") {
|
||||
var headerRow = new VisualElement {
|
||||
style = {
|
||||
unityFontStyleAndWeight = FontStyle.Bold,
|
||||
marginBottom = 6,
|
||||
color = new StyleColor(new Color(0.75f, 0.75f, 0.75f))
|
||||
flexDirection = FlexDirection.Row,
|
||||
alignItems = Align.Center,
|
||||
marginBottom = 6
|
||||
}
|
||||
};
|
||||
detailPane.Add(header);
|
||||
headerRow.Add(new Label($"{record.table.name} → [{record.index}]") {
|
||||
style = {
|
||||
unityFontStyleAndWeight = FontStyle.Bold,
|
||||
flexGrow = 1f,
|
||||
color = new StyleColor(new Color(0.75f, 0.75f, 0.75f))
|
||||
}
|
||||
});
|
||||
var pingButton = new Button(() => {
|
||||
Selection.activeObject = record.table;
|
||||
EditorGUIUtility.PingObject(record.table);
|
||||
}) {
|
||||
text = "Select Table",
|
||||
tooltip = "Select and ping the owning EncounterTable asset in the Project window."
|
||||
};
|
||||
headerRow.Add(pingButton);
|
||||
detailPane.Add(headerRow);
|
||||
|
||||
elementProp.isExpanded = true;
|
||||
var field = new PropertyField(elementProp);
|
||||
@@ -336,7 +583,7 @@ namespace Jovian.EncounterSystem.Editor {
|
||||
}
|
||||
|
||||
private void AddChainPreviewIfQuest(Record record) {
|
||||
if(record.encounter?.Kind is not QuestKind questKind) {
|
||||
if(record.encounter?.EncounterDefinition?.Kind is not QuestKind questKind) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -367,7 +614,7 @@ namespace Jovian.EncounterSystem.Editor {
|
||||
}
|
||||
|
||||
foreach(var record in allRecords) {
|
||||
if(record.encounter?.Kind is not QuestKind kind) {
|
||||
if(record.encounter?.EncounterDefinition?.Kind is not QuestKind kind) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -379,6 +626,38 @@ namespace Jovian.EncounterSystem.Editor {
|
||||
return null;
|
||||
}
|
||||
|
||||
private void ShowTableHeaderDetail(EncounterTable table) {
|
||||
detailPane.Clear();
|
||||
if(table == null) {
|
||||
ShowEmptyDetail();
|
||||
return;
|
||||
}
|
||||
|
||||
var headerRow = new VisualElement {
|
||||
style = {
|
||||
flexDirection = FlexDirection.Row,
|
||||
alignItems = Align.Center,
|
||||
marginBottom = 6
|
||||
}
|
||||
};
|
||||
headerRow.Add(new Label(table.name) {
|
||||
style = {
|
||||
unityFontStyleAndWeight = FontStyle.Bold,
|
||||
flexGrow = 1f
|
||||
}
|
||||
});
|
||||
headerRow.Add(new Button(() => {
|
||||
Selection.activeObject = table;
|
||||
EditorGUIUtility.PingObject(table);
|
||||
}) { text = "Select Table" });
|
||||
detailPane.Add(headerRow);
|
||||
|
||||
var count = table.encounters?.Count ?? 0;
|
||||
detailPane.Add(new Label($"{count} encounter(s). Expand the row to browse them, or pick one to edit.") {
|
||||
style = { color = new StyleColor(Color.gray) }
|
||||
});
|
||||
}
|
||||
|
||||
private void ShowEmptyDetail() {
|
||||
if(detailPane == null) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user