509 lines
19 KiB
C#
509 lines
19 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Cysharp.Threading.Tasks;
|
|
using Enums;
|
|
using Models.Interfaces;
|
|
using Presenter;
|
|
using ScriptableObjects;
|
|
using Services.Interfaces;
|
|
using Structs;
|
|
using UnityEngine;
|
|
using Utils;
|
|
using VContainer.Unity;
|
|
using Views;
|
|
using Object = UnityEngine.Object;
|
|
|
|
namespace Services {
|
|
public class GameBoardService : IGameBoardService, ITickable, IDisposable {
|
|
#region Inject
|
|
private readonly GameVariables gameVariables;
|
|
|
|
private readonly IGameBoard gameBoard;
|
|
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 backgroundHolder;
|
|
#endregion
|
|
|
|
#region Variables
|
|
private readonly List<GemPresenter> gemPresenters = new List<GemPresenter>();
|
|
private readonly Dictionary<Gem, GemView> gemToView = new Dictionary<Gem, GemView>();
|
|
private GameState currentState = GameState.Setup;
|
|
#endregion
|
|
|
|
public GameBoardService(
|
|
GameVariables gameVariables,
|
|
IGameBoard gameBoard,
|
|
IObjectPool<GemView> objectPool,
|
|
IMatchService matchService,
|
|
IBombService bombService,
|
|
IScoreService scoreService,
|
|
ScorePresenter scorePresenter,
|
|
AudioPresenter audioPresenter,
|
|
Transform backgroundHolder) {
|
|
this.gameVariables = gameVariables;
|
|
this.gameBoard = gameBoard;
|
|
this.objectPool = objectPool;
|
|
this.matchService = matchService;
|
|
this.bombService = bombService;
|
|
this.scoreService = scoreService;
|
|
this.scorePresenter = scorePresenter;
|
|
this.audioPresenter = audioPresenter;
|
|
this.backgroundHolder = backgroundHolder;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Global tick. Update replacement of VContainer-managed objects.
|
|
/// </summary>
|
|
public void Tick() {
|
|
foreach (GemPresenter gemPresenter in gemPresenters) {
|
|
gemPresenter.Tick(this.gameVariables.gemSpeed);
|
|
}
|
|
|
|
this.scorePresenter.Tick(this.gameVariables.scoreSpeed);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initiates the game board by placing gems randomly.
|
|
/// </summary>
|
|
public void Setup() {
|
|
List<Gem> gemsToSpawn = new List<Gem>();
|
|
for (int x = 0; x < this.gameBoard.Width; x++)
|
|
for (int y = 0; y < this.gameBoard.Height; y++)
|
|
{
|
|
Vector2 position = new Vector2(x, y);
|
|
SpawnBackgroundTile(position);
|
|
|
|
int iterations = 0;
|
|
GemType gemToUse;
|
|
do {
|
|
gemToUse = RandomUtils.RandomGemType();
|
|
iterations++;
|
|
} while (this.matchService.MatchesAt(position.ToVector2Int(), gemToUse) && iterations < 100);
|
|
|
|
gemsToSpawn.Add(SetGemAt(position.ToVector2Int(), gemToUse));
|
|
}
|
|
|
|
SpawnCascade(gemsToSpawn).Forget();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a Gem object and place it on the GameBoard.
|
|
/// </summary>
|
|
/// <param name="position">
|
|
/// Position on the GameBoard to place the Gem.
|
|
/// </param>
|
|
/// <param name="gemType">
|
|
/// Type of Gem to create.
|
|
/// </param>
|
|
/// <param name="isBomb">
|
|
/// Whether the Gem is a Bomb or not.
|
|
/// </param>
|
|
/// <returns></returns>
|
|
private Gem SetGemAt(Vector2Int position, GemType gemType, bool isBomb = false) {
|
|
GemTypeValues gemValue = GemUtils.GetGemValues(gemType, this.gameVariables.gemsPrefabs);
|
|
Gem gem = new Gem(isBomb ? GemType.Bomb : gemType, position, gemValue, gemType);
|
|
this.gameBoard.SetGemAt(position, gem);
|
|
|
|
return gem;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Spawns a GemView object and binds it to the Gem object.
|
|
/// </summary>
|
|
/// <param name="gem">
|
|
/// Gem to bind to the GemView.
|
|
/// </param>
|
|
/// <param name="isBomb">
|
|
/// Whether the Gem is a Bomb or not.
|
|
/// </param>
|
|
private void SpawnGemGameObject(Gem gem, bool isBomb = false) {
|
|
GemView gemView = this.objectPool.Get(isBomb ? GemType.Bomb : gem.Type, gem.Position, isBomb ? 0 : this.gameVariables.dropHeight);
|
|
gemView.name = "Gem - " + gem.Position.x + ", " + gem.Position.y + ' ' + gem.Type;
|
|
|
|
GemTypeValues gemValue = GemUtils.GetGemValues(gem.MatchColor, this.gameVariables.gemsPrefabs);
|
|
gemView.Bind(gem, gemValue, isBomb: isBomb);
|
|
this.gemPresenters.Add(new GemPresenter(gem, gemView));
|
|
this.gemToView.Add(gem, gemView);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calls SetGemAt and SpawnGemGameObject.
|
|
/// </summary>
|
|
/// <param name="position">
|
|
/// Position on the GameBoard to place the Gem.
|
|
/// </param>
|
|
/// <param name="gemType">
|
|
/// Type of Gem to create.
|
|
/// </param>
|
|
/// <param name="isBomb">
|
|
/// Whether the Gem is a Bomb or not.
|
|
/// </param>
|
|
private void SetAndSpawnGem(Vector2Int position, GemType gemType, bool isBomb) {
|
|
if(isBomb)
|
|
ReleaseMatchedGems(position);
|
|
|
|
SpawnGemGameObject(SetGemAt(position, gemType, isBomb), isBomb);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Spawns a background tile at the given position. Only for setup.
|
|
/// </summary>
|
|
/// <param name="position">
|
|
/// Position on the GameBoard to place the tile.
|
|
/// </param>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to swap two gems on the GameBoard.
|
|
/// </summary>
|
|
/// <param name="from">
|
|
/// Original position of the gem to swap.
|
|
/// </param>
|
|
/// <param name="to">
|
|
/// Destination position of the gem to swap.
|
|
/// </param>
|
|
/// <returns></returns>
|
|
public async UniTask<bool> TrySwap(Vector2Int from, Vector2Int to) {
|
|
if (this.currentState != GameState.Move)
|
|
return false;
|
|
|
|
if (!GemUtils.IsInBounds(from, this.gameBoard) || !GemUtils.IsInBounds(to, this.gameBoard))
|
|
return false;
|
|
|
|
if (!from.IsAdjacent(to))
|
|
return false;
|
|
|
|
this.currentState = GameState.Wait;
|
|
|
|
ApplySwap(from, to);
|
|
|
|
await UniTask.Delay(this.gameVariables.swapDelayMs);
|
|
this.bombService.SetLastSwap(from, to);
|
|
this.matchService.FindAllMatches();
|
|
this.bombService.DetectBombSpawnFromLastSwap(this.matchService.CurrentMatches);
|
|
|
|
if (this.matchService.CurrentMatches.Count == 0) {
|
|
ApplySwap(to, from);
|
|
await UniTask.Delay(this.gameVariables.swapDelayMs);
|
|
this.currentState = GameState.Move;
|
|
return false;
|
|
}
|
|
|
|
List<Vector2Int> protectedPositions = this.bombService.ApplyPendingBombSpawns(SetAndSpawnGem);
|
|
await DestroyMatchesAsync(protectedPositions);
|
|
this.currentState = GameState.Move;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// For debug purposes. Switches the Gem at the given position to a different GemType.
|
|
/// </summary>
|
|
/// <param name="position">
|
|
/// Position of the Gem to switch.
|
|
/// </param>
|
|
/// <returns></returns>
|
|
public void TrySwitch(Vector2Int position) {
|
|
if (!this.gameVariables.debugMode)
|
|
return;
|
|
|
|
Gem gem = this.gameBoard.GetGemAt(position);
|
|
if(gem == null)
|
|
return;
|
|
|
|
GemType[] normalTypes = Enum.GetValues(typeof(GemType))
|
|
.Cast<GemType>()
|
|
.Where(t => t != GemType.Bomb)
|
|
.ToArray();
|
|
|
|
if (normalTypes.Length == 0)
|
|
return;
|
|
|
|
bool nextIsBomb;
|
|
GemType nextTypeOrMatchColor;
|
|
|
|
if (gem.Type != GemType.Bomb) {
|
|
int index = Array.IndexOf(normalTypes, gem.Type);
|
|
if (index < 0) index = 0;
|
|
|
|
if (index < normalTypes.Length - 1) {
|
|
nextIsBomb = false;
|
|
nextTypeOrMatchColor = normalTypes[index + 1];
|
|
} else {
|
|
nextIsBomb = true;
|
|
nextTypeOrMatchColor = normalTypes[0];
|
|
}
|
|
} else {
|
|
int idx = Array.IndexOf(normalTypes, gem.MatchColor);
|
|
if (idx < 0) idx = 0;
|
|
|
|
if (idx < normalTypes.Length - 1) {
|
|
nextIsBomb = true;
|
|
nextTypeOrMatchColor = normalTypes[idx + 1];
|
|
} else {
|
|
nextIsBomb = false;
|
|
nextTypeOrMatchColor = normalTypes[0];
|
|
}
|
|
}
|
|
|
|
// Replace both model+view by releasing current and respawning at the same position.
|
|
ReleaseMatchedGems(position);
|
|
SpawnGemGameObject(SetGemAt(position, nextTypeOrMatchColor, nextIsBomb), nextIsBomb);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper function for TrySwap. Swaps the Gems in the GameBoard.
|
|
/// </summary>
|
|
/// <param name="from">
|
|
/// Original position of the gem to swap.
|
|
/// </param>
|
|
/// <param name="to">
|
|
/// Destination position of the gem to swap.
|
|
/// </param>
|
|
private void ApplySwap(Vector2Int from, Vector2Int to) {
|
|
Gem fromGem = this.gameBoard.GetGemAt(from);
|
|
Gem toGem = this.gameBoard.GetGemAt(to);
|
|
// swap their stored positions
|
|
fromGem.SetPosition(to);
|
|
toGem.SetPosition(from);
|
|
|
|
// update grid
|
|
this.gameBoard.SetGemAt(from, toGem);
|
|
this.gameBoard.SetGemAt(to, fromGem);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Destroys all matches on the GameBoard.
|
|
/// </summary>
|
|
/// <param name="protectedPositions">
|
|
/// Gems we don't want to instantly destroy.
|
|
/// </param>
|
|
private async UniTask DestroyMatchesAsync(List<Vector2Int> protectedPositions) {
|
|
HashSet<Vector2Int> matchPositions = await this.matchService.GetMatchPositionsAsync(protectedPositions);
|
|
List<Vector2Int> initialBombs = await this.bombService.GetInitialBombs(protectedPositions, matchPositions);
|
|
|
|
if (initialBombs.Count > 0) {
|
|
await this.bombService.DetonateChainAsync(
|
|
initialBombs,
|
|
DestroyAtAsync);
|
|
|
|
foreach (Vector2Int p in matchPositions)
|
|
await DestroyAtAsync(p);
|
|
|
|
await UniTask.Delay(this.gameVariables.fillBoardDelayMs);
|
|
|
|
await MoveGemsDown();
|
|
return;
|
|
}
|
|
|
|
// For audio SFX
|
|
bool willBreakAnyNonBombGem = false;
|
|
foreach (Vector2Int pos in matchPositions) {
|
|
Gem g = this.gameBoard.GetGemAt(pos);
|
|
if (g != null && g.Type != GemType.Bomb) {
|
|
willBreakAnyNonBombGem = true;
|
|
break;
|
|
}
|
|
}
|
|
if (willBreakAnyNonBombGem)
|
|
this.audioPresenter.OnMatch(this.gameVariables.matchSfx);
|
|
|
|
// For score counting
|
|
foreach (Vector2Int pos in matchPositions) {
|
|
Gem gem = this.gameBoard.GetGemAt(pos);
|
|
if (gem == null) continue;
|
|
if (gem.Type == GemType.Bomb) continue;
|
|
|
|
this.scoreService.AddScore(gem.ScoreValue);
|
|
ReleaseMatchedGems(pos);
|
|
}
|
|
await UniTask.Delay(this.gameVariables.fillBoardDelayMs);
|
|
await MoveGemsDown();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Destroys the Gem at the given position.
|
|
/// </summary>
|
|
/// <param name="pos">
|
|
/// Position of the Gem to destroy.
|
|
/// </param>
|
|
/// <returns></returns>
|
|
private UniTask DestroyAtAsync(Vector2Int pos) {
|
|
Gem gem = this.gameBoard.GetGemAt(pos);
|
|
if (gem == null)
|
|
return UniTask.CompletedTask;
|
|
|
|
if (gem.Type == GemType.Bomb)
|
|
this.audioPresenter.OnBombExplosion(this.gameVariables.bombExplodeSfx);
|
|
|
|
this.scoreService.AddScore(gem.ScoreValue);
|
|
ReleaseMatchedGems(pos);
|
|
return UniTask.CompletedTask;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the GemView to the Object Pool and sets the Position in the GameBoard to null.
|
|
/// </summary>
|
|
/// <param name="position">
|
|
/// Position of the Gem to release.
|
|
/// </param>
|
|
private void ReleaseMatchedGems(Vector2Int position) {
|
|
Gem currentGem = this.gameBoard.GetGemAt(position);
|
|
if (currentGem != null)
|
|
{
|
|
if (!this.gemToView.TryGetValue(currentGem, out GemView gemView) || gemView == null)
|
|
return;
|
|
|
|
this.objectPool.Release(gemView);
|
|
RemovePresenterFor(gemView);
|
|
this.gameBoard.SetGemAt(position, null);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Moves Gems Down to free spaces.
|
|
/// </summary>
|
|
private async UniTask MoveGemsDown() {
|
|
List<FallMove> moves = new List<FallMove>();
|
|
while (true) {
|
|
moves.Clear();
|
|
for (int x = 0; x < this.gameBoard.Width; x++)
|
|
{
|
|
for (int y = 1; y < this.gameBoard.Height; y++)
|
|
{
|
|
Vector2Int from = new Vector2Int(x, y);
|
|
Vector2Int to = new Vector2Int(x, y - 1);
|
|
|
|
Gem gem = this.gameBoard.GetGemAt(from);
|
|
if (gem == null)
|
|
continue;
|
|
|
|
if (this.gameBoard.GetGemAt(to) != null)
|
|
continue;
|
|
|
|
moves.Add(new FallMove {
|
|
from = from,
|
|
to = to,
|
|
gem = gem
|
|
});
|
|
}
|
|
}
|
|
|
|
if (moves.Count == 0)
|
|
break;
|
|
|
|
foreach (FallMove move in moves) {
|
|
this.gameBoard.SetGemAt(move.to, move.gem);
|
|
this.gameBoard.SetGemAt(move.from, null);
|
|
move.gem.SetPosition(move.to);
|
|
}
|
|
|
|
await UniTask.Delay(this.gameVariables.cascadeDelayMs);
|
|
}
|
|
|
|
await FillBoard();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fills the GameBoard with gems. If there are resulting matches, process them.
|
|
/// </summary>
|
|
private async UniTask FillBoard() {
|
|
await RefillBoard();
|
|
|
|
this.matchService.FindAllMatches();
|
|
if (this.matchService.CurrentMatches.Count > 0) {
|
|
await UniTask.Delay(this.gameVariables.fillBoardDelayMs);
|
|
|
|
await DestroyMatchesAsync(new List<Vector2Int>());
|
|
} else {
|
|
this.currentState = GameState.Move;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Refills the GameBoard with gems.
|
|
/// </summary>
|
|
private async UniTask RefillBoard() {
|
|
List<Gem> gemsToSpawn = new List<Gem>();
|
|
for (int x = 0; x < this.gameBoard.Width; x++)
|
|
{
|
|
for (int y = 0; y < this.gameBoard.Height; y++)
|
|
{
|
|
Gem currentGem = this.gameBoard.GetGemAt(new Vector2Int(x,y));
|
|
if (currentGem == null) {
|
|
GemType gemToUse = RandomUtils.RandomGemType();
|
|
|
|
int iterations = 0;
|
|
while (this.matchService.MatchesAt(new Vector2Int(x, y), gemToUse) && iterations < 100)
|
|
{
|
|
gemToUse = RandomUtils.RandomGemType();
|
|
iterations++;
|
|
}
|
|
|
|
gemsToSpawn.Add(SetGemAt(new Vector2Int(x,y), gemToUse));
|
|
}
|
|
}
|
|
}
|
|
|
|
await SpawnCascade(gemsToSpawn);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Spawns gems in cascade.
|
|
/// </summary>
|
|
/// <param name="gemsToSpawn">
|
|
/// List of gems to spawn.
|
|
/// </param>
|
|
private async UniTask SpawnCascade(List<Gem> gemsToSpawn)
|
|
{
|
|
List<IGrouping<int, Gem>> groups = gemsToSpawn
|
|
.GroupBy(gem => gem.Position.y)
|
|
.OrderBy(group => group.Key)
|
|
.ToList();
|
|
|
|
for (int i = 0; i < groups.Count; i++)
|
|
{
|
|
foreach (Gem gem in groups[i])
|
|
SpawnGemGameObject(gem);
|
|
|
|
if (i < groups.Count - 1)
|
|
await UniTask.Delay(this.gameVariables.cascadeDelayMs);
|
|
}
|
|
|
|
if(this.currentState == GameState.Setup)
|
|
this.currentState = GameState.Move;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes the Presenter for the given GemView from the list to avoid calling its Tick method.
|
|
/// </summary>
|
|
/// <param name="gemView">
|
|
/// GemView to remove from the List of presenters.
|
|
/// </param>
|
|
private void RemovePresenterFor(GemView gemView) {
|
|
if (gemView is null) {
|
|
return;
|
|
}
|
|
|
|
GemPresenter presenter = this.gemPresenters.FirstOrDefault(p => p.GemView == gemView);
|
|
this.gemPresenters.Remove(presenter);
|
|
}
|
|
|
|
public void Dispose() {
|
|
this.objectPool.Clear();
|
|
this.gemPresenters.Clear();
|
|
this.gemToView.Clear();
|
|
}
|
|
}
|
|
} |