modify scripts

This commit is contained in:
2025-10-17 10:59:23 +08:00
parent 9336ed0d6f
commit 4f782a638e
131 changed files with 79880 additions and 3549 deletions

View File

@ -11,7 +11,8 @@
"GUID:c98377141161c7746a178fb5cb1af075",
"GUID:75469ad4d38634e559750d17036d5f7c",
"GUID:b0214a6008ed146ff8f122a6a9c2f6cc",
"jp.hadashikick.vcontainer"
"jp.hadashikick.vcontainer",
"GUID:63a57c8b658089e49a173b0f0c4870a7"
],
"includePlatforms": [],
"excludePlatforms": [],

View File

@ -0,0 +1,126 @@
using System;
using UnityEngine;
using UnityEditor;
using System.IO;
namespace WordsToolkit.Scripts.Editor
{
public class CustomModelPostProcessor : AssetPostprocessor
{
private static readonly string SOURCE_PATH = "Assets/WordsToolkit/model/custom";
private static readonly string TARGET_PATH = "Assets/StreamingAssets/WordConnectGameToolkit/model/custom";
static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
{
bool hasCustomModelChanges = false;
// Check imported assets
foreach (string assetPath in importedAssets)
{
if (IsCustomModelFile(assetPath))
{
hasCustomModelChanges = true;
break;
}
}
// Check moved assets
if (!hasCustomModelChanges)
{
foreach (string assetPath in movedAssets)
{
if (IsCustomModelFile(assetPath))
{
hasCustomModelChanges = true;
break;
}
}
}
if (hasCustomModelChanges)
{
CopyCustomModelsToStreamingAssets();
}
}
private static bool IsCustomModelFile(string assetPath)
{
return assetPath.StartsWith(SOURCE_PATH) &&
(assetPath.EndsWith(".bin") || assetPath.EndsWith(".json") || assetPath.EndsWith(".txt"));
}
private static void CopyCustomModelsToStreamingAssets()
{
if (!Directory.Exists(SOURCE_PATH))
{
return;
}
// Create target directory
Directory.CreateDirectory(TARGET_PATH);
// Get all custom model files
string[] files = Directory.GetFiles(SOURCE_PATH, "*", SearchOption.AllDirectories);
foreach (string sourceFile in files)
{
// Skip .meta files
if (sourceFile.EndsWith(".meta"))
continue;
// Calculate relative path from source directory
string relativePath = Path.GetRelativePath(SOURCE_PATH, sourceFile);
string targetFile = Path.Combine(TARGET_PATH, relativePath);
// Create target subdirectory if needed
string targetDir = Path.GetDirectoryName(targetFile);
if (!Directory.Exists(targetDir))
{
Directory.CreateDirectory(targetDir);
}
try
{
// Copy file if it doesn't exist or is newer
if (!File.Exists(targetFile) || File.GetLastWriteTime(sourceFile) > File.GetLastWriteTime(targetFile))
{
File.Copy(sourceFile, targetFile, true);
Debug.Log($"[CustomModelPostProcessor] Copied {relativePath} to StreamingAssets");
}
}
catch (Exception e)
{
Debug.LogError($"[CustomModelPostProcessor] Failed to copy {sourceFile}: {e.Message}");
}
}
// Refresh the asset database so Unity sees the new files
AssetDatabase.Refresh();
}
[MenuItem("WordToolkit/Copy Custom Models to StreamingAssets")]
private static void ManualCopyCustomModels()
{
CopyCustomModelsToStreamingAssets();
Debug.Log("[CustomModelPostProcessor] Manual copy completed");
}
[MenuItem("WordToolkit/Clean Custom Models from StreamingAssets")]
private static void CleanCustomModelsFromStreamingAssets()
{
if (Directory.Exists(TARGET_PATH))
{
try
{
Directory.Delete(TARGET_PATH, true);
Debug.Log("[CustomModelPostProcessor] Cleaned custom models from StreamingAssets");
AssetDatabase.Refresh();
}
catch (Exception e)
{
Debug.LogError($"[CustomModelPostProcessor] Failed to clean StreamingAssets: {e.Message}");
}
}
}
}
}

View File

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

View File

@ -23,71 +23,71 @@ namespace WordsToolkit.Scripts.Editor
public static string WordConnect = "WordConnect";
private static string WordConnectPath = "Assets/WordConnectGameToolkit";
[MenuItem(nameof(WordConnect) + "/Settings/Shop settings")]
[MenuItem( nameof(WordConnect) + "/Settings/Shop settings")]
public static void IAPProducts()
{
Selection.activeObject = AssetDatabase.LoadMainAssetAtPath(WordConnectPath + "/Resources/Settings/CoinsShopSettings.asset");
}
[MenuItem(nameof(WordConnect) + "/Settings/Ads settings")]
[MenuItem( nameof(WordConnect) + "/Settings/Ads settings")]
public static void AdsSettings()
{
Selection.activeObject = AssetDatabase.LoadMainAssetAtPath(WordConnectPath + "/Resources/Settings/AdsSettings.asset");
}
//DailyBonusSettings
[MenuItem(nameof(WordConnect) + "/Settings/Daily bonus settings")]
[MenuItem( nameof(WordConnect) + "/Settings/Daily bonus settings")]
public static void DailyBonusSettings()
{
Selection.activeObject = AssetDatabase.LoadMainAssetAtPath(WordConnectPath + "/Resources/Settings/DailyBonusSettings.asset");
}
//GameSettings
[MenuItem(nameof(WordConnect) + "/Settings/Game settings")]
[MenuItem( nameof(WordConnect) + "/Settings/Game settings")]
public static void GameSettings()
{
Selection.activeObject = AssetDatabase.LoadMainAssetAtPath(WordConnectPath + "/Resources/Settings/GameSettings.asset");
}
//SpinSettings
[MenuItem(nameof(WordConnect) + "/Settings/Spin settings")]
[MenuItem( nameof(WordConnect) + "/Settings/Spin settings")]
public static void SpinSettings()
{
Selection.activeObject = AssetDatabase.LoadMainAssetAtPath(WordConnectPath + "/Resources/Settings/SpinSettings.asset");
}
//DebugSettings
[MenuItem(nameof(WordConnect) + "/Settings/Debug settings")]
[MenuItem( nameof(WordConnect) + "/Settings/Debug settings")]
public static void DebugSettings()
{
Selection.activeObject = AssetDatabase.LoadMainAssetAtPath(WordConnectPath + "/Resources/Settings/DebugSettings.asset");
}
[MenuItem(nameof(WordConnect) + "/Settings/Crossword config")]
[MenuItem( nameof(WordConnect) + "/Settings/Crossword config")]
public static void CrosswordConfig()
{
Selection.activeObject = AssetDatabase.LoadMainAssetAtPath(WordConnectPath + "/Resources/Settings/CrosswordConfig.asset");
}
[MenuItem(nameof(WordConnect) + "/Settings/Tutorial settings")]
[MenuItem( nameof(WordConnect) + "/Settings/Tutorial settings")]
public static void TutorialSettings()
{
Selection.activeObject = AssetDatabase.LoadMainAssetAtPath(WordConnectPath + "/Resources/Settings/TutorialSettings.asset");
}
[MenuItem(nameof(WordConnect) + "/Settings/Language configuration")]
[MenuItem( nameof(WordConnect) + "/Settings/Language configuration")]
public static void LanguageConfiguration()
{
Selection.activeObject = AssetDatabase.LoadMainAssetAtPath(WordConnectPath + "/Resources/Settings/LanguageConfiguration.asset");
}
[MenuItem(nameof(WordConnect) + "/Settings/Gift settings")]
[MenuItem( nameof(WordConnect) + "/Settings/Gift settings")]
public static void GiftSettings()
{
Selection.activeObject = AssetDatabase.LoadMainAssetAtPath(WordConnectPath + "/Resources/Settings/GiftSettings.asset");
Selection.activeObject = AssetDatabase.LoadMainAssetAtPath(WordConnectPath + "/Resources/Settings/GiftsSettings.asset");
}
[MenuItem(nameof(WordConnect) + "/Scenes/Main scene &1", priority = 0)]
[MenuItem( nameof(WordConnect) + "/Scenes/Main scene &1", priority = 0)]
public static void MainScene()
{
EditorSceneManager.OpenScene(WordConnectPath + "/Scenes/main.unity");
@ -99,7 +99,7 @@ namespace WordsToolkit.Scripts.Editor
EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());
}
[MenuItem(nameof(WordConnect) + "/Scenes/Game scene &2")]
[MenuItem( nameof(WordConnect) + "/Scenes/Game scene &2")]
public static void GameScene()
{
var stateManager = Object.FindObjectOfType<StateManager>();
@ -107,7 +107,7 @@ namespace WordsToolkit.Scripts.Editor
EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());
}
[MenuItem(nameof(WordConnect) + "/Editor/Tile editor", priority = 1)]
[MenuItem( nameof(WordConnect) + "/Editor/Tile editor", priority = 1)]
public static void ColorEditor()
{
string folderPath = WordConnectPath + "/Resources/ColorsTile";
@ -130,44 +130,37 @@ namespace WordsToolkit.Scripts.Editor
EditorGUIUtility.PingObject(tileAsset);
}
[MenuItem(nameof(WordConnect) + "/Documentation/Main", priority = 2)]
[MenuItem( nameof(WordConnect) + "/Documentation/Main", priority = 2)]
public static void MainDoc()
{
Application.OpenURL("https://candy-smith.gitbook.io/main");
}
[MenuItem(nameof(WordConnect) + "/Documentation/ADS/Setup ads")]
[MenuItem( nameof(WordConnect) + "/Documentation/ADS/Setup ads")]
public static void UnityadsDoc()
{
Application.OpenURL("https://candy-smith.gitbook.io/bubble-shooter-toolkit/tutorials/ads-setup/");
}
[MenuItem(nameof(WordConnect) + "/Documentation/Unity IAP (in-apps)")]
[MenuItem( nameof(WordConnect) + "/Documentation/Unity IAP (in-apps)")]
public static void Inapp()
{
Application.OpenURL("https://candy-smith.gitbook.io/main/block-puzzle-game-toolkit/setting-up-in-app-purchase-products");
}
[MenuItem(nameof(WordConnect) + "/NLP/Training Language Model")]
[MenuItem( nameof(WordConnect) + "/NLP/Training Language Model")]
public static void TrainingModel()
{
Application.OpenURL("https://colab.research.google.com/drive/199zNcB3FPfnrD6E7OiwmwCcf27jMnY1b?usp=sharing");
}
[MenuItem(nameof(WordConnect) + "/Reset PlayerPrefs &e")]
[MenuItem( nameof(WordConnect) + "/Reset PlayerPrefs &e")]
private static void ResetPlayerPrefs()
{
GameDataManager.ClearALlData();
PlayerPrefs.DeleteKey("GameState");
Debug.Log("PlayerPrefs are reset");
}
// 🔥 新增菜单项,打开 PrefabBatchViewer 窗口
[MenuItem(nameof(WordConnect) + "/Settings/Prefab Batch Viewer")]
public static void OpenPrefabBatchViewer()
{
PopupPreview.ShowWindow();
}
}
}

View File

