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; using Random = UnityEngine.Random; namespace Services { public class GameBoardService : IGameBoardService, ITickable, IDisposable { #region Inject private readonly IGameBoard gameBoard; private readonly GameVariables gameVariables; private readonly IMatchService matchService; private readonly IScoreService scoreService; private readonly IBombService bombService; private readonly IObjectPool objectPool; private readonly Transform gemsHolder; #endregion #region Variables private readonly List gemPresenters = new List(); private readonly ScorePresenter scorePresenter; private GameState currentState = GameState.Move; #endregion public GameBoardService(IGameBoard gameBoard, GameVariables gameVariables, IMatchService matchService, IScoreService scoreSerivce, IBombService bombService, IObjectPool objectPool, Transform gemsHolder, ScorePresenter scorePresenter) { this.gameBoard = gameBoard; this.gameVariables = gameVariables; this.matchService = matchService; this.scoreService = scoreSerivce; this.bombService = bombService; this.objectPool = objectPool; this.gemsHolder = gemsHolder; this.scorePresenter = scorePresenter; } public void Tick() { foreach (GemPresenter gemPresenter in gemPresenters) { gemPresenter.Tick(this.gameVariables.gemSpeed); } this.scorePresenter.Tick(this.gameVariables.scoreSpeed); } //Instantiates background tiles and calls SpawnGems //Uses MatchService.MatchesAt to avoid matching Gems public void Setup() { for (int x = 0; x < this.gameBoard.Width; x++) for (int y = 0; y < this.gameBoard.Height; y++) { Vector2 position = new Vector2(x, y); GameObject backgroundTile = Object.Instantiate(this.gameVariables.bgTilePrefabs, position, Quaternion.identity); backgroundTile.transform.SetParent(this.gemsHolder); backgroundTile.name = "BG Tile - " + x + ", " + y; int gemToUse = RandomUtils.RandomGemTypeAsInt(); int iterations = 0; while (this.matchService.MatchesAt(new Vector2Int(x, y), (GemType)gemToUse) && iterations < 100) { gemToUse = RandomUtils.RandomGemTypeAsInt(); iterations++; } SpawnGem(new Vector2Int(x, y), (GemType)gemToUse); } this.currentState = GameState.Move; } //Uses the ObjectPool to spawn a gem at the given position private void SpawnGem(Vector2Int position, GemType gemType) { GemView gemView = this.objectPool.Get(gemType, position, this.gameVariables.dropHeight); gemView.name = "Gem - " + position.x + ", " + position.y + ' ' + gemType; // If we randomly spawned a bomb, give it a random color group (so it can match by color). int scoreValue = GemUtils.GetGemValues(gemType, this.gameVariables.gemsPrefabs).scoreValue; Gem gem = new Gem(gemType, position, scoreValue); gemView.Bind(gem); this.gemPresenters.Add(new GemPresenter(gem, gemView)); SetGem(new Vector2Int(position.x, position.y), gem); } private void SpawnBomb(Vector2Int position, GemType color) { // remove existing gem/view at that position DestroyMatchedGems(position); GemView gemView = this.objectPool.Get(GemType.Bomb, position, 0); gemView.name = "Bomb - " + position.x + ", " + position.y + ' ' + GemType.Bomb; int scoreValue = GemUtils.GetGemValues(color, this.gameVariables.gemsPrefabs).scoreValue; Gem bombGem = new Gem(GemType.Bomb, position, scoreValue, color); gemView.Bind(bombGem, isBomb: true); this.gemPresenters.Add(new GemPresenter(bombGem, gemView)); SetGem(position, bombGem); } //Sets the gem on the GameBoard private void SetGem(Vector2Int position, Gem gem) { this.gameBoard.SetGemAt(new Vector2Int(position.x, position.y), gem); } //Gets the gem from the GameBoard private Gem GetGem(Vector2Int position) { return this.gameBoard.GetGemAt(position); } //Listens to InputService OnSwapRequest public async UniTask TrySwap(Vector2Int from, Vector2Int to) { if (this.currentState != GameState.Move) return false; if (!InBounds(from) || !InBounds(to)) return false; if (!AreAdjacentCardinal(from, to)) return false; Gem fromGem = GetGem(from); Gem toGem = GetGem(to); if(fromGem == null || toGem == null) return false; this.currentState = GameState.Wait; ApplySwap(from, to, fromGem, toGem); await UniTask.Delay(600); this.matchService.SetLastSwap(from, to); this.matchService.FindAllMatches(); bool hasMatch = this.matchService.CurrentMatches.Count > 0; if (!hasMatch) { ApplySwap(to, from, fromGem, toGem); await UniTask.Delay(600); this.currentState = GameState.Move; return false; } List protectedPositions = ApplyPendingBombSpawns(); await DestroyMatchesAsync(protectedPositions); this.currentState = GameState.Move; return true; } private void ApplySwap(Vector2Int posA, Vector2Int posB, Gem gemA, Gem gemB) { // swap their stored positions gemA.SetPosition(posB); gemB.SetPosition(posA); // update grid SetGem(posA, gemB); SetGem(posB, gemA); } private List ApplyPendingBombSpawns() { List positions = new List(); foreach (BombSpawnRequest bomSpawnRequest in this.matchService.PendingBombSpawns) { positions.Add(bomSpawnRequest.Position); SpawnBomb(bomSpawnRequest.Position, bomSpawnRequest.Color); } this.matchService.ClearPendingBombs(); return positions; } private async UniTask DestroyMatchesAsync(List protectedPositions) { List matchPositions = new List(this.matchService.CurrentMatches.Count); for (int i = 0; i < this.matchService.CurrentMatches.Count; i++) { Gem match = this.matchService.CurrentMatches[i]; if (match == null) continue; Vector2Int pos = match.Position; if (protectedPositions != null && protectedPositions.Contains(pos)) continue; matchPositions.Add(pos); } IReadOnlyList bombCandidates = this.bombService.CollectTriggeredBombs(matchPositions); List initialBombs = new List(); foreach (Vector2Int p in bombCandidates) { if (!InBounds(p)) continue; if (protectedPositions != null && protectedPositions.Contains(p)) continue; Gem gem = GetGem(p); if (gem is { Type: GemType.Bomb }) initialBombs.Add(p); } initialBombs = initialBombs.Distinct().ToList(); // If a bomb is part of the match, do NOT destroy matching pieces immediately. // Let the bomb's manhattan-distance explosion destroy them in sequence. if (initialBombs.Count > 0) { await this.bombService.DetonateChainAsync( initialBombs, InBounds, GetGem, DestroyAtAsync, this.gameVariables.bombRadius, this.gameVariables.bombDelay); await MoveGemsDown(); return; } foreach (Vector2Int pos in matchPositions.Distinct().ToList()) { Gem gem = GetGem(pos); if (gem == null) continue; if (gem.Type == GemType.Bomb) continue; this.scoreService.ScoreCheck(gem.ScoreValue); DestroyMatchedGems(pos); } await this.bombService.DetonateChainAsync( initialBombs, InBounds, GetGem, DestroyAtAsync, this.gameVariables.bombRadius, this.gameVariables.bombDelay); await MoveGemsDown(); } private UniTask DestroyAtAsync(Vector2Int pos) { Gem gem = GetGem(pos); if (gem == null) return UniTask.CompletedTask; this.scoreService.ScoreCheck(gem.ScoreValue); DestroyMatchedGems(pos); return UniTask.CompletedTask; } private async UniTask MoveGemsDown() { await UniTask.Delay(50); int nullCounter = 0; 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) { nullCounter++; } else if (nullCounter > 0) { currentGem.SetPosition(new Vector2Int(currentGem.Position.x, currentGem.Position.y - nullCounter)); SetGem(currentGem.Position, currentGem); SetGem(new Vector2Int(x,y), null); } } nullCounter = 0; } await FillBoard(); } private async UniTask FillBoard() { await UniTask.Delay(250); RefillBoard(); await UniTask.Delay(600); this.matchService.FindAllMatches(); if (this.matchService.CurrentMatches.Count > 0) { await UniTask.Delay(600); // In cascades, there is no "creating slot" bomb protection. await DestroyMatchesAsync(new List()); } else { await UniTask.Delay(250); this.currentState = GameState.Move; } } private void RefillBoard() { 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) { int gemToUse = RandomUtils.RandomGemTypeAsInt(); SpawnGem(new Vector2Int(x, y), (GemType)gemToUse); } } } } private void DestroyMatchedGems(Vector2Int position) { List gemsViews = GemsViews(); Gem currentGem = this.gameBoard.GetGemAt(position); if (currentGem != null) { GemView gemView = gemsViews.FirstOrDefault(gv => gv.Gem == currentGem); if (gemView is null) { return; } this.objectPool.Release(gemView); RemovePresenterFor(gemView); SetGem(position, null); } } #region Utils private void RemovePresenterFor(GemView gemView) { if (gemView is null) { return; } GemPresenter presenter = this.gemPresenters.FirstOrDefault(p => p.GemView == gemView); this.gemPresenters.Remove(presenter); } private List GemsViews() { return this.gemsHolder.GetComponentsInChildren().ToList(); } private bool InBounds(Vector2Int p) { return p.x >= 0 && p.x < this.gameBoard.Width && p.y >= 0 && p.y < this.gameBoard.Height; } private static bool AreAdjacentCardinal(Vector2Int a, Vector2Int b) { Vector2Int d = b - a; return (Mathf.Abs(d.x) == 1 && d.y == 0) || (Mathf.Abs(d.y) == 1 && d.x == 0); } #endregion public void Dispose() { this.objectPool.Clear(); } } }