using System; using System.Collections.Generic; using System.Linq; using Cysharp.Threading.Tasks; using Enums; using Models.Interfaces; using ScriptableObjects; using Services.Interfaces; using Structs; using UnityEngine; using Utils; namespace Services { public class BombService : IBombService { private readonly GameVariables gameVariables; private readonly IGameBoard gameBoard; private Vector2Int lastSwapFrom; private Vector2Int lastSwapTo; private BombSpawnRequest? pendingBombSpawn; public BombService(GameVariables gameVariables, IGameBoard gameBoard) { this.gameVariables = gameVariables; this.gameBoard = gameBoard; } public void SetLastSwap(Vector2Int from, Vector2Int to) { this.lastSwapFrom = from; this.lastSwapTo = to; ClearPendingBombs(); } public UniTask> GetInitialBombs(List protectedPositions, HashSet bombCandidates) { HashSet initialBombs = new HashSet(); foreach (Vector2Int p in bombCandidates) { if (!GemUtils.IsInBounds(p, this.gameBoard)) continue; if (protectedPositions != null && protectedPositions.Contains(p)) continue; Gem gem = this.gameBoard.GetGemAt(p); if (gem is { Type: GemType.Bomb }) initialBombs.Add(p); } return UniTask.FromResult(initialBombs.ToList()); } public List ApplyPendingBombSpawns(Action spawnGem) { List positions = new List(); BombSpawnRequest? bombSpawnRequest = this.pendingBombSpawn; if (bombSpawnRequest != null) { BombSpawnRequest bombRequest = this.pendingBombSpawn.GetValueOrDefault(); positions.Add(bombRequest.Position); spawnGem(bombRequest.Position, bombRequest.Color, true); } ClearPendingBombs(); return positions; } public void DetectBombSpawnFromLastSwap(HashSet currentMatches) { Vector2Int from = this.lastSwapFrom; Vector2Int to = this.lastSwapTo; TryCreateBombSpawnAt(from, currentMatches); TryCreateBombSpawnAt(to, currentMatches); } private void TryCreateBombSpawnAt(Vector2Int pivot, HashSet currentMatches) { Gem pivotGem = this.gameBoard.GetGemAt(pivot); if (pivotGem == null) return; // If it's already a bomb, don't create another. if (pivotGem.Type == GemType.Bomb) return; if (currentMatches == null || !currentMatches.Contains(pivotGem)) return; // Only create a bomb if the pivot is part of a straight 4+ line of the SAME color. int longestLine = GetLongestMatchedLineThroughPivot(pivot, pivotGem.MatchColor); if (longestLine < 4) return; // Prevent duplicates for the same cell. if (this.pendingBombSpawn.GetValueOrDefault().Position == pivot) return; this.pendingBombSpawn = new BombSpawnRequest(pivot, pivotGem.MatchColor); } public async UniTask DetonateChainAsync( IReadOnlyList initialBombs, Func destroyAtAsync, IGameBoard gameBoard) { if (initialBombs == null || initialBombs.Count == 0) return; int waveDelayMs = Mathf.RoundToInt(this.gameVariables.bombDelay * 1000f); HashSet processedBombs = new HashSet(); Queue waveQueue = new Queue(); foreach (Vector2Int position in initialBombs) { if (GemUtils.IsInBounds(position, gameBoard)) { Gem gem = gameBoard.GetGemAt(position); if(gem is { Type: GemType.Bomb }) waveQueue.Enqueue(position); } } while (waveQueue.Count > 0) { Vector2Int bombPos = waveQueue.Dequeue(); if (processedBombs.Contains(bombPos)) continue; if (!GemUtils.IsInBounds(bombPos, gameBoard)) continue; Gem g = gameBoard.GetGemAt(bombPos); if (g is not { Type: GemType.Bomb }) continue; processedBombs.Add(bombPos); // delay once per bomb await UniTask.Delay(waveDelayMs); HashSet toDestroyNow = new HashSet(); // destroy self when it detonates toDestroyNow.Add(bombPos); foreach (Vector2Int position in DiamondAreaInclusive(bombPos, this.gameVariables.bombRadius)) { if (!GemUtils.IsInBounds(position, gameBoard)) continue; if (position == bombPos) continue; Gem cellGem = gameBoard.GetGemAt(position); if (cellGem == null) continue; if (cellGem.Type == GemType.Bomb) { // bombs in range are NOT destroyed now. triggered to explode in a later "step". if (!processedBombs.Contains(position)) waveQueue.Enqueue(position); continue; } // Non-bomb gem gets destroyed by this bomb toDestroyNow.Add(position); } // Destroy everything for this specific bomb detonation foreach (Vector2Int p in toDestroyNow) await destroyAtAsync(p); } } private static IEnumerable DiamondAreaInclusive(Vector2Int center, int radius) { // Manhattan-distance filled diamond: for (int distanceX = -radius; distanceX <= radius; distanceX++) { int remaining = radius - Mathf.Abs(distanceX); for (int distanceY = -remaining; distanceY <= remaining; distanceY++) { yield return new Vector2Int(center.x + distanceX, center.y + distanceY); } } } private int GetLongestMatchedLineThroughPivot(Vector2Int pivot, GemType color) { int horizontal = 1 + CountSameColorInDirection(pivot, Vector2Int.left, color) + CountSameColorInDirection(pivot, Vector2Int.right, color); int vertical = 1 + CountSameColorInDirection(pivot, Vector2Int.up, color) + CountSameColorInDirection(pivot, Vector2Int.down, color); return Mathf.Max(horizontal, vertical); } private int CountSameColorInDirection(Vector2Int start, Vector2Int direction, GemType color) { int count = 0; Vector2Int pivot = start + direction; while (pivot.x >= 0 && pivot.x < this.gameBoard.Width && pivot.y >= 0 && pivot.y < this.gameBoard.Height) { Gem g = this.gameBoard.GetGemAt(pivot); if (g == null || g.Type == GemType.Bomb || g.MatchColor != color) break; count++; pivot += direction; } return count; } private void ClearPendingBombs() { this.pendingBombSpawn = null; } } }