using System; using System.Collections.Generic; using System.IO; using NightclubArcadia.Skills; using TMPro; using UnityEditor; using UnityEditor.Callbacks; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.UI; using Yarn.Unity; /// /// Regenerates SkillDefinition assets + SkillDatabase from the Skill Bible JSON mirror. /// /// Source of truth for prose/roster: the Obsidian Skill System Draft.md. /// Checked-in machine mirror: Assets/Skills/skill_bible.json (edit that, not the .assets). /// Hand-edits to Definitions/*.asset are overwritten on script reload by EnsureSkillAssetsOnly. /// public static class SkillSystemSetup { const string DefinitionsFolder = "Assets/Skills/Definitions"; const string SkillsFolder = "Assets/Skills"; const string BiblePath = "Assets/Skills/skill_bible.json"; const string DatabasePath = "Assets/Skills/SkillDatabase.asset"; const string ScenePath = "Assets/Scenes/DialogueTest.unity"; const string AutoSetupSessionKey = "NightclubArcadia.SkillSystem.AutoSetupDone"; [DidReloadScripts] static void AutoSetupAfterReload() { if (EditorApplication.isPlayingOrWillChangePlaymode) { return; } if (SessionState.GetBool(AutoSetupSessionKey, false)) { return; } // Defer one frame so AssetDatabase is fully ready after domain reload. EditorApplication.delayCall += () => { if (SessionState.GetBool(AutoSetupSessionKey, false)) { return; } SessionState.SetBool(AutoSetupSessionKey, true); EnsureSkillAssetsOnly(); }; } /// /// Creates/updates ScriptableObject assets without opening a scene. /// Safe to run on domain reload. Scene wiring stays on the menu item / /// the checked-in DialogueTest.unity Skill System objects. /// static void EnsureSkillAssetsOnly() { if (!TryLoadRoster(out var roster)) { return; } EnsureFolder("Assets", "Skills"); EnsureFolder("Assets/Skills", "Definitions"); var definitions = new List(); var rosterIds = new HashSet(StringComparer.Ordinal); foreach (var entry in roster) { rosterIds.Add(entry.id); definitions.Add(CreateOrUpdateDefinition(entry)); } DeleteOrphanDefinitions(rosterIds); var database = AssetDatabase.LoadAssetAtPath(DatabasePath); if (database == null) { database = ScriptableObject.CreateInstance(); AssetDatabase.CreateAsset(database, DatabasePath); } database.EditorSetSkills(definitions); EditorUtility.SetDirty(database); AssetDatabase.SaveAssets(); Debug.Log($"[SkillSystemSetup] Skill assets ready at {DatabasePath}. Run Tools/Nightclub Arcadia/Set Up Skill System if scene refs need rewiring."); } [MenuItem("Tools/Nightclub Arcadia/Set Up Skill System")] public static void SetUpSkillSystem() { if (!TryLoadRoster(out var roster)) { return; } EnsureFolder("Assets", "Skills"); EnsureFolder("Assets/Skills", "Definitions"); var definitions = new List(); var rosterIds = new HashSet(StringComparer.Ordinal); foreach (var entry in roster) { rosterIds.Add(entry.id); definitions.Add(CreateOrUpdateDefinition(entry)); } DeleteOrphanDefinitions(rosterIds); var database = AssetDatabase.LoadAssetAtPath(DatabasePath); if (database == null) { database = ScriptableObject.CreateInstance(); AssetDatabase.CreateAsset(database, DatabasePath); } database.EditorSetSkills(definitions); EditorUtility.SetDirty(database); AssetDatabase.SaveAssets(); WireDialogueTestScene(database); AssetDatabase.SaveAssets(); Debug.Log($"Skill system setup complete. Database at {DatabasePath}; scene wired at {ScenePath}."); } static bool TryLoadRoster(out SkillBibleEntry[] roster) { roster = null; var text = AssetDatabase.LoadAssetAtPath(BiblePath); if (text == null) { Debug.LogError($"[SkillSystemSetup] Missing bible at {BiblePath}."); return false; } SkillBibleRoot root; try { root = JsonUtility.FromJson(text.text); } catch (Exception ex) { Debug.LogError($"[SkillSystemSetup] Failed to parse {BiblePath}: {ex.Message}"); return false; } if (root?.skills == null || root.skills.Length == 0) { Debug.LogError($"[SkillSystemSetup] {BiblePath} has no skills."); return false; } foreach (var entry in root.skills) { if (entry == null || string.IsNullOrEmpty(entry.id)) { Debug.LogError($"[SkillSystemSetup] {BiblePath} contains a skill with empty id."); return false; } // Fail loudly on enum typos — silent defaults are content bugs. if (!Enum.TryParse(entry.axis, ignoreCase: true, out _)) { Debug.LogError($"[SkillSystemSetup] Skill '{entry.id}': invalid axis '{entry.axis}'."); return false; } if (!Enum.TryParse(entry.primaryChannel, ignoreCase: true, out _)) { Debug.LogError($"[SkillSystemSetup] Skill '{entry.id}': invalid primaryChannel '{entry.primaryChannel}'."); return false; } var secondary = string.IsNullOrEmpty(entry.secondaryChannel) ? "None" : entry.secondaryChannel; if (!Enum.TryParse(secondary, ignoreCase: true, out _)) { Debug.LogError($"[SkillSystemSetup] Skill '{entry.id}': invalid secondaryChannel '{entry.secondaryChannel}'."); return false; } if (!Enum.TryParse(entry.clueYield, ignoreCase: true, out _)) { Debug.LogError($"[SkillSystemSetup] Skill '{entry.id}': invalid clueYield '{entry.clueYield}'."); return false; } } roster = root.skills; return true; } static SkillDefinition CreateOrUpdateDefinition(SkillBibleEntry entry) { var path = $"{DefinitionsFolder}/{entry.id}.asset"; var asset = AssetDatabase.LoadAssetAtPath(path); if (asset == null) { asset = ScriptableObject.CreateInstance(); AssetDatabase.CreateAsset(asset, path); } asset.EditorSet(entry); EditorUtility.SetDirty(asset); return asset; } static void DeleteOrphanDefinitions(HashSet rosterIds) { if (!AssetDatabase.IsValidFolder(DefinitionsFolder)) { return; } var guids = AssetDatabase.FindAssets("t:SkillDefinition", new[] { DefinitionsFolder }); foreach (var guid in guids) { var path = AssetDatabase.GUIDToAssetPath(guid); var fileName = Path.GetFileNameWithoutExtension(path); if (rosterIds.Contains(fileName)) { continue; } Debug.Log($"[SkillSystemSetup] Deleting orphan definition {path}"); AssetDatabase.DeleteAsset(path); } } static void WireDialogueTestScene(SkillDatabase database) { if (!File.Exists(ScenePath)) { Debug.LogError($"Scene not found at {ScenePath}. Run Tools/Nightclub Arcadia/Set Up Yarn Dialogue Test first."); return; } var scene = EditorSceneManager.OpenScene(ScenePath, OpenSceneMode.Single); var runner = UnityEngine.Object.FindAnyObjectByType(); if (runner == null) { Debug.LogError("No DialogueRunner in DialogueTest.unity."); return; } var skillGo = GameObject.Find("Skill System"); if (skillGo == null) { skillGo = new GameObject("Skill System"); } var runtime = skillGo.GetComponent() ?? skillGo.AddComponent(); var commentary = skillGo.GetComponent() ?? skillGo.AddComponent(); var idle = skillGo.GetComponent() ?? skillGo.AddComponent(); var banner = EnsureBanner(runner.transform); var view = banner.GetComponent() ?? banner.AddComponent(); var runtimeSo = new SerializedObject(runtime); runtimeSo.FindProperty("database").objectReferenceValue = database; runtimeSo.FindProperty("dialogueRunner").objectReferenceValue = runner; runtimeSo.FindProperty("resultView").objectReferenceValue = view; runtimeSo.FindProperty("commentary").objectReferenceValue = commentary; runtimeSo.FindProperty("debugContextId").stringValue = "Entrance_Queue"; runtimeSo.ApplyModifiedPropertiesWithoutUndo(); var commentarySo = new SerializedObject(commentary); commentarySo.FindProperty("dialogueRunner").objectReferenceValue = runner; commentarySo.FindProperty("database").objectReferenceValue = database; commentarySo.ApplyModifiedPropertiesWithoutUndo(); var idleSo = new SerializedObject(idle); idleSo.FindProperty("dialogueRunner").objectReferenceValue = runner; idleSo.FindProperty("contextId").stringValue = "Entrance_Queue"; idleSo.ApplyModifiedPropertiesWithoutUndo(); var group = banner.GetComponent(); var label = banner.GetComponentInChildren(true); var viewSo = new SerializedObject(view); viewSo.FindProperty("group").objectReferenceValue = group; viewSo.FindProperty("label").objectReferenceValue = label; viewSo.ApplyModifiedPropertiesWithoutUndo(); EditorSceneManager.MarkSceneDirty(scene); EditorSceneManager.SaveScene(scene); } static GameObject EnsureBanner(Transform dialogueRoot) { var canvas = dialogueRoot.GetComponentInChildren(true); if (canvas == null) { var canvasGo = new GameObject("Skill Check Canvas", typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster)); canvas = canvasGo.GetComponent(); canvas.renderMode = RenderMode.ScreenSpaceOverlay; canvasGo.transform.SetParent(dialogueRoot, false); } var existing = canvas.transform.Find("Skill Check Banner"); if (existing != null) { return existing.gameObject; } var banner = new GameObject("Skill Check Banner", typeof(RectTransform), typeof(CanvasGroup), typeof(Image)); banner.transform.SetParent(canvas.transform, false); var rt = banner.GetComponent(); rt.anchorMin = new Vector2(0.1f, 0.82f); rt.anchorMax = new Vector2(0.9f, 0.95f); rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero; var image = banner.GetComponent(); image.color = new Color(0f, 0f, 0f, 0.75f); var group = banner.GetComponent(); group.alpha = 0f; group.blocksRaycasts = false; group.interactable = false; var labelGo = new GameObject("Label", typeof(RectTransform)); labelGo.transform.SetParent(banner.transform, false); var labelRt = labelGo.GetComponent(); labelRt.anchorMin = Vector2.zero; labelRt.anchorMax = Vector2.one; labelRt.offsetMin = new Vector2(16f, 8f); labelRt.offsetMax = new Vector2(-16f, -8f); var tmp = labelGo.AddComponent(); tmp.text = string.Empty; tmp.alignment = TextAlignmentOptions.Center; tmp.fontSize = 28f; tmp.textWrappingMode = TextWrappingModes.Normal; tmp.color = Color.white; return banner; } static void EnsureFolder(string parent, string child) { var path = $"{parent}/{child}"; if (!AssetDatabase.IsValidFolder(path)) { AssetDatabase.CreateFolder(parent, child); } } }