diff --git a/Assets/AddressableAssetsData/AssetGroups/Default Local Group.asset b/Assets/AddressableAssetsData/AssetGroups/Default Local Group.asset index a7fb6d32..34a52fdb 100644 --- a/Assets/AddressableAssetsData/AssetGroups/Default Local Group.asset +++ b/Assets/AddressableAssetsData/AssetGroups/Default Local Group.asset @@ -15,6 +15,11 @@ MonoBehaviour: m_GroupName: Default Local Group m_GUID: 6f3207429a65b3e4b83935ac19791077 m_SerializeEntries: + - m_GUID: 1a9d24004b795e147b8544845a7a9ae3 + m_Address: Puzzles/Testing + m_ReadOnly: 0 + m_SerializedLabels: [] + FlaggedDuringContentUpdateRestriction: 0 - m_GUID: d28c589c05c122f449a8b34e696cda53 m_Address: Puzzles/Quarry m_ReadOnly: 0 diff --git a/Assets/Data/Puzzles/Quarry/Quarry.asset b/Assets/Data/Puzzles/Quarry/Quarry.asset index bb5746f9..0ccbac59 100644 --- a/Assets/Data/Puzzles/Quarry/Quarry.asset +++ b/Assets/Data/Puzzles/Quarry/Quarry.asset @@ -17,10 +17,6 @@ MonoBehaviour: 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} @@ -31,7 +27,5 @@ MonoBehaviour: 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/Testing.meta b/Assets/Data/Puzzles/Testing.meta new file mode 100644 index 00000000..f87db99d --- /dev/null +++ b/Assets/Data/Puzzles/Testing.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a9958845b52d0d7468c9c36a6969e9ea +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Data/Puzzles/Quarry/ExampleAssPuzzle.meta b/Assets/Data/Puzzles/Testing/ExampleAssPuzzle.meta similarity index 100% rename from Assets/Data/Puzzles/Quarry/ExampleAssPuzzle.meta rename to Assets/Data/Puzzles/Testing/ExampleAssPuzzle.meta diff --git a/Assets/Data/Puzzles/Quarry/ExampleAssPuzzle/Ass1.asset b/Assets/Data/Puzzles/Testing/ExampleAssPuzzle/Ass1.asset similarity index 100% rename from Assets/Data/Puzzles/Quarry/ExampleAssPuzzle/Ass1.asset rename to Assets/Data/Puzzles/Testing/ExampleAssPuzzle/Ass1.asset diff --git a/Assets/Data/Puzzles/Quarry/ExampleAssPuzzle/Ass1.asset.meta b/Assets/Data/Puzzles/Testing/ExampleAssPuzzle/Ass1.asset.meta similarity index 100% rename from Assets/Data/Puzzles/Quarry/ExampleAssPuzzle/Ass1.asset.meta rename to Assets/Data/Puzzles/Testing/ExampleAssPuzzle/Ass1.asset.meta diff --git a/Assets/Data/Puzzles/Quarry/ExampleAssPuzzle/Ass2.asset b/Assets/Data/Puzzles/Testing/ExampleAssPuzzle/Ass2.asset similarity index 100% rename from Assets/Data/Puzzles/Quarry/ExampleAssPuzzle/Ass2.asset rename to Assets/Data/Puzzles/Testing/ExampleAssPuzzle/Ass2.asset diff --git a/Assets/Data/Puzzles/Quarry/ExampleAssPuzzle/Ass2.asset.meta b/Assets/Data/Puzzles/Testing/ExampleAssPuzzle/Ass2.asset.meta similarity index 100% rename from Assets/Data/Puzzles/Quarry/ExampleAssPuzzle/Ass2.asset.meta rename to Assets/Data/Puzzles/Testing/ExampleAssPuzzle/Ass2.asset.meta diff --git a/Assets/Data/Puzzles/Quarry/ExampleAssPuzzle/Ass3.asset b/Assets/Data/Puzzles/Testing/ExampleAssPuzzle/Ass3.asset similarity index 100% rename from Assets/Data/Puzzles/Quarry/ExampleAssPuzzle/Ass3.asset rename to Assets/Data/Puzzles/Testing/ExampleAssPuzzle/Ass3.asset diff --git a/Assets/Data/Puzzles/Quarry/ExampleAssPuzzle/Ass3.asset.meta b/Assets/Data/Puzzles/Testing/ExampleAssPuzzle/Ass3.asset.meta similarity index 100% rename from Assets/Data/Puzzles/Quarry/ExampleAssPuzzle/Ass3.asset.meta rename to Assets/Data/Puzzles/Testing/ExampleAssPuzzle/Ass3.asset.meta diff --git a/Assets/Data/Puzzles/Quarry/ExampleAssPuzzle/Head.asset b/Assets/Data/Puzzles/Testing/ExampleAssPuzzle/Head.asset similarity index 100% rename from Assets/Data/Puzzles/Quarry/ExampleAssPuzzle/Head.asset rename to Assets/Data/Puzzles/Testing/ExampleAssPuzzle/Head.asset diff --git a/Assets/Data/Puzzles/Quarry/ExampleAssPuzzle/Head.asset.meta b/Assets/Data/Puzzles/Testing/ExampleAssPuzzle/Head.asset.meta similarity index 100% rename from Assets/Data/Puzzles/Quarry/ExampleAssPuzzle/Head.asset.meta rename to Assets/Data/Puzzles/Testing/ExampleAssPuzzle/Head.asset.meta diff --git a/Assets/Data/Puzzles/Testing/Testing.asset b/Assets/Data/Puzzles/Testing/Testing.asset new file mode 100644 index 00000000..cf3ef9f7 --- /dev/null +++ b/Assets/Data/Puzzles/Testing/Testing.asset @@ -0,0 +1,24 @@ +%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: Testing + m_EditorClassIdentifier: AppleHillsScripts::PuzzleS.PuzzleLevelDataSO + levelId: Testing + displayName: Testing + allSteps: + - {fileID: 11400000, guid: 0b13ff4f31443b74281b13e0eef865c2, type: 2} + - {fileID: 11400000, guid: a84cbe9804e13f74e857c55d90cc10d1, type: 2} + - {fileID: 11400000, guid: 13b0c411066f85a41ba40c3bbbc281ed, type: 2} + - {fileID: 11400000, guid: 9de0c57af6191384e96e2ba7c04a3d0d, type: 2} + initialSteps: + - {fileID: 11400000, guid: 0b13ff4f31443b74281b13e0eef865c2, type: 2} + - {fileID: 11400000, guid: a84cbe9804e13f74e857c55d90cc10d1, type: 2} diff --git a/Assets/Data/Puzzles/Testing/Testing.asset.meta b/Assets/Data/Puzzles/Testing/Testing.asset.meta new file mode 100644 index 00000000..70d7ef48 --- /dev/null +++ b/Assets/Data/Puzzles/Testing/Testing.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1a9d24004b795e147b8544845a7a9ae3 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor/PuzzleSystem.meta b/Assets/Editor/PuzzleSystem.meta new file mode 100644 index 00000000..66996b2e --- /dev/null +++ b/Assets/Editor/PuzzleSystem.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 453e2745b86c424da42227fbc38ed6d7 +timeCreated: 1760539457 \ No newline at end of file diff --git a/Assets/Editor/PuzzleSystem/PuzzleEditorWindow.cs b/Assets/Editor/PuzzleSystem/PuzzleEditorWindow.cs new file mode 100644 index 00000000..cbbef1d6 --- /dev/null +++ b/Assets/Editor/PuzzleSystem/PuzzleEditorWindow.cs @@ -0,0 +1,875 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using UnityEditor; +using UnityEngine; +using PuzzleS; + +namespace AppleHills.Editor.PuzzleSystem +{ + /// + /// Editor utility for managing puzzle steps and debugging puzzle state at runtime. + /// Provides tabs for both editing puzzle data and runtime debugging. + /// + public class PuzzleEditorWindow : EditorWindow + { + // Paths + private const string PuzzleDataBasePath = "Assets/Data"; + private const string MenuPath = "AppleHills/Puzzle Editor"; + + // Editor state + private List _allPuzzleSteps = new List(); + private Dictionary> _puzzleStepsByFolder = new Dictionary>(); + private PuzzleStepSO _selectedStep; + private Dictionary _folderExpanded = new Dictionary(); + private Vector2 _puzzleListScrollPosition; + private Vector2 _puzzleEditScrollPosition; + private string _searchQuery = ""; + private bool _isDirty = false; + + // Tab management + private int _selectedTab = 0; + private readonly string[] _tabNames = { "Edit", "Debug" }; + + // Runtime debug state + private Dictionary _stepUnlockState = new Dictionary(); + private Dictionary _stepCompletedState = new Dictionary(); + private Vector2 _debugScrollPosition; + private bool _isPlaying = false; + private bool _hasRuntimeData = false; + private PuzzleLevelDataSO _runtimeLevelData; + + // New step creation + private string _newStepName = "New Step"; + private string _selectedFolder = ""; + private bool _showCreateNewStepDialog = false; + + [MenuItem(MenuPath)] + public static void ShowWindow() + { + var window = GetWindow("Puzzle Editor"); + window.minSize = new Vector2(800, 600); + window.Show(); + } + + private void OnEnable() + { + // Load all puzzle steps + LoadAllPuzzleSteps(); + + // Register for undo/redo + Undo.undoRedoPerformed += OnUndoRedo; + + // Set up update callbacks + EditorApplication.update += OnEditorUpdate; + EditorApplication.playModeStateChanged += OnPlayModeStateChanged; + } + + private void OnDisable() + { + // Unregister from undo/redo + Undo.undoRedoPerformed -= OnUndoRedo; + + // Unregister from update + EditorApplication.update -= OnEditorUpdate; + EditorApplication.playModeStateChanged -= OnPlayModeStateChanged; + } + + private void OnEditorUpdate() + { + // Check if we're in play mode + bool currentlyPlaying = EditorApplication.isPlaying && !EditorApplication.isPaused; + if (_isPlaying != currentlyPlaying) + { + _isPlaying = currentlyPlaying; + Repaint(); + } + + // In play mode, update runtime data periodically + if (_isPlaying) + { + UpdateRuntimeData(); + Repaint(); + } + } + + private void OnUndoRedo() + { + _isDirty = true; + Repaint(); + } + + private void OnPlayModeStateChanged(PlayModeStateChange state) + { + if (state == PlayModeStateChange.EnteredPlayMode) + { + _isPlaying = true; + _hasRuntimeData = false; + _stepUnlockState.Clear(); + _stepCompletedState.Clear(); + } + else if (state == PlayModeStateChange.ExitingPlayMode) + { + _isPlaying = false; + _hasRuntimeData = false; + _runtimeLevelData = null; + } + + Repaint(); + } + + private void OnGUI() + { + DrawHeader(); + + _selectedTab = GUILayout.Toolbar(_selectedTab, _tabNames); + + EditorGUILayout.Space(); + + switch (_selectedTab) + { + case 0: // Edit tab + DrawEditTab(); + break; + case 1: // Debug tab + DrawDebugTab(); + break; + } + + // Apply any pending changes + if (_isDirty) + { + SaveChanges(); + _isDirty = false; + } + } + + #region Header UI + + private void DrawHeader() + { + EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); + + if (GUILayout.Button("Refresh", EditorStyles.toolbarButton, GUILayout.Width(60))) + { + LoadAllPuzzleSteps(); + } + + GUILayout.FlexibleSpace(); + + // Tab-specific toolbar options + if (_selectedTab == 0) // Edit tab + { + if (GUILayout.Button("Create New", EditorStyles.toolbarButton, GUILayout.Width(80))) + { + _showCreateNewStepDialog = true; + } + } + else if (_selectedTab == 1) // Debug tab + { + EditorGUILayout.LabelField(_isPlaying ? "Runtime Active" : "Editor Mode", EditorStyles.toolbarButton, GUILayout.Width(100)); + } + + EditorGUILayout.EndHorizontal(); + } + + #endregion + + #region Edit Tab + + private void DrawEditTab() + { + EditorGUILayout.BeginHorizontal(); + + // Left panel - puzzle step list + EditorGUILayout.BeginVertical(GUILayout.Width(250)); + DrawStepListPanel(); + EditorGUILayout.EndVertical(); + + // Separator + EditorGUILayout.Space(); + + // Right panel - puzzle step editor + EditorGUILayout.BeginVertical(); + DrawStepEditorPanel(); + EditorGUILayout.EndVertical(); + + EditorGUILayout.EndHorizontal(); + + // Draw create new step dialog if needed + if (_showCreateNewStepDialog) + { + DrawCreateNewStepDialog(); + } + } + + private void DrawStepListPanel() + { + // Search field + EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); + _searchQuery = EditorGUILayout.TextField(_searchQuery, EditorStyles.toolbarSearchField); + if (GUILayout.Button("×", EditorStyles.toolbarButton, GUILayout.Width(20)) && !string.IsNullOrEmpty(_searchQuery)) + { + _searchQuery = ""; + GUI.FocusControl(null); + } + EditorGUILayout.EndHorizontal(); + + _puzzleListScrollPosition = EditorGUILayout.BeginScrollView(_puzzleListScrollPosition); + + // If search query exists, show filtered results across all folders + if (!string.IsNullOrEmpty(_searchQuery)) + { + var filteredSteps = _allPuzzleSteps.Where(step => + step.displayName.ToLower().Contains(_searchQuery.ToLower()) || + step.stepId.ToLower().Contains(_searchQuery.ToLower())).ToList(); + + if (filteredSteps.Any()) + { + EditorGUILayout.LabelField($"Search Results ({filteredSteps.Count}):", EditorStyles.boldLabel); + + foreach (var step in filteredSteps) + { + if (DrawStepListItem(step)) + { + _selectedStep = step; + GUI.FocusControl(null); + } + } + } + else + { + EditorGUILayout.LabelField("No matching steps found"); + } + } + // Otherwise show organized by folder + else + { + foreach (var folderEntry in _puzzleStepsByFolder) + { + string folderName = folderEntry.Key; + List steps = folderEntry.Value; + + if (!_folderExpanded.ContainsKey(folderName)) + { + _folderExpanded[folderName] = true; // Default to expanded + } + + // Folder header with toggle + EditorGUILayout.BeginHorizontal(); + _folderExpanded[folderName] = EditorGUILayout.Foldout(_folderExpanded[folderName], folderName, true); + + // Show step count + EditorGUILayout.LabelField($"({steps.Count})", GUILayout.Width(40)); + EditorGUILayout.EndHorizontal(); + + // Draw steps in folder if expanded + if (_folderExpanded[folderName]) + { + EditorGUI.indentLevel++; + + foreach (var step in steps) + { + if (DrawStepListItem(step)) + { + _selectedStep = step; + GUI.FocusControl(null); + } + } + + EditorGUI.indentLevel--; + } + } + } + + EditorGUILayout.EndScrollView(); + } + + private bool DrawStepListItem(PuzzleStepSO step) + { + if (step == null) return false; + + bool isSelected = step == _selectedStep; + EditorGUILayout.BeginHorizontal(isSelected ? EditorStyles.selectionRect : EditorStyles.helpBox); + + // Icon if available + if (step.icon != null) + { + GUILayout.Label(new GUIContent(step.icon.texture), GUILayout.Width(20), GUILayout.Height(20)); + } + else + { + GUILayout.Space(24); + } + + // Name and ID + EditorGUILayout.BeginVertical(); + EditorGUILayout.LabelField(step.displayName, EditorStyles.boldLabel); + EditorGUILayout.LabelField(step.stepId, EditorStyles.miniLabel); + EditorGUILayout.EndVertical(); + + EditorGUILayout.EndHorizontal(); + + // Check if this item was clicked + Rect itemRect = GUILayoutUtility.GetLastRect(); + bool wasClicked = Event.current.type == EventType.MouseDown && Event.current.button == 0 + && itemRect.Contains(Event.current.mousePosition); + + if (wasClicked) + { + Event.current.Use(); + } + + return wasClicked; + } + + private void DrawStepEditorPanel() + { + if (_selectedStep == null) + { + EditorGUILayout.HelpBox("Select a puzzle step to edit", MessageType.Info); + return; + } + + _puzzleEditScrollPosition = EditorGUILayout.BeginScrollView(_puzzleEditScrollPosition); + + EditorGUI.BeginChangeCheck(); + + // Header with name and ID + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("Editing:", EditorStyles.boldLabel, GUILayout.Width(60)); + EditorGUILayout.LabelField(_selectedStep.displayName, EditorStyles.boldLabel); + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(); + + // Basic properties + EditorGUILayout.LabelField("Basic Properties", EditorStyles.boldLabel); + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + + // Step ID + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("Step ID:", GUILayout.Width(100)); + string newStepId = EditorGUILayout.TextField(_selectedStep.stepId); + if (newStepId != _selectedStep.stepId) + { + Undo.RecordObject(_selectedStep, "Change Step ID"); + _selectedStep.stepId = newStepId; + _isDirty = true; + } + EditorGUILayout.EndHorizontal(); + + // Display Name + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("Display Name:", GUILayout.Width(100)); + string newDisplayName = EditorGUILayout.TextField(_selectedStep.displayName); + if (newDisplayName != _selectedStep.displayName) + { + Undo.RecordObject(_selectedStep, "Change Display Name"); + _selectedStep.displayName = newDisplayName; + _isDirty = true; + } + EditorGUILayout.EndHorizontal(); + + // Description + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("Description:", GUILayout.Width(100)); + string newDescription = EditorGUILayout.TextArea(_selectedStep.description, GUILayout.Height(60)); + if (newDescription != _selectedStep.description) + { + Undo.RecordObject(_selectedStep, "Change Description"); + _selectedStep.description = newDescription; + _isDirty = true; + } + EditorGUILayout.EndHorizontal(); + + // Icon + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("Icon:", GUILayout.Width(100)); + Sprite newIcon = (Sprite)EditorGUILayout.ObjectField(_selectedStep.icon, typeof(Sprite), false); + if (newIcon != _selectedStep.icon) + { + Undo.RecordObject(_selectedStep, "Change Icon"); + _selectedStep.icon = newIcon; + _isDirty = true; + } + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.EndVertical(); + + EditorGUILayout.Space(); + + // Unlocks (dependencies) + EditorGUILayout.LabelField("Unlocks", EditorStyles.boldLabel); + EditorGUILayout.HelpBox("Steps that will be unlocked when this step is completed", MessageType.Info); + + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + + // Show unlocked steps list + if (_selectedStep.unlocks.Count > 0) + { + for (int i = 0; i < _selectedStep.unlocks.Count; i++) + { + EditorGUILayout.BeginHorizontal(); + + // Draw step selector + PuzzleStepSO newUnlockedStep = (PuzzleStepSO)EditorGUILayout.ObjectField( + _selectedStep.unlocks[i], typeof(PuzzleStepSO), false); + + if (newUnlockedStep != _selectedStep.unlocks[i]) + { + Undo.RecordObject(_selectedStep, "Change Unlocked Step"); + _selectedStep.unlocks[i] = newUnlockedStep; + _isDirty = true; + } + + // Remove button + if (GUILayout.Button("-", GUILayout.Width(20))) + { + Undo.RecordObject(_selectedStep, "Remove Unlocked Step"); + _selectedStep.unlocks.RemoveAt(i); + _isDirty = true; + i--; + } + + EditorGUILayout.EndHorizontal(); + } + } + else + { + EditorGUILayout.LabelField("No steps will be unlocked"); + } + + // Add new dependency + if (GUILayout.Button("Add Unlocked Step")) + { + Undo.RecordObject(_selectedStep, "Add Unlocked Step"); + _selectedStep.unlocks.Add(null); + _isDirty = true; + } + + EditorGUILayout.EndVertical(); + + EditorGUILayout.Space(); + + // Asset path info + string assetPath = AssetDatabase.GetAssetPath(_selectedStep); + EditorGUILayout.LabelField("Asset Path:", EditorStyles.miniLabel); + EditorGUILayout.LabelField(assetPath, EditorStyles.miniLabel); + + // Delete button + EditorGUILayout.Space(); + if (GUILayout.Button("Delete Step", GUILayout.Width(100))) + { + if (EditorUtility.DisplayDialog("Delete Puzzle Step", + $"Are you sure you want to delete '{_selectedStep.displayName}'? This action cannot be undone.", + "Delete", "Cancel")) + { + DeletePuzzleStep(_selectedStep); + _selectedStep = null; + } + } + + if (EditorGUI.EndChangeCheck()) + { + EditorUtility.SetDirty(_selectedStep); + } + + EditorGUILayout.EndScrollView(); + } + + private void DrawCreateNewStepDialog() + { + // Create a centered window + Rect windowRect = new Rect( + (position.width - 400) / 2, + (position.height - 200) / 2, + 400, 200); + + GUI.Box(windowRect, "Create New Puzzle Step", EditorStyles.helpBox); + GUILayout.BeginArea(new Rect(windowRect.x + 10, windowRect.y + 30, windowRect.width - 20, windowRect.height - 40)); + + // Name field + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("Name:", GUILayout.Width(80)); + _newStepName = EditorGUILayout.TextField(_newStepName); + EditorGUILayout.EndHorizontal(); + + // Folder selection + EditorGUILayout.BeginHorizontal(); + EditorGUILayout.LabelField("Folder:", GUILayout.Width(80)); + + // Create folder dropdown + List folderNames = _puzzleStepsByFolder.Keys.ToList(); + int selectedFolderIndex = folderNames.IndexOf(_selectedFolder); + int newSelectedFolderIndex = EditorGUILayout.Popup(selectedFolderIndex >= 0 ? selectedFolderIndex : 0, folderNames.ToArray()); + if (newSelectedFolderIndex >= 0 && newSelectedFolderIndex < folderNames.Count) + { + _selectedFolder = folderNames[newSelectedFolderIndex]; + } + else if (folderNames.Count > 0) + { + _selectedFolder = folderNames[0]; + } + + EditorGUILayout.EndHorizontal(); + + EditorGUILayout.Space(); + + // Buttons + EditorGUILayout.BeginHorizontal(); + GUILayout.FlexibleSpace(); + + if (GUILayout.Button("Cancel", GUILayout.Width(100))) + { + _showCreateNewStepDialog = false; + } + + if (GUILayout.Button("Create", GUILayout.Width(100))) + { + if (!string.IsNullOrEmpty(_newStepName) && !string.IsNullOrEmpty(_selectedFolder)) + { + CreateNewPuzzleStep(_newStepName, _selectedFolder); + _showCreateNewStepDialog = false; + _newStepName = "New Step"; + } + } + + EditorGUILayout.EndHorizontal(); + + GUILayout.EndArea(); + } + + #endregion + + #region Debug Tab + + private void DrawDebugTab() + { + if (!_isPlaying) + { + EditorGUILayout.HelpBox("Enter Play Mode to debug puzzles at runtime", MessageType.Info); + return; + } + + if (!_hasRuntimeData || _runtimeLevelData == null) + { + EditorGUILayout.HelpBox("Waiting for puzzle data to be loaded...", MessageType.Info); + return; + } + + EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); + EditorGUILayout.LabelField($"Current Level: {_runtimeLevelData.levelId}", EditorStyles.boldLabel); + EditorGUILayout.EndHorizontal(); + + _debugScrollPosition = EditorGUILayout.BeginScrollView(_debugScrollPosition); + + // List all steps with their current state + EditorGUILayout.LabelField("Puzzle Steps", EditorStyles.boldLabel); + + // Show steps directly from the level data in a flat list + foreach (var step in _runtimeLevelData.allSteps) + { + if (step == null) continue; + + DrawRuntimeStepItem(step); + } + + EditorGUILayout.EndScrollView(); + } + + private void DrawRuntimeStepItem(PuzzleStepSO step) + { + bool isUnlocked = _stepUnlockState.ContainsKey(step.stepId) && _stepUnlockState[step.stepId]; + bool isCompleted = _stepCompletedState.ContainsKey(step.stepId) && _stepCompletedState[step.stepId]; + + // Set background color based on state + Color originalColor = GUI.backgroundColor; + + if (isCompleted) + GUI.backgroundColor = new Color(0.5f, 1f, 0.5f); // Green for completed + else if (isUnlocked) + GUI.backgroundColor = new Color(1f, 1f, 0.5f); // Yellow for unlocked but not completed + else + GUI.backgroundColor = new Color(1f, 0.5f, 0.5f); // Red for locked + + EditorGUILayout.BeginVertical(EditorStyles.helpBox); + + // Reset color + GUI.backgroundColor = originalColor; + + EditorGUILayout.BeginHorizontal(); + + // Step info + EditorGUILayout.BeginVertical(GUILayout.ExpandWidth(true)); + EditorGUILayout.LabelField(step.displayName, EditorStyles.boldLabel); + EditorGUILayout.LabelField(step.stepId, EditorStyles.miniLabel); + + // Status text + string statusText = isCompleted ? "Completed" : (isUnlocked ? "Unlocked" : "Locked"); + EditorGUILayout.LabelField($"Status: {statusText}", EditorStyles.miniLabel); + + EditorGUILayout.EndVertical(); + + // Action buttons + EditorGUILayout.BeginVertical(GUILayout.Width(100)); + + EditorGUI.BeginDisabledGroup(isCompleted); + if (GUILayout.Button(isUnlocked ? "Lock" : "Unlock")) + { + ToggleStepUnlocked(step); + } + EditorGUI.EndDisabledGroup(); + + EditorGUI.BeginDisabledGroup(!isUnlocked || isCompleted); + if (GUILayout.Button("Complete")) + { + CompleteStep(step); + } + EditorGUI.EndDisabledGroup(); + + EditorGUILayout.EndVertical(); + + EditorGUILayout.EndHorizontal(); + EditorGUILayout.EndVertical(); + } + + #endregion + + #region Data Management + + private void LoadAllPuzzleSteps() + { + _allPuzzleSteps.Clear(); + _puzzleStepsByFolder.Clear(); + + // Find all PuzzleStepSO assets in the project + string[] guids = AssetDatabase.FindAssets("t:PuzzleStepSO"); + + foreach (string guid in guids) + { + string assetPath = AssetDatabase.GUIDToAssetPath(guid); + + // Only include assets from the Data folder + if (assetPath.StartsWith(PuzzleDataBasePath)) + { + PuzzleStepSO step = AssetDatabase.LoadAssetAtPath(assetPath); + + if (step != null) + { + _allPuzzleSteps.Add(step); + + // Add to folder dictionary for organization + string folder = Path.GetDirectoryName(assetPath)?.Replace("\\", "/"); + + if (folder != null) + { + if (!_puzzleStepsByFolder.ContainsKey(folder)) + { + _puzzleStepsByFolder[folder] = new List(); + } + + _puzzleStepsByFolder[folder].Add(step); + } + } + } + } + + // Make sure each folder is sorted by name + foreach (var key in _puzzleStepsByFolder.Keys.ToList()) + { + _puzzleStepsByFolder[key] = _puzzleStepsByFolder[key] + .OrderBy(step => step.displayName) + .ToList(); + } + + _isDirty = false; + } + + private void SaveChanges() + { + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + } + + private void CreateNewPuzzleStep(string stepName, string folderPath) + { + // Create a new PuzzleStepSO + PuzzleStepSO newStep = CreateInstance(); + newStep.stepId = GenerateUniqueStepId(stepName); + newStep.displayName = stepName; + + // Create the path + string assetPath = Path.Combine(folderPath, $"{stepName}.asset").Replace("\\", "/"); + + // Make sure the directory exists + string directory = Path.GetDirectoryName(assetPath); + if (!Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + // Create the asset + AssetDatabase.CreateAsset(newStep, assetPath); + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + + // Reload all steps and select the new one + LoadAllPuzzleSteps(); + _selectedStep = newStep; + } + + private void DeletePuzzleStep(PuzzleStepSO step) + { + if (step == null) return; + + string assetPath = AssetDatabase.GetAssetPath(step); + + if (!string.IsNullOrEmpty(assetPath)) + { + // Also need to remove all references to this step from other steps' unlocks lists + foreach (var otherStep in _allPuzzleSteps) + { + if (otherStep != null && otherStep != step) + { + if (otherStep.unlocks.Contains(step)) + { + Undo.RecordObject(otherStep, "Remove Deleted Step Reference"); + otherStep.unlocks.Remove(step); + EditorUtility.SetDirty(otherStep); + } + } + } + + AssetDatabase.DeleteAsset(assetPath); + AssetDatabase.SaveAssets(); + AssetDatabase.Refresh(); + + // Reload all steps + LoadAllPuzzleSteps(); + } + } + + private string GenerateUniqueStepId(string baseName) + { + // Convert to lowercase and replace spaces with underscores + string baseId = baseName.ToLower().Replace(" ", "_"); + + // Check if this ID already exists + bool idExists = _allPuzzleSteps.Any(step => step.stepId == baseId); + + if (!idExists) + { + return baseId; + } + + // Add a number suffix if ID already exists + int counter = 1; + while (_allPuzzleSteps.Any(step => step.stepId == $"{baseId}_{counter}")) + { + counter++; + } + + return $"{baseId}_{counter}"; + } + + #endregion + + #region Runtime Debug Helpers + + private void UpdateRuntimeData() + { + if (!_isPlaying) return; + + // Find PuzzleManager instance + PuzzleManager puzzleManager = Object.FindObjectOfType(); + + if (puzzleManager == null) + { + _hasRuntimeData = false; + return; + } + + // Get current level data + var levelData = puzzleManager.GetCurrentLevelData(); + if (levelData == null) + { + _hasRuntimeData = false; + return; + } + + _hasRuntimeData = true; + _runtimeLevelData = levelData; + + // Update step states + foreach (var step in _runtimeLevelData.allSteps) + { + if (step != null) + { + _stepUnlockState[step.stepId] = puzzleManager.IsStepUnlocked(step); + _stepCompletedState[step.stepId] = puzzleManager.IsPuzzleStepCompleted(step.stepId); + } + } + } + + private void ToggleStepUnlocked(PuzzleStepSO step) + { + if (!_isPlaying || step == null) return; + + PuzzleManager puzzleManager = Object.FindObjectOfType(); + if (puzzleManager == null) return; + + // Get current unlock state + bool isCurrentlyUnlocked = _stepUnlockState.ContainsKey(step.stepId) && _stepUnlockState[step.stepId]; + + // Call appropriate method using reflection since these might be private methods + System.Type managerType = puzzleManager.GetType(); + + if (isCurrentlyUnlocked) + { + // Find the LockStep method that takes a PuzzleStepSO parameter + System.Reflection.MethodInfo lockMethod = managerType.GetMethod("LockStep", + System.Reflection.BindingFlags.Instance | + System.Reflection.BindingFlags.Public | + System.Reflection.BindingFlags.NonPublic); + + if (lockMethod != null) + { + lockMethod.Invoke(puzzleManager, new object[] { step }); + } + } + else + { + // Find the UnlockStep method that takes a PuzzleStepSO parameter + System.Reflection.MethodInfo unlockMethod = managerType.GetMethod("UnlockStep", + System.Reflection.BindingFlags.Instance | + System.Reflection.BindingFlags.Public | + System.Reflection.BindingFlags.NonPublic); + + if (unlockMethod != null) + { + unlockMethod.Invoke(puzzleManager, new object[] { step }); + } + } + + // Update state + UpdateRuntimeData(); + } + + private void CompleteStep(PuzzleStepSO step) + { + if (!_isPlaying || step == null) return; + + PuzzleManager puzzleManager = Object.FindObjectOfType(); + if (puzzleManager == null) return; + + // Complete the step + puzzleManager.MarkPuzzleStepCompleted(step); + + // Update state + UpdateRuntimeData(); + } + + #endregion + } +} diff --git a/Assets/Editor/PuzzleSystem/PuzzleEditorWindow.cs.meta b/Assets/Editor/PuzzleSystem/PuzzleEditorWindow.cs.meta new file mode 100644 index 00000000..9eccad68 --- /dev/null +++ b/Assets/Editor/PuzzleSystem/PuzzleEditorWindow.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c733f3c6624e4e5486c07abfe4fab81e +timeCreated: 1760539457 \ No newline at end of file diff --git a/Assets/Scripts/Interactions/Interactable.cs b/Assets/Scripts/Interactions/Interactable.cs index 521d3c1b..e21cec6a 100644 --- a/Assets/Scripts/Interactions/Interactable.cs +++ b/Assets/Scripts/Interactions/Interactable.cs @@ -418,7 +418,8 @@ namespace Interactions { // Check for ObjectiveStepBehaviour and lock state var step = GetComponent(); - if (step != null && !step.IsStepUnlocked()) + var slot = GetComponent(); + if (step != null && !step.IsStepUnlocked() && slot == null) { DebugUIMessage.Show("This step is locked!", Color.yellow); BroadcastInteractionComplete(false);