refactor the skill system to adhere to the lore handbook
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using NightclubArcadia.Skills;
|
||||
@@ -10,43 +11,21 @@ using UnityEngine.UI;
|
||||
using Yarn.Unity;
|
||||
|
||||
/// <summary>
|
||||
/// Creates SkillDefinition assets, SkillDatabase, and wires SkillRuntime + roll
|
||||
/// banner + commentary into Assets/Scenes/DialogueTest.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.
|
||||
/// </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";
|
||||
|
||||
struct SkillSpec
|
||||
{
|
||||
public string Id;
|
||||
public string DisplayName;
|
||||
public SkillDomain Domain;
|
||||
public Color Accent;
|
||||
public int StartingRank;
|
||||
}
|
||||
|
||||
static readonly SkillSpec[] Roster =
|
||||
{
|
||||
new SkillSpec { Id = "deduction", DisplayName = "Deduction", Domain = SkillDomain.Reason, Accent = new Color(0.55f, 0.75f, 1f), StartingRank = 1 },
|
||||
new SkillSpec { Id = "streetwise", DisplayName = "Streetwise", Domain = SkillDomain.Reason, Accent = new Color(0.7f, 0.85f, 0.55f), StartingRank = 1 },
|
||||
new SkillSpec { Id = "recall", DisplayName = "Recall", Domain = SkillDomain.Reason, Accent = new Color(0.65f, 0.7f, 1f), StartingRank = 1 },
|
||||
new SkillSpec { Id = "read_room", DisplayName = "Read the Room", Domain = SkillDomain.Instinct, Accent = new Color(1f, 0.75f, 0.45f), StartingRank = 1 },
|
||||
new SkillSpec { Id = "paranoia", DisplayName = "Paranoia", Domain = SkillDomain.Instinct, Accent = new Color(1f, 0.45f, 0.55f), StartingRank = 1 },
|
||||
new SkillSpec { Id = "reflexes", DisplayName = "Reflexes", Domain = SkillDomain.Instinct, Accent = new Color(1f, 0.9f, 0.4f), StartingRank = 1 },
|
||||
new SkillSpec { Id = "persuasion", DisplayName = "Persuasion", Domain = SkillDomain.Presence, Accent = new Color(0.85f, 0.65f, 1f), StartingRank = 1 },
|
||||
// Rank 2 so the Bouncer "Let me through" option gated by skill_rank >= 2 is visible in the demo.
|
||||
new SkillSpec { Id = "intimidation", DisplayName = "Intimidation", Domain = SkillDomain.Presence, Accent = new Color(1f, 0.4f, 0.4f), StartingRank = 2 },
|
||||
new SkillSpec { Id = "rapport", DisplayName = "Rapport", Domain = SkillDomain.Presence, Accent = new Color(0.55f, 0.95f, 0.85f), StartingRank = 1 },
|
||||
new SkillSpec { Id = "endurance", DisplayName = "Endurance", Domain = SkillDomain.Body, Accent = new Color(0.85f, 0.7f, 0.5f), StartingRank = 1 },
|
||||
new SkillSpec { Id = "sleight_of_hand", DisplayName = "Sleight of Hand", Domain = SkillDomain.Body, Accent = new Color(0.7f, 0.9f, 0.95f), StartingRank = 1 },
|
||||
new SkillSpec { Id = "nerve", DisplayName = "Nerve", Domain = SkillDomain.Body, Accent = new Color(0.95f, 0.6f, 0.35f), StartingRank = 1 },
|
||||
};
|
||||
|
||||
[DidReloadScripts]
|
||||
static void AutoSetupAfterReload()
|
||||
{
|
||||
@@ -80,15 +59,24 @@ public static class SkillSystemSetup
|
||||
/// </summary>
|
||||
static void EnsureSkillAssetsOnly()
|
||||
{
|
||||
if (!TryLoadRoster(out var roster))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureFolder("Assets", "Skills");
|
||||
EnsureFolder("Assets/Skills", "Definitions");
|
||||
|
||||
var definitions = new List<SkillDefinition>();
|
||||
foreach (var spec in Roster)
|
||||
var rosterIds = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var entry in roster)
|
||||
{
|
||||
definitions.Add(CreateOrUpdateDefinition(spec));
|
||||
rosterIds.Add(entry.id);
|
||||
definitions.Add(CreateOrUpdateDefinition(entry));
|
||||
}
|
||||
|
||||
DeleteOrphanDefinitions(rosterIds);
|
||||
|
||||
var database = AssetDatabase.LoadAssetAtPath<SkillDatabase>(DatabasePath);
|
||||
if (database == null)
|
||||
{
|
||||
@@ -105,15 +93,24 @@ public static class SkillSystemSetup
|
||||
[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>();
|
||||
foreach (var spec in Roster)
|
||||
var rosterIds = new HashSet<string>(StringComparer.Ordinal);
|
||||
foreach (var entry in roster)
|
||||
{
|
||||
definitions.Add(CreateOrUpdateDefinition(spec));
|
||||
rosterIds.Add(entry.id);
|
||||
definitions.Add(CreateOrUpdateDefinition(entry));
|
||||
}
|
||||
|
||||
DeleteOrphanDefinitions(rosterIds);
|
||||
|
||||
var database = AssetDatabase.LoadAssetAtPath<SkillDatabase>(DatabasePath);
|
||||
if (database == null)
|
||||
{
|
||||
@@ -130,9 +127,75 @@ public static class SkillSystemSetup
|
||||
Debug.Log($"Skill system setup complete. Database at {DatabasePath}; scene wired at {ScenePath}.");
|
||||
}
|
||||
|
||||
static SkillDefinition CreateOrUpdateDefinition(SkillSpec spec)
|
||||
static bool TryLoadRoster(out SkillBibleEntry[] roster)
|
||||
{
|
||||
var path = $"{DefinitionsFolder}/{spec.Id}.asset";
|
||||
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)
|
||||
{
|
||||
@@ -140,17 +203,33 @@ public static class SkillSystemSetup
|
||||
AssetDatabase.CreateAsset(asset, path);
|
||||
}
|
||||
|
||||
asset.EditorSet(
|
||||
spec.Id,
|
||||
spec.DisplayName,
|
||||
spec.Domain,
|
||||
$"Placeholder skill — replace when Skill_System_Draft.md is available.",
|
||||
spec.StartingRank,
|
||||
spec.Accent);
|
||||
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))
|
||||
@@ -161,7 +240,7 @@ public static class SkillSystemSetup
|
||||
|
||||
var scene = EditorSceneManager.OpenScene(ScenePath, OpenSceneMode.Single);
|
||||
|
||||
var runner = Object.FindAnyObjectByType<DialogueRunner>();
|
||||
var runner = UnityEngine.Object.FindAnyObjectByType<DialogueRunner>();
|
||||
if (runner == null)
|
||||
{
|
||||
Debug.LogError("No DialogueRunner in DialogueTest.unity.");
|
||||
|
||||
Reference in New Issue
Block a user