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) { SavePuzzleChanges(); _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 SavePuzzleChanges() { 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.FindFirstObjectByType(); 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.FindFirstObjectByType(); 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.FindFirstObjectByType(); if (puzzleManager == null) return; // Complete the step puzzleManager.MarkPuzzleStepCompleted(step); // Update state UpdateRuntimeData(); } #endregion } }