Move Bomb Responsibilities to BombService

This commit is contained in:
2025-12-17 05:47:28 +08:00
parent 9f2ef833b2
commit b3dc2cb4bd
12 changed files with 262 additions and 248 deletions

View File

@@ -183,7 +183,10 @@ Transform:
m_LocalPosition: {x: 2.36, y: 7.7, z: 0} m_LocalPosition: {x: 2.36, y: 7.7, z: 0}
m_LocalScale: {x: 3.09, y: 3.09, z: 3.09} m_LocalScale: {x: 3.09, y: 3.09, z: 3.09}
m_ConstrainProportionsScale: 1 m_ConstrainProportionsScale: 1
m_Children: [] m_Children:
- {fileID: 638697049}
- {fileID: 544543388}
- {fileID: 1136728301}
m_Father: {fileID: 1879498210} m_Father: {fileID: 1879498210}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!212 &96423353 --- !u!212 &96423353
@@ -356,6 +359,37 @@ Transform:
m_Children: [] m_Children: []
m_Father: {fileID: 3199143} m_Father: {fileID: 3199143}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &299575752
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 299575753}
m_Layer: 0
m_Name: TileBGHolder
m_TagString: UnityObject
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &299575753
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 299575752}
serializedVersion: 2
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1879498210}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &519420028 --- !u!1 &519420028
GameObject: GameObject:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@@ -474,11 +508,11 @@ Transform:
m_GameObject: {fileID: 544543387} m_GameObject: {fileID: 544543387}
serializedVersion: 2 serializedVersion: 2
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 3, y: 3, z: 0} m_LocalPosition: {x: 0.20711978, y: -1.5210356, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1} m_LocalScale: {x: 0.3236246, y: 0.3236246, z: 0.3236246}
m_ConstrainProportionsScale: 0 m_ConstrainProportionsScale: 0
m_Children: [] m_Children: []
m_Father: {fileID: 1879498210} m_Father: {fileID: 96423352}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!212 &544543389 --- !u!212 &544543389
SpriteRenderer: SpriteRenderer:
@@ -558,11 +592,11 @@ Transform:
m_GameObject: {fileID: 638697048} m_GameObject: {fileID: 638697048}
serializedVersion: 2 serializedVersion: 2
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 3, y: 3, z: 0} m_LocalPosition: {x: 0.20711978, y: -1.5210356, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1} m_LocalScale: {x: 0.3236246, y: 0.3236246, z: 0.3236246}
m_ConstrainProportionsScale: 0 m_ConstrainProportionsScale: 0
m_Children: [] m_Children: []
m_Father: {fileID: 1879498210} m_Father: {fileID: 96423352}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!212 &638697050 --- !u!212 &638697050
SpriteRenderer: SpriteRenderer:
@@ -915,11 +949,11 @@ Transform:
m_GameObject: {fileID: 1136728300} m_GameObject: {fileID: 1136728300}
serializedVersion: 2 serializedVersion: 2
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 3, y: 3, z: 0} m_LocalPosition: {x: 0.20711978, y: -1.5210356, z: 0}
m_LocalScale: {x: 1.21, y: 1.21, z: 1.21} m_LocalScale: {x: 0.3915858, y: 0.3915858, z: 0.3915858}
m_ConstrainProportionsScale: 1 m_ConstrainProportionsScale: 1
m_Children: [] m_Children: []
m_Father: {fileID: 1879498210} m_Father: {fileID: 96423352}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!212 &1136728302 --- !u!212 &1136728302
SpriteRenderer: SpriteRenderer:
@@ -1099,12 +1133,12 @@ Transform:
m_PrefabAsset: {fileID: 0} m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1740955908} m_GameObject: {fileID: 1740955908}
serializedVersion: 2 serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1} m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0 m_ConstrainProportionsScale: 0
m_Children: [] m_Children: []
m_Father: {fileID: 0} m_Father: {fileID: 1879498210}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &1879498208 --- !u!1 &1879498208
GameObject: GameObject:
@@ -1136,9 +1170,8 @@ Transform:
m_ConstrainProportionsScale: 0 m_ConstrainProportionsScale: 0
m_Children: m_Children:
- {fileID: 96423352} - {fileID: 96423352}
- {fileID: 638697049} - {fileID: 299575753}
- {fileID: 544543388} - {fileID: 1740955909}
- {fileID: 1136728301}
m_Father: {fileID: 0} m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &2056718315 --- !u!1 &2056718315
@@ -1176,6 +1209,7 @@ MonoBehaviour:
autoInjectGameObjects: [] autoInjectGameObjects: []
gameVariables: {fileID: 11400000, guid: edd9a973e745f4f41bce834af2c68d05, type: 2} gameVariables: {fileID: 11400000, guid: edd9a973e745f4f41bce834af2c68d05, type: 2}
gemsHolder: {fileID: 1740955909} gemsHolder: {fileID: 1740955909}
backgroundHolder: {fileID: 299575753}
--- !u!4 &2056718317 --- !u!4 &2056718317
Transform: Transform:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0
@@ -1197,7 +1231,6 @@ SceneRoots:
m_Roots: m_Roots:
- {fileID: 519420032} - {fileID: 519420032}
- {fileID: 1879498210} - {fileID: 1879498210}
- {fileID: 1740955909}
- {fileID: 1450061022} - {fileID: 1450061022}
- {fileID: 3199143} - {fileID: 3199143}
- {fileID: 2056718317} - {fileID: 2056718317}