@ -23,6 +23,7 @@ namespace WordsToolkit.Scripts.Editor
private static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
{
CheckDefines("Assets/GoogleMobileAds", "ADMOB");
CheckUMPAvailable();
CheckDefines("Assets/FacebookSDK", "FACEBOOK");
CheckDefines("Assets/PlayFabSDK", "PLAYFAB");
CheckDefines("Assets/GameSparks", "GAMESPARKS");
@ -41,6 +42,18 @@ namespace WordsToolkit.Scripts.Editor
}
}
private static void CheckUMPAvailable()
{
if (( Directory.Exists("Assets/GoogleMobileAds")))
{
DefineSymbolsUtils.AddSymbol("UMP_AVAILABLE");
}
else
{
DefineSymbolsUtils.DeleteSymbol("UMP_AVAILABLE");
}
}
public static void CheckIronsourceFolder()
{
var str = "Assets/LevelPlay";

View File

@ -26,7 +26,6 @@ namespace WordsToolkit.Scripts.Enums
LanguageChanged,
TileSelected,
PurchaseSucceeded,
PurchaseFailed,
ButtonClicked,
WordAnimated,
ExtraWordClaimed

View File

@ -34,6 +34,7 @@ namespace WordsToolkit.Scripts.GUI.Buttons.Boosts
private CanvasGroup canvasGroup;
private bool isActive;
private bool isAnimating;
protected override void OnEnable()
@ -79,6 +80,12 @@ namespace WordsToolkit.Scripts.GUI.Buttons.Boosts
protected void OnClick()
{
// Prevent clicks during animation
if (isAnimating)
{
return;
}
if (isActive)
{
Refund();
@ -105,6 +112,7 @@ namespace WordsToolkit.Scripts.GUI.Buttons.Boosts
protected virtual void ActivateBoost(bool hideButtons = true)
{
isAnimating = true;
UpdatePriceDisplay();
if(hideButtons)
buttonViewController.HideOtherButtons(this);
@ -117,6 +125,7 @@ namespace WordsToolkit.Scripts.GUI.Buttons.Boosts
protected virtual void DeactivateBoost()
{
isActive = false;
isAnimating = false;
buttonViewController.ShowButtons();
waves.Clear();
waves.Stop();

View File

@ -0,0 +1,17 @@
using UnityEngine;
using WordsToolkit.Scripts.Settings;
namespace WordsToolkit.Scripts.GUI.Labels
{
public class IAPDisabler : MonoBehaviour
{
private void OnEnable()
{
var gameSettings = Resources.Load<GameSettings>("Settings/GameSettings");
if (gameSettings != null && !gameSettings.enableInApps)
{
gameObject.SetActive(false);
}
}
}
}

View File

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

View File

@ -10,6 +10,7 @@
// // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// // THE SOFTWARE.
using DG.Tweening;
using TMPro;
using UnityEngine;
using VContainer;
@ -65,11 +66,7 @@ namespace WordsToolkit.Scripts.GUI
public void ShowLanguageSelector()
{
menuManager.CloseAllPopups();
menuManager.ShowPopup<LanguageSelectionGame>(null, result =>
{
UpdateText();
});
menuManager.ShowPopup<LanguageSelectionGame>(null, result => { UpdateText(); });
}
}
}

View File

@ -23,12 +23,6 @@ namespace WordsToolkit.Scripts.GUI.Tutorials
{
public class TutorialWordSubstitution : TutorialPopupBase
{
[Inject]
protected LevelManager levelManager;
[Inject]
protected GameManager gameManager;
[SerializeField]
private GameObject hand;

View File

@ -28,6 +28,7 @@ namespace WordsToolkit.Scripts.Gameplay
private WordSelectionManager wordSelectionManager;
private bool isSelected = false;
private Color color;
private string originalLetter; // Store the original letter for validation
private void Awake()
{
@ -74,12 +75,13 @@ namespace WordsToolkit.Scripts.Gameplay
public string GetLetter()
{
return letterText.text;
return originalLetter ?? letterText.text;
}
public void SetText(string toString)
{
letterText.text = toString.ToUpper();
originalLetter = toString; // Store the original letter
letterText.text = toString.ToUpper(); // Display in uppercase
}
public void SetColor(Color color)

View File

@ -523,7 +523,7 @@ namespace WordsToolkit.Scripts.Gameplay.Managers
if (string.IsNullOrEmpty(word))
return false;
if (!wordValidator.IsWordKnown(word.ToLower(), gameStateManager.CurrentLanguage))
if (!wordValidator.IsWordKnown(word, gameStateManager.CurrentLanguage))
return false;
bool wasOpened = false;

View File

@ -37,7 +37,6 @@ namespace WordsToolkit.Scripts.Gameplay
private Color[] openColors = new Color[3];
private bool isSelected = false;
private int wordNumber = -1;
private bool isOpen = false;
[Header("Special Item")]
@ -321,6 +320,8 @@ namespace WordsToolkit.Scripts.Gameplay
// Only respond if the tile is selectable and closed
if (!isOpen && levelManager != null && levelManager.hammerMode)
{
// Immediately disable hammer mode to prevent multiple uses
levelManager.hammerMode = false;
// Instead of opening immediately, play hammer animation first
PlayHammerAnimationAndOpen();
}

View File

@ -42,7 +42,6 @@ namespace WordsToolkit.Scripts.Gameplay
private float idleTimer = 0f;
private Vector2 lastPosition;
public Image cursorImage; // Reference to cursor image
private bool isCursorVisible = true;
private float targetAlpha;
private float currentAlpha;

View File

@ -66,7 +66,6 @@ namespace WordsToolkit.Scripts.Gameplay
private CanvasGroup panelCanvasGroup;
[Header("UI References")]
[SerializeField] private float characterSpacing = 30f; // Spacing between characters
public Image backgroundSelectedWord; // Reference to the background image
[SerializeField]
private TextMeshProUGUI selectedWordText;
@ -463,7 +462,7 @@ namespace WordsToolkit.Scripts.Gameplay
if (selectedWordText != null)
{
selectedWordText.color = new Color(selectedWordText.color.r, selectedWordText.color.g, selectedWordText.color.b, 1f);
selectedWordText.text = GetSelectedWord();
selectedWordText.text = GetSelectedWord().ToUpper();
UpdateHorizontalLayout(layout);
}
}

View File

@ -33,7 +33,6 @@ namespace WordsToolkit.Scripts.Gameplay.WordValidator
if (string.IsNullOrEmpty(word))
return false;
word = word.ToLower();
return (modelController != null && modelController.IsWordKnown(word, currentLanguage)) ||
(customWordRepository != null && customWordRepository.ContainsWord(word));
}

View File

@ -721,17 +721,27 @@ namespace WordsToolkit.Scripts.Levels.Editor
// Handle selection from LevelHierarchyTreeView
if (selectedItem != null && selectedItem.type == LevelHierarchyItem.ItemType.Level && selectedItem.levelAsset != null)
{
// Try to find any available language data in the level
string foundLanguage = FindAvailableLanguage(selectedItem.levelAsset);
// Use the same language selection logic as the "Open Grid" button
// This preserves the user's currently selected language tab
string languageCode = LevelEditorUtility.GetLanguageCodeForLevel(selectedItem.levelAsset);
if (!string.IsNullOrEmpty(foundLanguage))
if (!string.IsNullOrEmpty(languageCode))
{
SetCurrentLevel(selectedItem.levelAsset, foundLanguage);
SetCurrentLevel(selectedItem.levelAsset, languageCode);
}
else
{
// Set the level anyway but with a default language
SetCurrentLevel(selectedItem.levelAsset, "en");
// Fallback: try to find any available language data in the level
string foundLanguage = FindAvailableLanguage(selectedItem.levelAsset);
if (!string.IsNullOrEmpty(foundLanguage))
{
SetCurrentLevel(selectedItem.levelAsset, foundLanguage);
}
else
{
// Set the level anyway but with a default language
SetCurrentLevel(selectedItem.levelAsset, "en");
}
}
}
else if (selectedItem == null || selectedItem.type != LevelHierarchyItem.ItemType.Level)
@ -767,13 +777,25 @@ namespace WordsToolkit.Scripts.Levels.Editor
Level latestLevel = AssetDatabase.LoadAssetAtPath<Level>(path);
if (latestLevel != null)
{
string foundLanguage = FindAvailableLanguage(latestLevel);
if (!string.IsNullOrEmpty(foundLanguage))
// Use the same language selection logic as the rest of the system
string languageCode = LevelEditorUtility.GetLanguageCodeForLevel(latestLevel);
if (!string.IsNullOrEmpty(languageCode))
{
SetCurrentLevel(latestLevel, foundLanguage);
Debug.Log($"CrosswordGridWindow: Loaded latest level '{latestLevel.name}' with language '{foundLanguage}' from EditorPrefs");
SetCurrentLevel(latestLevel, languageCode);
Debug.Log($"CrosswordGridWindow: Loaded latest level '{latestLevel.name}' with language '{languageCode}' from EditorPrefs");
return;
}
else
{
// Fallback
string foundLanguage = FindAvailableLanguage(latestLevel);
if (!string.IsNullOrEmpty(foundLanguage))
{
SetCurrentLevel(latestLevel, foundLanguage);
Debug.Log($"CrosswordGridWindow: Loaded latest level '{latestLevel.name}' with fallback language '{foundLanguage}' from EditorPrefs");
return;
}
}
}
}
}
@ -784,17 +806,26 @@ namespace WordsToolkit.Scripts.Levels.Editor
Level selectedLevel = Selection.activeObject as Level;
if (selectedLevel != null)
{
// Try to find any available language data in the level
string foundLanguage = FindAvailableLanguage(selectedLevel);
if (!string.IsNullOrEmpty(foundLanguage))
// Use the same language selection logic as the rest of the system
string languageCode = LevelEditorUtility.GetLanguageCodeForLevel(selectedLevel);
if (!string.IsNullOrEmpty(languageCode))
{
SetCurrentLevel(selectedLevel, foundLanguage);
Debug.Log($"CrosswordGridWindow: Loaded level '{selectedLevel.name}' with language '{foundLanguage}' from project selection");
SetCurrentLevel(selectedLevel, languageCode);
Debug.Log($"CrosswordGridWindow: Loaded level '{selectedLevel.name}' with language '{languageCode}' from project selection");
}
else
{
Debug.LogWarning($"Level '{selectedLevel.name}' has no language data available.");
// Fallback
string foundLanguage = FindAvailableLanguage(selectedLevel);
if (!string.IsNullOrEmpty(foundLanguage))
{
SetCurrentLevel(selectedLevel, foundLanguage);
Debug.Log($"CrosswordGridWindow: Loaded level '{selectedLevel.name}' with fallback language '{foundLanguage}' from project selection");
}
else
{
Debug.LogWarning($"Level '{selectedLevel.name}' has no language data available.");
}
}
}
}
@ -823,23 +854,30 @@ namespace WordsToolkit.Scripts.Levels.Editor
private string FindAvailableLanguage(Level level)
{
if (level == null) return null;
// Try common language codes in order of preference
string[] languageCodes = { "en", "eng", "english", "en-US", "en-GB", "es", "fr", "de", "ru", "zh" };
foreach (string code in languageCodes)
if (level == null || level.languages == null || level.languages.Count == 0)
return null;
// First, try to use the currently selected language tab (same as LevelEditorUtility.GetLanguageCodeForLevel)
int selectedTabIndex = EditorPrefs.GetInt("WordsToolkit_SelectedLanguageTab", 0);
if (selectedTabIndex >= 0 && selectedTabIndex < level.languages.Count)
{
var languageData = level.GetLanguageData(code);
if (languageData != null)
var selectedLanguage = level.languages[selectedTabIndex];
if (selectedLanguage != null && !string.IsNullOrEmpty(selectedLanguage.language))
{
return code;
return selectedLanguage.language;
}
}
// Fallback: return the first available language
for (int i = 0; i < level.languages.Count; i++)
{
var languageData = level.languages[i];
if (languageData != null && !string.IsNullOrEmpty(languageData.language))
{
return languageData.language;
}
}
// If no common languages found, this would require reflection or other means
// to get all available languages from the Level object
// For now, we'll return null and let the user manually specify
return null;
}
@ -851,19 +889,28 @@ namespace WordsToolkit.Scripts.Levels.Editor
Level selectedLevel = Selection.activeObject as Level;
if (selectedLevel != null)
{
// Try to find any available language data in the level
string foundLanguage = FindAvailableLanguage(selectedLevel);
if (!string.IsNullOrEmpty(foundLanguage))
// Use the same language selection logic as the rest of the system
string languageCode = LevelEditorUtility.GetLanguageCodeForLevel(selectedLevel);
if (!string.IsNullOrEmpty(languageCode))
{
SetCurrentLevel(selectedLevel, foundLanguage);
Debug.Log($"Loaded level: {selectedLevel.name} with language: {foundLanguage}");
SetCurrentLevel(selectedLevel, languageCode);
Debug.Log($"Loaded level: {selectedLevel.name} with language: {languageCode}");
}
else
{
// Set the level anyway but with a default language
SetCurrentLevel(selectedLevel, "en");
Debug.LogWarning($"Level '{selectedLevel.name}' has no language data. Using default language 'en'.");
// Fallback: try to find any available language data in the level
string foundLanguage = FindAvailableLanguage(selectedLevel);
if (!string.IsNullOrEmpty(foundLanguage))
{
SetCurrentLevel(selectedLevel, foundLanguage);
Debug.Log($"Loaded level: {selectedLevel.name} with fallback language: {foundLanguage}");
}
else
{
// Set the level anyway but with a default language
SetCurrentLevel(selectedLevel, "en");
Debug.LogWarning($"Level '{selectedLevel.name}' has no language data. Using default language 'en'.");
}
}
}
else
@ -1023,148 +1070,142 @@ namespace WordsToolkit.Scripts.Levels.Editor
if (_currentLevel == null) return;
var serializedObject = new SerializedObject(_currentLevel);
// Bind letters field (TextField with label)
var lettersField = paletteRoot.Q<TextField>("letters-field");
if (lettersField != null)
var initialValue = languageData.letters ?? "";
lettersField.RegisterValueChangedCallback(evt =>
{
var initialValue = languageData.letters ?? "";
lettersField.RegisterValueChangedCallback(evt =>
// Don't trigger on initial setup (when previous value is empty and new value matches initial data)
if (evt.newValue != evt.previousValue &&
!(string.IsNullOrEmpty(evt.previousValue) && evt.newValue == initialValue))
{
// Don't trigger on initial setup (when previous value is empty and new value matches initial data)
if (evt.newValue != evt.previousValue &&
!(string.IsNullOrEmpty(evt.previousValue) && evt.newValue == initialValue))
// Record undo before changing letters
if (_currentLevel != null)
{
// Record undo before changing letters
if (_currentLevel != null)
// Ensure grid data is serialized before recording undo
var langData = _currentLevel.GetLanguageData(_currentLanguageCode);
if (langData?.crosswordData != null)
{
// Ensure grid data is serialized before recording undo
var langData = _currentLevel.GetLanguageData(_currentLanguageCode);
if (langData?.crosswordData != null)
{
langData.crosswordData.SerializeGrid();
}
Undo.RecordObject(_currentLevel, "Change Letters");
}
languageData.letters = evt.newValue;
var lettersLength = paletteRoot.Q<IntegerField>("letters-length");
if (lettersLength != null)
{
lettersLength.value = evt.newValue?.Length ?? 0;
langData.crosswordData.SerializeGrid();
}
_currentLevel.letters = evt.newValue?.Length ?? 0;
EditorUtility.SetDirty(_currentLevel);
// Generate new words for the updated letters
if (!string.IsNullOrEmpty(evt.newValue))
{
LevelDataEditor.UpdateAvailableWordsForLevel(_currentLevel);
RefreshPreviewData();
}
UpdateGridDisplay();
Undo.RecordObject(_currentLevel, "Change Letters");
}
});
lettersField.value = initialValue;
}
languageData.letters = evt.newValue;
var lettersLength = paletteRoot.Q<IntegerField>("letters-length");
if (lettersLength != null)
{
lettersLength.value = evt.newValue?.Length ?? 0;
}
_currentLevel.letters = evt.newValue?.Length ?? 0;
EditorUtility.SetDirty(_currentLevel);
// Generate new words for the updated letters
if (!string.IsNullOrEmpty(evt.newValue))
{
LevelDataEditor.UpdateAvailableWordsForLevel(_currentLevel);
RefreshPreviewData();
}
UpdateGridDisplay();
}
});
lettersField.value = initialValue;
// Bind letters length field
var lettersLength = paletteRoot.Q<TextField>("letters-length");
if (lettersLength != null)
lettersLength.value = _currentLevel.letters.ToString();
lettersLength.RegisterValueChangedCallback(evt =>
{
lettersLength.value = _currentLevel.letters.ToString();
lettersLength.RegisterValueChangedCallback(evt =>
if (int.TryParse(evt.newValue, out int value))
{
if (int.TryParse(evt.newValue, out int value))
{
_currentLevel.letters = value;
EditorUtility.SetDirty(_currentLevel);
EditorPrefs.SetInt("WordsToolkit_LettersAmount", value);
}
});
}
_currentLevel.letters = value;
EditorUtility.SetDirty(_currentLevel);
EditorPrefs.SetInt("WordsToolkit_LettersAmount", value);
}
});
var generateLettersCheck = paletteRoot.Q<Toggle>("generate-letters-check");
generateLettersCheck.value = EditorPrefs.GetBool("WordsToolkit_GenerateLetters", true);
generateLettersCheck.RegisterValueChangedCallback(evt => {
if (_currentLevel != null)
{
EditorUtility.SetDirty(_currentLevel);
EditorPrefs.SetBool("WordsToolkit_GenerateLetters", evt.newValue);
}
});
// Bind generate button
var generateButton = paletteRoot.Q<Button>("generate-button");
if (generateButton != null)
{
generateButton.clicked += () => {
if (_currentLevel != null && languageData != null)
generateButton.clicked += () => {
if (_currentLevel != null && languageData != null)
{
// Record undo before generating new letters
if (_currentLevel != null)
{
// Record undo before generating new letters
if (_currentLevel != null)
// Ensure grid data is serialized before recording undo
var langData = _currentLevel.GetLanguageData(_currentLanguageCode);
if (langData?.crosswordData != null)
{
// Ensure grid data is serialized before recording undo
var langData = _currentLevel.GetLanguageData(_currentLanguageCode);
if (langData?.crosswordData != null)
{
langData.crosswordData.SerializeGrid();
}
Undo.RecordObject(_currentLevel, "Generate Random Letters");
langData.crosswordData.SerializeGrid();
}
var modelController = EditorScope.Resolve<IModelController>();
string letters = LevelEditorServices.GenerateRandomLetters(languageData, languageData.wordsAmount, _currentLevel.letters);
languageData.letters = letters;
// Update the letters field in the UI
if (lettersField != null) lettersField.value = languageData.letters;
if (lettersLength != null) lettersLength.value = (languageData.letters?.Length ?? 0).ToString();
languageData.words = new string[0];
EditorUtility.SetDirty(_currentLevel);
LevelEditorServices.GenerateAvailableWords(_currentLevel, modelController, languageData);
UpdateCrossword(_currentLevel, languageData);
LevelDataEditor.NotifyWordsListNeedsUpdate(_currentLevel, languageData.language);
RefreshPreviewData();
UpdateLetterPalette();
UpdateGridDisplay();
UpdateStatusBar();
Undo.RecordObject(_currentLevel, "Generate Random Letters");
}
};
}
var modelController = EditorScope.Resolve<IModelController>();
string letters = LevelEditorServices.GenerateRandomLetters(languageData, languageData.wordsAmount, _currentLevel.letters, generateLettersCheck.value );
languageData.letters = letters;
// Update the letters field in the UI
if (lettersField != null) lettersField.value = languageData.letters;
if (lettersLength != null) lettersLength.value = (languageData.letters?.Length ?? 0).ToString();
languageData.words = new string[0];
EditorUtility.SetDirty(_currentLevel);
LevelEditorServices.GenerateAvailableWords(_currentLevel, modelController, languageData);
UpdateCrossword(_currentLevel, languageData);
LevelDataEditor.NotifyWordsListNeedsUpdate(_currentLevel, languageData.language);
RefreshPreviewData();
UpdateLetterPalette();
UpdateGridDisplay();
UpdateStatusBar();
}
};
// Bind grid size controls - columns field
var columnsField = paletteRoot.Q<TextField>("columns-field");
if (columnsField != null)
{
columnsField.value = (_previewData?.columns ?? 10).ToString();
columnsField.RegisterValueChangedCallback(evt => {
if (_previewData != null && int.TryParse(evt.newValue, out int value) && value >= 5 && value <= 50)
{
_previewData.columns = value;
CrosswordPreviewHandler.SavePreviewToLevel(_currentLevel, _currentLanguageCode, _previewData);
EditorUtility.SetDirty(_currentLevel);
EditorPrefs.SetInt("WordsToolkit_grid_x", value);
UpdateGridDisplay();
UpdateStatusBar();
}
});
}
columnsField.value = (_previewData?.columns ?? 10).ToString();
columnsField.RegisterValueChangedCallback(evt => {
if (_previewData != null && int.TryParse(evt.newValue, out int value) && value >= 5 && value <= 50)
{
_previewData.columns = value;
CrosswordPreviewHandler.SavePreviewToLevel(_currentLevel, _currentLanguageCode, _previewData);
EditorUtility.SetDirty(_currentLevel);
EditorPrefs.SetInt("WordsToolkit_grid_x", value);
UpdateGridDisplay();
UpdateStatusBar();
}
});
// Bind grid size controls - rows field
var rowsField = paletteRoot.Q<TextField>("rows-field");
if (rowsField != null)
{
rowsField.value = (_previewData?.rows ?? 7).ToString();
rowsField.RegisterValueChangedCallback(evt => {
if (_previewData != null && int.TryParse(evt.newValue, out int value) && value >= 5 && value <= 50)
{
_previewData.rows = value;
CrosswordPreviewHandler.SavePreviewToLevel(_currentLevel, _currentLanguageCode, _previewData);
EditorUtility.SetDirty(_currentLevel);
EditorPrefs.SetInt("WordsToolkit_grid_y", value);
UpdateGridDisplay();
UpdateStatusBar();
}
});
}
rowsField.value = (_previewData?.rows ?? 7).ToString();
rowsField.RegisterValueChangedCallback(evt => {
if (_previewData != null && int.TryParse(evt.newValue, out int value) && value >= 5 && value <= 50)
{
_previewData.rows = value;
CrosswordPreviewHandler.SavePreviewToLevel(_currentLevel, _currentLanguageCode, _previewData);
EditorUtility.SetDirty(_currentLevel);
EditorPrefs.SetInt("WordsToolkit_grid_y", value);
UpdateGridDisplay();
UpdateStatusBar();
}
});
// Bind Test Level button
var testLevelButton = paletteRoot.Q<Button>("test-level-button");
@ -1319,8 +1360,11 @@ namespace WordsToolkit.Scripts.Levels.Editor
if (_enableEditing && _currentLevel != null)
{
var buttonsRow = CreateLetterButtonsRow();
buttonsRow.style.alignSelf = Align.FlexEnd;
letterButtonsContainer.Add(buttonsRow);
if (buttonsRow != null)
{
buttonsRow.style.alignSelf = Align.FlexEnd;
letterButtonsContainer.Add(buttonsRow);
}
}
}

