update assets to 1.03

This commit is contained in:
2025-08-04 16:04:54 +08:00
parent f14db75802
commit 0ac6d9882e
23 changed files with 499 additions and 1034 deletions

View File

@ -0,0 +1,154 @@
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;
namespace WordsToolkit.Scripts.GUI
{
[AddComponentMenu("Layout/Flexible Grid Layout")]
public class FlexibleGridLayout : LayoutGroup
{
[Header("Grid Settings")]
[SerializeField] private bool fitX = true;
[SerializeField] private bool fitY = true;
[SerializeField] private int rows = 1;
[SerializeField] private int columns = 1;
[Header("Cell Settings")]
[SerializeField] private Vector2 cellSize = new Vector2(100, 100);
[SerializeField] private Vector2 spacing = Vector2.zero;
[SerializeField] private bool overrideHeight = false;
[SerializeField] private float fixedCellHeight = 100f;
[Header("Flexible Width Options")]
[SerializeField] private bool enableFlexibleWidth = true;
[SerializeField] private bool enableFlexibleHeight = false;
[SerializeField] private float minCellWidth = 50f;
[SerializeField] private float maxCellWidth = 200f;
[SerializeField] private bool keepAspectRatio = false;
[SerializeField] private bool stretchLastRow = true;
public override void CalculateLayoutInputHorizontal()
{
base.CalculateLayoutInputHorizontal();
if (fitX || fitY)
{
float sqrRt = Mathf.Sqrt(transform.childCount);
rows = Mathf.CeilToInt(sqrRt);
columns = Mathf.CeilToInt(sqrRt);
}
if (fitX)
{
rows = Mathf.CeilToInt(transform.childCount / (float)columns);
}
if (fitY)
{
columns = Mathf.CeilToInt(transform.childCount / (float)rows);
}
CalculateAndApplyLayout();
}
private void CalculateAndApplyLayout()
{
float parentWidth = rectTransform.rect.width;
float parentHeight = rectTransform.rect.height;
float availableWidth = parentWidth - padding.left - padding.right;
float availableHeight = parentHeight - padding.top - padding.bottom;
float totalSpacingWidth = spacing.x * (columns - 1);
float totalSpacingHeight = spacing.y * (rows - 1);
float cellWidth = (availableWidth - totalSpacingWidth) / columns;
float cellHeight = (availableHeight - totalSpacingHeight) / rows;
if (enableFlexibleWidth)
{
cellWidth = Mathf.Clamp(cellWidth, minCellWidth, maxCellWidth);
if (keepAspectRatio)
{
cellHeight = cellWidth * (cellSize.y / cellSize.x);
}
}
if (enableFlexibleHeight && !keepAspectRatio)
{
cellHeight = enableFlexibleHeight ? cellHeight : cellSize.y;
}
cellSize.x = enableFlexibleWidth ? cellWidth : cellSize.x;
cellSize.y = enableFlexibleHeight || keepAspectRatio ? cellHeight : cellSize.y;
// Override height if specified
if (overrideHeight)
{
cellSize.y = fixedCellHeight;
}
SetChildrenPositions();
}
private void SetChildrenPositions()
{
int totalChildren = rectChildren.Count;
int lastRowStart = ((totalChildren - 1) / columns) * columns;
int elementsInLastRow = totalChildren - lastRowStart;
bool isLastRowIncomplete = elementsInLastRow < columns && elementsInLastRow > 0;
for (int i = 0; i < rectChildren.Count; i++)
{
int rowIndex = i / columns;
int columnIndex = i % columns;
bool isInLastRow = i >= lastRowStart;
var item = rectChildren[i];
float cellWidth = cellSize.x;
float xPos;
if (stretchLastRow && isLastRowIncomplete && isInLastRow)
{
// Calculate stretched width for last row elements
float availableWidth = rectTransform.rect.width - padding.left - padding.right;
float totalSpacing = spacing.x * (elementsInLastRow - 1);
float stretchedCellWidth = (availableWidth - totalSpacing) / elementsInLastRow;
if (enableFlexibleWidth)
{
stretchedCellWidth = Mathf.Clamp(stretchedCellWidth, minCellWidth, maxCellWidth);
}
cellWidth = stretchedCellWidth;
// Calculate position for stretched elements
int positionInLastRow = i - lastRowStart;
xPos = padding.left + (cellWidth * positionInLastRow) + (spacing.x * positionInLastRow);
}
else
{
// Normal positioning
xPos = padding.left + (cellSize.x * columnIndex) + (spacing.x * columnIndex);
}
float yPos = padding.top + (cellSize.y * rowIndex) + (spacing.y * rowIndex);
SetChildAlongAxis(item, 0, xPos, cellWidth);
SetChildAlongAxis(item, 1, yPos, cellSize.y);
}
}
public override void CalculateLayoutInputVertical()
{
}
public override void SetLayoutHorizontal()
{
}
public override void SetLayoutVertical()
{
}
}
}

View File

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

View File

