Initial commit: Unity WordConnect project

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

View File

@ -0,0 +1,131 @@
// // ©2015 - 2025 Candy Smith
// // All rights reserved
// // Redistribution of this software is strictly not allowed.
// // Copy of this software can be obtained from unity asset store only.
// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// // FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
// // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// // THE SOFTWARE.
using UnityEngine;
using UnityEngine.UI;
using DG.Tweening;
namespace WordsToolkit.Scripts.Gameplay
{
[RequireComponent(typeof(RectTransform))]
public class ConnectionCircle : MonoBehaviour
{
[Header("Animation Settings")]
public float pulseDuration = 0.5f;
public float pulseScale = 1.2f;
public float appearDuration = 0.2f;
private RectTransform rectTransform;
private Image image;
private Sequence pulseSequence;
private void Awake()
{
rectTransform = GetComponent<RectTransform>();
image = GetComponent<Image>();
if (image == null)
image = gameObject.AddComponent<Image>();
// Start with circle invisible
if (image != null)
{
Color color = image.color;
color.a = 0;
image.color = color;
}
}
public void Appear()
{
// Stop any running animations
if (pulseSequence != null)
pulseSequence.Kill();
// Reset scale
rectTransform.localScale = Vector3.zero;
// Make sure it's visible
gameObject.SetActive(true);
// Animate appear with pop effect
rectTransform.DOScale(Vector3.one, appearDuration)
.SetEase(Ease.OutBack);
// Fade in
if (image != null)
{
Color color = image.color;
Color targetColor = new Color(color.r, color.g, color.b, 1f);
image.DOColor(targetColor, appearDuration);
}
// Start pulsing
StartPulse();
}
public void Disappear()
{
// Stop any running animations
if (pulseSequence != null)
pulseSequence.Kill();
// Animate disappear
rectTransform.DOScale(Vector3.zero, appearDuration)
.SetEase(Ease.InBack)
.OnComplete(() => gameObject.SetActive(false));
// Fade out
if (image != null)
{
Color color = image.color;
Color targetColor = new Color(color.r, color.g, color.b, 0f);
image.DOColor(targetColor, appearDuration);
}
}
private void StartPulse()
{
// Create a gentle pulse animation
pulseSequence = DOTween.Sequence();
pulseSequence.Append(rectTransform.DOScale(Vector3.one * pulseScale, pulseDuration / 2)
.SetEase(Ease.InOutSine));
pulseSequence.Append(rectTransform.DOScale(Vector3.one, pulseDuration / 2)
.SetEase(Ease.InOutSine));
pulseSequence.SetLoops(-1); // Loop indefinitely
}
private void OnDisable()
{
// Kill animations when disabled
if (pulseSequence != null)
pulseSequence.Kill();
}
// Set circle color
public void SetColor(Color newColor)
{
if (image != null)
image.color = newColor;
}
// Set circle size
public void SetSize(float size)
{
if (rectTransform != null)
rectTransform.sizeDelta = new Vector2(size, size);
}
}
}

View File

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

View File