View File

@ -616,12 +616,16 @@ namespace WordsToolkit.Scripts.Levels.Editor
if (candidate != null && candidate.isValid)
{
// Check for problematic overlaps (both position overlaps and missing words)
bool hasProblematicOverlaps = LevelEditorServices.CheckForOverlappingWords(candidate.placements, wordsToUse);
// Create a string representation of the grid for uniqueness checking
string gridSignature = GridToString(candidate.grid);
// Check if this grid layout is unique and has a good word count
// Check if this grid layout is unique, has a good word count, and no problematic overlaps
if (!generatedGrids.Contains(gridSignature) &&
candidate.placements.Count > bestWordCount)
candidate.placements.Count > bestWordCount &&
!hasProblematicOverlaps)
{
bestVariant = candidate;
bestWordCount = candidate.placements.Count;

View File

@ -1037,6 +1037,7 @@ namespace WordsToolkit.Scripts.Levels.Editor
// Word text field
var wordField = new TextField();
wordField.name = "word-field";
wordField.isReadOnly = true;
wordField.style.flexGrow = 1;
wordField.style.flexShrink = 1;
wordField.style.minWidth = 100;
@ -1677,7 +1678,7 @@ namespace WordsToolkit.Scripts.Levels.Editor
if (string.IsNullOrEmpty(letters))
{
// Generate random letters based on language if needed
letters = LevelEditorServices.GenerateRandomLetters(languageData, languageData.wordsAmount, lettersAmount);
letters = LevelEditorServices.GenerateRandomLetters(languageData, languageData.wordsAmount, lettersAmount, false);
lettersProp.stringValue = letters;
serializedObject.ApplyModifiedProperties();
}
@ -1778,7 +1779,7 @@ namespace WordsToolkit.Scripts.Levels.Editor
private void GenerateWordsForLanguage(Level level, LanguageData languageData, IModelController Controller)
{
string letters = LevelEditorServices.GenerateRandomLetters(languageData, languageData.wordsAmount, level.letters);
string letters = LevelEditorServices.GenerateRandomLetters(languageData, languageData.wordsAmount, level.letters, false);
languageData.letters = letters;
LevelEditorServices.GenerateWordsForLanguage(level, languageData, Controller, false);
UpdateCrossword(level, languageData);
@ -1970,7 +1971,7 @@ namespace WordsToolkit.Scripts.Levels.Editor
{
if (!AssetDatabase.IsValidFolder("Assets/WordConnectGameToolkit/Resources"))
{
AssetDatabase.CreateFolder("Assets/WordsToolkit", "Resources");
AssetDatabase.CreateFolder("Assets/WordConnectGameToolkit", "Resources");
}
AssetDatabase.CreateFolder("Assets/WordConnectGameToolkit/Resources", "Settings");
}

View File

@ -95,7 +95,7 @@ namespace WordsToolkit.Scripts.Levels.Editor
$"Processing language: {languageData.language}",
(float)processedLanguages / totalLanguages);
string letters = GenerateRandomLetters(languageData, languageData.wordsAmount, level.letters);
string letters = GenerateRandomLetters(languageData, languageData.wordsAmount, level.letters, false);
if (!string.IsNullOrEmpty(letters))
{
languageData.letters = letters;
@ -165,10 +165,10 @@ namespace WordsToolkit.Scripts.Levels.Editor
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;
}
Debug.Log($"Generated words for language {languageData.language} in level {level.number}");
// Shuffle the final list
var words = selectedWords.OrderBy(x => UnityEngine.Random.value).ToList();
@ -192,7 +192,7 @@ namespace WordsToolkit.Scripts.Levels.Editor
return model.GetWordsFromSymbols(languageData.letters, languageData.language);
}
public static string GenerateRandomLetters(LanguageData languageData, int count, int lettersAmount)
public static string GenerateRandomLetters(LanguageData languageData, int count, int lettersAmount, bool generateLetters)
{
var controller = EditorScope.Resolve<IModelController>();
var bannedWordsService = EditorScope.Resolve<IBannedWordsService>();
@ -204,6 +204,19 @@ namespace WordsToolkit.Scripts.Levels.Editor
return "";
}
// 30% chance to generate actual random letters instead of finding optimal words
if (generateLetters)
{
string randomLetters = GenerateActualRandomLetters(languageData.language, lettersAmount);
// Check if these random letters can generate any words
var wordsFromRandomLetters = controller.GetWordsFromSymbols(randomLetters, languageData.language);
if (wordsFromRandomLetters != null && wordsFromRandomLetters.Count() > 0)
{
return randomLetters;
}
}
var usedWords = LevelEditorServices.GetUsedWords(languageData.language);
// Try getting words with specified length
@ -212,28 +225,38 @@ namespace WordsToolkit.Scripts.Levels.Editor
// 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++)
// Check if model is loaded for this language
if (!controller.IsModelLoaded(languageData.language))
{
// Try smaller length
if (lettersAmount - offset > 2)
controller.LoadModels();
// Try again after reloading
words = controller.GetWordsWithLength(lettersAmount, languageData.language).Where(w => !usedWords.Contains(w)).ToList();
}
// If still no words found after reloading, try with other lengths
if (words.Count == 0)
{
// Try with smaller lengths first, then larger
for (int offset = 1; offset <= 3; offset++)
{
words = controller.GetWordsWithLength(lettersAmount - offset, languageData.language);
// 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;
}
// 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>
{
@ -248,7 +271,7 @@ namespace WordsToolkit.Scripts.Levels.Editor
}
else
{
return "".Substring(0, Mathf.Min(lettersAmount, 26));
return new string('a', lettersAmount);
}
}
@ -270,7 +293,6 @@ namespace WordsToolkit.Scripts.Levels.Editor
if (wordsCount >= count)
{
Debug.Log($"Found optimal word '{words[i]}' that can generate {wordsCount} words (excluding banned words)");
return words[i];
}
@ -281,10 +303,73 @@ namespace WordsToolkit.Scripts.Levels.Editor
}
}
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 string GenerateActualRandomLetters(string language, int length)
{
// Define Latin languages that use vowel/consonant distinction
HashSet<string> latinLanguages = new HashSet<string> { "en", "es", "fr", "de", "it", "pt", "nl", "da", "sv", "no" };
if (latinLanguages.Contains(language))
{
// For Latin languages, use vowel/consonant distribution
string vowels = "aeiou";
string consonants = "bcdfghjklmnpqrstvwxyz";
var result = new List<char>();
int vowelCount = Mathf.Max(1, length / 2); // About 1/2 vowels
int consonantCount = length - vowelCount;
// Add vowels
for (int i = 0; i < vowelCount; i++)
{
result.Add(vowels[UnityEngine.Random.Range(0, vowels.Length)]);
}
// Add consonants
for (int i = 0; i < consonantCount; i++)
{
result.Add(consonants[UnityEngine.Random.Range(0, consonants.Length)]);
}
// Shuffle the letters
var chars = result.ToArray();
for (int i = 0; i < chars.Length; i++)
{
int randomIndex = UnityEngine.Random.Range(i, chars.Length);
(chars[i], chars[randomIndex]) = (chars[randomIndex], chars[i]);
}
return new string(chars);
}
else
{
// For non-Latin languages, use default letters without vowel distinction
Dictionary<string, string> defaultLetters = new Dictionary<string, string>
{
{ "ru", "оеаинтсрвлкмдпуяыьгзбчйхжшюцщэфъ" },
{ "zh", "一二三四五六七八九十百千万亿" },
{ "ja", "あいうえおかきくけこさしすせそたちつてとなにぬねの" },
{ "ar", "ابتثجحخدذرزسشصضطظعغفقكلمنهوي" }
};
string letters;
if (!defaultLetters.TryGetValue(language, out letters))
{
letters = "abcdefghijklmnopqrstuvwxyz"; // Fallback to Latin alphabet
}
var result = new List<char>();
for (int i = 0; i < length; i++)
{
result.Add(letters[UnityEngine.Random.Range(0, letters.Length)]);
}
return new string(result.ToArray());
}
}
private static List<string> GetUsedWords(string languageDataLanguage)
{
// Get all levels in the project
@ -503,5 +588,77 @@ namespace WordsToolkit.Scripts.Levels.Editor
}
};
}
// Check for problematic overlapping words
public static bool CheckForOverlappingWords(List<WordPlacement> placements, string[] originalWords = null)
{
// Check for words starting at the same position
var sameStartGroups = placements
.GroupBy(p => p.startPosition)
.Where(g => g.Count() > 1)
.ToList();
bool hasPositionOverlaps = sameStartGroups.Count > 0;
// Check for same-orientation overlaps (words that overlap along their length)
bool hasSameOrientationOverlaps = false;
for (int i = 0; i < placements.Count; i++)
{
for (int j = i + 1; j < placements.Count; j++)
{
var word1 = placements[i];
var word2 = placements[j];
// Only check words with the same orientation
if (word1.isHorizontal == word2.isHorizontal)
{
if (word1.isHorizontal)
{
// Both horizontal - check if they're on the same row and overlap
if (word1.startPosition.y == word2.startPosition.y)
{
int word1End = word1.startPosition.x + word1.word.Length - 1;
int word2End = word2.startPosition.x + word2.word.Length - 1;
// Check for overlap
bool overlaps = !(word1End < word2.startPosition.x || word2End < word1.startPosition.x);
if (overlaps)
{
hasSameOrientationOverlaps = true;
break;
}
}
}
else
{
// Both vertical - check if they're on the same column and overlap
if (word1.startPosition.x == word2.startPosition.x)
{
int word1End = word1.startPosition.y + word1.word.Length - 1;
int word2End = word2.startPosition.y + word2.word.Length - 1;
// Check for overlap
bool overlaps = !(word1End < word2.startPosition.y || word2End < word1.startPosition.y);
if (overlaps)
{
hasSameOrientationOverlaps = true;
break;
}
}
}
}
}
if (hasSameOrientationOverlaps) break;
}
// Check if some words are missing from placements (indicating overlaps prevented placement)
bool hasMissingWords = false;
if (originalWords != null)
{
hasMissingWords = originalWords.Length > placements.Count;
}
return hasPositionOverlaps || hasSameOrientationOverlaps || hasMissingWords;
}
}
}