@ -10,13 +10,17 @@ namespace WordsToolkit.Scripts.Levels.Editor
public static class EditorScope
{
private static IObjectResolver editorContainer;
private static bool isDisposed = false;
public static IObjectResolver Container
{
get
{
if (editorContainer == null)
if (isDisposed || editorContainer == null)
{
if (isDisposed)
return null;
var builder = new ContainerBuilder();
Configure(builder);
editorContainer = builder.Build();
@ -48,7 +52,31 @@ namespace WordsToolkit.Scripts.Levels.Editor
public static T Resolve<T>() where T : class
{
return Container.Resolve<T>();
var container = Container;
if (container == null || isDisposed)
return null;
return container.Resolve<T>();
}
public static void Dispose()
{
isDisposed = true;
if (editorContainer != null)
{
if (editorContainer is global::System.IDisposable disposableContainer)
{
disposableContainer.Dispose();
}
editorContainer = null;
}
}
// Called by Unity when domain is reloading
[UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.SubsystemRegistration)]
private static void Reset()
{
isDisposed = false;
editorContainer = null;
}
}
}

View File

@ -15,6 +15,9 @@ namespace WordsToolkit.Scripts.Levels.Editor.EditorWindows
// Static field to track all windows and handle script recompilation
private static List<LevelManagerWindow> activeWindows = new List<LevelManagerWindow>();
// Static flag to track if Unity is quitting
private static bool isQuitting = false;
// Static event for when hierarchy selection changes - other windows can subscribe to this
public static event Action<LevelHierarchyItem> OnHierarchySelectionChanged;
@ -64,6 +67,9 @@ namespace WordsToolkit.Scripts.Levels.Editor.EditorWindows
activeWindows.Add(this);
}
// Subscribe to Unity quitting event
EditorApplication.quitting += OnUnityQuitting;
// Initialize UI
LevelManagerWindowUI.InitializeUI(this);
@ -82,10 +88,23 @@ namespace WordsToolkit.Scripts.Levels.Editor.EditorWindows
if (!string.IsNullOrEmpty(m_SelectedLanguage))
EditorPrefs.SetString(SELECTED_LANGUAGE_KEY, m_SelectedLanguage);
// Unsubscribe from Unity quitting event
EditorApplication.quitting -= OnUnityQuitting;
// Unregister this window instance
activeWindows.Remove(this);
}
private static void OnUnityQuitting()
{
isQuitting = true;
// Dispose the editor scope container to prevent crashes
EditorScope.Dispose();
}
// Public static property to check if Unity is quitting
public static bool IsQuitting => isQuitting;
private void InitializeTreeView()
{
// Create tree view state if needed

View File

@ -845,20 +845,20 @@ namespace WordsToolkit.Scripts.Levels.Editor
var wordsListContainer = new VisualElement();
wordsListContainer.style.width = new StyleLength(new Length(100, LengthUnit.Percent));
wordsListContainer.style.flexGrow = 1;
// Store reference to the words list container for this language
wordsListElements[langCode] = wordsListContainer;
// Fill words list from the SerializedProperty using ListView
FillWordsList(wordsListContainer, wordsProp, langCode);
wordsContent.Add(wordsListContainer);
// Add Clear Words button
var clearWordsButton = new Button(() =>
{
if (EditorUtility.DisplayDialog("Clear Words",
"Are you sure you want to clear all words and crossword? This action cannot be undone.",
if (EditorUtility.DisplayDialog("Clear Words",
"Are you sure you want to clear all words and crossword? This action cannot be undone.",
"Clear", "Cancel"))
{
serializedObject.Update();
@ -871,7 +871,7 @@ namespace WordsToolkit.Scripts.Levels.Editor
{
previewDataDict[langCode] = null;
}
// Clear the saved crossword data in the level
var languageData = level.GetLanguageData(langCode);
if (languageData != null)
@ -886,10 +886,10 @@ namespace WordsToolkit.Scripts.Levels.Editor
EditorUtility.SetDirty(serializedObject.targetObject);
AssetDatabase.SaveAssets();
// Notify that the level needs update to refresh the crossword
NotifyLevelNeedsUpdate(level);
// Refresh the UI using LevelManagerWindow
EditorWindows.LevelManagerWindow.RefreshInspectorForLevel(level);
}
@ -912,13 +912,13 @@ namespace WordsToolkit.Scripts.Levels.Editor
{
// Clear existing content first
wordsListContainer.Clear();
// Create ListView for words
var wordsListView = new ListView();
wordsListView.name = $"words-listview-{langCode}";
wordsListView.style.flexGrow = 1;
wordsListView.style.height = StyleKeyword.Auto; // Auto height based on content
// Schedule setting the title after the ListView is fully constructed
wordsListView.schedule.Execute(() =>
{
@ -928,7 +928,7 @@ namespace WordsToolkit.Scripts.Levels.Editor
{
var label = sizeField.Q<Label>();
label.text = "Words";
// Set the text input value directly on the TextField
sizeField.value = wordsProp.arraySize.ToString();
}
@ -949,10 +949,10 @@ namespace WordsToolkit.Scripts.Levels.Editor
}
}
}).ExecuteLater(1); // Execute after UI is built
// Bind the ListView to the words property
wordsListView.BindProperty(wordsProp);
// Setup ListView properties
wordsListView.showBorder = true;
wordsListView.showAlternatingRowBackgrounds = AlternatingRowBackground.None;
@ -961,16 +961,16 @@ namespace WordsToolkit.Scripts.Levels.Editor
wordsListView.reorderMode = ListViewReorderMode.Animated;
wordsListView.virtualizationMethod = CollectionVirtualizationMethod.FixedHeight;
wordsListView.fixedItemHeight = 25; // Set a fixed height per item to help with layout
// Set up the make item callback
wordsListView.makeItem = () => CreateWordListItem(langCode);
// Set up the bind item callback
wordsListView.bindItem = (element, index) => BindWordListItem(element, index, wordsProp, langCode);
// Set up the unbind item callback to clean up event handlers
wordsListView.unbindItem = (element, index) => UnbindWordListItem(element, index);
// Add callback for when items are added/removed
wordsListView.itemsAdded += (items) =>
{
@ -981,7 +981,7 @@ namespace WordsToolkit.Scripts.Levels.Editor
// Refresh available words UI when words are added
EditorWindows.LevelManagerWindow.RefreshInspectorForLevel(level);
};
wordsListView.itemsRemoved += (items) =>
{
serializedObject.ApplyModifiedProperties();
@ -1000,7 +1000,7 @@ namespace WordsToolkit.Scripts.Levels.Editor
// Refresh available words UI when words are removed
EditorWindows.LevelManagerWindow.RefreshInspectorForLevel(level);
};
wordsListContainer.Add(wordsListView);
}
@ -1219,20 +1219,21 @@ namespace WordsToolkit.Scripts.Levels.Editor
private void UpdateWordIcon(VisualElement iconContainer, string wordValue, string langCode)
{
iconContainer.Clear();
// Check if word is used in other levels
var usedInLevels = LevelEditorServices.GetUsedInLevels(wordValue, langCode, level);
bool hasWarning = !string.IsNullOrEmpty(wordValue) && usedInLevels.Length > 0;
// Get banned words service for banned status check
var bannedWordsService = EditorScope.Resolve<IBannedWordsService>();
bool isWordBanned = !string.IsNullOrEmpty(wordValue) && bannedWordsService != null && bannedWordsService.IsWordBanned(wordValue, langCode);
// Check if word is duplicate within current level
bool isDuplicate = IsWordDuplicateInLevel(wordValue, langCode);
// Check if word is not known in model controller
bool isWordUnknown = !string.IsNullOrEmpty(wordValue) && Controller != null &&
bool isWordUnknown = !string.IsNullOrEmpty(wordValue)
&& Controller != null &&
!Controller.IsWordKnown(wordValue, langCode);
if (hasWarning || isWordBanned || isDuplicate || isWordUnknown)
@ -1241,7 +1242,7 @@ namespace WordsToolkit.Scripts.Levels.Editor
warningImage.image = warningIcon;
warningImage.style.width = 16;
warningImage.style.height = 16;
// Priority: Banned > Duplicate > Used in other levels
if (isWordBanned && isDuplicate && hasWarning)
{
@ -1264,7 +1265,7 @@ namespace WordsToolkit.Scripts.Levels.Editor
else if (isDuplicate && hasWarning)
{
// Duplicate + used in other levels
string tooltipText = usedInLevels.Length == 1
string tooltipText = usedInLevels.Length == 1
? $"WARNING: This word is DUPLICATED in this level AND already used in level {usedInLevels[0].number}"
: $"WARNING: This word is DUPLICATED in this level AND already used in levels: {string.Join(", ", usedInLevels.OrderBy(l => l.number).Select(l => l.number.ToString()))}";
iconContainer.tooltip = tooltipText;
@ -1285,7 +1286,7 @@ namespace WordsToolkit.Scripts.Levels.Editor
else if (hasWarning)
{
// Only used in other levels
string tooltipText = usedInLevels.Length == 1
string tooltipText = usedInLevels.Length == 1
? $"This word has already been used in level {usedInLevels[0].number}"
: $"This word has already been used in levels: {string.Join(", ", usedInLevels.OrderBy(l => l.number).Select(l => l.number.ToString()))}";
iconContainer.tooltip = tooltipText;
@ -1297,7 +1298,7 @@ namespace WordsToolkit.Scripts.Levels.Editor
iconContainer.tooltip = "This word is not known in the model controller";
warningImage.tintColor = Color.green;
}
iconContainer.Add(warningImage);
}
}

View File

@ -53,9 +53,9 @@ namespace WordsToolkit.Scripts.Services.IAP
public void UnsubscribeFromPurchaseEvent(Action<string> purchaseHandler)
{
#if UNITY_PURCHASING
#if UNITY_PURCHASING
IAPController.OnSuccessfulPurchase -= purchaseHandler;
#endif
#endif
}
public void BuyProduct(string productId)