Initial commit: Unity WordConnect project

This commit is contained in:
2025-08-01 19:12:05 +08:00
commit f14db75802
3503 changed files with 448337 additions and 0 deletions

View File

@ -0,0 +1,88 @@
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using UnityEditor.UIElements;
using WordsToolkit.Scripts.Settings;
namespace WordsToolkit.Scripts.Levels.Editor
{
[CustomPropertyDrawer(typeof(ColorsTile))]
public class ColorsTileDrawer : PropertyDrawer
{
private const float previewSize = 20f;
// Add a static event to notify when a color tile is selected
public static global::System.Action<ColorsTile> OnColorTileSelected;
public override VisualElement CreatePropertyGUI(SerializedProperty property)
{
// Create the root container
var container = new VisualElement();
container.style.flexDirection = FlexDirection.Row;
container.style.alignItems = Align.Center;
container.style.justifyContent = Justify.FlexStart;
container.style.height = EditorGUIUtility.singleLineHeight;
// Create label
var label = new Label(property.displayName);
label.style.paddingRight = 20;
container.Add(label);
// Create color preview
var colorPreview = new VisualElement();
colorPreview.style.width = previewSize;
colorPreview.style.height = previewSize;
colorPreview.style.borderTopWidth = 1;
colorPreview.style.borderBottomWidth = 1;
colorPreview.style.borderLeftWidth = 1;
colorPreview.style.borderRightWidth = 1;
colorPreview.style.borderTopColor = Color.white;
colorPreview.style.borderBottomColor = Color.white;
colorPreview.style.borderLeftColor = Color.white;
colorPreview.style.borderRightColor = Color.white;
container.Add(colorPreview);
// Update color preview based on current value
UpdateColorPreview(colorPreview, property);
// Make the entire container clickable
container.RegisterCallback<MouseDownEvent>(evt =>
{
if (evt.button == 0) // Left mouse button
{
var rect = new Rect(container.worldBound.x, container.worldBound.y,
container.worldBound.width, container.worldBound.height);
ColorsTilePopupWindow.Show(rect, property, () =>
{
UpdateColorPreview(colorPreview, property);
OnColorTileSelected?.Invoke(property.objectReferenceValue as ColorsTile);
});
evt.StopPropagation();
}
});
// Listen for property changes to update the preview
container.TrackPropertyValue(property, prop =>
{
UpdateColorPreview(colorPreview, prop);
});
return container;
}
private void UpdateColorPreview(VisualElement colorPreview, SerializedProperty property)
{
ColorsTile selectedTile = property.objectReferenceValue as ColorsTile;
if (selectedTile != null)
{
colorPreview.style.backgroundColor = selectedTile.faceColor;
}
else
{
colorPreview.style.backgroundColor = Color.gray;
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b70c67eb6c0da45b9a0d0552f52bd6c9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,256 @@
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using UnityEditor.UIElements;
using WordsToolkit.Scripts.Settings;
using WordsToolkit.Scripts.Gameplay;
using System.Linq;
using System.IO;
namespace WordsToolkit.Scripts.Levels.Editor
{
[CustomEditor(typeof(ColorsTile))]
public class ColorsTileInspector : UnityEditor.Editor
{
private ColorsTile[] allColorsTiles;
private int currentIndex;
private void OnEnable()
{
RefreshColorsTilesList();
}
private void RefreshColorsTilesList()
{
// Find all ColorsTile assets in the project
string[] guids = AssetDatabase.FindAssets("t:ColorsTile");
allColorsTiles = guids.Select(guid => AssetDatabase.LoadAssetAtPath<ColorsTile>(AssetDatabase.GUIDToAssetPath(guid)))
.Where(tile => tile != null)
.OrderBy(tile => tile.name)
.ToArray();
// Find current index
currentIndex = global::System.Array.IndexOf(allColorsTiles, target);
if (currentIndex == -1) currentIndex = 0;
}
public override VisualElement CreateInspectorGUI()
{
// Create root container
var root = new VisualElement();
// Add custom management panel at the top
var managementPanel = CreateManagementPanel();
root.Add(managementPanel);
// Add default property fields
InspectorElement.FillDefaultInspector(root, serializedObject, this);
return root;
}
private VisualElement CreateManagementPanel()
{
var panel = new VisualElement();
panel.style.marginBottom = 15;
panel.style.paddingBottom = 10;
panel.style.borderBottomWidth = 1;
panel.style.borderBottomColor = new Color(0.5f, 0.5f, 0.5f, 0.5f);
var label = new Label("Tile editor");
label.style.fontSize = 12;
label.style.unityFontStyleAndWeight = FontStyle.Bold;
label.style.marginBottom = 10;
panel.Add(label);
// Navigation and name editing section
var navigationContainer = new VisualElement();
navigationContainer.style.flexDirection = FlexDirection.Row;
navigationContainer.style.alignItems = Align.Center;
navigationContainer.style.marginBottom = 10;
var prevButton = new Button(() => NavigateToAsset(-1));
prevButton.text = "<<";
prevButton.style.width = 30;
prevButton.style.marginRight = 5;
navigationContainer.Add(prevButton);
var nameField = new TextField();
nameField.value = target.name;
nameField.style.width = 100;
nameField.style.marginRight = 5;
nameField.RegisterValueChangedCallback(evt => {
if (!string.IsNullOrEmpty(evt.newValue) && evt.newValue != target.name)
{
string assetPath = AssetDatabase.GetAssetPath(target);
AssetDatabase.RenameAsset(assetPath, evt.newValue);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
target.name = evt.newValue;
EditorUtility.SetDirty(target);
RefreshColorsTilesList();
}
});
navigationContainer.Add(nameField);
var nextButton = new Button(() => NavigateToAsset(1));
nextButton.text = ">>";
nextButton.style.width = 30;
navigationContainer.Add(nextButton);
var createButton = new Button(() => CreateNewColorsTile());
createButton.text = "+";
createButton.style.width = 25;
createButton.style.marginRight = 2;
navigationContainer.Add(createButton);
var deleteButton = new Button(() => DeleteCurrentColorsTile());
deleteButton.text = "-";
deleteButton.style.width = 25;
deleteButton.style.marginRight = 5;
navigationContainer.Add(deleteButton);
panel.Add(navigationContainer);
// Utility buttons
var buttonsContainer = new VisualElement();
buttonsContainer.style.flexDirection = FlexDirection.Row;
buttonsContainer.style.marginTop = 5;
var resetButton = new Button(() => {
var colorsTile = target as ColorsTile;
if (colorsTile != null)
{
serializedObject.FindProperty("faceColor").colorValue = Color.white;
serializedObject.FindProperty("topColor").colorValue = Color.white;
serializedObject.FindProperty("bottomColor").colorValue = Color.white;
serializedObject.ApplyModifiedProperties();
}
});
panel.Add(buttonsContainer);
return panel;
}
private void CreateNewColorsTile()
{
// Make sure the directory exists
string folderPath = "Assets/WordConnectGameToolkit/Resources/ColorsTile";
if (!Directory.Exists(folderPath))
{
Directory.CreateDirectory(folderPath);
AssetDatabase.Refresh();
}
string baseName = "NewColorsTile";
string fileName = baseName;
int counter = 1;
string path = $"{folderPath}/{fileName}.asset";
while (File.Exists(path))
{
fileName = $"{baseName}_{counter}";
path = $"{folderPath}/{fileName}.asset";
counter++;
}
ColorsTile newColorsTile = ScriptableObject.CreateInstance<ColorsTile>();
// Load and assign the tile prefab
string prefabPath = "Assets/WordConnectGameToolkit/Prefabs/Game/Tile.prefab";
var tilePrefab = AssetDatabase.LoadAssetAtPath<FillAndPreview>(prefabPath);
if (tilePrefab != null)
{
newColorsTile.prefab = tilePrefab;
}
// Set default colors
newColorsTile.faceColor = Color.white;
newColorsTile.topColor = Color.white;
newColorsTile.bottomColor = Color.white;
AssetDatabase.CreateAsset(newColorsTile, path);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
// Select the new asset
Selection.activeObject = newColorsTile;
EditorGUIUtility.PingObject(newColorsTile);
// Refresh the list
RefreshColorsTilesList();
}
private void DeleteCurrentColorsTile()
{
if (target == null) return;
string assetPath = AssetDatabase.GetAssetPath(target);
if (string.IsNullOrEmpty(assetPath)) return;
if (EditorUtility.DisplayDialog("Delete ColorsTile",
$"Are you sure you want to delete '{target.name}'?\nThis action cannot be undone.",
"Delete", "Cancel"))
{
// Navigate to next asset before deleting
if (allColorsTiles != null && allColorsTiles.Length > 1)
{
int nextIndex = currentIndex + 1;
if (nextIndex >= allColorsTiles.Length) nextIndex = currentIndex - 1;
if (nextIndex >= 0 && nextIndex < allColorsTiles.Length && nextIndex != currentIndex)
{
var nextAsset = allColorsTiles[nextIndex];
if (nextAsset != null)
{
Selection.activeObject = nextAsset;
}
}
}
AssetDatabase.DeleteAsset(assetPath);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
RefreshColorsTilesList();
}
}
private void NavigateToAsset(int direction)
{
if (allColorsTiles == null || allColorsTiles.Length <= 1) return;
int newIndex = currentIndex + direction;
if (newIndex < 0) newIndex = allColorsTiles.Length - 1;
if (newIndex >= allColorsTiles.Length) newIndex = 0;
var newAsset = allColorsTiles[newIndex];
if (newAsset != null)
{
Selection.activeObject = newAsset;
EditorGUIUtility.PingObject(newAsset);
}
}
public override void OnInspectorGUI()
{
// Handle object picker for copy functionality
if (Event.current.commandName == "ObjectSelectorClosed")
{
var selectedObject = EditorGUIUtility.GetObjectPickerObject() as ColorsTile;
if (selectedObject != null)
{
var colorsTile = target as ColorsTile;
if (colorsTile != null)
{
serializedObject.FindProperty("faceColor").colorValue = selectedObject.faceColor;
serializedObject.FindProperty("topColor").colorValue = selectedObject.topColor;
serializedObject.FindProperty("bottomColor").colorValue = selectedObject.bottomColor;
serializedObject.ApplyModifiedProperties();
}
}
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a4b8c9d7e6f5a3b2c1d0e9f8a7b6c5d4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,195 @@
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using System.Collections.Generic;
using WordsToolkit.Scripts.Gameplay;
using WordsToolkit.Scripts.Settings;
public class ColorsTilePopupWindow : EditorWindow
{
private List<ColorsTile> colorTiles = new List<ColorsTile>();
private SerializedProperty targetProperty;
private float tileSize = 20f;
private float spacing = 3f;
private float padding = 5f;
private System.Action onSelectionChanged;
public static void Show(Rect activatorRect, SerializedProperty property, System.Action onSelectionChanged)
{
ColorsTilePopupWindow window = CreateInstance<ColorsTilePopupWindow>();
window.titleContent = new GUIContent("Select Color Tile");
window.targetProperty = property;
window.onSelectionChanged = onSelectionChanged;
window.LoadColorTiles();
var screenRect = GUIUtility.GUIToScreenRect(activatorRect);
Vector2 position = new Vector2(screenRect.x, screenRect.yMax);
float rowWidth = 6 * (window.tileSize + window.spacing) - window.spacing;
int totalTiles = window.colorTiles.Count + 1;
int rows = Mathf.CeilToInt((float)totalTiles / 6);
float contentWidth = rowWidth + (window.padding * 2);
float contentHeight = (rows * (window.tileSize + window.spacing)) - window.spacing + (window.padding * 2) + 25;
window.position = new Rect(position, new Vector2(contentWidth, contentHeight));
window.ShowPopup();
window.Focus();
}
private void LoadColorTiles()
{
colorTiles.Clear();
string[] guids = AssetDatabase.FindAssets("t:ColorsTile");
foreach (string guid in guids)
{
string assetPath = AssetDatabase.GUIDToAssetPath(guid);
ColorsTile colorTile = AssetDatabase.LoadAssetAtPath<ColorsTile>(assetPath);
if (colorTile != null)
{
colorTiles.Add(colorTile);
}
}
}
private void OnLostFocus()
{
Close();
}
private void CreateGUI()
{
var root = rootVisualElement;
root.style.paddingTop = padding;
root.style.paddingBottom = padding;
root.style.paddingLeft = padding;
root.style.paddingRight = padding;
root.style.flexDirection = FlexDirection.Column;
root.style.backgroundColor = new Color(0.1f, 0.1f, 0.1f, 1f);
var container = new VisualElement();
container.style.flexDirection = FlexDirection.Row;
container.style.flexWrap = Wrap.Wrap;
container.style.justifyContent = Justify.FlexStart;
container.style.alignContent = Align.FlexStart;
float rowWidth = 6 * (tileSize + spacing) - spacing;
container.style.width = rowWidth;
foreach (var tile in colorTiles)
{
var tileButton = CreateColorTileButton(tile);
container.Add(tileButton);
}
var addButton = CreateAddButton();
container.Add(addButton);
root.Add(container);
}
private Button CreateColorTileButton(ColorsTile tile)
{
var button = new Button(() => SelectTile(tile));
button.style.width = tileSize;
button.style.height = tileSize;
button.style.marginRight = spacing;
button.style.marginBottom = spacing;
button.style.backgroundColor = tile.faceColor;
button.style.borderTopWidth = 0;
button.style.borderBottomWidth = 0;
button.style.borderLeftWidth = 0;
button.style.borderRightWidth = 0;
button.style.flexShrink = 0;
button.style.flexGrow = 0;
bool isSelected = targetProperty != null && targetProperty.objectReferenceValue == tile;
if (isSelected)
{
var dot = new VisualElement();
float dotSize = tileSize * 0.4f;
dot.style.width = dotSize;
dot.style.height = dotSize;
dot.style.backgroundColor = Color.white;
dot.style.position = Position.Absolute;
dot.style.left = (tileSize - dotSize) / 2;
dot.style.top = (tileSize - dotSize) / 2;
dot.style.flexShrink = 0;
button.Add(dot);
}
return button;
}
private Button CreateAddButton()
{
var button = new Button(() => CreateNewColorsTile());
button.text = "+";
button.style.width = tileSize;
button.style.height = tileSize;
button.style.marginRight = spacing;
button.style.marginBottom = spacing;
button.style.flexShrink = 0;
button.style.flexGrow = 0;
return button;
}
private void SelectTile(ColorsTile tile)
{
targetProperty.objectReferenceValue = tile;
targetProperty.serializedObject.ApplyModifiedProperties();
EditorUtility.SetDirty(targetProperty.serializedObject.targetObject);
onSelectionChanged?.Invoke();
Close();
}
private void CreateNewColorsTile()
{
// Make sure the directory exists
string folderPath = "Assets/WordConnectGameToolkit/Resources/ColorsTile";
if (!System.IO.Directory.Exists(folderPath))
{
System.IO.Directory.CreateDirectory(folderPath);
AssetDatabase.Refresh();
}
string baseName = "NewColorsTile";
string fileName = baseName;
int counter = 1;
string path = $"{folderPath}/{fileName}.asset";
while (System.IO.File.Exists(path))
{
fileName = $"{baseName}_{counter}";
path = $"{folderPath}/{fileName}.asset";
counter++;
}
ColorsTile newColorsTile = ScriptableObject.CreateInstance<ColorsTile>();
// Load and assign the tile prefab
string prefabPath = "Assets/WordConnectGameToolkit/Prefabs/Game/Tile.prefab";
var tilePrefab = AssetDatabase.LoadAssetAtPath<FillAndPreview>(prefabPath);
if (tilePrefab != null)
{
newColorsTile.prefab = tilePrefab;
}
else
{
Debug.LogError($"Could not load Tile prefab at path: {prefabPath}");
}
AssetDatabase.CreateAsset(newColorsTile, path);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
SelectTile(newColorsTile);
// Select and highlight the new asset in the Project window
Selection.activeObject = newColorsTile;
EditorGUIUtility.PingObject(newColorsTile);
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e7a3fc0e02ed54267b36a36b295a1466
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 38232146e525448c0b57e1114e383a55
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 302c885e5557448fb8a02c22df487f16
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,54 @@
using UnityEngine;
using VContainer;
using WordsToolkit.Scripts.Gameplay.WordValidator;
using WordsToolkit.Scripts.NLP;
using WordsToolkit.Scripts.Services.BannedWords;
using WordsToolkit.Scripts.Services;
namespace WordsToolkit.Scripts.Levels.Editor
{
public static class EditorScope
{
private static IObjectResolver editorContainer;
public static IObjectResolver Container
{
get
{
if (editorContainer == null)
{
var builder = new ContainerBuilder();
Configure(builder);
editorContainer = builder.Build();
}
return editorContainer;
}
}
public static void Configure(IContainerBuilder builder)
{
var languageConfig = Resources.Load<LanguageConfiguration>("Settings/LanguageConfiguration");
var bannedWordsConfig = Resources.Load<BannedWordsConfiguration>("BannedWords/BannedWords");
if (!languageConfig || !bannedWordsConfig)
{
Debug.LogError("Failed to load required configurations for editor container");
return;
}
builder.RegisterInstance(languageConfig);
builder.RegisterInstance(bannedWordsConfig);
builder.Register<ILanguageService, LanguageService>(Lifetime.Singleton);
builder.Register<IBannedWordsService, BannedWordsService>(Lifetime.Singleton);
builder.Register<IModelController, ModelController>(Lifetime.Singleton);
builder.Register<ICustomWordRepository, CustomWordRepository>(Lifetime.Singleton);
builder.Register<IWordValidator, DefaultWordValidator>(Lifetime.Singleton);
}
public static T Resolve<T>() where T : class
{
return Container.Resolve<T>();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fe3b3d7c1fe9f4128848bad9a7cc5b08
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 06d9eaa919f7d4289927e7731170fab0
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,34 @@
using UnityEditor;
using UnityEngine;
using WordsToolkit.Scripts.Levels;
namespace WordsToolkit.Scripts.Levels.Editor.EditorWindows
{
public class CollectionStats
{
public int groupCount;
public int levelCount;
public static CollectionStats GetCollectionStats(string folderPath)
{
var stats = new CollectionStats();
// Get all level groups in the folder
var groupGuids = AssetDatabase.FindAssets("t:LevelGroup", new[] { folderPath });
stats.groupCount = groupGuids.Length;
// Count total levels across all groups
foreach (var guid in groupGuids)
{
var path = AssetDatabase.GUIDToAssetPath(guid);
var group = AssetDatabase.LoadAssetAtPath<LevelGroup>(path);
if (group != null && group.levels != null)
{
stats.levelCount += group.levels.Count;
}
}
return stats;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5ec01e6ad50034ca884917dd077bf01d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,251 @@
using UnityEditor;
using UnityEngine;
using System.Collections.Generic;
using System.IO;
using System.Linq;
namespace WordsToolkit.Scripts.Levels.Editor.EditorWindows
{
public static class LevelEditorUtility
{
private const string SELECTED_ASSET_GUID_KEY = "WordsToolkit_SelectedAssetGUID";
private const string LATEST_LEVEL_GUID_KEY = "WordsToolkit_LatestLevelGUID";
public static void SaveSelectedItem(LevelHierarchyItem selectedItem)
{
if (selectedItem == null) return;
string path = string.Empty;
Object asset = null;
switch (selectedItem.type)
{
case LevelHierarchyItem.ItemType.Level:
asset = selectedItem.levelAsset;
break;
case LevelHierarchyItem.ItemType.Group:
asset = selectedItem.groupAsset;
break;
}
if (asset != null)
{
path = AssetDatabase.GetAssetPath(asset);
string guid = AssetDatabase.AssetPathToGUID(path);
if (!string.IsNullOrEmpty(guid))
{
EditorPrefs.SetString(SELECTED_ASSET_GUID_KEY, guid);
}
}
}
public static void SaveLatestLevelSelection(LevelHierarchyItem selectedItem)
{
if (selectedItem?.type == LevelHierarchyItem.ItemType.Level && selectedItem.levelAsset != null)
{
string path = AssetDatabase.GetAssetPath(selectedItem.levelAsset);
string guid = AssetDatabase.AssetPathToGUID(path);
if (!string.IsNullOrEmpty(guid))
{
EditorPrefs.SetString(LATEST_LEVEL_GUID_KEY, guid);
}
}
}
public static void RestoreSelectedItem()
{
string guid = EditorPrefs.GetString(SELECTED_ASSET_GUID_KEY, string.Empty);
if (string.IsNullOrEmpty(guid)) return;
string path = AssetDatabase.GUIDToAssetPath(guid);
if (string.IsNullOrEmpty(path)) return;
var asset = AssetDatabase.LoadAssetAtPath<Object>(path);
if (asset != null)
{
Selection.activeObject = asset;
}
}
public static string GetDefaultLanguage()
{
// Implement your default language logic here
return "en";
}
public static string GetLanguageCodeForLevel(Level level)
{
if (level == null || level.languages == null || level.languages.Count == 0)
return null;
// Get currently selected language tab from EditorPrefs (same as Open Grid button)
int selectedTabIndex = EditorPrefs.GetInt("WordsToolkit_SelectedLanguageTab", 0);
// Get the language code for the selected tab
if (selectedTabIndex >= 0 && selectedTabIndex < level.languages.Count)
{
return level.languages[selectedTabIndex].language;
}
// Fallback to first available language
return level.languages[0].language;
}
public static void DeleteGroup(LevelGroup group)
{
if (group == null) return;
string assetPath = AssetDatabase.GetAssetPath(group);
if (string.IsNullOrEmpty(assetPath)) return;
var allGroups = AssetDatabase.FindAssets("t:LevelGroup")
.Select(guid => AssetDatabase.LoadAssetAtPath<LevelGroup>(AssetDatabase.GUIDToAssetPath(guid)))
.Where(g => g != null && g != group)
.ToList();
if (group.levels != null && group.levels.Count > 0)
{
var levelsToDelete = new List<Level>(group.levels);
foreach (var otherGroup in allGroups)
{
bool modified = false;
for (int i = otherGroup.levels.Count - 1; i >= 0; i--)
{
if (levelsToDelete.Contains(otherGroup.levels[i]))
{
otherGroup.levels.RemoveAt(i);
modified = true;
}
}
if (modified)
{
EditorUtility.SetDirty(otherGroup);
}
}
foreach (var level in levelsToDelete)
{
if (level != null)
{
string levelPath = AssetDatabase.GetAssetPath(level);
if (!string.IsNullOrEmpty(levelPath))
{
AssetDatabase.DeleteAsset(levelPath);
}
}
}
AssetDatabase.SaveAssets();
}
AssetDatabase.DeleteAsset(assetPath);
}
public static void DeleteLevel(Level level)
{
if (level == null) return;
var parentGroup = FindParentGroup(level);
if (parentGroup != null)
{
var serializedObject = new SerializedObject(parentGroup);
var levelsProperty = serializedObject.FindProperty("levels");
for (int i = 0; i < levelsProperty.arraySize; i++)
{
var elementProperty = levelsProperty.GetArrayElementAtIndex(i);
if (elementProperty.objectReferenceValue == level)
{
levelsProperty.DeleteArrayElementAtIndex(i);
break;
}
}
serializedObject.ApplyModifiedProperties();
EditorUtility.SetDirty(parentGroup);
AssetDatabase.SaveAssets();
}
string assetPath = AssetDatabase.GetAssetPath(level);
if (!string.IsNullOrEmpty(assetPath))
{
AssetDatabase.DeleteAsset(assetPath);
}
}
public static LevelGroup FindParentGroup(Level level)
{
if (level == null) return null;
var groups = AssetDatabase.FindAssets("t:LevelGroup")
.Select(guid => AssetDatabase.LoadAssetAtPath<LevelGroup>(AssetDatabase.GUIDToAssetPath(guid)))
.Where(g => g != null)
.ToList();
return groups.FirstOrDefault(g => g.levels != null && g.levels.Contains(level));
}
public static void RefreshHierarchy(LevelHierarchyTreeView m_HierarchyTree)
{
// Refresh assets and rebuild tree
AssetDatabase.Refresh();
m_HierarchyTree.Reload();
// Renumber levels sequentially based on current tree order
RenumberLevelsByOrder(m_HierarchyTree);
// Reload to update display names after renumbering
m_HierarchyTree.Reload();
// Restore previous selection after hierarchy refresh
// RestoreSelectedItem();
}
/// <summary>
/// Renumber all levels sequentially based on the hierarchy order, considering all levels even if groups are collapsed.
/// </summary>
/// <param name="m_HierarchyTree"></param>
private static void RenumberLevelsByOrder(LevelHierarchyTreeView m_HierarchyTree)
{
// Get all items regardless of collapsed state and sort them properly
var allLevels = GetAllLevelsInHierarchyOrder(m_HierarchyTree);
int number = 1;
foreach (var level in allLevels)
{
// Record undo for renaming
Undo.RecordObject(level, "Renumber Levels");
level.number = number;
EditorUtility.SetDirty(level);
number++;
}
// Save all changes to assets
AssetDatabase.SaveAssets();
}
/// <summary>
/// Gets all levels in proper hierarchy order, regardless of collapsed state.
/// </summary>
/// <param name="hierarchyTree"></param>
/// <returns>All levels sorted in hierarchy display order</returns>
private static List<Level> GetAllLevelsInHierarchyOrder(LevelHierarchyTreeView hierarchyTree)
{
var allItems = hierarchyTree.GetAllItems();
var levelItems = allItems.Where(item => item.type == LevelHierarchyItem.ItemType.Level && item.levelAsset != null).ToList();
// Sort level items by their hierarchy order:
// 1. Group levels by their parent group
// 2. Sort groups by name/hierarchy
// 3. Within each group, sort levels by their current number
var groupedLevels = levelItems
.GroupBy(item => FindParentGroup(item.levelAsset))
.OrderBy(group => group.Key?.name ?? "")
.SelectMany(group => group.OrderBy(item => item.levelAsset.number))
.Select(item => item.levelAsset)
.ToList();
return groupedLevels;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1806c13ab2db34e6ea22b6784e8597d5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,319 @@
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using UnityEditor.IMGUI.Controls;
using System;
namespace WordsToolkit.Scripts.Levels.Editor.EditorWindows
{
[ExecuteAlways]
public class LevelManagerWindow : EditorWindow
{
// Static field to track all windows and handle script recompilation
private static List<LevelManagerWindow> activeWindows = new List<LevelManagerWindow>();
// Static event for when hierarchy selection changes - other windows can subscribe to this
public static event Action<LevelHierarchyItem> OnHierarchySelectionChanged;
// Tree view for hierarchy
private LevelHierarchyTreeView m_HierarchyTree;
private TreeViewState m_TreeViewState;
private MultiColumnHeaderState m_MultiColumnHeaderState;
// UI Elements
public VisualElement m_RightPanel;
public VisualElement m_InspectorContainer;
private Label m_HeaderLabel;
private TextField m_LevelNumberField;
private VisualElement m_LevelTitleContainer;
public VisualElement m_ActionButtonsContainer;
// Currently selected item
private LevelHierarchyItem m_SelectedItem;
// Selected language for level editing
private string m_SelectedLanguage;
private const string SELECTED_LANGUAGE_KEY = "WordsToolkit_SelectedLanguage";
// Selection persistence
private const string SELECTED_ASSET_GUID_KEY = "WordsToolkit_SelectedAssetGUID";
private const string LATEST_LEVEL_GUID_KEY = "WordsToolkit_LatestLevelGUID";
// Scroll position for the inspector scrollview
private Vector2 inspectorScrollPosition;
// Cache for editors to prevent GC allocations, exposed publicly to allow ref access
public UnityEditor.Editor cachedEditor;
[MenuItem("WordConnect/Editor/Level Editor _C", false, 1000)]
public static void ShowWindow()
{
var window = GetWindow<LevelManagerWindow>();
window.titleContent = new GUIContent("Level Editor");
window.minSize = new Vector2(1000, 500);
}
private void OnEnable()
{
// Register this window instance
if (!activeWindows.Contains(this))
{
activeWindows.Add(this);
}
// Initialize UI
LevelManagerWindowUI.InitializeUI(this);
// Load selected language preference
m_SelectedLanguage = EditorPrefs.GetString(SELECTED_LANGUAGE_KEY, LevelEditorUtility.GetDefaultLanguage());
InitializeTreeView();
}
private void OnDisable()
{
// Save the current selection when window is closed or disabled
LevelEditorUtility.SaveSelectedItem(m_SelectedItem);
// Save selected language preference
if (!string.IsNullOrEmpty(m_SelectedLanguage))
EditorPrefs.SetString(SELECTED_LANGUAGE_KEY, m_SelectedLanguage);
// Unregister this window instance
activeWindows.Remove(this);
}
private void InitializeTreeView()
{
// Create tree view state if needed
if (m_TreeViewState == null)
m_TreeViewState = new TreeViewState();
// Column header
var headerState = LevelHierarchyTreeView.CreateDefaultMultiColumnHeaderState();
if (m_MultiColumnHeaderState == null)
m_MultiColumnHeaderState = headerState;
var multiColumnHeader = new MultiColumnHeader(headerState);
multiColumnHeader.ResizeToFit();
m_HierarchyTree = new LevelHierarchyTreeView(m_TreeViewState, multiColumnHeader);
m_HierarchyTree.OnSelectionChanged += OnTreeSelectionChanged;
m_HierarchyTree.OnDeleteItem += OnDeleteRequested;
m_HierarchyTree.OnCreateSubgroup += OnTreeViewCreateSubgroup;
m_HierarchyTree.OnCreateLevel += OnTreeViewCreateLevel;
m_HierarchyTree.OnHierarchyChanged += OnHierarchyChanged;
// Refresh the tree
m_HierarchyTree.Reload();
m_HierarchyTree.ExpandAll();
// Auto-select the latest level, but only if not during compilation
if (!EditorApplication.isCompiling && !EditorApplication.isUpdating)
{
EditorApplication.delayCall += () =>
{
// Double check we're still not compiling after the delay
if (!EditorApplication.isCompiling && !EditorApplication.isUpdating)
{
Level latestLevel = GetLastWorkedOnLevel();
if (latestLevel != null)
{
m_HierarchyTree.SelectAsset(latestLevel);
}
}
// Trigger selection changed event to update UI
m_HierarchyTree.OnSelectionChanged?.Invoke(m_HierarchyTree.GetSelectedItem());
};
}
else
{
// If compiling, just trigger the selection event without auto-selecting
m_HierarchyTree.OnSelectionChanged?.Invoke(m_HierarchyTree.GetSelectedItem());
}
}
private void OnHierarchyChanged()
{
LevelEditorUtility.RefreshHierarchy(m_HierarchyTree);
}
private void OnTreeViewCreateLevel(LevelHierarchyItem parentItem)
{
if (parentItem?.groupAsset == null) return;
var newItem = m_HierarchyTree.CreateLevel(parentItem);
if (newItem != null)
{
EditorApplication.delayCall += () =>
{
m_HierarchyTree.SetSelection(new List<int> { newItem.id }, TreeViewSelectionOptions.RevealAndFrame);
OnTreeSelectionChanged(newItem);
};
}
}
private void OnTreeViewCreateSubgroup(LevelHierarchyItem parentItem)
{
// Use CreateSubGroup which automatically creates a level inside the new group
var newItem = m_HierarchyTree.CreateSubGroup(parentItem);
}
private void OnLostFocus()
{
// // Save current state when window loses focus
// if (m_SelectedItem != null && m_SelectedItem.levelAsset != null)
// {
// // Notify LevelDataEditor that it needs to update this level
// LevelDataEditor.NotifyLevelNeedsUpdate(m_SelectedItem.levelAsset);
// }
LevelEditorUtility.SaveSelectedItem(m_SelectedItem);
if (!string.IsNullOrEmpty(m_SelectedLanguage))
EditorPrefs.SetString(SELECTED_LANGUAGE_KEY, m_SelectedLanguage);
}
private void OnDeleteRequested(LevelHierarchyItem item)
{
if (item == null) return;
switch (item.type)
{
case LevelHierarchyItem.ItemType.Group:
if (EditorUtility.DisplayDialog("Delete Group",
"Are you sure you want to delete this group and all its levels?", "Yes", "No"))
{
LevelEditorUtility.DeleteGroup(item.groupAsset);
}
break;
case LevelHierarchyItem.ItemType.Level:
if (EditorUtility.DisplayDialog("Delete Level",
"Are you sure you want to delete this level?", "Yes", "No"))
{
LevelEditorUtility.DeleteLevel(item.levelAsset);
}
break;
}
LevelEditorUtility.RefreshHierarchy(m_HierarchyTree);
}
private void OnTreeSelectionChanged(LevelHierarchyItem selectedItem)
{
m_SelectedItem = selectedItem;
inspectorScrollPosition = Vector2.zero;
LevelManagerWindowUI.UpdateInspector(this, m_SelectedItem);
// Save selections
LevelEditorUtility.SaveSelectedItem(selectedItem);
LevelEditorUtility.SaveLatestLevelSelection(selectedItem);
// Trigger static event for other windows to listen to
OnHierarchySelectionChanged?.Invoke(selectedItem);
}
// Expose necessary properties for other classes
public VisualElement RightPanel => m_RightPanel;
public VisualElement ActionButtonsContainer => m_ActionButtonsContainer;
public VisualElement LevelTitleContainer
{
get => m_LevelTitleContainer;
set => m_LevelTitleContainer = value;
}
public Label HeaderLabel
{
get => m_HeaderLabel;
set => m_HeaderLabel = value;
}
public TextField LevelNumberField
{
get => m_LevelNumberField;
set => m_LevelNumberField = value;
}
// Editor field is exposed publicly to allow ref access in other classes
public Vector2 InspectorScrollPosition { get => inspectorScrollPosition; set => inspectorScrollPosition = value; }
public LevelHierarchyTreeView HierarchyTree => m_HierarchyTree;
public LevelHierarchyItem SelectedItem { get => m_HierarchyTree?.GetSelectedItem(); set => m_SelectedItem = value; }
public VisualElement InspectorContainer => m_InspectorContainer;
public LevelHierarchyTreeView GetHierarchyTree()
{
return m_HierarchyTree;
}
/// <summary>
/// Static method to refresh inspector in all open LevelManagerWindow instances
/// Called from LevelDataEditor.HandleLevelUpdate when available words are updated
/// </summary>
public static void RefreshInspectorForLevel(Level level)
{
if (level == null) return;
foreach (var window in activeWindows)
{
if (window != null &&
window.SelectedItem != null &&
window.SelectedItem.type == LevelHierarchyItem.ItemType.Level &&
window.SelectedItem.levelAsset == level)
{
// Refresh the inspector for this level
LevelManagerWindowUI.UpdateInspector(window, window.SelectedItem);
}
}
}
/// <summary>
/// Public method to trigger selection changed event and update UI.
/// Used by NavigateLevel to ensure proper event propagation to other windows.
/// </summary>
public void TriggerSelectionChanged(LevelHierarchyItem selectedItem)
{
// Update internal state
m_SelectedItem = selectedItem;
inspectorScrollPosition = Vector2.zero;
// Update the inspector
LevelManagerWindowUI.UpdateInspector(this, m_SelectedItem);
// Save selections
LevelEditorUtility.SaveSelectedItem(selectedItem);
LevelEditorUtility.SaveLatestLevelSelection(selectedItem);
// Trigger static event for other windows to listen to
OnHierarchySelectionChanged?.Invoke(selectedItem);
}
private Level GetLastWorkedOnLevel()
{
string guid = EditorPrefs.GetString(LATEST_LEVEL_GUID_KEY, string.Empty);
if (!string.IsNullOrEmpty(guid))
{
string path = AssetDatabase.GUIDToAssetPath(guid);
if (!string.IsNullOrEmpty(path))
{
var level = AssetDatabase.LoadAssetAtPath<Level>(path);
if (level != null)
return level;
}
}
// Fallback: Find the level with the highest number (most recently created)
Level[] allLevels = Resources.LoadAll<Level>("Levels");
if (allLevels.Length > 0)
{
return allLevels.OrderByDescending(l => l.number).FirstOrDefault();
}
return null;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8bf3e0bb5a06c4dd7875f49962721675
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,834 @@
using System;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using System.Collections.Generic;
using System.Linq;
using UnityEditor.IMGUI.Controls;
using UnityEditor.UIElements;
using Object = UnityEngine.Object;
using WordsToolkit.Scripts.System;
using WordsToolkit.Scripts.Gameplay.Managers;
using WordsToolkit.Scripts.Enums;
using System.Globalization;
namespace WordsToolkit.Scripts.Levels.Editor.EditorWindows
{
public static class LevelManagerWindowUI
{
public static void InitializeUI(LevelManagerWindow window)
{
// Load and apply stylesheet
var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/WordConnectGameToolkit/Scripts/Levels/Editor/LevelManagerStyles.uss");
if (styleSheet != null)
window.rootVisualElement.styleSheets.Add(styleSheet);
CreateBasicLayout(window);
}
private static void CreateBasicLayout(LevelManagerWindow window)
{
// Split view layout - making right panel (now first) narrower at 300px, left panel (now second) wider
var splitView = new TwoPaneSplitView(0, 400, TwoPaneSplitViewOrientation.Horizontal);
splitView.style.width = new StyleLength(new Length(100, LengthUnit.Percent));
var toolbar = CreateLeftPanelToolbar(window);
window.rootVisualElement.Add(toolbar);
window.rootVisualElement.Add(splitView);
// Create left panel
var leftPanel = CreateLeftPanel(window);
// Create right panel
var rightPanel = CreateRightPanel(window);
// Add panels in switched order - right panel first, then left panel
splitView.Add(rightPanel);
splitView.Add(leftPanel);
}
private static VisualElement CreateLeftPanel(LevelManagerWindow window)
{
var leftPanel = new VisualElement();
leftPanel.style.flexGrow = 1;
leftPanel.style.width = new StyleLength(new Length(100, LengthUnit.Percent));
leftPanel.AddToClassList("left-panel");
var treeViewContainer = CreateTreeViewContainer(window);
leftPanel.Add(treeViewContainer);
return leftPanel;
}
private static Toolbar CreateLeftPanelToolbar(LevelManagerWindow window)
{
var toolbar = new Toolbar();
toolbar.style.width = new StyleLength(new Length(100, LengthUnit.Percent));
toolbar.style.flexDirection = FlexDirection.Row;
toolbar.style.flexWrap = Wrap.NoWrap;
toolbar.AddToClassList("toolbar");
var refreshButton = new ToolbarButton(() =>
{
LevelEditorUtility.RefreshHierarchy(window.HierarchyTree);
}) { text = "Refresh" };
refreshButton.style.width = 60;
refreshButton.AddToClassList("toolbar-button");
var createButton = new ToolbarMenu { text = " Create" };
createButton.AddToClassList("toolbar-menu");
// Populate Create menu
createButton.menu.AppendAction("New Root Group", _ =>
{
window.HierarchyTree?.CreateRootGroup();
window.HierarchyTree?.Reload();
});
// New Subgroup - only enabled when a group is selected
createButton.menu.AppendAction("New Subgroup", _ =>
{
var sel = window.SelectedItem;
if (sel != null && sel.type == LevelHierarchyItem.ItemType.Group)
{
window.HierarchyTree?.OnCreateSubgroup?.Invoke(sel);
window.HierarchyTree?.Reload();
}
}, _ =>
window.SelectedItem != null && window.SelectedItem.type == LevelHierarchyItem.ItemType.Group
? DropdownMenuAction.Status.Normal
: DropdownMenuAction.Status.Disabled);
// New Level - only enabled when a group is selected
createButton.menu.AppendAction("New Level", _ =>
{
var sel = window.SelectedItem;
if (sel != null && sel.type == LevelHierarchyItem.ItemType.Group)
{
window.HierarchyTree?.OnCreateLevel?.Invoke(sel);
window.HierarchyTree?.Reload();
}
}, _ =>
window.SelectedItem != null && window.SelectedItem.type == LevelHierarchyItem.ItemType.Group
? DropdownMenuAction.Status.Normal
: DropdownMenuAction.Status.Disabled);
var deleteButton = new ToolbarButton(() => {
if (window.SelectedItem != null)
{
switch (window.SelectedItem.type)
{
case LevelHierarchyItem.ItemType.Group:
if (EditorUtility.DisplayDialog("Delete Group",
"Are you sure you want to delete this group and all its levels?", "Yes", "No"))
{
LevelEditorUtility.DeleteGroup(window.SelectedItem.groupAsset);
window.HierarchyTree?.Reload();
}
break;
case LevelHierarchyItem.ItemType.Level:
if (EditorUtility.DisplayDialog("Delete Level",
"Are you sure you want to delete this level?", "Yes", "No"))
{
LevelEditorUtility.DeleteLevel(window.SelectedItem.levelAsset);
window.HierarchyTree?.Reload();
}
break;
}
}
}) { text = "Delete" };
deleteButton.AddToClassList("toolbar-button");
var languageConfigButton = new ToolbarButton(() => {
var languageConfig = AssetDatabase.LoadAssetAtPath<ScriptableObject>("Assets/WordConnectGameToolkit/Resources/Settings/LanguageConfiguration.asset");
if (languageConfig != null)
{
Selection.activeObject = languageConfig;
// Focus the Inspector window to make it active
var inspectorType = typeof(UnityEditor.Editor).Assembly.GetType("UnityEditor.InspectorWindow");
if (inspectorType != null)
{
var inspectorWindow = EditorWindow.GetWindow(inspectorType);
if (inspectorWindow != null)
{
inspectorWindow.Focus();
}
}
EditorGUIUtility.PingObject(languageConfig);
}
}) { text = "Language Config" };
languageConfigButton.AddToClassList("toolbar-button");
var bannedWordsButton = CreateBannedWordsButton();
bannedWordsButton.AddToClassList("toolbar-button");
var deleteAllButton = new ToolbarButton(() => {
if (EditorUtility.DisplayDialog("Delete All Levels and Groups",
"Are you sure you want to delete ALL levels and groups? This action cannot be undone!",
"Yes, Delete Everything", "Cancel"))
{
// Find and delete all level groups
string[] groupGuids = AssetDatabase.FindAssets("t:LevelGroup");
foreach (string guid in groupGuids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
AssetDatabase.DeleteAsset(path);
}
// Find and delete all levels
string[] levelGuids = AssetDatabase.FindAssets("t:Level");
foreach (string guid in levelGuids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
AssetDatabase.DeleteAsset(path);
}
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
window.HierarchyTree?.Reload();
EditorUtility.DisplayDialog("Deletion Complete",
"All levels and groups have been deleted.", "OK");
}
}) { text = "Delete All Levels" };
deleteAllButton.AddToClassList("toolbar-button");
toolbar.Add(refreshButton);
// toolbar.Add(createButton);
// toolbar.Add(deleteButton);
toolbar.Add(languageConfigButton);
toolbar.Add(bannedWordsButton);
toolbar.Add(deleteAllButton);
return toolbar;
}
private static ToolbarButton CreateBannedWordsButton()
{
return new ToolbarButton(() => {
var bannedWords = AssetDatabase.LoadAssetAtPath<ScriptableObject>("Assets/WordConnectGameToolkit/Resources/BannedWords/BannedWords.asset");
if (bannedWords != null)
{
Selection.activeObject = bannedWords;
// Focus the Inspector window to make it active
var inspectorType = typeof(UnityEditor.Editor).Assembly.GetType("UnityEditor.InspectorWindow");
if (inspectorType != null)
{
var inspectorWindow = EditorWindow.GetWindow(inspectorType);
if (inspectorWindow != null)
{
inspectorWindow.Focus();
}
}
EditorGUIUtility.PingObject(bannedWords);
}
})
{
text = "Banned Words"
};
}
private static IMGUIContainer CreateTreeViewContainer(LevelManagerWindow window)
{
return new IMGUIContainer(() =>
{
if (window.HierarchyTree != null)
{
Rect rect = EditorGUILayout.GetControlRect(
false,
GUILayout.ExpandHeight(true),
GUILayout.ExpandWidth(true)
);
window.HierarchyTree.OnGUI(rect);
}
})
{
style = { flexGrow = 1 }
};
}
private static VisualElement CreateRightPanel(LevelManagerWindow window)
{
window.m_RightPanel = new VisualElement();
window.m_RightPanel.AddToClassList("right-panel");
var levelControlPanel = CreateLevelControlPanel(window);
window.m_RightPanel.Add(levelControlPanel);
window.m_ActionButtonsContainer = new VisualElement();
window.m_ActionButtonsContainer.AddToClassList("action-buttons-container");
window.m_RightPanel.Add(window.m_ActionButtonsContainer);
// Create inspector container for UIToolkit content
window.m_InspectorContainer = new VisualElement();
window.m_InspectorContainer.AddToClassList("inspector-container");
window.m_InspectorContainer.style.flexGrow = 1;
window.m_InspectorContainer.style.width = new StyleLength(new Length(100, LengthUnit.Percent));
window.m_RightPanel.Add(window.m_InspectorContainer);
return window.m_RightPanel;
}
private static VisualElement CreateLevelControlPanel(LevelManagerWindow window)
{
var levelControlPanel = new VisualElement();
levelControlPanel.style.flexDirection = FlexDirection.Row;
levelControlPanel.style.alignItems = Align.Center;
levelControlPanel.style.justifyContent = Justify.FlexStart;
levelControlPanel.AddToClassList("header-panel");
// Create navigation controls
var prevButton = new Button(() => NavigateLevel(window, -1)) { text = "<" };
prevButton.AddToClassList("nav-button");
// Create level title container
window.LevelTitleContainer = new VisualElement();
window.LevelTitleContainer.style.flexDirection = FlexDirection.Row;
window.LevelTitleContainer.style.alignItems = Align.Center;
var levelLabel = new Label("Level");
levelLabel.style.marginRight = 5;
window.LevelTitleContainer.Add(levelLabel);
window.LevelNumberField = new TextField();
window.LevelNumberField.style.width = 40;
window.LevelNumberField.style.marginLeft = 3;
window.LevelNumberField.style.marginRight = 3;
window.LevelNumberField.style.paddingLeft = 5;
window.LevelNumberField.style.paddingRight = 5;
window.LevelNumberField.style.paddingTop = 0;
window.LevelNumberField.style.paddingBottom = 0;
window.LevelNumberField.style.unityTextAlign = TextAnchor.MiddleCenter;
window.LevelNumberField.isDelayed = true;
window.LevelNumberField.RegisterValueChangedCallback(evt => {
if (int.TryParse(evt.newValue, out int levelNum))
{
JumpToLevel(window, levelNum);
}
});
window.LevelTitleContainer.Add(window.LevelNumberField);
window.HeaderLabel = new Label("No Selection");
window.HeaderLabel.AddToClassList("header-label");
window.HeaderLabel.style.fontSize = 14;
window.HeaderLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
window.HeaderLabel.style.flexGrow = 1;
var nextButton = new Button(() => NavigateLevel(window, 1)) { text = ">" };
nextButton.AddToClassList("nav-button");
var addButton = new Button(() => AddLevel(window)) { text = "+" };
addButton.AddToClassList("nav-button");
var removeButton = new Button(() => RemoveLevel(window)) { text = "-" };
removeButton.AddToClassList("nav-button");
// Open Grid button
var openGridButton = new Button(() => {
var selectedItem = window.SelectedItem;
if (selectedItem?.levelAsset != null)
{
// Get language code using the static utility method
string languageCode = LevelEditorUtility.GetLanguageCodeForLevel(selectedItem.levelAsset);
if (!string.IsNullOrEmpty(languageCode))
{
// Open the CrosswordGridWindow
WordsToolkit.Scripts.Levels.Editor.CrosswordGridWindow.ShowWindow(selectedItem.levelAsset, languageCode);
}
}
}) { text = "Open Grid" };
openGridButton.style.flexGrow = 1;
openGridButton.style.backgroundColor = new Color(112 / 255f, 63 / 255f, 33 / 255f, 1f);
openGridButton.style.height = 30;
openGridButton.style.marginRight = 5;
// // Test level button
// var testButton = new Button(() => {
// var selectedItem = window.SelectedItem;
// if (selectedItem?.levelAsset != null)
// {
// // Get currently selected language tab from LevelDataEditor
// int selectedTabIndex = EditorPrefs.GetInt("WordsToolkit_SelectedLanguageTab", 0);
// string languageCode = selectedItem.levelAsset.languages[selectedTabIndex].language;
//
// // Use the encapsulated TestLevel method
// LevelEditorServices.TestLevel(selectedItem.levelAsset, languageCode);
// }
// }) { text = "Test level" };
// Add all controls to panel
levelControlPanel.Add(window.HeaderLabel); // Move header label to first position
levelControlPanel.Add(prevButton);
levelControlPanel.Add(window.LevelTitleContainer);
levelControlPanel.Add(nextButton);
levelControlPanel.Add(addButton);
levelControlPanel.Add(removeButton);
levelControlPanel.Add(openGridButton);
// levelControlPanel.Add(testButton);
return levelControlPanel;
}
private static void NavigateLevel(LevelManagerWindow window, int direction)
{
if (window.SelectedItem == null || window.SelectedItem.type == LevelHierarchyItem.ItemType.Group)
return;
var level = window.SelectedItem.levelAsset;
if (level == null) return;
var targetIndex = level.number + direction;
var allLevels = Resources.LoadAll<Level>("Levels").ToList();
var targetLevel = allLevels.FirstOrDefault(l => l.number == targetIndex);
if (targetLevel != null)
{
window.HierarchyTree.SelectAsset(targetLevel);
window.LevelNumberField.value = targetLevel.number.ToString();
// Get the selected item and trigger the selection changed event
var selectedItem = window.HierarchyTree.GetSelectedItem();
if (selectedItem != null)
{
// Update the window's internal state and trigger events
window.TriggerSelectionChanged(selectedItem);
}
}
else
{
// Show error and reset field
if (window.SelectedItem.type == LevelHierarchyItem.ItemType.Level)
{
window.LevelNumberField.value = window.SelectedItem.levelAsset.number.ToString();
}
else
{
window.LevelNumberField.value = "1";
}
EditorUtility.DisplayDialog("Invalid Level Number",
$"Level {targetIndex} not found in this group.", "OK");
}
}
private static void JumpToLevel(LevelManagerWindow window, int levelNum)
{
if (window.SelectedItem == null) return;
var allLevels = Resources.LoadAll<Level>("Levels").ToList();
// Find the level with the given number
var targetLevel = allLevels.FirstOrDefault(l => l.number == levelNum);
// If level not found, show error and reset field
if (targetLevel == null)
{
// Show error and reset field to current level
if (window.SelectedItem.type == LevelHierarchyItem.ItemType.Level)
{
window.LevelNumberField.value = window.SelectedItem.levelAsset.number.ToString();
}
else
{
window.LevelNumberField.value = "1";
}
EditorUtility.DisplayDialog("Invalid Level Number",
$"Level {levelNum} not found in this group.", "OK");
return;
}
// Level found, select it
if (targetLevel != null)
{
window.HierarchyTree.SelectAsset(targetLevel);
// Get the selected item and trigger the selection changed event
var selectedItem = window.HierarchyTree.GetSelectedItem();
if (selectedItem != null)
{
// Update the window's internal state and trigger events
window.TriggerSelectionChanged(selectedItem);
}
}
}
private static void AddLevel(LevelManagerWindow window)
{
if (window.SelectedItem == null) return;
// If a level is selected, get its parent group
LevelGroup parentGroup = null;
if (window.SelectedItem.type == LevelHierarchyItem.ItemType.Level && window.SelectedItem.levelAsset != null)
{
parentGroup = FindParentGroup(window.SelectedItem.levelAsset);
}
// If a group is selected, use it directly
else if (window.SelectedItem.type == LevelHierarchyItem.ItemType.Group && window.SelectedItem.groupAsset != null)
{
parentGroup = window.SelectedItem.groupAsset;
}
if (parentGroup != null)
{
var levelItem = new LevelHierarchyItem
{
type = LevelHierarchyItem.ItemType.Group,
groupAsset = parentGroup
};
window.HierarchyTree.OnCreateLevel?.Invoke(levelItem);
}
}
private static void RemoveLevel(LevelManagerWindow window)
{
if (window.SelectedItem == null ||
window.SelectedItem.type != LevelHierarchyItem.ItemType.Level ||
window.SelectedItem.levelAsset == null)
return;
var level = window.SelectedItem.levelAsset;
var parentGroup = FindParentGroup(level);
if (parentGroup != null)
{
// Get the level index before deletion
int levelIndex = -1;
if (parentGroup.levels != null)
{
levelIndex = parentGroup.levels.IndexOf(level);
}
// Show confirmation dialog
if (EditorUtility.DisplayDialog("Delete Level",
$"Are you sure you want to delete Level {level.number }?",
"Delete", "Cancel"))
{
// Remove from parent group
if (parentGroup.levels != null)
{
parentGroup.levels.Remove(level);
EditorUtility.SetDirty(parentGroup);
}
// Delete the asset
string assetPath = AssetDatabase.GetAssetPath(level);
if (!string.IsNullOrEmpty(assetPath))
{
AssetDatabase.DeleteAsset(assetPath);
}
// Save changes and notify about hierarchy change
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
// Notify about the change in hierarchy
window.HierarchyTree?.OnHierarchyChanged?.Invoke();
window.HierarchyTree?.Reload();
// Try to select another level or the parent group
if (parentGroup.levels != null && parentGroup.levels.Count > 0)
{
// Try to select the same index or the previous one
int newIndex = Mathf.Min(levelIndex, parentGroup.levels.Count - 1);
if (newIndex >= 0 && newIndex < parentGroup.levels.Count)
{
window.HierarchyTree.SelectAsset(parentGroup.levels[newIndex]);
}
}
else
{
// If no levels left, select the group
window.HierarchyTree.SelectAsset(parentGroup);
}
}
}
}
private static void AddCollectionButtons(VisualElement buttonContainer, LevelHierarchyItem item)
{
var addGroupButton = new Button(() =>
{
var newGroup = ScriptableObject.CreateInstance<LevelGroup>();
newGroup.name = "New Group";
string assetPath = AssetDatabase.GenerateUniqueAssetPath($"{item.folderPath}/NewGroup.asset");
AssetDatabase.CreateAsset(newGroup, assetPath);
AssetDatabase.SaveAssets();
EditorUtility.SetDirty(newGroup);
})
{
text = "Add Group"
};
addGroupButton.AddToClassList("action-button");
buttonContainer.Add(addGroupButton);
}
private static void AddLevelButtons(VisualElement buttonContainer, LevelHierarchyItem item)
{
// Test button has been moved to the level control panel, so this method is now empty
// but kept for potential future level-specific buttons
}
public static void UpdateInspector(LevelManagerWindow window, LevelHierarchyItem selectedItem)
{
// Clear previous editor state
window.ActionButtonsContainer.Clear();
window.InspectorContainer.Clear();
if (window.cachedEditor != null)
{
Object.DestroyImmediate(window.cachedEditor);
window.cachedEditor = null;
}
UpdateHeaderDisplay(window, selectedItem);
CreateActionButtonsForSelection(window, selectedItem);
PopulateInspectorContainer(window, selectedItem);
window.Repaint();
}
private static void PopulateInspectorContainer(LevelManagerWindow window, LevelHierarchyItem selectedItem)
{
if (selectedItem == null)
{
var helpBox = new HelpBox("Select an item from the hierarchy to edit its properties", HelpBoxMessageType.Info);
window.InspectorContainer.Add(helpBox);
var spacer = new VisualElement { style = { height = 10 } };
window.InspectorContainer.Add(spacer);
var createButton = new Button(() =>
{
window.HierarchyTree?.CreateRootGroup();
window.HierarchyTree?.Reload();
})
{
text = "Create New Group"
};
createButton.style.height = 30;
window.InspectorContainer.Add(createButton);
return;
}
switch (selectedItem.type)
{
case LevelHierarchyItem.ItemType.Collection:
CreateCollectionInspectorUI(window, selectedItem);
break;
case LevelHierarchyItem.ItemType.Group:
CreateGroupInspectorUI(window, selectedItem);
break;
case LevelHierarchyItem.ItemType.Level:
CreateLevelInspectorUI(window, selectedItem);
break;
default:
var unknownLabel = new Label("Unknown item type");
window.InspectorContainer.Add(unknownLabel);
break;
}
}
private static void CreateCollectionInspectorUI(LevelManagerWindow window, LevelHierarchyItem item)
{
var titleLabel = new Label($"Collection Folder: {item.folderPath}");
titleLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
window.InspectorContainer.Add(titleLabel);
var spacer = new VisualElement { style = { height = 10 } };
window.InspectorContainer.Add(spacer);
var stats = CollectionStats.GetCollectionStats(item.folderPath);
var statsContainer = new VisualElement();
statsContainer.style.backgroundColor = new Color(0.2f, 0.2f, 0.2f, 0.3f);
statsContainer.style.paddingTop = 10;
statsContainer.style.paddingBottom = 10;
statsContainer.style.paddingLeft = 10;
statsContainer.style.paddingRight = 10;
statsContainer.style.marginTop = 5;
statsContainer.style.marginBottom = 5;
var groupCountLabel = new Label($"Total Groups: {stats.groupCount}");
groupCountLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
statsContainer.Add(groupCountLabel);
var levelCountLabel = new Label($"Total Levels: {stats.levelCount}");
levelCountLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
statsContainer.Add(levelCountLabel);
window.InspectorContainer.Add(statsContainer);
}
private static void CreateGroupInspectorUI(LevelManagerWindow window, LevelHierarchyItem item)
{
if (item.groupAsset == null) return;
UnityEditor.Editor.CreateCachedEditor(item.groupAsset, null, ref window.cachedEditor);
if (window.cachedEditor != null)
{
// Check if the editor supports UI Toolkit (like our new LevelGroupEditor)
if (window.cachedEditor is LevelGroupEditor levelGroupEditor)
{
// Use the UI Toolkit CreateInspectorGUI method
var inspectorUI = levelGroupEditor.CreateInspectorGUI();
if (inspectorUI != null)
{
// Configure the inspector UI to fit the window
inspectorUI.style.width = new StyleLength(new Length(100, LengthUnit.Percent));
inspectorUI.style.height = new StyleLength(new Length(100, LengthUnit.Percent));
inspectorUI.style.flexGrow = 1;
inspectorUI.style.flexShrink = 1;
// Add directly to container without additional scroll view wrapper
window.InspectorContainer.Add(inspectorUI);
// Refresh the UI to ensure it's up to date
levelGroupEditor.RefreshUI();
}
}
}
}
private static void CreateLevelInspectorUI(LevelManagerWindow window, LevelHierarchyItem item)
{
if (item.levelAsset == null) return;
UnityEditor.Editor.CreateCachedEditor(item.levelAsset, null, ref window.cachedEditor);
if (window.cachedEditor != null)
{
// Check if the editor is a LevelDataEditor and supports UIToolkit
if (window.cachedEditor is LevelDataEditor levelDataEditor)
{
// Use the UIToolkit CreateInspectorGUI method
var inspectorUI = levelDataEditor.CreateInspectorGUI();
if (inspectorUI != null)
{
// Configure the inspector UI to fit the window
inspectorUI.style.width = new StyleLength(new Length(100, LengthUnit.Percent));
inspectorUI.style.height = new StyleLength(new Length(100, LengthUnit.Percent));
inspectorUI.style.flexGrow = 1;
inspectorUI.style.flexShrink = 1;
// Ensure container is clear before adding (safety check)
if (window.InspectorContainer.childCount > 0)
{
window.InspectorContainer.Clear();
}
// Add directly to container without additional scroll view wrapper
window.InspectorContainer.Add(inspectorUI);
}
}
}
else
{
Debug.LogWarning("Failed to create cached editor for level asset");
}
}
private static void CreateActionButtonsForSelection(LevelManagerWindow window, LevelHierarchyItem selectedItem)
{
if (selectedItem == null) return;
var buttonContainer = new VisualElement();
buttonContainer.AddToClassList("button-row");
switch (selectedItem.type)
{
case LevelHierarchyItem.ItemType.Collection:
AddCollectionButtons(buttonContainer, selectedItem);
break;
case LevelHierarchyItem.ItemType.Level:
AddLevelButtons(buttonContainer, selectedItem);
break;
}
window.ActionButtonsContainer.Add(buttonContainer);
}
private static void UpdateHeaderDisplay(LevelManagerWindow window, LevelHierarchyItem selectedItem)
{
var levelControlPanel = window.m_RightPanel.Q<VisualElement>(className: "header-panel");
if (selectedItem?.type == LevelHierarchyItem.ItemType.Group)
{
// Show group name in header, hide navigation
window.LevelTitleContainer.style.display = DisplayStyle.None;
window.HeaderLabel.style.display = DisplayStyle.Flex;
window.HeaderLabel.text = selectedItem.groupAsset != null ? selectedItem.groupAsset.groupName : "No Group Name";
// Hide navigation buttons
foreach (var button in levelControlPanel.Children().Where(c => c.ClassListContains("nav-button")))
{
button.style.display = DisplayStyle.None;
}
// Hide OpenGrid and test level buttons
foreach (var button in levelControlPanel.Children().OfType<Button>())
{
if (button.text == "Open Grid" || button.text == "Test level")
{
button.style.display = DisplayStyle.None;
}
}
return;
}
// Show navigation for levels and other types
window.HeaderLabel.style.display = DisplayStyle.None;
foreach (var button in levelControlPanel.Children().Where(c => c.ClassListContains("nav-button")))
{
button.style.display = DisplayStyle.Flex;
}
// Show OpenGrid and test level buttons for levels
foreach (var button in levelControlPanel.Children().OfType<Button>())
{
if (button.text == "Open Grid" || button.text == "Test level")
{
button.style.display = DisplayStyle.Flex;
}
}
if (selectedItem?.type == LevelHierarchyItem.ItemType.Level && selectedItem.levelAsset != null)
{
// Use the level's internal number property
var level = selectedItem.levelAsset;
int levelNumber = level.number;
// Show level title container
window.LevelTitleContainer.style.display = DisplayStyle.Flex;
window.LevelNumberField.value = levelNumber.ToString();
}
else
{
window.LevelTitleContainer.style.display = DisplayStyle.None;
window.HeaderLabel.style.display = DisplayStyle.Flex;
window.HeaderLabel.text = selectedItem?.displayName ?? "No Selection";
}
}
private static LevelGroup FindParentGroup(Level level)
{
if (level == null) return null;
// Search for the level in all groups
string[] groupGuids = AssetDatabase.FindAssets("t:LevelGroup");
foreach (string guid in groupGuids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
LevelGroup group = AssetDatabase.LoadAssetAtPath<LevelGroup>(path);
if (group != null && group.levels != null && group.levels.Contains(level))
{
return group;
}
}
return null;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 32fc77f3d3f54455d95d924b4ea943ca
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,124 @@
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
namespace WordsToolkit.Scripts.Levels.Editor
{
[CustomEditor(typeof(LanguageConfiguration))]
public class LanguageConfigurationEditor : UnityEditor.Editor
{
private SerializedProperty languagesProp;
private SerializedProperty defaultLanguageProp;
public override VisualElement CreateInspectorGUI()
{
var root = new VisualElement();
languagesProp = serializedObject.FindProperty("languages");
defaultLanguageProp = serializedObject.FindProperty("defaultLanguage");
// Create default language dropdown
var defaultLanguageField = new PopupField<string>("Default Language");
UpdateDefaultLanguageChoices(defaultLanguageField);
root.Add(defaultLanguageField);
// Create languages list
var listView = new ListView
{
reorderable = true,
showAddRemoveFooter = true,
showBorder = true,
showFoldoutHeader = true,
headerTitle = "Languages",
fixedItemHeight = 20
};
listView.makeItem = () =>
{
var row = new VisualElement();
row.style.flexDirection = FlexDirection.Row;
var codeField = new TextField { name = "code", style = { width = 60 } };
var nameField = new TextField { name = "displayName", style = { width = 120 } };
var localizedField = new TextField { name = "localizedName", style = { width = 120 } };
var enabledToggle = new Toggle { name = "enabledByDefault", style = { width = 20 } };
var modelField = new ObjectField { name = "languageModel", style = { width = 120 } };
var baseField = new ObjectField { name = "localizationBase", style = { width = 120 } };
row.Add(codeField);
row.Add(nameField);
row.Add(localizedField);
row.Add(enabledToggle);
row.Add(modelField);
row.Add(baseField);
return row;
};
listView.bindItem = (element, index) =>
{
var itemProperty = languagesProp.GetArrayElementAtIndex(index);
var codeField = element.Q<TextField>("code");
var nameField = element.Q<TextField>("displayName");
var localizedField = element.Q<TextField>("localizedName");
var enabledToggle = element.Q<Toggle>("enabledByDefault");
var modelField = element.Q<ObjectField>("languageModel");
var baseField = element.Q<ObjectField>("localizationBase");
codeField.BindProperty(itemProperty.FindPropertyRelative("code"));
nameField.BindProperty(itemProperty.FindPropertyRelative("displayName"));
localizedField.BindProperty(itemProperty.FindPropertyRelative("localizedName"));
enabledToggle.BindProperty(itemProperty.FindPropertyRelative("enabledByDefault"));
modelField.BindProperty(itemProperty.FindPropertyRelative("languageModel"));
baseField.BindProperty(itemProperty.FindPropertyRelative("localizationBase"));
};
listView.itemsAdded += (indexes) =>
{
foreach (int index in indexes)
{
var element = languagesProp.GetArrayElementAtIndex(index);
element.FindPropertyRelative("code").stringValue = "new";
element.FindPropertyRelative("displayName").stringValue = "New Language";
element.FindPropertyRelative("localizedName").stringValue = "New Language";
element.FindPropertyRelative("enabledByDefault").boolValue = true;
serializedObject.ApplyModifiedProperties();
UpdateDefaultLanguageChoices(defaultLanguageField);
}
};
listView.itemsRemoved += (indexes) =>
{
UpdateDefaultLanguageChoices(defaultLanguageField);
};
// Bind the list view to the languages property
listView.bindingPath = languagesProp.propertyPath;
listView.Bind(serializedObject);
root.Add(listView);
return root;
}
private void UpdateDefaultLanguageChoices(PopupField<string> popup)
{
var choices = new List<string>();
for (int i = 0; i < languagesProp.arraySize; i++)
{
var element = languagesProp.GetArrayElementAtIndex(i);
choices.Add(element.FindPropertyRelative("code").stringValue);
}
popup.choices = choices;
popup.value = defaultLanguageProp.stringValue;
popup.RegisterValueChangedCallback(evt =>
{
defaultLanguageProp.stringValue = evt.newValue;
serializedObject.ApplyModifiedProperties();
});
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3b3d634181f2741ffa2abff6af6ceb5e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,168 @@
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
namespace WordsToolkit.Scripts.Levels.Editor
{
// [CustomPropertyDrawer(typeof(LanguageData))]
public class LanguageDataPropertyDrawer : PropertyDrawer
{
// Track expanded state for each property path
private static Dictionary<string, bool> expandedState = new Dictionary<string, bool>();
// Reference to language configuration (cached)
private LanguageConfiguration languageConfig;
// Heights for different elements
private const float HeaderHeight = 22f;
private const float PropertyHeight = 18f;
private const float PropertyMargin = 2f;
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
string propertyPath = property.propertyPath;
bool isExpanded = IsExpanded(propertyPath);
// Base height for the header
float height = HeaderHeight;
// Add height for expanded properties
if (isExpanded)
{
// Basic properties (language, letters, wordsAmount)
height += (PropertyHeight + PropertyMargin) * 3;
// Words array
SerializedProperty wordsProperty = property.FindPropertyRelative("words");
height += EditorGUI.GetPropertyHeight(wordsProperty, true);
height += PropertyMargin * 2;
}
return height;
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
EditorGUI.BeginProperty(position, label, property);
string propertyPath = property.propertyPath;
SerializedProperty languageProp = property.FindPropertyRelative("language");
SerializedProperty lettersProp = property.FindPropertyRelative("letters");
SerializedProperty wordsAmountProp = property.FindPropertyRelative("wordsAmount");
SerializedProperty wordsProp = property.FindPropertyRelative("words");
// Get language display name
string languageCode = languageProp.stringValue;
string displayName = GetLanguageDisplayName(languageCode);
// Draw header with foldout
Rect headerRect = new Rect(position.x, position.y, position.width, HeaderHeight);
// Background for header
Color headerColor = IsExpanded(propertyPath) ? new Color(0.7f, 0.7f, 0.7f, 0.3f) : new Color(0.6f, 0.6f, 0.6f, 0.2f);
EditorGUI.DrawRect(headerRect, headerColor);
// Foldout and header content
Rect foldoutRect = new Rect(headerRect.x + 10, headerRect.y, headerRect.width - 60, headerRect.height);
// Create a proper label with flag icon if available
GUIContent headerContent = new GUIContent(displayName);
bool newExpanded = EditorGUI.Foldout(foldoutRect, IsExpanded(propertyPath), headerContent, true);
if (newExpanded != IsExpanded(propertyPath))
{
SetExpanded(propertyPath, newExpanded);
}
// Delete button
Rect deleteButtonRect = new Rect(headerRect.xMax - 30, headerRect.y + 2, 25, 18);
if (UnityEngine.GUI.Button(deleteButtonRect, "X"))
{
// Schedule deletion to avoid modifying collection during iteration
// Find the parent array
string parentPath = property.propertyPath.Substring(0, property.propertyPath.LastIndexOf('.'));
SerializedProperty parentArray = property.serializedObject.FindProperty(parentPath);
int index = int.Parse(property.propertyPath.Substring(property.propertyPath.LastIndexOf('[') + 1).Replace("]", ""));
if (EditorUtility.DisplayDialog("Remove Language",
$"Are you sure you want to remove {displayName} language?", "Yes", "No"))
{
parentArray.DeleteArrayElementAtIndex(index);
property.serializedObject.ApplyModifiedProperties();
GUIUtility.ExitGUI(); // Exit the GUI to avoid errors
}
}
// Draw properties if expanded
if (IsExpanded(propertyPath))
{
// Calculate positions for properties
float y = position.y + HeaderHeight + PropertyMargin;
float indent = 15f;
Rect propRect = new Rect(position.x + indent, y, position.width - indent, PropertyHeight);
// Draw language code
EditorGUI.BeginDisabledGroup(true); // Make language code read-only
EditorGUI.PropertyField(propRect, languageProp, new GUIContent("Language Code"));
EditorGUI.EndDisabledGroup();
// Draw letters field
y += PropertyHeight + PropertyMargin;
propRect.y = y;
EditorGUI.PropertyField(propRect, lettersProp);
// Draw words amount field
y += PropertyHeight + PropertyMargin;
propRect.y = y;
EditorGUI.PropertyField(propRect, wordsAmountProp);
// Draw words array
y += PropertyHeight + PropertyMargin;
propRect.y = y;
propRect.height = EditorGUI.GetPropertyHeight(wordsProp, true);
EditorGUI.PropertyField(propRect, wordsProp, true);
}
EditorGUI.EndProperty();
}
private string GetLanguageDisplayName(string languageCode)
{
// Try to find and cache language configuration
if (languageConfig == null)
{
string[] guids = AssetDatabase.FindAssets("t:LanguageConfiguration");
if (guids.Length > 0)
{
string path = AssetDatabase.GUIDToAssetPath(guids[0]);
languageConfig = AssetDatabase.LoadAssetAtPath<LanguageConfiguration>(path);
}
}
if (languageConfig != null)
{
var langInfo = languageConfig.GetLanguageInfo(languageCode);
if (langInfo != null && !string.IsNullOrEmpty(langInfo.displayName))
{
return $"{langInfo.displayName} ({languageCode})";
}
}
return $"Language: {languageCode}";
}
private bool IsExpanded(string propertyPath)
{
if (!expandedState.ContainsKey(propertyPath))
{
expandedState[propertyPath] = false; // Default to collapsed
}
return expandedState[propertyPath];
}
private void SetExpanded(string propertyPath, bool expanded)
{
expandedState[propertyPath] = expanded;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8ed5a762c3f7b4e8bb91c7a0d23f7104
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: da38953d3af4d4daa88294115b03bb83
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,507 @@
// // ©2015 - 2025 Candy Smith
// // All rights reserved
// // Redistribution of this software is strictly not allowed.
// // Copy of this software can be obtained from unity asset store only.
// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// // FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
// // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// // THE SOFTWARE.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using WordsToolkit.Scripts.NLP;
using WordsToolkit.Scripts.Utilities;
using WordsToolkit.Scripts.Services.BannedWords;
using WordsToolkit.Scripts.System;
using WordsToolkit.Scripts.Enums;
using WordsToolkit.Scripts.Gameplay.Managers;
namespace WordsToolkit.Scripts.Levels.Editor
{
public static class LevelEditorServices
{
public static void GenerateWordsForLevel(Level level, IModelController model)
{
// Ensure model is properly loaded before proceeding
if (model == null)
{
Debug.LogError("Model controller is null, cannot generate words for level");
return;
}
// Make sure all language models are loaded
model.LoadModels();
// Wait a moment to ensure models are loaded
EditorApplication.delayCall += () => {
// Check if model is loaded properly
bool allLanguagesLoaded = true;
foreach (var langData in level.languages)
{
if (!model.IsModelLoaded(langData.language))
{
allLanguagesLoaded = false;
Debug.LogWarning($"Model for language {langData.language} is not loaded properly");
}
}
if (allLanguagesLoaded)
{
Debug.Log($"Generating words for level {level.number}");
GenerateAllWords(level, model);
}
else
{
// Retry once after a short delay
Debug.Log("Models not fully loaded, retrying word generation shortly...");
EditorApplication.delayCall += () => {
Debug.Log($"Retrying word generation for level {level.number}");
GenerateAllWords(level, model);
};
}
};
}
public static void GenerateAllWords(Level level,IModelController model)
{
if (level == null) return;
model.LoadModels();
if (level.languages == null || level.languages.Count == 0)
{
Debug.LogWarning("The level has no languages defined. Add at least one language first.");
return;
}
// Show progress bar
int totalLanguages = level.languages.Count;
int processedLanguages = 0;
try
{
foreach (var languageData in level.languages)
{
EditorUtility.DisplayProgressBar("Generating Words",
$"Processing language: {languageData.language}",
(float)processedLanguages / totalLanguages);
string letters = GenerateRandomLetters(languageData, languageData.wordsAmount, level.letters);
if (!string.IsNullOrEmpty(letters))
{
languageData.letters = letters;
languageData.wordsAmount = level.words;
GenerateWordsForLanguage(level, languageData, model, false);
}
processedLanguages++;
}
EditorUtility.SetDirty(level);
AssetDatabase.SaveAssets();
}
finally
{
EditorUtility.ClearProgressBar();
}
}
public static void GenerateWordsForLanguage(Level level, LanguageData languageData, IModelController Controller, bool updateWordsAmount = true)
{
if (string.IsNullOrEmpty(languageData.letters)) return;
// Get banned words service to filter out banned words
var bannedWordsService = EditorScope.Resolve<IBannedWordsService>();
// Generate words with length and probability constraints
float probability = level.difficulty / 100f;
var minLettersInWord = level.min;
var maxLettersInWord = level.max;
// Group words by length
var wordsFromSymbols = Controller.GetWordsFromSymbols(languageData.letters, languageData.language);
var wordsByLength = wordsFromSymbols.Where(i=> !IsWordUsed( i, languageData.language, level, out _))
.Where(w => w.Length >= minLettersInWord && w.Length <= maxLettersInWord)
.Where(w => bannedWordsService == null || !bannedWordsService.IsWordBanned(w, languageData.language)) // Filter out banned words
.GroupBy(w => w.Length)
.ToDictionary(g => g.Key, g => g.ToList());
// Use the new GetWordsByFactor function to select words
var selectedWords = GetWordsByFactor(wordsByLength, probability, languageData.wordsAmount);
if(selectedWords.Count < languageData.wordsAmount)
{
// If not enough words were selected, try to fill the gap with random words
var allWords = wordsFromSymbols.Where(w => w.Length >= minLettersInWord && w.Length <= maxLettersInWord)
.Where(w => bannedWordsService == null || !bannedWordsService.IsWordBanned(w, languageData.language)) // Filter out banned words
.ToList();
var additionalWordsNeeded = languageData.wordsAmount - selectedWords.Count;
var remainingWords = new HashSet<string>(allWords);
remainingWords.ExceptWith(selectedWords);
// Break if we don't have enough unique words left
if (remainingWords.Count < additionalWordsNeeded)
{
additionalWordsNeeded = remainingWords.Count;
}
// Add remaining available words
while (selectedWords.Count < languageData.wordsAmount && additionalWordsNeeded > 0 && remainingWords.Count > 0)
{
string randomWord = remainingWords.ElementAt(UnityEngine.Random.Range(0, remainingWords.Count));
selectedWords.Add(randomWord);
remainingWords.Remove(randomWord);
additionalWordsNeeded--;
}
}
if (selectedWords.Count == 0)
{
Debug.LogWarning($"No valid words with {minLettersInWord} to {maxLettersInWord} letters could be generated from '{languageData.letters}' for language {languageData.language}");
return;
}
// Shuffle the final list
var words = selectedWords.OrderBy(x => UnityEngine.Random.value).ToList();
// Update the words array
languageData.words = words.ToArray();
if (updateWordsAmount)
{
languageData.wordsAmount = words.Count;
}
}
public static List<string> GenerateAvailableWords(Level level, IModelController model, LanguageData languageData)
{
if (level == null || model == null || languageData == null)
{
Debug.LogError("Level, model or language data is null, cannot generate available words");
return new List<string>();
}
model.LoadModels();
return model.GetWordsFromSymbols(languageData.letters, languageData.language);
}
public static string GenerateRandomLetters(LanguageData languageData, int count, int lettersAmount)
{
var controller = EditorScope.Resolve<IModelController>();
var bannedWordsService = EditorScope.Resolve<IBannedWordsService>();
// Ensure model is loaded
if (controller == null)
{
Debug.LogError($"Failed to resolve IModelController for generating letters");
return "";
}
var usedWords = LevelEditorServices.GetUsedWords(languageData.language);
// Try getting words with specified length
var words = controller.GetWordsWithLength(lettersAmount, languageData.language).Where(w => !usedWords.Contains(w)).ToList();
// If no words found with the specified length, try with other lengths
if (words.Count == 0)
{
Debug.LogWarning($"No words with length {lettersAmount} found for language {languageData.language}. Trying other lengths...");
// Try with smaller lengths first, then larger
for (int offset = 1; offset <= 3; offset++)
{
// Try smaller length
if (lettersAmount - offset > 2)
{
words = controller.GetWordsWithLength(lettersAmount - offset, languageData.language);
if (words.Count > 0) break;
}
// Try larger length
words = controller.GetWordsWithLength(lettersAmount + offset, languageData.language);
if (words.Count > 0) break;
}
}
// If still no words found, use some default letters
if (words.Count == 0)
{
Debug.LogWarning($"No suitable words found for language {languageData.language}. Using default letters.");
// Default letter sets by language
Dictionary<string, string> defaultLetters = new Dictionary<string, string>
{
{ "en", "eariotnslcudpmhgbfywkvxzjq" },
{ "es", "eaosrnidlctumpbgvyqjhfzñxw" },
{ "ru", "оеаинтсрвлкмдпуяыьгзбчйхжшюцщэфъ" },
};
if (defaultLetters.TryGetValue(languageData.language, out string letters))
{
return letters.Substring(0, Mathf.Min(lettersAmount, letters.Length));
}
else
{
return "".Substring(0, Mathf.Min(lettersAmount, 26));
}
}
// Continue with the normal process if words are found
string bestWord = words[0];
int maxWordsGenerated = 0;
for (int i = 0; i < words.Count; i++)
{
var generatedWords = controller.GetWordsFromSymbols(words[i], languageData.language);
// Filter out banned words when counting generated words
if (bannedWordsService != null)
{
generatedWords = generatedWords.Where(w => !bannedWordsService.IsWordBanned(w, languageData.language)).ToList();
}
int wordsCount = generatedWords?.Count() ?? 0;
if (wordsCount >= count)
{
Debug.Log($"Found optimal word '{words[i]}' that can generate {wordsCount} words (excluding banned words)");
return words[i];
}
if (wordsCount > maxWordsGenerated)
{
maxWordsGenerated = wordsCount;
bestWord = words[i];
}
}
Debug.Log($"Using best word '{bestWord}' which can generate {maxWordsGenerated} words (excluding banned words)");
return bestWord; // Return the word that generated the most derived words
}
private static List<string> GetUsedWords(string languageDataLanguage)
{
// Get all levels in the project
var allLevels = Resources.FindObjectsOfTypeAll<Level>();
var usedWords = new List<string>();
// Iterate through each level and collect words for the specified language
foreach (var level in allLevels)
{
if (level.languages == null) continue;
var langData = level.languages.FirstOrDefault(l => l.language == languageDataLanguage);
if (langData != null && langData.words != null)
{
usedWords.AddRange(langData.words);
}
}
return usedWords.Distinct().ToList(); // Return distinct words to avoid duplicates
}
public static List<string> GetWordsByFactor(Dictionary<int, List<string>> wordsByLength, float factor, int count)
{
// Ensure factor is clamped between 0 and 1
factor = Mathf.Clamp01(factor);
// Create a weighted list of lengths based on the factor
var weightedLengths = wordsByLength.Keys
.OrderBy(length => length) // Sort lengths in ascending order
.Select(length => new
{
Length = length,
Weight = Mathf.Lerp(0f, 1f, factor) * (length - wordsByLength.Keys.Min()) +
Mathf.Lerp(1f, 0f, factor) * (wordsByLength.Keys.Max() - length)
})
.OrderByDescending(x => x.Weight) // Sort by weight descending
.ToList();
// Select words based on weighted lengths
var selectedWords = new List<string>();
foreach (var weightedLength in weightedLengths)
{
if (selectedWords.Count >= count)
break;
if (wordsByLength.TryGetValue(weightedLength.Length, out var words))
{
var availableWords = new List<string>(words);
while (selectedWords.Count < count && availableWords.Count > 0)
{
int index = UnityEngine.Random.Range(0, availableWords.Count);
selectedWords.Add(availableWords[index]);
availableWords.RemoveAt(index);
}
}
}
return selectedWords;
}
// Extract and update words from crossword placement
public static void UpdateWordsFromCrossword(Level level, string langCode, List<WordPlacement> placements)
{
if (level == null || string.IsNullOrEmpty(langCode) || placements == null)
{
Debug.LogWarning("Cannot update words: invalid parameters");
return;
}
var languageData = level.GetLanguageData(langCode);
if (languageData == null)
{
Debug.LogError($"Language {langCode} not found in level {level.name}");
return;
}
// Extract words from placements
var crosswordWords = placements
.Where(p => !string.IsNullOrEmpty(p.word))
.Select(p => p.word.ToLower())
.ToArray();
// Always clear existing words first
if (crosswordWords == null || crosswordWords.Length == 0)
{
// If no words in crossword, set to empty array
languageData.words = Array.Empty<string>();
}
else
{
// Replace with crossword words
languageData.words = crosswordWords;
}
bool wordsChanged = true;
if (wordsChanged)
{
EditorUtility.SetDirty(level);
AssetDatabase.SaveAssets();
}
}
public static Level[] GetUsedInLevels(string elementStringValue, string langCode, Level thisLevel)
{
var usedInLevels = new List<Level>();
foreach (var l in Resources.LoadAll<Level>("Levels"))
{
var languageData = l.GetLanguageData(langCode);
if (l != thisLevel && languageData != null && languageData.words != null)
{
if (languageData.words.Contains(elementStringValue.ToLower()))
{
usedInLevels.Add(l); // Add the level where the word is used
}
}
}
return usedInLevels.ToArray(); // Return all levels where the word is used
}
public static bool IsWordUsed(string elementStringValue, string langCode, Level thisLevel, out Level usedInLevel)
{
usedInLevel = null;
foreach (var l in Resources.LoadAll<Level>("Levels"))
{
var languageData = l.GetLanguageData(langCode);
if (l != thisLevel && languageData != null && languageData.words != null)
{
if (languageData.words.Contains(elementStringValue.ToLower()))
{
usedInLevel = l; // Set the level where the word is used
return true; // Word is used in this level
}
}
}
return false; // Word is not used in any other level
}
public static void TestLevel(Level level, string languageCode)
{
if (level == null)
{
Debug.LogWarning("No level provided to test.");
return;
}
if (string.IsNullOrEmpty(languageCode))
{
Debug.LogWarning("No language code provided for testing.");
return;
}
// First, ensure we load the main scene before testing
string mainScenePath = "Assets/WordConnectGameToolkit/Scenes/main.unity";
if (File.Exists(mainScenePath))
{
// Load the main scene first
EditorSceneManager.OpenScene(mainScenePath);
Debug.Log($"Loaded main scene: {mainScenePath}");
}
else
{
Debug.LogError($"Main scene not found at path: {mainScenePath}");
return;
}
// Set test play mode and level
GameDataManager.isTestPlay = true;
GameDataManager.SetLevel(level);
GameDataManager.SetLevelNum(level.number);
// Set the current language
PlayerPrefs.SetString("SelectedLanguage", languageCode);
// Set state manager to main menu first, then it will transition to game
var stateManager = UnityEngine.Object.FindObjectOfType<StateManager>();
if (stateManager != null)
{
stateManager.CurrentState = EScreenStates.MainMenu;
}
// Enter play mode if not already in it
if (!EditorApplication.isPlaying)
{
EditorApplication.isPlaying = true;
}
// Give Unity a moment to enter play mode before loading the level
EditorApplication.delayCall += () =>
{
if (EditorApplication.isPlaying)
{
// Wait a bit more for the scene to be fully loaded
EditorApplication.delayCall += () =>
{
if (EditorApplication.isPlaying)
{
// Now set the state to game to start the level
var playModeStateManager = UnityEngine.Object.FindObjectOfType<StateManager>();
if (playModeStateManager != null)
{
playModeStateManager.CurrentState = EScreenStates.Game;
}
// Find and initialize the LevelManager
var levelManager = UnityEngine.Object.FindObjectOfType<LevelManager>();
if (levelManager != null)
{
levelManager.Load();
}
else
{
Debug.LogError("LevelManager not found in the scene. Make sure it exists before testing levels.");
}
}
};
}
};
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4ca31ab19e5a47c8a42cd1750d70c2ba
timeCreated: 1746527120

View File

@ -0,0 +1,634 @@
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using UnityEditor.UIElements;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using WordsToolkit.Scripts.Levels.Editor.EditorWindows;
using WordsToolkit.Scripts.NLP;
using WordsToolkit.Scripts.Settings;
namespace WordsToolkit.Scripts.Levels.Editor
{
[CustomEditor(typeof(LevelGroup))]
public class LevelGroupEditor : UnityEditor.Editor
{
private VisualElement rootContainer;
private VisualElement localizedTextsList;
private ScrollView localizedTextsScroll;
private VisualElement backgroundPreview;
private PropertyField groupNameField;
private PropertyField backgroundField;
private PropertyField colorsTileField;
private PropertyField targetExtraWordsField;
private Button addLanguageButton;
private Label noLanguagesMessage;
SerializedProperty groupNameProperty;
SerializedProperty parentGroupProperty;
SerializedProperty levelsProperty;
SerializedProperty backgroundProperty;
SerializedProperty localizedTextsProperty;
SerializedProperty colorsTileProperty;
SerializedProperty targetExtraWordsProperty;
private LanguageConfiguration languageConfig;
private string[] languageCodes;
private string[] languageNames;
private bool colorsTileChanged;
private void OnEnable()
{
groupNameProperty = serializedObject.FindProperty("groupName");
parentGroupProperty = serializedObject.FindProperty("parentGroup");
levelsProperty = serializedObject.FindProperty("levels");
backgroundProperty = serializedObject.FindProperty("background");
localizedTextsProperty = serializedObject.FindProperty("localizedTexts");
colorsTileProperty = serializedObject.FindProperty("colorsTile");
targetExtraWordsProperty = serializedObject.FindProperty("targetExtraWords");
ColorsTileDrawer.OnColorTileSelected += OnColorTileSelected;
// Load language configuration
LoadLanguageConfiguration();
// Register for undo/redo events
// Undo.undoRedoPerformed += OnUndoRedo;
}
private void OnDisable()
{
ColorsTileDrawer.OnColorTileSelected -= OnColorTileSelected;
// Unregister from undo/redo events
// Undo.undoRedoPerformed -= OnUndoRedo;
}
private void OnColorTileSelected(ColorsTile obj)
{
// This is called by the ColorsTileDrawer when a color tile is selected
colorsTileChanged = true;
// Also trigger the change handler directly
HandleColorsTileChange();
}
private void HandleColorsTileChange()
{
colorsTileChanged = false; // Reset the flag after processing
var lg = (LevelGroup)target;
lg.ApplyColorsTileToLevels();
if (lg.levels != null && lg.levels.Count > 0)
{
Undo.RecordObjects(lg.levels.ToArray(), "Update Levels ColorsTile");
foreach (var level in lg.levels)
{
if (level != null)
{
EditorUtility.SetDirty(level);
}
}
AssetDatabase.SaveAssets();
Debug.Log($"Updated colorsTile for {lg.levels.Count} levels in group {lg.groupName}");
}
}
private void OnUndoRedo()
{
// Refresh hierarchy when undo/redo is performed
RefreshLevelManagerWindow();
}
private void RefreshLevelManagerWindow()
{
// Find and refresh the Level Manager Window if it's open
var levelManagerWindow = EditorWindow.GetWindow<LevelManagerWindow>(false);
if (levelManagerWindow != null)
{
levelManagerWindow.Repaint();
var hierarchyTree = levelManagerWindow.GetHierarchyTree();
if (hierarchyTree != null)
{
hierarchyTree.Reload();
}
}
}
private void LoadLanguageConfiguration()
{
// Find the language configuration asset
string[] guids = AssetDatabase.FindAssets("t:LanguageConfiguration");
if (guids.Length > 0)
{
string path = AssetDatabase.GUIDToAssetPath(guids[0]);
languageConfig = AssetDatabase.LoadAssetAtPath<LanguageConfiguration>(path);
if (languageConfig != null && languageConfig.languages != null)
{
// Get enabled languages
var enabledLanguages = languageConfig.GetEnabledLanguages();
// Initialize arrays
languageCodes = enabledLanguages.Select(l => l.code).ToArray();
languageNames = enabledLanguages.Select(l => l.displayName).ToArray();
// If arrays are empty, provide at least English as fallback
if (languageCodes.Length == 0)
{
languageCodes = new[] { "en" };
languageNames = new[] { "English" };
}
}
}
// Fallback if no configuration is found
if (languageCodes == null || languageNames == null)
{
languageCodes = new[] { "en" };
languageNames = new[] { "English" };
}
}
public override VisualElement CreateInspectorGUI()
{
// Load the UXML file
var directory = "Assets/WordConnectGameToolkit/UIBuilder";
var uxmlPath = Path.Combine(directory, "LevelGroupEditor.uxml");
var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(uxmlPath);
if (visualTree == null)
{
Debug.LogError($"Could not load UXML file at path: {uxmlPath}");
return new Label("Failed to load UI");
}
rootContainer = visualTree.CloneTree();
// Find UI elements
localizedTextsList = rootContainer.Q<VisualElement>("localized-texts-list");
localizedTextsScroll = rootContainer.Q<ScrollView>("localized-texts-scroll");
backgroundPreview = rootContainer.Q<VisualElement>("background-preview");
groupNameField = rootContainer.Q<PropertyField>("group-name-field");
backgroundField = rootContainer.Q<PropertyField>("background-field");
colorsTileField = rootContainer.Q<PropertyField>("colors-tile-field");
targetExtraWordsField = rootContainer.Q<PropertyField>("target-extra-words-field");
addLanguageButton = rootContainer.Q<Button>("add-language-button");
noLanguagesMessage = rootContainer.Q<Label>("no-languages-message");
// Bind properties explicitly
rootContainer.Bind(serializedObject);
// Set tooltip for targetExtraWordsField if found
if (targetExtraWordsField != null)
{
targetExtraWordsField.tooltip = "Target number of extra words for levels in this group";
}
// If colorsTileField is null, create it manually as a fallback
if (colorsTileField == null)
{
Debug.Log("Creating colorsTile PropertyField manually");
var fieldsContainer = rootContainer.Q<VisualElement>("fields-container");
if (fieldsContainer != null)
{
colorsTileField = new PropertyField(colorsTileProperty);
colorsTileField.name = "colors-tile-field-manual";
fieldsContainer.Add(colorsTileField);
Debug.Log("Manual colorsTile PropertyField created and added");
}
}
// If targetExtraWordsField is null, create it manually as a fallback
if (targetExtraWordsField == null)
{
var fieldsContainer = rootContainer.Q<VisualElement>("fields-container");
if (fieldsContainer != null)
{
targetExtraWordsField = new PropertyField(targetExtraWordsProperty);
targetExtraWordsField.name = "target-extra-words-field-manual";
targetExtraWordsField.tooltip = "Target number of extra words to be found to get a reward";
fieldsContainer.Add(targetExtraWordsField);
}
}
// Setup callbacks
SetupCallbacks();
// Track only array size changes for localized texts, not content changes
// This prevents recreating text fields when the user is typing
lastLocalizedTextsCount = localizedTextsProperty.arraySize;
// Schedule initial updates for next frame to ensure all properties are ready
rootContainer.schedule.Execute(() =>
{
UpdateLocalizedTextsList();
UpdateBackgroundPreview();
});
return rootContainer;
}
private void SetupCallbacks()
{
// Group name change callback
groupNameField.RegisterValueChangeCallback(evt =>
{
serializedObject.ApplyModifiedProperties();
EditorUtility.SetDirty(target);
RefreshLevelManagerWindow();
});
// Background change callback
backgroundField.RegisterValueChangeCallback(evt =>
{
UpdateBackgroundPreview();
HandleBackgroundChange();
});
// Colors tile change callback
colorsTileField.RegisterValueChangeCallback(evt =>
{
HandleColorsTileChange();
});
// Button callbacks
addLanguageButton.clicked += AddNewLanguage;
}
private void UpdateBackgroundPreview()
{
if (backgroundProperty.objectReferenceValue != null)
{
Sprite sprite = (Sprite)backgroundProperty.objectReferenceValue;
backgroundPreview.style.backgroundImage = new StyleBackground(sprite);
backgroundPreview.style.backgroundColor = StyleKeyword.None;
backgroundPreview.style.display = DisplayStyle.Flex;
}
else
{
backgroundPreview.style.backgroundImage = StyleKeyword.None;
backgroundPreview.style.backgroundColor = new Color(0.2f, 0.2f, 0.2f);
backgroundPreview.style.display = DisplayStyle.Flex;
}
}
private void HandleBackgroundChange()
{
var lg = (LevelGroup)target;
if (lg.levels != null && lg.levels.Count > 0)
{
Sprite newBackground = (Sprite)backgroundProperty.objectReferenceValue;
Undo.RecordObjects(lg.levels.ToArray(), "Update Levels Background");
foreach (var level in lg.levels)
{
if (level != null)
{
level.background = newBackground;
EditorUtility.SetDirty(level);
}
}
AssetDatabase.SaveAssets();
}
}
private void AddNewLanguage()
{
localizedTextsProperty.arraySize++;
SerializedProperty newElement = localizedTextsProperty.GetArrayElementAtIndex(localizedTextsProperty.arraySize - 1);
newElement.FindPropertyRelative("language").stringValue = languageCodes[0]; // Default to first language
newElement.FindPropertyRelative("title").stringValue = "";
newElement.FindPropertyRelative("text").stringValue = "";
serializedObject.ApplyModifiedProperties();
// Update the count and refresh the list
lastLocalizedTextsCount = localizedTextsProperty.arraySize;
UpdateLocalizedTextsList();
}
private void UpdateLocalizedTextsList()
{
// Clear existing items
localizedTextsList.Clear();
if (localizedTextsProperty.arraySize == 0)
{
noLanguagesMessage.style.display = DisplayStyle.Flex;
localizedTextsList.Add(noLanguagesMessage);
return;
}
noLanguagesMessage.style.display = DisplayStyle.None;
// Add language items
bool addedAnyItems = false;
for (int i = 0; i < localizedTextsProperty.arraySize; i++)
{
SerializedProperty element = localizedTextsProperty.GetArrayElementAtIndex(i);
SerializedProperty languageProp = element.FindPropertyRelative("language");
string languageCode = languageProp.stringValue;
// Skip disabled languages
if (!IsLanguageEnabled(languageCode))
continue;
int index = i; // Capture for closure
var localizedTextItem = CreateLocalizedTextItem(index);
// Add separator if we've already added items
if (addedAnyItems)
{
var separator = new VisualElement();
separator.AddToClassList("separator");
localizedTextsList.Add(separator);
}
localizedTextsList.Add(localizedTextItem);
addedAnyItems = true;
}
}
private bool IsLanguageEnabled(string languageCode)
{
if (languageConfig == null || languageConfig.languages == null)
return true; // Default to enabled if no config
var languageInfo = languageConfig.GetLanguageInfo(languageCode);
return languageInfo != null && languageInfo.enabledByDefault;
}
private VisualElement CreateLocalizedTextItem(int index)
{
SerializedProperty element = localizedTextsProperty.GetArrayElementAtIndex(index);
SerializedProperty languageProp = element.FindPropertyRelative("language");
SerializedProperty titleProp = element.FindPropertyRelative("title");
SerializedProperty textProp = element.FindPropertyRelative("text");
var container = new VisualElement();
container.AddToClassList("localized-text-item");
var contentContainer = new VisualElement();
contentContainer.AddToClassList("localized-text-content");
var fieldsContainer = new VisualElement();
fieldsContainer.AddToClassList("localized-text-fields");
// Language dropdown
var languageContainer = new VisualElement();
languageContainer.AddToClassList("language-dropdown");
var languageLabel = new Label("Language");
languageContainer.Add(languageLabel);
var languageDropdown = new DropdownField();
languageDropdown.AddToClassList("language-dropdown-field");
languageDropdown.choices = languageNames.ToList();
string currentLanguage = languageProp.stringValue;
int selectedIndex = 0;
for (int j = 0; j < languageCodes.Length; j++)
{
if (languageCodes[j] == currentLanguage)
{
selectedIndex = j;
break;
}
}
languageDropdown.index = selectedIndex;
languageDropdown.RegisterValueChangedCallback(evt =>
{
int newIndex = languageDropdown.index;
if (newIndex >= 0 && newIndex < languageCodes.Length)
{
languageProp.stringValue = languageCodes[newIndex];
serializedObject.ApplyModifiedProperties();
}
});
languageContainer.Add(languageDropdown);
fieldsContainer.Add(languageContainer);
// Title field
var titleLabel = new Label("Title");
titleLabel.AddToClassList("text-field-label");
fieldsContainer.Add(titleLabel);
var titleField = new TextField();
titleField.AddToClassList("title-field");
titleField.value = titleProp.stringValue;
// Register for input changes (includes paste operations)
titleField.RegisterValueChangedCallback(evt =>
{
titleProp.stringValue = evt.newValue;
serializedObject.ApplyModifiedProperties();
EditorUtility.SetDirty(target);
AssetDatabase.SaveAssets();
});
// Also keep focus out event as backup
titleField.RegisterCallback<FocusOutEvent>(evt =>
{
titleProp.stringValue = titleField.value;
serializedObject.ApplyModifiedProperties();
EditorUtility.SetDirty(target);
AssetDatabase.SaveAssets();
});
fieldsContainer.Add(titleField);
// Text field
var textLabel = new Label("Text");
textLabel.AddToClassList("text-field-label");
fieldsContainer.Add(textLabel);
var textField = new TextField();
textField.AddToClassList("text-area");
textField.multiline = true;
textField.value = textProp.stringValue;
// Register for input changes (includes paste operations)
textField.RegisterValueChangedCallback(evt =>
{
textProp.stringValue = evt.newValue;
serializedObject.ApplyModifiedProperties();
EditorUtility.SetDirty(target);
AssetDatabase.SaveAssets();
});
// Also keep focus out event as backup
textField.RegisterCallback<FocusOutEvent>(evt =>
{
textProp.stringValue = textField.value;
serializedObject.ApplyModifiedProperties();
EditorUtility.SetDirty(target);
AssetDatabase.SaveAssets();
});
fieldsContainer.Add(textField);
contentContainer.Add(fieldsContainer);
// Remove button
var removeButton = new Button(() => RemoveLanguageItem(index));
removeButton.text = "Remove";
removeButton.AddToClassList("remove-button");
contentContainer.Add(removeButton);
container.Add(contentContainer);
return container;
}
private void RemoveLanguageItem(int index)
{
if (index >= 0 && index < localizedTextsProperty.arraySize)
{
localizedTextsProperty.DeleteArrayElementAtIndex(index);
serializedObject.ApplyModifiedProperties();
// Update the count and refresh the list
lastLocalizedTextsCount = localizedTextsProperty.arraySize;
UpdateLocalizedTextsList();
}
}
// Helper method to get default language
private string GetDefaultLanguage()
{
// Try to find language configuration to get default language
string[] guids = AssetDatabase.FindAssets("t:LanguageConfiguration");
if (guids.Length > 0)
{
string path = AssetDatabase.GUIDToAssetPath(guids[0]);
var config = AssetDatabase.LoadAssetAtPath<LanguageConfiguration>(path);
if (config != null && !string.IsNullOrEmpty(config.defaultLanguage))
{
return config.defaultLanguage;
}
}
// Default to English if no configuration found
return "en";
}
private void UpdateLevelsLanguages(LevelGroup group)
{
if (group == null || group.levels == null || group.levels.Count == 0)
{
EditorUtility.DisplayDialog("No Levels", "This group has no levels to update.", "OK");
return;
}
if (group.localizedTexts == null || group.localizedTexts.Count == 0)
{
EditorUtility.DisplayDialog("No Languages",
"This group has no languages defined. Add languages to the group first.", "OK");
return;
}
int updatedLevelCount = 0;
Dictionary<string, int> languageUpdateCounts = new Dictionary<string, int>();
var modelController = EditorScope.Resolve<IModelController>();
EditorUtility.DisplayProgressBar("Updating Languages", "Processing levels...", 0f);
try
{
for (int i = 0; i < group.levels.Count; i++)
{
var level = group.levels[i];
if (level == null) continue;
float progress = (float)i / group.levels.Count;
EditorUtility.DisplayProgressBar("Updating Languages",
$"Processing level {i + 1} of {group.levels.Count}...", progress);
bool levelUpdated = false;
HashSet<string> existingLangs = new HashSet<string>(level.languages.Select(l => l.language));
foreach (var localizedText in group.localizedTexts)
{
if (!existingLangs.Contains(localizedText.language))
{
level.AddLanguage(localizedText.language);
// Generate words for the new language
var languageData = level.GetLanguageData(localizedText.language);
if (languageData != null)
{
// Generate random word for letters
var words = modelController.GetWordsWithLength(level.letters, localizedText.language);
string letters = words.Count > 0 ? words[0] : "";
if (!string.IsNullOrEmpty(letters))
{
languageData.letters = letters;
// Generate words from the letters
var generatedWords = modelController.GetWordsFromSymbols(letters, localizedText.language);
if (generatedWords != null && generatedWords.Count() > 0)
{
languageData.words = generatedWords.ToArray();
}
}
if (!languageUpdateCounts.ContainsKey(localizedText.language))
languageUpdateCounts[localizedText.language] = 0;
languageUpdateCounts[localizedText.language]++;
levelUpdated = true;
}
}
}
if (levelUpdated)
{
EditorUtility.SetDirty(level);
updatedLevelCount++;
}
}
if (updatedLevelCount > 0)
{
AssetDatabase.SaveAssets();
string updateDetails = string.Join("\n",
languageUpdateCounts.Select(kvp => $"- {kvp.Key}: {kvp.Value} levels"));
string message = $"Updated {updatedLevelCount} levels:\n{updateDetails}";
EditorUtility.DisplayDialog("Update Complete", message, "OK");
Debug.Log($"[{group.groupName}] {message}");
}
else
{
EditorUtility.DisplayDialog("No Changes",
"All levels already have all group languages.", "OK");
}
}
finally
{
EditorUtility.ClearProgressBar();
}
}
private int lastLocalizedTextsCount = -1;
public void RefreshUI()
{
if (rootContainer != null)
{
// Schedule updates for next frame to ensure properties are current
rootContainer.schedule.Execute(() =>
{
// Only update the list if the count has changed
if (localizedTextsProperty.arraySize != lastLocalizedTextsCount)
{
UpdateLocalizedTextsList();
lastLocalizedTextsCount = localizedTextsProperty.arraySize;
}
UpdateBackgroundPreview();
});
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7fe4a63d0bf0d4939a5adc6347e61df2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,18 @@
using UnityEngine;
using UnityEditor.IMGUI.Controls;
namespace WordsToolkit.Scripts.Levels.Editor
{
// Custom TreeViewItem for level hierarchy
public class LevelHierarchyItem : TreeViewItem
{
public enum ItemType { Collection, Group, Level }
public ItemType type;
public string folderPath; // For collections
public LevelGroup groupAsset; // For groups
public Level levelAsset; // For levels
public string assetPath;
public new Texture2D icon;
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ccb44bddb137f4464b9f46cddcd56920
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ab257a82c035e4899ae9610e9e216a6f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,104 @@
using System.IO;
using UnityEditor;
using UnityEngine;
using WordsToolkit.Scripts.Levels.Editor.EditorWindows;
namespace WordsToolkit.Scripts.Levels.Editor
{
public static class LevelHierarchyUtility
{
[MenuItem("Assets/Create/Game/Level Collection", false, 81)]
public static void CreateLevelCollection()
{
// Find the currently selected folder in the Project window
string selectedPath = AssetDatabase.GetAssetPath(Selection.activeObject);
if (string.IsNullOrEmpty(selectedPath))
{
// Create in a Levels folder if no selection
string levelsFolder = "Assets/WordConnectGameToolkit/Levels";
if (!Directory.Exists(levelsFolder))
{
string parentFolder = "Assets";
string folderName = "Levels";
AssetDatabase.CreateFolder(parentFolder, folderName);
AssetDatabase.Refresh();
}
selectedPath = levelsFolder;
}
else if (!Directory.Exists(selectedPath))
{
selectedPath = Path.GetDirectoryName(selectedPath);
}
// Create a new folder for the level collection
string collectionFolderPath = AssetDatabase.GenerateUniqueAssetPath($"{selectedPath}/LevelCollection");
AssetDatabase.CreateFolder(Path.GetDirectoryName(collectionFolderPath), Path.GetFileName(collectionFolderPath));
// Create a main level group asset
LevelGroup mainGroup = ScriptableObject.CreateInstance<LevelGroup>();
mainGroup.groupName = "Main Levels";
string mainGroupPath = $"{collectionFolderPath}/MainLevels.asset";
AssetDatabase.CreateAsset(mainGroup, mainGroupPath);
// Create a bonus level group asset
LevelGroup bonusGroup = ScriptableObject.CreateInstance<LevelGroup>();
bonusGroup.groupName = "Bonus Levels";
string bonusGroupPath = $"{collectionFolderPath}/BonusLevels.asset";
AssetDatabase.CreateAsset(bonusGroup, bonusGroupPath);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
// Select the new folder in the Project window
var folderObject = AssetDatabase.LoadAssetAtPath<Object>(collectionFolderPath);
Selection.activeObject = folderObject;
// Log success message
Debug.Log($"Created level collection at {collectionFolderPath}");
}
[MenuItem("Assets/Open in Level Manager", true)]
private static bool ValidateOpenInLevelManager()
{
var selection = Selection.activeObject;
return selection is LevelGroup || selection is Level;
}
[MenuItem("Assets/Open in Level Manager", false, 30)]
public static void OpenInLevelManager()
{
var window = EditorWindow.GetWindow<LevelManagerWindow>();
window.Show();
// Focus on the selected asset
EditorApplication.delayCall += () => {
var selection = Selection.activeObject;
var treeView = window.GetHierarchyTree();
if (treeView != null)
{
treeView.SelectAsset(selection);
}
};
}
// Get the highest level number across all levels in the Resources/Levels folder
public static int GetHighestLevelNumber()
{
int highestNumber = 0;
Level[] allLevels = Resources.LoadAll<Level>("Levels");
foreach (Level level in allLevels)
{
if (level != null && level.number > highestNumber)
{
highestNumber = level.number;
}
}
return highestNumber;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 946fe42597ba04a3589ae5a1a171c2e5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,524 @@
using UnityEngine;
using UnityEngine.UIElements;
using UnityEditor;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using WordsToolkit.Scripts.NLP;
namespace WordsToolkit.Scripts.Levels.Editor
{
public class LevelHierarchyVisualTree : VisualElement
{
private IModelController ModelController => EditorScope.Resolve<IModelController>();
private Dictionary<VisualElement, EventCallback<ClickEvent>> addButtonCallbacks = new Dictionary<VisualElement, EventCallback<ClickEvent>>();
private Dictionary<VisualElement, EventCallback<ClickEvent>> deleteButtonCallbacks = new Dictionary<VisualElement, EventCallback<ClickEvent>>();
// Events
public event Action<LevelHierarchyItem> OnSelectionChanged;
public event Action<LevelHierarchyItem> OnDeleteItem;
public event Action<LevelHierarchyItem> OnCreateSubgroup;
public event Action<LevelHierarchyItem> OnCreateLevel;
public event Action OnHierarchyChanged;
// UI Elements
private TreeView treeView;
private Dictionary<int, LevelHierarchyItem> idToItem = new Dictionary<int, LevelHierarchyItem>();
private List<TreeViewItemData<LevelHierarchyItem>> rootItems = new List<TreeViewItemData<LevelHierarchyItem>>();
private LevelHierarchyItem selectedItem;
public LevelHierarchyVisualTree()
{
Init();
}
private void Init()
{
style.flexGrow = 1;
// Create TreeView
treeView = new TreeView();
treeView.style.flexGrow = 1;
treeView.selectionType = SelectionType.Single;
// Configure data callbacks
treeView.makeItem = MakeTreeItem;
treeView.bindItem = (element, index) =>
{
var itemData = treeView.GetItemDataForIndex<TreeViewItemData<LevelHierarchyItem>>(index);
BindTreeItem(element, itemData);
};
treeView.unbindItem = UnbindTreeItem;
// Set up TreeView events
treeView.selectionChanged += OnTreeSelectionChanged;
treeView.RegisterCallback<KeyDownEvent>(OnKeyDown);
treeView.RegisterCallback<ContextClickEvent>(OnContextClick);
// Enable drag and drop
treeView.reorderable = true;
treeView.RegisterCallback<DragUpdatedEvent>(OnDragUpdated);
treeView.RegisterCallback<DragPerformEvent>(OnDragPerform);
Add(treeView);
Reload();
}
private VisualElement MakeTreeItem()
{
var itemContainer = new VisualElement();
itemContainer.style.flexDirection = FlexDirection.Row;
itemContainer.style.alignItems = Align.Center;
var label = new Label();
label.style.flexGrow = 1;
itemContainer.Add(label);
// Buttons container for group items
var buttonsContainer = new VisualElement();
buttonsContainer.style.flexDirection = FlexDirection.Row;
buttonsContainer.style.display = DisplayStyle.None;
var addButton = new Button(() => { }) { text = "+" };
addButton.AddToClassList("unity-button");
addButton.style.width = 20;
addButton.style.marginRight = 2;
var deleteButton = new Button(() => { }) { text = "" };
deleteButton.AddToClassList("unity-button");
deleteButton.style.width = 20;
buttonsContainer.Add(addButton);
buttonsContainer.Add(deleteButton);
itemContainer.Add(buttonsContainer);
return itemContainer;
}
private void BindTreeItem(VisualElement element, TreeViewItemData<LevelHierarchyItem> itemData)
{
var item = itemData.data;
var label = element.Q<Label>();
var buttonsContainer = element.Children().Last();
var addButton = buttonsContainer.Q<Button>(null, "unity-button");
var deleteButton = buttonsContainer.Children().Last() as Button;
label.text = GetItemDisplayName(item);
label.style.unityFontStyleAndWeight = item.type == LevelHierarchyItem.ItemType.Group ?
FontStyle.Bold : FontStyle.Normal;
// Show/hide buttons based on item type
buttonsContainer.style.display = item.type == LevelHierarchyItem.ItemType.Group ?
DisplayStyle.Flex : DisplayStyle.None;
if (item.type == LevelHierarchyItem.ItemType.Group)
{
// Create callbacks and store them for cleanup
var addCallback = new EventCallback<ClickEvent>((evt) => OnCreateSubgroup?.Invoke(item));
var deleteCallback = new EventCallback<ClickEvent>((evt) =>
{
if (EditorUtility.DisplayDialog(
"Delete Group",
"Are you sure you want to delete this group?",
"Delete",
"Cancel"))
{
OnDeleteItem?.Invoke(item);
}
});
addButton.RegisterCallback(addCallback);
deleteButton.RegisterCallback(deleteCallback);
addButtonCallbacks[element] = addCallback;
deleteButtonCallbacks[element] = deleteCallback;
}
// Set icon
if (item.icon != null)
{
var iconElement = element.Q<Image>("icon");
if (iconElement == null)
{
iconElement = new Image { name = "icon" };
element.Insert(0, iconElement);
}
iconElement.image = item.icon;
}
}
private void UnbindTreeItem(VisualElement element, int index)
{
var buttonsContainer = element.Children().LastOrDefault();
if (buttonsContainer == null) return;
var addButton = buttonsContainer.Q<Button>(null, "unity-button");
var deleteButton = buttonsContainer.Children().LastOrDefault() as Button;
// Remove click event handlers
if (addButtonCallbacks.TryGetValue(element, out var addCallback))
{
if (addButton != null)
{
addButton.UnregisterCallback(addCallback);
}
addButtonCallbacks.Remove(element);
}
if (deleteButtonCallbacks.TryGetValue(element, out var deleteCallback))
{
if (deleteButton != null)
{
deleteButton.UnregisterCallback(deleteCallback);
}
deleteButtonCallbacks.Remove(element);
}
// Clear references
if (element.Q<Image>("icon") is Image iconElement)
{
iconElement.image = null;
element.Remove(iconElement);
}
// Clear any text content
if (element.Q<Label>() is Label label)
{
label.text = string.Empty;
}
// Reset button container visibility
buttonsContainer.style.display = DisplayStyle.None;
}
private string GetItemDisplayName(LevelHierarchyItem item)
{
switch (item.type)
{
case LevelHierarchyItem.ItemType.Collection:
return Path.GetFileName(item.folderPath);
case LevelHierarchyItem.ItemType.Group:
return item.groupAsset?.name ?? "Missing Group";
case LevelHierarchyItem.ItemType.Level:
return $"Level {item.levelAsset?.number ?? 0}";
default:
return "Unknown Item";
}
}
private void OnTreeSelectionChanged(IEnumerable<object> items)
{
selectedItem = items.FirstOrDefault() as LevelHierarchyItem;
OnSelectionChanged?.Invoke(selectedItem);
}
private void OnKeyDown(KeyDownEvent evt)
{
if (evt.keyCode == KeyCode.Delete || evt.keyCode == KeyCode.Backspace)
{
if (selectedItem != null)
{
if (EditorUtility.DisplayDialog(
"Delete Item",
$"Are you sure you want to delete {GetItemDisplayName(selectedItem)}?",
"Delete",
"Cancel"))
{
OnDeleteItem?.Invoke(selectedItem);
}
}
evt.StopPropagation();
}
}
private void OnContextClick(ContextClickEvent evt)
{
var menu = new GenericMenu();
if (selectedItem != null)
{
switch (selectedItem.type)
{
case LevelHierarchyItem.ItemType.Group:
menu.AddItem(new GUIContent("Create Subgroup"), false, () => OnCreateSubgroup?.Invoke(selectedItem));
menu.AddItem(new GUIContent("Create Level"), false, () => OnCreateLevel?.Invoke(selectedItem));
menu.AddSeparator("");
menu.AddItem(new GUIContent("Delete Group"), false, () => OnDeleteItem?.Invoke(selectedItem));
break;
case LevelHierarchyItem.ItemType.Level:
menu.AddItem(new GUIContent("Delete Level"), false, () => OnDeleteItem?.Invoke(selectedItem));
break;
}
}
else
{
menu.AddItem(new GUIContent("Create Root Group"), false, () => CreateRootGroup());
}
menu.ShowAsContext();
evt.StopPropagation();
}
private void CreateRootGroup()
{
var newGroup = ScriptableObject.CreateInstance<LevelGroup>();
var path = EditorUtility.SaveFilePanelInProject(
"Create New Group",
"NewGroup",
"asset",
"Choose location for the new group"
);
if (!string.IsNullOrEmpty(path))
{
AssetDatabase.CreateAsset(newGroup, path);
AssetDatabase.SaveAssets();
Reload();
}
}
private void OnDragUpdated(DragUpdatedEvent evt)
{
var draggedItem = DragAndDrop.GetGenericData("DraggedItem") as LevelHierarchyItem;
if (draggedItem != null)
{
DragAndDrop.visualMode = DragAndDropVisualMode.Move;
evt.StopPropagation();
}
}
private void OnDragPerform(DragPerformEvent evt)
{
var draggedItem = DragAndDrop.GetGenericData("DraggedItem") as LevelHierarchyItem;
var dropTarget = selectedItem;
if (draggedItem != null && dropTarget != null && dropTarget.type == LevelHierarchyItem.ItemType.Group)
{
if (!WouldCreateCircularReference(draggedItem, dropTarget))
{
UpdateItemParent(draggedItem, dropTarget);
OnHierarchyChanged?.Invoke();
Reload();
}
}
evt.StopPropagation();
}
private bool WouldCreateCircularReference(LevelHierarchyItem draggedItem, LevelHierarchyItem newParent)
{
if (draggedItem.type != LevelHierarchyItem.ItemType.Group || newParent == null)
return false;
var current = newParent;
while (current != null)
{
if (current == draggedItem)
return true;
current = GetParentItem(current);
}
return false;
}
private LevelHierarchyItem GetParentItem(LevelHierarchyItem item)
{
if (item?.type != LevelHierarchyItem.ItemType.Group || item.groupAsset?.parentGroup == null)
return null;
return idToItem.Values.FirstOrDefault(i =>
i.type == LevelHierarchyItem.ItemType.Group &&
i.groupAsset == item.groupAsset.parentGroup);
}
private void UpdateItemParent(LevelHierarchyItem item, LevelHierarchyItem newParent)
{
switch (item.type)
{
case LevelHierarchyItem.ItemType.Group:
UpdateGroupParent(item, newParent);
break;
case LevelHierarchyItem.ItemType.Level:
UpdateLevelParent(item, newParent);
break;
}
}
private void UpdateGroupParent(LevelHierarchyItem groupItem, LevelHierarchyItem newParent)
{
if (groupItem.groupAsset == null) return;
var oldParent = groupItem.groupAsset.parentGroup;
groupItem.groupAsset.parentGroup = newParent?.groupAsset;
EditorUtility.SetDirty(groupItem.groupAsset);
if (oldParent != null)
EditorUtility.SetDirty(oldParent);
if (newParent?.groupAsset != null)
EditorUtility.SetDirty(newParent.groupAsset);
}
private void UpdateLevelParent(LevelHierarchyItem levelItem, LevelHierarchyItem newParent)
{
if (levelItem.levelAsset == null || newParent?.groupAsset == null) return;
// Find current parent
var oldParent = idToItem.Values
.FirstOrDefault(i => i.type == LevelHierarchyItem.ItemType.Group &&
i.groupAsset?.levels?.Contains(levelItem.levelAsset) == true)?.groupAsset;
// Remove from old parent
if (oldParent != null)
{
oldParent.levels.Remove(levelItem.levelAsset);
EditorUtility.SetDirty(oldParent);
}
// Add to new parent
if (newParent.groupAsset.levels == null)
newParent.groupAsset.levels = new List<Level>();
newParent.groupAsset.levels.Add(levelItem.levelAsset);
EditorUtility.SetDirty(newParent.groupAsset);
}
public void Reload()
{
idToItem.Clear();
rootItems.Clear();
// Discover all groups and levels
DiscoverLevelGroups();
// Update tree view
var items = new List<TreeViewItemData<LevelHierarchyItem>>(rootItems);
treeView.Clear();
treeView.SetRootItems(items);
treeView.Rebuild();
}
private void DiscoverLevelGroups()
{
var processedLevelIds = new HashSet<int>();
int itemId = 1;
// Find all LevelGroup assets
string[] groupGuids = AssetDatabase.FindAssets("t:LevelGroup");
var groupToItem = new Dictionary<LevelGroup, TreeViewItemData<LevelHierarchyItem>>();
foreach (string groupGuid in groupGuids)
{
string assetPath = AssetDatabase.GUIDToAssetPath(groupGuid);
var group = AssetDatabase.LoadAssetAtPath<LevelGroup>(assetPath);
if (group != null)
{
var item = new LevelHierarchyItem
{
type = LevelHierarchyItem.ItemType.Group,
groupAsset = group,
assetPath = assetPath
};
var itemData = new TreeViewItemData<LevelHierarchyItem>(itemId++, item);
groupToItem[group] = itemData;
idToItem[itemData.id] = item;
}
}
// Build hierarchy
foreach (var kvp in groupToItem)
{
var group = kvp.Key;
var itemData = kvp.Value;
if (group.parentGroup != null && groupToItem.TryGetValue(group.parentGroup, out var parentItemData))
{
// Add as child to parent
var parentChildren = parentItemData.children?.ToList() ?? new List<TreeViewItemData<LevelHierarchyItem>>();
parentChildren.Add(itemData);
parentItemData = parentItemData.UpdateChildren(parentChildren);
groupToItem[group.parentGroup] = parentItemData;
}
else
{
// Add to root items
rootItems.Add(itemData);
}
}
// Add levels to their groups
string[] levelGuids = AssetDatabase.FindAssets("t:Level");
foreach (string levelGuid in levelGuids)
{
string assetPath = AssetDatabase.GUIDToAssetPath(levelGuid);
var level = AssetDatabase.LoadAssetAtPath<Level>(assetPath);
if (level != null)
{
var parent = groupToItem.FirstOrDefault(kvp =>
kvp.Key.levels != null && kvp.Key.levels.Contains(level));
var item = new LevelHierarchyItem
{
type = LevelHierarchyItem.ItemType.Level,
levelAsset = level,
assetPath = assetPath
};
var itemData = new TreeViewItemData<LevelHierarchyItem>(itemId++, item);
idToItem[itemData.id] = item;
if (parent.Key != null)
{
var parentData = parent.Value;
var children = parentData.children?.ToList() ?? new List<TreeViewItemData<LevelHierarchyItem>>();
children.Add(itemData);
parentData = parentData.UpdateChildren(children);
groupToItem[parent.Key] = parentData;
}
else
{
rootItems.Add(itemData);
}
}
}
// Sort items
SortItems(rootItems);
}
private void SortItems(List<TreeViewItemData<LevelHierarchyItem>> items)
{
items.Sort((a, b) =>
{
var itemA = a.data;
var itemB = b.data;
// Groups come before levels
if (itemA.type != itemB.type)
return itemA.type == LevelHierarchyItem.ItemType.Group ? -1 : 1;
// Sort levels by number
if (itemA.type == LevelHierarchyItem.ItemType.Level)
return (itemA.levelAsset?.number ?? 0).CompareTo(itemB.levelAsset?.number ?? 0);
// Sort groups by name
return string.Compare(itemA.groupAsset?.name, itemB.groupAsset?.name);
});
// Sort children recursively
foreach (var item in items)
{
if (item.hasChildren)
{
var children = item.children.ToList();
SortItems(children);
// Replace children with sorted list
items[items.IndexOf(item)] = item.UpdateChildren(children);
}
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b4fffc430190042368d3a78a856aa8dc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,14 @@
using System.Collections.Generic;
using System.Linq;
using UnityEngine.UIElements;
namespace WordsToolkit.Scripts.Levels.Editor
{
public static class TreeViewItemDataExtensions
{
public static TreeViewItemData<T> UpdateChildren<T>(this TreeViewItemData<T> item, List<TreeViewItemData<T>> newChildren)
{
return new TreeViewItemData<T>(item.id, item.data, newChildren);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7c7acf3899c974adb83bcfbb46dd05f9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,83 @@
using UnityEditor;
using UnityEngine;
namespace WordsToolkit.Scripts.Levels.Editor
{
[CustomEditor(typeof(WordEmbeddingModel))]
public class WordEmbeddingModelEditor : UnityEditor.Editor
{
private bool showTestSection = false;
private string testLetters = "";
private string testLanguage = "en";
private int testWordCount = 10;
private string[] testResults = new string[0];
public override void OnInspectorGUI()
{
WordEmbeddingModel model = (WordEmbeddingModel)target;
// Draw default inspector
DrawDefaultInspector();
EditorGUILayout.Space();
// Button to load all dictionaries
if (GUILayout.Button("Reload All Dictionaries"))
{
// Force reload by setting isLoaded to false for all
foreach (var dict in model.dictionaries)
{
dict.isLoaded = false;
}
// Call Awake to reload
model.enabled = false;
model.enabled = true;
}
EditorGUILayout.Space();
// Test section
showTestSection = EditorGUILayout.Foldout(showTestSection, "Test Word Generation", true);
if (showTestSection)
{
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
testLanguage = EditorGUILayout.TextField("Language Code", testLanguage);
testLetters = EditorGUILayout.TextField("Letters", testLetters);
testWordCount = EditorGUILayout.IntSlider("Word Count", testWordCount, 1, 20);
if (GUILayout.Button("Test Find Words"))
{
if (string.IsNullOrEmpty(testLetters))
{
EditorUtility.DisplayDialog("Invalid Input", "Please enter some letters to test with.", "OK");
}
else
{
testResults = model.FindWordsFromSymbols(testLetters, testWordCount, testLanguage).ToArray();
}
}
if (testResults.Length > 0)
{
EditorGUILayout.Space();
EditorGUILayout.LabelField($"Found {testResults.Length} Words:", EditorStyles.boldLabel);
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
foreach (string word in testResults)
{
EditorGUILayout.LabelField(word);
}
EditorGUILayout.EndVertical();
}
else if (testResults != null)
{
EditorGUILayout.HelpBox("No valid words found with these letters.", MessageType.Info);
}
EditorGUILayout.EndVertical();
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8c02956a92daa46a7afff65adeff9c9f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,20 @@
{
"name": "WordsToolkit.LevelEditor",
"rootNamespace": "",
"references": [
"GUID:343deaaf83e0cee4ca978e7df0b80d21",
"GUID:d3bf71b33c0c04eb9bc1a8d6513d76bb",
"GUID:b0214a6008ed146ff8f122a6a9c2f6cc"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 6950726ed8cda408f8f60d7901d00cd7
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: