using System.Collections.Generic; using System.IO; using System.Linq; using AppleHills.Data.CardSystem; using AppleHills.Editor.Utilities; using UI.CardSystem; using UnityEditor; using UnityEngine; using UnityEngine.UI; using UnityEngine.Audio; namespace Editor.CardSystem { /// /// Editor utility for managing card definitions with live preview using the same UI as in-game. /// public class CardEditorWindow : EditorWindow { // Paths private const string CardDefinitionsPath = "Assets/Data/Cards"; private const string MenuPath = "AppleHills/Cards/Card Editor"; private const string CardUIPrefabPath = "Assets/Prefabs/UI/CardsSystem/Cards/Card.prefab"; private const string CardVisualConfigPath = CardDefinitionsPath + "/CardVisualConfig.asset"; // Preview settings private static readonly Vector2 PreviewCardSize = new Vector2(200f, 300f); private const float CameraPaddingFactor = 1.2f; // Editor state private List _cards = new List(); private CardDefinition _selectedCard; private CardDefinition _editingCard; private Vector2 _cardListScrollPosition; private Vector2 _cardEditScrollPosition; private string _searchQuery = ""; private bool _showPreview = true; private bool _isDirty = false; // Preview state private PreviewRenderUtility _previewUtility; private GameObject _previewRoot; private CardDisplay _previewCardDisplay; private GameObject _cardUIPrefab; private CardVisualConfig _cardVisualConfig; private float _zoomLevel = 1.0f; [MenuItem(MenuPath)] public static void ShowWindow() { var window = GetWindow("Card Editor"); window.minSize = new Vector2(1000, 700); window.Show(); } private void OnEnable() { LoadCardDefinitions(); InitializePreview(); Undo.undoRedoPerformed += OnUndoRedo; } private void OnDisable() { CleanupPreview(); Undo.undoRedoPerformed -= OnUndoRedo; } private void OnUndoRedo() { LoadCardDefinitions(); Repaint(); } private void LoadCardDefinitions() { _cards.Clear(); // Ensure directory exists if (!Directory.Exists(CardDefinitionsPath)) { Directory.CreateDirectory(CardDefinitionsPath); AssetDatabase.Refresh(); } // Find all card definitions string[] guids = AssetDatabase.FindAssets("t:CardDefinition", new[] { CardDefinitionsPath }); foreach (string guid in guids) { string path = AssetDatabase.GUIDToAssetPath(guid); CardDefinition card = AssetDatabase.LoadAssetAtPath(path); if (card != null) { _cards.Add(card); } } _cards = _cards.OrderBy(c => c.Name).ToList(); // Restore selection if possible if (_selectedCard != null) { _selectedCard = _cards.FirstOrDefault(c => c.Id == _selectedCard.Id); if (_selectedCard != null) { _editingCard = CloneCard(_selectedCard); UpdatePreview(); } } } private void InitializePreview() { if (_previewUtility == null) { _previewUtility = new PreviewRenderUtility(); var cam = _previewUtility.camera; cam.clearFlags = CameraClearFlags.SolidColor; cam.backgroundColor = new Color(0.2f, 0.2f, 0.2f); cam.orthographic = true; // Calculate ortho size based on card height with padding float baseOrthoSize = (PreviewCardSize.y / 2f) * CameraPaddingFactor; cam.orthographicSize = baseOrthoSize; cam.nearClipPlane = 0.01f; cam.farClipPlane = 100f; cam.transform.position = new Vector3(0, 0, -10); cam.transform.rotation = Quaternion.identity; } // Load prefab and config _cardUIPrefab = AssetDatabase.LoadAssetAtPath(CardUIPrefabPath); if (_cardUIPrefab == null) { Debug.LogError($"[CardEditorWindow] Card UI prefab not found at {CardUIPrefabPath}"); } _cardVisualConfig = AssetDatabase.LoadAssetAtPath(CardVisualConfigPath); if (_cardVisualConfig == null) { Debug.LogWarning($"[CardEditorWindow] Card visual config not found at {CardVisualConfigPath}"); } CreatePreviewCardObject(); } private void CreatePreviewCardObject() { CleanupPreviewInstance(); if (_cardUIPrefab == null || _previewUtility == null) return; try { // Create root and canvas _previewRoot = new GameObject("PreviewRoot"); _previewRoot.hideFlags = HideFlags.HideAndDontSave; GameObject canvasGO = new GameObject("PreviewCanvas"); canvasGO.transform.SetParent(_previewRoot.transform, false); Canvas canvas = canvasGO.AddComponent(); canvas.renderMode = RenderMode.WorldSpace; canvas.worldCamera = _previewUtility.camera; RectTransform canvasRect = canvasGO.GetComponent(); canvasRect.sizeDelta = new Vector2(20f, 20f); canvasRect.localPosition = Vector3.zero; CanvasScaler scaler = canvasGO.AddComponent(); scaler.uiScaleMode = CanvasScaler.ScaleMode.ConstantPixelSize; scaler.scaleFactor = 1f; canvasGO.AddComponent(); // Instantiate card prefab GameObject cardInstance = Instantiate(_cardUIPrefab, canvasGO.transform, false); RectTransform cardRect = cardInstance.GetComponent(); if (cardRect != null) { // Set explicit size for preview cardRect.sizeDelta = PreviewCardSize; // Anchor to center cardRect.anchorMin = new Vector2(0.5f, 0.5f); cardRect.anchorMax = new Vector2(0.5f, 0.5f); cardRect.pivot = new Vector2(0.5f, 0.5f); cardRect.anchoredPosition = Vector2.zero; } // Get CardDisplay component _previewCardDisplay = cardInstance.GetComponent(); if (_previewCardDisplay == null) { _previewCardDisplay = cardInstance.GetComponentInChildren(); } if (_previewCardDisplay != null && _cardVisualConfig != null) { _previewCardDisplay.SetVisualConfig(_cardVisualConfig); } _previewUtility.AddSingleGO(_previewRoot); // Update camera UpdatePreviewCamera(); } catch (System.Exception e) { Debug.LogError($"[CardEditorWindow] Error creating preview: {e.Message}"); } } private void UpdatePreviewCamera() { if (_previewUtility == null) return; Camera cam = _previewUtility.camera; // Calculate base ortho size from card height with padding float baseOrthoSize = (PreviewCardSize.y / 2f) * CameraPaddingFactor; // Apply zoom as divisor (higher zoom = smaller ortho size = more zoomed in) cam.orthographicSize = baseOrthoSize / _zoomLevel; } private void CleanupPreviewInstance() { if (_previewRoot != null) { DestroyImmediate(_previewRoot); _previewRoot = null; } _previewCardDisplay = null; } private void CleanupPreview() { CleanupPreviewInstance(); if (_previewUtility != null) { _previewUtility.Cleanup(); _previewUtility = null; } } private void UpdatePreview() { if (_editingCard == null || _previewCardDisplay == null || !_showPreview) return; try { // Create CardData from definition and setup the display CardData previewData = _editingCard.CreateCardData(); _previewCardDisplay.SetupCard(previewData); Repaint(); } catch (System.Exception ex) { Debug.LogError($"[CardEditorWindow] Error updating preview: {ex.Message}"); } } private void OnGUI() { EditorGUILayout.BeginHorizontal(); // Left panel - Card list DrawCardList(); // Middle panel - Card editor DrawCardEditor(); // Right panel - Preview if (_showPreview) { DrawPreview(); } EditorGUILayout.EndHorizontal(); } private void DrawCardList() { EditorGUILayout.BeginVertical(GUILayout.Width(250)); EditorGUILayout.LabelField("Card List", EditorStyles.boldLabel); // Search bar EditorGUILayout.BeginHorizontal(); _searchQuery = EditorGUILayout.TextField(_searchQuery, EditorStyles.toolbarSearchField); if (GUILayout.Button("X", GUILayout.Width(20))) { _searchQuery = ""; GUI.FocusControl(null); } EditorGUILayout.EndHorizontal(); // New card button if (GUILayout.Button("Create New Card")) { CreateNewCard(); } // Card list _cardListScrollPosition = EditorGUILayout.BeginScrollView(_cardListScrollPosition); var filteredCards = string.IsNullOrEmpty(_searchQuery) ? _cards : _cards.Where(c => c.Name.ToLower().Contains(_searchQuery.ToLower())).ToList(); foreach (var card in filteredCards) { bool isSelected = _selectedCard == card; GUI.backgroundColor = isSelected ? Color.cyan : Color.white; if (GUILayout.Button(card.Name, EditorStyles.miniButton)) { SelectCard(card); } GUI.backgroundColor = Color.white; } EditorGUILayout.EndScrollView(); EditorGUILayout.EndVertical(); } private void DrawCardEditor() { EditorGUILayout.BeginVertical(GUILayout.Width(350)); if (_editingCard == null) { EditorGUILayout.HelpBox("Select a card from the list or create a new one.", MessageType.Info); EditorGUILayout.EndVertical(); return; } EditorGUILayout.LabelField("Card Editor", EditorStyles.boldLabel); _cardEditScrollPosition = EditorGUILayout.BeginScrollView(_cardEditScrollPosition); EditorGUI.BeginChangeCheck(); // Basic Info EditorGUILayout.LabelField("Basic Information", EditorStyles.boldLabel); _editingCard.Name = EditorGUILayout.TextField("Name", _editingCard.Name); // Custom file name option _editingCard.UseCustomFileName = EditorGUILayout.Toggle("Use Custom File Name", _editingCard.UseCustomFileName); GUI.enabled = _editingCard.UseCustomFileName; _editingCard.CustomFileName = EditorGUILayout.TextField("Custom File Name", _editingCard.CustomFileName); GUI.enabled = true; _editingCard.Description = EditorGUILayout.TextArea(_editingCard.Description, GUILayout.Height(60)); EditorGUILayout.Space(); // Properties EditorGUILayout.LabelField("Properties", EditorStyles.boldLabel); _editingCard.Rarity = (CardRarity)EditorGUILayout.EnumPopup("Rarity", _editingCard.Rarity); _editingCard.Zone = (CardZone)EditorGUILayout.EnumPopup("Zone", _editingCard.Zone); EditorGUILayout.Space(); // Visuals EditorGUILayout.LabelField("Visuals", EditorStyles.boldLabel); _editingCard.CardImage = (Sprite)EditorGUILayout.ObjectField("Card Image", _editingCard.CardImage, typeof(Sprite), false); EditorGUILayout.Space(); EditorGUILayout.HelpBox("Card visuals (frames, overlays, backgrounds, shapes) are configured in CardVisualConfig asset based on rarity and zone.", MessageType.Info); // Collection EditorGUILayout.Space(); EditorGUILayout.LabelField("Collection", EditorStyles.boldLabel); _editingCard.CollectionIndex = EditorGUILayout.IntField("Collection Index", _editingCard.CollectionIndex); EditorGUILayout.Space(); // Identification EditorGUILayout.LabelField("Identification", EditorStyles.boldLabel); GUI.enabled = false; EditorGUILayout.TextField("ID", _editingCard.Id); GUI.enabled = true; // Audio EditorGUILayout.LabelField("Audio", EditorStyles.boldLabel); _editingCard.reactionVoiceClip = (AudioResource)EditorGUILayout.ObjectField("Reaction audio clip", _editingCard.reactionVoiceClip, typeof(AudioResource),false); _editingCard.nameVoiceClip = (AudioResource)EditorGUILayout.ObjectField("Name audio clip", _editingCard.nameVoiceClip, typeof(AudioResource), false); if (EditorGUI.EndChangeCheck()) { _isDirty = true; UpdatePreview(); } EditorGUILayout.EndScrollView(); // Action buttons EditorGUILayout.Space(); EditorGUILayout.BeginHorizontal(); GUI.enabled = _isDirty; if (GUILayout.Button("Apply Changes")) { ApplyChanges(); } GUI.enabled = true; if (GUILayout.Button("Revert")) { RevertChanges(); } EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("Duplicate")) { DuplicateCard(); } GUI.backgroundColor = Color.red; if (GUILayout.Button("Delete")) { DeleteCard(); } GUI.backgroundColor = Color.white; EditorGUILayout.EndHorizontal(); EditorGUILayout.EndVertical(); } private void DrawPreview() { EditorGUILayout.BeginVertical(); EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("Preview", EditorStyles.boldLabel); _zoomLevel = EditorGUILayout.Slider("Zoom", _zoomLevel, 0.5f, 2.0f); EditorGUILayout.EndHorizontal(); if (Event.current.type == EventType.Repaint && _zoomLevel != _previewUtility.camera.orthographicSize) { UpdatePreviewCamera(); } Rect previewRect = GUILayoutUtility.GetRect(400, 600); if (_previewUtility != null && _editingCard != null) { _previewUtility.BeginPreview(previewRect, GUIStyle.none); _previewUtility.camera.Render(); Texture resultTexture = _previewUtility.EndPreview(); GUI.DrawTexture(previewRect, resultTexture, ScaleMode.ScaleToFit, false); } else { EditorGUI.DrawRect(previewRect, new Color(0.2f, 0.2f, 0.2f)); EditorGUI.LabelField(previewRect, "No Preview Available", EditorStyles.centeredGreyMiniLabel); } EditorGUILayout.EndVertical(); } private void SelectCard(CardDefinition card) { if (_isDirty && _selectedCard != null) { if (EditorUtility.DisplayDialog("Unsaved Changes", "You have unsaved changes. Apply them before switching cards?", "Apply", "Discard")) { ApplyChanges(); } } _selectedCard = card; _editingCard = CloneCard(card); _isDirty = false; UpdatePreview(); } private void CreateNewCard() { CardDefinition newCard = CreateInstance(); newCard.Id = System.Guid.NewGuid().ToString(); newCard.Name = "New Card"; newCard.Description = "Card description"; newCard.Rarity = CardRarity.Normal; newCard.Zone = CardZone.AppleHills; newCard.CollectionIndex = _cards.Count; string path = $"{CardDefinitionsPath}/Card_{newCard.Name}.asset"; path = AssetDatabase.GenerateUniqueAssetPath(path); AssetDatabase.CreateAsset(newCard, path); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); // Add to Addressables group "BlokkemonCards" and apply "BlokkemonCard" label if (AddressablesUtility.EnsureAssetInGroupWithLabel(path, "BlokkemonCards", "BlokkemonCard")) { AddressablesUtility.SaveAddressableAssets(); Debug.Log($"[CardEditorWindow] Added new card to Addressables with 'BlokkemonCard' label"); } else { Debug.LogWarning("[CardEditorWindow] Failed to add new card to Addressables. Please ensure Addressables are set up in this project."); } LoadCardDefinitions(); SelectCard(newCard); } private void ApplyChanges() { if (_selectedCard == null || _editingCard == null) return; Undo.RecordObject(_selectedCard, "Modify Card"); string oldPath = AssetDatabase.GetAssetPath(_selectedCard); EditorUtility.CopySerialized(_editingCard, _selectedCard); EditorUtility.SetDirty(_selectedCard); AssetDatabase.SaveAssets(); // Rename file if needed string desiredFileName = _selectedCard.UseCustomFileName && !string.IsNullOrEmpty(_selectedCard.CustomFileName) ? _selectedCard.CustomFileName : _selectedCard.Name; string finalPath = oldPath; if (!string.IsNullOrEmpty(desiredFileName)) { // Strip spaces from file name desiredFileName = desiredFileName.Replace(" ", ""); string directory = Path.GetDirectoryName(oldPath); string extension = Path.GetExtension(oldPath); string currentFileName = Path.GetFileNameWithoutExtension(oldPath); string expectedFileName = $"Card_{desiredFileName}"; // Only rename if the current file name doesn't match the expected name if (currentFileName != expectedFileName) { string newPath = Path.Combine(directory, $"{expectedFileName}{extension}"); string uniquePath = AssetDatabase.GenerateUniqueAssetPath(newPath); string error = AssetDatabase.MoveAsset(oldPath, uniquePath); if (!string.IsNullOrEmpty(error)) { Debug.LogError($"[CardEditorWindow] Failed to rename asset: {error}"); } else finalPath = uniquePath; { AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); } } } // Add to Addressables group "BlokkemonCards" and apply "BlokkemonCard" label if (!string.IsNullOrEmpty(finalPath)) { if (AddressablesUtility.EnsureAssetInGroupWithLabel(finalPath, "BlokkemonCards", "BlokkemonCard")) { AddressablesUtility.SaveAddressableAssets(); Debug.Log($"[CardEditorWindow] Added {_selectedCard.Name} to Addressables with 'BlokkemonCard' label"); } else { Debug.LogError("[CardEditorWindow] Failed to add card to Addressables. Please ensure Addressables are set up in this project."); } } _isDirty = false; Debug.Log($"[CardEditorWindow] Applied changes to {_selectedCard.Name}"); } private void RevertChanges() { if (_selectedCard == null) return; _editingCard = CloneCard(_selectedCard); _isDirty = false; UpdatePreview(); } private void DuplicateCard() { if (_selectedCard == null) return; CardDefinition duplicate = Instantiate(_selectedCard); duplicate.Id = System.Guid.NewGuid().ToString(); duplicate.Name = _selectedCard.Name + " (Copy)"; string path = $"{CardDefinitionsPath}/Card_{duplicate.Name}.asset"; path = AssetDatabase.GenerateUniqueAssetPath(path); AssetDatabase.CreateAsset(duplicate, path); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); LoadCardDefinitions(); SelectCard(duplicate); } private void DeleteCard() { if (_selectedCard == null) return; if (!EditorUtility.DisplayDialog("Delete Card", $"Are you sure you want to delete '{_selectedCard.Name}'?", "Delete", "Cancel")) { return; } string path = AssetDatabase.GetAssetPath(_selectedCard); AssetDatabase.DeleteAsset(path); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); _selectedCard = null; _editingCard = null; _isDirty = false; LoadCardDefinitions(); CleanupPreviewInstance(); CreatePreviewCardObject(); } private CardDefinition CloneCard(CardDefinition original) { CardDefinition clone = CreateInstance(); EditorUtility.CopySerialized(original, clone); return clone; } } }