quot;Lv {lvl,3}: total {total,10:N0} (+{step:N0})"); } table.text = sb.ToString(); } Refresh(); root.TrackSerializedObjectValue(serializedObject, _ => Refresh()); return root; } } ``` ## 7.2 Validation menu This menu item finds common data mistakes: derived-stat cycles, items without ids, duplicate item ids, and classes missing a primary stat. ```csharp // Editor/RpgValidator.cs using System.Collections.Generic; using UnityEditor; using UnityEngine; public static class RpgValidator { [MenuItem("Tools/RPG/Validate Data")] public static void Validate() { int problems = 0; var items = Load<ItemDefinition>(); var seen = new Dictionary<string, ItemDefinition>(); foreach (var i in items) { if (string.IsNullOrEmpty(i.id)) { Debug.LogError(quot;Item without id: {i.name}", i); problems++; continue; } if (seen.TryGetValue(i.id, out var other)) { Debug.LogError(quot;Duplicate item id '{i.id}': {i.name} and {other.name}", i); problems++; } else seen[i.id] = i; } var formulas = Load<DerivedStatFormula>(); var graph = new Dictionary<StatDefinition, List<StatDefinition>>(); foreach (var f in formulas) { if (!f.output) continue; graph[f.output] = new List<StatDefinition>(); foreach (var t in f.terms) if (t.input) graph[f.output].Add(t.input); } foreach (var start in graph.Keys) if (HasCycle(start, graph, new HashSet<StatDefinition>())) { Debug.LogError(quot;Derived stat cycle involving {start.name}", start); problems++; } var stats = Load<StatDefinition>(); foreach (var c in Load<CharacterClass>()) foreach (var s in stats) if (!s.isDerived && !c.baseStats.Exists(b => b.stat == s)) { Debug.LogWarning(quot;Class {c.name} has no base value for {s.name}", c); problems++; } Debug.Log(problems == 0 ? "RPG data OK" : quot;RPG data: {problems} problem(s)"); } static bool HasCycle(StatDefinition node, Dictionary<StatDefinition, List<StatDefinition>> g, HashSet<StatDefinition> path) { if (!path.Add(node)) return true; if (g.TryGetValue(node, out var deps)) foreach (var d in deps) if (HasCycle(d, g, path)) return true; path.Remove(node); return false; } static List<T> Load<T>() where T : Object { var list = new List<T>(); foreach (var guid in AssetDatabase.FindAssets(quot;t:{typeof(T).Name}")) { var a = AssetDatabase.LoadAssetAtPath<T>(AssetDatabase.GUIDToAssetPath(guid)); if (a) list.Add(a); } return list; } } ``` ## 7.3 More tips - **Fill the RpgDatabase automatically.** Add a `[MenuItem("Tools/RPG/Rebuild Database")]` that runs `AssetDatabase.FindAssets` for each type and assigns the lists, so no one has to drag items in by hand. - **Live stat debugger.** Put a `[ContextMenu("Dump Stats")]` on `CharacterStats` that logs each stat with its base, modifiers, and sources. It's quick to add and answers most "why is my damage 3?" questions. - **Balance in a spreadsheet, import as assets.** For dozens of items, keep a CSV (`id,name,slot,minDmg,maxDmg,STR,DEX…`) and write an importer that creates or updates `ItemDefinition` assets. Use `AssetDatabase.CreateAsset` for new ones and `EditorUtility.SetDirty` plus `AssetDatabase.SaveAssets` for edits. - **Simulate fights in Edit Mode.** Because `DamageCalculator` is pure, an editor window can run 10,000 simulated hits and show average time-to-kill for each level pairing. - **Show expected DPS in item inspectors** using `DamageCalculator.ExpectedDamage`. - **Don't edit ScriptableObjects at runtime** to "test a buff". Use the `BuffController` so you aren't left with dirty assets afterward.