diff --git a/NightclubArcadia/Assets/Dialogue/Characters/Bouncer.yarn b/NightclubArcadia/Assets/Dialogue/Characters/Bouncer.yarn index 4dc4965..90e3193 100644 --- a/NightclubArcadia/Assets/Dialogue/Characters/Bouncer.yarn +++ b/NightclubArcadia/Assets/Dialogue/Characters/Bouncer.yarn @@ -14,9 +14,29 @@ Bouncer: Evening. Name on the list? You: Depends who's asking. -> I'm on the list. <> - Bouncer: ...you're not on the list. - Bouncer: But it's slow tonight. Go on. - <> + <> + <> + Bouncer: ...you're not on the list. + Bouncer: But it's slow tonight. Go on. + <> + <> + Bouncer: Nobody's on the list tonight. Especially not you. + <> + <> +-> Let me through. <= 2>> + <> + <> + Bouncer: ...yeah. Yeah, okay. + <> + <> + <> + Bouncer: Fine. Don't make it a thing. + <> + <> + Bouncer: Try that again and you're talking to the pavement. + <> + <> + <> -> Actually, I'll head home. Bouncer: Smart choice. <> diff --git a/NightclubArcadia/Assets/Dialogue/Commentary.yarn b/NightclubArcadia/Assets/Dialogue/Commentary.yarn new file mode 100644 index 0000000..2c21ea0 --- /dev/null +++ b/NightclubArcadia/Assets/Dialogue/Commentary.yarn @@ -0,0 +1,18 @@ +// Passive skill commentary. Fired by CommentarySystem, never <>ed to. +// One node per (context, skill) pair; if a skill has nothing to say about a +// context, simply omit the node and it won't be considered. + +title: Commentary_Entrance_Queue_paranoia +--- +Paranoia: The man three back hasn't looked at his phone once. Nobody waits like that. +=== + +title: Commentary_Entrance_Queue_read_room +--- +Read the Room: Everyone here is pretending they're not cold. +=== + +title: Commentary_Entrance_Queue_streetwise +--- +Streetwise: Door's been "slow tonight" for forty minutes. That's a price, not a queue. +=== diff --git a/NightclubArcadia/Assets/Dialogue/Commentary.yarn.meta b/NightclubArcadia/Assets/Dialogue/Commentary.yarn.meta new file mode 100644 index 0000000..d79394c --- /dev/null +++ b/NightclubArcadia/Assets/Dialogue/Commentary.yarn.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: 26e9b5dea1744e39a5fe59bb1bc20fa7 +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 11500000, guid: 94073015eacc34c1d8fc6786e43d60ca, type: 3} diff --git a/NightclubArcadia/Assets/Dialogue/Common.yarn b/NightclubArcadia/Assets/Dialogue/Common.yarn index 52db3dc..4bb5741 100644 --- a/NightclubArcadia/Assets/Dialogue/Common.yarn +++ b/NightclubArcadia/Assets/Dialogue/Common.yarn @@ -18,4 +18,13 @@ title: Declarations // Smart variable: derived, not stored. Can never drift out of sync with // $reputation, so there's nothing to remember to update. < 10>> + +// Written by <> (YarnSkillCommands). Never <> these by hand. +<> +<> +<> +<> +<> +<> +<> === diff --git a/NightclubArcadia/Assets/Dialogue/Scenes/Debug.yarn b/NightclubArcadia/Assets/Dialogue/Scenes/Debug.yarn new file mode 100644 index 0000000..cf206be --- /dev/null +++ b/NightclubArcadia/Assets/Dialogue/Scenes/Debug.yarn @@ -0,0 +1,22 @@ +// Scratch node for verifying the skill-check plumbing. Not part of the story. +// Point DialogueRunner.startNode at "Debug_SkillCheck" to run it, then set it +// back to "Start" when you're done. + +title: Debug_SkillCheck +--- +Narrator: Skill check harness. +<> +<> + Narrator: Passed. Rolled {$check_roll}, total {$check_total} against DC {$check_dc}. +<> + Narrator: Failed. Rolled {$check_roll}, total {$check_total} against DC {$check_dc}. +<> + +// Explicit numeric DC plus a situational modifier. +<> +Narrator: Deduction came out {$check_degree}. + +// Deliberate typo: should log an error and continue, not deadlock. +<> +Narrator: Survived the bad skill id. +=== diff --git a/NightclubArcadia/Assets/Dialogue/Scenes/Debug.yarn.meta b/NightclubArcadia/Assets/Dialogue/Scenes/Debug.yarn.meta new file mode 100644 index 0000000..11a1b41 --- /dev/null +++ b/NightclubArcadia/Assets/Dialogue/Scenes/Debug.yarn.meta @@ -0,0 +1,10 @@ +fileFormatVersion: 2 +guid: e497c84fe7bd48ea8c828ac1702f79bf +ScriptedImporter: + internalIDToNameTable: [] + externalObjects: {} + serializedVersion: 2 + userData: + assetBundleName: + assetBundleVariant: + script: {fileID: 11500000, guid: 94073015eacc34c1d8fc6786e43d60ca, type: 3} diff --git a/NightclubArcadia/Assets/Editor/SkillSystemSetup.cs b/NightclubArcadia/Assets/Editor/SkillSystemSetup.cs new file mode 100644 index 0000000..648217f --- /dev/null +++ b/NightclubArcadia/Assets/Editor/SkillSystemSetup.cs @@ -0,0 +1,273 @@ +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; + +/// +/// Creates SkillDefinition assets, SkillDatabase, and wires SkillRuntime + roll +/// banner + commentary into Assets/Scenes/DialogueTest.unity. +/// +public static class SkillSystemSetup +{ + const string DefinitionsFolder = "Assets/Skills/Definitions"; + const string SkillsFolder = "Assets/Skills"; + 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() + { + 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() + { + EnsureFolder("Assets", "Skills"); + EnsureFolder("Assets/Skills", "Definitions"); + + var definitions = new List(); + foreach (var spec in Roster) + { + definitions.Add(CreateOrUpdateDefinition(spec)); + } + + 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() + { + EnsureFolder("Assets", "Skills"); + EnsureFolder("Assets/Skills", "Definitions"); + + var definitions = new List(); + foreach (var spec in Roster) + { + definitions.Add(CreateOrUpdateDefinition(spec)); + } + + 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 SkillDefinition CreateOrUpdateDefinition(SkillSpec spec) + { + var path = $"{DefinitionsFolder}/{spec.Id}.asset"; + var asset = AssetDatabase.LoadAssetAtPath(path); + if (asset == null) + { + asset = ScriptableObject.CreateInstance(); + 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); + EditorUtility.SetDirty(asset); + return asset; + } + + 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 = 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); + } + } +} diff --git a/NightclubArcadia/Assets/Editor/SkillSystemSetup.cs.meta b/NightclubArcadia/Assets/Editor/SkillSystemSetup.cs.meta new file mode 100644 index 0000000..7a461a6 --- /dev/null +++ b/NightclubArcadia/Assets/Editor/SkillSystemSetup.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 231654cf350a49ce9920bc4f24a1f002 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Editor/SkillSystemTestRunner.cs b/NightclubArcadia/Assets/Editor/SkillSystemTestRunner.cs new file mode 100644 index 0000000..eb2d8e1 --- /dev/null +++ b/NightclubArcadia/Assets/Editor/SkillSystemTestRunner.cs @@ -0,0 +1,48 @@ +using UnityEditor; +using UnityEditor.TestTools.TestRunner.Api; +using UnityEngine; + +/// +/// One-click EditMode test run for the skill system. +/// +public static class SkillSystemTestRunner +{ + [MenuItem("Tools/Nightclub Arcadia/Run Skill EditMode Tests")] + public static void RunEditModeTests() + { + var api = ScriptableObject.CreateInstance(); + var filter = new Filter + { + testMode = TestMode.EditMode, + assemblyNames = new[] { "NightclubArcadia.Skills.Tests" }, + }; + + api.RegisterCallbacks(new Callbacks()); + api.Execute(new ExecutionSettings(filter)); + Debug.Log("[SkillSystemTestRunner] EditMode tests started — watch the Console / Test Runner."); + } + + sealed class Callbacks : ICallbacks + { + public void RunStarted(ITestAdaptor testsToRun) { } + + public void RunFinished(ITestResultAdaptor result) + { + Debug.Log($"[SkillSystemTestRunner] Finished. Pass:{result.PassCount} Fail:{result.FailCount} Skip:{result.SkipCount}"); + if (result.FailCount > 0) + { + Debug.LogError("[SkillSystemTestRunner] One or more skill EditMode tests failed."); + } + } + + public void TestStarted(ITestAdaptor test) { } + + public void TestFinished(ITestResultAdaptor result) + { + if (result.TestStatus == TestStatus.Failed) + { + Debug.LogError($"[SkillSystemTestRunner] FAIL {result.FullName}\n{result.Message}\n{result.StackTrace}"); + } + } + } +} diff --git a/NightclubArcadia/Assets/Editor/SkillSystemTestRunner.cs.meta b/NightclubArcadia/Assets/Editor/SkillSystemTestRunner.cs.meta new file mode 100644 index 0000000..044d37c --- /dev/null +++ b/NightclubArcadia/Assets/Editor/SkillSystemTestRunner.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7c8e1a2b3d4f4e6a9b0c1d2e3f4a5b6c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Editor/YarnDemoSetup.cs b/NightclubArcadia/Assets/Editor/YarnDemoSetup.cs index c2b3c91..8e673d9 100644 --- a/NightclubArcadia/Assets/Editor/YarnDemoSetup.cs +++ b/NightclubArcadia/Assets/Editor/YarnDemoSetup.cs @@ -56,7 +56,7 @@ public static class YarnDemoSetup EditorUtility.SetDirty(runner); #if UNITY_2023_1_OR_NEWER - var eventSystems = Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None); + var eventSystems = Object.FindObjectsByType(FindObjectsInactive.Include); #else var eventSystems = Object.FindObjectsOfType(true); #endif diff --git a/NightclubArcadia/Assets/Scenes/DialogueTest.unity b/NightclubArcadia/Assets/Scenes/DialogueTest.unity index c33696c..94f5b16 100644 --- a/NightclubArcadia/Assets/Scenes/DialogueTest.unity +++ b/NightclubArcadia/Assets/Scenes/DialogueTest.unity @@ -562,6 +562,446 @@ MonoBehaviour: nextLineKeyCode: 27 hurryUpOptionsKeyCode: 32 cancelDialogueKeyCode: 0 +--- !u!114 &910000019 stripped +MonoBehaviour: + m_CorrespondingSourceObject: {fileID: 8309902628685535410, guid: 52571f68872914e24837210513edea1d, type: 3} + m_PrefabInstance: {fileID: 1720996731} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4bec29c0a230741bdac901dba8da47ee, type: 3} + m_Name: + m_EditorClassIdentifier: YarnSpinner.Unity::Yarn.Unity.DialogueRunner +--- !u!1 &910000001 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 910000002} + - component: {fileID: 910000003} + - component: {fileID: 910000004} + - component: {fileID: 910000005} + m_Layer: 0 + m_Name: Skill System + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &910000002 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 910000001} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 910000007} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &910000003 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 910000001} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: cb04a8b1bce645fba398b22bbb4db58a, type: 3} + m_Name: + m_EditorClassIdentifier: NightclubArcadia.Skills::NightclubArcadia.Skills.SkillRuntime + database: {fileID: 11400000, guid: d15ae35514b14ded9b398d630e8ef373, type: 2} + dialogueRunner: {fileID: 910000019} + resultView: {fileID: 910000015} + commentary: {fileID: 910000004} + rollSeed: 0 + debugContextId: Entrance_Queue +--- !u!114 &910000004 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 910000001} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 6626d81593b24235b36ae6c168aef87f, type: 3} + m_Name: + m_EditorClassIdentifier: NightclubArcadia.Skills::NightclubArcadia.Skills.CommentarySystem + dialogueRunner: {fileID: 910000019} + database: {fileID: 11400000, guid: d15ae35514b14ded9b398d630e8ef373, type: 2} + minSecondsBetween: 20 + minRankToSpeak: 1 + recencyWindow: 3 +--- !u!114 &910000005 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 910000001} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 0d65653213924ceaa756c6b6e65b522c, type: 3} + m_Name: + m_EditorClassIdentifier: NightclubArcadia.Skills::NightclubArcadia.Skills.CommentaryIdleTimer + dialogueRunner: {fileID: 910000019} + contextId: Entrance_Queue + idleSeconds: 45 +--- !u!1 &910000006 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 910000007} + - component: {fileID: 910000008} + - component: {fileID: 910000009} + - component: {fileID: 910000010} + m_Layer: 5 + m_Name: Skill Check Canvas + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &910000007 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 910000006} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 0, y: 0, z: 0} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 910000012} + m_Father: {fileID: 910000002} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0, y: 0} +--- !u!223 &910000008 +Canvas: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 910000006} + m_Enabled: 1 + serializedVersion: 3 + m_RenderMode: 0 + m_Camera: {fileID: 0} + m_PlaneDistance: 100 + m_PixelPerfect: 0 + m_ReceivesEvents: 1 + m_OverrideSorting: 0 + m_OverridePixelPerfect: 0 + m_SortingBucketNormalizedSize: 0 + m_VertexColorAlwaysGammaSpace: 0 + m_AdditionalShaderChannelsFlag: 25 + m_UpdateRectTransformForStandalone: 0 + m_SortingLayerID: 0 + m_SortingOrder: 100 + m_TargetDisplay: 0 +--- !u!114 &910000009 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 910000006} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 0cd44c1031e13a943bb63640046fad76, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.CanvasScaler + m_UiScaleMode: 1 + m_ReferencePixelsPerUnit: 100 + m_ScaleFactor: 1 + m_ReferenceResolution: {x: 1920, y: 1080} + m_ScreenMatchMode: 0 + m_MatchWidthOrHeight: 0.5 + m_PhysicalUnit: 3 + m_FallbackScreenDPI: 96 + m_DefaultSpriteDPI: 96 + m_DynamicPixelsPerUnit: 1 + m_PresetInfoIsWorld: 0 +--- !u!114 &910000010 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 910000006} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: dc42784cf147c0c48a680349fa168899, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.GraphicRaycaster + m_IgnoreReversedGraphics: 1 + m_BlockingObjects: 0 + m_BlockingMask: + serializedVersion: 2 + m_Bits: 4294967295 +--- !u!1 &910000011 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 910000012} + - component: {fileID: 910000020} + - component: {fileID: 910000013} + - component: {fileID: 910000014} + - component: {fileID: 910000015} + m_Layer: 5 + m_Name: Skill Check Banner + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &910000012 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 910000011} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 910000017} + m_Father: {fileID: 910000007} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.1, y: 0.82} + m_AnchorMax: {x: 0.9, y: 0.95} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &910000020 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 910000011} + m_CullTransparentMesh: 1 +--- !u!225 &910000013 +CanvasGroup: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 910000011} + m_Enabled: 1 + m_Alpha: 0 + m_Interactable: 0 + m_BlocksRaycasts: 0 + m_IgnoreParentGroups: 0 +--- !u!114 &910000014 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 910000011} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image + m_Material: {fileID: 0} + m_Color: {r: 0, g: 0, b: 0, a: 0.75} + m_RaycastTarget: 0 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 0} + m_Type: 0 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!114 &910000015 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 910000011} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: af3c81416f5f4a869baa347fd9dee9e3, type: 3} + m_Name: + m_EditorClassIdentifier: NightclubArcadia.Skills::NightclubArcadia.Skills.SkillCheckResultView + group: {fileID: 910000013} + label: {fileID: 910000018} + holdSeconds: 1.6 + fadeSeconds: 0.15 +--- !u!1 &910000016 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 910000017} + - component: {fileID: 910000021} + - component: {fileID: 910000018} + m_Layer: 5 + m_Name: Label + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &910000017 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 910000016} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 910000012} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: -32, y: -16} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!222 &910000021 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 910000016} + m_CullTransparentMesh: 1 +--- !u!114 &910000018 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 910000016} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} + m_Name: + m_EditorClassIdentifier: Unity.TextMeshPro::TMPro.TextMeshProUGUI + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 0 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_text: + m_isRightToLeft: 0 + m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} + m_sharedMaterial: {fileID: 0} + m_fontSharedMaterials: [] + m_fontMaterial: {fileID: 0} + m_fontMaterials: [] + m_fontColor32: + serializedVersion: 2 + rgba: 4294967295 + m_fontColor: {r: 1, g: 1, b: 1, a: 1} + m_enableVertexGradient: 0 + m_colorMode: 3 + m_fontColorGradient: + topLeft: {r: 1, g: 1, b: 1, a: 1} + topRight: {r: 1, g: 1, b: 1, a: 1} + bottomLeft: {r: 1, g: 1, b: 1, a: 1} + bottomRight: {r: 1, g: 1, b: 1, a: 1} + m_fontColorGradientPreset: {fileID: 0} + m_spriteAsset: {fileID: 0} + m_tintAllSprites: 0 + m_StyleSheet: {fileID: 0} + m_TextStyleHashCode: -1183493901 + m_overrideHtmlColors: 0 + m_faceColor: + serializedVersion: 2 + rgba: 4294967295 + m_fontSize: 28 + m_fontSizeBase: 28 + m_fontWeight: 400 + m_enableAutoSizing: 0 + m_fontSizeMin: 18 + m_fontSizeMax: 72 + m_fontStyle: 0 + m_HorizontalAlignment: 2 + m_VerticalAlignment: 512 + m_textAlignment: 65535 + m_characterSpacing: 0 + m_wordSpacing: 0 + m_lineSpacing: 0 + m_lineSpacingMax: 0 + m_paragraphSpacing: 0 + m_charWidthMaxAdj: 0 + m_TextWrappingMode: 1 + m_wordWrappingRatios: 0.4 + m_overflowMode: 0 + m_linkedTextComponent: {fileID: 0} + parentLinkedComponent: {fileID: 0} + m_enableKerning: 0 + m_ActiveFontFeatures: 6e72656b + m_enableExtraPadding: 0 + checkPaddingRequired: 0 + m_isRichText: 1 + m_EmojiFallbackSupport: 1 + m_parseCtrlCharacters: 1 + m_isOrthographic: 1 + m_isCullingEnabled: 0 + m_horizontalMapping: 0 + m_verticalMapping: 0 + m_uvLineOffset: 0 + m_geometrySortingOrder: 0 + m_IsTextObjectScaleStatic: 0 + m_VertexBufferAutoSizeReduction: 0 + m_useMaxVisibleDescender: 1 + m_pageToDisplay: 1 + m_margin: {x: 0, y: 0, z: 0, w: 0} + m_isUsingLegacyAnimationComponent: 0 + m_isVolumetricText: 0 + m_hasFontAssetChanged: 0 + m_baseMaterial: {fileID: 0} + m_maskOffset: {x: 0, y: 0, z: 0, w: 0} --- !u!1660057539 &9223372036854775807 SceneRoots: m_ObjectHideFlags: 0 @@ -569,3 +1009,4 @@ SceneRoots: - {fileID: 214568766} - {fileID: 1516720415} - {fileID: 1720996731} + - {fileID: 910000002} diff --git a/NightclubArcadia/Assets/Scripts.meta b/NightclubArcadia/Assets/Scripts.meta new file mode 100644 index 0000000..69d687c --- /dev/null +++ b/NightclubArcadia/Assets/Scripts.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 13f14dfeaec742b38567bb2e4c1ba440 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Scripts/Skills.meta b/NightclubArcadia/Assets/Scripts/Skills.meta new file mode 100644 index 0000000..1d1c91b --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 26f1b62937714176b821c2ee871f6cd3 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Scripts/Skills/Commentary.meta b/NightclubArcadia/Assets/Scripts/Skills/Commentary.meta new file mode 100644 index 0000000..d668fee --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/Commentary.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d9e3484419734ba89ff5bcd5a991f8e2 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Scripts/Skills/Commentary/CommentaryIdleTimer.cs b/NightclubArcadia/Assets/Scripts/Skills/Commentary/CommentaryIdleTimer.cs new file mode 100644 index 0000000..a2cda29 --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/Commentary/CommentaryIdleTimer.cs @@ -0,0 +1,44 @@ +using UnityEngine; +using Yarn.Unity; + +namespace NightclubArcadia.Skills +{ + public sealed class CommentaryIdleTimer : MonoBehaviour + { + [SerializeField] DialogueRunner dialogueRunner; + [SerializeField] string contextId = "Entrance_Queue"; + [SerializeField] float idleSeconds = 45f; + + float _idleAccum; + + void Update() + { + if (string.IsNullOrEmpty(contextId)) + { + return; + } + + if (dialogueRunner != null && dialogueRunner.IsDialogueRunning) + { + _idleAccum = 0f; + return; + } + + _idleAccum += Time.deltaTime; + if (_idleAccum < idleSeconds) + { + return; + } + + _idleAccum = 0f; + + var runtime = SkillRuntime.Instance; + if (runtime == null || runtime.Commentary == null) + { + return; + } + + runtime.Commentary.RequestCommentary(contextId, repeatable: true); + } + } +} diff --git a/NightclubArcadia/Assets/Scripts/Skills/Commentary/CommentaryIdleTimer.cs.meta b/NightclubArcadia/Assets/Scripts/Skills/Commentary/CommentaryIdleTimer.cs.meta new file mode 100644 index 0000000..69f8f7d --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/Commentary/CommentaryIdleTimer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0d65653213924ceaa756c6b6e65b522c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Scripts/Skills/Commentary/CommentarySystem.cs b/NightclubArcadia/Assets/Scripts/Skills/Commentary/CommentarySystem.cs new file mode 100644 index 0000000..da3839f --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/Commentary/CommentarySystem.cs @@ -0,0 +1,194 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using Yarn.Unity; + +namespace NightclubArcadia.Skills +{ + /// + /// Passive skill commentary outside the Yarn node tree. + /// Fires at most one speaker per RequestCommentary call. + /// + public sealed class CommentarySystem : MonoBehaviour + { + [SerializeField] DialogueRunner dialogueRunner; + [SerializeField] SkillDatabase database; + [SerializeField] float minSecondsBetween = 20f; + [SerializeField] int minRankToSpeak = 1; + [SerializeField] int recencyWindow = 3; + + readonly List _recent = new List(); + readonly HashSet _firedContexts = new HashSet(StringComparer.Ordinal); + float _lastFiredAt = float.NegativeInfinity; + + public void RequestCommentary(string contextId) + { + RequestCommentary(contextId, repeatable: false); + } + + public void RequestCommentary(string contextId, bool repeatable) + { + if (string.IsNullOrEmpty(contextId)) + { + return; + } + + if (dialogueRunner == null || database == null) + { + Debug.LogWarning($"[{nameof(CommentarySystem)}] Missing DialogueRunner or SkillDatabase.", this); + return; + } + + if (dialogueRunner.IsDialogueRunning) + { + return; + } + + if (Time.time - _lastFiredAt < minSecondsBetween) + { + return; + } + + if (!repeatable && _firedContexts.Contains(contextId)) + { + return; + } + + var runtime = SkillRuntime.Instance; + if (runtime == null || runtime.Skills == null || runtime.Rolls == null) + { + Debug.LogWarning($"[{nameof(CommentarySystem)}] SkillRuntime is not ready.", this); + return; + } + + var candidates = new List(); + foreach (var skill in database.All) + { + if (skill == null || string.IsNullOrEmpty(skill.Id)) + { + continue; + } + + if (runtime.Skills.GetRank(skill.Id) < minRankToSpeak) + { + continue; + } + + var nodeName = $"Commentary_{contextId}_{skill.Id}"; + if (!NodeExists(nodeName)) + { + continue; + } + + candidates.Add(skill); + } + + var picked = Pick(candidates, runtime.Skills, _recent, recencyWindow, runtime.Rolls); + if (picked == null) + { + return; + } + + var startNode = $"Commentary_{contextId}_{picked.Id}"; + _ = dialogueRunner.StartDialogue(startNode); + _lastFiredAt = Time.time; + _firedContexts.Add(contextId); + _recent.Add(picked.Id); + while (_recent.Count > Math.Max(1, recencyWindow)) + { + _recent.RemoveAt(0); + } + } + + bool NodeExists(string nodeName) + { + if (dialogueRunner.Dialogue != null && dialogueRunner.Dialogue.NodeExists(nodeName)) + { + return true; + } + + var project = dialogueRunner.YarnProject; + if (project != null) + { + foreach (var name in project.NodeNames) + { + if (string.Equals(name, nodeName, StringComparison.Ordinal)) + { + return true; + } + } + } + + return false; + } + + /// + /// Weighted pick: weight = max(1, rank); recent speakers (within recencyWindow) have weight halved. + /// + public static SkillDefinition Pick( + IReadOnlyList candidates, + PlayerSkills skills, + IReadOnlyList recent, + int recencyWindowSize, + IRollSource rolls) + { + if (candidates == null || candidates.Count == 0 || skills == null || rolls == null) + { + return null; + } + + var weights = new int[candidates.Count]; + var total = 0; + for (var i = 0; i < candidates.Count; i++) + { + var skill = candidates[i]; + var weight = Math.Max(1, skills.GetRank(skill.Id)); + if (WasRecent(skill.Id, recent, recencyWindowSize)) + { + weight = Math.Max(1, weight / 2); + } + + weights[i] = weight; + total += weight; + } + + if (total <= 0) + { + return null; + } + + // IRollSource returns 1..sides; map onto [0, total). + var pick = rolls.Roll(total) - 1; + var cumulative = 0; + for (var i = 0; i < candidates.Count; i++) + { + cumulative += weights[i]; + if (pick < cumulative) + { + return candidates[i]; + } + } + + return candidates[candidates.Count - 1]; + } + + static bool WasRecent(string skillId, IReadOnlyList recent, int window) + { + if (recent == null || recent.Count == 0 || window <= 0) + { + return false; + } + + var start = Math.Max(0, recent.Count - window); + for (var i = start; i < recent.Count; i++) + { + if (string.Equals(recent[i], skillId, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + } +} diff --git a/NightclubArcadia/Assets/Scripts/Skills/Commentary/CommentarySystem.cs.meta b/NightclubArcadia/Assets/Scripts/Skills/Commentary/CommentarySystem.cs.meta new file mode 100644 index 0000000..0ee59ba --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/Commentary/CommentarySystem.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6626d81593b24235b36ae6c168aef87f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Scripts/Skills/Commentary/CommentaryTrigger.cs b/NightclubArcadia/Assets/Scripts/Skills/Commentary/CommentaryTrigger.cs new file mode 100644 index 0000000..62ac6eb --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/Commentary/CommentaryTrigger.cs @@ -0,0 +1,33 @@ +using UnityEngine; + +namespace NightclubArcadia.Skills +{ + [RequireComponent(typeof(Collider))] + public sealed class CommentaryTrigger : MonoBehaviour + { + [SerializeField] string contextId; + [SerializeField] bool repeatable; + [SerializeField] string playerTag = "Player"; + + void OnTriggerEnter(Collider other) + { + if (string.IsNullOrEmpty(contextId)) + { + return; + } + + if (!string.IsNullOrEmpty(playerTag) && !other.CompareTag(playerTag)) + { + return; + } + + var runtime = SkillRuntime.Instance; + if (runtime == null || runtime.Commentary == null) + { + return; + } + + runtime.Commentary.RequestCommentary(contextId, repeatable); + } + } +} diff --git a/NightclubArcadia/Assets/Scripts/Skills/Commentary/CommentaryTrigger.cs.meta b/NightclubArcadia/Assets/Scripts/Skills/Commentary/CommentaryTrigger.cs.meta new file mode 100644 index 0000000..679ea7d --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/Commentary/CommentaryTrigger.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 04f12949da1741e5a591519698c9ae95 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Scripts/Skills/Data.meta b/NightclubArcadia/Assets/Scripts/Skills/Data.meta new file mode 100644 index 0000000..a6ed7ac --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/Data.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 249854c65e7742beaac1d2c969863d03 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Scripts/Skills/Data/DcBand.cs b/NightclubArcadia/Assets/Scripts/Skills/Data/DcBand.cs new file mode 100644 index 0000000..bd78f9d --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/Data/DcBand.cs @@ -0,0 +1,43 @@ +using System; +using System.Globalization; + +namespace NightclubArcadia.Skills +{ + public enum DcBand + { + Trivial = 5, + Easy = 8, + Standard = 11, + Hard = 14, + Formidable = 17, + Legendary = 20, + } + + public static class DcBands + { + public static bool TryParse(string input, out int dc) + { + dc = 0; + if (string.IsNullOrWhiteSpace(input)) + { + return false; + } + + input = input.Trim(); + + if (int.TryParse(input, NumberStyles.Integer, CultureInfo.InvariantCulture, out var numeric)) + { + dc = numeric; + return true; + } + + if (Enum.TryParse(input, ignoreCase: true, out var band)) + { + dc = (int)band; + return true; + } + + return false; + } + } +} diff --git a/NightclubArcadia/Assets/Scripts/Skills/Data/DcBand.cs.meta b/NightclubArcadia/Assets/Scripts/Skills/Data/DcBand.cs.meta new file mode 100644 index 0000000..acfcfe5 --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/Data/DcBand.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 04013b352063419eb6f0c8932a3cb116 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Scripts/Skills/Data/SkillCheckDegree.cs b/NightclubArcadia/Assets/Scripts/Skills/Data/SkillCheckDegree.cs new file mode 100644 index 0000000..a080507 --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/Data/SkillCheckDegree.cs @@ -0,0 +1,10 @@ +namespace NightclubArcadia.Skills +{ + public enum SkillCheckDegree + { + CriticalFailure, + Failure, + Success, + CriticalSuccess, + } +} diff --git a/NightclubArcadia/Assets/Scripts/Skills/Data/SkillCheckDegree.cs.meta b/NightclubArcadia/Assets/Scripts/Skills/Data/SkillCheckDegree.cs.meta new file mode 100644 index 0000000..a698e93 --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/Data/SkillCheckDegree.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a099dc0cf50346e98ea9795411a21f99 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Scripts/Skills/Data/SkillCheckResult.cs b/NightclubArcadia/Assets/Scripts/Skills/Data/SkillCheckResult.cs new file mode 100644 index 0000000..2a2fa07 --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/Data/SkillCheckResult.cs @@ -0,0 +1,40 @@ +namespace NightclubArcadia.Skills +{ + public readonly struct SkillCheckResult + { + public SkillCheckResult( + string skillId, + string skillDisplayName, + int roll, + int rankMod, + int situationalMod, + int stateMod, + int total, + int dc, + bool success, + SkillCheckDegree degree) + { + SkillId = skillId; + SkillDisplayName = skillDisplayName; + Roll = roll; + RankMod = rankMod; + SituationalMod = situationalMod; + StateMod = stateMod; + Total = total; + DC = dc; + Success = success; + Degree = degree; + } + + public string SkillId { get; } + public string SkillDisplayName { get; } + public int Roll { get; } + public int RankMod { get; } + public int SituationalMod { get; } + public int StateMod { get; } + public int Total { get; } + public int DC { get; } + public bool Success { get; } + public SkillCheckDegree Degree { get; } + } +} diff --git a/NightclubArcadia/Assets/Scripts/Skills/Data/SkillCheckResult.cs.meta b/NightclubArcadia/Assets/Scripts/Skills/Data/SkillCheckResult.cs.meta new file mode 100644 index 0000000..e0b33c5 --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/Data/SkillCheckResult.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2ee1160f9fa3439280ca256e5f23193f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Scripts/Skills/Data/SkillDatabase.cs b/NightclubArcadia/Assets/Scripts/Skills/Data/SkillDatabase.cs new file mode 100644 index 0000000..c274ad1 --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/Data/SkillDatabase.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace NightclubArcadia.Skills +{ + [CreateAssetMenu(menuName = "Nightclub Arcadia/Skill Database", fileName = "SkillDatabase")] + public sealed class SkillDatabase : ScriptableObject + { + [SerializeField] List skills = new List(); + + Dictionary _lookup; + + public IReadOnlyList All => skills; + + public bool TryGet(string skillId, out SkillDefinition def) + { + EnsureLookup(); + if (string.IsNullOrEmpty(skillId)) + { + def = null; + return false; + } + + return _lookup.TryGetValue(skillId, out def); + } + + void EnsureLookup() + { + if (_lookup != null) + { + return; + } + + _lookup = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (skills == null) + { + return; + } + + foreach (var skill in skills) + { + if (skill == null || string.IsNullOrEmpty(skill.Id)) + { + continue; + } + + _lookup[skill.Id] = skill; + } + } + + void OnValidate() + { + _lookup = null; + if (skills == null) + { + return; + } + + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var skill in skills) + { + if (skill == null) + { + Debug.LogError($"[{nameof(SkillDatabase)}] Null skill entry in {name}.", this); + continue; + } + + if (string.IsNullOrEmpty(skill.Id)) + { + Debug.LogError($"[{nameof(SkillDatabase)}] Skill '{skill.name}' has an empty Id.", this); + continue; + } + + if (!seen.Add(skill.Id)) + { + Debug.LogError($"[{nameof(SkillDatabase)}] Duplicate skill Id '{skill.Id}'.", this); + } + } + } + +#if UNITY_EDITOR + public void EditorSetSkills(List newSkills) + { + skills = newSkills ?? new List(); + _lookup = null; + } +#endif + } +} diff --git a/NightclubArcadia/Assets/Scripts/Skills/Data/SkillDatabase.cs.meta b/NightclubArcadia/Assets/Scripts/Skills/Data/SkillDatabase.cs.meta new file mode 100644 index 0000000..306fadc --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/Data/SkillDatabase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 73f6e7831a60457b918d303c16fc6cec +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Scripts/Skills/Data/SkillDefinition.cs b/NightclubArcadia/Assets/Scripts/Skills/Data/SkillDefinition.cs new file mode 100644 index 0000000..fbdc700 --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/Data/SkillDefinition.cs @@ -0,0 +1,40 @@ +using UnityEngine; + +namespace NightclubArcadia.Skills +{ + [CreateAssetMenu(menuName = "Nightclub Arcadia/Skill Definition", fileName = "NewSkill")] + public sealed class SkillDefinition : ScriptableObject + { + [SerializeField] string id; + [SerializeField] string displayName; + [SerializeField] SkillDomain domain; + [TextArea] [SerializeField] string description; + [SerializeField] int startingRank; + [SerializeField] Color accentColor = Color.white; + + public string Id => id; + public string DisplayName => displayName; + public SkillDomain Domain => domain; + public string Description => description; + public int StartingRank => startingRank; + public Color AccentColor => accentColor; + +#if UNITY_EDITOR + public void EditorSet( + string newId, + string newDisplayName, + SkillDomain newDomain, + string newDescription, + int newStartingRank, + Color newAccentColor) + { + id = newId; + displayName = newDisplayName; + domain = newDomain; + description = newDescription; + startingRank = newStartingRank; + accentColor = newAccentColor; + } +#endif + } +} diff --git a/NightclubArcadia/Assets/Scripts/Skills/Data/SkillDefinition.cs.meta b/NightclubArcadia/Assets/Scripts/Skills/Data/SkillDefinition.cs.meta new file mode 100644 index 0000000..0e65491 --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/Data/SkillDefinition.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9a26230b6f20436891aa772e8f858f3f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Scripts/Skills/Data/SkillDomain.cs b/NightclubArcadia/Assets/Scripts/Skills/Data/SkillDomain.cs new file mode 100644 index 0000000..3088f65 --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/Data/SkillDomain.cs @@ -0,0 +1,10 @@ +namespace NightclubArcadia.Skills +{ + public enum SkillDomain + { + Reason, + Instinct, + Presence, + Body, + } +} diff --git a/NightclubArcadia/Assets/Scripts/Skills/Data/SkillDomain.cs.meta b/NightclubArcadia/Assets/Scripts/Skills/Data/SkillDomain.cs.meta new file mode 100644 index 0000000..febe2f6 --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/Data/SkillDomain.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0bdf9a02be4b42a4a48a099fe924980e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Scripts/Skills/NightclubArcadia.Skills.asmdef b/NightclubArcadia/Assets/Scripts/Skills/NightclubArcadia.Skills.asmdef new file mode 100644 index 0000000..127e189 --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/NightclubArcadia.Skills.asmdef @@ -0,0 +1,17 @@ +{ + "name": "NightclubArcadia.Skills", + "rootNamespace": "NightclubArcadia.Skills", + "references": [ + "YarnSpinner.Unity", + "Unity.TextMeshPro" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/NightclubArcadia/Assets/Scripts/Skills/NightclubArcadia.Skills.asmdef.meta b/NightclubArcadia/Assets/Scripts/Skills/NightclubArcadia.Skills.asmdef.meta new file mode 100644 index 0000000..ba80406 --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/NightclubArcadia.Skills.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: ac01048d38154701b13372655db532b2 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Scripts/Skills/Runtime.meta b/NightclubArcadia/Assets/Scripts/Skills/Runtime.meta new file mode 100644 index 0000000..264f9bc --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: e76dc9c927344424a7f8a4acf54d7253 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Scripts/Skills/Runtime/IRollSource.cs b/NightclubArcadia/Assets/Scripts/Skills/Runtime/IRollSource.cs new file mode 100644 index 0000000..c5ed9c2 --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/Runtime/IRollSource.cs @@ -0,0 +1,8 @@ +namespace NightclubArcadia.Skills +{ + public interface IRollSource + { + /// Returns a value in [1, sides] inclusive. + int Roll(int sides); + } +} diff --git a/NightclubArcadia/Assets/Scripts/Skills/Runtime/IRollSource.cs.meta b/NightclubArcadia/Assets/Scripts/Skills/Runtime/IRollSource.cs.meta new file mode 100644 index 0000000..f977269 --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/Runtime/IRollSource.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 43de4384f09d4bd7b142d105f2c04007 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Scripts/Skills/Runtime/PlayerSkills.cs b/NightclubArcadia/Assets/Scripts/Skills/Runtime/PlayerSkills.cs new file mode 100644 index 0000000..35cac89 --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/Runtime/PlayerSkills.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; + +namespace NightclubArcadia.Skills +{ + /// + /// Pure rank state. No rolling, DC comparison, modifier math, UI, or Yarn. + /// + public sealed class PlayerSkills + { + readonly Dictionary _ranks = + new Dictionary(StringComparer.OrdinalIgnoreCase); + + public PlayerSkills(SkillDatabase db) + { + if (db == null) + { + throw new ArgumentNullException(nameof(db)); + } + + foreach (var skill in db.All) + { + if (skill == null || string.IsNullOrEmpty(skill.Id)) + { + continue; + } + + _ranks[skill.Id] = skill.StartingRank; + } + } + + public IReadOnlyDictionary AllRanks => _ranks; + + public int GetRank(string skillId) + { + if (string.IsNullOrEmpty(skillId)) + { + return 0; + } + + return _ranks.TryGetValue(skillId, out var rank) ? rank : 0; + } + + public void SetRank(string skillId, int rank) + { + if (string.IsNullOrEmpty(skillId)) + { + throw new ArgumentException("Skill id is required.", nameof(skillId)); + } + + _ranks[skillId] = rank; + } + } +} diff --git a/NightclubArcadia/Assets/Scripts/Skills/Runtime/PlayerSkills.cs.meta b/NightclubArcadia/Assets/Scripts/Skills/Runtime/PlayerSkills.cs.meta new file mode 100644 index 0000000..80a529b --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/Runtime/PlayerSkills.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ace1f9c8cfc64ff9aa040d60495b6115 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Scripts/Skills/Runtime/SeededRollSource.cs b/NightclubArcadia/Assets/Scripts/Skills/Runtime/SeededRollSource.cs new file mode 100644 index 0000000..f004493 --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/Runtime/SeededRollSource.cs @@ -0,0 +1,24 @@ +using System; + +namespace NightclubArcadia.Skills +{ + public sealed class SeededRollSource : IRollSource + { + readonly Random _random; + + public SeededRollSource(int seed) + { + _random = new Random(seed); + } + + public int Roll(int sides) + { + if (sides < 1) + { + throw new ArgumentOutOfRangeException(nameof(sides)); + } + + return _random.Next(1, sides + 1); + } + } +} diff --git a/NightclubArcadia/Assets/Scripts/Skills/Runtime/SeededRollSource.cs.meta b/NightclubArcadia/Assets/Scripts/Skills/Runtime/SeededRollSource.cs.meta new file mode 100644 index 0000000..605556c --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/Runtime/SeededRollSource.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5e89e99b02ed453db3efbd72ec8d038a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Scripts/Skills/Runtime/SkillCheckSystem.cs b/NightclubArcadia/Assets/Scripts/Skills/Runtime/SkillCheckSystem.cs new file mode 100644 index 0000000..c27d63f --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/Runtime/SkillCheckSystem.cs @@ -0,0 +1,84 @@ +using System; + +namespace NightclubArcadia.Skills +{ + /// + /// Pure check math. Exactly three modifier sources: rank, situational, state. + /// No UnityEngine types. No side effects on PlayerSkills / VariableStorage / UI. + /// + public sealed class SkillCheckSystem + { + readonly SkillDatabase _database; + readonly PlayerSkills _skills; + readonly SkillStateModifiers _state; + readonly IRollSource _rolls; + + public SkillCheckSystem( + SkillDatabase database, + PlayerSkills skills, + SkillStateModifiers state, + IRollSource rolls) + { + _database = database ?? throw new ArgumentNullException(nameof(database)); + _skills = skills ?? throw new ArgumentNullException(nameof(skills)); + _state = state ?? throw new ArgumentNullException(nameof(state)); + _rolls = rolls ?? throw new ArgumentNullException(nameof(rolls)); + } + + public SkillCheckResult Resolve(string skillId, int dc, int situationalMod) + { + skillId ??= string.Empty; + + string displayName; + int rankMod; + + if (_database.TryGet(skillId, out var def) && def != null) + { + displayName = string.IsNullOrEmpty(def.DisplayName) + ? def.Id.ToUpperInvariant() + : def.DisplayName; + rankMod = _skills.GetRank(skillId); + } + else + { + displayName = skillId.ToUpperInvariant(); + rankMod = 0; + } + + var roll = _rolls.Roll(20); + var stateMod = _state.TotalFor(skillId); + var total = roll + rankMod + situationalMod + stateMod; + + bool success; + SkillCheckDegree degree; + + if (roll == 1) + { + success = false; + degree = SkillCheckDegree.CriticalFailure; + } + else if (roll == 20) + { + success = true; + degree = SkillCheckDegree.CriticalSuccess; + } + else + { + success = total >= dc; + degree = success ? SkillCheckDegree.Success : SkillCheckDegree.Failure; + } + + return new SkillCheckResult( + skillId, + displayName, + roll, + rankMod, + situationalMod, + stateMod, + total, + dc, + success, + degree); + } + } +} diff --git a/NightclubArcadia/Assets/Scripts/Skills/Runtime/SkillCheckSystem.cs.meta b/NightclubArcadia/Assets/Scripts/Skills/Runtime/SkillCheckSystem.cs.meta new file mode 100644 index 0000000..8fe3f8a --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/Runtime/SkillCheckSystem.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 83a2adbf493a4d6eb4f5911582cc15ac +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Scripts/Skills/Runtime/SkillRuntime.cs b/NightclubArcadia/Assets/Scripts/Skills/Runtime/SkillRuntime.cs new file mode 100644 index 0000000..6812c16 --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/Runtime/SkillRuntime.cs @@ -0,0 +1,75 @@ +using UnityEngine; +using Yarn.Unity; + +namespace NightclubArcadia.Skills +{ + /// + /// Composition root and singleton that static Yarn commands route through. + /// + public sealed class SkillRuntime : MonoBehaviour + { + public static SkillRuntime Instance { get; private set; } + + [SerializeField] SkillDatabase database; + [SerializeField] DialogueRunner dialogueRunner; + [SerializeField] SkillCheckResultView resultView; + [SerializeField] CommentarySystem commentary; + [SerializeField] int rollSeed; + [SerializeField] string debugContextId = "Entrance_Queue"; + + public SkillDatabase Database => database; + public DialogueRunner DialogueRunner => dialogueRunner; + public SkillCheckResultView ResultView => resultView; + public CommentarySystem Commentary => commentary; + public PlayerSkills Skills { get; private set; } + public SkillStateModifiers States { get; private set; } + public SkillCheckSystem Checks { get; private set; } + public IRollSource Rolls { get; private set; } + + void Awake() + { + if (Instance != null && Instance != this) + { + Debug.LogError($"[{nameof(SkillRuntime)}] Duplicate instance on '{name}'. Destroying this component.", this); + Destroy(this); + return; + } + + Instance = this; + + if (database == null) + { + Debug.LogError($"[{nameof(SkillRuntime)}] SkillDatabase is not assigned.", this); + } + + Skills = database != null ? new PlayerSkills(database) : null; + States = new SkillStateModifiers(); + Rolls = rollSeed != 0 ? (IRollSource)new SeededRollSource(rollSeed) : new UnityRollSource(); + + if (Skills != null) + { + Checks = new SkillCheckSystem(database, Skills, States, Rolls); + } + } + + void OnDestroy() + { + if (Instance == this) + { + Instance = null; + } + } + + [ContextMenu("Fire Commentary (debug)")] + void FireCommentaryDebug() + { + if (commentary == null) + { + Debug.LogWarning($"[{nameof(SkillRuntime)}] No CommentarySystem assigned.", this); + return; + } + + commentary.RequestCommentary(debugContextId); + } + } +} diff --git a/NightclubArcadia/Assets/Scripts/Skills/Runtime/SkillRuntime.cs.meta b/NightclubArcadia/Assets/Scripts/Skills/Runtime/SkillRuntime.cs.meta new file mode 100644 index 0000000..149cd4a --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/Runtime/SkillRuntime.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cb04a8b1bce645fba398b22bbb4db58a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Scripts/Skills/Runtime/SkillStateModifiers.cs b/NightclubArcadia/Assets/Scripts/Skills/Runtime/SkillStateModifiers.cs new file mode 100644 index 0000000..b184654 --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/Runtime/SkillStateModifiers.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; + +namespace NightclubArcadia.Skills +{ + /// + /// Transient state modifiers (third of three modifier sources). Pure state — no check math. + /// Use skillId "*" to apply to every skill. + /// + public sealed class SkillStateModifiers + { + readonly Dictionary> _bySource = + new Dictionary>(StringComparer.Ordinal); + + public void Set(string sourceKey, string skillId, int delta) + { + if (string.IsNullOrEmpty(sourceKey)) + { + throw new ArgumentException("Source key is required.", nameof(sourceKey)); + } + + if (string.IsNullOrEmpty(skillId)) + { + throw new ArgumentException("Skill id is required.", nameof(skillId)); + } + + if (!_bySource.TryGetValue(sourceKey, out var map)) + { + map = new Dictionary(StringComparer.OrdinalIgnoreCase); + _bySource[sourceKey] = map; + } + + map[skillId] = delta; + } + + public void Clear(string sourceKey) + { + if (string.IsNullOrEmpty(sourceKey)) + { + return; + } + + _bySource.Remove(sourceKey); + } + + public int TotalFor(string skillId) + { + if (string.IsNullOrEmpty(skillId)) + { + return 0; + } + + var total = 0; + foreach (var map in _bySource.Values) + { + if (map.TryGetValue(skillId, out var specific)) + { + total += specific; + } + + if (map.TryGetValue("*", out var wildcard)) + { + total += wildcard; + } + } + + return total; + } + } +} diff --git a/NightclubArcadia/Assets/Scripts/Skills/Runtime/SkillStateModifiers.cs.meta b/NightclubArcadia/Assets/Scripts/Skills/Runtime/SkillStateModifiers.cs.meta new file mode 100644 index 0000000..81259be --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/Runtime/SkillStateModifiers.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 02f6b5bf30ad4f8db0711ed25b083247 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Scripts/Skills/Runtime/UnityRollSource.cs b/NightclubArcadia/Assets/Scripts/Skills/Runtime/UnityRollSource.cs new file mode 100644 index 0000000..adc2c78 --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/Runtime/UnityRollSource.cs @@ -0,0 +1,17 @@ +using UnityEngine; + +namespace NightclubArcadia.Skills +{ + public sealed class UnityRollSource : IRollSource + { + public int Roll(int sides) + { + if (sides < 1) + { + throw new System.ArgumentOutOfRangeException(nameof(sides)); + } + + return Random.Range(1, sides + 1); + } + } +} diff --git a/NightclubArcadia/Assets/Scripts/Skills/Runtime/UnityRollSource.cs.meta b/NightclubArcadia/Assets/Scripts/Skills/Runtime/UnityRollSource.cs.meta new file mode 100644 index 0000000..747e80b --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/Runtime/UnityRollSource.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 54552e499883406b9118fd2ba28e08ec +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Scripts/Skills/UI.meta b/NightclubArcadia/Assets/Scripts/Skills/UI.meta new file mode 100644 index 0000000..f571996 --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/UI.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: bdc1a9f9c02f400fa01d8341f81dc1c3 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Scripts/Skills/UI/SkillCheckResultView.cs b/NightclubArcadia/Assets/Scripts/Skills/UI/SkillCheckResultView.cs new file mode 100644 index 0000000..1a2386f --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/UI/SkillCheckResultView.cs @@ -0,0 +1,98 @@ +using System.Collections; +using TMPro; +using UnityEngine; + +namespace NightclubArcadia.Skills +{ + /// + /// Transparent roll banner. Must show roll, each of the three modifiers, total, and DC. + /// + public sealed class SkillCheckResultView : MonoBehaviour + { + [SerializeField] CanvasGroup group; + [SerializeField] TMP_Text label; + [SerializeField] float holdSeconds = 1.6f; + [SerializeField] float fadeSeconds = 0.15f; + + void Awake() + { + if (group != null) + { + group.alpha = 0f; + group.blocksRaycasts = false; + group.interactable = false; + } + } + + public IEnumerator Show(SkillCheckResult result) + { + if (label != null) + { + label.text = Format(result); + label.color = ResolveAccent(result.SkillId); + } + + if (group == null) + { + yield return new WaitForSeconds(holdSeconds); + yield break; + } + + yield return Fade(0f, 1f); + yield return new WaitForSeconds(holdSeconds); + yield return Fade(1f, 0f); + } + + static Color ResolveAccent(string skillId) + { + var runtime = SkillRuntime.Instance; + if (runtime != null && + runtime.Database != null && + runtime.Database.TryGet(skillId, out var def) && + def != null) + { + return def.AccentColor; + } + + return Color.white; + } + + static string Format(SkillCheckResult r) + { + var outcome = r.Degree switch + { + SkillCheckDegree.CriticalSuccess => "CRITICAL SUCCESS", + SkillCheckDegree.CriticalFailure => "CRITICAL FAILURE", + SkillCheckDegree.Success => "SUCCESS", + _ => "FAILURE", + }; + + var name = string.IsNullOrEmpty(r.SkillDisplayName) + ? (r.SkillId ?? string.Empty).ToUpperInvariant() + : r.SkillDisplayName.ToUpperInvariant(); + + return $"{name} — d20 {r.Roll} {Signed(r.RankMod)} rank {Signed(r.SituationalMod)} situation {Signed(r.StateMod)} state = {r.Total} vs DC {r.DC} · {outcome}"; + } + + static string Signed(int value) => value >= 0 ? $"+{value}" : value.ToString(); + + IEnumerator Fade(float from, float to) + { + if (fadeSeconds <= 0f) + { + group.alpha = to; + yield break; + } + + var elapsed = 0f; + while (elapsed < fadeSeconds) + { + elapsed += Time.unscaledDeltaTime; + group.alpha = Mathf.Lerp(from, to, Mathf.Clamp01(elapsed / fadeSeconds)); + yield return null; + } + + group.alpha = to; + } + } +} diff --git a/NightclubArcadia/Assets/Scripts/Skills/UI/SkillCheckResultView.cs.meta b/NightclubArcadia/Assets/Scripts/Skills/UI/SkillCheckResultView.cs.meta new file mode 100644 index 0000000..ef4a659 --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/UI/SkillCheckResultView.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: af3c81416f5f4a869baa347fd9dee9e3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Scripts/Skills/Yarn.meta b/NightclubArcadia/Assets/Scripts/Skills/Yarn.meta new file mode 100644 index 0000000..ed3e62d --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/Yarn.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a5b25da74b134d59828005e34c846be0 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Scripts/Skills/Yarn/YarnSkillCommands.cs b/NightclubArcadia/Assets/Scripts/Skills/Yarn/YarnSkillCommands.cs new file mode 100644 index 0000000..a71e83b --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/Yarn/YarnSkillCommands.cs @@ -0,0 +1,115 @@ +using System.Collections; +using UnityEngine; +using Yarn.Unity; + +namespace NightclubArcadia.Skills +{ + /// + /// Static Yarn adapter. No MonoBehaviour, no check math — translation only. + /// Call sites: <<check persuasion standard>> / <<check intimidation hard -1>> + /// + public static class YarnSkillCommands + { + [YarnCommand("check")] + public static IEnumerator Check(string skillId, string difficulty, int modifier = 0) + { + var runtime = SkillRuntime.Instance; + if (runtime == null || runtime.Checks == null || runtime.DialogueRunner == null) + { + Debug.LogError("[YarnSkillCommands] <> called but SkillRuntime is not ready."); + var fallbackRunner = runtime != null ? runtime.DialogueRunner : null; + if (fallbackRunner == null) + { + fallbackRunner = Object.FindFirstObjectByType(); + } + WriteFailedResult(fallbackRunner, skillId, difficulty); + yield break; + } + + if (!DcBands.TryParse(difficulty, out var dc)) + { + Debug.LogError($"[YarnSkillCommands] <>: unknown difficulty; treating as Standard (11)."); + dc = (int)DcBand.Standard; + } + + if (runtime.Database == null || !runtime.Database.TryGet(skillId, out _)) + { + Debug.LogError($"[YarnSkillCommands] <>: no skill '{skillId}'."); + } + + var result = runtime.Checks.Resolve(skillId, dc, modifier); + WriteResult(runtime.DialogueRunner.VariableStorage, result); + + if (runtime.ResultView == null) + { + Debug.LogWarning("[YarnSkillCommands] SkillCheckResultView is not assigned; skipping roll banner."); + yield break; + } + + yield return runtime.StartCoroutine(runtime.ResultView.Show(result)); + } + + [YarnFunction("skill_rank")] + public static float SkillRank(string skillId) + { + var runtime = SkillRuntime.Instance; + if (runtime == null || runtime.Skills == null) + { + return 0f; + } + + return runtime.Skills.GetRank(skillId); + } + + static void WriteResult(VariableStorageBehaviour storage, SkillCheckResult result) + { + if (storage == null) + { + return; + } + + storage.SetValue("$check_result", result.Success); + storage.SetValue("$check_roll", (float)result.Roll); + storage.SetValue("$check_total", (float)result.Total); + storage.SetValue("$check_dc", (float)result.DC); + storage.SetValue("$check_modifier", (float)(result.RankMod + result.SituationalMod + result.StateMod)); + storage.SetValue("$check_degree", DegreeToString(result.Degree)); + storage.SetValue("$check_skill", result.SkillId ?? string.Empty); + } + + static void WriteFailedResult(DialogueRunner runner, string skillId, string difficulty) + { + if (runner == null || runner.VariableStorage == null) + { + return; + } + + DcBands.TryParse(difficulty, out var dc); + if (dc == 0) + { + dc = (int)DcBand.Standard; + } + + var storage = runner.VariableStorage; + storage.SetValue("$check_result", false); + storage.SetValue("$check_roll", 0f); + storage.SetValue("$check_total", 0f); + storage.SetValue("$check_dc", (float)dc); + storage.SetValue("$check_modifier", 0f); + storage.SetValue("$check_degree", "failure"); + storage.SetValue("$check_skill", skillId ?? string.Empty); + } + + static string DegreeToString(SkillCheckDegree degree) + { + return degree switch + { + SkillCheckDegree.CriticalFailure => "critical_failure", + SkillCheckDegree.Failure => "failure", + SkillCheckDegree.Success => "success", + SkillCheckDegree.CriticalSuccess => "critical_success", + _ => "failure", + }; + } + } +} diff --git a/NightclubArcadia/Assets/Scripts/Skills/Yarn/YarnSkillCommands.cs.meta b/NightclubArcadia/Assets/Scripts/Skills/Yarn/YarnSkillCommands.cs.meta new file mode 100644 index 0000000..b25e646 --- /dev/null +++ b/NightclubArcadia/Assets/Scripts/Skills/Yarn/YarnSkillCommands.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8c00579d400d44ac85b16d9b27be7a87 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Skills.meta b/NightclubArcadia/Assets/Skills.meta new file mode 100644 index 0000000..f0b00dd --- /dev/null +++ b/NightclubArcadia/Assets/Skills.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 89b18dcb780d45cc94f8de93f4ba62d4 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Skills/Definitions.meta b/NightclubArcadia/Assets/Skills/Definitions.meta new file mode 100644 index 0000000..a2db9aa --- /dev/null +++ b/NightclubArcadia/Assets/Skills/Definitions.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: aa1191ce507748a7aea886e539eb9d27 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Skills/Definitions/deduction.asset b/NightclubArcadia/Assets/Skills/Definitions/deduction.asset new file mode 100644 index 0000000..bea4f6a --- /dev/null +++ b/NightclubArcadia/Assets/Skills/Definitions/deduction.asset @@ -0,0 +1,20 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9a26230b6f20436891aa772e8f858f3f, type: 3} + m_Name: deduction + m_EditorClassIdentifier: + id: deduction + displayName: Deduction + domain: 0 + description: "Placeholder skill \u2014 replace when Skill_System_Draft.md is available." + startingRank: 1 + accentColor: {r: 0.55, g: 0.75, b: 1, a: 1} diff --git a/NightclubArcadia/Assets/Skills/Definitions/deduction.asset.meta b/NightclubArcadia/Assets/Skills/Definitions/deduction.asset.meta new file mode 100644 index 0000000..c424879 --- /dev/null +++ b/NightclubArcadia/Assets/Skills/Definitions/deduction.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 21592f6cf7a6457a8198553ab124d41b +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Skills/Definitions/endurance.asset b/NightclubArcadia/Assets/Skills/Definitions/endurance.asset new file mode 100644 index 0000000..9407a3f --- /dev/null +++ b/NightclubArcadia/Assets/Skills/Definitions/endurance.asset @@ -0,0 +1,20 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9a26230b6f20436891aa772e8f858f3f, type: 3} + m_Name: endurance + m_EditorClassIdentifier: + id: endurance + displayName: Endurance + domain: 3 + description: "Placeholder skill \u2014 replace when Skill_System_Draft.md is available." + startingRank: 1 + accentColor: {r: 0.85, g: 0.7, b: 0.5, a: 1} diff --git a/NightclubArcadia/Assets/Skills/Definitions/endurance.asset.meta b/NightclubArcadia/Assets/Skills/Definitions/endurance.asset.meta new file mode 100644 index 0000000..7a717af --- /dev/null +++ b/NightclubArcadia/Assets/Skills/Definitions/endurance.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 84586fcc502c42889a44dd32ed2a2f1d +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Skills/Definitions/intimidation.asset b/NightclubArcadia/Assets/Skills/Definitions/intimidation.asset new file mode 100644 index 0000000..1147717 --- /dev/null +++ b/NightclubArcadia/Assets/Skills/Definitions/intimidation.asset @@ -0,0 +1,20 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9a26230b6f20436891aa772e8f858f3f, type: 3} + m_Name: intimidation + m_EditorClassIdentifier: + id: intimidation + displayName: Intimidation + domain: 2 + description: "Placeholder skill \u2014 replace when Skill_System_Draft.md is available." + startingRank: 2 + accentColor: {r: 1, g: 0.4, b: 0.4, a: 1} diff --git a/NightclubArcadia/Assets/Skills/Definitions/intimidation.asset.meta b/NightclubArcadia/Assets/Skills/Definitions/intimidation.asset.meta new file mode 100644 index 0000000..f037f73 --- /dev/null +++ b/NightclubArcadia/Assets/Skills/Definitions/intimidation.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 254c6612b22d44e7b3552a8aac9311be +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Skills/Definitions/nerve.asset b/NightclubArcadia/Assets/Skills/Definitions/nerve.asset new file mode 100644 index 0000000..da9ca0e --- /dev/null +++ b/NightclubArcadia/Assets/Skills/Definitions/nerve.asset @@ -0,0 +1,20 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9a26230b6f20436891aa772e8f858f3f, type: 3} + m_Name: nerve + m_EditorClassIdentifier: + id: nerve + displayName: Nerve + domain: 3 + description: "Placeholder skill \u2014 replace when Skill_System_Draft.md is available." + startingRank: 1 + accentColor: {r: 0.95, g: 0.6, b: 0.35, a: 1} diff --git a/NightclubArcadia/Assets/Skills/Definitions/nerve.asset.meta b/NightclubArcadia/Assets/Skills/Definitions/nerve.asset.meta new file mode 100644 index 0000000..d4eafd5 --- /dev/null +++ b/NightclubArcadia/Assets/Skills/Definitions/nerve.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ee266337568a41db9aca1f2af43ec466 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Skills/Definitions/paranoia.asset b/NightclubArcadia/Assets/Skills/Definitions/paranoia.asset new file mode 100644 index 0000000..d77fa01 --- /dev/null +++ b/NightclubArcadia/Assets/Skills/Definitions/paranoia.asset @@ -0,0 +1,20 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9a26230b6f20436891aa772e8f858f3f, type: 3} + m_Name: paranoia + m_EditorClassIdentifier: + id: paranoia + displayName: Paranoia + domain: 1 + description: "Placeholder skill \u2014 replace when Skill_System_Draft.md is available." + startingRank: 1 + accentColor: {r: 1, g: 0.45, b: 0.55, a: 1} diff --git a/NightclubArcadia/Assets/Skills/Definitions/paranoia.asset.meta b/NightclubArcadia/Assets/Skills/Definitions/paranoia.asset.meta new file mode 100644 index 0000000..be982c3 --- /dev/null +++ b/NightclubArcadia/Assets/Skills/Definitions/paranoia.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: b9de7affe9fa49f8a1f3bb64a87d817f +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Skills/Definitions/persuasion.asset b/NightclubArcadia/Assets/Skills/Definitions/persuasion.asset new file mode 100644 index 0000000..3b66ad1 --- /dev/null +++ b/NightclubArcadia/Assets/Skills/Definitions/persuasion.asset @@ -0,0 +1,20 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9a26230b6f20436891aa772e8f858f3f, type: 3} + m_Name: persuasion + m_EditorClassIdentifier: + id: persuasion + displayName: Persuasion + domain: 2 + description: "Placeholder skill \u2014 replace when Skill_System_Draft.md is available." + startingRank: 1 + accentColor: {r: 0.85, g: 0.65, b: 1, a: 1} diff --git a/NightclubArcadia/Assets/Skills/Definitions/persuasion.asset.meta b/NightclubArcadia/Assets/Skills/Definitions/persuasion.asset.meta new file mode 100644 index 0000000..2a95b12 --- /dev/null +++ b/NightclubArcadia/Assets/Skills/Definitions/persuasion.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: f510b7fc944e4c1190c35a5f82a52650 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Skills/Definitions/rapport.asset b/NightclubArcadia/Assets/Skills/Definitions/rapport.asset new file mode 100644 index 0000000..b57c4b6 --- /dev/null +++ b/NightclubArcadia/Assets/Skills/Definitions/rapport.asset @@ -0,0 +1,20 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9a26230b6f20436891aa772e8f858f3f, type: 3} + m_Name: rapport + m_EditorClassIdentifier: + id: rapport + displayName: Rapport + domain: 2 + description: "Placeholder skill \u2014 replace when Skill_System_Draft.md is available." + startingRank: 1 + accentColor: {r: 0.55, g: 0.95, b: 0.85, a: 1} diff --git a/NightclubArcadia/Assets/Skills/Definitions/rapport.asset.meta b/NightclubArcadia/Assets/Skills/Definitions/rapport.asset.meta new file mode 100644 index 0000000..4f2c50a --- /dev/null +++ b/NightclubArcadia/Assets/Skills/Definitions/rapport.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: eb856a5941c0497b8eb7796793c617ba +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Skills/Definitions/read_room.asset b/NightclubArcadia/Assets/Skills/Definitions/read_room.asset new file mode 100644 index 0000000..4d513eb --- /dev/null +++ b/NightclubArcadia/Assets/Skills/Definitions/read_room.asset @@ -0,0 +1,20 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9a26230b6f20436891aa772e8f858f3f, type: 3} + m_Name: read_room + m_EditorClassIdentifier: + id: read_room + displayName: Read the Room + domain: 1 + description: "Placeholder skill \u2014 replace when Skill_System_Draft.md is available." + startingRank: 1 + accentColor: {r: 1, g: 0.75, b: 0.45, a: 1} diff --git a/NightclubArcadia/Assets/Skills/Definitions/read_room.asset.meta b/NightclubArcadia/Assets/Skills/Definitions/read_room.asset.meta new file mode 100644 index 0000000..04672e5 --- /dev/null +++ b/NightclubArcadia/Assets/Skills/Definitions/read_room.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: abfd9204c20644c090c30bd6404c45a6 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Skills/Definitions/recall.asset b/NightclubArcadia/Assets/Skills/Definitions/recall.asset new file mode 100644 index 0000000..74306d9 --- /dev/null +++ b/NightclubArcadia/Assets/Skills/Definitions/recall.asset @@ -0,0 +1,20 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9a26230b6f20436891aa772e8f858f3f, type: 3} + m_Name: recall + m_EditorClassIdentifier: + id: recall + displayName: Recall + domain: 0 + description: "Placeholder skill \u2014 replace when Skill_System_Draft.md is available." + startingRank: 1 + accentColor: {r: 0.65, g: 0.7, b: 1, a: 1} diff --git a/NightclubArcadia/Assets/Skills/Definitions/recall.asset.meta b/NightclubArcadia/Assets/Skills/Definitions/recall.asset.meta new file mode 100644 index 0000000..873220d --- /dev/null +++ b/NightclubArcadia/Assets/Skills/Definitions/recall.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c675bc7c6c864e6e9e4f71394ba1f07f +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Skills/Definitions/reflexes.asset b/NightclubArcadia/Assets/Skills/Definitions/reflexes.asset new file mode 100644 index 0000000..cf963c6 --- /dev/null +++ b/NightclubArcadia/Assets/Skills/Definitions/reflexes.asset @@ -0,0 +1,20 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9a26230b6f20436891aa772e8f858f3f, type: 3} + m_Name: reflexes + m_EditorClassIdentifier: + id: reflexes + displayName: Reflexes + domain: 1 + description: "Placeholder skill \u2014 replace when Skill_System_Draft.md is available." + startingRank: 1 + accentColor: {r: 1, g: 0.9, b: 0.4, a: 1} diff --git a/NightclubArcadia/Assets/Skills/Definitions/reflexes.asset.meta b/NightclubArcadia/Assets/Skills/Definitions/reflexes.asset.meta new file mode 100644 index 0000000..d44b84f --- /dev/null +++ b/NightclubArcadia/Assets/Skills/Definitions/reflexes.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 78d1f1a41b2b48358f889935c3e3094c +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Skills/Definitions/sleight_of_hand.asset b/NightclubArcadia/Assets/Skills/Definitions/sleight_of_hand.asset new file mode 100644 index 0000000..c9f3915 --- /dev/null +++ b/NightclubArcadia/Assets/Skills/Definitions/sleight_of_hand.asset @@ -0,0 +1,20 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9a26230b6f20436891aa772e8f858f3f, type: 3} + m_Name: sleight_of_hand + m_EditorClassIdentifier: + id: sleight_of_hand + displayName: Sleight of Hand + domain: 3 + description: "Placeholder skill \u2014 replace when Skill_System_Draft.md is available." + startingRank: 1 + accentColor: {r: 0.7, g: 0.9, b: 0.95, a: 1} diff --git a/NightclubArcadia/Assets/Skills/Definitions/sleight_of_hand.asset.meta b/NightclubArcadia/Assets/Skills/Definitions/sleight_of_hand.asset.meta new file mode 100644 index 0000000..4be5c06 --- /dev/null +++ b/NightclubArcadia/Assets/Skills/Definitions/sleight_of_hand.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 60239bdfcace42f89d3ff82e383e98b8 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Skills/Definitions/streetwise.asset b/NightclubArcadia/Assets/Skills/Definitions/streetwise.asset new file mode 100644 index 0000000..ba4a875 --- /dev/null +++ b/NightclubArcadia/Assets/Skills/Definitions/streetwise.asset @@ -0,0 +1,20 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 9a26230b6f20436891aa772e8f858f3f, type: 3} + m_Name: streetwise + m_EditorClassIdentifier: + id: streetwise + displayName: Streetwise + domain: 0 + description: "Placeholder skill \u2014 replace when Skill_System_Draft.md is available." + startingRank: 1 + accentColor: {r: 0.7, g: 0.85, b: 0.55, a: 1} diff --git a/NightclubArcadia/Assets/Skills/Definitions/streetwise.asset.meta b/NightclubArcadia/Assets/Skills/Definitions/streetwise.asset.meta new file mode 100644 index 0000000..8f18b7a --- /dev/null +++ b/NightclubArcadia/Assets/Skills/Definitions/streetwise.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 910cd1397efb4e23bca22bbe6c3c6971 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Skills/SkillDatabase.asset b/NightclubArcadia/Assets/Skills/SkillDatabase.asset new file mode 100644 index 0000000..56b594e --- /dev/null +++ b/NightclubArcadia/Assets/Skills/SkillDatabase.asset @@ -0,0 +1,27 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 73f6e7831a60457b918d303c16fc6cec, type: 3} + m_Name: SkillDatabase + m_EditorClassIdentifier: + skills: + - {fileID: 11400000, guid: 21592f6cf7a6457a8198553ab124d41b, type: 2} + - {fileID: 11400000, guid: 910cd1397efb4e23bca22bbe6c3c6971, type: 2} + - {fileID: 11400000, guid: c675bc7c6c864e6e9e4f71394ba1f07f, type: 2} + - {fileID: 11400000, guid: abfd9204c20644c090c30bd6404c45a6, type: 2} + - {fileID: 11400000, guid: b9de7affe9fa49f8a1f3bb64a87d817f, type: 2} + - {fileID: 11400000, guid: 78d1f1a41b2b48358f889935c3e3094c, type: 2} + - {fileID: 11400000, guid: f510b7fc944e4c1190c35a5f82a52650, type: 2} + - {fileID: 11400000, guid: 254c6612b22d44e7b3552a8aac9311be, type: 2} + - {fileID: 11400000, guid: eb856a5941c0497b8eb7796793c617ba, type: 2} + - {fileID: 11400000, guid: 84586fcc502c42889a44dd32ed2a2f1d, type: 2} + - {fileID: 11400000, guid: 60239bdfcace42f89d3ff82e383e98b8, type: 2} + - {fileID: 11400000, guid: ee266337568a41db9aca1f2af43ec466, type: 2} diff --git a/NightclubArcadia/Assets/Skills/SkillDatabase.asset.meta b/NightclubArcadia/Assets/Skills/SkillDatabase.asset.meta new file mode 100644 index 0000000..3b4bf2a --- /dev/null +++ b/NightclubArcadia/Assets/Skills/SkillDatabase.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d15ae35514b14ded9b398d630e8ef373 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Tests.meta b/NightclubArcadia/Assets/Tests.meta new file mode 100644 index 0000000..21063da --- /dev/null +++ b/NightclubArcadia/Assets/Tests.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 40490aad9dd94cdca1d9170d5f729cba +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Tests/EditMode.meta b/NightclubArcadia/Assets/Tests/EditMode.meta new file mode 100644 index 0000000..5a14d45 --- /dev/null +++ b/NightclubArcadia/Assets/Tests/EditMode.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c70934ddbc404dfcb0952241bec17b6c +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Tests/EditMode/CommentaryPickerTests.cs b/NightclubArcadia/Assets/Tests/EditMode/CommentaryPickerTests.cs new file mode 100644 index 0000000..184f5a4 --- /dev/null +++ b/NightclubArcadia/Assets/Tests/EditMode/CommentaryPickerTests.cs @@ -0,0 +1,90 @@ +using System.Collections.Generic; +using NUnit.Framework; +using NightclubArcadia.Skills; +using UnityEngine; + +namespace NightclubArcadia.Skills.Tests +{ + public class CommentaryPickerTests + { + sealed class FixedRollSource : IRollSource + { + readonly int _value; + public FixedRollSource(int value) => _value = value; + public int Roll(int sides) => _value; + } + + SkillDefinition Make(string id, int startingRank) + { + var def = ScriptableObject.CreateInstance(); + def.EditorSet(id, id, SkillDomain.Instinct, string.Empty, startingRank, Color.white); + return def; + } + + PlayerSkills SkillsFor(params (string id, int rank)[] ranks) + { + var db = ScriptableObject.CreateInstance(); + var list = new List(); + foreach (var (id, rank) in ranks) + { + list.Add(Make(id, rank)); + } + + db.EditorSetSkills(list); + return new PlayerSkills(db); + } + + [Test] + public void Pick_EmptyCandidates_ReturnsNull() + { + var skills = SkillsFor(("paranoia", 2)); + var result = CommentarySystem.Pick( + new List(), + skills, + new List(), + 3, + new FixedRollSource(1)); + + Assert.IsNull(result); + } + + [Test] + public void Pick_HigherRankPreferred_GivenLowRoll() + { + // ranks 1 and 3 → weights 1 and 3 → total 4 + // roll 1 maps to pick index 0 of cumulative: only weight-1 skill + // roll 2..4 maps to higher-ranked skill + var low = Make("read_room", 1); + var high = Make("paranoia", 3); + var skills = SkillsFor(("read_room", 1), ("paranoia", 3)); + + var highPick = CommentarySystem.Pick( + new List { low, high }, + skills, + new List(), + 3, + new FixedRollSource(2)); + + Assert.AreEqual("paranoia", highPick.Id); + } + + [Test] + public void Pick_RecentSpeaker_HasHalvedWeight() + { + // Without recency: weights 2 and 2, total 4 + // With "a" recent: weights 1 and 2, total 3 → roll 1 picks a, roll 2+ picks b + var a = Make("a", 2); + var b = Make("b", 2); + var skills = SkillsFor(("a", 2), ("b", 2)); + + var pick = CommentarySystem.Pick( + new List { a, b }, + skills, + new List { "a" }, + 3, + new FixedRollSource(2)); + + Assert.AreEqual("b", pick.Id); + } + } +} diff --git a/NightclubArcadia/Assets/Tests/EditMode/CommentaryPickerTests.cs.meta b/NightclubArcadia/Assets/Tests/EditMode/CommentaryPickerTests.cs.meta new file mode 100644 index 0000000..3dffe40 --- /dev/null +++ b/NightclubArcadia/Assets/Tests/EditMode/CommentaryPickerTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6ba3f2a4a2a44285acd610ab51884753 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Tests/EditMode/DcBandTests.cs b/NightclubArcadia/Assets/Tests/EditMode/DcBandTests.cs new file mode 100644 index 0000000..9a660bf --- /dev/null +++ b/NightclubArcadia/Assets/Tests/EditMode/DcBandTests.cs @@ -0,0 +1,26 @@ +using NUnit.Framework; +using NightclubArcadia.Skills; + +namespace NightclubArcadia.Skills.Tests +{ + public class DcBandTests + { + [TestCase("standard", 11)] + [TestCase("STANDARD", 11)] + [TestCase("13", 13)] + [TestCase("-3", -3)] + public void TryParse_AcceptsBandNamesAndIntegers(string input, int expected) + { + Assert.IsTrue(DcBands.TryParse(input, out var dc)); + Assert.AreEqual(expected, dc); + } + + [TestCase("banana")] + [TestCase("")] + [TestCase(null)] + public void TryParse_RejectsGarbage(string input) + { + Assert.IsFalse(DcBands.TryParse(input, out _)); + } + } +} diff --git a/NightclubArcadia/Assets/Tests/EditMode/DcBandTests.cs.meta b/NightclubArcadia/Assets/Tests/EditMode/DcBandTests.cs.meta new file mode 100644 index 0000000..da2a639 --- /dev/null +++ b/NightclubArcadia/Assets/Tests/EditMode/DcBandTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9057738b1d8f402d907c1d9dab8d82a2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Tests/EditMode/NightclubArcadia.Skills.Tests.asmdef b/NightclubArcadia/Assets/Tests/EditMode/NightclubArcadia.Skills.Tests.asmdef new file mode 100644 index 0000000..9c200a8 --- /dev/null +++ b/NightclubArcadia/Assets/Tests/EditMode/NightclubArcadia.Skills.Tests.asmdef @@ -0,0 +1,24 @@ +{ + "name": "NightclubArcadia.Skills.Tests", + "rootNamespace": "NightclubArcadia.Skills.Tests", + "references": [ + "NightclubArcadia.Skills", + "UnityEngine.TestRunner", + "UnityEditor.TestRunner" + ], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": true, + "precompiledReferences": [ + "nunit.framework.dll" + ], + "autoReferenced": false, + "defineConstraints": [ + "UNITY_INCLUDE_TESTS" + ], + "versionDefines": [], + "noEngineReferences": false +} diff --git a/NightclubArcadia/Assets/Tests/EditMode/NightclubArcadia.Skills.Tests.asmdef.meta b/NightclubArcadia/Assets/Tests/EditMode/NightclubArcadia.Skills.Tests.asmdef.meta new file mode 100644 index 0000000..85abf62 --- /dev/null +++ b/NightclubArcadia/Assets/Tests/EditMode/NightclubArcadia.Skills.Tests.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 09d3d01ec5554542b5dd23239ace7a70 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/NightclubArcadia/Assets/Tests/EditMode/SkillCheckSystemTests.cs b/NightclubArcadia/Assets/Tests/EditMode/SkillCheckSystemTests.cs new file mode 100644 index 0000000..ec75a4c --- /dev/null +++ b/NightclubArcadia/Assets/Tests/EditMode/SkillCheckSystemTests.cs @@ -0,0 +1,137 @@ +using System.Collections.Generic; +using NUnit.Framework; +using NightclubArcadia.Skills; +using UnityEngine; + +namespace NightclubArcadia.Skills.Tests +{ + public class SkillCheckSystemTests + { + sealed class FixedRollSource : IRollSource + { + readonly int _value; + public FixedRollSource(int value) => _value = value; + public int Roll(int sides) => _value; + } + + SkillDatabase CreateDatabase(params (string id, string display, int rank)[] skills) + { + var db = ScriptableObject.CreateInstance(); + var list = new List(); + foreach (var (id, display, rank) in skills) + { + var def = ScriptableObject.CreateInstance(); + def.EditorSet(id, display, SkillDomain.Presence, string.Empty, rank, Color.white); + list.Add(def); + } + + db.EditorSetSkills(list); + return db; + } + + [Test] + public void Resolve_SumsModifiers_AndSucceedsWhenTotalMeetsDc() + { + var db = CreateDatabase(("persuasion", "Persuasion", 2)); + var skills = new PlayerSkills(db); + var state = new SkillStateModifiers(); + var system = new SkillCheckSystem(db, skills, state, new FixedRollSource(10)); + + var result = system.Resolve("persuasion", 12, 1); + + Assert.AreEqual(13, result.Total); + Assert.IsTrue(result.Success); + Assert.AreEqual(SkillCheckDegree.Success, result.Degree); + Assert.AreEqual(2, result.RankMod); + Assert.AreEqual(1, result.SituationalMod); + Assert.AreEqual(0, result.StateMod); + } + + [Test] + public void Resolve_NaturalOne_IsCriticalFailureEvenIfModifiersWouldPass() + { + var db = CreateDatabase(("persuasion", "Persuasion", 10)); + var skills = new PlayerSkills(db); + var state = new SkillStateModifiers(); + var system = new SkillCheckSystem(db, skills, state, new FixedRollSource(1)); + + var result = system.Resolve("persuasion", 5, 5); + + Assert.IsFalse(result.Success); + Assert.AreEqual(SkillCheckDegree.CriticalFailure, result.Degree); + } + + [Test] + public void Resolve_NaturalTwenty_IsCriticalSuccessEvenIfBelowDc() + { + var db = CreateDatabase(("persuasion", "Persuasion", 0)); + var skills = new PlayerSkills(db); + var state = new SkillStateModifiers(); + var system = new SkillCheckSystem(db, skills, state, new FixedRollSource(20)); + + var result = system.Resolve("persuasion", 25, 0); + + Assert.IsTrue(result.Success); + Assert.AreEqual(SkillCheckDegree.CriticalSuccess, result.Degree); + } + + [Test] + public void Resolve_ExactDc_IsSuccess() + { + var db = CreateDatabase(("persuasion", "Persuasion", 1)); + var skills = new PlayerSkills(db); + var state = new SkillStateModifiers(); + var system = new SkillCheckSystem(db, skills, state, new FixedRollSource(10)); + + var result = system.Resolve("persuasion", 11, 0); + + Assert.AreEqual(11, result.Total); + Assert.IsTrue(result.Success); + } + + [Test] + public void Resolve_AppliesNegativeAndWildcardStateModifiers() + { + var db = CreateDatabase(("reflexes", "Reflexes", 1)); + var skills = new PlayerSkills(db); + var state = new SkillStateModifiers(); + state.Set("drunk", "reflexes", -2); + state.Set("crowd", "*", -1); + var system = new SkillCheckSystem(db, skills, state, new FixedRollSource(10)); + + var result = system.Resolve("reflexes", 5, 0); + + Assert.AreEqual(-3, result.StateMod); + Assert.AreEqual(8, result.Total); + } + + [Test] + public void Resolve_UnknownSkill_DoesNotThrow_AndUsesZeroRank() + { + var db = CreateDatabase(("persuasion", "Persuasion", 3)); + var skills = new PlayerSkills(db); + var state = new SkillStateModifiers(); + var system = new SkillCheckSystem(db, skills, state, new FixedRollSource(10)); + + SkillCheckResult result = default; + Assert.DoesNotThrow(() => result = system.Resolve("nosuchskill", 10, 0)); + Assert.AreEqual(0, result.RankMod); + Assert.AreEqual("NOSUCHSKILL", result.SkillDisplayName); + } + + [Test] + public void Resolve_DoesNotMutatePlayerSkills() + { + var db = CreateDatabase(("persuasion", "Persuasion", 2)); + var skills = new PlayerSkills(db); + var before = skills.GetRank("persuasion"); + var state = new SkillStateModifiers(); + var system = new SkillCheckSystem(db, skills, state, new FixedRollSource(10)); + + system.Resolve("persuasion", 10, 0); + + Assert.AreEqual(before, skills.GetRank("persuasion")); + Assert.AreEqual(1, skills.AllRanks.Count); + } + } +} diff --git a/NightclubArcadia/Assets/Tests/EditMode/SkillCheckSystemTests.cs.meta b/NightclubArcadia/Assets/Tests/EditMode/SkillCheckSystemTests.cs.meta new file mode 100644 index 0000000..0b1deb6 --- /dev/null +++ b/NightclubArcadia/Assets/Tests/EditMode/SkillCheckSystemTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 78734080b9b5473db4365b8203fc79b0 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: