Files
match3-unity/Assets/Scripts/Services/BombService.cs

205 lines
7.2 KiB
C#

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 void ClearPendingBombs() {
this.pendingBombSpawn = null;
}
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;
}
public void DetectBombSpawnFromLastSwap(List<Gem> currentMatches) {
Vector2Int from = this.lastSwapFrom;
Vector2Int to = this.lastSwapTo;
TryCreateBombSpawnAt(from, currentMatches);
TryCreateBombSpawnAt(to, currentMatches);
}
private void TryCreateBombSpawnAt(Vector2Int pivot, List<Gem> 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.All(g => g.Position != pivot))
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<Vector2Int> initialBombs,
Func<Vector2Int, UniTask> destroyAtAsync,
IGameBoard gameBoard)
{
if (initialBombs == null || initialBombs.Count == 0)
return;
int waveDelayMs = Mathf.RoundToInt(this.gameVariables.bombDelay * 1000f);
HashSet<Vector2Int> processedBombs = new HashSet<Vector2Int>();
Queue<Vector2Int> waveQueue = new Queue<Vector2Int>(
initialBombs.Where(p =>
{
if (!GemUtils.IsInBounds(p, gameBoard)) return false;
Gem g = gameBoard.GetGemAt(p);
return g is { Type: GemType.Bomb };
})
);
while (waveQueue.Count > 0)
{
// current wave (per bomb)
List<Vector2Int> waveBombs = new List<Vector2Int>();
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<Vector2Int> nextWaveBombs = new HashSet<Vector2Int>();
HashSet<Vector2Int> toDestroyNow = new HashSet<Vector2Int>();
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<Vector2Int> 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;
}
}
}