Files
WordConnect/Assets/WordConnectGameToolkit/Scripts/Gameplay/Managers/FieldManager.cs

1131 lines
44 KiB
C#

// // ©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 System.Collections.Generic;
using System.Linq;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
using WordsToolkit.Scripts.Levels;
using DG.Tweening;
using VContainer;
using VContainer.Unity;
using WordsToolkit.Scripts.Audio;
using WordsToolkit.Scripts.Enums;
using WordsToolkit.Scripts.Gameplay.WordValidator;
using WordsToolkit.Scripts.GUI;
using WordsToolkit.Scripts.GUI.Buttons;
using WordsToolkit.Scripts.Infrastructure.Service;
using WordsToolkit.Scripts.NLP;
using WordsToolkit.Scripts.System;
using WordsToolkit.Scripts.Utilities;
namespace WordsToolkit.Scripts.Gameplay.Managers
{
public class FieldManager : MonoBehaviour, IHideableForWin, IShowable
{
public Tile tile;
public int gridSize = 20;
public float tileSize = 50f;
public float spacing = 5f;
public CrosswordGenerationConfigSO crosswordConfig;
private char[,] grid;
private List<WordPlacement> placedWords = new List<WordPlacement>();
private Dictionary<Vector2Int, Tile> tileMap = new Dictionary<Vector2Int, Tile>();
public List<Tile> allTiles = new List<Tile>();
public TextMeshProUGUI characterPrefab;
private Queue<TextMeshProUGUI> characterPool = new Queue<TextMeshProUGUI>();
private HashSet<string> openedWords = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
private Level levelData;
[SerializeField]
private Transform extraWordPositionTransform;
[SerializeField]
private Transform specialItemsContainer;
[SerializeField]
private GameObject defaultSpecialItemPrefab;
public UnityEvent OnAllTilesOpened = new UnityEvent();
public UnityEvent OnAllRequiredWordsFound = new UnityEvent();
public UnityEvent<string> OnExtraWordFound = new UnityEvent<string>();
private IWordValidator wordValidator;
private IGameStateManager gameStateManager;
private GameManager gameManager;
private IModelController modelController;
private ICustomWordRepository customWordRepo;
private IObjectResolver resolver;
private IExtraWordService extraWordService;
[SerializeField]
private WordBubble wordBubblePrefab;
[SerializeField]
private Transform extraWordAlreadyFoundPosition;
private IAudioService audioService;
[SerializeField]
private CanvasGroup canvasGroup;
[SerializeField]
private AudioClip extrawordSound;
[SerializeField]
private RectTransform fieldRect;
[Inject]
public void Construct(
GameManager gameManager,
IModelController modelController,
ICustomWordRepository customWordRepo,
IObjectResolver resolver,
IExtraWordService extraWordService,
IAudioService audioService, ButtonViewController buttonViewController)
{
this.gameManager = gameManager;
this.modelController = modelController;
this.customWordRepo = customWordRepo;
this.resolver = resolver;
this.extraWordService = extraWordService;
this.audioService = audioService;
buttonViewController.RegisterButton(this);
}
public TextMeshProUGUI GetPooledCharacter()
{
if (characterPool.Count > 0)
{
var character = characterPool.Dequeue();
character.transform.SetAsLastSibling();
character.gameObject.SetActive(true);
return character;
}
var newChar = Instantiate(characterPrefab, transform);
return newChar;
}
// Return character to the pool
public void ReturnToPool(TextMeshProUGUI character)
{
if (character == null) return;
character.gameObject.SetActive(false);
characterPool.Enqueue(character);
}
public void Generate(Level levelData, string language)
{
wordValidator = new DefaultWordValidator(modelController, customWordRepo, levelData);
gameStateManager = new DefaultGameStateManager(gameManager, levelData);
this.levelData = levelData;
var words = levelData.GetWords(language);
if (words == null || words.Length == 0)
return;
// Add level words to custom repository so they are recognized even if not in model
customWordRepo.InitWords(words, language);
// Clear opened words when generating a new level
openedWords.Clear();
// Clear any existing tiles
foreach (Transform child in transform)
{
Destroy(child.gameObject);
}
// Initialize collections
tileMap.Clear();
allTiles.Clear();
// First, try to load saved crossword data from the level
var languageData = levelData.GetLanguageData(language);
bool useSavedData = false;
if (languageData != null && languageData.crosswordData != null)
{
useSavedData = LoadSavedCrosswordData(languageData.crosswordData, words);
}
// If we couldn't load saved data, generate a new crossword
if (!useSavedData)
{
// Load config if not set
if (crosswordConfig == null)
{
crosswordConfig = Resources.Load<CrosswordGenerationConfigSO>("Settings/CrosswordConfig");
}
// Use the CrosswordGenerator with configuration
bool success;
if (crosswordConfig != null)
{
// Use configuration-based generation
success = CrosswordGenerator.RegenerateCrossword(words, crosswordConfig, out grid, out placedWords);
}
else
{
// Fallback to legacy method for backward compatibility
success = CrosswordGenerator.GenerateCrossword(words, gridSize, out grid, out placedWords);
}
if (!success)
{
Debug.LogError("Could not place any words. Check word list.");
return;
}
}
// Calculate center offset for the grid
CrosswordGenerator.CalculateGridBounds(grid, out Vector2Int min, out Vector2Int max);
Vector2Int gridCenter = new Vector2Int(
(min.x + max.x) / 2,
(min.y + max.y) / 2
);
// Start coroutine to wait for layout calculation before creating visual grid
StartCoroutine(CreateVisualGridWhenReady(gridCenter, levelData));
}
private IEnumerator CreateVisualGridWhenReady(Vector2Int gridCenter, Level levelData)
{
// Wait for the end of frame to ensure layout calculations are completed
yield return new WaitForEndOfFrame();
// Wait one more frame to be extra sure
yield return null;
// Force layout rebuild
LayoutRebuilder.ForceRebuildLayoutImmediate(GetComponent<RectTransform>());
// Wait one more frame after forcing rebuild
yield return null;
// Now create the visual grid
CreateVisualGrid(gridCenter, levelData);
foreach (Tile tile in allTiles)
{
tile.SetTileClosed();
}
}
private void CreateVisualGrid(Vector2Int gridCenter, Level levelData)
{
// Calculate the used grid dimensions
CrosswordGenerator.CalculateGridBounds(grid, out Vector2Int minBounds, out Vector2Int maxBounds);
int gridWidth = maxBounds.x - minBounds.x + 1;
int gridHeight = maxBounds.y - minBounds.y + 1;
// Get the field's RectTransform to determine available space
RectTransform fieldRect = GetComponent<RectTransform>();
// Get the parent canvas scale
float canvasScale = 1f;
Canvas parentCanvas = GetComponentInParent<Canvas>();
if (parentCanvas != null && parentCanvas.renderMode == RenderMode.ScreenSpaceOverlay)
{
canvasScale = parentCanvas.scaleFactor;
}
// Calculate available space with margins
float totalMargin = 20f * 2; // 20 pixels margin on each side
float availableWidth = fieldRect.rect.width - totalMargin;
float availableHeight = fieldRect.rect.height - totalMargin;
// Ensure we have valid dimensions, otherwise use fallback values
if (availableWidth <= 0 || availableHeight <= 0)
{
availableWidth = 1000f;
availableHeight = 1000f;
}
// Calculate cell size based on available space and grid dimensions
float cellSize = Mathf.Min(
availableWidth / gridWidth,
availableHeight / gridHeight
);
// Apply reasonable limits
cellSize = Mathf.Clamp(cellSize, 40f, 300);
// Calculate spacing as a percentage of cell size
spacing = cellSize * -0.01f; // 10% of cell size
// Get original tile size from prefab for positioning calculations
RectTransform tilePrefabRect = tile.GetComponent<RectTransform>();
float originalTileSize = tilePrefabRect != null ? tilePrefabRect.sizeDelta.x : 50f; // fallback to 50f
// Calculate grid positioning offsets using original tile size
float totalWidth = gridWidth * originalTileSize + (gridWidth - 1) * spacing;
float totalHeight = gridHeight * originalTileSize + (gridHeight - 1) * spacing;
float startX = -totalWidth / 2f;
float startY = totalHeight / 2f;
// Fill the grid with tiles or placeholders
int gridX = 0, gridY = 0;
for (int y = minBounds.y; y <= maxBounds.y; y++)
{
gridX = 0;
for (int x = minBounds.x; x <= maxBounds.x; x++)
{
if (grid[x, y] != 0)
{
// Create a tile with letter using VContainer
Tile newTile = resolver.Instantiate(tile, transform);
newTile.SetColors(levelData.colorsTile);
newTile.SetCharacter(grid[x, y]);
// Position tile manually without changing its size
RectTransform tileRect = newTile.GetComponent<RectTransform>();
// Keep original tile size, just position it
float posX = startX + gridX * (originalTileSize + spacing);
float posY = startY - gridY * (originalTileSize + spacing);
tileRect.anchoredPosition = new Vector2(posX, posY);
// Store tile in the map
tileMap[new Vector2Int(x, y)] = newTile;
allTiles.Add(newTile);
}
gridX++;
}
gridY++;
}
// Force canvas update
// Calculate center of the created tiles grid
if (allTiles.Count > 0)
{
// Find the actual bounds of placed tiles
float minX = float.MaxValue, maxX = float.MinValue;
float minY = float.MaxValue, maxY = float.MinValue;
foreach (var tile in allTiles)
{
Vector2 tilePos = tile.GetComponent<RectTransform>().anchoredPosition;
minX = Mathf.Min(minX, tilePos.x);
maxX = Mathf.Max(maxX, tilePos.x);
minY = Mathf.Min(minY, tilePos.y);
maxY = Mathf.Max(maxY, tilePos.y);
}
// Calculate the actual center of the tiles grid
Vector2 tilesGridCenter = new Vector2((minX + maxX) / 2f, (minY + maxY) / 2f);
// Get the field's rect center (use actual rect center, not zero)
Vector2 fieldCenter = fieldRect.rect.center; // Use actual field rect center
// Calculate the offset needed to center the grid precisely
Vector2 centerOffset = fieldCenter - tilesGridCenter;
// Apply the centering offset to each individual tile instead of moving the entire transform
foreach (var tile in allTiles)
{
RectTransform tileRect = tile.GetComponent<RectTransform>();
tileRect.anchoredPosition += centerOffset;
}
// Calculate the scale needed to fit the grid into the field rect
float actualGridWidth = maxX - minX + originalTileSize; // Add tile size to account for tile dimensions
float actualGridHeight = maxY - minY + originalTileSize;
// Calculate available space with margins
float marginPercent = 0.001f; // 10% margin
float usableWidth = fieldRect.rect.width * (1f - marginPercent);
float usableHeight = fieldRect.rect.height * (1f - marginPercent);
// Calculate scale factors for both dimensions
float scaleX = usableWidth / actualGridWidth;
float scaleY = usableHeight / actualGridHeight;
// Use the smaller scale to ensure the grid fits in both dimensions
float finalScale = Mathf.Min(scaleX, scaleY);
// Apply reasonable limits to prevent extreme scaling
finalScale = Mathf.Clamp(finalScale, 0.1f, 3.0f);
// Apply the scale to the transform
transform.localScale = Vector3.one * finalScale;
// After scaling, apply a final centering correction for maximum precision
RectTransform thisRect = GetComponent<RectTransform>();
// Recalculate actual centers after scaling
float finalMinX = float.MaxValue, finalMaxX = float.MinValue;
float finalMinY = float.MaxValue, finalMaxY = float.MinValue;
foreach (var tile in allTiles)
{
Vector3 globalTilePos = tile.GetComponent<RectTransform>().TransformPoint(Vector3.zero);
finalMinX = Mathf.Min(finalMinX, globalTilePos.x);
finalMaxX = Mathf.Max(finalMaxX, globalTilePos.x);
finalMinY = Mathf.Min(finalMinY, globalTilePos.y);
finalMaxY = Mathf.Max(finalMaxY, globalTilePos.y);
}
Vector3 finalTilesGlobalCenter = new Vector3((finalMinX + finalMaxX) / 2f, (finalMinY + finalMaxY) / 2f, 0);
Vector3 finalFieldGlobalCenter = thisRect.TransformPoint(fieldCenter);
// Apply micro-adjustment to the entire transform if needed
Vector3 finalCenteringOffset = finalFieldGlobalCenter - finalTilesGlobalCenter;
if (finalCenteringOffset.magnitude > 0.01f) // Only adjust if difference is significant
{
// Convert global offset back to local space and apply
Vector3 localOffset = thisRect.InverseTransformDirection(finalCenteringOffset);
thisRect.anchoredPosition += new Vector2(localOffset.x, localOffset.y);
}
}
// Associate tiles with their words
AssociateTilesWithWords();
// Store the current tile size for later reference
tileSize = cellSize;
}
private void AssociateTilesWithWords()
{
// For each word placement, find and store its tiles
foreach (var wordPlacement in placedWords)
{
var tilesList = wordPlacement.tiles as List<Tile>;
tilesList?.Clear(); // Clear without casting to avoid type errors
// Create a new typed list and copy to the object list
List<Tile> typedTiles = new List<Tile>();
for (int i = 0; i < wordPlacement.word.Length; i++)
{
int x = wordPlacement.isHorizontal ? wordPlacement.startPosition.x + i : wordPlacement.startPosition.x;
int y = wordPlacement.isHorizontal ? wordPlacement.startPosition.y : wordPlacement.startPosition.y + i;
Vector2Int pos = new Vector2Int(x, y);
if (tileMap.TryGetValue(pos, out Tile tile))
{
typedTiles.Add(tile);
tilesList?.Add(tile);
}
}
}
}
private void ShakeOpenedWordTiles(WordPlacement wordPlacement)
{
foreach (var tileObj in wordPlacement.tiles)
{
Tile tile = tileObj as Tile;
if (tile == null)
continue;
tile.ShakeTile();
}
}
public bool IsWordOpen(string word)
{
return openedWords.Contains(word);
}
private void AnimateCharacters(WordPlacement wordPlacement, List<Vector3> letterPositions)
{
// Convert the generic object list to typed Tile list
var tiles = wordPlacement.tiles.Cast<Tile>().ToList();
int minCount = Mathf.Min(tiles.Count, letterPositions.Count);
for (int i = 0; i < minCount; i++)
{
var fromPos = letterPositions[i];
Tile tile = tiles[i];
if (tile == null) continue;
// Get character from pool
TextMeshProUGUI animChar = GetPooledCharacter();
animChar.text = tile.character.text;
var duration = 0.3f;
// Set initial position and make visible
RectTransform rectTransform = animChar.GetComponent<RectTransform>();
rectTransform.position = fromPos;
// Store original scale
Vector3 originalScale = rectTransform.localScale;
// Create animation sequence
Sequence animSequence = DOTween.Sequence();
// Initial slight scale up with bounce effect
animSequence.Append(rectTransform.DOScale(originalScale * 1.2f, 0.2f)
.SetEase(Ease.OutBack));
// Movement animation
Tween moveTween = rectTransform.DOMove(tile.transform.position, duration)
.SetDelay(0.1f * i)
.SetEase(Ease.InOutQuad);
animSequence.Append(moveTween);
// Create custom scale animation that peaks at the middle of the movement
float scaleUpTime = duration / 2;
float scaleDownTime = duration / 2;
// Scale up during first half of movement
animSequence.Join(rectTransform.DOScale(originalScale * 4.5f, scaleUpTime)
.SetEase(Ease.OutQuad));
// Scale down during second half of movement while also fading out (happening simultaneously)
animSequence.Append(rectTransform.DOScale(originalScale, scaleDownTime)
.SetEase(Ease.InQuad));
animSequence.Join(animChar.DOFade(0, scaleDownTime).SetEase(Ease.InQuad));
// Capture the current tile and animChar in a local closure to ensure correct references
TextMeshProUGUI currentAnimChar = animChar;
Tile currentTile = tile;
var isLastCharacter = (i == minCount - 1);
// When this specific character's animation completes
animSequence.OnComplete(() => {
currentTile.SetTileOpen();
Color textColor = currentAnimChar.color;
textColor.a = 1f;
currentAnimChar.color = textColor;
audioService.PlayOpenWord();
ReturnToPool(currentAnimChar);
// Check if all words are open after the last character animation
if (isLastCharacter)
{
EventManager.GetEvent(EGameEvent.WordAnimated).Invoke();
CheckAllTilesOpened();
}
});
}
// Make sure any remaining tiles are opened if we don't have positions for them
for (int i = minCount; i < tiles.Count; i++)
{
tiles[i]?.SetTileOpen();
}
}
// New method that validates a word with the ModelController and opens it if valid
public bool ValidateWord(string word, List<Vector3> letterPositions = null)
{
if (string.IsNullOrEmpty(word))
return false;
if (!wordValidator.IsWordKnown(word.ToLower(), gameStateManager.CurrentLanguage))
return false;
bool wasOpened = false;
// Find the word in the placed words list
WordPlacement wordPlacement = placedWords?.FirstOrDefault(w => w.word.Equals(word, StringComparison.OrdinalIgnoreCase));
// Check if the word is already open
bool alreadyOpen = IsWordOpen(word);
// Case 1: Word exists on the field and is already open - shake it
if (alreadyOpen && wordPlacement != null)
{
ShakeOpenedWordTiles(wordPlacement);
return true;
}
// Case 2: Word exists on the field but is not open - animate to tiles
if (wordPlacement != null)
{
openedWords.Add(word);
if (letterPositions != null && letterPositions.Count > 0)
{
AnimateCharacters(wordPlacement, letterPositions);
}
else
{
foreach (var tileObj in wordPlacement.tiles)
{
if (tileObj is Tile tile)
{
tile.SetTileOpen();
}
}
}
wasOpened = true;
var levelWords = gameStateManager.GetLevelWords();
if (levelWords != null)
{
var allRequiredWords = new HashSet<string>(levelWords, StringComparer.OrdinalIgnoreCase);
}
}
// Case 3: Word is known but not on the field or is slang - it's an extra word
else
{
var levelWords = gameStateManager.GetLevelWords();
bool isExtraWord = (levelWords == null || !levelWords.Contains(word, StringComparer.OrdinalIgnoreCase));
if (isExtraWord && letterPositions != null && letterPositions.Count > 0)
{
OnExtraWordFound?.Invoke(word);
if(customWordRepo.AddExtraWord(word))
{
AnimateExtraWord(word, letterPositions);
wasOpened = true;
}
else
{
ShakeWord(word);
audioService.PlayWrong();
}
}
}
if (wasOpened && wordPlacement != null)
{
foreach (var tileObj in wordPlacement.tiles)
{
if (tileObj is Tile tile && tile.HasSpecialItem(out Vector2Int itemPosition))
{
StartCoroutine(CollectSpecialItemDelayed(itemPosition, 0.5f));
}
}
}
return wasOpened || alreadyOpen;
}
private void ShakeWord(string word)
{
// Implement the shake animation or effect here
var wordBubbleObject = Instantiate(wordBubblePrefab, transform);
wordBubbleObject.transform.position = extraWordAlreadyFoundPosition.position;
wordBubbleObject.SetWord(word);
}
// New method to animate extra words (words not on the field)
private void AnimateExtraWord(string word, List<Vector3> letterPositions)
{
if (letterPositions == null || letterPositions.Count == 0)
return;
// Use the extraWordPositionTransform if available, otherwise fall back to hardcoded position
Vector3 targetPosition;
if (extraWordPositionTransform != null)
{
targetPosition = extraWordPositionTransform.position;
}
else
{
// Fallback to hardcoded position
targetPosition = new Vector3(Screen.width * 0.5f, Screen.height * 0.85f, 0);
Debug.LogWarning("extraWordPositionTransform not assigned! Using default screen position.");
}
audioService.PlayDelayed(extrawordSound, 0.5f);
for (int i = 0; i < word.Length && i < letterPositions.Count; i++)
{
var fromPos = letterPositions[i];
char letter = word[i];
// Get character from pool
TextMeshProUGUI animChar = GetPooledCharacter();
animChar.text = letter.ToString();
var duration = 0.7f;
// Set initial position and make visible
RectTransform rectTransform = animChar.GetComponent<RectTransform>();
rectTransform.position = fromPos;
// Store original scale
Vector3 originalScale = rectTransform.localScale;
// Create animation sequence
Sequence animSequence = DOTween.Sequence();
// Initial slight scale up with bounce effect
animSequence.Append(rectTransform.DOScale(originalScale * 1.2f, 0.2f)
.SetEase(Ease.OutBack));
// Calculate a mid-point for the arc
Vector3 midPoint = (fromPos + targetPosition) / 2f;
// Determine arc height based on the distance between points
float arcHeight = Vector3.Distance(fromPos, targetPosition) * 0.3f; // 30% of the distance
midPoint.y += arcHeight; // In screen space, higher Y is lower on screen
// Create a path array with control points for the arc
Vector3[] arcPath = new Vector3[] {
fromPos,
midPoint,
targetPosition
};
// Calculate half duration for scaling
float halfDuration = duration / 2f;
// Create path animation
var pathTween = rectTransform.DOPath(
arcPath,
duration,
PathType.CatmullRom)
.SetDelay(0.1f * i) // Stagger the animations
.SetEase(Ease.OutQuad);
// Add the path animation to the sequence
animSequence.Append(pathTween);
// Scale down during the second half
animSequence.Join(rectTransform.DOScale(originalScale * 0.1f, halfDuration).SetDelay(.2f)
.SetEase(Ease.InQuad));
// Quick fade out at the end
animSequence.Join(animChar.DOFade(0, 0.3f).SetEase(Ease.InQuad));
// Capture the current animChar in a closure
TextMeshProUGUI currentAnimChar = animChar;
// When animation completes
animSequence.OnComplete(() => {
// Reset alpha
Color textColor = currentAnimChar.color;
textColor.a = 1f;
currentAnimChar.color = textColor;
currentAnimChar.transform.localScale = originalScale;
// Return to pool
ReturnToPool(currentAnimChar);
// the last character will trigger the extra word found event
if(currentAnimChar.text == word.First().ToString())
{
EventManager.GetEvent<string>(EGameEvent.ExtraWordFound).Invoke(word);
}
});
}
}
// Enhanced generation method that accepts special item placements
public void GenerateWithSpecialItems(Level levelData, string language, List<SerializableWordPlacement> specialItemPlacements)
{
// First attempt to load or generate the basic crossword
Generate(levelData, language);
// Early exit if generation failed (no grid)
if (grid == null)
{
Debug.LogError("Failed to generate or load crossword grid");
return;
}
// Check if we're using a loaded crossword or a newly generated one
var languageData = levelData.GetLanguageData(language);
bool isLoadedCrossword = (languageData != null &&
languageData.crosswordData != null &&
languageData.crosswordData.grid != null);
// Then add special items
if (specialItemPlacements != null && specialItemPlacements.Count > 0)
{
// Filter out non-special items for clarity
var onlySpecialItems = specialItemPlacements.Where(p => p.isSpecialItem).ToList();
if (onlySpecialItems.Count > 0)
{
StartCoroutine(AddSpecialItemsWhenReady(onlySpecialItems));
}
}
}
// Overloaded method to work with the new SerializableSpecialItem format
public void GenerateWithSpecialItems(Level levelData, string language, List<SerializableSpecialItem> specialItems)
{
// First attempt to load or generate the basic crossword
Generate(levelData, language);
// Early exit if generation failed (no grid)
if (grid == null)
{
Debug.LogError("Failed to generate or load crossword grid");
return;
}
// Then add special items from the new format
if (specialItems != null && specialItems.Count > 0)
{
StartCoroutine(AddSpecialItemsWhenReady(specialItems));
}
}
// Coroutine to add special items after the grid is fully generated
private IEnumerator AddSpecialItemsWhenReady(List<SerializableWordPlacement> specialItemPlacements)
{
// Wait until the main grid is fully built
yield return new WaitUntil(() => allTiles.Count > 0 && tileMap.Count > 0);
// Add each special item
foreach (var placement in specialItemPlacements)
{
if (!placement.isSpecialItem) continue;
// Only place special items above letters
Vector2Int position = placement.startPosition;
// Check if we have a tile at this position
if (!tileMap.TryGetValue(position, out Tile tile))
{
Debug.LogWarning($"Cannot place special item at {position}: No letter tile found.");
continue;
}
// Just tell the tile to create a special item - it has its own prefab
tile.AssociateSpecialItem(position);
}
}
// Coroutine to add special items from the new SerializableSpecialItem format
private IEnumerator AddSpecialItemsWhenReady(List<SerializableSpecialItem> specialItems)
{
// Wait until the main grid is fully built
yield return new WaitUntil(() => allTiles.Count > 0 && tileMap.Count > 0);
// Add each special item
foreach (var specialItem in specialItems)
{
// Only place special items above letters
Vector2Int position = specialItem.position;
// Check if we have a tile at this position
if (!tileMap.TryGetValue(position, out Tile tile))
{
Debug.LogWarning($"Cannot place special item at {position}: No letter tile found.");
continue;
}
// Tell the tile to create a special item - it has its own prefab
tile.AssociateSpecialItem(position);
}
}
// Updated method to load saved crossword data with better logging
private bool LoadSavedCrosswordData(SerializableCrosswordData savedData, string[] words)
{
try
{
// Deserialize grid from saved data
savedData.DeserializeGrid();
if (savedData.grid == null)
{
Debug.LogWarning("Saved grid data couldn't be loaded (grid is null).");
return false;
}
// Additional validation - check grid dimensions
if (savedData.grid.GetLength(0) <= 0 || savedData.grid.GetLength(1) <= 0)
{
Debug.LogWarning($"Invalid grid dimensions: {savedData.grid.GetLength(0)}x{savedData.grid.GetLength(1)}");
return false;
}
// Set the grid
grid = savedData.grid;
// Filter out special items from placements to get just words
var wordPlacements = savedData.placements;
// Convert saved placements to runtime placements
placedWords = new List<WordPlacement>();
foreach (var savedPlacement in wordPlacements)
{
WordPlacement placement = new WordPlacement
{
word = savedPlacement.word,
wordNumber = savedPlacement.wordNumber,
startPosition = savedPlacement.startPosition,
isHorizontal = savedPlacement.isHorizontal,
tiles = new List<Tile>()
};
placedWords.Add(placement);
}
// Store special items for later processing (after visual grid is created)
if (savedData.specialItems != null && savedData.specialItems.Count > 0)
{
// We'll add these special items after the visual grid is created
StartCoroutine(AddSpecialItemsFromSavedData(savedData.specialItems));
}
// Verify the words match what we expect
var savedWords = placedWords.Select(p => p.word.ToLower()).ToArray();
var levelWords = words.Select(w => w.ToLower()).ToArray();
return true;
}
catch (Exception ex)
{
Debug.LogError($"Failed to load saved crossword data: {ex.Message}");
return false;
}
}
// Coroutine to add special items from saved crossword data
private IEnumerator AddSpecialItemsFromSavedData(List<SerializableSpecialItem> specialItems)
{
// Wait until the visual grid is fully created
yield return new WaitUntil(() => allTiles.Count > 0 && tileMap.Count > 0);
// Add each special item
foreach (var specialItem in specialItems)
{
Vector2Int position = specialItem.position;
// Check if we have a tile at this position
if (!tileMap.TryGetValue(position, out Tile tile))
{
Debug.LogWarning($"Cannot place special item at {position}: No letter tile found.");
continue;
}
// Tell the tile to create a special item
tile.AssociateSpecialItem(position);
}
}
// Coroutine to collect special item with a delay
private IEnumerator CollectSpecialItemDelayed(Vector2Int position, float delay)
{
yield return new WaitForSeconds(delay);
// Check if the tile still has a special item
if (tileMap.TryGetValue(position, out Tile tile) && tile.HasSpecialItem(out _))
{
// The tile handles the animation and collection itself now via the SpecialItem component
// We don't need to do anything here as it will be handled automatically
// when the tile is opened and the special item is animated.
}
}
/// <summary>
/// Opens a random closed tile on the field
/// </summary>
/// <returns>True if a tile was opened, false if no closed tiles remain</returns>
public bool OpenRandomTile()
{
// Get list of all tiles that are not yet open
var closedTiles = allTiles.Where(t => t != null && !t.IsOpen()).ToList();
if (closedTiles.Count > 0)
{
// Select random tile from closed tiles
int randomIndex = UnityEngine.Random.Range(0, closedTiles.Count);
var selectedTile = closedTiles[randomIndex];
selectedTile.SetTileOpen();
selectedTile.ShowEffect();
audioService.PlayBonus();
CheckAllTilesOpened();
return true;
}
return false;
}
/// <summary>
/// Opens the first closed tile found on the field
/// </summary>
/// <returns>True if a tile was opened, false if no closed tiles remain</returns>
public bool OpenFirstClosedTile()
{
var firstClosedTile = allTiles.FirstOrDefault(t => t != null && !t.IsOpen());
if (firstClosedTile != null)
{
firstClosedTile.SetTileOpen();
CheckAllTilesOpened();
return true;
}
return false;
}
/// <summary>
/// Gets the number of remaining closed tiles
/// </summary>
public int GetClosedTilesCount()
{
return allTiles.Count(t => t != null && !t.IsOpen());
}
public void Clear()
{
// Stop all running coroutines
StopAllCoroutines();
// Kill any active DOTween animations
DOTween.Kill(transform);
// Clear and destroy all tiles
foreach (Transform child in transform)
{
if (child != null)
{
// Kill any DOTween animations on the child
DOTween.Kill(child);
Destroy(child.gameObject);
}
}
// Clear collections
tileMap?.Clear();
allTiles?.Clear();
placedWords?.Clear();
openedWords?.Clear();
// Clear character pool
while (characterPool != null && characterPool.Count > 0)
{
var character = characterPool.Dequeue();
if (character != null)
{
DOTween.Kill(character.transform);
Destroy(character.gameObject);
}
}
characterPool = new Queue<TextMeshProUGUI>();
// Reset other variables
grid = null;
levelData = null;
tileSize = 50f; // Reset to default tile size
// Force immediate layout update
Canvas.ForceUpdateCanvases();
LayoutRebuilder.ForceRebuildLayoutImmediate(GetComponent<RectTransform>());
}
public bool AreAllTilesOpen()
{
if (allTiles == null) return false;
foreach (var tile in allTiles)
{
if (tile != null && !tile.IsOpen())
{
return false;
}
}
return true;
}
public void CheckAllTilesOpened()
{
if (allTiles == null) return;
bool allOpened = true;
foreach (var tile in allTiles)
{
if (tile != null && !tile.IsOpen())
{
allOpened = false;
break;
}
}
if (allOpened)
{
Debug.Log( "All tiles have been opened!");
OnAllTilesOpened?.Invoke();
}
}
private void OnEnable()
{
EventManager.GetEvent<Tile>(EGameEvent.TileSelected).Subscribe(OnTileSelected);
}
private void OnDisable()
{
EventManager.GetEvent<Tile>(EGameEvent.TileSelected).Unsubscribe(OnTileSelected);
}
private void OnTileSelected(Tile tile)
{
audioService.PlayBonus();
CheckAllTilesOpened();
}
public bool HasSpecialItems()
{
if (allTiles == null || allTiles.Count == 0)
return false;
// Check if any tile has a special item
foreach (var tile in allTiles)
{
if (tile.HasSpecialItem(out _))
return true;
}
return false;
}
public List<Tile> GetTilesWordWithSpecialItems(out string wordWithSpecialItems)
{
List<Tile> tilesWithSpecialItems = new List<Tile>();
wordWithSpecialItems = string.Empty;
bool gemFound = false;
foreach (var words in placedWords)
{
tilesWithSpecialItems.Clear(); // Clear previous results
foreach (var til in words.tiles)
{
tilesWithSpecialItems.Add(til);
if (til != null && til.HasSpecialItem(out _))
{
gemFound = true;
wordWithSpecialItems = words.word;
}
}
if (gemFound)
{
break; // Stop after finding the first special item
}
}
return tilesWithSpecialItems;
}
public List<Tile> GetTilesWord(string word)
{
var wordPlacement = placedWords.FirstOrDefault(w => w.word.Equals(word, StringComparison.OrdinalIgnoreCase));
if (wordPlacement != null)
{
return wordPlacement.tiles.ToList();
}
return new List<Tile>();
}
public void HideForWin()
{
canvasGroup.DOFade(0, .5f);
}
public void Show()
{
canvasGroup.DOFade(1, .5f);
}
}
}