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 List pendingBombSpawns = new List(); 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(); foreach (BombSpawnRequest bombRequest in this.pendingBombSpawns) { 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; // Create a bomb if it's a 4+ line OR an L/T shape (both horizontal and vertical >= 3) bool isEligibleForBomb = IsEligibleForBomb(pivot, pivotGem.MatchColor); if (!isEligibleForBomb) return; // Prevent duplicates for the same cell. if (this.pendingBombSpawns.Any(b => b.Position == pivot)) return; this.pendingBombSpawns.Add(new BombSpawnRequest(pivot, pivotGem.MatchColor)); } private bool IsEligibleForBomb(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); // Straight line of 4 or more if (horizontal >= 4 || vertical >= 4) return true; // L or T shape: both directions have at least a 3-match intersecting at this pivot if (horizontal >= 3 && vertical >= 3) return true; return false; } public async UniTask DetonateChainAsync( IReadOnlyList initialBombs, Func destroyAtAsync) { 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, this.gameBoard)) { Gem gem = this.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, this.gameBoard)) continue; Gem g = this.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, this.gameBoard)) continue; if (position == bombPos) continue; Gem cellGem = this.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 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.pendingBombSpawns.Clear(); } } }