216 lines
7.8 KiB
C#
216 lines
7.8 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 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<List<Vector2Int>> GetInitialBombs(List<Vector2Int> protectedPositions, HashSet<Vector2Int> bombCandidates) {
|
|
HashSet<Vector2Int> initialBombs = new HashSet<Vector2Int>();
|
|
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<Vector2Int> ApplyPendingBombSpawns(Action<Vector2Int, GemType, bool> spawnGem) {
|
|
List<Vector2Int> positions = new List<Vector2Int>();
|
|
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<Gem> currentMatches) {
|
|
Vector2Int from = this.lastSwapFrom;
|
|
Vector2Int to = this.lastSwapTo;
|
|
|
|
TryCreateBombSpawnAt(from, currentMatches);
|
|
TryCreateBombSpawnAt(to, currentMatches);
|
|
}
|
|
|
|
private void TryCreateBombSpawnAt(Vector2Int pivot, HashSet<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 == 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<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>();
|
|
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<Vector2Int> toDestroyNow = new HashSet<Vector2Int>();
|
|
|
|
// 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<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 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;
|
|
}
|
|
}
|
|
} |