Files
nightclub-arcadia/NightclubArcadia/Assets/Editor/SkillSystemSetup.cs
T

353 lines
12 KiB
C#

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;
/// <summary>
/// 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.
/// </summary>
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();
};
}
/// <summary>
/// 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.
/// </summary>
static void EnsureSkillAssetsOnly()
{
if (!TryLoadRoster(out var roster))
{
return;
}
EnsureFolder("Assets", "Skills");
EnsureFolder("Assets/Skills", "Definitions");
var definitions = new List<SkillDefinition>();
var rosterIds = new HashSet<string>(StringComparer.Ordinal);
foreach (var entry in roster)
{
rosterIds.Add(entry.id);
definitions.Add(CreateOrUpdateDefinition(entry));
}
DeleteOrphanDefinitions(rosterIds);
var database = AssetDatabase.LoadAssetAtPath<SkillDatabase>(DatabasePath);
if (database == null)
{
database = ScriptableObject.CreateInstance<SkillDatabase>();
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<SkillDefinition>();
var rosterIds = new HashSet<string>(StringComparer.Ordinal);
foreach (var entry in roster)
{
rosterIds.Add(entry.id);
definitions.Add(CreateOrUpdateDefinition(entry));
}
DeleteOrphanDefinitions(rosterIds);
var database = AssetDatabase.LoadAssetAtPath<SkillDatabase>(DatabasePath);
if (database == null)
{
database = ScriptableObject.CreateInstance<SkillDatabase>();
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<TextAsset>(BiblePath);
if (text == null)
{
Debug.LogError($"[SkillSystemSetup] Missing bible at {BiblePath}.");
return false;
}
SkillBibleRoot root;
try
{
root = JsonUtility.FromJson<SkillBibleRoot>(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<SkillAxis>(entry.axis, ignoreCase: true, out _))
{
Debug.LogError($"[SkillSystemSetup] Skill '{entry.id}': invalid axis '{entry.axis}'.");
return false;
}
if (!Enum.TryParse<TraceChannel>(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<TraceChannel>(secondary, ignoreCase: true, out _))
{
Debug.LogError($"[SkillSystemSetup] Skill '{entry.id}': invalid secondaryChannel '{entry.secondaryChannel}'.");
return false;
}
if (!Enum.TryParse<ClueYield>(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<SkillDefinition>(path);
if (asset == null)
{
asset = ScriptableObject.CreateInstance<SkillDefinition>();
AssetDatabase.CreateAsset(asset, path);
}
asset.EditorSet(entry);
EditorUtility.SetDirty(asset);
return asset;
}
static void DeleteOrphanDefinitions(HashSet<string> 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<DialogueRunner>();
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<SkillRuntime>() ?? skillGo.AddComponent<SkillRuntime>();
var commentary = skillGo.GetComponent<CommentarySystem>() ?? skillGo.AddComponent<CommentarySystem>();
var idle = skillGo.GetComponent<CommentaryIdleTimer>() ?? skillGo.AddComponent<CommentaryIdleTimer>();
var banner = EnsureBanner(runner.transform);
var view = banner.GetComponent<SkillCheckResultView>() ?? banner.AddComponent<SkillCheckResultView>();
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<CanvasGroup>();
var label = banner.GetComponentInChildren<TMP_Text>(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<Canvas>(true);
if (canvas == null)
{
var canvasGo = new GameObject("Skill Check Canvas", typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster));
canvas = canvasGo.GetComponent<Canvas>();
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<RectTransform>();
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>();
image.color = new Color(0f, 0f, 0f, 0.75f);
var group = banner.GetComponent<CanvasGroup>();
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<RectTransform>();
labelRt.anchorMin = Vector2.zero;
labelRt.anchorMax = Vector2.one;
labelRt.offsetMin = new Vector2(16f, 8f);
labelRt.offsetMax = new Vector2(-16f, -8f);
var tmp = labelGo.AddComponent<TextMeshProUGUI>();
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);
}
}
}