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