using System; using System.Collections.Generic; using System.Linq; using Cysharp.Threading.Tasks; using Enums; using Models.Interfaces; using Presenter; using ScriptableObjects; using Services.Interfaces; using Structs; using UnityEngine; using Utils; using VContainer.Unity; using Views; using Object = UnityEngine.Object; namespace Services { public class GameBoardService : IGameBoardService, ITickable, IDisposable { #region Inject private readonly GameVariables gameVariables; private readonly IGameBoard gameBoard; private readonly IObjectPool objectPool; private readonly IMatchService matchService; private readonly IBombService bombService; private readonly IScoreService scoreService; private readonly ScorePresenter scorePresenter; private readonly AudioPresenter audioPresenter; private readonly Transform backgroundHolder; #endregion #region Variables private readonly List gemPresenters = new List(); private readonly Dictionary gemToView = new Dictionary(); private GameState currentState = GameState.Setup; #endregion public GameBoardService( GameVariables gameVariables, IGameBoard gameBoard, IObjectPool objectPool, IMatchService matchService, IBombService bombService, IScoreService scoreService, ScorePresenter scorePresenter, AudioPresenter audioPresenter, Transform backgroundHolder) { this.gameVariables = gameVariables; this.gameBoard = gameBoard; this.objectPool = objectPool; this.matchService = matchService; this.bombService = bombService; this.scoreService = scoreService; this.scorePresenter = scorePresenter; this.audioPresenter = audioPresenter; this.backgroundHolder = backgroundHolder; } /// /// Global tick. Update replacement of VContainer-managed objects. /// public void Tick() { foreach (GemPresenter gemPresenter in gemPresenters) { gemPresenter.Tick(this.gameVariables.gemSpeed); } this.scorePresenter.Tick(this.gameVariables.scoreSpeed); } /// /// Initiates the game board by placing gems randomly. /// public void Setup() { List gemsToSpawn = new List(); for (int x = 0; x < this.gameBoard.Width; x++) for (int y = 0; y < this.gameBoard.Height; y++) { Vector2 position = new Vector2(x, y); SpawnBackgroundTile(position); int iterations = 0; GemType gemToUse; do { gemToUse = RandomUtils.RandomGemType(); iterations++; } while (this.matchService.MatchesAt(position.ToVector2Int(), gemToUse) && iterations < 100); gemsToSpawn.Add(SetGemAt(position.ToVector2Int(), gemToUse)); } SpawnCascade(gemsToSpawn).Forget(); } /// /// Create a Gem object and place it on the GameBoard. /// /// /// Position on the GameBoard to place the Gem. /// /// /// Type of Gem to create. /// /// /// Whether the Gem is a Bomb or not. /// /// private Gem SetGemAt(Vector2Int position, GemType gemType, bool isBomb = false) { GemTypeValues gemValue = GemUtils.GetGemValues(gemType, this.gameVariables.gemsPrefabs); Gem gem = new Gem(isBomb ? GemType.Bomb : gemType, position, gemValue, gemType); this.gameBoard.SetGemAt(position, gem); return gem; } /// /// Spawns a GemView object and binds it to the Gem object. /// /// /// Gem to bind to the GemView. /// /// /// Whether the Gem is a Bomb or not. /// private void SpawnGemGameObject(Gem gem, bool isBomb = false) { GemView gemView = this.objectPool.Get(isBomb ? GemType.Bomb : gem.Type, gem.Position, isBomb ? 0 : this.gameVariables.dropHeight); gemView.name = "Gem - " + gem.Position.x + ", " + gem.Position.y + ' ' + gem.Type; GemTypeValues gemValue = GemUtils.GetGemValues(gem.MatchColor, this.gameVariables.gemsPrefabs); gemView.Bind(gem, gemValue, isBomb: isBomb); this.gemPresenters.Add(new GemPresenter(gem, gemView)); this.gemToView.Add(gem, gemView); } /// /// Calls SetGemAt and SpawnGemGameObject. /// /// /// Position on the GameBoard to place the Gem. /// /// /// Type of Gem to create. /// /// /// Whether the Gem is a Bomb or not. /// private void SetAndSpawnGem(Vector2Int position, GemType gemType, bool isBomb) { if(isBomb) ReleaseMatchedGems(position); SpawnGemGameObject(SetGemAt(position, gemType, isBomb), isBomb); } /// /// Spawns a background tile at the given position. Only for setup. /// /// /// Position on the GameBoard to place the tile. /// private void SpawnBackgroundTile(Vector2 position) { GameObject backgroundTile = Object.Instantiate(this.gameVariables.bgTilePrefabs, position, Quaternion.identity); backgroundTile.transform.SetParent(this.backgroundHolder); backgroundTile.name = "BG Tile - " + position.x + ", " + position.y; } /// /// Attempts to swap two gems on the GameBoard. /// /// /// Original position of the gem to swap. /// /// /// Destination position of the gem to swap. /// /// public async UniTask TrySwap(Vector2Int from, Vector2Int to) { if (this.currentState != GameState.Move) return false; if (!GemUtils.IsInBounds(from, this.gameBoard) || !GemUtils.IsInBounds(to, this.gameBoard)) return false; if (!from.IsAdjacent(to)) return false; this.currentState = GameState.Wait; ApplySwap(from, to); await UniTask.Delay(this.gameVariables.swapDelayMs); this.bombService.SetLastSwap(from, to); this.matchService.FindAllMatches(); this.bombService.DetectBombSpawnFromLastSwap(this.matchService.CurrentMatches); if (this.matchService.CurrentMatches.Count == 0) { ApplySwap(to, from); await UniTask.Delay(this.gameVariables.swapDelayMs); this.currentState = GameState.Move; return false; } List protectedPositions = this.bombService.ApplyPendingBombSpawns(SetAndSpawnGem); await DestroyMatchesAsync(protectedPositions); this.currentState = GameState.Move; return true; } /// /// For debug purposes. Switches the Gem at the given position to a different GemType. /// /// /// Position of the Gem to switch. /// /// public void TrySwitch(Vector2Int position) { Gem gem = this.gameBoard.GetGemAt(position); if(gem == null) return; GemType[] normalTypes = Enum.GetValues(typeof(GemType)) .Cast() .Where(t => t != GemType.Bomb) .ToArray(); if (normalTypes.Length == 0) return; bool nextIsBomb; GemType nextTypeOrMatchColor; if (gem.Type != GemType.Bomb) { int index = Array.IndexOf(normalTypes, gem.Type); if (index < 0) index = 0; if (index < normalTypes.Length - 1) { nextIsBomb = false; nextTypeOrMatchColor = normalTypes[index + 1]; } else { nextIsBomb = true; nextTypeOrMatchColor = normalTypes[0]; } } else { int idx = Array.IndexOf(normalTypes, gem.MatchColor); if (idx < 0) idx = 0; if (idx < normalTypes.Length - 1) { nextIsBomb = true; nextTypeOrMatchColor = normalTypes[idx + 1]; } else { nextIsBomb = false; nextTypeOrMatchColor = normalTypes[0]; } } // Replace both model+view by releasing current and respawning at the same position. ReleaseMatchedGems(position); SpawnGemGameObject(SetGemAt(position, nextTypeOrMatchColor, nextIsBomb), nextIsBomb); } /// /// Helper function for TrySwap. Swaps the Gems in the GameBoard. /// /// /// Original position of the gem to swap. /// /// /// Destination position of the gem to swap. /// private void ApplySwap(Vector2Int from, Vector2Int to) { Gem fromGem = this.gameBoard.GetGemAt(from); Gem toGem = this.gameBoard.GetGemAt(to); // swap their stored positions fromGem.SetPosition(to); toGem.SetPosition(from); // update grid this.gameBoard.SetGemAt(from, toGem); this.gameBoard.SetGemAt(to, fromGem); } /// /// Destroys all matches on the GameBoard. /// /// /// Gems we don't want to instantly destroy. /// private async UniTask DestroyMatchesAsync(List protectedPositions) { HashSet matchPositions = await this.matchService.GetMatchPositionsAsync(protectedPositions); List initialBombs = await this.bombService.GetInitialBombs(protectedPositions, matchPositions); if (initialBombs.Count > 0) { await this.bombService.DetonateChainAsync( initialBombs, DestroyAtAsync, this.gameBoard); foreach (Vector2Int p in matchPositions) await DestroyAtAsync(p); await UniTask.Delay(this.gameVariables.fillBoardDelayMs); await MoveGemsDown(); return; } // For audio SFX bool willBreakAnyNonBombGem = false; foreach (Vector2Int pos in matchPositions) { Gem g = this.gameBoard.GetGemAt(pos); if (g != null && g.Type != GemType.Bomb) { willBreakAnyNonBombGem = true; break; } } if (willBreakAnyNonBombGem) this.audioPresenter.OnMatch(this.gameVariables.matchSfx); // For score counting foreach (Vector2Int pos in matchPositions) { Gem gem = this.gameBoard.GetGemAt(pos); if (gem == null) continue; if (gem.Type == GemType.Bomb) continue; this.scoreService.AddScore(gem.ScoreValue); ReleaseMatchedGems(pos); } await UniTask.Delay(this.gameVariables.fillBoardDelayMs); await MoveGemsDown(); } /// /// Destroys the Gem at the given position. /// /// /// Position of the Gem to destroy. /// /// private UniTask DestroyAtAsync(Vector2Int pos) { Gem gem = this.gameBoard.GetGemAt(pos); if (gem == null) return UniTask.CompletedTask; if (gem.Type == GemType.Bomb) this.audioPresenter.OnBombExplosion(this.gameVariables.bombExplodeSfx); this.scoreService.AddScore(gem.ScoreValue); ReleaseMatchedGems(pos); return UniTask.CompletedTask; } /// /// Returns the GemView to the Object Pool and sets the Position in the GameBoard to null. /// /// /// Position of the Gem to release. /// private void ReleaseMatchedGems(Vector2Int position) { Gem currentGem = this.gameBoard.GetGemAt(position); if (currentGem != null) { if (!this.gemToView.TryGetValue(currentGem, out GemView gemView) || gemView == null) return; this.objectPool.Release(gemView); RemovePresenterFor(gemView); this.gameBoard.SetGemAt(position, null); } } /// /// Moves Gems Down to free spaces. /// private async UniTask MoveGemsDown() { List moves = new List(); while (true) { moves.Clear(); for (int x = 0; x < this.gameBoard.Width; x++) { for (int y = 1; y < this.gameBoard.Height; y++) { Vector2Int from = new Vector2Int(x, y); Vector2Int to = new Vector2Int(x, y - 1); Gem gem = this.gameBoard.GetGemAt(from); if (gem == null) continue; if (this.gameBoard.GetGemAt(to) != null) continue; moves.Add(new FallMove { from = from, to = to, gem = gem }); } } if (moves.Count == 0) break; foreach (FallMove move in moves) { this.gameBoard.SetGemAt(move.to, move.gem); this.gameBoard.SetGemAt(move.from, null); move.gem.SetPosition(move.to); } await UniTask.Delay(this.gameVariables.cascadeDelayMs); } await FillBoard(); } /// /// Fills the GameBoard with gems. If there are resulting matches, process them. /// private async UniTask FillBoard() { await RefillBoard(); this.matchService.FindAllMatches(); if (this.matchService.CurrentMatches.Count > 0) { await UniTask.Delay(this.gameVariables.fillBoardDelayMs); await DestroyMatchesAsync(new List()); } else { this.currentState = GameState.Move; } } /// /// Refills the GameBoard with gems. /// private async UniTask RefillBoard() { List gemsToSpawn = new List(); for (int x = 0; x < this.gameBoard.Width; x++) { for (int y = 0; y < this.gameBoard.Height; y++) { Gem currentGem = this.gameBoard.GetGemAt(new Vector2Int(x,y)); if (currentGem == null) { GemType gemToUse = RandomUtils.RandomGemType(); int iterations = 0; while (this.matchService.MatchesAt(new Vector2Int(x, y), gemToUse) && iterations < 100) { gemToUse = RandomUtils.RandomGemType(); iterations++; } gemsToSpawn.Add(SetGemAt(new Vector2Int(x,y), gemToUse)); } } } await SpawnCascade(gemsToSpawn); } /// /// Spawns gems in cascade. /// /// /// List of gems to spawn. /// private async UniTask SpawnCascade(List gemsToSpawn) { List> groups = gemsToSpawn .GroupBy(gem => gem.Position.y) .OrderBy(group => group.Key) .ToList(); for (int i = 0; i < groups.Count; i++) { foreach (Gem gem in groups[i]) SpawnGemGameObject(gem); if (i < groups.Count - 1) await UniTask.Delay(this.gameVariables.cascadeDelayMs); } if(this.currentState == GameState.Setup) this.currentState = GameState.Move; } /// /// Removes the Presenter for the given GemView from the list to avoid calling its Tick method. /// /// /// GemView to remove from the List of presenters. /// private void RemovePresenterFor(GemView gemView) { if (gemView is null) { return; } GemPresenter presenter = this.gemPresenters.FirstOrDefault(p => p.GemView == gemView); this.gemPresenters.Remove(presenter); } public void Dispose() { this.objectPool.Clear(); this.gemPresenters.Clear(); this.gemToView.Clear(); } } }