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 BombSpawnRequest? PendingBombSpawn => this.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, List 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 = PendingBombSpawn; if (bombSpawnRequest != null) { BombSpawnRequest bombRequest = 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 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) { // current wave (per bomb) List waveBombs = new List(); while (waveQueue.Count > 0) { Vector2Int b = waveQueue.Dequeue(); if (processedBombs.Contains(b)) continue; if (!GemUtils.IsInBounds(b, gameBoard)) continue; Gem g = gameBoard.GetGemAt(b); if (g is not { Type: GemType.Bomb }) continue; processedBombs.Add(b); waveBombs.Add(b); } if (waveBombs.Count == 0) continue; // delay once per wave if (waveDelayMs > 0) await UniTask.Delay(waveDelayMs); HashSet nextWaveBombs = new HashSet(); HashSet toDestroyNow = new HashSet(); for (int i = 0; i < waveBombs.Count; i++) { Vector2Int bombPos = waveBombs[i]; // destroy self when it detonates toDestroyNow.Add(bombPos); foreach (Vector2Int p in DiamondAreaInclusive(bombPos, this.gameVariables.bombRadius)) { if (!GemUtils.IsInBounds(p, gameBoard)) continue; if (p == bombPos) continue; Gem cellGem = gameBoard.GetGemAt(p); if (cellGem == null) continue; if (cellGem.Type == GemType.Bomb) { // bombs in range are NOT destroyed now. triggered to explode in a later wave. if (!processedBombs.Contains(p)) nextWaveBombs.Add(p); continue; } // Non-bomb gem gets destroyed by this bomb toDestroyNow.Add(p); } } // Destroy everything for this wave (non-bombs in range + the detonating bombs themselves) foreach (Vector2Int p in toDestroyNow) await destroyAtAsync(p); // Schedule the next wave (triggered bombs) foreach (Vector2Int b in nextWaveBombs) waveQueue.Enqueue(b); } } 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 oivot = start + direction; while (oivot.x >= 0 && oivot.x < this.gameBoard.Width && oivot.y >= 0 && oivot.y < this.gameBoard.Height) { Gem g = this.gameBoard.GetGemAt(oivot); if (g == null || g.Type == GemType.Bomb || g.MatchColor != color) break; count++; oivot += direction; } return count; } public void ClearPendingBombs() { this.pendingBombSpawn = null; } } }