View File

@@ -16,8 +16,7 @@ namespace Services {
} }
public Gem GetGemAt(Vector2Int pos) { public Gem GetGemAt(Vector2Int pos) {
Gem gameObject = this.gemsGrid[pos.x, pos.y]; return this.gemsGrid[pos.x, pos.y];
return gameObject;
} }
public void SetGemAt(Vector2Int pos, Gem gameObject) { public void SetGemAt(Vector2Int pos, Gem gameObject) {

View File

@@ -14,32 +14,38 @@ namespace Scopes
{ {
[SerializeField] private GameVariables gameVariables; [SerializeField] private GameVariables gameVariables;
[SerializeField] private Transform gemsHolder; [SerializeField] private Transform gemsHolder;
[SerializeField] private Transform backgroundHolder;
protected override void Configure(IContainerBuilder builder) protected override void Configure(IContainerBuilder builder)
{ {
//Register variables
builder.RegisterInstance(this.gameVariables); builder.RegisterInstance(this.gameVariables);
builder.RegisterInstance(this.gemsHolder); builder.RegisterInstance(this.gemsHolder);
//Register component
builder.RegisterComponentInHierarchy<ScoreView>(); builder.RegisterComponentInHierarchy<ScoreView>();
builder.Register<IGameBoard>(_ => builder.Register<IGameBoard>(_ =>
new GameBoard(this.gameVariables.width, this.gameVariables.height), new GameBoard(this.gameVariables.width, this.gameVariables.height),
Lifetime.Scoped); Lifetime.Scoped);
//Register Services
builder.Register<IMatchService, MatchService>(Lifetime.Scoped); builder.Register<IMatchService, MatchService>(Lifetime.Scoped);
builder.Register<IScoreService, ScoreService>(Lifetime.Scoped); builder.Register<IScoreService, ScoreService>(Lifetime.Scoped);
builder.Register<IBombService, BombService>(Lifetime.Scoped);
//Register Pool
builder.Register<IObjectPool<GemView>>(_ => builder.Register<IObjectPool<GemView>>(_ =>
new ObjectPoolService(this.gameVariables.gemsPrefabs, this.gemsHolder), new ObjectPoolService(this.gameVariables.gemsPrefabs, this.gemsHolder),
Lifetime.Scoped); Lifetime.Scoped);
builder.Register<IBombService, BombService>(Lifetime.Scoped); //Presenters
builder.Register<AudioPresenter>(Lifetime.Scoped); builder.Register<AudioPresenter>(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();
//Entry Point
builder.RegisterEntryPoint<LevelEntryPoint>(); builder.RegisterEntryPoint<LevelEntryPoint>();
} }
} }

View File

@@ -14,7 +14,6 @@ namespace ScriptableObjects {
[Header("Audio")] [Header("Audio")]
public AudioClip matchSfx; public AudioClip matchSfx;
public AudioClip bombExplodeSfx; public AudioClip bombExplodeSfx;
[Header("Bomb")] [Header("Bomb")]

View File

@@ -1,45 +1,90 @@
// Assets/Scripts/Services/BombService.cs
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Cysharp.Threading.Tasks; using Cysharp.Threading.Tasks;
using Enums; using Enums;
using Models.Interfaces;
using ScriptableObjects;
using Services.Interfaces; using Services.Interfaces;
using Structs;
using UnityEngine; using UnityEngine;
using Utils;
namespace Services namespace Services
{ {
public class BombService : IBombService public class BombService : IBombService {
{ private readonly GameVariables gameVariables;
public IReadOnlyList<Vector2Int> CollectTriggeredBombs(IReadOnlyList<Vector2Int> matchPositions) private readonly IGameBoard gameBoard;
{
if (matchPositions == null || matchPositions.Count == 0) private Vector2Int lastSwapFrom;
return Array.Empty<Vector2Int>(); private Vector2Int lastSwapTo;
private BombSpawnRequest? pendingBombSpawn;
return matchPositions.Distinct().ToList(); 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( public async UniTask DetonateChainAsync(
IReadOnlyList<Vector2Int> initialBombs, IReadOnlyList<Vector2Int> initialBombs,
Func<Vector2Int, bool> inBounds,
Func<Vector2Int, Gem> getGemAt,
Func<Vector2Int, UniTask> destroyAtAsync, Func<Vector2Int, UniTask> destroyAtAsync,
int radius, IGameBoard gameBoard)
float bombDelaySeconds)
{ {
if (initialBombs == null || initialBombs.Count == 0) if (initialBombs == null || initialBombs.Count == 0)
return; return;
int waveDelayMs = Mathf.Max(0, Mathf.RoundToInt(bombDelaySeconds * 1000f)); int waveDelayMs = Mathf.RoundToInt(this.gameVariables.bombDelay * 1000f);
HashSet<Vector2Int> processedBombs = new HashSet<Vector2Int>(); HashSet<Vector2Int> processedBombs = new HashSet<Vector2Int>();
Queue<Vector2Int> waveQueue = new Queue<Vector2Int>( Queue<Vector2Int> waveQueue = new Queue<Vector2Int>(
initialBombs.Where(p => initialBombs.Where(p =>
{ {
if (!inBounds(p)) return false; if (!GemUtils.IsInBounds(p, gameBoard)) return false;
Gem g = getGemAt(p); Gem g = gameBoard.GetGemAt(p);
return g is { Type: GemType.Bomb }; return g is { Type: GemType.Bomb };
}) })
); );
@@ -54,10 +99,10 @@ namespace Services
if (processedBombs.Contains(b)) if (processedBombs.Contains(b))
continue; continue;
if (!inBounds(b)) if (!GemUtils.IsInBounds(b, gameBoard))
continue; continue;
Gem g = getGemAt(b); Gem g = gameBoard.GetGemAt(b);
if (g is not { Type: GemType.Bomb }) if (g is not { Type: GemType.Bomb })
continue; continue;
@@ -82,15 +127,15 @@ namespace Services
// destroy self when it detonates // destroy self when it detonates
toDestroyNow.Add(bombPos); toDestroyNow.Add(bombPos);
foreach (Vector2Int p in DiamondAreaInclusive(bombPos, radius)) foreach (Vector2Int p in DiamondAreaInclusive(bombPos, this.gameVariables.bombRadius))
{ {
if (!inBounds(p)) if (!GemUtils.IsInBounds(p, gameBoard))
continue; continue;
if (p == bombPos) if (p == bombPos)
continue; continue;
Gem cellGem = getGemAt(p); Gem cellGem = gameBoard.GetGemAt(p);
if (cellGem == null) if (cellGem == null)
continue; continue;
@@ -130,5 +175,31 @@ namespace Services
} }
} }
} }
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;
}
} }
} }

View File

@@ -17,32 +17,48 @@ using Object = UnityEngine.Object;
namespace Services { namespace Services {
public class GameBoardService : IGameBoardService, ITickable, IDisposable { public class GameBoardService : IGameBoardService, ITickable, IDisposable {
#region Inject #region Inject
private readonly IGameBoard gameBoard;
private readonly GameVariables gameVariables; private readonly GameVariables gameVariables;
private readonly IMatchService matchService;
private readonly IScoreService scoreService; private readonly IGameBoard gameBoard;
private readonly AudioPresenter audioPresenter;
private readonly IBombService bombService;
private readonly IObjectPool<GemView> objectPool; private readonly IObjectPool<GemView> objectPool;
private readonly IMatchService matchService;
private readonly IBombService bombService;
private readonly IScoreService scoreService;
private readonly ScorePresenter scorePresenter;
private readonly AudioPresenter audioPresenter;
private readonly Transform gemsHolder; private readonly Transform gemsHolder;
private readonly Transform backgroundHolder;
#endregion #endregion
#region Variables #region Variables
private readonly List<GemPresenter> gemPresenters = new List<GemPresenter>(); private readonly List<GemPresenter> gemPresenters = new List<GemPresenter>();
private readonly ScorePresenter scorePresenter;
private GameState currentState = GameState.Move; private GameState currentState = GameState.Move;
#endregion #endregion
public GameBoardService(IGameBoard gameBoard, GameVariables gameVariables, IMatchService matchService, IScoreService scoreSerivce, IBombService bombService, IObjectPool<GemView> objectPool, Transform gemsHolder, ScorePresenter scorePresenter, AudioPresenter audioPresenter) { public GameBoardService(
this.gameBoard = gameBoard; GameVariables gameVariables,
IGameBoard gameBoard,
IObjectPool<GemView> objectPool,
IMatchService matchService,
IBombService bombService,
IScoreService scoreService,
ScorePresenter scorePresenter,
AudioPresenter audioPresenter,
Transform gemsHolder,
Transform backgroundHolder) {
this.gameVariables = gameVariables; this.gameVariables = gameVariables;
this.matchService = matchService; this.gameBoard = gameBoard;
this.scoreService = scoreSerivce;
this.bombService = bombService;
this.objectPool = objectPool; this.objectPool = objectPool;
this.gemsHolder = gemsHolder; this.matchService = matchService;
this.bombService = bombService;
this.scoreService = scoreService;
this.scorePresenter = scorePresenter; this.scorePresenter = scorePresenter;
this.audioPresenter = audioPresenter; this.audioPresenter = audioPresenter;
this.gemsHolder = gemsHolder;
this.backgroundHolder = backgroundHolder;
} }
public void Tick() { public void Tick() {
@@ -60,65 +76,42 @@ namespace Services {
for (int y = 0; y < this.gameBoard.Height; y++) for (int y = 0; y < this.gameBoard.Height; y++)
{ {
Vector2 position = new Vector2(x, y); Vector2 position = new Vector2(x, y);
GameObject backgroundTile = Object.Instantiate(this.gameVariables.bgTilePrefabs, position, Quaternion.identity); SpawnBackgroundTile(position);
backgroundTile.transform.SetParent(this.gemsHolder);
backgroundTile.name = "BG Tile - " + x + ", " + y;
int gemToUse = RandomUtils.RandomGemTypeAsInt();
int iterations = 0; int iterations = 0;
while (this.matchService.MatchesAt(new Vector2Int(x, y), (GemType)gemToUse) && iterations < 100) int gemToUse = -1;
{ do {
gemToUse = RandomUtils.RandomGemTypeAsInt(); gemToUse = RandomUtils.RandomGemTypeAsInt();
iterations++; iterations++;
} } while (this.matchService.MatchesAt(position.ToVector2Int(), (GemType)gemToUse) && iterations < 100);
SpawnGem(new Vector2Int(x, y), (GemType)gemToUse); SpawnGem(position.ToVector2Int(), (GemType)gemToUse);
} }
this.currentState = GameState.Move; this.currentState = GameState.Move;
} }
private void SpawnBackgroundTile(Vector2 position) {
GameObject backgroundTile = Object.Instantiate(this.gameVariables.bgTilePrefabs, position, Quaternion.identity);
backgroundTile.transform.SetParent(this.backgroundHolder);
backgroundTile.name = "BG Tile - " + position.x + ", " + position.y;
}
//Uses the ObjectPool to spawn a gem at the given position //Uses the ObjectPool to spawn a gem at the given position
private void SpawnGem(Vector2Int position, GemType gemType) { private void SpawnGem(Vector2Int position, GemType gemType, bool isBomb = false) {
GemView gemView = this.objectPool.Get(gemType, position, this.gameVariables.dropHeight); if (isBomb) {
gemView.name = "Gem - " + position.x + ", " + position.y + ' ' + gemType; DestroyMatchedGems(position);
}
GemTypeValues gemValue = GemUtils.GetGemValues(gemType, this.gameVariables.gemsPrefabs);
// If we randomly spawned a bomb, give it a random color group (so it can match by color). GemView gemView = this.objectPool.Get(isBomb ? GemType.Bomb : gemType, position, isBomb ? 0 : this.gameVariables.dropHeight);
Gem gem = new Gem(gemType, position, gemValue); gemView.name = "Gem - " + position.x + ", " + position.y + ' ' + gemType;
gemView.Bind(gem, gemValue); GemTypeValues gemValue = GemUtils.GetGemValues(gemType, this.gameVariables.gemsPrefabs);
Gem gem = new Gem(isBomb ? GemType.Bomb : gemType, position, gemValue, gemType);
gemView.Bind(gem, gemValue, isBomb: isBomb);
this.gemPresenters.Add(new GemPresenter(gem, gemView)); this.gemPresenters.Add(new GemPresenter(gem, gemView));
SetGem(new Vector2Int(position.x, position.y), gem); this.gameBoard.SetGemAt(position, gem);
}
private void SpawnBomb(Vector2Int position, GemType color) {
// remove existing gem/view at that position
DestroyMatchedGems(position);
GemView gemView = this.objectPool.Get(GemType.Bomb, position, 0);
gemView.name = "Bomb - " + position.x + ", " + position.y + ' ' + GemType.Bomb;
GemTypeValues gemValue = GemUtils.GetGemValues(color, this.gameVariables.gemsPrefabs);
Gem bombGem = new Gem(GemType.Bomb, position, gemValue, color);
gemView.Bind(bombGem, gemValue, isBomb: true);
this.gemPresenters.Add(new GemPresenter(bombGem, gemView));
SetGem(position, bombGem);
}
//Sets the gem on the GameBoard
private void SetGem(Vector2Int position, Gem gem) {
this.gameBoard.SetGemAt(new Vector2Int(position.x, position.y), gem);
}
//Gets the gem from the GameBoard
private Gem GetGem(Vector2Int position) {
return this.gameBoard.GetGemAt(position);
} }
//Listens to InputService OnSwapRequest //Listens to InputService OnSwapRequest
@@ -126,60 +119,58 @@ namespace Services {
if (this.currentState != GameState.Move) if (this.currentState != GameState.Move)
return false; return false;
if (!InBounds(from) || !InBounds(to)) if (!GemUtils.IsInBounds(from, this.gameBoard) || !GemUtils.IsInBounds(to, this.gameBoard))
return false; return false;
if (!AreAdjacentCardinal(from, to)) if (!AreAdjacentCardinal(from, to))
return false; return false;
Gem fromGem = GetGem(from);
Gem toGem = GetGem(to);
if(fromGem == null || toGem == null)
return false;
this.currentState = GameState.Wait; this.currentState = GameState.Wait;
ApplySwap(from, to, fromGem, toGem); ApplySwap(from, to);
await UniTask.Delay(600); await UniTask.Delay(600);
this.matchService.SetLastSwap(from, to); this.bombService.SetLastSwap(from, to);
this.bombService.ClearPendingBombs();
this.matchService.FindAllMatches(); this.matchService.FindAllMatches();
bool hasMatch = this.matchService.CurrentMatches.Count > 0; this.bombService.DetectBombSpawnFromLastSwap(this.matchService.CurrentMatches);
if (!hasMatch) { if (this.matchService.CurrentMatches.Count == 0) {
ApplySwap(to, from, fromGem, toGem); ApplySwap(to, from);
await UniTask.Delay(600); await UniTask.Delay(600);
this.currentState = GameState.Move; this.currentState = GameState.Move;
return false; return false;
} }
List<Vector2Int> protectedPositions = ApplyPendingBombSpawns(); List<Vector2Int> protectedPositions = ApplyPendingBombSpawns();
await DestroyMatchesAsync(protectedPositions); await DestroyMatchesAsync(protectedPositions);
this.currentState = GameState.Move; this.currentState = GameState.Move;
return true; return true;
} }
private void ApplySwap(Vector2Int posA, Vector2Int posB, Gem gemA, Gem gemB) { private void ApplySwap(Vector2Int from, Vector2Int to) {
Gem fromGem = this.gameBoard.GetGemAt(from);
Gem toGem = this.gameBoard.GetGemAt(to);
// swap their stored positions // swap their stored positions
gemA.SetPosition(posB); fromGem.SetPosition(to);
gemB.SetPosition(posA); toGem.SetPosition(from);
// update grid // update grid
SetGem(posA, gemB); this.gameBoard.SetGemAt(from, toGem);
SetGem(posB, gemA); this.gameBoard.SetGemAt(to, fromGem);
} }
private List<Vector2Int> ApplyPendingBombSpawns() { private List<Vector2Int> ApplyPendingBombSpawns() {
List<Vector2Int> positions = new List<Vector2Int>(); List<Vector2Int> positions = new List<Vector2Int>();
BombSpawnRequest? bombSpawnRequest = this.bombService.PendingBombSpawn;
foreach (BombSpawnRequest bomSpawnRequest in this.matchService.PendingBombSpawns) {
positions.Add(bomSpawnRequest.Position); if (bombSpawnRequest != null) {
SpawnBomb(bomSpawnRequest.Position, bomSpawnRequest.Color); BombSpawnRequest bombRequest = this.bombService.PendingBombSpawn.GetValueOrDefault();
positions.Add(bombRequest.Position);
SpawnGem(bombRequest.Position, bombRequest.Color, isBomb: true);
} }
this.matchService.ClearPendingBombs(); this.bombService.ClearPendingBombs();
return positions; return positions;
} }
@@ -196,16 +187,16 @@ namespace Services {
matchPositions.Add(pos); matchPositions.Add(pos);
} }
IReadOnlyList<Vector2Int> bombCandidates = this.bombService.CollectTriggeredBombs(matchPositions); IReadOnlyList<Vector2Int> bombCandidates = matchPositions.Distinct().ToList();
List<Vector2Int> initialBombs = new List<Vector2Int>(); List<Vector2Int> initialBombs = new List<Vector2Int>();
foreach (Vector2Int p in bombCandidates) { foreach (Vector2Int p in bombCandidates) {
if (!InBounds(p)) continue; if (!GemUtils.IsInBounds(p, this.gameBoard)) continue;
if (protectedPositions != null && protectedPositions.Contains(p)) if (protectedPositions != null && protectedPositions.Contains(p))
continue; continue;
Gem gem = GetGem(p); Gem gem = this.gameBoard.GetGemAt(p);
if (gem is { Type: GemType.Bomb }) if (gem is { Type: GemType.Bomb })
initialBombs.Add(p); initialBombs.Add(p);
} }
@@ -216,31 +207,20 @@ namespace Services {
if (initialBombs.Count > 0) { if (initialBombs.Count > 0) {
await this.bombService.DetonateChainAsync( await this.bombService.DetonateChainAsync(
initialBombs, initialBombs,
InBounds,
GetGem,
DestroyAtAsync, DestroyAtAsync,
this.gameVariables.bombRadius, this.gameBoard);
this.gameVariables.bombDelay);
await MoveGemsDown(); await MoveGemsDown();
return; return;
} }
bool willBreakAnyNonBombGem = false; bool willBreakAnyNonBombGem = matchPositions.Select(pos => this.gameBoard.GetGemAt(pos)).Where(gem => gem != null).Any(gem => gem.Type != GemType.Bomb);
foreach (Vector2Int pos in matchPositions) {
Gem gem = GetGem(pos);
if (gem == null) continue;
if (gem.Type == GemType.Bomb) continue;
willBreakAnyNonBombGem = true;
break;
}
if (willBreakAnyNonBombGem) if (willBreakAnyNonBombGem)
this.audioPresenter.OnMatch(this.gameVariables.matchSfx); this.audioPresenter.OnMatch(this.gameVariables.matchSfx);
foreach (Vector2Int pos in matchPositions.Distinct().ToList()) { foreach (Vector2Int pos in matchPositions.Distinct().ToList()) {
Gem gem = GetGem(pos); Gem gem = this.gameBoard.GetGemAt(pos);
if (gem == null) continue; if (gem == null) continue;
if (gem.Type == GemType.Bomb) continue; if (gem.Type == GemType.Bomb) continue;
@@ -250,17 +230,14 @@ namespace Services {
await this.bombService.DetonateChainAsync( await this.bombService.DetonateChainAsync(
initialBombs, initialBombs,
InBounds,
GetGem,
DestroyAtAsync, DestroyAtAsync,
this.gameVariables.bombRadius, this.gameBoard);
this.gameVariables.bombDelay);
await MoveGemsDown(); await MoveGemsDown();
} }
private UniTask DestroyAtAsync(Vector2Int pos) { private UniTask DestroyAtAsync(Vector2Int pos) {
Gem gem = GetGem(pos); Gem gem = this.gameBoard.GetGemAt(pos);
if (gem == null) if (gem == null)
return UniTask.CompletedTask; return UniTask.CompletedTask;
@@ -288,8 +265,8 @@ namespace Services {
else if (nullCounter > 0) else if (nullCounter > 0)
{ {
currentGem.SetPosition(new Vector2Int(currentGem.Position.x, currentGem.Position.y - nullCounter)); currentGem.SetPosition(new Vector2Int(currentGem.Position.x, currentGem.Position.y - nullCounter));
SetGem(currentGem.Position, currentGem); this.gameBoard.SetGemAt(currentGem.Position, currentGem);
SetGem(new Vector2Int(x,y), null); this.gameBoard.SetGemAt(new Vector2Int(x,y), null);
} }
} }
nullCounter = 0; nullCounter = 0;
@@ -338,7 +315,7 @@ namespace Services {
} }
private void DestroyMatchedGems(Vector2Int position) { private void DestroyMatchedGems(Vector2Int position) {
List<GemView> gemsViews = GemsViews(); List<GemView> gemsViews = this.gemsHolder.GetComponentsInChildren<GemView>().ToList();
Gem currentGem = this.gameBoard.GetGemAt(position); Gem currentGem = this.gameBoard.GetGemAt(position);
if (currentGem != null) if (currentGem != null)
{ {
@@ -349,7 +326,7 @@ namespace Services {
this.objectPool.Release(gemView); this.objectPool.Release(gemView);
RemovePresenterFor(gemView); RemovePresenterFor(gemView);
SetGem(position, null); this.gameBoard.SetGemAt(position, null);
} }
} }
@@ -363,14 +340,6 @@ namespace Services {
this.gemPresenters.Remove(presenter); this.gemPresenters.Remove(presenter);
} }
private List<GemView> GemsViews() {
return this.gemsHolder.GetComponentsInChildren<GemView>().ToList();
}
private bool InBounds(Vector2Int p) {
return p.x >= 0 && p.x < this.gameBoard.Width && p.y >= 0 && p.y < this.gameBoard.Height;
}
private static bool AreAdjacentCardinal(Vector2Int a, Vector2Int b) { private static bool AreAdjacentCardinal(Vector2Int a, Vector2Int b) {
Vector2Int d = b - a; Vector2Int d = b - a;
return (Mathf.Abs(d.x) == 1 && d.y == 0) || (Mathf.Abs(d.y) == 1 && d.x == 0); return (Mathf.Abs(d.x) == 1 && d.y == 0) || (Mathf.Abs(d.y) == 1 && d.x == 0);

View File

@@ -2,19 +2,23 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Cysharp.Threading.Tasks; using Cysharp.Threading.Tasks;
using Models.Interfaces;
using Structs;
using UnityEngine; using UnityEngine;
namespace Services.Interfaces namespace Services.Interfaces
{ {
public interface IBombService public interface IBombService {
{ public BombSpawnRequest? PendingBombSpawn { get; }
IReadOnlyList<Vector2Int> CollectTriggeredBombs(IReadOnlyList<Vector2Int> matchPositions);
void SetLastSwap(Vector2Int from, Vector2Int to);
void ClearPendingBombs();
void DetectBombSpawnFromLastSwap(List<Gem> currentMatches);
UniTask DetonateChainAsync( UniTask DetonateChainAsync(
IReadOnlyList<Vector2Int> initialBombs, IReadOnlyList<Vector2Int> initialBombs,
Func<Vector2Int, bool> inBounds,
Func<Vector2Int, Gem> getGemAt,
Func<Vector2Int, UniTask> destroyAtAsync, Func<Vector2Int, UniTask> destroyAtAsync,
int radius, IGameBoard gameBoard);
float bombDelaySeconds);
} }
} }

View File

@@ -6,10 +6,7 @@ using Structs;
namespace Services.Interfaces { namespace Services.Interfaces {
public interface IMatchService { public interface IMatchService {
List<Gem> CurrentMatches { get; } List<Gem> CurrentMatches { get; }
IReadOnlyList<BombSpawnRequest> PendingBombSpawns { get; }
bool MatchesAt(Vector2Int positionToCheck, GemType gemTypeToCheck); bool MatchesAt(Vector2Int positionToCheck, GemType gemTypeToCheck);
void FindAllMatches(); void FindAllMatches();
void SetLastSwap(Vector2Int from, Vector2Int to);
void ClearPendingBombs();
} }
} }

View File

@@ -8,29 +8,15 @@ using UnityEngine;
namespace Services { namespace Services {
public class MatchService : IMatchService { public class MatchService : IMatchService {
private readonly IGameBoard gameBoard;
private List<Gem> currentMatches = new List<Gem>(); private List<Gem> currentMatches = new List<Gem>();
public List<Gem> CurrentMatches => this.currentMatches; public List<Gem> CurrentMatches => this.currentMatches;
private readonly List<BombSpawnRequest> pendingBombSpawns = new List<BombSpawnRequest>();
public IReadOnlyList<BombSpawnRequest> PendingBombSpawns => this.pendingBombSpawns;
private Vector2Int lastSwapFrom;
private Vector2Int lastSwapTo;
private readonly IGameBoard gameBoard;
public MatchService(IGameBoard gameBoard) { public MatchService(IGameBoard gameBoard) {
this.gameBoard = gameBoard; this.gameBoard = gameBoard;
} }
public void SetLastSwap(Vector2Int from, Vector2Int to) {
this.lastSwapFrom = from;
this.lastSwapTo = to;
}
public void ClearPendingBombs() {
this.pendingBombSpawns.Clear();
}
public bool MatchesAt(Vector2Int positionToCheck, GemType gemTypeToCheck) { public bool MatchesAt(Vector2Int positionToCheck, GemType gemTypeToCheck) {
Gem[,] gems = this.gameBoard.GemsGrid; Gem[,] gems = this.gameBoard.GemsGrid;
@@ -66,7 +52,6 @@ namespace Services {
public void FindAllMatches() { public void FindAllMatches() {
this.currentMatches.Clear(); this.currentMatches.Clear();
this.pendingBombSpawns.Clear();
for (int x = 0; x < this.gameBoard.Width; x++) for (int x = 0; x < this.gameBoard.Width; x++)
for (int y = 0; y < this.gameBoard.Height; y++) { for (int y = 0; y < this.gameBoard.Height; y++) {
@@ -101,66 +86,6 @@ namespace Services {
if (this.currentMatches.Count > 0) if (this.currentMatches.Count > 0)
this.currentMatches = this.currentMatches.Distinct().ToList(); this.currentMatches = this.currentMatches.Distinct().ToList();
DetectBombSpawnFromLastSwap();
}
private void DetectBombSpawnFromLastSwap() {
Vector2Int from = this.lastSwapFrom;
Vector2Int to = this.lastSwapTo;
TryCreateBombSpawnAt(from);
TryCreateBombSpawnAt(to);
}
private void TryCreateBombSpawnAt(Vector2Int pivot) {
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 (this.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.pendingBombSpawns.Any(b => b.Position == pivot))
return;
this.pendingBombSpawns.Add(new BombSpawnRequest(pivot, pivotGem.MatchColor));
}
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;
} }
} }
} }

View File

@@ -1,5 +1,8 @@
using Enums; using Enums;
using Models.Interfaces;
using Services;
using Structs; using Structs;
using UnityEngine;
namespace Utils { namespace Utils {
public static class GemUtils { public static class GemUtils {
@@ -10,5 +13,9 @@ namespace Utils {
return default; return default;
} }
public static bool IsInBounds(Vector2Int position, IGameBoard gameBoard) {
return position.x >= 0 && position.x < gameBoard.Width && position.y >= 0 && position.y < gameBoard.Height;
}
} }
} }

View File

@@ -10,5 +10,9 @@ namespace Utils {
public static Vector2 ToVector2(this Vector2Int v) { public static Vector2 ToVector2(this Vector2Int v) {
return new Vector2(v.x, v.y); return new Vector2(v.x, v.y);
} }
public static Vector2Int ToVector2Int(this Vector2 v) {
return new Vector2Int((int)v.x, (int)v.y);
}
} }
} }

View File

@@ -100,7 +100,7 @@ namespace Views {
return; return;
if (Vector2.Distance(this.transform.position, positionBasedOnIndex.ToVector2()) > 0.01f) { if (Vector2.Distance(this.transform.position, positionBasedOnIndex.ToVector2()) > 0.01f) {
this.transform.position = Vector2.Lerp(this.transform.position, positionBasedOnIndex.ToVector2(), gemSpeed); this.transform.position = Vector2.Lerp(this.transform.position, positionBasedOnIndex.ToVector2(), gemSpeed * Time.deltaTime);
} }
} }
} }