validation and dialogue system
This commit is contained in:
122
Editor/DialogLineRefDrawer.cs
Normal file
122
Editor/DialogLineRefDrawer.cs
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
using Jovian.EncounterSystem;
|
||||||
|
using UnityEditor;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Jovian.EncounterSystem.Editor {
|
||||||
|
/// <summary>
|
||||||
|
/// Drawer for <see cref="DialogLineRef"/>. 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.
|
||||||
|
/// </summary>
|
||||||
|
[CustomPropertyDrawer(typeof(DialogLineRef))]
|
||||||
|
public class DialogLineRefDrawer : PropertyDrawer {
|
||||||
|
private const string NonePlaceholder = "<none>";
|
||||||
|
private const string EmptyLibraryPlaceholder = "<assign a library first>";
|
||||||
|
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) ? $"<unnamed {i}>" : 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) ? "<no text will display>" : resolved;
|
||||||
|
EditorGUI.LabelField(rect, new GUIContent("Preview: " + label), style);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -160,7 +160,13 @@ namespace Jovian.EncounterSystem.Editor {
|
|||||||
|
|
||||||
for(int o = 0; o < optionSet.options.Count; o++) {
|
for(int o = 0; o < optionSet.options.Count; o++) {
|
||||||
var option = optionSet.options[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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,6 +196,43 @@ namespace Jovian.EncounterSystem.Editor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void ValidateDialogLineRef(EncounterDialogOptionSet optionSet, Encounter encounter, string path, DialogLineRef lineRef, List<ValidationIssue> 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<ValidationIssue> issues) {
|
private static void ValidateReward(Reward reward, List<ValidationIssue> issues) {
|
||||||
if(reward == null) {
|
if(reward == null) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
24
README.md
24
README.md
@@ -11,7 +11,9 @@ Packages/com.jovian.encounter-system/
|
|||||||
│ ├── IEncounterKind.cs ← marker interface + Combat/Quest/Social/Puzzle/...
|
│ ├── IEncounterKind.cs ← marker interface + Combat/Quest/Social/Puzzle/...
|
||||||
│ ├── EncounterTable.cs ← ScriptableObject: list of encounters + roll helpers
|
│ ├── EncounterTable.cs ← ScriptableObject: list of encounters + roll helpers
|
||||||
│ ├── EncountersCollection.cs ← ScriptableObject: group of tables
|
│ ├── 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)
|
│ ├── EncounterLink.cs ← cross-table reference (table + internalId)
|
||||||
│ ├── EncounterReference.cs ← MonoBehaviour scaffold wiring common UI fields
|
│ ├── EncounterReference.cs ← MonoBehaviour scaffold wiring common UI fields
|
||||||
│ ├── EncounterRegistry.cs ← id → encounter lookup cache
|
│ ├── EncounterRegistry.cs ← id → encounter lookup cache
|
||||||
@@ -26,6 +28,8 @@ Packages/com.jovian.encounter-system/
|
|||||||
└── Editor/
|
└── Editor/
|
||||||
├── SubclassSelectorDrawer.cs ← dropdown + inline children for [SerializeReference] fields
|
├── SubclassSelectorDrawer.cs ← dropdown + inline children for [SerializeReference] fields
|
||||||
├── EncounterLinkDrawer.cs ← two-row table + encounter picker
|
├── 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
|
├── EncounterValidator.cs ← project-wide scan + "Validate All" menu + browser badges
|
||||||
└── EncounterBrowserWindow.cs ← Jovian → Encounters → Encounter Browser
|
└── EncounterBrowserWindow.cs ← Jovian → Encounters → Encounter Browser
|
||||||
```
|
```
|
||||||
@@ -90,6 +94,21 @@ resolver.Register<GiveRewardEvent>((evt, ctx) => rewardApplier.Apply(evt.reward,
|
|||||||
|
|
||||||
`rewardApplier` lives in game code — the package ships only the data types.
|
`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<SomeEvent>((evt, ctx) => {
|
||||||
|
var text = option.text.Resolve(); // library lookup, inline fallback, null if both empty
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
### Cross-table references via `EncounterLink`
|
### 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.
|
`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 → Encounter Table | New table asset |
|
||||||
| Assets → Create → Jovian → Encounter System → Encounters Collection | New collection 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 |
|
| Assets → Create → Jovian → Encounter System → Encounter Registry | New registry asset |
|
||||||
| Jovian → Encounters → Encounter Browser | Searchable designer browser |
|
| Jovian → Encounters → Encounter Browser | Searchable designer browser |
|
||||||
| Jovian → Encounters → Validate All | Scan all tables/rewards for issues, print click-through report |
|
| Jovian → Encounters → Validate All | Scan all tables/rewards for issues, print click-through report |
|
||||||
|
|||||||
Reference in New Issue
Block a user