diff --git a/Assets/AddressableAssetsData/AssetGroups/Default Local Group.asset b/Assets/AddressableAssetsData/AssetGroups/Default Local Group.asset index c94410e0..a7fb6d32 100644 --- a/Assets/AddressableAssetsData/AssetGroups/Default Local Group.asset +++ b/Assets/AddressableAssetsData/AssetGroups/Default Local Group.asset @@ -14,7 +14,12 @@ MonoBehaviour: m_EditorClassIdentifier: m_GroupName: Default Local Group m_GUID: 6f3207429a65b3e4b83935ac19791077 - m_SerializeEntries: [] + m_SerializeEntries: + - m_GUID: d28c589c05c122f449a8b34e696cda53 + m_Address: Puzzles/Quarry + m_ReadOnly: 0 + m_SerializedLabels: [] + FlaggedDuringContentUpdateRestriction: 0 m_ReadOnly: 0 m_Settings: {fileID: 11400000, guid: 11da9bb90d9dd5848b4f7629415a6937, type: 2} m_SchemaSet: diff --git a/Assets/Data/Puzzles/Quarry.meta b/Assets/Data/Puzzles/Quarry.meta new file mode 100644 index 00000000..36f2ab7f --- /dev/null +++ b/Assets/Data/Puzzles/Quarry.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a17ef190d30246143a8f7a83591f2c1f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Data/Puzzles/ChocolateBirdPuzzle.meta b/Assets/Data/Puzzles/Quarry/ChocolateBirdPuzzle.meta similarity index 100% rename from Assets/Data/Puzzles/ChocolateBirdPuzzle.meta rename to Assets/Data/Puzzles/Quarry/ChocolateBirdPuzzle.meta diff --git a/Assets/Data/Puzzles/ChocolateBirdPuzzle/LureWolter.asset b/Assets/Data/Puzzles/Quarry/ChocolateBirdPuzzle/LureWolter.asset similarity index 95% rename from Assets/Data/Puzzles/ChocolateBirdPuzzle/LureWolter.asset rename to Assets/Data/Puzzles/Quarry/ChocolateBirdPuzzle/LureWolter.asset index 8f148371..bebae427 100644 --- a/Assets/Data/Puzzles/ChocolateBirdPuzzle/LureWolter.asset +++ b/Assets/Data/Puzzles/Quarry/ChocolateBirdPuzzle/LureWolter.asset @@ -13,7 +13,7 @@ MonoBehaviour: m_Name: LureWolter m_EditorClassIdentifier: stepId: LureWolter - displayName: Wolter Lured! + displayName: Wolter Lured!! description: Place Chocolate in the luring spot. icon: {fileID: 0} unlocks: diff --git a/Assets/Data/Puzzles/ChocolateBirdPuzzle/LureWolter.asset.meta b/Assets/Data/Puzzles/Quarry/ChocolateBirdPuzzle/LureWolter.asset.meta similarity index 100% rename from Assets/Data/Puzzles/ChocolateBirdPuzzle/LureWolter.asset.meta rename to Assets/Data/Puzzles/Quarry/ChocolateBirdPuzzle/LureWolter.asset.meta diff --git a/Assets/Data/Puzzles/ChocolateBirdPuzzle/PickChocolate.asset b/Assets/Data/Puzzles/Quarry/ChocolateBirdPuzzle/PickChocolate.asset similarity index 100% rename from Assets/Data/Puzzles/ChocolateBirdPuzzle/PickChocolate.asset rename to Assets/Data/Puzzles/Quarry/ChocolateBirdPuzzle/PickChocolate.asset diff --git a/Assets/Data/Puzzles/ChocolateBirdPuzzle/PickChocolate.asset.meta b/Assets/Data/Puzzles/Quarry/ChocolateBirdPuzzle/PickChocolate.asset.meta similarity index 100% rename from Assets/Data/Puzzles/ChocolateBirdPuzzle/PickChocolate.asset.meta rename to Assets/Data/Puzzles/Quarry/ChocolateBirdPuzzle/PickChocolate.asset.meta diff --git a/Assets/Data/Puzzles/ExampleAssPuzzle.meta b/Assets/Data/Puzzles/Quarry/ExampleAssPuzzle.meta similarity index 100% rename from Assets/Data/Puzzles/ExampleAssPuzzle.meta rename to Assets/Data/Puzzles/Quarry/ExampleAssPuzzle.meta diff --git a/Assets/Data/Puzzles/ExampleAssPuzzle/Ass1.asset b/Assets/Data/Puzzles/Quarry/ExampleAssPuzzle/Ass1.asset similarity index 100% rename from Assets/Data/Puzzles/ExampleAssPuzzle/Ass1.asset rename to Assets/Data/Puzzles/Quarry/ExampleAssPuzzle/Ass1.asset diff --git a/Assets/Data/Puzzles/ExampleAssPuzzle/Ass1.asset.meta b/Assets/Data/Puzzles/Quarry/ExampleAssPuzzle/Ass1.asset.meta similarity index 100% rename from Assets/Data/Puzzles/ExampleAssPuzzle/Ass1.asset.meta rename to Assets/Data/Puzzles/Quarry/ExampleAssPuzzle/Ass1.asset.meta diff --git a/Assets/Data/Puzzles/ExampleAssPuzzle/Ass2.asset b/Assets/Data/Puzzles/Quarry/ExampleAssPuzzle/Ass2.asset similarity index 100% rename from Assets/Data/Puzzles/ExampleAssPuzzle/Ass2.asset rename to Assets/Data/Puzzles/Quarry/ExampleAssPuzzle/Ass2.asset diff --git a/Assets/Data/Puzzles/ExampleAssPuzzle/Ass2.asset.meta b/Assets/Data/Puzzles/Quarry/ExampleAssPuzzle/Ass2.asset.meta similarity index 100% rename from Assets/Data/Puzzles/ExampleAssPuzzle/Ass2.asset.meta rename to Assets/Data/Puzzles/Quarry/ExampleAssPuzzle/Ass2.asset.meta diff --git a/Assets/Data/Puzzles/ExampleAssPuzzle/Ass3.asset b/Assets/Data/Puzzles/Quarry/ExampleAssPuzzle/Ass3.asset similarity index 100% rename from Assets/Data/Puzzles/ExampleAssPuzzle/Ass3.asset rename to Assets/Data/Puzzles/Quarry/ExampleAssPuzzle/Ass3.asset diff --git a/Assets/Data/Puzzles/ExampleAssPuzzle/Ass3.asset.meta b/Assets/Data/Puzzles/Quarry/ExampleAssPuzzle/Ass3.asset.meta similarity index 100% rename from Assets/Data/Puzzles/ExampleAssPuzzle/Ass3.asset.meta rename to Assets/Data/Puzzles/Quarry/ExampleAssPuzzle/Ass3.asset.meta diff --git a/Assets/Data/Puzzles/ExampleAssPuzzle/Head.asset b/Assets/Data/Puzzles/Quarry/ExampleAssPuzzle/Head.asset similarity index 100% rename from Assets/Data/Puzzles/ExampleAssPuzzle/Head.asset rename to Assets/Data/Puzzles/Quarry/ExampleAssPuzzle/Head.asset diff --git a/Assets/Data/Puzzles/ExampleAssPuzzle/Head.asset.meta b/Assets/Data/Puzzles/Quarry/ExampleAssPuzzle/Head.asset.meta similarity index 100% rename from Assets/Data/Puzzles/ExampleAssPuzzle/Head.asset.meta rename to Assets/Data/Puzzles/Quarry/ExampleAssPuzzle/Head.asset.meta diff --git a/Assets/Data/Puzzles/FootballBirdPuzzle.meta b/Assets/Data/Puzzles/Quarry/FootballBirdPuzzle.meta similarity index 100% rename from Assets/Data/Puzzles/FootballBirdPuzzle.meta rename to Assets/Data/Puzzles/Quarry/FootballBirdPuzzle.meta diff --git a/Assets/Data/Puzzles/FootballBirdPuzzle/InteractWithBallTree.asset b/Assets/Data/Puzzles/Quarry/FootballBirdPuzzle/InteractWithBallTree.asset similarity index 100% rename from Assets/Data/Puzzles/FootballBirdPuzzle/InteractWithBallTree.asset rename to Assets/Data/Puzzles/Quarry/FootballBirdPuzzle/InteractWithBallTree.asset diff --git a/Assets/Data/Puzzles/FootballBirdPuzzle/InteractWithBallTree.asset.meta b/Assets/Data/Puzzles/Quarry/FootballBirdPuzzle/InteractWithBallTree.asset.meta similarity index 100% rename from Assets/Data/Puzzles/FootballBirdPuzzle/InteractWithBallTree.asset.meta rename to Assets/Data/Puzzles/Quarry/FootballBirdPuzzle/InteractWithBallTree.asset.meta diff --git a/Assets/Data/Puzzles/FootballBirdPuzzle/PickUpFootball.asset b/Assets/Data/Puzzles/Quarry/FootballBirdPuzzle/PickUpFootball.asset similarity index 100% rename from Assets/Data/Puzzles/FootballBirdPuzzle/PickUpFootball.asset rename to Assets/Data/Puzzles/Quarry/FootballBirdPuzzle/PickUpFootball.asset diff --git a/Assets/Data/Puzzles/FootballBirdPuzzle/PickUpFootball.asset.meta b/Assets/Data/Puzzles/Quarry/FootballBirdPuzzle/PickUpFootball.asset.meta similarity index 100% rename from Assets/Data/Puzzles/FootballBirdPuzzle/PickUpFootball.asset.meta rename to Assets/Data/Puzzles/Quarry/FootballBirdPuzzle/PickUpFootball.asset.meta diff --git a/Assets/Data/Puzzles/FootballBirdPuzzle/PlaceFootballInLureSpotA.asset b/Assets/Data/Puzzles/Quarry/FootballBirdPuzzle/PlaceFootballInLureSpotA.asset similarity index 100% rename from Assets/Data/Puzzles/FootballBirdPuzzle/PlaceFootballInLureSpotA.asset rename to Assets/Data/Puzzles/Quarry/FootballBirdPuzzle/PlaceFootballInLureSpotA.asset diff --git a/Assets/Data/Puzzles/FootballBirdPuzzle/PlaceFootballInLureSpotA.asset.meta b/Assets/Data/Puzzles/Quarry/FootballBirdPuzzle/PlaceFootballInLureSpotA.asset.meta similarity index 100% rename from Assets/Data/Puzzles/FootballBirdPuzzle/PlaceFootballInLureSpotA.asset.meta rename to Assets/Data/Puzzles/Quarry/FootballBirdPuzzle/PlaceFootballInLureSpotA.asset.meta diff --git a/Assets/Data/Puzzles/HammerBirdPuzzle.meta b/Assets/Data/Puzzles/Quarry/HammerBirdPuzzle.meta similarity index 100% rename from Assets/Data/Puzzles/HammerBirdPuzzle.meta rename to Assets/Data/Puzzles/Quarry/HammerBirdPuzzle.meta diff --git a/Assets/Data/Puzzles/HammerBirdPuzzle/InteractWLawnMower.asset b/Assets/Data/Puzzles/Quarry/HammerBirdPuzzle/InteractWLawnMower.asset similarity index 100% rename from Assets/Data/Puzzles/HammerBirdPuzzle/InteractWLawnMower.asset rename to Assets/Data/Puzzles/Quarry/HammerBirdPuzzle/InteractWLawnMower.asset diff --git a/Assets/Data/Puzzles/HammerBirdPuzzle/InteractWLawnMower.asset.meta b/Assets/Data/Puzzles/Quarry/HammerBirdPuzzle/InteractWLawnMower.asset.meta similarity index 100% rename from Assets/Data/Puzzles/HammerBirdPuzzle/InteractWLawnMower.asset.meta rename to Assets/Data/Puzzles/Quarry/HammerBirdPuzzle/InteractWLawnMower.asset.meta diff --git a/Assets/Data/Puzzles/HammerBirdPuzzle/PickUpNails.asset b/Assets/Data/Puzzles/Quarry/HammerBirdPuzzle/PickUpNails.asset similarity index 100% rename from Assets/Data/Puzzles/HammerBirdPuzzle/PickUpNails.asset rename to Assets/Data/Puzzles/Quarry/HammerBirdPuzzle/PickUpNails.asset diff --git a/Assets/Data/Puzzles/HammerBirdPuzzle/PickUpNails.asset.meta b/Assets/Data/Puzzles/Quarry/HammerBirdPuzzle/PickUpNails.asset.meta similarity index 100% rename from Assets/Data/Puzzles/HammerBirdPuzzle/PickUpNails.asset.meta rename to Assets/Data/Puzzles/Quarry/HammerBirdPuzzle/PickUpNails.asset.meta diff --git a/Assets/Data/Puzzles/HammerBirdPuzzle/PlaceNailsinlureC.asset b/Assets/Data/Puzzles/Quarry/HammerBirdPuzzle/PlaceNailsinlureC.asset similarity index 100% rename from Assets/Data/Puzzles/HammerBirdPuzzle/PlaceNailsinlureC.asset rename to Assets/Data/Puzzles/Quarry/HammerBirdPuzzle/PlaceNailsinlureC.asset diff --git a/Assets/Data/Puzzles/HammerBirdPuzzle/PlaceNailsinlureC.asset.meta b/Assets/Data/Puzzles/Quarry/HammerBirdPuzzle/PlaceNailsinlureC.asset.meta similarity index 100% rename from Assets/Data/Puzzles/HammerBirdPuzzle/PlaceNailsinlureC.asset.meta rename to Assets/Data/Puzzles/Quarry/HammerBirdPuzzle/PlaceNailsinlureC.asset.meta diff --git a/Assets/Data/Puzzles/Quarry/Quarry.asset b/Assets/Data/Puzzles/Quarry/Quarry.asset new file mode 100644 index 00000000..bb5746f9 --- /dev/null +++ b/Assets/Data/Puzzles/Quarry/Quarry.asset @@ -0,0 +1,37 @@ +%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: 0a79780a5a0d498084afd737d4515e3b, type: 3} + m_Name: Quarry + m_EditorClassIdentifier: AppleHillsScripts::PuzzleS.PuzzleLevelDataSO + levelId: Quarry + displayName: Quarry + allSteps: + - {fileID: 11400000, guid: d0851a7610551104fa285c0748549d90, type: 2} + - {fileID: 11400000, guid: ed967c44f3a8b914aabc1539f774efc7, type: 2} + - {fileID: 11400000, guid: 0b13ff4f31443b74281b13e0eef865c2, type: 2} + - {fileID: 11400000, guid: a84cbe9804e13f74e857c55d90cc10d1, type: 2} + - {fileID: 11400000, guid: 13b0c411066f85a41ba40c3bbbc281ed, type: 2} + - {fileID: 11400000, guid: 9de0c57af6191384e96e2ba7c04a3d0d, type: 2} + - {fileID: 11400000, guid: 8ac614a698631554ab8ac39aed04a189, type: 2} + - {fileID: 11400000, guid: 6386246caab8faa40b2da221d9ab9b8a, type: 2} + - {fileID: 11400000, guid: 0fb0ab2b55d93a24685e9f6651adcf30, type: 2} + - {fileID: 11400000, guid: ea383d1dee861f54c9a1d4f32a2f6afc, type: 2} + - {fileID: 11400000, guid: f9da68caaae2a244885a13cf2e2e45c0, type: 2} + - {fileID: 11400000, guid: 28848561ff31fe24ea9f8590dee0bf8f, type: 2} + - {fileID: 11400000, guid: 5700dd3bf16fa9e4aa9905379118d1bd, type: 2} + initialSteps: + - {fileID: 11400000, guid: d0851a7610551104fa285c0748549d90, type: 2} + - {fileID: 11400000, guid: ed967c44f3a8b914aabc1539f774efc7, type: 2} + - {fileID: 11400000, guid: 0b13ff4f31443b74281b13e0eef865c2, type: 2} + - {fileID: 11400000, guid: a84cbe9804e13f74e857c55d90cc10d1, type: 2} + - {fileID: 11400000, guid: 8ac614a698631554ab8ac39aed04a189, type: 2} + - {fileID: 11400000, guid: ea383d1dee861f54c9a1d4f32a2f6afc, type: 2} diff --git a/Assets/Data/Puzzles/Quarry/Quarry.asset.meta b/Assets/Data/Puzzles/Quarry/Quarry.asset.meta new file mode 100644 index 00000000..fba4d3b7 --- /dev/null +++ b/Assets/Data/Puzzles/Quarry/Quarry.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d28c589c05c122f449a8b34e696cda53 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Data/Puzzles/SoundBirdPuzzle.meta b/Assets/Data/Puzzles/Quarry/SoundBirdPuzzle.meta similarity index 100% rename from Assets/Data/Puzzles/SoundBirdPuzzle.meta rename to Assets/Data/Puzzles/Quarry/SoundBirdPuzzle.meta diff --git a/Assets/Data/Puzzles/SoundBirdPuzzle/HeadbandPickup.asset b/Assets/Data/Puzzles/Quarry/SoundBirdPuzzle/HeadbandPickup.asset similarity index 100% rename from Assets/Data/Puzzles/SoundBirdPuzzle/HeadbandPickup.asset rename to Assets/Data/Puzzles/Quarry/SoundBirdPuzzle/HeadbandPickup.asset diff --git a/Assets/Data/Puzzles/SoundBirdPuzzle/HeadbandPickup.asset.meta b/Assets/Data/Puzzles/Quarry/SoundBirdPuzzle/HeadbandPickup.asset.meta similarity index 100% rename from Assets/Data/Puzzles/SoundBirdPuzzle/HeadbandPickup.asset.meta rename to Assets/Data/Puzzles/Quarry/SoundBirdPuzzle/HeadbandPickup.asset.meta diff --git a/Assets/Editor/PuzzleAssetProcessor.cs b/Assets/Editor/PuzzleAssetProcessor.cs new file mode 100644 index 00000000..a3fdb7d4 --- /dev/null +++ b/Assets/Editor/PuzzleAssetProcessor.cs @@ -0,0 +1,321 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEditor; +using UnityEngine; +using UnityEditor.AddressableAssets; +using UnityEditor.AddressableAssets.Settings; + +namespace PuzzleS.Editor +{ + /// + /// Handles asset post-processing for puzzle step data. + /// Automatically builds level data from folder structure. + /// + public class PuzzleAssetProcessor : AssetPostprocessor + { + // Base path for puzzle data + private const string PuzzleDataBasePath = "Assets/Data/Puzzles"; + + /// + /// Called after assets have been imported, deleted, or moved. + /// + private static void OnPostprocessAllAssets( + string[] importedAssets, + string[] deletedAssets, + string[] movedAssets, + string[] movedFromAssetPaths) + { + bool puzzleAssetsChanged = false; + + // Check for changes to puzzle step assets + foreach (string assetPath in importedAssets.Concat(movedAssets)) + { + if (IsPuzzleAssetPath(assetPath) && Path.GetExtension(assetPath) == ".asset") + { + var asset = AssetDatabase.LoadAssetAtPath(assetPath); + + if (asset is PuzzleStepSO) + { + // Find which level this step belongs to + string assetDir = Path.GetDirectoryName(assetPath); + string levelFolderPath = FindLevelFolder(assetDir); + + if (!string.IsNullOrEmpty(levelFolderPath)) + { + ProcessPuzzleLevelFolder(levelFolderPath); + puzzleAssetsChanged = true; + } + } + } + } + + // Check for changes to puzzle folder structure + foreach (string assetPath in deletedAssets.Concat(movedFromAssetPaths)) + { + if (IsPuzzleAssetPath(assetPath)) + { + // Extract parent folders for processing + string assetDir = Path.GetDirectoryName(assetPath); + string levelFolderPath = FindLevelFolder(assetDir); + + if (!string.IsNullOrEmpty(levelFolderPath) && Directory.Exists(levelFolderPath)) + { + ProcessPuzzleLevelFolder(levelFolderPath); + puzzleAssetsChanged = true; + } + } + } + + if (puzzleAssetsChanged) + { + // Make sure changes are saved + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + + Debug.Log("[PuzzleProcessor] Puzzle data processing complete"); + } + } + + /// + /// Checks if an asset path is within the puzzle data folder structure + /// + private static bool IsPuzzleAssetPath(string assetPath) + { + // Unity's AssetDatabase always uses forward slashes, so we need to normalize for comparison + string normalizedPath = assetPath.Replace('\\', '/'); + string normalizedBasePath = PuzzleDataBasePath; + + return normalizedPath.StartsWith(normalizedBasePath) && + !normalizedPath.Contains("/.") && // Skip hidden folders + !Path.GetExtension(normalizedPath).Equals(".meta"); + } + + /// + /// Find the level folder that contains this asset path + /// Assumes level folders are direct children of PuzzleDataBasePath + /// + private static string FindLevelFolder(string assetPath) + { + if (string.IsNullOrEmpty(assetPath)) return null; + + // Unity's AssetDatabase always uses forward slashes + string normalizedPath = assetPath.Replace('\\', '/'); + + if (!normalizedPath.StartsWith(PuzzleDataBasePath)) + return null; + + // Get the relative path from the base path + string relativePath = normalizedPath.Substring(PuzzleDataBasePath.Length); + if (relativePath.StartsWith("/")) + relativePath = relativePath.Substring(1); + + // Split into path components and get the first folder (level name) + string[] pathComponents = relativePath.Split('/'); + + // First component after PuzzleDataBasePath should be the level name + if (pathComponents.Length > 0) + { + // Use proper path joining with Unity's forward slash convention + return PuzzleDataBasePath + "/" + pathComponents[0]; + } + + return null; + } + + /// + /// Process a level folder to build/update a PuzzleLevelDataSO with all puzzle steps + /// + private static void ProcessPuzzleLevelFolder(string folderPath) + { + if (!Directory.Exists(folderPath)) return; + + // Get level name from folder name + string levelName = Path.GetFileName(folderPath); + if (string.IsNullOrEmpty(levelName)) return; + + // Find all puzzle step assets in this level (including subfolders) + var stepAssets = FindAssetsOfTypeRecursive(folderPath); + if (stepAssets.Count == 0) return; + + // Get or create level data asset + var levelDataAsset = GetOrCreateLevelDataAsset(folderPath, levelName); + if (levelDataAsset == null) return; + + // Update level data + levelDataAsset.levelId = levelName; + levelDataAsset.displayName = levelName; // Default display name matches folder name + levelDataAsset.allSteps = stepAssets; + + // Pre-process initial steps (those with no dependencies) + levelDataAsset.initialSteps = FindInitialSteps(stepAssets); + + // Pre-process dependencies (which steps are required by each step) + PrecomputeDependencies(levelDataAsset); + + // Mark as dirty and save + EditorUtility.SetDirty(levelDataAsset); + + // Setup addressables + SetupAddressableAsset(AssetDatabase.GetAssetPath(levelDataAsset), $"Puzzles/{levelName}"); + + Debug.Log($"[PuzzleProcessor] Processed level: {levelName} with {stepAssets.Count} steps"); + } + + /// + /// Find all assets of a specific type in a folder and its subfolders + /// + private static List FindAssetsOfTypeRecursive(string folderPath) where T : ScriptableObject + { + var result = new List(); + + if (!Directory.Exists(folderPath)) return result; + + var guids = AssetDatabase.FindAssets("t:" + typeof(T).Name, new[] { folderPath }); + foreach (var guid in guids) + { + string assetPath = AssetDatabase.GUIDToAssetPath(guid); + var asset = AssetDatabase.LoadAssetAtPath(assetPath); + if (asset != null) + { + result.Add(asset); + } + } + + return result; + } + + /// + /// Get existing level data asset or create a new one + /// + private static PuzzleLevelDataSO GetOrCreateLevelDataAsset(string folderPath, string levelName) + { + // Check for existing level data asset + string levelDataAssetPath = $"{folderPath}/{levelName}.asset"; + var levelDataAsset = AssetDatabase.LoadAssetAtPath(levelDataAssetPath); + + if (levelDataAsset == null) + { + // Create new level data asset + levelDataAsset = ScriptableObject.CreateInstance(); + AssetDatabase.CreateAsset(levelDataAsset, levelDataAssetPath); + Debug.Log($"[PuzzleProcessor] Created new level data asset: {levelDataAssetPath}"); + } + + return levelDataAsset; + } + + /// + /// Find steps that have no dependencies (initial steps) + /// + private static List FindInitialSteps(List steps) + { + var initialSteps = new List(); + var dependentSteps = new HashSet(); + + // Find all steps that are dependencies of other steps + foreach (var step in steps) + { + if (step == null) continue; + + foreach (var unlockStep in step.unlocks) + { + if (unlockStep != null && steps.Contains(unlockStep)) + { + dependentSteps.Add(unlockStep); + } + } + } + + // Initial steps are those not depended on by any other step + foreach (var step in steps) + { + if (step != null && !dependentSteps.Contains(step)) + { + initialSteps.Add(step); + } + } + + return initialSteps; + } + + /// + /// Pre-compute dependency information for a puzzle level + /// + private static void PrecomputeDependencies(PuzzleLevelDataSO levelDataAsset) + { + levelDataAsset.stepDependencies.Clear(); + + // Build reverse dependency map (which steps are required by each step) + var reverseDependencies = new Dictionary>(); + + foreach (var step in levelDataAsset.allSteps) + { + if (step == null) continue; + + foreach (var unlockStep in step.unlocks) + { + if (unlockStep == null) continue; + + if (!reverseDependencies.ContainsKey(unlockStep.stepId)) + { + reverseDependencies[unlockStep.stepId] = new List(); + } + + reverseDependencies[unlockStep.stepId].Add(step.stepId); + } + } + + // Convert to serialized form + foreach (var entry in reverseDependencies) + { + levelDataAsset.stepDependencies[entry.Key] = entry.Value.ToArray(); + } + } + + /// + /// Set up an asset as an Addressable with the specified address + /// + private static void SetupAddressableAsset(string assetPath, string address) + { + if (!File.Exists(assetPath)) return; + + // Get Addressable settings + var settings = AddressableAssetSettingsDefaultObject.Settings; + if (settings == null) + { + Debug.LogWarning("[PuzzleProcessor] Addressable Asset Settings not found. Make sure Addressables is set up in your project."); + return; + } + + // Get default group + var defaultGroup = settings.DefaultGroup; + if (defaultGroup == null) + { + Debug.LogWarning("[PuzzleProcessor] Default Addressable Asset Group not found."); + return; + } + + // Get asset GUID + var guid = AssetDatabase.AssetPathToGUID(assetPath); + + // Check if entry exists + var existingEntry = settings.FindAssetEntry(guid); + if (existingEntry != null) + { + // Update existing entry + existingEntry.SetAddress(address); + } + else + { + // Create new entry + settings.CreateOrMoveEntry(guid, defaultGroup); + var newEntry = settings.FindAssetEntry(guid); + if (newEntry != null) + { + newEntry.SetAddress(address); + } + } + } + } +} diff --git a/Assets/Editor/PuzzleAssetProcessor.cs.meta b/Assets/Editor/PuzzleAssetProcessor.cs.meta new file mode 100644 index 00000000..1e34c156 --- /dev/null +++ b/Assets/Editor/PuzzleAssetProcessor.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5097566c60d341dbb6f2bf5175b048cb +timeCreated: 1760532147 \ No newline at end of file diff --git a/Assets/Scripts/PuzzleS/ObjectiveStepBehaviour.cs b/Assets/Scripts/PuzzleS/ObjectiveStepBehaviour.cs index e11e168d..e4687273 100644 --- a/Assets/Scripts/PuzzleS/ObjectiveStepBehaviour.cs +++ b/Assets/Scripts/PuzzleS/ObjectiveStepBehaviour.cs @@ -1,10 +1,7 @@ using Input; using Interactions; using UnityEngine; -using System; -using AppleHills.Core.Settings; using Core; -using UnityEngine.Serialization; namespace PuzzleS { @@ -48,12 +45,13 @@ namespace PuzzleS _interactable.interactionStarted.AddListener(OnInteractionStarted); _interactable.interactionComplete.AddListener(OnInteractionComplete); } + } + + void Start() + { + // Register with PuzzleManager, regardless of enabled/disabled state + // This happens in Start instead of OnEnable to ensure PuzzleManager has loaded level data PuzzleManager.Instance?.RegisterStepBehaviour(this); - // Check if this step was already unlocked - if (stepData != null && PuzzleManager.Instance != null && PuzzleManager.Instance.IsStepUnlocked(stepData)) - { - UnlockStep(); - } } void OnDestroy() @@ -73,7 +71,7 @@ namespace PuzzleS public void UpdateProximityState(ProximityState newState) { if (_currentProximityState == newState) return; - if (_indicator == null) return; + if (!_isUnlocked) return; // Determine state changes and call appropriate methods if (newState == ProximityState.Close) @@ -205,10 +203,12 @@ namespace PuzzleS /// public void UnlockStep() { + if (_isUnlocked) return; + _isUnlocked = true; Logging.Debug($"[Puzzles] Step unlocked: {stepData?.stepId} on {gameObject.name}"); - // Show indicator if enabled in settings + // Show indicator if available if (puzzleIndicator != null) { // Try to get the IPuzzlePrompt component from the spawned indicator @@ -264,6 +264,8 @@ namespace PuzzleS /// public void LockStep() { + if (!_isUnlocked) return; + _isUnlocked = false; Logging.Debug($"[Puzzles] Step locked: {stepData?.stepId} on {gameObject.name}"); @@ -297,12 +299,17 @@ namespace PuzzleS private void OnInteractionComplete(bool success) { if (!_isUnlocked) return; - if (success) + if (success && !_isCompleted) { Logging.Debug($"[Puzzles] Step interacted: {stepData?.stepId} on {gameObject.name}"); _isCompleted = true; PuzzleManager.Instance?.MarkPuzzleStepCompleted(stepData); - Destroy(puzzleIndicator); + + if (puzzleIndicator != null) + { + Destroy(puzzleIndicator); + _indicator = null; + } } } @@ -323,7 +330,7 @@ namespace PuzzleS // Draw threshold circle Gizmos.color = Color.cyan; - Gizmos.DrawWireSphere(transform.position, promptRange / 2f); + Gizmos.DrawWireSphere(transform.position, promptRange); } } } diff --git a/Assets/Scripts/PuzzleS/PuzzleChainSO.cs b/Assets/Scripts/PuzzleS/PuzzleChainSO.cs new file mode 100644 index 00000000..3340dfa7 --- /dev/null +++ b/Assets/Scripts/PuzzleS/PuzzleChainSO.cs @@ -0,0 +1,103 @@ +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.AddressableAssets; + +namespace PuzzleS +{ + /// + /// Represents a complete chain of puzzle steps that form a logical sequence. + /// This is automatically generated from folder structure during asset import. + /// + [CreateAssetMenu(fileName = "PuzzleChain", menuName = "AppleHills/Items & Puzzles/PuzzleChain")] + public class PuzzleChainSO : ScriptableObject + { + /// + /// Unique identifier for this puzzle chain, automatically set to match folder name + /// + public string chainId; + + /// + /// Display name for this chain + /// + public string displayName; + + /// + /// Description of this puzzle chain + /// + [TextArea] + public string description; + + /// + /// All steps that belong to this puzzle chain + /// + public List allSteps = new List(); + + /// + /// Initial steps that should be unlocked when the puzzle chain starts + /// (steps with no dependencies) + /// + public List initialSteps = new List(); + + /// + /// Optional requirement for this entire chain to be activated + /// If not null, this chain requires the specified chain to be completed first + /// + public PuzzleChainSO requiredChain; + + /// + /// Pre-processed dependency data built at edit time. + /// Maps step IDs to arrays of dependency step IDs + /// + [HideInInspector] + public Dictionary stepDependencies = new Dictionary(); + + /// + /// Gets all steps that will be unlocked by completing the given step + /// + public List GetUnlockedSteps(string stepId) + { + var result = new List(); + foreach (var step in allSteps) + { + if (step.stepId == stepId && step != null) + { + return step.unlocks; + } + } + return result; + } + + /// + /// Gets all steps that will be unlocked by completing the given step + /// + public List GetUnlockedSteps(PuzzleStepSO completedStep) + { + return completedStep != null ? completedStep.unlocks : new List(); + } + + /// + /// Check if this step is an initial step (no dependencies) + /// + public bool IsInitialStep(PuzzleStepSO step) + { + return step != null && initialSteps.Contains(step); + } + + /// + /// Check if all steps in this chain are completed + /// + public bool IsChainComplete(HashSet completedSteps) + { + if (completedSteps == null) return false; + + foreach (var step in allSteps) + { + if (step != null && !completedSteps.Contains(step)) + { + return false; + } + } + return true; + } + } +} diff --git a/Assets/Scripts/PuzzleS/PuzzleChainSO.cs.meta b/Assets/Scripts/PuzzleS/PuzzleChainSO.cs.meta new file mode 100644 index 00000000..62518a80 --- /dev/null +++ b/Assets/Scripts/PuzzleS/PuzzleChainSO.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 58109a40325e47f2a8a3b9264d8938dd +timeCreated: 1760532067 \ No newline at end of file diff --git a/Assets/Scripts/PuzzleS/PuzzleLevelDataSO.cs b/Assets/Scripts/PuzzleS/PuzzleLevelDataSO.cs new file mode 100644 index 00000000..b5757cb3 --- /dev/null +++ b/Assets/Scripts/PuzzleS/PuzzleLevelDataSO.cs @@ -0,0 +1,75 @@ +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.AddressableAssets; + +namespace PuzzleS +{ + /// + /// Represents all puzzle steps in a level. + /// This is automatically generated from folder structure during asset import. + /// + [CreateAssetMenu(fileName = "LevelPuzzleData", menuName = "AppleHills/Items & Puzzles/LevelPuzzleData")] + public class PuzzleLevelDataSO : ScriptableObject + { + /// + /// Unique identifier for this level, automatically set to match folder name + /// + public string levelId; + + /// + /// Display name for this level + /// + public string displayName; + + /// + /// All puzzle steps in this level + /// + public List allSteps = new List(); + + /// + /// Steps that should be unlocked at level start (no dependencies) + /// + [HideInInspector] + public List initialSteps = new List(); + + /// + /// Pre-processed dependency data built at edit time. + /// Maps step IDs to arrays of dependency step IDs (which steps are required by each step) + /// + [HideInInspector] + public Dictionary stepDependencies = new Dictionary(); + + /// + /// Check if all steps in the level are complete + /// + public bool IsLevelComplete(HashSet completedSteps) + { + if (completedSteps == null) return false; + + foreach (var step in allSteps) + { + if (step != null && !completedSteps.Contains(step)) + { + return false; + } + } + return true; + } + + /// + /// Gets all steps that will be unlocked by completing the given step + /// + public List GetUnlockedSteps(PuzzleStepSO completedStep) + { + return completedStep != null ? completedStep.unlocks : new List(); + } + + /// + /// Check if this step is an initial step (no dependencies) + /// + public bool IsInitialStep(PuzzleStepSO step) + { + return step != null && initialSteps.Contains(step); + } + } +} diff --git a/Assets/Scripts/PuzzleS/PuzzleLevelDataSO.cs.meta b/Assets/Scripts/PuzzleS/PuzzleLevelDataSO.cs.meta new file mode 100644 index 00000000..0fde9380 --- /dev/null +++ b/Assets/Scripts/PuzzleS/PuzzleLevelDataSO.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0a79780a5a0d498084afd737d4515e3b +timeCreated: 1760532084 \ No newline at end of file diff --git a/Assets/Scripts/PuzzleS/PuzzleManager.cs b/Assets/Scripts/PuzzleS/PuzzleManager.cs index 61e1a2b3..6b184cbd 100644 --- a/Assets/Scripts/PuzzleS/PuzzleManager.cs +++ b/Assets/Scripts/PuzzleS/PuzzleManager.cs @@ -5,7 +5,9 @@ using System.Linq; using UnityEngine; using UnityEngine.SceneManagement; using AppleHills.Core.Settings; -using Core; // Added for IInteractionSettings +using Core; +using UnityEngine.AddressableAssets; +using UnityEngine.ResourceManagement.AsyncOperations; namespace PuzzleS { @@ -26,6 +28,11 @@ namespace PuzzleS // Settings reference private IInteractionSettings _interactionSettings; + // Current level puzzle data + private PuzzleLevelDataSO _currentLevelData; + private AsyncOperationHandle _levelDataLoadOperation; + private bool _isLoadingLevelData = false; + /// /// Singleton instance of the PuzzleManager. /// @@ -50,22 +57,24 @@ namespace PuzzleS // Events to notify about step lifecycle public event Action OnStepCompleted; public event Action OnStepUnlocked; + public event Action OnLevelDataLoaded; + public event Action OnAllPuzzlesComplete; private HashSet _completedSteps = new HashSet(); private HashSet _unlockedSteps = new HashSet(); // Registration for ObjectiveStepBehaviour private Dictionary _stepBehaviours = new Dictionary(); - // Runtime dependency graph - private Dictionary> _runtimeDependencies = new Dictionary>(); void Awake() { _instance = this; // DontDestroyOnLoad(gameObject); - SceneManager.sceneLoaded += OnSceneLoaded; - + // Initialize settings reference _interactionSettings = GameManager.GetSettingsObject(); + + // Subscribe to scene manager events + SceneManagerService.Instance.SceneLoadCompleted += OnSceneLoadCompleted; } void Start() @@ -75,22 +84,44 @@ namespace PuzzleS // Start proximity check coroutine StartProximityChecks(); + + // Load puzzle data for the current scene if not already loading + if (_currentLevelData == null && !_isLoadingLevelData) + { + LoadPuzzleDataForCurrentScene(); + } } void OnDestroy() { - SceneManager.sceneLoaded -= OnSceneLoaded; StopProximityChecks(); + + // Unsubscribe from scene manager events + if (SceneManagerService.Instance != null) + { + SceneManagerService.Instance.SceneLoadCompleted -= OnSceneLoadCompleted; + } + + // Release addressable handle if needed + if (_levelDataLoadOperation.IsValid()) + { + Addressables.Release(_levelDataLoadOperation); + } } - private void OnSceneLoaded(Scene scene, LoadSceneMode mode) + /// + /// Called when a scene is loaded + /// + private void OnSceneLoadCompleted(string sceneName) { - SceneManager.sceneLoaded -= OnSceneLoaded; + // Skip for non-gameplay scenes + if (sceneName == "BootstrapScene" || string.IsNullOrEmpty(sceneName)) + { + return; + } - Logging.Debug("[MDPI] OnSceneLoaded"); - _runtimeDependencies.Clear(); - BuildRuntimeDependencies(); - UnlockInitialSteps(); + Logging.Debug($"[Puzzles] Scene loaded: {sceneName}, loading puzzle data"); + LoadPuzzleDataForCurrentScene(); // Find player transform again in case it changed with scene load _playerTransform = GameObject.FindGameObjectWithTag("Player")?.transform; @@ -99,6 +130,60 @@ namespace PuzzleS StartProximityChecks(); } + /// + /// Load puzzle data for the current scene + /// + private void LoadPuzzleDataForCurrentScene() + { + string currentScene = SceneManagerService.Instance.CurrentGameplayScene; + if (string.IsNullOrEmpty(currentScene)) + { + Logging.Warning("[Puzzles] Cannot load puzzle data: Current scene name is empty"); + return; + } + + _isLoadingLevelData = true; + string addressablePath = $"Puzzles/{currentScene}"; + + Logging.Debug($"[Puzzles] Loading puzzle data from addressable: {addressablePath}"); + + // Release previous handle if needed + if (_levelDataLoadOperation.IsValid()) + { + Addressables.Release(_levelDataLoadOperation); + } + + // Load the level data asset + _levelDataLoadOperation = Addressables.LoadAssetAsync(addressablePath); + _levelDataLoadOperation.Completed += handle => + { + _isLoadingLevelData = false; + + if (handle.Status == AsyncOperationStatus.Succeeded) + { + _currentLevelData = handle.Result; + Logging.Debug($"[Puzzles] Loaded level data: {_currentLevelData.levelId} with {_currentLevelData.allSteps.Count} steps"); + + // Reset state + _completedSteps.Clear(); + _unlockedSteps.Clear(); + + // Unlock initial steps + UnlockInitialSteps(); + + // Update existing ObjectiveStepBehaviours with the current state + UpdateRegisteredStepStates(); + + // Notify listeners + OnLevelDataLoaded?.Invoke(_currentLevelData); + } + else + { + Logging.Warning($"[Puzzles] Failed to load puzzle data for {currentScene}: {handle.OperationException?.Message}"); + } + }; + } + /// /// Start the proximity check coroutine. /// @@ -138,7 +223,7 @@ namespace PuzzleS foreach (var kvp in _stepBehaviours) { if (kvp.Value == null) continue; - if (IsPuzzleStepCompleted(kvp.Value.stepData.stepId)) continue; + if (IsPuzzleStepCompleted(kvp.Key.stepId)) continue; float distance = Vector3.Distance(_playerTransform.position, kvp.Value.transform.position); @@ -164,18 +249,48 @@ namespace PuzzleS public void RegisterStepBehaviour(ObjectiveStepBehaviour behaviour) { if (behaviour?.stepData == null) return; + if (!_stepBehaviours.ContainsKey(behaviour.stepData)) { _stepBehaviours.Add(behaviour.stepData, behaviour); - _runtimeDependencies.Clear(); - foreach (var step in _stepBehaviours.Values) - { - step.LockStep(); - } - _unlockedSteps.Clear(); - BuildRuntimeDependencies(); - UnlockInitialSteps(); Logging.Debug($"[Puzzles] Registered step: {behaviour.stepData.stepId} on {behaviour.gameObject.name}"); + + // Immediately set the correct state based on current puzzle state + UpdateStepState(behaviour); + } + } + + /// + /// Updates a step's state based on the current puzzle state. + /// + private void UpdateStepState(ObjectiveStepBehaviour behaviour) + { + if (behaviour?.stepData == null) return; + + // If step is already completed, ignore + if (_completedSteps.Contains(behaviour.stepData)) + return; + + // If step is already unlocked, update the behaviour + if (_unlockedSteps.Contains(behaviour.stepData)) + { + behaviour.UnlockStep(); + } + else + { + // Make sure it's locked + behaviour.LockStep(); + } + } + + /// + /// Updates the states of all registered step behaviours based on current puzzle state. + /// + private void UpdateRegisteredStepStates() + { + foreach (var kvp in _stepBehaviours) + { + UpdateStepState(kvp.Value); } } @@ -191,52 +306,19 @@ namespace PuzzleS } /// - /// Builds the runtime dependency graph for all registered steps. - /// - private void BuildRuntimeDependencies() - { - _runtimeDependencies = PuzzleGraphUtility.BuildDependencyGraph(_stepBehaviours.Keys); - foreach (var step in _runtimeDependencies.Keys) - { - foreach (var dep in _runtimeDependencies[step]) - { - Logging.Debug($"[Puzzles] Step {step.stepId} depends on {dep.stepId}"); - } - } - Logging.Debug($"[Puzzles] Runtime dependencies built. Total steps: {_stepBehaviours.Count}"); - } - - /// - /// Unlocks all initial steps (those with no dependencies) and any steps whose dependencies are already met. + /// Unlocks all initial steps (those with no dependencies) /// private void UnlockInitialSteps() { - // First, unlock all steps with no dependencies (initial steps) - var initialSteps = PuzzleGraphUtility.FindInitialSteps(_runtimeDependencies); - foreach (var step in initialSteps) + if (_currentLevelData == null) return; + + // Unlock initial steps + foreach (var step in _currentLevelData.initialSteps) { - Logging.Debug($"[Puzzles] Initial step unlocked: {step.stepId}"); UnlockStep(step); } - // Keep trying to unlock steps as long as we're making progress - bool madeProgress; - do - { - madeProgress = false; - - // Check all steps that haven't been unlocked yet - foreach (var step in _runtimeDependencies.Keys.Where(s => !_unlockedSteps.Contains(s))) - { - // Check if all dependencies have been completed - if (AreRuntimeDependenciesMet(step)) - { - Logging.Debug($"[Puzzles] Chain step unlocked: {step.stepId}"); - UnlockStep(step); - madeProgress = true; - } - } - } while (madeProgress); + Logging.Debug($"[Puzzles] Unlocked {_unlockedSteps.Count} initial steps"); } /// @@ -246,24 +328,29 @@ namespace PuzzleS public void MarkPuzzleStepCompleted(PuzzleStepSO step) { if (_completedSteps.Contains(step)) return; + if (_currentLevelData == null) return; + _completedSteps.Add(step); Logging.Debug($"[Puzzles] Step completed: {step.stepId}"); // Broadcast completion OnStepCompleted?.Invoke(step); - foreach (var unlock in step.unlocks) + // Unlock steps that are unlocked by this step + foreach (var unlockStep in _currentLevelData.GetUnlockedSteps(step)) { - if (AreRuntimeDependenciesMet(unlock)) + if (AreStepDependenciesMet(unlockStep)) { - Logging.Debug($"[Puzzles] Unlocking step {unlock.stepId} after completing {step.stepId}"); - UnlockStep(unlock); + Logging.Debug($"[Puzzles] Unlocking step {unlockStep.stepId} after completing {step.stepId}"); + UnlockStep(unlockStep); } else { - Logging.Debug($"[Puzzles] Step {unlock.stepId} not unlocked yet, waiting for other dependencies"); + Logging.Debug($"[Puzzles] Step {unlockStep.stepId} not unlocked yet, waiting for other dependencies"); } } + + // Check if all puzzle steps are now complete CheckPuzzleCompletion(); } @@ -272,13 +359,33 @@ namespace PuzzleS /// /// The step to check. /// True if all dependencies are met, false otherwise. - private bool AreRuntimeDependenciesMet(PuzzleStepSO step) + private bool AreStepDependenciesMet(PuzzleStepSO step) { - if (!_runtimeDependencies.ContainsKey(step) || _runtimeDependencies[step].Count == 0) return true; - foreach (var dep in _runtimeDependencies[step]) + if (_currentLevelData == null || step == null) return false; + + // If it's an initial step, it has no dependencies + if (_currentLevelData.IsInitialStep(step)) return true; + + // Check if dependencies are met using pre-processed data + if (_currentLevelData.stepDependencies.TryGetValue(step.stepId, out string[] dependencies)) { - if (!_completedSteps.Contains(dep)) return false; + foreach (var depId in dependencies) + { + // Find the dependency step + bool dependencyMet = false; + foreach (var completedStep in _completedSteps) + { + if (completedStep.stepId == depId) + { + dependencyMet = true; + break; + } + } + + if (!dependencyMet) return false; + } } + return true; } @@ -290,6 +397,7 @@ namespace PuzzleS { if (_unlockedSteps.Contains(step)) return; _unlockedSteps.Add(step); + if (_stepBehaviours.TryGetValue(step, out var behaviour)) { behaviour.UnlockStep(); @@ -301,14 +409,18 @@ namespace PuzzleS } /// - /// Checks if the puzzle is complete (all steps finished). + /// Checks if the puzzle is complete (all steps in level finished). /// private void CheckPuzzleCompletion() { - if (_completedSteps.Count == _stepBehaviours.Count) + if (_currentLevelData == null) return; + + if (_currentLevelData.IsLevelComplete(_completedSteps)) { - Logging.Debug("[Puzzles] Puzzle complete! All steps finished."); - // TODO: Fire puzzle complete event or trigger outcome logic + Logging.Debug("[Puzzles] All puzzles complete! Level finished."); + + // Fire level complete event + OnAllPuzzlesComplete?.Invoke(_currentLevelData); } } @@ -317,9 +429,6 @@ namespace PuzzleS /// public bool IsStepUnlocked(PuzzleStepSO step) { - // _runtimeDependencies.Clear(); - // BuildRuntimeDependencies(); - // UnlockInitialSteps(); return _unlockedSteps.Contains(step); } @@ -332,6 +441,14 @@ namespace PuzzleS { return _completedSteps.Any(step => step.stepId == stepId); } + + /// + /// Get the current level puzzle data + /// + public PuzzleLevelDataSO GetCurrentLevelData() + { + return _currentLevelData; + } void OnApplicationQuit() {