Separate bomb logic
This commit is contained in:
@@ -34,6 +34,8 @@ namespace Scopes
|
|||||||
new ObjectPoolService(this.gameVariables.gemsPrefabs, this.gemsHolder),
|
new ObjectPoolService(this.gameVariables.gemsPrefabs, this.gemsHolder),
|
||||||
Lifetime.Scoped);
|
Lifetime.Scoped);
|
||||||
|
|
||||||
|
builder.Register<IBombService, BombService>(Lifetime.Scoped);
|
||||||
|
|
||||||
builder.Register<ScorePresenter>(Lifetime.Scoped);
|
builder.Register<ScorePresenter>(Lifetime.Scoped);
|
||||||
builder.Register<IGameBoardService, GameBoardService>(Lifetime.Scoped).AsImplementedInterfaces();
|
builder.Register<IGameBoardService, GameBoardService>(Lifetime.Scoped).AsImplementedInterfaces();
|
||||||
|
|
||||||
|
|||||||
117
Assets/Scripts/Services/BombService.cs
Normal file
117
Assets/Scripts/Services/BombService.cs
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
// Assets/Scripts/Services/BombService.cs
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Cysharp.Threading.Tasks;
|
||||||
|
using Enums;
|
||||||
|
using Models;
|
||||||
|
using Services.Interfaces;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Services
|
||||||
|
{
|
||||||
|
public class BombService : IBombService
|
||||||
|
{
|
||||||
|
public IReadOnlyList<Vector2Int> CollectTriggeredBombs(IReadOnlyList<Vector2Int> matchPositions)
|
||||||
|
{
|
||||||
|
if (matchPositions == null || matchPositions.Count == 0)
|
||||||
|
return Array.Empty<Vector2Int>();
|
||||||
|
|
||||||
|
// Activation: any match cell that is a bomb OR cardinal-adjacent to a bomb.
|
||||||
|
// NOTE: The actual "is bomb?" check depends on the board, so we only return
|
||||||
|
// the positions to be checked/queued by caller if desired.
|
||||||
|
// To keep BombService isolated, we’ll let DetonateChainAsync validate bombs via getGemAt.
|
||||||
|
// Here we return: all matched positions + their cardinal neighbors.
|
||||||
|
HashSet<Vector2Int> candidates = new HashSet<Vector2Int>(matchPositions);
|
||||||
|
|
||||||
|
foreach (Vector2Int p in matchPositions)
|
||||||
|
{
|
||||||
|
candidates.Add(p + Vector2Int.left);
|
||||||
|
candidates.Add(p + Vector2Int.right);
|
||||||
|
candidates.Add(p + Vector2Int.up);
|
||||||
|
candidates.Add(p + Vector2Int.down);
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidates.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async UniTask DetonateChainAsync(
|
||||||
|
IReadOnlyList<Vector2Int> initialBombs,
|
||||||
|
Func<Vector2Int, bool> inBounds,
|
||||||
|
Func<Vector2Int, Gem> getGemAt,
|
||||||
|
Func<Vector2Int, UniTask> destroyAtAsync,
|
||||||
|
int radius,
|
||||||
|
float bombDelaySeconds,
|
||||||
|
float bombSelfDelaySeconds)
|
||||||
|
{
|
||||||
|
if (initialBombs == null || initialBombs.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Queue<Vector2Int> queue = new Queue<Vector2Int>(initialBombs);
|
||||||
|
HashSet<Vector2Int> processed = new HashSet<Vector2Int>();
|
||||||
|
|
||||||
|
while (queue.Count > 0)
|
||||||
|
{
|
||||||
|
Vector2Int bombPos = queue.Dequeue();
|
||||||
|
if (processed.Contains(bombPos))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!inBounds(bombPos))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
Gem bomb = getGemAt(bombPos);
|
||||||
|
if (bomb is not { Type: GemType.Bomb })
|
||||||
|
continue;
|
||||||
|
|
||||||
|
processed.Add(bombPos);
|
||||||
|
|
||||||
|
// Delay before neighbor blast
|
||||||
|
int neighborDelayMs = Mathf.Max(0, Mathf.RoundToInt(bombDelaySeconds * 1000f));
|
||||||
|
if (neighborDelayMs > 0)
|
||||||
|
await UniTask.Delay(neighborDelayMs);
|
||||||
|
|
||||||
|
// Blast neighbors first (cross)
|
||||||
|
foreach (Vector2Int n in CrossNeighbors(bombPos, radius))
|
||||||
|
{
|
||||||
|
if (!inBounds(n))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
Gem g = getGemAt(n);
|
||||||
|
if (g == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Chain: if another bomb is in blast area, queue it
|
||||||
|
if (g.Type == GemType.Bomb)
|
||||||
|
{
|
||||||
|
if (!processed.Contains(n))
|
||||||
|
queue.Enqueue(n);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await destroyAtAsync(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delay before destroying the bomb itself
|
||||||
|
int selfDelayMs = Mathf.Max(0, Mathf.RoundToInt(bombSelfDelaySeconds * 1000f));
|
||||||
|
if (selfDelayMs > 0)
|
||||||
|
await UniTask.Delay(selfDelayMs);
|
||||||
|
|
||||||
|
// Destroy bomb last
|
||||||
|
Gem stillBomb = getGemAt(bombPos);
|
||||||
|
if (stillBomb is { Type: GemType.Bomb })
|
||||||
|
await destroyAtAsync(bombPos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<Vector2Int> CrossNeighbors(Vector2Int center, int radius)
|
||||||
|
{
|
||||||
|
for (int i = 1; i <= radius; i++)
|
||||||
|
{
|
||||||
|
yield return center + Vector2Int.left * i;
|
||||||
|
yield return center + Vector2Int.right * i;
|
||||||
|
yield return center + Vector2Int.up * i;
|
||||||
|
yield return center + Vector2Int.down * i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
Assets/Scripts/Services/BombService.cs.meta
Normal file
3
Assets/Scripts/Services/BombService.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 25e4f7a32f2f4b5e9b9f5984d10d17ab
|
||||||
|
timeCreated: 1765744719
|
||||||
@@ -22,6 +22,7 @@ namespace Services {
|
|||||||
private readonly GameVariables gameVariables;
|
private readonly GameVariables gameVariables;
|
||||||
private readonly IMatchService matchService;
|
private readonly IMatchService matchService;
|
||||||
private readonly IScoreService scoreService;
|
private readonly IScoreService scoreService;
|
||||||
|
private readonly IBombService bombService;
|
||||||
private readonly IObjectPool<GemView> objectPool;
|
private readonly IObjectPool<GemView> objectPool;
|
||||||
private readonly Transform gemsHolder;
|
private readonly Transform gemsHolder;
|
||||||
#endregion
|
#endregion
|
||||||
@@ -32,11 +33,12 @@ namespace Services {
|
|||||||
private GameState currentState = GameState.Move;
|
private GameState currentState = GameState.Move;
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public GameBoardService(IGameBoard gameBoard, GameVariables gameVariables, IMatchService matchService, IScoreService scoreSerivce, IObjectPool<GemView> objectPool, Transform gemsHolder, ScorePresenter scorePresenter) {
|
public GameBoardService(IGameBoard gameBoard, GameVariables gameVariables, IMatchService matchService, IScoreService scoreSerivce, IBombService bombService, IObjectPool<GemView> objectPool, Transform gemsHolder, ScorePresenter scorePresenter) {
|
||||||
this.gameBoard = gameBoard;
|
this.gameBoard = gameBoard;
|
||||||
this.gameVariables = gameVariables;
|
this.gameVariables = gameVariables;
|
||||||
this.matchService = matchService;
|
this.matchService = matchService;
|
||||||
this.scoreService = scoreSerivce;
|
this.scoreService = scoreSerivce;
|
||||||
|
this.bombService = bombService;
|
||||||
this.objectPool = objectPool;
|
this.objectPool = objectPool;
|
||||||
this.gemsHolder = gemsHolder;
|
this.gemsHolder = gemsHolder;
|
||||||
this.scorePresenter = scorePresenter;
|
this.scorePresenter = scorePresenter;
|
||||||
@@ -179,109 +181,63 @@ namespace Services {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async UniTask DestroyMatchesAsync(List<Vector2Int> protectedPositions) {
|
private async UniTask DestroyMatchesAsync(List<Vector2Int> protectedPositions) {
|
||||||
// Build initial queues from current matches
|
// Collect match positions, excluding protected (bomb creation slots).
|
||||||
Queue<Vector2Int> bombsToProcess = new Queue<Vector2Int>();
|
List<Vector2Int> matchPositions = new List<Vector2Int>(this.matchService.CurrentMatches.Count);
|
||||||
List<Vector2Int> processedBombs = new List<Vector2Int>();
|
|
||||||
List<Vector2Int> regularToDestroy = new List<Vector2Int>();
|
|
||||||
|
|
||||||
for (int i = 0; i < this.matchService.CurrentMatches.Count; i++) {
|
for (int i = 0; i < this.matchService.CurrentMatches.Count; i++) {
|
||||||
Gem matchedGem = this.matchService.CurrentMatches[i];
|
var m = this.matchService.CurrentMatches[i];
|
||||||
if (matchedGem == null)
|
if (m == null) continue;
|
||||||
continue;
|
|
||||||
|
|
||||||
Vector2Int pos = matchedGem.Position;
|
Vector2Int pos = m.Position;
|
||||||
|
|
||||||
// If a bomb was spawned at this cell due to 4+ creation, it must survive this destruction pass.
|
|
||||||
if (protectedPositions != null && protectedPositions.Contains(pos))
|
if (protectedPositions != null && protectedPositions.Contains(pos))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
Gem current = GetGem(pos);
|
matchPositions.Add(pos);
|
||||||
if (current == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (current.Type == GemType.Bomb) {
|
|
||||||
bombsToProcess.Enqueue(pos);
|
|
||||||
} else {
|
|
||||||
regularToDestroy.Add(pos);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process bombs: neighbors first (after delay), then the bomb itself (after delay).
|
// Bombs are handled by BombService so they can respect delays + chaining.
|
||||||
while (bombsToProcess.Count > 0) {
|
foreach (Vector2Int pos in matchPositions.Distinct().ToList()) {
|
||||||
Vector2Int bombPos = bombsToProcess.Dequeue();
|
var g = GetGem(pos);
|
||||||
if (processedBombs.Contains(bombPos))
|
if (g == null) continue;
|
||||||
continue;
|
if (g.Type == GemType.Bomb) continue;
|
||||||
|
|
||||||
Gem bomb = GetGem(bombPos);
|
|
||||||
if (bomb is not { Type: GemType.Bomb })
|
|
||||||
continue;
|
|
||||||
|
|
||||||
processedBombs.Add(bombPos);
|
|
||||||
|
|
||||||
// Delay before destroying neighbor group
|
|
||||||
if (this.gameVariables.bombDelay > 0f) {
|
|
||||||
int msDelay = Mathf.RoundToInt(this.gameVariables.bombDelay * 1000f);
|
|
||||||
await UniTask.Delay(msDelay);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect cross neighbors
|
|
||||||
foreach (Vector2Int neighborPosition in CrossNeighbors(bombPos, this.gameVariables.bombRadius)) {
|
|
||||||
if (!InBounds(neighborPosition))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
Gem g = GetGem(neighborPosition);
|
|
||||||
if (g == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// If we encounter another bomb, queue it (so it explodes too).
|
|
||||||
if (g.Type == GemType.Bomb) {
|
|
||||||
if (!processedBombs.Contains(neighborPosition))
|
|
||||||
bombsToProcess.Enqueue(neighborPosition);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
regularToDestroy.Add(neighborPosition);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destroy the neighbor group now
|
|
||||||
foreach (Vector2Int position in regularToDestroy.ToList()) {
|
|
||||||
Gem gem = GetGem(position);
|
|
||||||
if (gem == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
this.scoreService.ScoreCheck(gem.ScoreValue);
|
|
||||||
DestroyMatchedGems(position);
|
|
||||||
}
|
|
||||||
regularToDestroy.Clear();
|
|
||||||
|
|
||||||
// Delay before destroying the bomb itself
|
|
||||||
if (this.gameVariables.bombSelfDelay > 0f) {
|
|
||||||
int ms = Mathf.RoundToInt(this.gameVariables.bombSelfDelay * 1000f);
|
|
||||||
await UniTask.Delay(ms);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destroy the bomb
|
|
||||||
Gem b = GetGem(bombPos);
|
|
||||||
if (b != null && b.Type == GemType.Bomb) {
|
|
||||||
this.scoreService.ScoreCheck(b.ScoreValue);
|
|
||||||
DestroyMatchedGems(bombPos);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destroy any remaining regular matches (non-bomb) after bomb processing
|
|
||||||
foreach (Vector2Int pos in regularToDestroy) {
|
|
||||||
Gem g = GetGem(pos);
|
|
||||||
if (g == null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
this.scoreService.ScoreCheck(g.ScoreValue);
|
this.scoreService.ScoreCheck(g.ScoreValue);
|
||||||
DestroyMatchedGems(pos);
|
DestroyMatchedGems(pos);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now we can cascade
|
IReadOnlyList<Vector2Int> bombCandidates = this.bombService.CollectTriggeredBombs(matchPositions);
|
||||||
|
|
||||||
|
List<Vector2Int> initialBombs = new List<Vector2Int>();
|
||||||
|
foreach (Vector2Int p in bombCandidates) {
|
||||||
|
if (!InBounds(p)) continue;
|
||||||
|
var g = GetGem(p);
|
||||||
|
if (g is { Type: GemType.Bomb })
|
||||||
|
initialBombs.Add(p);
|
||||||
|
}
|
||||||
|
initialBombs = initialBombs.Distinct().ToList();
|
||||||
|
|
||||||
|
await this.bombService.DetonateChainAsync(
|
||||||
|
initialBombs,
|
||||||
|
InBounds,
|
||||||
|
GetGem,
|
||||||
|
DestroyAtAsync,
|
||||||
|
this.gameVariables.bombRadius,
|
||||||
|
this.gameVariables.bombDelay,
|
||||||
|
this.gameVariables.bombSelfDelay
|
||||||
|
);
|
||||||
|
|
||||||
await MoveGemsDown();
|
await MoveGemsDown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private UniTask DestroyAtAsync(Vector2Int pos) {
|
||||||
|
Gem gem = GetGem(pos);
|
||||||
|
if (gem == null)
|
||||||
|
return UniTask.CompletedTask;
|
||||||
|
|
||||||
|
this.scoreService.ScoreCheck(gem.ScoreValue);
|
||||||
|
DestroyMatchedGems(pos);
|
||||||
|
return UniTask.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
private static IEnumerable<Vector2Int> CrossNeighbors(Vector2Int center, int radius) {
|
private static IEnumerable<Vector2Int> CrossNeighbors(Vector2Int center, int radius) {
|
||||||
// center excluded for "neighbors first"
|
// center excluded for "neighbors first"
|
||||||
for (int i = 1; i <= radius; i++) {
|
for (int i = 1; i <= radius; i++) {
|
||||||
|
|||||||
21
Assets/Scripts/Services/Interfaces/IBombService.cs
Normal file
21
Assets/Scripts/Services/Interfaces/IBombService.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// Assets/Scripts/Services/Interfaces/IBombService.cs
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Cysharp.Threading.Tasks;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Services.Interfaces
|
||||||
|
{
|
||||||
|
public interface IBombService
|
||||||
|
{
|
||||||
|
IReadOnlyList<Vector2Int> CollectTriggeredBombs(IReadOnlyList<Vector2Int> matchPositions);
|
||||||
|
UniTask DetonateChainAsync(
|
||||||
|
IReadOnlyList<Vector2Int> initialBombs,
|
||||||
|
Func<Vector2Int, bool> inBounds,
|
||||||
|
Func<Vector2Int, Gem> getGemAt,
|
||||||
|
Func<Vector2Int, UniTask> destroyAtAsync,
|
||||||
|
int radius,
|
||||||
|
float bombDelaySeconds,
|
||||||
|
float bombSelfDelaySeconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
3
Assets/Scripts/Services/Interfaces/IBombService.cs.meta
Normal file
3
Assets/Scripts/Services/Interfaces/IBombService.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 596ff7c31da640178508db656fc88e9b
|
||||||
|
timeCreated: 1765744682
|
||||||
Reference in New Issue
Block a user