From 8ce041e2d8916745a48c0ca27baeecc3b87b2af2 Mon Sep 17 00:00:00 2001 From: Sebastian Bularca Date: Sun, 19 Apr 2026 12:27:13 +0200 Subject: [PATCH] validation and dialogue system --- Editor/DialogLineRefDrawer.cs | 122 ++++++++++++++++++++++++++++++++++ Editor/EncounterValidator.cs | 45 ++++++++++++- README.md | 24 ++++++- 3 files changed, 188 insertions(+), 3 deletions(-) create mode 100644 Editor/DialogLineRefDrawer.cs diff --git a/Editor/DialogLineRefDrawer.cs b/Editor/DialogLineRefDrawer.cs new file mode 100644 index 0000000..692aa09 --- /dev/null +++ b/Editor/DialogLineRefDrawer.cs @@ -0,0 +1,122 @@ +using Jovian.EncounterSystem; +using UnityEditor; +using UnityEngine; + +namespace Jovian.EncounterSystem.Editor { + /// + /// Drawer for . Shows the library + id picker on one row, the inline + /// fallback text area underneath, and a resolved preview below that. Preserves WYSIWYG — the + /// final text is always visible. + /// + [CustomPropertyDrawer(typeof(DialogLineRef))] + public class DialogLineRefDrawer : PropertyDrawer { + private const string NonePlaceholder = ""; + private const string EmptyLibraryPlaceholder = ""; + private const float PreviewHeight = 32f; + + public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { + var libraryProp = property.FindPropertyRelative("library"); + var idProp = property.FindPropertyRelative("id"); + var inlineProp = property.FindPropertyRelative("inlineText"); + + EditorGUI.BeginProperty(position, label, property); + + var lineHeight = EditorGUIUtility.singleLineHeight; + var spacing = EditorGUIUtility.standardVerticalSpacing; + + var libraryRect = new Rect(position.x, position.y, position.width, lineHeight); + EditorGUI.BeginChangeCheck(); + EditorGUI.PropertyField(libraryRect, libraryProp, label); + var libraryChanged = EditorGUI.EndChangeCheck(); + + using(new EditorGUI.IndentLevelScope()) { + var idRect = new Rect(position.x, position.y + lineHeight + spacing, position.width, lineHeight); + DrawIdPicker(idRect, libraryProp, idProp, libraryChanged); + + var inlineRect = new Rect( + position.x, + position.y + (lineHeight + spacing) * 2, + position.width, + lineHeight * 2); + EditorGUI.PropertyField(inlineRect, inlineProp, new GUIContent("Inline")); + + var previewRect = new Rect( + position.x, + position.y + (lineHeight + spacing) * 2 + lineHeight * 2 + spacing, + position.width, + PreviewHeight); + DrawPreview(previewRect, libraryProp, idProp, inlineProp); + } + + EditorGUI.EndProperty(); + } + + public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { + var lineHeight = EditorGUIUtility.singleLineHeight; + var spacing = EditorGUIUtility.standardVerticalSpacing; + // library + id + (inline 2 lines) + preview + return (lineHeight + spacing) * 2 + lineHeight * 2 + spacing + PreviewHeight; + } + + private static void DrawIdPicker(Rect rect, SerializedProperty libraryProp, SerializedProperty idProp, bool libraryChanged) { + var library = libraryProp.objectReferenceValue as DialogLineLibrary; + if(library == null || library.lines == null || library.lines.Count == 0) { + using(new EditorGUI.DisabledScope(true)) { + EditorGUI.Popup(rect, "Id", 0, new[] { EmptyLibraryPlaceholder }); + } + if(libraryChanged) { + idProp.stringValue = string.Empty; + } + 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) ? $"" : id; + if(!libraryChanged && id == idProp.stringValue && !string.IsNullOrEmpty(id)) { + currentIndex = i + 1; + } + } + + if(libraryChanged) { + idProp.stringValue = string.Empty; + currentIndex = 0; + } + + var newIndex = EditorGUI.Popup(rect, "Id", currentIndex, names); + if(newIndex != currentIndex) { + idProp.stringValue = ids[newIndex]; + } + } + + private static void DrawPreview(Rect rect, SerializedProperty libraryProp, SerializedProperty idProp, SerializedProperty inlineProp) { + var library = libraryProp.objectReferenceValue as DialogLineLibrary; + 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) ? "" : resolved; + EditorGUI.LabelField(rect, new GUIContent("Preview: " + label), style); + } + } +} diff --git a/Editor/EncounterValidator.cs b/Editor/EncounterValidator.cs index 0992c34..0528b7e 100644 --- a/Editor/EncounterValidator.cs +++ b/Editor/EncounterValidator.cs @@ -160,7 +160,13 @@ namespace Jovian.EncounterSystem.Editor { for(int o = 0; o < optionSet.options.Count; o++) { var option = optionSet.options[o]; - if(option?.events == null) { + if(option == null) { + continue; + } + + ValidateDialogLineRef(optionSet, encounter, $"options[{o}].text", option.text, issues); + + if(option.events == null) { continue; } @@ -190,6 +196,43 @@ namespace Jovian.EncounterSystem.Editor { } } + private static void ValidateDialogLineRef(EncounterDialogOptionSet optionSet, Encounter encounter, string path, DialogLineRef lineRef, List issues) { + var hasLibrary = lineRef.library != null; + var hasId = !string.IsNullOrEmpty(lineRef.id); + var hasInline = !string.IsNullOrEmpty(lineRef.inlineText); + + if(!hasLibrary && !hasInline) { + issues.Add(new ValidationIssue { + asset = optionSet, + encounter = encounter, + path = path, + severity = ValidationSeverity.Warning, + message = "Dialog line is empty (no library reference and no inline text)." + }); + return; + } + + if(hasLibrary && hasId && string.IsNullOrEmpty(lineRef.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 '{lineRef.library.name}'." + }); + } + + if(hasLibrary && !hasId && !hasInline) { + issues.Add(new ValidationIssue { + asset = optionSet, + encounter = encounter, + path = path, + severity = ValidationSeverity.Warning, + message = "Library assigned but no id picked; will fall back to empty inline text." + }); + } + } + private static void ValidateReward(Reward reward, List issues) { if(reward == null) { return; diff --git a/README.md b/README.md index 1c5d7ae..4548ca0 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,9 @@ Packages/com.jovian.encounter-system/ │ ├── IEncounterKind.cs ← marker interface + Combat/Quest/Social/Puzzle/... │ ├── EncounterTable.cs ← ScriptableObject: list of encounters + roll helpers │ ├── EncountersCollection.cs ← ScriptableObject: group of tables -│ ├── EncounterDialogOptionSet.cs ← ScriptableObject: shared option list +│ ├── EncounterDialogOptionSet.cs ← ScriptableObject: shared option list (auto-renames to id) +│ ├── DialogLineLibrary.cs ← ScriptableObject: id → text registry for reusable lines +│ ├── DialogLineRef.cs ← struct: library+id reference with inline fallback │ ├── EncounterLink.cs ← cross-table reference (table + internalId) │ ├── EncounterReference.cs ← MonoBehaviour scaffold wiring common UI fields │ ├── EncounterRegistry.cs ← id → encounter lookup cache @@ -26,6 +28,8 @@ Packages/com.jovian.encounter-system/ └── Editor/ ├── SubclassSelectorDrawer.cs ← dropdown + inline children for [SerializeReference] fields ├── EncounterLinkDrawer.cs ← two-row table + encounter picker + ├── DialogLineRefDrawer.cs ← library+id picker with inline fallback and live preview + ├── EncounterDrawer.cs ← list element label shows encounter id ├── EncounterValidator.cs ← project-wide scan + "Validate All" menu + browser badges └── EncounterBrowserWindow.cs ← Jovian → Encounters → Encounter Browser ``` @@ -90,6 +94,21 @@ resolver.Register((evt, ctx) => rewardApplier.Apply(evt.reward, `rewardApplier` lives in game code — the package ships only the data types. +### Reusable dialog lines via `DialogLineLibrary` + `DialogLineRef` + +A single `DialogLineLibrary` asset (or a handful split by topic) holds `{ id → text }` entries. Every `EncounterDialogOption.text` is a `DialogLineRef` struct that resolves in this order: + +1. If the library reference + id resolves, use that. +2. Otherwise fall back to `inlineText`. + +The drawer shows all three inputs plus a live preview of the final text — WYSIWYG survives. Designers prototype inline and promote common lines to the library later without changing the field's type. + +```csharp +resolver.Register((evt, ctx) => { + var text = option.text.Resolve(); // library lookup, inline fallback, null if both empty +}); +``` + ### Cross-table references via `EncounterLink` `EncounterLink` stores a table asset + stable `internalId` GUID. Rename-safe (the GUID doesn't change) and diffable. The custom drawer renders two dropdowns: first pick a table, then pick an encounter inside it. @@ -141,7 +160,8 @@ encounterRegistry.GetEncounters().TryGetValue(id, out var encounter); |-----------|-------------| | Assets → Create → Jovian → Encounter System → Encounter Table | New table asset | | Assets → Create → Jovian → Encounter System → Encounters Collection | New collection asset | -| Assets → Create → Jovian → Encounter System → Dialog Option Set | New dialog option set | +| Assets → Create → Jovian → Encounter System → Dialog Option Set | New dialog option set (auto-renames to id) | +| Assets → Create → Jovian → Encounter System → Dialog Line Library | New shared dialog line library | | Assets → Create → Jovian → Encounter System → Encounter Registry | New registry asset | | Jovian → Encounters → Encounter Browser | Searchable designer browser | | Jovian → Encounters → Validate All | Scan all tables/rewards for issues, print click-through report |