View File

@ -4,11 +4,7 @@
"references": [
"GUID:343deaaf83e0cee4ca978e7df0b80d21",
"GUID:d3bf71b33c0c04eb9bc1a8d6513d76bb",
"GUID:b0214a6008ed146ff8f122a6a9c2f6cc",
"GUID:00dd4a7ac8c24c898083910c81898ecc",
"GUID:ac6e78962cfc743b9a5fc5f5a808aa72",
"GUID:b25ad8286798741e3b2cc3883283e669",
"GUID:75bdbcf23199f4cfb86c610d1d946666"
"GUID:b0214a6008ed146ff8f122a6a9c2f6cc"
],
"includePlatforms": [
"Editor"

View File

@ -1,6 +1,7 @@
using System.Collections.Generic;
using Unity.Sentis;
using Unity.InferenceEngine;
using UnityEngine;
using ModelAsset = Unity.InferenceEngine.ModelAsset;
namespace WordsToolkit.Scripts.Levels
{

View File

@ -158,7 +158,17 @@ namespace WordsToolkit.Scripts.Localization
if (Application.isEditor)
{
return _debugSettings.TestLanguage;
// Use TestLanguageCode from DebugSettings and convert to SystemLanguage
try
{
var cultureInfo = new CultureInfo(_debugSettings.TestLanguageCode);
return (SystemLanguage)Enum.Parse(typeof(SystemLanguage), cultureInfo.EnglishName);
}
catch (Exception)
{
Debug.LogWarning($"Failed to parse TestLanguageCode '{_debugSettings.TestLanguageCode}'. Using English as fallback.");
return SystemLanguage.English;
}
}
return Application.systemLanguage;

View File

@ -12,6 +12,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using TMPro;
using UnityEngine;
@ -64,14 +65,24 @@ namespace WordsToolkit.Scripts.Localization
SystemLanguage lang;
if (Application.platform == RuntimePlatform.WindowsEditor || Application.platform == RuntimePlatform.OSXEditor || Application.platform == RuntimePlatform.LinuxEditor)
{
lang = _debugSettings.TestLanguage;
// Use TestLanguageCode from DebugSettings and convert to SystemLanguage
try
{
var cultureInfo = new CultureInfo(_debugSettings.TestLanguageCode);
lang = (SystemLanguage)Enum.Parse(typeof(SystemLanguage), cultureInfo.EnglishName);
}
catch (Exception)
{
Debug.LogWarning($"Failed to parse TestLanguageCode '{_debugSettings.TestLanguageCode}'. Using English as fallback.");
lang = SystemLanguage.English;
}
}
else
{
lang = Application.systemLanguage;
}
return this.Any(i => i.language == lang) ? _debugSettings.TestLanguage : SystemLanguage.English;
return this.Any(i => i.language == lang) ? lang : SystemLanguage.English;
}
public IEnumerable<LanguageObject> GetText(DebugSettings _debugSettings)

View File

@ -3,9 +3,10 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using Newtonsoft.Json.Linq;
using Unity.Sentis;
using Unity.InferenceEngine;
using UnityEngine;
using System.Text;
using System.Globalization;
using WordsToolkit.Scripts.Levels;
using WordsToolkit.Scripts.Services;
using WordsToolkit.Scripts.Services.BannedWords;
@ -36,6 +37,32 @@ namespace WordsToolkit.Scripts.NLP
// NOTE: This is now mainly for the old SaveModelBinary method - new architecture uses custom words files
private bool protectBinaryFile = false;
/// <summary>
/// Normalizes text by removing diacritics, accents, and converting to lowercase.
/// This allows word matching to ignore emphasis marks.
/// </summary>
private string NormalizeText(string text)
{
if (string.IsNullOrEmpty(text))
return text;
text = text.ToLower();
var normalizedString = text.Normalize(NormalizationForm.FormD);
var stringBuilder = new StringBuilder();
foreach (var c in normalizedString)
{
var unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c);
if (unicodeCategory != UnicodeCategory.NonSpacingMark)
{
stringBuilder.Append(c);
}
}
return stringBuilder.ToString().Normalize(NormalizationForm.FormC);
}
public bool IsModelLoaded(string language = null)
{
language = language ?? (languageService?.GetCurrentLanguageCode() ?? m_DefaultLanguage);
@ -82,6 +109,7 @@ namespace WordsToolkit.Scripts.NLP
public void LoadModels()
{
InitializeFromConfiguration();
foreach (var languagePair in languageModels)
{
LoadModelBin(languagePair.Key, languagePair.Value);
@ -191,29 +219,62 @@ namespace WordsToolkit.Scripts.NLP
LoadCustomWordsFromBinary(language);
}
/// <summary>
/// Loads bytes from StreamingAssets using UnityWebRequest for Android compatibility
/// </summary>
private byte[] LoadStreamingAssetBytes(string path)
{
try
{
#if UNITY_ANDROID && !UNITY_EDITOR
using var request = UnityEngine.Networking.UnityWebRequest.Get(path);
var operation = request.SendWebRequest();
while (!operation.isDone) { }
if (request.result == UnityEngine.Networking.UnityWebRequest.Result.Success)
{
return request.downloadHandler.data;
}
return null;
#else
if (File.Exists(path))
{
return File.ReadAllBytes(path);
}
return null;
#endif
}
catch (Exception e)
{
Debug.LogError($"[ModelController] Exception in LoadStreamingAssetBytes: {e.Message}");
return null;
}
}
/// <summary>
/// Loads custom words from binary file and adds them to the existing vocabulary.
/// Binary file contains ONLY custom words, not the entire model cache.
/// </summary>
private void LoadCustomWordsFromBinary(string language)
{
string path = Path.Combine(Application.dataPath, "WordsToolkit", "model",
string path = Path.Combine(Application.streamingAssetsPath, "WordConnectGameToolkit", "model",
"custom", $"{language}_custom_words.bin");
if (!File.Exists(path))
if (!wordToIndexByLanguage.ContainsKey(language))
{
return;
}
if (!wordToIndexByLanguage.ContainsKey(language))
byte[] fileData = LoadStreamingAssetBytes(path);
if (fileData == null)
{
return;
}
try
{
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read);
using var br = new BinaryReader(fs, Encoding.UTF8);
using var ms = new MemoryStream(fileData);
using var br = new BinaryReader(ms, Encoding.UTF8);
// Read header
if (br.ReadInt32() != 0x43555354) // "CUST" magic number
@ -265,9 +326,11 @@ namespace WordsToolkit.Scripts.NLP
return;
}
string dir = Path.Combine(Application.dataPath, "WordsToolkit", "model", "custom");
string path = Path.Combine(dir, $"{language}_custom_words.bin");
Directory.CreateDirectory(dir);
// Create StreamingAssets folder structure
string streamingAssetsDir = Path.Combine(Application.dataPath, "StreamingAssets");
string modelDir = Path.Combine(streamingAssetsDir, "WordConnectGameToolkit", "model", "custom");
string path = Path.Combine(modelDir, $"{language}_custom_words.bin");
Directory.CreateDirectory(modelDir);
try
{
@ -353,7 +416,8 @@ namespace WordsToolkit.Scripts.NLP
foreach (var pair in wordIndexDict)
{
wordToIndex[pair.Key] = pair.Value;
string normalizedWord = NormalizeText(pair.Key);
wordToIndex[normalizedWord] = pair.Value;
}
@ -379,6 +443,7 @@ namespace WordsToolkit.Scripts.NLP
return null;
}
word = NormalizeText(word);
if (!wordToIndexByLanguage[language].ContainsKey(word))
{
return null;
@ -413,7 +478,8 @@ namespace WordsToolkit.Scripts.NLP
public bool IsWordKnown(string word, string language = null)
{
language = language ?? (languageService?.GetCurrentLanguageCode() ?? m_DefaultLanguage);
if (bannedWordsService.IsWordBanned(word, language))
string normalizedWord = NormalizeText(word);
if (bannedWordsService.IsWordBanned(normalizedWord, language))
{
return false;
}
@ -451,6 +517,8 @@ namespace WordsToolkit.Scripts.NLP
return -1f;
}
word1 = NormalizeText(word1);
word2 = NormalizeText(word2);
float[] vector1 = GetWordVector(word1, language);
float[] vector2 = GetWordVector(word2, language);
@ -466,10 +534,11 @@ namespace WordsToolkit.Scripts.NLP
if (!IsModelLoaded(language))
{
Debug.LogWarning($"[ModelController] AddWord failed  model for '{language}' not loaded.");
Debug.LogWarning($"[ModelController] AddWord failed model for '{language}' not loaded.");
return false;
}
newWord = NormalizeText(newWord);
if (wordToIndexByLanguage[language].ContainsKey(newWord))
{
Debug.LogWarning($"[ModelController] Word '{newWord}' already exists in vocab.");
@ -523,8 +592,8 @@ namespace WordsToolkit.Scripts.NLP
Buffer.BlockCopy(oldBuf, 0, newBuf, 0, oldElems * sizeof(float));
Buffer.BlockCopy(newVector,0, newBuf, oldElems * sizeof(float), dim * sizeof(float));
// Sentis requires a nongeneric NativeTensorArrayFromManagedArray
// Sentis requires (Array, bytesPerElem, length, channels)
// Inference Engine requires a nongeneric NativeTensorArrayFromManagedArray
// Inference Engine requires (Array, bytesPerElem, length, channels)
// ctor args: (Array data, int srcElementOffset, int srcElementSize, int numDestElement)
var newWeights = new NativeTensorArrayFromManagedArray(
newBuf, // managed float[]
@ -638,7 +707,7 @@ namespace WordsToolkit.Scripts.NLP
if (string.IsNullOrEmpty(inputSymbols))
return new List<string>();
inputSymbols = inputSymbols.ToLower();
inputSymbols = NormalizeText(inputSymbols);
Dictionary<char, int> charCounts = new Dictionary<char, int>();
foreach (char c in inputSymbols)
{
@ -702,7 +771,8 @@ namespace WordsToolkit.Scripts.NLP
if (string.IsNullOrEmpty(inputSymbols))
return null;
var symbolSet = new HashSet<char>(inputSymbols.ToLower());
inputSymbols = NormalizeText(inputSymbols);
var symbolSet = new HashSet<char>(inputSymbols);
var bestMatches = wordToIndexByLanguage[language].Keys
.Select(word => new {
@ -855,7 +925,7 @@ namespace WordsToolkit.Scripts.NLP
/// <param name="language">Language to clear, or null to clear all</param>
public void ClearCustomWordsCache(string language = null)
{
string customDir = Path.Combine(Application.dataPath, "WordsToolkit", "model", "custom");
string customDir = Path.Combine(Application.dataPath, "StreamingAssets", "WordConnectGameToolkit", "model", "custom");
if (!Directory.Exists(customDir))
return;

View File

@ -11,14 +11,9 @@
// // THE SOFTWARE.
using System.Linq;
using TMPro;
using UnityEngine;
using UnityEngine.Serialization;
using WordsToolkit.Scripts.Audio;
using WordsToolkit.Scripts.Data;
using WordsToolkit.Scripts.Enums;
using WordsToolkit.Scripts.GUI;
using WordsToolkit.Scripts.GUI.Labels;
using WordsToolkit.Scripts.Services;
using WordsToolkit.Scripts.Services.IAP;
using WordsToolkit.Scripts.Settings;
using WordsToolkit.Scripts.System;
@ -31,6 +26,8 @@ namespace WordsToolkit.Scripts.Popups
private CoinsShopSettings shopSettings;
public ProductID noAdsProduct;
[SerializeField]
public TextMeshProUGUI noAdsPriceText;
[SerializeField]
private AudioClip coinsSound;
private void OnEnable()
@ -49,11 +46,14 @@ namespace WordsToolkit.Scripts.Popups
}
EventManager.GetEvent<string>(EGameEvent.PurchaseSucceeded).Subscribe(PurchaseSucceded);
EventManager.GetEvent<(string, string)>(EGameEvent.PurchaseFailed).Subscribe(PurchaseFailed);
// Update NoAds price display
UpdateNoAdsPriceDisplay();
}
private void OnDisable()
protected override void OnDisable()
{
base.OnDisable();
EventManager.GetEvent<string>(EGameEvent.PurchaseSucceeded).Unsubscribe(PurchaseSucceded);
}
@ -86,44 +86,6 @@ namespace WordsToolkit.Scripts.Popups
}
}
private void PurchaseFailed((string, string) info)
{
var productId = info.Item1;
var errorMessage = info.Item2;
var errorType = IAPErrorHelper.ParseError(errorMessage);
Debug.LogWarning($"Purchase failed for product {productId}: {errorMessage}, Error Type: {errorType}");
switch (errorType)
{
case IAPErrorType.InvalidProductID:
Debug.LogError($"Invalid product ID: {productId}");
ShowErrorMessage("Invalid product ID. Please try again later.");
break;
case IAPErrorType.UserCancelled:
Debug.LogWarning("Purchase cancelled by user.");
break;
case IAPErrorType.DuplicateTransaction:
Debug.LogWarning($"Duplicate transaction for product {productId}. This usually means the purchase was already completed.");
break;
case IAPErrorType.IAPInitFailed:
Debug.LogError("IAP initialization failed. Please check your IAP settings.");
ShowErrorMessage("IAP initialization failed. Please try again later.");
break;
default:
ShowErrorMessage($"Payment failed: {errorMessage}");
break;
}
}
private void ShowErrorMessage(string message)
{
var popup = menuManager.ShowPopup<MessagePopup>();
if (popup != null)
{
popup.SetMessage(message);
}
}
public void BuyCoins(string id)
{
// StopInteration();
@ -133,5 +95,17 @@ namespace WordsToolkit.Scripts.Popups
iapManager.BuyProduct(id);
#endif
}
private void UpdateNoAdsPriceDisplay()
{
if (noAdsPriceText != null && noAdsProduct != null)
{
var localizedPrice = iapManager.GetProductLocalizedPriceString(noAdsProduct.ID);
if (!string.IsNullOrEmpty(localizedPrice))
{
noAdsPriceText.text = localizedPrice;
}
}
}
}
}

View File

@ -10,6 +10,8 @@
// // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// // THE SOFTWARE.
using TMPro;
using UnityEngine;
using WordsToolkit.Scripts.Enums;
using WordsToolkit.Scripts.GUI;
using WordsToolkit.Scripts.GUI.Buttons;
@ -23,11 +25,16 @@ namespace WordsToolkit.Scripts.Popups
{
public CustomButton removeAdsButton;
public ProductID productID;
[SerializeField]
public TextMeshProUGUI priceText; // Add price display UI element
private void OnEnable()
{
removeAdsButton.onClick.AddListener(RemoveAds);
EventManager.GetEvent<string>(EGameEvent.PurchaseSucceeded).Subscribe(PurchaseSucceeded);
// Set localized price when popup opens
UpdatePriceDisplay();
}
protected override void OnDisable()
@ -49,5 +56,17 @@ namespace WordsToolkit.Scripts.Popups
{
iapManager.BuyProduct(productID.ID);
}
private void UpdatePriceDisplay()
{
if (priceText != null && productID != null)
{
var localizedPrice = iapManager.GetProductLocalizedPriceString(productID.ID);
if (!string.IsNullOrEmpty(localizedPrice))
{
priceText.text = localizedPrice;
}
}
}
}
}

View File

@ -18,6 +18,7 @@ using WordsToolkit.Scripts.GUI;
using WordsToolkit.Scripts.System;
using VContainer;
using WordsToolkit.Scripts.GUI.Buttons;
using WordsToolkit.Scripts.Services;
namespace WordsToolkit.Scripts.Popups
{
@ -26,6 +27,9 @@ namespace WordsToolkit.Scripts.Popups
[SerializeField]
private CustomButton privacypolicy;
[SerializeField]
private CustomButton googleUMPConsent;
[SerializeField]
private Button restorePurchase;
@ -38,6 +42,7 @@ namespace WordsToolkit.Scripts.Popups
protected virtual void OnEnable()
{
privacypolicy?.onClick.AddListener(PrivacyPolicy);
googleUMPConsent?.onClick.AddListener(ReconsiderGoogleUMPConsent);
LoadVibrationLevel();
vibrationSlider.onValueChanged.AddListener(SaveVibrationLevel);
closeButton.onClick.RemoveAllListeners();
@ -94,6 +99,14 @@ namespace WordsToolkit.Scripts.Popups
Close();
}
private void ReconsiderGoogleUMPConsent()
{
StopInteration();
DisablePause();
adsManager.ReconsiderUMPConsent();
Close();
}
private void DisablePause()
{
if (stateManager.CurrentState == EScreenStates.Game)

View File

@ -22,10 +22,11 @@ namespace WordsToolkit.Scripts.Services.Ads.AdUnits
public Action<string> OnShown { get; set; }
public Action<string> OnInitialized { get; set; }
public AdUnit(string placementId, AdReference adReference)
public AdUnit(string placementId, AdReference adReference, AdsHandlerBase adsHandler)
{
PlacementId = placementId;
AdReference = adReference;
AdsHandler = adsHandler;
}
public AdsHandlerBase AdsHandler { get; set; }

View File

@ -10,8 +10,14 @@
// // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// // THE SOFTWARE.
#if IRONSOURCE
using Unity.Services.LevelPlay;
#endif
using UnityEngine;
using WordsToolkit.Scripts.Services.Ads.AdUnits;
#if UMP_AVAILABLE
using GoogleMobileAds.Ump.Api;
#endif
namespace WordsToolkit.Scripts.Services.Ads.Networks
{
@ -19,14 +25,42 @@ namespace WordsToolkit.Scripts.Services.Ads.Networks
public class IronsourceAdsHandler : AdsHandlerBase
{
private IAdsListener _listener;
#if IRONSOURCE
private LevelPlayInterstitialAd _interstitialAd;
private LevelPlayRewardedAd _rewardedAd;
#endif
private void Init(string _id)
{
#if IRONSOURCE
IronSource.Agent.setManualLoadRewardedVideo(true);
IronSource.Agent.validateIntegration();
IronSource.Agent.init(_id);
LevelPlay.ValidateIntegration();
// Set consent for LevelPlay
SetConsentStatus();
// Register initialization events
LevelPlay.OnInitSuccess += SdkInitializationCompletedEvent;
LevelPlay.OnInitFailed += SdkInitializationFailedEvent;
LevelPlay.Init(_id);
#endif
}
private void SetConsentStatus()
{
#if IRONSOURCE && UMP_AVAILABLE
bool hasConsent = ConsentInformation.CanRequestAds();
Debug.Log($"LevelPlay consent status: {hasConsent}");
// Set consent for GDPR
LevelPlay.SetConsent(hasConsent);
// For CCPA compliance (optional)
LevelPlay.SetMetaData("do_not_sell", hasConsent ? "false" : "true");
#elif IRONSOURCE
// Default to no consent if UMP not available
LevelPlay.SetConsent(false);
Debug.Log("UMP not available - setting LevelPlay consent to false");
#endif
}
@ -34,78 +68,121 @@ namespace WordsToolkit.Scripts.Services.Ads.Networks
{
_listener = listener;
Debug.Log(_listener);
#if IRONSOURCE
//Add Rewarded Video Events
IronSourceInterstitialEvents.onAdReadyEvent += OnInterstitialAdReady;
IronSourceInterstitialEvents.onAdLoadFailedEvent += InterstitialAdLoadFailedEvent;
IronSourceRewardedVideoEvents.onAdReadyEvent += OnRewardedVideoAdReady;
IronSourceRewardedVideoEvents.onAdLoadFailedEvent += RewardedVideoAdShowFailedEvent;
IronSourceRewardedVideoEvents.onAdRewardedEvent += Rewardeded;
IronSourceEvents.onSdkInitializationCompletedEvent += SdkInitializationCompletedEvent;
#endif
}
#if IRONSOURCE
private void Rewardeded(IronSourcePlacement obj, IronSourceAdInfo ironSourceAdInfo)
private void SdkInitializationCompletedEvent(LevelPlayConfiguration config)
{
Debug.Log("Ironsource Rewardeded");
_listener?.OnAdsShowComplete();
}
private void SdkInitializationCompletedEvent()
{
Debug.Log("Ironsource SdkInitializationCompletedEvent");
Debug.Log("LevelPlay SdkInitializationCompletedEvent");
_listener?.OnAdsInitialized();
}
private void InterstitialAdLoadFailedEvent(IronSourceError obj)
private void SdkInitializationFailedEvent(LevelPlayInitError error)
{
Debug.Log("Ironsource InterstitialAdLoadFailedEvent " + obj.getCode() + " " + obj.getDescription());
Debug.Log($"LevelPlay SdkInitializationFailedEvent: {error}");
_listener?.OnInitFailed();
}
// Interstitial event handlers
private void InterstitialAdLoadedEvent(LevelPlayAdInfo adInfo)
{
Debug.Log("LevelPlay OnInterstitialAdReady");
_listener?.OnAdsLoaded(adInfo.AdUnitId);
}
private void InterstitialAdLoadFailedEvent(LevelPlayAdError error)
{
Debug.Log($"LevelPlay InterstitialAdLoadFailedEvent: {error}");
_listener?.OnAdsLoadFailed();
}
private void RewardedVideoAdShowFailedEvent(IronSourceError obj)
private void InterstitialAdDisplayFailedEvent(LevelPlayAdInfo levelPlayAdInfo, LevelPlayAdError levelPlayAdError)
{
Debug.Log("1" + obj.getCode());
Debug.Log("2" + obj.getDescription());
Debug.Log("Ironsource RewardedVideoAdShowFailedEvent " + obj.getCode() + " " + obj.getDescription());
Debug.Log(_listener);
Debug.Log($"LevelPlay InterstitialAdDisplayFailedEvent: {levelPlayAdError}");
_listener?.OnAdsShowFailed();
LevelPlay.SetPauseGame(false);
}
private void OnRewardedVideoAdReady(IronSourceAdInfo obj)
#if LEVELPLAY8
private void InterstitialAdDisplayFailedEvent(LevelPlayAdDisplayInfoError obj)
{
Debug.Log("Ironsource OnRewardedVideoAdReady");
_listener?.OnAdsLoaded(obj.instanceId);
}
private void OnInterstitialAdReady(IronSourceAdInfo obj)
{
Debug.Log("Ironsource OnInterstitialAdReady");
_listener?.OnAdsLoaded(obj.instanceId);
Debug.Log($"LevelPlay InterstitialAdDisplayFailedEvent: {obj}");
_listener?.OnAdsShowFailed();
LevelPlay.SetPauseGame(false);
}
#endif
// Rewarded video event handlers
private void RewardedAdLoadedEvent(LevelPlayAdInfo adInfo)
{
Debug.Log("LevelPlay OnRewardedVideoAdReady");
_listener?.OnAdsLoaded(adInfo.AdUnitId);
}
private void RewardedAdLoadFailedEvent(LevelPlayAdError error)
{
Debug.Log($"LevelPlay RewardedVideoAdLoadFailedEvent: {error}");
_listener?.OnAdsLoadFailed();
}
private void RewardedAdDisplayFailedEvent(LevelPlayAdInfo levelPlayAdInfo, LevelPlayAdError levelPlayAdError)
{
Debug.Log($"LevelPlay RewardedVideoAdShowFailedEvent: {levelPlayAdError}");
_listener?.OnAdsShowFailed();
LevelPlay.SetPauseGame(false);
}
#if LEVELPLAY8
private void RewardedAdDisplayFailedEvent(LevelPlayAdDisplayInfoError obj)
{
Debug.Log($"LevelPlay RewardedAdDisplayFailedEvent: {obj}");
_listener?.OnAdsShowFailed();
LevelPlay.SetPauseGame(false);
}
#endif
private void RewardedAdRewardedEvent(LevelPlayAdInfo adInfo, LevelPlayReward reward)
{
Debug.Log("LevelPlay Rewarded");
_listener?.OnAdsShowComplete();
LevelPlay.SetPauseGame(false);
}
private void InterstitialDisplayedEvent(LevelPlayAdInfo obj)
{
Debug.Log("LevelPlay InterstitialDisplayedEvent");
_listener?.OnAdsShowStart();
LevelPlay.SetPauseGame(false);
}
#endif
public override void Init(string _id, bool adSettingTestMode, IAdsListener listener)
{
Debug.Log("Ironsource Init");
Debug.Log("LevelPlay Init");
Init(_id);
Debug.Log("Ironsource SetListener");
Debug.Log("LevelPlay SetListener");
SetListener(listener);
}
public override void Show(AdUnit adUnit)
{
#if IRONSOURCE
if (adUnit.AdReference.adType == EAdType.Interstitial)
if (adUnit.AdReference.adType == EAdType.Interstitial && _interstitialAd != null)
{
IronSource.Agent.showInterstitial();
if (_interstitialAd.IsAdReady())
{
_interstitialAd.ShowAd();
LevelPlay.SetPauseGame(true);
}
}
else if (adUnit.AdReference.adType == EAdType.Rewarded)
else if (adUnit.AdReference.adType == EAdType.Rewarded && _rewardedAd != null)
{
IronSource.Agent.showRewardedVideo();
if (_rewardedAd.IsAdReady())
{
_rewardedAd.ShowAd();
LevelPlay.SetPauseGame(true);
}
}
_listener?.Show(adUnit);
@ -117,26 +194,43 @@ namespace WordsToolkit.Scripts.Services.Ads.Networks
#if IRONSOURCE
if (adUnit.AdReference.adType == EAdType.Interstitial)
{
IronSource.Agent.loadInterstitial();
_interstitialAd = new LevelPlayInterstitialAd(adUnit.PlacementId);
_interstitialAd.OnAdLoaded += InterstitialAdLoadedEvent;
_interstitialAd.OnAdDisplayed += InterstitialDisplayedEvent;
_interstitialAd.OnAdLoadFailed += InterstitialAdLoadFailedEvent;
_interstitialAd.OnAdDisplayFailed += InterstitialAdDisplayFailedEvent;
_interstitialAd.LoadAd();
}
else if (adUnit.AdReference.adType == EAdType.Rewarded)
{
IronSource.Agent.loadRewardedVideo();
_rewardedAd = new LevelPlayRewardedAd(adUnit.PlacementId);
_rewardedAd.OnAdLoaded += RewardedAdLoadedEvent;
_rewardedAd.OnAdLoadFailed += RewardedAdLoadFailedEvent;
_rewardedAd.OnAdDisplayFailed += RewardedAdDisplayFailedEvent;
_rewardedAd.OnAdRewarded += RewardedAdRewardedEvent;
_rewardedAd.LoadAd();
}
#endif
}
public override bool IsAvailable(AdUnit adUnit)
{
#if IRONSOURCE
if (adUnit.AdReference.adType == EAdType.Interstitial)
{
return IronSource.Agent.isInterstitialReady();
return _interstitialAd != null && _interstitialAd.IsAdReady();
}
if (adUnit.AdReference.adType == EAdType.Rewarded)
{
return IronSource.Agent.isRewardedVideoAvailable();
return _rewardedAd != null && _rewardedAd.IsAdReady();
}
#endif
return false;

View File

@ -10,6 +10,9 @@
// // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// // THE SOFTWARE.
#if IRONSOURCE
using Unity.Services.LevelPlay;
#endif
using UnityEngine;
using WordsToolkit.Scripts.Services.Ads.AdUnits;
@ -19,65 +22,100 @@ namespace WordsToolkit.Scripts.Services.Ads.Networks
public class IronsourceBannerHandler : AdsHandlerBase
{
private IAdsListener _listener;
#if IRONSOURCE
private LevelPlayBannerAd _bannerAd;
private LevelPlayBannerAd.Config.Builder configBuilder;
#endif
private void Init(string _id)
{
#if IRONSOURCE
IronSource.Agent.validateIntegration();
IronSource.Agent.init(_id);
LevelPlay.ValidateIntegration();
LevelPlay.Init(_id);
#endif
}
private void SetListener(IAdsListener listener)
{
_listener = listener;
#if IRONSOURCE
IronSourceBannerEvents.onAdLoadedEvent += BannerAdLoadedEvent;
IronSourceBannerEvents.onAdLoadFailedEvent += BannerAdLoadFailedEvent;
IronSourceBannerEvents.onAdClickedEvent += BannerAdClickedEvent;
IronSourceBannerEvents.onAdScreenPresentedEvent += BannerAdScreenPresentedEvent;
IronSourceBannerEvents.onAdScreenDismissedEvent += BannerAdScreenDismissedEvent;
IronSourceBannerEvents.onAdLeftApplicationEvent += BannerAdLeftApplicationEvent;
#endif
}
#if IRONSOURCE
private void BannerAdLoadedEvent(IronSourceAdInfo adInfo)
private void BannerAdLoadedEvent(LevelPlayAdInfo adInfo)
{
Debug.Log("IronSource Banner ad loaded");
_listener?.OnAdsLoaded(adInfo.instanceId);
Debug.Log("LevelPlay Banner ad loaded");
_listener?.OnAdsLoaded(adInfo.AdUnitId);
}
private void BannerAdLoadFailedEvent(IronSourceError error)
private void BannerAdLoadFailedEvent(LevelPlayAdError error)
{
Debug.Log($"IronSource Banner ad load failed. Error: {error.getCode()} - {error.getDescription()}");
Debug.Log($"LevelPlay Banner ad load failed. Error: {error}");
_listener?.OnAdsLoadFailed();
}
private void BannerAdClickedEvent(IronSourceAdInfo adInfo)
private void BannerAdClickedEvent(LevelPlayAdInfo adInfo)
{
Debug.Log("IronSource Banner ad clicked");
Debug.Log("LevelPlay Banner ad clicked");
}
private void BannerAdScreenPresentedEvent(IronSourceAdInfo adInfo)
private void BannerAdDisplayedEvent(LevelPlayAdInfo adInfo)
{
Debug.Log("IronSource Banner ad screen presented");
Debug.Log("LevelPlay Banner ad displayed");
}
private void BannerAdScreenDismissedEvent(IronSourceAdInfo adInfo)
private void BannerAdDisplayFailedEvent(LevelPlayAdInfo levelPlayAdInfo, LevelPlayAdError levelPlayAdError)
{
Debug.Log("IronSource Banner ad screen dismissed");
Debug.Log($"LevelPlay Banner ad display failed. Error: {levelPlayAdError}");
}
private void BannerAdLeftApplicationEvent(IronSourceAdInfo adInfo)
#if LEVELPLAY8
private void BannerAdDisplayFailedEvent(LevelPlayAdDisplayInfoError error)
{
Debug.Log("IronSource Banner ad caused app to leave");
Debug.Log($"LevelPlay Banner ad display failed. Error: {error}");
}
#endif
private void BannerAdLeftApplicationEvent(LevelPlayAdInfo adInfo)
{
Debug.Log("LevelPlay Banner ad caused app to leave");
}
private LevelPlayBannerAd GetBannerAd(AdUnit adUnit)
{
configBuilder ??= new LevelPlayBannerAd.Config.Builder();
#if LEVELPLAY9
configBuilder.SetSize(LevelPlayAdSize.BANNER);
#elif LEVELPLAY8
configBuilder.SetSize(com.unity3d.mediation.LevelPlayAdSize.BANNER);
#endif
#if LEVELPLAY9
configBuilder.SetPosition(LevelPlayBannerPosition.BottomCenter);
#elif LEVELPLAY8
configBuilder.SetPosition(com.unity3d.mediation.LevelPlayBannerPosition.BottomCenter);
#endif
configBuilder.SetDisplayOnLoad(true);
#if UNITY_ANDROID
configBuilder.SetRespectSafeArea(true); // Only relevant for Android
#endif
configBuilder.SetPlacementName("bannerPlacement");
configBuilder.SetBidFloor(1.0); // Minimum bid price in USD
var bannerConfig = configBuilder.Build();
var bannerAd = new LevelPlayBannerAd(adUnit.PlacementId, bannerConfig);
bannerAd.OnAdLoaded += BannerAdLoadedEvent;
bannerAd.OnAdLoadFailed += BannerAdLoadFailedEvent;
bannerAd.OnAdClicked += BannerAdClickedEvent;
bannerAd.OnAdDisplayed += BannerAdDisplayedEvent;
bannerAd.OnAdDisplayFailed += BannerAdDisplayFailedEvent;
bannerAd.OnAdLeftApplication += BannerAdLeftApplicationEvent;
return bannerAd;
}
#endif
public override void Init(string _id, bool adSettingTestMode, IAdsListener listener)
{
Debug.Log("IronSource Banner Init");
Debug.Log("LevelPlay Banner Init");
Init(_id);
SetListener(listener);
}
@ -85,22 +123,23 @@ namespace WordsToolkit.Scripts.Services.Ads.Networks
public override void Show(AdUnit adUnit)
{
#if IRONSOURCE
if (adUnit.AdReference.adType == EAdType.Banner)
if (_bannerAd == null)
{
IronSource.Agent.displayBanner();
_bannerAd = GetBannerAd(adUnit);
}
if (adUnit.AdReference.adType == EAdType.Banner && _bannerAd != null)
{
_bannerAd.ShowAd();
_listener?.Show(adUnit);
}
#endif
}
public override void Load(AdUnit adUnit)
{
#if IRONSOURCE
if (adUnit.AdReference.adType == EAdType.Banner)
{
IronSourceBannerSize bannerSize = IronSourceBannerSize.BANNER;
IronSource.Agent.loadBanner(bannerSize, IronSourceBannerPosition.BOTTOM);
}
_bannerAd.LoadAd();
#endif
}
@ -114,9 +153,9 @@ namespace WordsToolkit.Scripts.Services.Ads.Networks
public override void Hide(AdUnit adUnit)
{
#if IRONSOURCE
if (adUnit.AdReference.adType == EAdType.Banner)
if (adUnit.AdReference.adType == EAdType.Banner && _bannerAd != null)
{
IronSource.Agent.hideBanner();
_bannerAd.HideAd();
}
#endif
}

View File

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

View File

@ -0,0 +1,33 @@
{
"name": "CandySmith.LevelPlay",
"rootNamespace": "",
"references": [
"GUID:00dd4a7ac8c24c898083910c81898ecc",
"GUID:760a4c7888534400e882b82c5b3fba06"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [
{
"name": "com.unity.services.levelplay",
"expression": "[0.0,9.0)",
"define": "LEVELPLAY8"
},
{
"name": "com.unity.services.levelplay",
"expression": "9.0.0",
"define": "LEVELPLAY9"
},
{
"name": "com.unity.services.levelplay",
"expression": "",
"define": "IRONSOURCE"
}
],
"noEngineReferences": false
}

View File

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

View File

@ -12,7 +12,9 @@
using UnityEngine;
using WordsToolkit.Scripts.Services.Ads.AdUnits;
#if UMP_AVAILABLE
using GoogleMobileAds.Ump.Api;
#endif
namespace WordsToolkit.Scripts.Services.Ads.Networks
{
#if UNITY_ADS
@ -28,10 +30,33 @@ namespace WordsToolkit.Scripts.Services.Ads.Networks
public override void Init(string _id, bool adSettingTestMode, IAdsListener listener)
{
SetConsentStatus();
Advertisement.Initialize(_id, adSettingTestMode, this);
this.listener = listener;
}
private void SetConsentStatus()
{
#if UNITY_ADS && UMP_AVAILABLE
bool hasConsent = ConsentInformation.CanRequestAds();
Debug.Log($"Unity Ads consent status: {hasConsent}");
var consentMetaData = new MetaData("gdpr");
consentMetaData.Set("consent", hasConsent);
Advertisement.SetMetaData(consentMetaData);
var privacyMetaData = new MetaData("privacy");
privacyMetaData.Set("mode", "mixed");
Advertisement.SetMetaData(privacyMetaData);
#elif UNITY_ADS
// Default to no consent if UMP not available
var consentMetaData = new MetaData("gdpr");
consentMetaData.Set("consent", false);
Advertisement.SetMetaData(consentMetaData);
Debug.Log("UMP not available - setting Unity Ads consent to false");
#endif
}
public override void Show(AdUnit adUnit)
{
Advertisement.Show(adUnit.PlacementId, this);

View File

@ -12,14 +12,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using VContainer;
using WordConnectGameToolkit.Scripts.Settings;
using WordsToolkit.Scripts.Popups;
using WordsToolkit.Scripts.Services.Ads;
using WordsToolkit.Scripts.Services.Ads.AdUnits;
using WordsToolkit.Scripts.Services.IAP;
using WordsToolkit.Scripts.Settings;
using WordsToolkit.Scripts.System;
#if UMP_AVAILABLE
using GoogleMobileAds.Ump.Api;
#endif
namespace WordsToolkit.Scripts.Services
{
@ -29,6 +34,8 @@ namespace WordsToolkit.Scripts.Services
private readonly List<AdUnit> adUnits = new();
private readonly Dictionary<AdUnit, IAdLifecycleManager> lifecycleManagers = new();
private EPlatforms platforms;
private bool consentInfoUpdateInProgress = false;
private bool adsInitialized = false;
[Inject]
private GameSettings gameSettings;
@ -38,6 +45,8 @@ namespace WordsToolkit.Scripts.Services
[SerializeField]
private ProductID noAdsProduct;
private InterstitialSettings interstitialSettings;
private AdSetting[] adElements;
private void Awake()
{
@ -45,9 +54,86 @@ namespace WordsToolkit.Scripts.Services
{
return;
}
interstitialSettings = Resources.Load<InterstitialSettings>("Settings/AdsInterstitialSettings");
adElements = Resources.Load<AdsSettings>("Settings/AdsSettings").adProfiles;
StartConsentFlow();
}
private void StartConsentFlow()
{
if (consentInfoUpdateInProgress) return;
consentInfoUpdateInProgress = true;
#if UMP_AVAILABLE && (UNITY_ANDROID || UNITY_IOS)
var request = new ConsentRequestParameters();
if (Debug.isDebugBuild || Application.isEditor)
{
var debugSettings = new ConsentDebugSettings
{
DebugGeography = DebugGeography.EEA
};
var testDeviceIds = new List<string>();
// testDeviceIds.Add("YOUR-TEST-DEVICE-ID-HERE"); // Uncomment and add your device ID if needed
if (testDeviceIds.Count > 0)
{
debugSettings.TestDeviceHashedIds = testDeviceIds;
}
request.ConsentDebugSettings = debugSettings;
}
ConsentInformation.Update(request, OnConsentInfoUpdated);
#else
InitializeAds();
#endif
}
#if UMP_AVAILABLE
private void OnConsentInfoUpdated(FormError consentError)
{
consentInfoUpdateInProgress = false;
if (consentError != null)
{
Debug.LogError($"Consent info update failed: {consentError}");
InitializeAds();
return;
}
ConsentForm.LoadAndShowConsentFormIfRequired(OnConsentFormDismissed);
}
private void OnConsentFormDismissed(FormError formError)
{
if (formError != null)
{
Debug.LogError($"Consent form error: {formError}");
}
if (ConsentInformation.CanRequestAds())
{
InitializeAds();
RefreshBannerAds();
}
else
{
Debug.Log("User denied consent or consent not available");
InitializeAds();
}
}
#endif
private void InitializeAds()
{
if (adsInitialized) return;
adsInitialized = true;
platforms = GetPlatform();
var adElements = Resources.Load<AdsSettings>("Settings/AdsSettings").adProfiles;
foreach (var t in adElements)
{
if (t.platforms == platforms && t.enable)
@ -60,7 +146,7 @@ namespace WordsToolkit.Scripts.Services
adList.Add(t);
foreach (var adElement in t.adElements)
{
var adUnit = new AdUnit(adElement.placementId, adElement.adReference);
var adUnit = new AdUnit(adElement.placementId, adElement.adReference, t.adsHandler);
var lifecycleManager = new AdLifecycleManager(t.adsHandler);
lifecycleManagers[adUnit] = lifecycleManager;
adUnits.Add(adUnit);
@ -119,26 +205,72 @@ namespace WordsToolkit.Scripts.Services
private void OnPopupTrigger(Popup popup, bool open)
{
if (IsNoAdsPurchased())
if (IsNoAdsPurchased() || !CanShowAds())
{
return;
}
// Get current level number
int currentLevel = GameDataManager.GetLevelNum();
// Check interstitial ads using InterstitialSettings
if (interstitialSettings != null && interstitialSettings.interstitials != null)
{
foreach (var interstitialElement in interstitialSettings.interstitials)
{
// Check if this interstitial should trigger based on popup
if (((open && interstitialElement.showOnOpen) || (!open && interstitialElement.showOnClose))
&& popup.GetType() == interstitialElement.popup.GetType())
{
var adUnit = adUnits.Find(i => i.AdReference == interstitialElement.adReference);
if (adUnit == null || !adUnit.IsAvailable())
{
adUnit?.Load();
continue;
}
// Check level conditions
if (!IsLevelConditionMet(currentLevel, interstitialElement))
{
continue;
}
// Find placement ID for frequency tracking
string placementId = GetPlacementIdForAdReference(interstitialElement.adReference);
if (placementId == null) continue;
if (!IsFrequencyConditionMet(placementId, interstitialElement.frequency))
{
continue;
}
adUnit.Show();
adUnit.Load();
IncrementAdFrequency(placementId);
return;
}
}
}
// Handle non-interstitial ads (banners, rewarded) using the original logic
foreach (var ad in adList)
{
foreach (var adElement in ad.adElements)
{
if (adElement.adReference.adType == EAdType.Interstitial)
continue; // Skip interstitials as they're handled above
var adUnit = adUnits.Find(i => i.AdReference == adElement.adReference);
if (!lifecycleManagers[adUnit].IsAvailable(adUnit))
if (!adUnit.IsAvailable())
{
lifecycleManagers[adUnit].Load(adUnit);
adUnit.Load();
continue;
}
if (((open && adElement.popup.showOnOpen) || (!open && adElement.popup.showOnClose)) && popup.GetType() == adElement.popup.popup.GetType())
{
lifecycleManagers[adUnit].Show(adUnit);
lifecycleManagers[adUnit].Load(adUnit);
adUnit.Show();
adUnit.Load();
return;
}
}
@ -152,19 +284,48 @@ namespace WordsToolkit.Scripts.Services
public void ShowAdByType(AdReference adRef, Action<string> shown)
{
if (!gameSettings.enableAds)
if (!gameSettings.enableAds || !CanShowAds())
{
shown?.Invoke(null);
return;
}
int currentLevel = GameDataManager.GetLevelNum();
foreach (var adUnit in adUnits)
{
if (adUnit.AdReference == adRef && lifecycleManagers[adUnit].IsAvailable(adUnit))
if (adUnit.AdReference == adRef && adUnit.IsAvailable())
{
// Check level conditions for interstitial ads using InterstitialSettings
if (adRef.adType == EAdType.Interstitial && interstitialSettings != null)
{
var interstitialElement = interstitialSettings.interstitials?.FirstOrDefault(i => i.adReference == adRef);
if (interstitialElement != null)
{
if (!IsLevelConditionMet(currentLevel, interstitialElement))
{
shown?.Invoke(null);
return;
}
string placementId = GetPlacementIdForAdReference(adRef);
if (placementId != null)
{
if (!IsFrequencyConditionMet(placementId, interstitialElement.frequency))
{
shown?.Invoke(null);
return;
}
// Increment frequency counter
IncrementAdFrequency(placementId);
}
}
}
adUnit.OnShown = shown;
lifecycleManagers[adUnit].Show(adUnit);
lifecycleManagers[adUnit].Load(adUnit);
adUnit.Show();
adUnit.Load();
return;
}
}
@ -193,5 +354,70 @@ namespace WordsToolkit.Scripts.Services
}
}
}
private bool IsLevelConditionMet(int currentLevel, InterstitialAdElement popupSetting)
{
return currentLevel >= popupSetting.minLevel && currentLevel <= popupSetting.maxLevel;
}
private bool IsFrequencyConditionMet(string placementId, int frequency)
{
if (frequency <= 1) return true; // Always show if frequency is 1 or less
int adShowCount = PlayerPrefs.GetInt($"AdCount_{placementId}", 0);
return adShowCount % frequency == 0;
}
private void IncrementAdFrequency(string placementId)
{
int currentCount = PlayerPrefs.GetInt($"AdCount_{placementId}", 0);
PlayerPrefs.SetInt($"AdCount_{placementId}", currentCount + 1);
PlayerPrefs.Save();
}
private string GetPlacementIdForAdReference(AdReference adRef)
{
foreach (var ad in adList)
{
foreach (var adElement in ad.adElements)
{
if (adElement.adReference == adRef)
{
return adElement.placementId;
}
}
}
return null;
}
private bool CanShowAds()
{
#if UMP_AVAILABLE
return ConsentInformation.CanRequestAds();
#else
return true;
#endif
}
public void RefreshBannerAds()
{
if (!CanShowAds() || IsNoAdsPurchased()) return;
foreach (var adUnit in adUnits)
{
if (adUnit.AdReference.adType == EAdType.Banner)
{
lifecycleManagers[adUnit].Show(adUnit);
}
}
}
public void ReconsiderUMPConsent()
{
#if UMP_AVAILABLE && (UNITY_ANDROID || UNITY_IOS)
ConsentInformation.Reset();
#endif
StartConsentFlow();
}
}
}

View File

@ -25,7 +25,6 @@ namespace WordsToolkit.Scripts.Services.IAP
private IExtensionProvider extensionProvider;
public static event Action<string> OnSuccessfulPurchase;
public static event Action<(string,string)> OnFailedPurchase;
public static event Action<bool, List<string>> OnRestorePurchasesFinished;
public void InitializePurchasing(IEnumerable<(string productId, ProductTypeWrapper.ProductType productType)> products)
@ -112,24 +111,20 @@ namespace WordsToolkit.Scripts.Services.IAP
{
Debug.Log(string.Format("Purchasing product asychronously: '{0}'", product.definition.id));
storeController.InitiatePurchase(product);
OnFailedPurchase?.Invoke((productId, "Product not found or not available for purchase.")); // debug only
}
else
{
Debug.Log($"BuyProductID: FAIL. Not purchasing product, either is not found or is not available for purchase {productId}");
OnFailedPurchase?.Invoke((productId, "InvalidProductID: product not found or not available for purchase."));
}
}
else
{
Debug.Log("IAPInitFailed: BuyProductID FAIL. Not initialized.");
OnFailedPurchase?.Invoke((productId, "IAPInitFailed: Not initialized."));
Debug.Log("BuyProductID FAIL. Not initialized.");
}
}
catch (Exception e)
{
Debug.Log("BuyProductID: FAIL. Exception during purchase. " + e);
OnFailedPurchase?.Invoke((productId, "Exception during purchase: " + e.Message));
}
}
@ -164,13 +159,11 @@ namespace WordsToolkit.Scripts.Services.IAP
public void OnPurchaseFailed(Product product, PurchaseFailureDescription failureDescription)
{
Debug.Log("OnPurchaseFailed: FAIL. Product: " + product.definition.id + " PurchaseFailureDescription: " + failureDescription);
OnFailedPurchase?.Invoke((product.definition.id, failureDescription.message));
}
public void OnPurchaseFailed(Product i, PurchaseFailureReason p)
{
Debug.Log($"OnPurchaseFailed: FAIL. Product: '{i.definition.id}', PurchaseFailureReason: {p}");
OnFailedPurchase?.Invoke((i.definition.id, p.ToString()));
}
public void OnInitializeFailed(InitializationFailureReason reason)

View File

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

View File

@ -13,8 +13,6 @@ namespace WordsToolkit.Scripts.Services.IAP
bool IsProductPurchased(string productId);
void RestorePurchases(Action<bool, List<string>> action);
void SubscribeToPurchaseEvent(Action<string> purchaseHandler);
void SubscribeToPurchaseFailedEvent(Action<(string, string)> purchaseHandler);
void UnsubscribeFromPurchaseEvent(Action<string> purchaseHandler);
}
}

View File

@ -21,5 +21,6 @@ namespace WordsToolkit.Scripts.Services
bool IsRewardedAvailable(AdReference adRef);
void RemoveAds();
bool IsNoAdsPurchased();
void ReconsiderUMPConsent();
}
}

View File

@ -12,6 +12,7 @@
using UnityEngine;
using UnityEngine.InputSystem;
using WordsToolkit.Scripts.Levels;
namespace WordsToolkit.Scripts.Settings
{
@ -36,7 +37,30 @@ namespace WordsToolkit.Scripts.Settings
public Key SimulateDuplicate = Key.D;
[Header("")]
[Tooltip("Test language, only for editor")]
public SystemLanguage TestLanguage = SystemLanguage.English;
[Tooltip("Test language code, only for editor (e.g., 'en', 'fr', 'es')")]
[HideInInspector] // Hidden because we use custom editor dropdown
public string TestLanguageCode = "en";
/// <summary>
/// Validates if the TestLanguageCode exists in the provided LanguageConfiguration
/// </summary>
public bool IsValidTestLanguage(LanguageConfiguration config)
{
if (config == null || string.IsNullOrEmpty(TestLanguageCode))
return false;
return config.GetLanguageInfo(TestLanguageCode) != null;
}
/// <summary>
/// Gets a valid test language code, falling back to default if current one is invalid
/// </summary>
public string GetValidTestLanguageCode(LanguageConfiguration config)
{
if (IsValidTestLanguage(config))
return TestLanguageCode;
return config?.defaultLanguage ?? "en";
}
}
}

View File

@ -15,8 +15,9 @@ using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine.UIElements;
using WordsToolkit.Scripts.Services.Ads.AdUnits;
using WordsToolkit.Scripts.Settings;
namespace WordsToolkit.Scripts.Settings.Editor
namespace WordConnectGameToolkit.Scripts.Settings.Editor
{
[CustomPropertyDrawer(typeof(AdElement))]
public class AdElementDrawer : PropertyDrawer
@ -45,7 +46,24 @@ namespace WordsToolkit.Scripts.Settings.Editor
var adTypeScriptableObject = (AdReference)adTypeScriptableProperty.objectReferenceValue;
if (adTypeScriptableObject != null && adTypeScriptableObject.adType == EAdType.Interstitial)
{
popupField.visible = true;
// Add button to open InterstitialSettings for interstitial ads
var interstitialButton = new Button(() => {
OpenInterstitialSettings();
})
{
text = "Open Interstitial Settings"
};
interstitialButton.style.marginTop = 5;
// Show/hide button and popup field based on ad type
adTypeScriptableField.RegisterValueChangeCallback(evt =>
{
UpdateFieldVisibility(adTypeScriptableProperty, popupField, interstitialButton, root);
});
// Initial visibility setup
UpdateFieldVisibility(adTypeScriptableProperty, popupField, interstitialButton, root);
}
else
{
@ -69,6 +87,46 @@ namespace WordsToolkit.Scripts.Settings.Editor
// Return the root VisualElement
return root;
}
private void UpdateFieldVisibility(SerializedProperty adTypeScriptableProperty, VisualElement popupField, Button interstitialButton, VisualElement root)
{
var adTypeScriptableObject = (AdReference)adTypeScriptableProperty.objectReferenceValue;
if (adTypeScriptableObject != null && adTypeScriptableObject.adType == EAdType.Interstitial)
{
popupField.style.display = DisplayStyle.None; // Hide popup field for interstitials
if (!root.Contains(interstitialButton))
{
root.Add(interstitialButton);
}
}
else
{
popupField.style.display = DisplayStyle.Flex; // Show popup field for other ad types
if (root.Contains(interstitialButton))
{
root.Remove(interstitialButton);
}
}
}
private void OpenInterstitialSettings()
{
// Find InterstitialSettings asset
string[] guids = AssetDatabase.FindAssets("t:InterstitialSettings");
if (guids.Length > 0)
{
string path = AssetDatabase.GUIDToAssetPath(guids[0]);
var interstitialSettings = AssetDatabase.LoadAssetAtPath<InterstitialSettings>(path);
Selection.activeObject = interstitialSettings;
EditorGUIUtility.PingObject(interstitialSettings);
}
else
{
EditorUtility.DisplayDialog("InterstitialSettings Not Found",
"Could not find InterstitialSettings ScriptableObject in the project.", "OK");
}
}
}
}
#endif

View File

@ -3,8 +3,9 @@ using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using WordsToolkit.Scripts.Settings;
namespace WordsToolkit.Scripts.Settings.Editor
namespace WordConnectGameToolkit.Scripts.Settings.Editor
{
[CustomPropertyDrawer(typeof(AdSetting))]
public class AdSettingDrawer : PropertyDrawer
@ -18,7 +19,6 @@ namespace WordsToolkit.Scripts.Settings.Editor
var foldout = new Foldout { text = property.displayName, value = false };
root.Add(foldout);
// Add fields to the foldout
var nameField = new PropertyField(property.FindPropertyRelative("name"), "Name");
var enableField = new PropertyField(property.FindPropertyRelative("enable"), "Enable");
var testInEditorField = new PropertyField(property.FindPropertyRelative("testInEditor"), "Test In Editor");

View File

@ -3,11 +3,7 @@
"rootNamespace": "",
"references": [
"GUID:d3bf71b33c0c04eb9bc1a8d6513d76bb",
"GUID:00dd4a7ac8c24c898083910c81898ecc",
"GUID:ac6e78962cfc743b9a5fc5f5a808aa72",
"GUID:b25ad8286798741e3b2cc3883283e669",
"GUID:75bdbcf23199f4cfb86c610d1d946666",
"GUID:b0214a6008ed146ff8f122a6a9c2f6cc"
"GUID:00dd4a7ac8c24c898083910c81898ecc"
],
"includePlatforms": [
"Editor"

View File

@ -0,0 +1,92 @@
using UnityEngine;
using UnityEditor;
using WordsToolkit.Scripts.Settings;
using WordsToolkit.Scripts.Levels;
using System.Linq;
namespace WordsToolkit.Scripts.Settings.Editor
{
[CustomEditor(typeof(DebugSettings))]
public class DebugSettingsEditor : UnityEditor.Editor
{
private LanguageConfiguration languageConfig;
private string[] availableLanguageCodes;
private string[] availableLanguageNames;
private int selectedLanguageIndex = 0;
void OnEnable()
{
LoadLanguageConfiguration();
}
void LoadLanguageConfiguration()
{
// Find LanguageConfiguration asset in the project
var guids = AssetDatabase.FindAssets("t:LanguageConfiguration");
if (guids.Length > 0)
{
var path = AssetDatabase.GUIDToAssetPath(guids[0]);
languageConfig = AssetDatabase.LoadAssetAtPath<LanguageConfiguration>(path);
if (languageConfig != null && languageConfig.languages != null)
{
availableLanguageCodes = languageConfig.languages.Select(l => l.code).ToArray();
availableLanguageNames = languageConfig.languages.Select(l => $"{l.displayName} ({l.code})").ToArray();
// Find current selection
var debugSettings = (DebugSettings)target;
selectedLanguageIndex = global::System.Array.IndexOf(availableLanguageCodes, debugSettings.TestLanguageCode);
if (selectedLanguageIndex < 0) selectedLanguageIndex = 0;
}
}
}
public override void OnInspectorGUI()
{
var debugSettings = (DebugSettings)target;
// Refresh language configuration if it's null or changed
if (languageConfig == null || availableLanguageCodes == null)
{
LoadLanguageConfiguration();
}
serializedObject.Update();
// Draw default fields
EditorGUILayout.PropertyField(serializedObject.FindProperty("enableHotkeys"));
EditorGUILayout.Space();
EditorGUILayout.LabelField("Debug Hotkeys", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(serializedObject.FindProperty("Win"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("Lose"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("Back"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("Restart"));
EditorGUILayout.PropertyField(serializedObject.FindProperty("SimulateDuplicate"));
EditorGUILayout.Space();
EditorGUILayout.LabelField("Language Settings", EditorStyles.boldLabel);
// Custom dropdown for test language
if (languageConfig != null && availableLanguageCodes != null && availableLanguageCodes.Length > 0)
{
EditorGUI.BeginChangeCheck();
selectedLanguageIndex = EditorGUILayout.Popup("Test Language", selectedLanguageIndex, availableLanguageNames);
if (EditorGUI.EndChangeCheck())
{
debugSettings.TestLanguageCode = availableLanguageCodes[selectedLanguageIndex];
EditorUtility.SetDirty(debugSettings);
}
}
else
{
// Fallback to text field if no LanguageConfiguration found
EditorGUILayout.PropertyField(serializedObject.FindProperty("TestLanguageCode"), new GUIContent("Test Language Code"));
EditorGUILayout.HelpBox("No LanguageConfiguration found. Create one to enable language dropdown.", MessageType.Warning);
}
serializedObject.ApplyModifiedProperties();
}
}
}

View File

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

View File

@ -0,0 +1,128 @@
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using WordsToolkit.Scripts.Popups;
namespace WordConnectGameToolkit.Scripts.Settings.Editor
{
[CustomPropertyDrawer(typeof(InterstitialAdElement))]
public class InterstitialAdElementDrawer : PropertyDrawer
{
private Popup[] popupPrefabs;
private List<string> popupNames;
public override VisualElement CreatePropertyGUI(SerializedProperty property)
{
LoadPopupPrefabs();
var container = new VisualElement();
var adReferenceProperty = property.FindPropertyRelative("adReference");
var elementNameProperty = property.FindPropertyRelative("elementName");
var popupProperty = property.FindPropertyRelative("popup");
var showOnOpenProperty = property.FindPropertyRelative("showOnOpen");
var showOnCloseProperty = property.FindPropertyRelative("showOnClose");
var minLevelProperty = property.FindPropertyRelative("minLevel");
var maxLevelProperty = property.FindPropertyRelative("maxLevel");
var frequencyProperty = property.FindPropertyRelative("frequency");
// Update element name based on ad reference
UpdateElementName(adReferenceProperty, elementNameProperty);
// Ad Reference field
var adReferenceField = new PropertyField(adReferenceProperty);
adReferenceField.RegisterValueChangeCallback(evt =>
{
UpdateElementName(adReferenceProperty, elementNameProperty);
property.serializedObject.ApplyModifiedProperties();
});
container.Add(adReferenceField);
// Popup dropdown
var popupDropdown = new DropdownField("Popup", popupNames, GetPopupIndex(popupProperty.objectReferenceValue as Popup));
popupDropdown.RegisterValueChangedCallback(evt =>
{
int selectedIndex = popupNames.IndexOf(evt.newValue);
if (selectedIndex == 0)
{
popupProperty.objectReferenceValue = null;
}
else if (selectedIndex > 0)
{
popupProperty.objectReferenceValue = popupPrefabs[selectedIndex - 1];
}
popupProperty.serializedObject.ApplyModifiedProperties();
});
container.Add(popupDropdown);
// Show options
var showOnOpenField = new PropertyField(showOnOpenProperty);
container.Add(showOnOpenField);
var showOnCloseField = new PropertyField(showOnCloseProperty);
container.Add(showOnCloseField);
// Level conditions header
var levelHeader = new Label("Level Conditions");
levelHeader.style.unityFontStyleAndWeight = FontStyle.Bold;
levelHeader.style.marginTop = 5;
container.Add(levelHeader);
// Level conditions fields
var minLevelField = new PropertyField(minLevelProperty);
container.Add(minLevelField);
var maxLevelField = new PropertyField(maxLevelProperty);
container.Add(maxLevelField);
var frequencyField = new PropertyField(frequencyProperty);
container.Add(frequencyField);
return container;
}
private void LoadPopupPrefabs()
{
string[] guids = AssetDatabase.FindAssets("t:Prefab");
var popups = guids
.Select(guid => AssetDatabase.GUIDToAssetPath(guid))
.Select(path => AssetDatabase.LoadAssetAtPath<GameObject>(path))
.Where(go => go != null && go.GetComponent<Popup>() != null)
.Select(go => go.GetComponent<Popup>())
.OrderBy(popup => popup.name)
.ToArray();
popupPrefabs = popups;
popupNames = new List<string> { "None (Popup)" };
popupNames.AddRange(popups.Select(popup => popup.name));
}
private int GetPopupIndex(Popup popup)
{
if (popup == null) return 0;
for (int i = 0; i < popupPrefabs.Length; i++)
{
if (popupPrefabs[i] == popup)
return i + 1;
}
return 0;
}
private void UpdateElementName(SerializedProperty adReferenceProperty, SerializedProperty elementNameProperty)
{
if (adReferenceProperty.objectReferenceValue != null)
{
string adRefName = adReferenceProperty.objectReferenceValue.name;
elementNameProperty.stringValue = adRefName;
}
else
{
elementNameProperty.stringValue = "Unnamed";
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 868b5f9431a04f3a81e7484ada3d3f8f
timeCreated: 1756307465

View File

@ -0,0 +1,65 @@
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine.UIElements;
using WordsToolkit.Scripts.Settings;
namespace WordConnectGameToolkit.Scripts.Settings.Editor
{
[CustomEditor(typeof(InterstitialSettings))]
public class InterstitialSettingsEditor : UnityEditor.Editor
{
public override VisualElement CreateInspectorGUI()
{
var root = new VisualElement();
var interstitialSettings = (InterstitialSettings)target;
// if (interstitialSettings.interstitials == null || interstitialSettings.interstitials.Length == 0)
{
var helpBox = new HelpBox("InterstitialSettings is empty. Click the button below to populate from AdsSettings.", HelpBoxMessageType.Info);
root.Add(helpBox);
var populateButton = new Button(() =>
{
var adsSettings = FindAdsSettings();
if (adsSettings != null)
{
interstitialSettings.PopulateFromAdsSettings(adsSettings);
EditorUtility.SetDirty(interstitialSettings);
AssetDatabase.SaveAssets();
}
else
{
EditorUtility.DisplayDialog("AdsSettings Not Found",
"Could not find AdsSettings ScriptableObject in the project.", "OK");
}
})
{
text = "Populate from AdsSettings"
};
root.Add(populateButton);
}
CreateDefaultInspector(root);
return root;
}
private void CreateDefaultInspector(VisualElement root)
{
var interstitialsProperty = serializedObject.FindProperty("interstitials");
var interstitialsField = new PropertyField(interstitialsProperty);
interstitialsField.Bind(serializedObject);
root.Add(interstitialsField);
}
private AdsSettings FindAdsSettings()
{
string[] guids = AssetDatabase.FindAssets("t:AdsSettings");
if (guids.Length > 0)
{
string path = AssetDatabase.GUIDToAssetPath(guids[0]);
return AssetDatabase.LoadAssetAtPath<AdsSettings>(path);
}
return null;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 285dda4a69e24274b3b50bd12857b12b
timeCreated: 1756307465

View File

@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using WordsToolkit.Scripts.Popups;
using WordsToolkit.Scripts.Services.Ads.AdUnits;
using WordsToolkit.Scripts.Settings;
namespace WordConnectGameToolkit.Scripts.Settings
{
[CreateAssetMenu(fileName = "InterstitialSettings", menuName = "WordConnectGameToolkit/Settings/InterstitialSettings", order = 1)]
public class InterstitialSettings : ScriptableObject
{
public InterstitialAdElement[] interstitials;
public void PopulateFromAdsSettings(AdsSettings adsSettings)
{
if (adsSettings == null) return;
var interstitialElements = new List<InterstitialAdElement>();
foreach (var adProfile in adsSettings.adProfiles)
{
if (!adProfile.enable) continue;
foreach (var adElement in adProfile.adElements)
{
if (adElement.adReference != null && adElement.adReference.adType == EAdType.Interstitial)
{
var interstitialElement = new InterstitialAdElement
{
elementName = adElement.adReference.name,
adReference = adElement.adReference,
popup = adElement.popup.popup,
showOnOpen = adElement.popup.showOnOpen,
showOnClose = adElement.popup.showOnClose,
};
interstitialElements.Add(interstitialElement);
}
}
}
interstitials = interstitialElements.ToArray();
}
}
[Serializable]
public class InterstitialAdElement
{
[HideInInspector]
public string elementName;
public AdReference adReference;
[Header("Popup that triggers Interstitial ads")]
public Popup popup;
public bool showOnOpen;
public bool showOnClose;
[Header("Level Conditions")]
public int minLevel = 1;
public int maxLevel = 1000;
public int frequency = 1;
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 44ad2bed4c494715ba8ad21e150b03ee
timeCreated: 1756307490

View File

@ -103,7 +103,6 @@ namespace WordsToolkit.Scripts.System
language = langName;
EventManager.GetEvent<string>(EGameEvent.LanguageChanged).Subscribe(LanguageChanged);
iapManager.SubscribeToPurchaseEvent(PurchaseSucceeded);
iapManager.SubscribeToPurchaseFailedEvent(PurchaseFailed);
stateManager.OnStateChanged.AddListener((state) => {
if (state != EScreenStates.MainMenu)
@ -230,11 +229,6 @@ namespace WordsToolkit.Scripts.System
{
EventManager.GetEvent<string>(EGameEvent.PurchaseSucceeded).Invoke(id);
}
public void PurchaseFailed((string, string) info)
{
EventManager.GetEvent<(string, string)>(EGameEvent.PurchaseFailed).Invoke(info);
}
public void SetGameMode(EGameMode gameMode)
{

View File

@ -65,7 +65,7 @@ namespace WordsToolkit.Scripts.System.Haptic
#endif
return true;
}
catch (Exception e)
catch (Exception)
{
return false;
}