@ -0,0 +1,21 @@
// // ©2015 - 2025 Candy Smith
// // All rights reserved
// // Redistribution of this software is strictly not allowed.
// // Copy of this software can be obtained from unity asset store only.
// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// // FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
// // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// // THE SOFTWARE.
using UnityEngine;
namespace WordsToolkit.Scripts.Gameplay
{
public abstract class FillAndPreview : MonoBehaviour
{
public abstract void FillIcon(ScriptableData iconScriptable);
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5bc0cae2446540b8847b5cf5c15742c1
timeCreated: 1746685265

View File

@ -0,0 +1,90 @@
// // ©2015 - 2025 Candy Smith
// // All rights reserved
// // Redistribution of this software is strictly not allowed.
// // Copy of this software can be obtained from unity asset store only.
// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// // FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
// // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// // THE SOFTWARE.
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using WordsToolkit.Scripts.Enums;
using WordsToolkit.Scripts.System;
using WordsToolkit.Scripts.System.Haptic;
namespace WordsToolkit.Scripts.Gameplay
{
public class LetterButton : MonoBehaviour, IPointerDownHandler, IPointerEnterHandler, IPointerUpHandler
{
public TextMeshProUGUI letterText;
public Image circleImage;
private WordSelectionManager wordSelectionManager;
private bool isSelected = false;
private Color color;
private void Awake()
{
// Get a reference to the WordSelectionManager
wordSelectionManager = GetComponentInParent<WordSelectionManager>();
}
public void OnPointerDown(PointerEventData eventData)
{
// Start selection process when the user taps/clicks on a letter
if (wordSelectionManager != null && EventManager.GameStatus == EGameState.Playing)
{
wordSelectionManager.StartSelection(this);
SetSelected(true);
}
}
public void OnPointerEnter(PointerEventData eventData)
{
// When dragging over this letter, add it to the selection
if (wordSelectionManager != null && wordSelectionManager.IsSelecting && EventManager.GameStatus == EGameState.Playing)
{
wordSelectionManager.AddToSelection(this);
SetSelected(true);
}
}
public void OnPointerUp(PointerEventData eventData)
{
// End selection process when the user lifts finger/mouse
if (wordSelectionManager != null && EventManager.GameStatus == EGameState.Playing)
{
wordSelectionManager.EndSelection();
}
}
public void SetSelected(bool selected)
{
HapticFeedback.TriggerHapticFeedback(HapticFeedback.HapticForce.Light);
isSelected = selected;
circleImage.color = new Color(color.r, color.g, color.b, selected ? 1f : 0);
letterText.color = selected ? Color.white : Color.black;
}
public string GetLetter()
{
return letterText.text;
}
public void SetText(string toString)
{
letterText.text = toString.ToUpper();
}
public void SetColor(Color color)
{
this.color = color;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 01a0c10078fb4995bb8ca1b753a8a62e
timeCreated: 1741690665

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 201b442989dd4b90bdfe248deda29441
timeCreated: 1730996862

View File

@ -0,0 +1,36 @@
// // ©2015 - 2025 Candy Smith
// // All rights reserved
// // Redistribution of this software is strictly not allowed.
// // Copy of this software can be obtained from unity asset store only.
// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// // FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
// // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// // THE SOFTWARE.
using WordsToolkit.Scripts.Levels;
using WordsToolkit.Scripts.System;
namespace WordsToolkit.Scripts.Gameplay.Managers
{
public class DefaultGameStateManager : IGameStateManager
{
private readonly GameManager gameManager;
private readonly Level levelData;
public DefaultGameStateManager(GameManager gameManager, Level levelData)
{
this.gameManager = gameManager;
this.levelData = levelData;
}
public string CurrentLanguage => gameManager?.language;
public string[] GetLevelWords()
{
return levelData?.GetWords(CurrentLanguage);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 51726f82dad7405dbff26d0cd8974025
timeCreated: 1745821654

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ce27424fabf24a21a7c79ef4b949a233
timeCreated: 1727533261

View File

@ -0,0 +1,20 @@
// // ©2015 - 2025 Candy Smith
// // All rights reserved
// // Redistribution of this software is strictly not allowed.
// // Copy of this software can be obtained from unity asset store only.
// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// // FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
// // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// // THE SOFTWARE.
namespace WordsToolkit.Scripts.Gameplay.Managers
{
public interface IGameStateManager
{
string CurrentLanguage { get; }
string[] GetLevelWords();
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 549c3179f3434c0b818fa99432205a43
timeCreated: 1745821637

View File

@ -0,0 +1,105 @@
// // ©2015 - 2025 Candy Smith
// // All rights reserved
// // Redistribution of this software is strictly not allowed.
// // Copy of this software can be obtained from unity asset store only.
// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// // FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
// // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// // THE SOFTWARE.
using UnityEngine;
using WordsToolkit.Scripts.Enums;
using WordsToolkit.Scripts.Popups;
using WordsToolkit.Scripts.System;
using DG.Tweening;
using VContainer;
using WordsToolkit.Scripts.Levels;
namespace WordsToolkit.Scripts.Gameplay.Managers
{
public partial class LevelManager
{
[Inject]
private MenuManager menuManager;
private void HandleGameStateChange(EGameState newState)
{
switch (newState)
{
case EGameState.PrepareGame:
EventManager.GameStatus = EGameState.Playing;
break;
case EGameState.Playing:
if (HasTimer)
{
StartTimer();
}
break;
case EGameState.PreWin:
DOVirtual.DelayedCall(0.5f, () =>
{
ShowPreWinPopup();
});
break;
case EGameState.Win:
if (_levelData.GroupIsFinished())
ShowMenuFinished();
else
ShowMenuPlay();
break;
case EGameState.PreFailed:
menuManager.ShowPopup<PreFailed>(null, PrefailedResult);
break;
default:
break;
}
}
private void ShowMenuFinished()
{
menuManager.ShowPopup<Finish>(null, _=>ShowMenuPlay());
}
private void PrefailedResult(EPopupResult obj)
{
if (obj == EPopupResult.Continue)
{
TimerLimit += gameSettings.continueTime;
EventManager.GameStatus = EGameState.Playing;
}
else if(obj == EPopupResult.Cancel)
{
sceneLoader.GoMain();
}
}
private void ShowPreWinPopup()
{
var p = menuManager.ShowPopup<PreWin>(null, _ =>
{
PanelWinAnimation();
DOVirtual.DelayedCall(1f, () =>
{
EventManager.GameStatus = EGameState.Win;
});
});
p.transform.position = bubbleAnchor.position;
}
private void ShowMenuPlay()
{
if (Resources.LoadAll<Level>("Levels").Length <= _levelData.number)
{
menuManager.ShowPopup<ComingSoon>();
}
else
{
menuManager.ShowPopup<MenuPlay>();
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a4994490efe84783a1374aaa5135c3c4
timeCreated: 1729094280

View File

@ -0,0 +1,390 @@
// // ©2015 - 2025 Candy Smith
// // All rights reserved
// // Redistribution of this software is strictly not allowed.
// // Copy of this software can be obtained from unity asset store only.
// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// // FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
// // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// // THE SOFTWARE.
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.InputSystem;
using UnityEngine.Serialization;
using WordsToolkit.Scripts.Enums;
using WordsToolkit.Scripts.GUI;
using WordsToolkit.Scripts.Levels;
using WordsToolkit.Scripts.System;
using Object = UnityEngine.Object;
using VContainer;
using WordsToolkit.Scripts.GUI.Buttons;
using WordsToolkit.Scripts.Infrastructure.Service;
using WordsToolkit.Scripts.NLP;
using WordsToolkit.Scripts.Popups;
using WordsToolkit.Scripts.Settings;
namespace WordsToolkit.Scripts.Gameplay.Managers
{
public partial class LevelManager : MonoBehaviour
{
public int currentLevel;
private Level _levelData;
private FieldManager field;
public UnityEvent<Level> OnLevelLoaded;
protected float gameTimer = 0f;
private bool isTimerRunning = false;
// Dictionary to store special items in the level
private Dictionary<Vector2Int, GameObject> specialItems = new Dictionary<Vector2Int, GameObject>();
// Event that fires when a special item is collected
public UnityEvent<Vector2Int> OnSpecialItemCollected = new UnityEvent<Vector2Int>();
[SerializeField]
private Transform giftButton;
public bool hammerMode;
public float GameTime { get => gameTimer; private set => gameTimer = value; }
public bool HasTimer { get; private set; }
// Timer limit in seconds, -1 means unlimited time
public float TimerLimit { get; private set; } = -1f;
// Event that fires when the timer runs out
public UnityEvent OnTimerExpired = new UnityEvent();
private StateManager stateManager;
private SceneLoader sceneLoader;
private GameManager gameManager;
private DebugSettings debugSettings;
private ILevelLoaderService levelLoaderService;
private ButtonViewController buttonController;
private GameSettings gameSettings;
[SerializeField]
private Transform bubbleAnchor;
private ICustomWordRepository customWordRepository;
// Debug panel for web demo
private bool showDebugPanel = true;
private bool isDebugPanelExpanded = false;
private Vector2 scrollPosition = Vector2.zero;
[Inject]
public void Construct(FieldManager fieldManager, StateManager stateManager,
SceneLoader sceneLoader, GameManager gameManager, DebugSettings debugSettings, ILevelLoaderService levelLoaderService, ButtonViewController buttonController, GameSettings gameSettings, ICustomWordRepository customWordRepository)
{
this.debugSettings = debugSettings;
this.gameManager = gameManager;
this.field = fieldManager;
this.stateManager = stateManager;
this.sceneLoader = sceneLoader;
this.levelLoaderService = levelLoaderService;
this.buttonController = buttonController;
this.gameSettings = gameSettings;
this.customWordRepository = customWordRepository;
}
private void OnEnable()
{
EventManager.GameStatus = EGameState.PrepareGame;
EventManager.GetEvent(EGameEvent.RestartLevel).Subscribe(RestartLevel);
EventManager.OnGameStateChanged += HandleGameStateChange;
if (field != null)
{
field.OnAllTilesOpened.AddListener(HandleAllTilesOpened);
field.OnAllRequiredWordsFound.AddListener(HandleAllRequiredWordsFound);
}
Load();
}
private void OnDisable()
{
EventManager.GetEvent(EGameEvent.RestartLevel).Unsubscribe(RestartLevel);
EventManager.OnGameStateChanged -= HandleGameStateChange;
if (field != null)
{
field.OnAllTilesOpened.RemoveListener(HandleAllTilesOpened);
field.OnAllRequiredWordsFound.RemoveListener(HandleAllRequiredWordsFound);
}
if (stateManager != null)
{
stateManager.OnStateChanged.RemoveListener(HandleStateChanged);
}
}
private void HandleAllTilesOpened()
{
SetWin();
}
private void RestartLevel()
{
GameDataManager.SetLevel(_levelData);
sceneLoader.StartGameScene();
}
public void Load()
{
// check the level is loaded
if (EventManager.GameStatus == EGameState.Playing)
{
return;
}
field.Clear();
// currentLevel = GameDataManager.GetLevelNum();
_levelData = GameDataManager.GetLevel();
currentLevel = _levelData.number;
if (_levelData == null)
{
// Try to find previous level
int previousLevel = currentLevel - 1;
while (previousLevel > 0)
{
GameDataManager.SetLevelNum(previousLevel);
_levelData = GameDataManager.GetLevel();
if (_levelData != null)
{
currentLevel = previousLevel;
break;
}
previousLevel--;
}
// If still null after trying previous levels
if (_levelData == null)
{
return;
}
}
// Clear special items collection when loading a new level
ClearSpecialItems();
levelLoaderService.NotifyBeforeLevelLoaded(_levelData);
LoadLevel(_levelData);
Invoke(nameof(StartGame), 0.5f);
}
private void StartGame()
{
buttonController.ShowButtons();
levelLoaderService.NotifyLevelLoaded(_levelData);
EventManager.GameStatus = EGameState.Playing;
EventManager.GetEvent<Level>(EGameEvent.Play).Invoke(_levelData);
if (stateManager != null)
{
stateManager.OnStateChanged.RemoveListener(HandleStateChanged);
stateManager.OnStateChanged.AddListener(HandleStateChanged);
}
}
public void LoadLevel(Level levelData)
{
// Get the current language setting
string language = gameManager.language;
// Check if level data contains saved crossword for this language
var languageData = levelData.GetLanguageData(language);
// Generate the field with level data
// Use the new specialItems list instead of filtering placements
var specialItems = languageData?.crosswordData?.specialItems ?? new List<SerializableSpecialItem>();
field.GenerateWithSpecialItems(levelData, language, specialItems);
// Initialize timer settings from level data
HasTimer = levelData.enableTimer;
TimerLimit = levelData.enableTimer ? levelData.timerDuration : -1f;
// Reset timer for new level
ResetTimer();
}
public void SetWin()
{
customWordRepository.ClearExtraWords();
GameDataManager.UnlockLevel(currentLevel + 1);
EventManager.GameStatus = EGameState.PreWin;
}
private void PanelWinAnimation()
{
buttonController.HideAllForWin();
}
private void SetLose()
{
EventManager.GameStatus = EGameState.PreFailed;
}
private void Update()
{
if (Keyboard.current != null)
{
if (Keyboard.current[debugSettings.Win].wasPressedThisFrame)
{
SetWin();
}
if (Keyboard.current[debugSettings.Lose].wasPressedThisFrame)
{
SetLose();
}
if (Keyboard.current[debugSettings.Restart].wasPressedThisFrame)
{
gameManager.RestartLevel();
}
#if UNITY_WEBGL && !UNITY_EDITOR
// Quick win shortcut for web demo testing
if (Keyboard.current[Key.W].wasPressedThisFrame)
{
SetWin();
}
// Toggle debug panel with T key
if (Keyboard.current[Key.T].wasPressedThisFrame)
{
showDebugPanel = !showDebugPanel;
}
#endif
}
// Update timer if it's running
if (isTimerRunning && HasTimer && !menuManager.IsAnyPopupOpened())
{
// Timer now decreases instead of increases
gameTimer += Time.deltaTime;
// Check if timer has expired (if there's a limit)
if (TimerLimit > 0 && gameTimer >= TimerLimit)
{
TimerExpired();
}
}
}
private void StartTimer()
{
isTimerRunning = true;
}
private void StopTimer()
{
isTimerRunning = false;
}
private void ResetTimer()
{
gameTimer = 0f;
isTimerRunning = false;
}
private void TimerExpired()
{
// Stop the timer
StopTimer();
// Notify listeners that the timer has expired
OnTimerExpired.Invoke();
// Just check if all tiles are opened
if (field != null && field.AreAllTilesOpen())
{
SetWin();
return;
}
// If not all tiles are opened, player loses
SetLose();
}
public Level GetCurrentLevel()
{
return _levelData;
}
public string GetCurrentLanguage()
{
return gameManager?.language ?? "en";
}
private void HandleAllRequiredWordsFound()
{
EventManager.GameStatus = EGameState.PreWin;
}
// Register a special item instance with its position
public void RegisterSpecialItem(Vector2Int position, GameObject itemInstance)
{
if (itemInstance == null)
return;
specialItems[position] = itemInstance;
}
// Collect a special item at the given position
public bool CollectSpecialItem(Vector2Int position)
{
if (specialItems.TryGetValue(position, out GameObject item))
{
// Fire event before removing the item
OnSpecialItemCollected.Invoke(position);
// Remove from dictionary and destroy the instance
specialItems.Remove(position);
Destroy(item);
return true;
}
return false;
}
// Clear all special items
private void ClearSpecialItems()
{
foreach (var item in specialItems.Values)
{
if (item != null)
{
Destroy(item);
}
}
specialItems.Clear();
}
// Keep the SerializableStringArray class as it might be used elsewhere or for future serialization
[Serializable]
private class SerializableStringArray
{
public string[] words;
}
public Vector3 GetSpecialItemCollectionPoint()
{
return this.giftButton.transform.position;
}
private void HandleStateChanged(EScreenStates newState)
{
if (newState == EScreenStates.Game)
{
Load();
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e40a00e9a36a45f0bbc7a894cfab08c1
timeCreated: 1727346788

View File

@ -0,0 +1,69 @@
// // ©2015 - 2025 Candy Smith
// // All rights reserved
// // Redistribution of this software is strictly not allowed.
// // Copy of this software can be obtained from unity asset store only.
// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// // FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
// // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// // THE SOFTWARE.
using UnityEngine;
using WordsToolkit.Scripts.System;
using VContainer;
using UnityEngine.Events;
namespace WordsToolkit.Scripts.Gameplay.Managers
{
public class StateManager : MonoBehaviour
{
[SerializeField]
private GameObject[] mainMenus;
[SerializeField]
private GameObject[] maps;
[SerializeField]
private GameObject[] games;
private EScreenStates _currentState;
public UnityEvent<EScreenStates> OnStateChanged = new UnityEvent<EScreenStates>();
public EScreenStates CurrentState
{
get => _currentState;
set
{
_currentState = value;
SetActiveState(mainMenus, _currentState == EScreenStates.MainMenu);
SetActiveState(games, _currentState == EScreenStates.Game);
OnStateChanged?.Invoke(_currentState);
}
}
public void HideMain()
{
SetActiveState(mainMenus, false);
}
private void SetActiveState(GameObject[] gameObjects, bool isActive)
{
foreach (var gameObject in gameObjects)
{
if (gameObject != null && gameObject.activeSelf != isActive)
{
gameObject.SetActive(isActive);
}
}
}
}
public enum EScreenStates
{
MainMenu,
Game
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 630490a36ddd4c0581994f1efaf73255
timeCreated: 1731134799

View File

@ -0,0 +1,178 @@
// // ©2015 - 2025 Candy Smith
// // All rights reserved
// // Redistribution of this software is strictly not allowed.
// // Copy of this software can be obtained from unity asset store only.
// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// // FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
// // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// // THE SOFTWARE.
using System;
using System.Linq;
using DG.Tweening;
using UnityEngine;
using VContainer;
using VContainer.Unity;
using WordsToolkit.Scripts.Enums;
using WordsToolkit.Scripts.GUI.Buttons;
using WordsToolkit.Scripts.GUI.Tutorials;
using WordsToolkit.Scripts.Levels;
using WordsToolkit.Scripts.Localization;
using WordsToolkit.Scripts.Popups;
using WordsToolkit.Scripts.Settings;
using WordsToolkit.Scripts.System;
namespace WordsToolkit.Scripts.Gameplay.Managers
{
public class TutorialManager : IStartable, IDisposable
{
private readonly TutorialSettings settings;
private readonly MenuManager menuManager;
private readonly ILocalizationService localizationManager;
private readonly GameManager gameManager;
private readonly IObjectResolver _resolver;
private TutorialPopupBase tutorial;
public TutorialManager(
TutorialSettings settings,
MenuManager menuManager,
ILocalizationService localizationManager,
GameManager gameManager,
IObjectResolver resolver)
{
this.settings = settings;
this.menuManager = menuManager;
this.localizationManager = localizationManager;
this.gameManager = gameManager;
this._resolver = resolver;
}
public void Start()
{
EventManager.GetEvent<Level>( EGameEvent.Play).Subscribe(OnLevelLoaded);
EventManager.GetEvent(EGameEvent.WordAnimated).Subscribe(OnWordOpened);
EventManager.GetEvent<string>(EGameEvent.ExtraWordFound).Subscribe(ExtraWordFound);
EventManager.GetEvent(EGameEvent.SpecialItemCollected).Subscribe(OnSpecialItemCollected);
EventManager.GetEvent<CustomButton>( EGameEvent.ButtonClicked).Subscribe(OnCustomButtonClicked);
}
public void Dispose()
{
EventManager.GetEvent<Level>( EGameEvent.Play).Unsubscribe(OnLevelLoaded);
EventManager.GetEvent(EGameEvent.WordAnimated).Unsubscribe(OnWordOpened);
EventManager.GetEvent<string>(EGameEvent.ExtraWordFound).Unsubscribe(ExtraWordFound);
EventManager.GetEvent(EGameEvent.SpecialItemCollected).Unsubscribe(OnSpecialItemCollected);
EventManager.GetEvent<CustomButton>( EGameEvent.ButtonClicked).Unsubscribe(OnCustomButtonClicked);
if (tutorial != null)
{
tutorial.OnCloseAction -= OnTutorialClosed;
tutorial = null;
}
}
private void OnSpecialItemCollected()
{
ShowTutorialPopup(t => t.showCondition.showCondition == ETutorialShowCondition.Event && t.kind == TutorialKind.GiftButton);
}
private void OnCustomButtonClicked(CustomButton obj)
{
CloseTutorial();
}
private void CloseTutorial()
{
if (tutorial != null)
{
tutorial.Close();
}
}
private void ExtraWordFound(string obj)
{
ShowTutorialPopup(t => t.showCondition.showCondition == ETutorialShowCondition.Event && t.kind == TutorialKind.ExtraWordsButton);
}
private void OnLevelLoaded(Level obj)
{
DOVirtual.DelayedCall(0.2f, () => UpdateTutorialAppearance(obj), false);
}
private void UpdateTutorialAppearance(Level obj)
{
var tutorialShown = ShowTutorialPopup(t => t.showCondition.showCondition == ETutorialShowCondition.Level && t.showCondition.level == obj.number|| t.showCondition.showCondition == ETutorialShowCondition.FirstAppearance);
if (tutorialShown)
return;
var hasSpecialItem = obj.GetLanguageData(gameManager.language).crosswordData.placements.Any(i => i.isSpecialItem);
if (hasSpecialItem)
{
ShowTutorialPopup(t => t.showCondition.showCondition == ETutorialShowCondition.FirstAppearance && t.kind == TutorialKind.RedGem);
}
}
private void OnWordOpened()
{
UpdateTutorialAppearance(GameDataManager.GetLevel());
}
private bool ShowTutorialPopup(Func<TutorialSettingsData, bool> predicate)
{
// Only show tutorials when game is in playing state
if (EventManager.GameStatus != EGameState.Playing)
return false;
var tutorialDatas = settings.tutorialSettings.Where(predicate);
foreach (var tutorialData in tutorialDatas)
{
bool notShow = false;
foreach (var tag in tutorialData.tagsToShow)
{
var obj = GameObject.FindGameObjectWithTag(tag);
if (obj == null || !obj.activeSelf || (obj.TryGetComponent(out CanvasGroup cg) && cg.alpha <= 0))
{
notShow = true;
break; // If any tag is not active, do not show the tutorial
}
}
if (notShow)
continue; // Skip to the next tutorial if any tag is not active
if (!PlayerPrefs.HasKey(tutorialData.GetID()) || PlayerPrefs.GetInt(tutorialData.GetID()) != 1)
{
ShowTutorial(tutorialData);
return true; // Indicate that a tutorial was shown
}
}
return false; // No tutorial was shown
}
private void ShowTutorial(TutorialSettingsData tutorialData)
{
if (tutorialData != null)
{
tutorial = (TutorialPopupBase)menuManager.ShowPopup(tutorialData.popup);
tutorial.SetData(tutorialData);
tutorial.SetTitle(localizationManager.GetText(tutorialData.kind.ToString(), "Use this booster"));
tutorial.OnCloseAction += OnTutorialClosed;
}
}
private void OnTutorialClosed(EPopupResult obj)
{
if (tutorial != null)
{
PlayerPrefs.SetInt(tutorial.GetData().GetID(), 1); // Mark as shown
PlayerPrefs.Save();
tutorial.OnCloseAction -= OnTutorialClosed;
if (tutorial)
{
tutorial = null; // Clear the reference
}
}
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,39 @@
// // ©2015 - 2025 Candy Smith
// // All rights reserved
// // Redistribution of this software is strictly not allowed.
// // Copy of this software can be obtained from unity asset store only.
// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// // FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
// // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// // THE SOFTWARE.
using UnityEngine;
namespace WordsToolkit.Scripts.Gameplay.Pool
{
public class AutoReturnToPool : MonoBehaviour
{
public float timeToReturn;
private void OnEnable()
{
if (timeToReturn > 0)
{
Invoke(nameof(ReturnToPool), timeToReturn);
}
}
private void ReturnToPool()
{
gameObject.SetActive(false);
}
private void OnDisable()
{
PoolObject.Return(gameObject);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6ce269b49d1a4b688c230dc3d12140b7
timeCreated: 1689440777

View File

@ -0,0 +1,33 @@
// // ©2015 - 2025 Candy Smith
// // All rights reserved
// // Redistribution of this software is strictly not allowed.
// // Copy of this software can be obtained from unity asset store only.
// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// // FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
// // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// // THE SOFTWARE.
using UnityEngine;
namespace WordsToolkit.Scripts.Gameplay.Pool
{
internal class InitialAmountPool : PoolObject
{
[SerializeField]
private int initialCapacity;
public override void Awake()
{
base.Awake();
for (var i = 0; i < initialCapacity; i++)
{
var item = Create();
item.SetActive(false);
pool.Enqueue(item);
}
}
}
}

View File

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

View File

@ -0,0 +1,118 @@
// // ©2015 - 2025 Candy Smith
// // All rights reserved
// // Redistribution of this software is strictly not allowed.
// // Copy of this software can be obtained from unity asset store only.
// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// // FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
// // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// // THE SOFTWARE.
using System.Collections.Generic;
using UnityEngine;
using WordsToolkit.Scripts.System;
namespace WordsToolkit.Scripts.Gameplay.Pool
{
public class PoolObject : MonoBehaviour
{
protected static readonly Dictionary<string, PoolObject> pools = new();
public GameObject prefab;
protected Queue<GameObject> pool = new();
public virtual void Awake()
{
if (prefab != null)
{
SetPrefab(prefab);
}
}
public void SetPrefab(GameObject newPrefab)
{
pools[newPrefab.name] = this;
prefab = newPrefab;
}
private void OnDestroy()
{
pools.Clear();
}
protected GameObject Create()
{
var item = Instantiate(prefab, transform);
item.name = prefab.name;
return item;
}
private GameObject Get()
{
var item = pool.Count == 0 ? Create() : pool.Dequeue();
if (item.activeSelf && pool.Count > 0)
{
item = pool.Dequeue();
}
item.SetActive(true);
return item;
}
public static void Return(GameObject item)
{
if (item == null)
{
return;
}
item.SetActive(false);
if (pools.TryGetValue(item.name, out var pool))
{
pool.pool.Enqueue(item);
}
}
public static GameObject GetObject(GameObject prefab)
{
return GetPool(prefab);
}
public static GameObject GetObject(GameObject prefab, Vector3 position)
{
var item = GetPool(prefab);
item.transform.position = position;
return item;
}
private static GameObject GetPool(GameObject prefab)
{
var prefabName = prefab.name;
if (pools.TryGetValue(prefabName, out var pool))
{
return pool.Get();
}
var poolObject = new GameObject(prefabName).AddComponent<PoolObject>();
poolObject.transform.SetParent(GameObject.Find("FXPool").transform);
poolObject.prefab = prefab;
poolObject.transform.localScale = Vector3.one;
pools.Add(prefabName, poolObject);
return poolObject.Get();
}
public static GameObject GetObject(string prefabName)
{
if (pools.TryGetValue(prefabName, out var pool))
{
return pool.Get();
}
Debug.LogError($"Pool with name {prefabName} not found");
return null;
}
}
}

View File

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

View File

@ -0,0 +1,28 @@
// // ©2015 - 2025 Candy Smith
// // All rights reserved
// // Redistribution of this software is strictly not allowed.
// // Copy of this software can be obtained from unity asset store only.
// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// // FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
// // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// // THE SOFTWARE.
using TMPro;
using UnityEngine;
namespace WordsToolkit.Scripts.Gameplay
{
public class ScoreText : MonoBehaviour
{
public TextMeshProUGUI scoreText;
public void ShowScore(int value, Vector3 transformPosition)
{
scoreText.transform.position = transformPosition;
scoreText.text = "+" + value;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 51758275a8aa4f4f8fcf16ea27912e97
timeCreated: 1728488609

View File

@ -0,0 +1,31 @@
// // ©2015 - 2025 Candy Smith
// // All rights reserved
// // Redistribution of this software is strictly not allowed.
// // Copy of this software can be obtained from unity asset store only.
// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// // FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
// // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// // THE SOFTWARE.
using System;
using UnityEngine;
using WordsToolkit.Scripts.Attributes;
namespace WordsToolkit.Scripts.Gameplay
{
public abstract class ScriptableData : ScriptableObject
{
[IconPreview]
public FillAndPreview prefab;
public virtual void OnValidate()
{
OnChange?.Invoke();
}
public event Action OnChange;
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 60337560ccc04a68b7b96cf29bff97a7
timeCreated: 1746685197

View File

@ -0,0 +1,122 @@
// // ©2015 - 2025 Candy Smith
// // All rights reserved
// // Redistribution of this software is strictly not allowed.
// // Copy of this software can be obtained from unity asset store only.
// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// // FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
// // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// // THE SOFTWARE.
using UnityEngine;
using DG.Tweening;
using System;
using UnityEngine.UI;
using VContainer;
using WordsToolkit.Scripts.Audio;
using WordsToolkit.Scripts.System;
using WordsToolkit.Scripts.Enums;
using WordsToolkit.Scripts.Gameplay.Pool;
namespace WordsToolkit.Scripts.Gameplay
{
public class SpecialItem : MonoBehaviour
{
[Header("Animation Settings")]
[SerializeField] private float moveDuration = 0.7f; // Matched with AnimateExtraWord
[SerializeField] private float fadeOutDuration = 0.2f;
[SerializeField] private AudioClip collectSound; // Sound to play when item is collected
[SerializeField] private AudioClip bounceSound; // Sound to play on bounce effect
[Inject]
private IAudioService audioService; // Audio service for playing sounds
private Sequence animationSequence;
[SerializeField]
private ParticleSystem collectEffect; // Optional particle effect to play on collection
/// <summary>
/// Animates the special item to a target position using an arc path animation
/// </summary>
/// <param name="targetPosition">The world position to move to</param>
/// <param name="onComplete">Optional callback when animation completes</param>
public void FlyToPosition(Vector3 targetPosition, Action onComplete = null)
{
// Kill any existing animation
animationSequence?.Kill();
// Make sure the item is visible
gameObject.SetActive(true);
// audioService?.PlaySound(collectSound);
// Create animation sequence
animationSequence = DOTween.Sequence();
// Store original scale
Vector3 originalScale = transform.localScale;
audioService?.PlaySound(bounceSound);
// Initial move up animation
Vector3 startPosition = transform.position;
animationSequence.Append(transform.DOMoveY(startPosition.y + 0.5f, 0.3f)
.SetEase(Ease.OutCubic));
// Add delay after move up
animationSequence.AppendInterval(0.2f);
// Calculate a mid-point for the arc
startPosition = transform.position;
Vector3 midPoint = (startPosition + targetPosition) / 2f;
// Determine arc height based on the distance between points (30% of distance)
float arcHeight = Vector3.Distance(startPosition, targetPosition) * 0.3f;
midPoint.y += arcHeight;
// Calculate half duration for scaling
float halfDuration = moveDuration / 2f;
// Create path animation
var pathTween = transform.DOMove(
targetPosition,
moveDuration)
.SetEase(Ease.OutQuad);
// Add the path animation to the sequence
animationSequence.Append(pathTween);
// Scale down during the second half (matches AnimateExtraWord)
animationSequence.Append(transform.DOScale(originalScale * 0.8f, .2f).SetLoops(1, LoopType.Yoyo)
.SetEase(Ease.InOutQuad));
// Fade out at the end
Image image = GetComponent<Image>();
if (image != null)
{
animationSequence.Append(image.DOFade(0, fadeOutDuration)
.SetEase(Ease.InQuad));
}
// Set completion callback
animationSequence.OnComplete(() => {
// Fire event to notify that a special item was collected
EventManager.GetEvent(EGameEvent.SpecialItemCollected).Invoke();
var fx = PoolObject.GetObject(collectEffect.gameObject, transform.position);
// Invoke the callback if provided
onComplete?.Invoke();
audioService?.PlaySound(collectSound);
// Clean up
Destroy(gameObject);
});
}
// Stop any ongoing animation when destroyed
private void OnDestroy()
{
animationSequence?.Kill();
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: cecf8650bf08408282be9a42762cc254
timeCreated: 1742467855

View File

@ -0,0 +1,351 @@
// // ©2015 - 2025 Candy Smith
// // All rights reserved
// // Redistribution of this software is strictly not allowed.
// // Copy of this software can be obtained from unity asset store only.
// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// // FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
// // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// // THE SOFTWARE.
using DG.Tweening;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using WordsToolkit.Scripts.Settings;
using WordsToolkit.Scripts.Gameplay.Managers;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem;
using WordsToolkit.Scripts.Enums;
using WordsToolkit.Scripts.System;
using VContainer;
using VContainer.Unity;
using WordsToolkit.Scripts.Audio;
using WordsToolkit.Scripts.System.Haptic;
namespace WordsToolkit.Scripts.Gameplay
{
public class Tile : FillAndPreview, IPointerClickHandler
{
public TextMeshProUGUI character;
public Image[] images;
[Header("Tile Colors")]
public Color[] closedColors;
private Color[] openColors = new Color[3];
private bool isSelected = false;
private int wordNumber = -1;
private bool isOpen = false;
[Header("Special Item")]
[SerializeField] private GameObject specialItemPrefab; // Direct reference to the special item prefab
[Header("Hammer Animation")]
[SerializeField] private GameObject hammerAnimationPrefab; // Reference to hammer animation prefab
// Added fields for special item support
private bool hasSpecialItem = false;
private Vector2Int specialItemPosition;
private GameObject specialItemInstance; // Reference to the instantiated special item
// Simple selection state
private LevelManager levelManager;
private FieldManager fieldManager;
[SerializeField]
private GameObject fx;
private IAudioService audioService;
private IObjectResolver objectResolver;
[Inject]
public void Construct(LevelManager levelManager, FieldManager fieldManager, IAudioService audioService, IObjectResolver objectResolver)
{
this.levelManager = levelManager;
this.fieldManager = fieldManager;
this.audioService = audioService;
this.objectResolver = objectResolver;
}
public void SetColors(ColorsTile colorsTile)
{
if (colorsTile == null)
return;
openColors[0] = colorsTile.faceColor;
openColors[1] = colorsTile.topColor;
openColors[2] = colorsTile.bottomColor;
}
// Set the tile to closed state
public void SetTileClosed()
{
isOpen = false;
// Hide character
if (character != null)
{
character.gameObject.SetActive(false);
}
// Apply closed color to all images
for (var i = 0; i < images.Length; i++)
{
var img = images[i];
if (img != null)
{
img.color = closedColors[i];
}
}
transform.SetAsFirstSibling();
}
// Set the tile to open state
public void SetTileOpen()
{
isOpen = true;
HapticFeedback.TriggerHapticFeedback(HapticFeedback.HapticForce.Light);
// Show character
if (character != null)
{
character.gameObject.SetActive(true);
}
// Apply open color to all images
for (var i = 0; i < images.Length; i++)
{
var img = images[i];
if (img != null)
{
img.color = openColors[i];
}
}
// Add bounce animation
transform.DOScale(1.2f, 0.1f)
.SetEase(Ease.OutQuad)
.OnComplete(() => {
transform.DOScale(1f, 0.15f)
.SetEase(Ease.Linear);
});
transform.SetAsLastSibling();
// If this tile has a special item, animate it and ensure it stays on top
if (hasSpecialItem && specialItemInstance != null)
{
specialItemInstance.transform.SetAsLastSibling();
SpecialItem specialItem = specialItemInstance.GetComponent<SpecialItem>();
if (specialItem != null)
{
// Try to find a target position - use level manager collection point if available
Vector3 targetPosition = transform.position + new Vector3(0, 300, 0); // Default fallback
// Get special item collection point if available
if (levelManager != null)
{
var collectionPoint = levelManager.GetSpecialItemCollectionPoint();
targetPosition = collectionPoint;
}
// Start the animation
specialItem.FlyToPosition(targetPosition, () => {
// Notify level manager that item was collected
if (levelManager != null)
{
levelManager.CollectSpecialItem(specialItemPosition);
}
// Clear the reference since the item destroys itself
specialItemInstance = null;
});
}
}
}
// Check if tile is open
public bool IsOpen()
{
return isOpen;
}
// Set the character for this tile
public void SetCharacter(char c)
{
if (character != null)
{
character.text = c.ToString().ToUpper();
}
}
public void ShakeTile()
{
RectTransform rectTransform = GetComponent<RectTransform>();
if (rectTransform == null)
return;
Vector2 originalPosition = rectTransform.anchoredPosition;
Sequence shakeSequence = DOTween.Sequence();
float shakeAmount = 5f;
float shakeDuration = 0.05f;
int shakeCount = 4;
for (int i = 0; i < shakeCount; i++)
{
float xOffset = (i % 2 == 0) ? shakeAmount : -shakeAmount;
shakeSequence.Append(rectTransform.DOAnchorPos(
new Vector2(originalPosition.x + xOffset, originalPosition.y),
shakeDuration).SetEase(Ease.OutQuad));
}
for (int i = 0; i < images.Length; i++)
{
if (images[i] != null)
{
shakeSequence.Join(images[i].DOColor(Color.white, shakeDuration)
.SetLoops(2, LoopType.Yoyo));
}
}
shakeSequence.Append(rectTransform.DOAnchorPos(originalPosition, shakeDuration));
}
// Enhanced method to associate a special item with this tile
public void AssociateSpecialItem(Vector2Int position)
{
hasSpecialItem = true;
specialItemPosition = position;
// Create the special item instance if we have a prefab
if (specialItemPrefab != null && specialItemInstance == null)
{
InstantiateSpecialItem();
}
}
// Associate with a specific prefab (override the default)
public void AssociateSpecialItem(Vector2Int position, GameObject itemPrefab)
{
// Set the prefab
specialItemPrefab = itemPrefab;
// Call the regular association method
AssociateSpecialItem(position);
}
// Create the special item instance
private void InstantiateSpecialItem()
{
if (specialItemPrefab == null)
{
// Try to load a default prefab if none is assigned
specialItemPrefab = Resources.Load<GameObject>("Prefabs/DefaultSpecialItem");
if (specialItemPrefab == null)
{
Debug.LogWarning("No special item prefab assigned to tile and no default found.");
return;
}
}
specialItemInstance = objectResolver.Instantiate(specialItemPrefab, transform);
specialItemInstance.transform.SetParent(transform.parent);
specialItemInstance.transform.SetAsLastSibling();
// fit size to tile
RectTransform rectTransform = specialItemInstance.GetComponent<RectTransform>();
if (rectTransform != null)
{
rectTransform.sizeDelta = GetComponent<RectTransform>().sizeDelta / 1.2f;
}
if (levelManager != null)
{
levelManager.RegisterSpecialItem(specialItemPosition, specialItemInstance);
}
}
// Remove special item association and destroy instance
public void RemoveSpecialItem()
{
hasSpecialItem = false;
if (specialItemInstance != null)
{
Destroy(specialItemInstance);
specialItemInstance = null;
}
}
// Check if this tile has a special item and return its position
public bool HasSpecialItem(out Vector2Int position)
{
position = specialItemPosition;
return hasSpecialItem;
}
// Play hammer animation and open tile with delay
private void PlayHammerAnimationAndOpen()
{
if (hammerAnimationPrefab != null)
{
// Instantiate hammer animation on this tile
var offset = Vector3.right * 1.5f + Vector3.up * 0.5f;
GameObject hammer = Instantiate(hammerAnimationPrefab, transform.position + offset, Quaternion.identity);
DOVirtual.DelayedCall(.6f, OpenTileAfterAnimation);
}
else
{
// If no hammer animation, just open immediately
OpenTileAfterAnimation();
}
}
// Open the tile after animation completes
private void OpenTileAfterAnimation()
{
SetTileOpen();
EventManager.GetEvent<Tile>(EGameEvent.TileSelected).Invoke(this);
}
// Implement UI touch interface method instead of OnMouseDown
public void OnPointerClick(PointerEventData eventData)
{
// Only respond if the tile is selectable and closed
if (!isOpen && levelManager != null && levelManager.hammerMode)
{
// Instead of opening immediately, play hammer animation first
PlayHammerAnimationAndOpen();
}
}
public override void FillIcon(ScriptableData iconScriptable)
{
UpdateColor((ColorsTile)iconScriptable);
}
private void UpdateColor(ColorsTile itemTemplate)
{
images[0].color = itemTemplate.faceColor;
images[1].color = itemTemplate.topColor;
images[2].color = itemTemplate.bottomColor;
}
public void ShowEffect()
{
fx.SetActive(true);
}
public GameObject GetSpecialItemInstance()
{
return specialItemInstance;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f1ff910e0235466b92d2570fc0b1f487
timeCreated: 1741668588

View File

@ -0,0 +1,214 @@
// // ©2015 - 2025 Candy Smith
// // All rights reserved
// // Redistribution of this software is strictly not allowed.
// // Copy of this software can be obtained from unity asset store only.
// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// // FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
// // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// // THE SOFTWARE.
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.InputSystem.UI;
using UnityEngine.UI;
namespace WordsToolkit.Scripts.Gameplay
{
public class VirtualMouse : MonoBehaviour
{
private VirtualMouseInput virtualMouseInput;
private bool isInitialized = false;
[SerializeField]
private Canvas canvas;
private RectTransform canvasRectTransform;
private Camera uiCamera;
[SerializeField]
private float cursorHideDelay = 1.0f; // Time in seconds before hiding cursor
[SerializeField]
private float fadeSpeed = 3.0f; // How fast the cursor fades in/out
[SerializeField]
private float minCursorAlpha = 0f; // Minimum alpha when hidden
[SerializeField]
private float maxCursorAlpha = .5f; // Maximum alpha when visible
private float idleTimer = 0f;
private Vector2 lastPosition;
public Image cursorImage; // Reference to cursor image
private bool isCursorVisible = true;
private float targetAlpha;
private float currentAlpha;
private void Awake()
{
virtualMouseInput = GetComponent<VirtualMouseInput>();
if (virtualMouseInput == null)
{
Debug.LogError("VirtualMouseInput component not found! Please add it to the same GameObject.");
return;
}
canvasRectTransform = canvas.GetComponent<RectTransform>();
if (canvas.renderMode == RenderMode.ScreenSpaceCamera || canvas.renderMode == RenderMode.WorldSpace)
{
uiCamera = canvas.worldCamera;
}
targetAlpha = maxCursorAlpha;
currentAlpha = maxCursorAlpha;
}
private void Start()
{
StartCoroutine(WaitForVirtualMouseInitialization());
}
private IEnumerator WaitForVirtualMouseInitialization()
{
float timeout = 2.0f;
float timer = 0f;
while (timer < timeout)
{
if (virtualMouseInput != null && virtualMouseInput.virtualMouse != null)
{
lastPosition = virtualMouseInput.virtualMouse.position.value;
isInitialized = true;
Debug.Log("Virtual mouse initialized successfully.");
yield break;
}
timer += 0.1f;
yield return new WaitForSeconds(0.1f);
}
Debug.LogError("Failed to initialize virtualMouse within timeout period.");
}
private void Update()
{
if (!isInitialized || virtualMouseInput == null || virtualMouseInput.virtualMouse == null)
{
return; // Skip if not initialized
}
transform.SetAsLastSibling();
if (canvas.renderMode == RenderMode.ScreenSpaceOverlay ||
canvas.renderMode == RenderMode.ScreenSpaceCamera)
{
transform.localScale = new Vector3(1f / canvas.scaleFactor, 1f / canvas.scaleFactor, 1f);
}
// Check if cursor moved - with null checks
try
{
Vector2 currentPosition = virtualMouseInput.virtualMouse.position.value;
if (Vector2.Distance(currentPosition, lastPosition) > 0.5f) // Small threshold to detect real movement
{
idleTimer = 0f;
targetAlpha = maxCursorAlpha; // Set target to maximum alpha rather than 1.0
lastPosition = currentPosition;
}
else
{
// No movement, increment timer
idleTimer += Time.deltaTime;
// Start fading cursor after delay
if (idleTimer >= cursorHideDelay)
{
targetAlpha = minCursorAlpha; // Set target to minimum alpha
}
}
// Smoothly fade the cursor
if (cursorImage != null)
{
// Gradually interpolate current alpha toward target alpha
currentAlpha = Mathf.Lerp(currentAlpha, targetAlpha, Time.deltaTime * fadeSpeed);
// Apply the alpha to the cursor image
Color cursorColor = cursorImage.color;
cursorColor.a = currentAlpha;
cursorImage.color = cursorColor;
}
}
catch (Exception e)
{
Debug.LogWarning("Error accessing virtual mouse position: " + e.Message);
}
}
private void LateUpdate()
{
if (!isInitialized || virtualMouseInput == null || virtualMouseInput.virtualMouse == null)
{
return; // Skip if not initialized
}
try
{
// Get the current virtual mouse position
var virtualMousePosition = virtualMouseInput.virtualMouse.position.value;
// Clamp to screen boundaries
virtualMousePosition.x = Mathf.Clamp(virtualMousePosition.x, 0, Screen.width);
virtualMousePosition.y = Mathf.Clamp(virtualMousePosition.y, 0, Screen.height);
if (canvas != null)
{
if (canvas.renderMode == RenderMode.ScreenSpaceOverlay)
{
float scaleFactor = canvas.scaleFactor;
Rect canvasRect = canvasRectTransform.rect;
float canvasWidth = canvasRect.width * scaleFactor;
float canvasHeight = canvasRect.height * scaleFactor;
float xOffset = (Screen.width - canvasWidth) / 2;
float yOffset = (Screen.height - canvasHeight) / 2;
virtualMousePosition.x = Mathf.Clamp(virtualMousePosition.x, xOffset, xOffset + canvasWidth);
virtualMousePosition.y = Mathf.Clamp(virtualMousePosition.y, yOffset, yOffset + canvasHeight);
}
else if (canvas.renderMode == RenderMode.ScreenSpaceCamera && uiCamera != null)
{
Vector3[] corners = new Vector3[4];
canvasRectTransform.GetWorldCorners(corners);
Vector2 min = new Vector2(float.MaxValue, float.MaxValue);
Vector2 max = new Vector2(float.MinValue, float.MinValue);
for (int i = 0; i < 4; i++)
{
Vector2 screenPos = RectTransformUtility.WorldToScreenPoint(uiCamera, corners[i]);
min.x = Mathf.Min(min.x, screenPos.x);
min.y = Mathf.Min(min.y, screenPos.y);
max.x = Mathf.Max(max.x, screenPos.x);
max.y = Mathf.Max(max.y, screenPos.y);
}
virtualMousePosition.x = Mathf.Clamp(virtualMousePosition.x, min.x, max.x);
virtualMousePosition.y = Mathf.Clamp(virtualMousePosition.y, min.y, max.y);
}
}
InputState.Change(virtualMouseInput.virtualMouse.position, virtualMousePosition);
}
catch (Exception e)
{
Debug.LogWarning("Error in virtual mouse LateUpdate: " + e.Message);
}
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 22ad4e27333e4e7da8ff07ce4de6c4c1
timeCreated: 1741938131

View File

@ -0,0 +1,748 @@
// // ©2015 - 2025 Candy Smith
// // All rights reserved
// // Redistribution of this software is strictly not allowed.
// // Copy of this software can be obtained from unity asset store only.
// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// // FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
// // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// // THE SOFTWARE.
using System;
using System.Collections.Generic;
using System.Linq;
using DG.Tweening;
using UnityEngine;
using UnityEngine.UI;
using WordsToolkit.Scripts.Gameplay.Managers;
using WordsToolkit.Scripts.GUI;
using WordsToolkit.Scripts.Levels;
using WordsToolkit.Scripts.System;
using TMPro;
using UnityEngine.Serialization;
using VContainer;
using WordsToolkit.Scripts.Audio;
using WordsToolkit.Scripts.Enums;
using WordsToolkit.Scripts.GUI.Buttons;
using WordsToolkit.Scripts.Infrastructure.Service;
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.UI;
namespace WordsToolkit.Scripts.Gameplay
{
public class WordSelectionManager : MonoBehaviour, IFadeable
{
// Event for when selected letters change
public event Action<List<LetterButton>> OnSelectionChanged;
// Event for when selection is completed
public event Action<string> OnSelectionCompleted;
[Header("References")]
public FieldManager fieldManager;
public LetterButton letterButtonPrefab;
[Header("Circle Layout Settings")]
public float radius = 200f;
public Vector2 circleCenter = Vector2.zero;
private List<LetterButton> selectedLetters = new List<LetterButton>();
private bool isSelecting = false;
private Vector2 currentMousePosition;
private VirtualMouseInput virtualMouseInput;
private float parallelThreshold = 0.9f; // Dot product threshold for small angle detection (0.9 = ~25 degrees)
private float minBackwardDistance = 100f; // Minimum distance to move backward before unselecting
private float maxAngleForUnselect = .5f; // Maximum angle in degrees between lines to trigger unselect
private bool isGameWon = false;
public bool IsSelecting => isSelecting;
public UILineRenderer lineRenderer;
[SerializeField]
private Transform parentLetters;
[SerializeField]
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;
[SerializeField]
private HorizontalLayoutGroup layout;
[Header("Rearrange Settings")]
[SerializeField] private float swapAnimationDuration = 0.5f;
[SerializeField] private Ease swapAnimationEase = Ease.InOutQuad;
[Header("Shuffle Button")]
[SerializeField] private Button shuffleButton;
private GameManager gameManager;
private ILevelLoaderService levelLoaderService;
private IAudioService soundBase;
[SerializeField]
private CanvasGroup canvasGroup;
[Inject]
public void Construct(ILevelLoaderService levelLoaderService, GameManager gameManager, IAudioService soundBase, ButtonViewController buttonViewController)
{
this.soundBase = soundBase;
this.gameManager = gameManager;
this.levelLoaderService = levelLoaderService;
this.levelLoaderService.OnLevelLoaded += OnLevelLoaded;
buttonViewController.RegisterButton(this);
}
private void Awake()
{
// Try to find VirtualMouseInput component in the scene
virtualMouseInput = FindObjectOfType<VirtualMouseInput>();
OnSelectionCompleted += ValidateWordWithModel;
}
private void Start()
{
if (shuffleButton != null)
{
shuffleButton.onClick.AddListener(RearrangeRandomLetters);
}
if (fieldManager != null)
{
fieldManager.OnAllTilesOpened.AddListener(OnGameWon);
}
}
private void OnDestroy()
{
if (shuffleButton != null)
{
shuffleButton.onClick.RemoveListener(RearrangeRandomLetters);
}
if (levelLoaderService != null)
{
levelLoaderService.OnLevelLoaded -= OnLevelLoaded;
}
if (fieldManager != null)
{
fieldManager.OnAllTilesOpened.RemoveListener(OnGameWon);
}
}
private void Update()
{
// Update mouse position and line renderer during selection
if (isSelecting)
{
// Check if input is still active, if not, end selection
if (!IsAnyInputActive())
{
EndSelection();
return;
}
UpdateMousePosition();
CheckForBackwardMovement();
UpdateLineRenderer();
}
}
private void CheckForBackwardMovement()
{
if (selectedLetters.Count < 2) return;
// Get the current line segment (from last selected letter to mouse position)
Vector2 lastLetterPos = selectedLetters[selectedLetters.Count - 1].GetComponent<RectTransform>().anchoredPosition;
Vector2 secondLastLetterPos = selectedLetters[selectedLetters.Count - 2].GetComponent<RectTransform>().anchoredPosition;
Vector2 currentMovement = currentMousePosition - lastLetterPos;
// Calculate minBackwardDistance as half the distance between the two latest letters
float distanceBetweenLatest = Vector2.Distance(lastLetterPos, secondLastLetterPos);
float dynamicMinBackwardDistance = distanceBetweenLatest * 0.5f;
// Check if we've moved back far enough to consider unselecting
if (currentMovement.magnitude < dynamicMinBackwardDistance) return;
// Check only the most recent line segment for backward movement
if (selectedLetters.Count >= 2)
{
int i = selectedLetters.Count - 2; // Only check the last segment
Vector2 letterPos = selectedLetters[i].GetComponent<RectTransform>().anchoredPosition;
Vector2 previousSegment = selectedLetters[i + 1].GetComponent<RectTransform>().anchoredPosition - letterPos;
// Check if current movement is backward and at small angle to previous segment
if (IsMovingBackwardWithSmallAngle(currentMovement, previousSegment, letterPos))
{
// Unselect letters from this point forward
UnselectLettersFromIndex(i + 1);
}
}
}
private bool IsMovingBackwardWithSmallAngle(Vector2 currentMovement, Vector2 previousSegment, Vector2 segmentStart)
{
// Normalize the segments
Vector2 currentDir = currentMovement.normalized;
Vector2 prevDir = previousSegment.normalized;
// Calculate the angle between the directions
float dotProduct = Vector2.Dot(currentDir, prevDir);
float angleInRadians = Mathf.Acos(Mathf.Clamp(dotProduct, -1f, 1f));
float angleInDegrees = angleInRadians * Mathf.Rad2Deg;
// Check if we're moving backward (opposite direction) with small angle
bool isOppositeDirection = dotProduct < 0; // Negative dot product means opposite directions
bool isSmallAngle = angleInDegrees > (180f - maxAngleForUnselect) && angleInDegrees < (180f + maxAngleForUnselect);
return isOppositeDirection && isSmallAngle;
}
private void UnselectLettersFromIndex(int fromIndex)
{
// Remove letters from the specified index to the end
for (int i = selectedLetters.Count - 1; i >= fromIndex; i--)
{
selectedLetters[i].SetSelected(false);
selectedLetters.RemoveAt(i);
}
// Update UI
UpdateSelectedWordText();
OnSelectionChanged?.Invoke(selectedLetters);
}
private void UpdateMousePosition()
{
Vector2 screenPosition = Vector2.zero;
// Check for touch input first (mobile devices)
if (Touchscreen.current != null && Touchscreen.current.primaryTouch.press.isPressed)
{
screenPosition = Touchscreen.current.primaryTouch.position.ReadValue();
}
// Check virtual mouse input
else if (virtualMouseInput != null && virtualMouseInput.virtualMouse != null && virtualMouseInput.virtualMouse.leftButton.isPressed)
{
screenPosition = virtualMouseInput.virtualMouse.position.ReadValue();
}
// Fallback to regular mouse input
else if (Mouse.current != null)
{
screenPosition = Mouse.current.position.ReadValue();
}
RectTransformUtility.ScreenPointToLocalPointInRectangle(
parentLetters.GetComponent<RectTransform>(),
screenPosition,
Camera.main,
out currentMousePosition
);
}
private bool IsAnyInputActive()
{
// Check touch input
if (Touchscreen.current != null && Touchscreen.current.primaryTouch.press.isPressed)
return true;
// Check virtual mouse
if (virtualMouseInput != null && virtualMouseInput.virtualMouse != null && virtualMouseInput.virtualMouse.leftButton.isPressed)
return true;
// Check regular mouse
if (Mouse.current != null && Mouse.current.leftButton.isPressed)
return true;
return false;
}
public void OnLevelLoaded(Level level)
{
if (lineRenderer != null)
{
lineRenderer.color = level.colorsTile.faceColor;
}
layout.GetComponent<Image>().color = level.colorsTile.faceColor;
// Clean up previous level
CleanupLevel();
var letters = level.GetLetters(gameManager.language);
int letterCount = letters.Length;
var letterSize = 132 - Mathf.Max(0, letterCount - 6) * 10;
for (int i = 0; i < letterCount; i++)
{
// Calculate the angle for this letter (in radians)
// Start from the top (90 degrees or π/2) and go clockwise
float angle = ((float)i / letterCount) * 2 * Mathf.PI - Mathf.PI/2;
// Calculate position on the circle
float x = circleCenter.x + radius * Mathf.Cos(angle);
float y = circleCenter.y + radius * Mathf.Sin(angle);
// Instantiate button
var button = Instantiate(letterButtonPrefab, parentLetters);
button.SetColor(level.colorsTile.faceColor);
RectTransform rectTransform = button.GetComponent<RectTransform>();
// Set position
rectTransform.anchoredPosition = new Vector2(x, y);
// Set text
button.SetText(letters[i].ToString());
button.letterText.fontSize = letterSize;
}
}
/// <summary>
/// Cleans up the current level state, including selections and UI elements
/// </summary>
public void CleanupLevel()
{
// Clear any active selection
ClearSelection();
// Clear any existing letters
ClearExistingLetters();
// Reset UI elements
if (selectedWordText != null)
{
selectedWordText.text = "";
}
// Reset background
SetBackgroundAlpha(0f);
// Reset selection state
isSelecting = false;
selectedLetters.Clear();
// Reset game won state and re-enable panel
isGameWon = false;
SetPanelBlockRaycast(true);
}
private void ClearExistingLetters()
{
// Find and destroy all existing letter buttons under parentLetters
if (parentLetters != null)
{
LetterButton[] existingButtons = parentLetters.GetComponentsInChildren<LetterButton>();
foreach (LetterButton button in existingButtons)
{
Destroy(button.gameObject);
}
}
}
public void StartSelection(LetterButton button)
{
// Clear any previous selection
ClearSelection();
// Start a new selection
isSelecting = true;
selectedLetters.Add(button);
soundBase.PlayIncremental(selectedLetters.Count);
// Initialize mouse position
UpdateMousePosition();
// Fade out rearrange button during selection
if (shuffleButton != null)
{
shuffleButton.GetComponent<CanvasGroup>().DOFade(0.0f, 0.3f);
}
// Make background visible when selection starts
SetBackgroundAlpha(1f);
// Start drawing the line
UpdateLineRenderer();
// Update the selected word text
UpdateSelectedWordText();
// Notify listeners about the selection change
OnSelectionChanged?.Invoke(selectedLetters);
}
public void AddToSelection(LetterButton button)
{
if (!isSelecting) return;
// Don't add if it's already the last selected letter
if (selectedLetters.Count > 0 && selectedLetters[selectedLetters.Count - 1] == button)
return;
// Check if this letter is already selected elsewhere in the chain
if (selectedLetters.Contains(button))
{
return;
}
soundBase.PlayIncremental(selectedLetters.Count);
// Add this letter to our selection
selectedLetters.Add(button);
// Update the line to include the new letter
UpdateLineRenderer();
// Update the selected word text
UpdateSelectedWordText();
// Notify listeners about the selection change
OnSelectionChanged?.Invoke(selectedLetters);
}
private void UpdateLineRenderer()
{
if (lineRenderer == null) return;
// Create array of points for the line renderer
// Add one extra point for the current mouse position when selecting
int pointCount = selectedLetters.Count + (isSelecting ? 1 : 0);
Vector2[] points = new Vector2[pointCount];
// Update each selected letter position
for (int i = 0; i < selectedLetters.Count; i++)
{
RectTransform letterRect = selectedLetters[i].GetComponent<RectTransform>();
// Use anchoredPosition since that's what we set in OnLevelLoaded
points[i] = letterRect.anchoredPosition;
}
// Add current mouse position as the last point if we're actively selecting
if (isSelecting && selectedLetters.Count > 0)
{
points[points.Length - 1] = currentMousePosition;
}
lineRenderer.points = points;
}
public void EndSelection()
{
if (!isSelecting) return;
isSelecting = false;
// Process the word that was formed
if (selectedLetters.Count > 0)
{
string word = GetSelectedWord();
// Notify listeners that a word selection is completed
OnSelectionCompleted?.Invoke(word);
// Here you would check if the word is valid and handle scoring
// For now, just clear selection after a short delay
Invoke("ClearSelection", 0.1f);
}
}
private string GetSelectedWord()
{
string word = "";
foreach (var letter in selectedLetters)
{
word += letter.GetLetter();
}
return word;
}
// Update the selected word display using character prefabs
private void UpdateSelectedWordText()
{
if (selectedWordText != null)
{
selectedWordText.color = new Color(selectedWordText.color.r, selectedWordText.color.g, selectedWordText.color.b, 1f);
selectedWordText.text = GetSelectedWord();
UpdateHorizontalLayout(layout);
}
}
public void ClearSelection()
{
// Clear all visual selection indicators
foreach (var letter in selectedLetters)
{
letter.SetSelected(false);
}
selectedLetters.Clear();
isSelecting = false;
// Fade in rearrange button when selection ends
if (shuffleButton != null)
{
shuffleButton.GetComponent<CanvasGroup>().DOFade(1f, 0.3f);
}
// Clear the line renderer
if (lineRenderer != null)
{
lineRenderer.points = new Vector2[0];
}
if (selectedWordText != null)
{
selectedWordText.text = "";
}
// Make background invisible when selection is cleared
SetBackgroundAlpha(0f);
}
// Updated method that gets character positions and delegates validation to FieldManager
private void ValidateWordWithModel(string word)
{
if (string.IsNullOrEmpty(word)) return;
// Get the positions from the actual text characters
List<Vector3> letterPositions = new List<Vector3>();
if (selectedWordText != null && !string.IsNullOrEmpty(selectedWordText.text))
{
// Force text to update
selectedWordText.ForceMeshUpdate();
// Get mesh info which contains character positions
TMP_TextInfo textInfo = selectedWordText.textInfo;
// Go through each character in the text
for (int i = 0; i < word.Length && i < textInfo.characterCount; i++)
{
// Make sure character is visible (not a space or control character)
if (!textInfo.characterInfo[i].isVisible) continue;
// Get the center position of the character in world space
Vector3 bottomLeft = selectedWordText.transform.TransformPoint(textInfo.characterInfo[i].bottomLeft);
Vector3 topRight = selectedWordText.transform.TransformPoint(textInfo.characterInfo[i].topRight);
Vector3 charCenter = (bottomLeft + topRight) / 2f;
letterPositions.Add(charCenter);
}
}
// Delegate the word validation to FieldManager
if (fieldManager != null)
{
// If the word is already open, show the shake animation
if (fieldManager.IsWordOpen(word))
{
ShakeSelectedWordBackground();
soundBase.PlayWrong();
}
// If the word is valid and was successfully opened (or was already open),
// animate the UI elements
if (fieldManager.ValidateWord(word, letterPositions))
{
SetPanelBlockRaycast(false);
AnimateAlphaDown(backgroundSelectedWord);
EventManager.GetEvent<string>(EGameEvent.WordOpened).Invoke(word);
}
}
}
private void OnGameWon()
{
isGameWon = true;
SetPanelBlockRaycast(false);
}
private void ShakeSelectedWordBackground()
{
if (backgroundSelectedWord == null) return;
RectTransform rectTransform = backgroundSelectedWord.GetComponent<RectTransform>();
if (rectTransform == null) return;
// Store original position
Vector2 originalPosition = rectTransform.anchoredPosition;
// Create shake sequence
Sequence shakeSequence = DOTween.Sequence();
// Create quick, small shakes (offset from original position)
float shakeAmount = 10f;
float shakeDuration = 0.08f;
int shakeCount = 3;
for (int i = 0; i < shakeCount; i++)
{
// Alternate directions: right, left, right
float xOffset = (i % 2 == 0) ? shakeAmount : -shakeAmount;
shakeSequence.Append(rectTransform.DOAnchorPos(
new Vector2(originalPosition.x + xOffset, originalPosition.y),
shakeDuration).SetEase(Ease.OutQuad));
}
shakeSequence.Append(rectTransform.DOAnchorPos(originalPosition, shakeDuration));
}
private void AnimateAlphaDown(Image image)
{
image.DOFade(0f, 0.3f);
selectedWordText.DOFade(0f, 0.3f);
DOVirtual.DelayedCall(1f, () => {
if (!isGameWon)
SetPanelBlockRaycast(true);
});
}
private void SetPanelBlockRaycast(bool blockRaycast)
{
if (panelCanvasGroup != null)
{
panelCanvasGroup.blocksRaycasts = blockRaycast;
}
}
// Helper method to set background alpha
private void SetBackgroundAlpha(float alpha)
{
if (backgroundSelectedWord != null)
{
Color color = backgroundSelectedWord.color;
color.a = alpha;
backgroundSelectedWord.color = color;
}
}
private void ForceUpdateLayout(RectTransform layoutRectTransform)
{
LayoutRebuilder.ForceRebuildLayoutImmediate(layoutRectTransform);
}
private void UpdateHorizontalLayout(HorizontalLayoutGroup layout)
{
ForceUpdateLayout(layout.GetComponent<RectTransform>());
}
public void RearrangeRandomLetters()
{
if (parentLetters == null) return;
// Get all letter buttons
var letterButtons = parentLetters.GetComponentsInChildren<LetterButton>();
if (letterButtons.Length < 2) return;
int swapCount = letterButtons.Length;
var lettersToSwap = letterButtons;
// Create a list of positions and shuffle them
var positions = lettersToSwap.Select(btn => btn.GetComponent<RectTransform>().anchoredPosition).ToList();
// Shuffle positions using Fisher-Yates algorithm
for (int i = positions.Count - 1; i > 0; i--)
{
int randomIndex = UnityEngine.Random.Range(0, i + 1);
Vector2 temp = positions[i];
positions[i] = positions[randomIndex];
positions[randomIndex] = temp;
}
// Create a single sequence for all animations
var sequence = DOTween.Sequence();
// Add all animations to the sequence simultaneously using a single Join group
var joinGroup = sequence.Join(DOTween.Sequence());
for (int i = 0; i < lettersToSwap.Length; i++)
{
joinGroup.Join(
lettersToSwap[i].GetComponent<RectTransform>()
.DOAnchorPos(positions[i], swapAnimationDuration)
.SetEase(swapAnimationEase)
);
}
sequence.Play();
}
private List<LetterButton> GetRandomLetters(LetterButton[] allLetters, int count)
{
List<LetterButton> randomLetters = new List<LetterButton>();
List<int> availableIndices = new List<int>();
// Initialize available indices
for (int i = 0; i < allLetters.Length; i++)
{
availableIndices.Add(i);
}
// Pick random letters
for (int i = 0; i < count && availableIndices.Count > 0; i++)
{
int randomIndex = UnityEngine.Random.Range(0, availableIndices.Count);
int selectedIndex = availableIndices[randomIndex];
randomLetters.Add(allLetters[selectedIndex]);
availableIndices.RemoveAt(randomIndex);
}
return randomLetters;
}
public List<LetterButton> GetLetters(string wordForTutorial)
{
var letters = parentLetters.GetComponentsInChildren<LetterButton>();
List<LetterButton> letterButtons = new List<LetterButton>();
List<LetterButton> usedLetters = new List<LetterButton>();
for (int i = 0; i < wordForTutorial.Length; i++)
{
var letter = wordForTutorial[i];
var availableLetter = letters.FirstOrDefault(l =>
l.GetLetter().ToLower() == letter.ToString().ToLower() &&
!usedLetters.Contains(l));
if (availableLetter != null)
{
letterButtons.Add(availableLetter);
usedLetters.Add(availableLetter);
}
else
{
letterButtons.Add(null);
}
}
return letterButtons;
}
public void Hide()
{
canvasGroup.DOFade(0f, 0.3f);
}
public void InstantHide()
{
canvasGroup.alpha = 0f;
}
public void HideForWin()
{
Hide();
}
public void Show()
{
canvasGroup.DOFade(1f, 0.3f);
}
/// <summary>
/// Manually set the VirtualMouseInput reference for controller support
/// </summary>
/// <param name="virtualMouse">The VirtualMouseInput component to use</param>
public void SetVirtualMouseInput(VirtualMouseInput virtualMouse)
{
virtualMouseInput = virtualMouse;
}
}
}

View File

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

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: fca769902e6c474d8f4b132521f640a9
timeCreated: 1745821503

View File

@ -0,0 +1,46 @@
// // ©2015 - 2025 Candy Smith
// // All rights reserved
// // Redistribution of this software is strictly not allowed.
// // Copy of this software can be obtained from unity asset store only.
// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// // FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
// // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// // THE SOFTWARE.
using WordsToolkit.Scripts.Levels;
using WordsToolkit.Scripts.NLP;
namespace WordsToolkit.Scripts.Gameplay.WordValidator
{
public class DefaultWordValidator : IWordValidator
{
private readonly IModelController modelController;
private readonly ICustomWordRepository customWordRepository;
private readonly Level levelData;
public DefaultWordValidator(IModelController modelController, ICustomWordRepository customWordRepository, Level levelData)
{
this.modelController = modelController;
this.customWordRepository = customWordRepository;
this.levelData = levelData;
}
public bool IsWordKnown(string word, string currentLanguage)
{
if (string.IsNullOrEmpty(word))
return false;
word = word.ToLower();
return (modelController != null && modelController.IsWordKnown(word, currentLanguage)) ||
(customWordRepository != null && customWordRepository.ContainsWord(word));
}
public bool IsExtraWordValid(string word, string language)
{
return customWordRepository.AddExtraWord(word);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d8d6a0260fc24efda1fc101f2cd8b6ba
timeCreated: 1745821510

View File

@ -0,0 +1,20 @@
// // ©2015 - 2025 Candy Smith
// // All rights reserved
// // Redistribution of this software is strictly not allowed.
// // Copy of this software can be obtained from unity asset store only.
// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// // FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
// // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// // THE SOFTWARE.
namespace WordsToolkit.Scripts.Gameplay.WordValidator
{
public interface IWordValidator
{
bool IsWordKnown(string word, string currentLanguage);
bool IsExtraWordValid(string word, string language);
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c58952109371407183beab35c1dcddb7
timeCreated: 1745821517