352 lines
13 KiB
C#
352 lines
13 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;
|
|
using Random = UnityEngine.Random;
|
|
|
|
namespace Services {
|
|
public class GameBoardService : IGameBoardService, ITickable, IDisposable {
|
|
#region Inject
|
|
private readonly IGameBoard gameBoard;
|
|
private readonly GameVariables gameVariables;
|
|
private readonly IMatchService matchService;
|
|
private readonly IScoreService scoreService;
|
|
private readonly IBombService bombService;
|
|
private readonly IObjectPool<GemView> objectPool;
|
|
private readonly Transform gemsHolder;
|
|
#endregion
|
|
|
|
#region Variables
|
|
private readonly List<GemPresenter> gemPresenters = new List<GemPresenter>();
|
|
private readonly ScorePresenter scorePresenter;
|
|
private GameState currentState = GameState.Move;
|
|
#endregion
|
|
|
|
public GameBoardService(IGameBoard gameBoard, GameVariables gameVariables, IMatchService matchService, IScoreService scoreSerivce, IBombService bombService, IObjectPool<GemView> objectPool, Transform gemsHolder, ScorePresenter scorePresenter) {
|
|
this.gameBoard = gameBoard;
|
|
this.gameVariables = gameVariables;
|
|
this.matchService = matchService;
|
|
this.scoreService = scoreSerivce;
|
|
this.bombService = bombService;
|
|
this.objectPool = objectPool;
|
|
this.gemsHolder = gemsHolder;
|
|
this.scorePresenter = scorePresenter;
|
|
}
|
|
|
|
public void Tick() {
|
|
foreach (GemPresenter gemPresenter in gemPresenters) {
|
|
gemPresenter.Tick(this.gameVariables.gemSpeed);
|
|
}
|
|
|
|
this.scorePresenter.Tick(this.gameVariables.scoreSpeed);
|
|
}
|
|
|
|
//Instantiates background tiles and calls SpawnGems
|
|
//Uses MatchService.MatchesAt to avoid matching Gems
|
|
public void Setup() {
|
|
for (int x = 0; x < this.gameBoard.Width; x++)
|
|
for (int y = 0; y < this.gameBoard.Height; y++)
|
|
{
|
|
Vector2 position = new Vector2(x, y);
|
|
GameObject backgroundTile = Object.Instantiate(this.gameVariables.bgTilePrefabs, position, Quaternion.identity);
|
|
backgroundTile.transform.SetParent(this.gemsHolder);
|
|
backgroundTile.name = "BG Tile - " + x + ", " + y;
|
|
|
|
int gemToUse = RandomUtils.RandomGemTypeAsInt();
|
|
|
|
int iterations = 0;
|
|
while (this.matchService.MatchesAt(new Vector2Int(x, y), (GemType)gemToUse) && iterations < 100)
|
|
{
|
|
gemToUse = RandomUtils.RandomGemTypeAsInt();
|
|
iterations++;
|
|
}
|
|
|
|
SpawnGem(new Vector2Int(x, y), (GemType)gemToUse);
|
|
}
|
|
|
|
this.currentState = GameState.Move;
|
|
}
|
|
|
|
//Uses the ObjectPool to spawn a gem at the given position
|
|
private void SpawnGem(Vector2Int position, GemType gemType) {
|
|
GemView gemView = this.objectPool.Get(gemType, position, this.gameVariables.dropHeight);
|
|
gemView.name = "Gem - " + position.x + ", " + position.y + ' ' + gemType;
|
|
|
|
// If we randomly spawned a bomb, give it a random color group (so it can match by color).
|
|
int scoreValue = GemUtils.GetGemValues(gemType, this.gameVariables.gemsPrefabs).scoreValue;
|
|
Gem gem = new Gem(gemType, position, scoreValue);
|
|
|
|
gemView.Bind(gem);
|
|
|
|
this.gemPresenters.Add(new GemPresenter(gem, gemView));
|
|
SetGem(new Vector2Int(position.x, position.y), 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, this.gameVariables.dropHeight);
|
|
gemView.name = "Gem - " + position.x + ", " + position.y + ' ' + GemType.Bomb;
|
|
|
|
int scoreValue = GemUtils.GetGemValues(color, this.gameVariables.gemsPrefabs).scoreValue;
|
|
Gem bombGem = new Gem(GemType.Bomb, position, scoreValue, color);
|
|
gemView.Bind(bombGem);
|
|
|
|
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
|
|
public async UniTask<bool> TrySwap(Vector2Int from, Vector2Int to) {
|
|
if (this.currentState != GameState.Move)
|
|
return false;
|
|
|
|
if (!InBounds(from) || !InBounds(to))
|
|
return false;
|
|
|
|
if (!AreAdjacentCardinal(from, to))
|
|
return false;
|
|
|
|
Gem fromGem = GetGem(from);
|
|
Gem toGem = GetGem(to);
|
|
|
|
if(fromGem == null || toGem == null)
|
|
return false;
|
|
|
|
this.currentState = GameState.Wait;
|
|
|
|
ApplySwap(from, to, fromGem, toGem);
|
|
|
|
await UniTask.Delay(600);
|
|
this.matchService.SetLastSwap(from, to);
|
|
this.matchService.FindAllMatches();
|
|
bool hasMatch = this.matchService.CurrentMatches.Count > 0;
|
|
|
|
if (!hasMatch) {
|
|
ApplySwap(to, from, fromGem, toGem);
|
|
await UniTask.Delay(600);
|
|
this.currentState = GameState.Move;
|
|
return false;
|
|
}
|
|
|
|
List<Vector2Int> protectedPositions = ApplyPendingBombSpawns();
|
|
|
|
await DestroyMatchesAsync(protectedPositions);
|
|
this.currentState = GameState.Move;
|
|
return true;
|
|
}
|
|
|
|
private void ApplySwap(Vector2Int posA, Vector2Int posB, Gem gemA, Gem gemB) {
|
|
// swap their stored positions
|
|
gemA.SetPosition(posB);
|
|
gemB.SetPosition(posA);
|
|
|
|
// update grid
|
|
SetGem(posA, gemB);
|
|
SetGem(posB, gemA);
|
|
}
|
|
|
|
private List<Vector2Int> ApplyPendingBombSpawns() {
|
|
List<Vector2Int> positions = new List<Vector2Int>();
|
|
|
|
foreach (BombSpawnRequest bomSpawnRequest in this.matchService.PendingBombSpawns) {
|
|
positions.Add(bomSpawnRequest.Position);
|
|
SpawnBomb(bomSpawnRequest.Position, bomSpawnRequest.Color);
|
|
}
|
|
|
|
this.matchService.ClearPendingBombs();
|
|
return positions;
|
|
}
|
|
|
|
private async UniTask DestroyMatchesAsync(List<Vector2Int> protectedPositions) {
|
|
// Collect match positions, excluding protected (bomb creation slots).
|
|
List<Vector2Int> matchPositions = new List<Vector2Int>(this.matchService.CurrentMatches.Count);
|
|
for (int i = 0; i < this.matchService.CurrentMatches.Count; i++) {
|
|
var m = this.matchService.CurrentMatches[i];
|
|
if (m == null) continue;
|
|
|
|
Vector2Int pos = m.Position;
|
|
if (protectedPositions != null && protectedPositions.Contains(pos))
|
|
continue;
|
|
|
|
matchPositions.Add(pos);
|
|
}
|
|
|
|
// Bombs are handled by BombService so they can respect delays + chaining.
|
|
foreach (Vector2Int pos in matchPositions.Distinct().ToList()) {
|
|
var g = GetGem(pos);
|
|
if (g == null) continue;
|
|
if (g.Type == GemType.Bomb) continue;
|
|
|
|
this.scoreService.ScoreCheck(g.ScoreValue);
|
|
DestroyMatchedGems(pos);
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
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) {
|
|
// center excluded for "neighbors first"
|
|
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;
|
|
}
|
|
}
|
|
|
|
private async UniTask MoveGemsDown() {
|
|
await UniTask.Delay(50);
|
|
|
|
int nullCounter = 0;
|
|
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)
|
|
{
|
|
nullCounter++;
|
|
}
|
|
else if (nullCounter > 0)
|
|
{
|
|
currentGem.SetPosition(new Vector2Int(currentGem.Position.x, currentGem.Position.y - nullCounter));
|
|
SetGem(currentGem.Position, currentGem);
|
|
SetGem(new Vector2Int(x,y), null);
|
|
}
|
|
}
|
|
nullCounter = 0;
|
|
}
|
|
|
|
await FillBoard();
|
|
}
|
|
|
|
private async UniTask FillBoard() {
|
|
await UniTask.Delay(5);
|
|
RefillBoard();
|
|
await UniTask.Delay(5);
|
|
|
|
this.matchService.FindAllMatches();
|
|
if (this.matchService.CurrentMatches.Count > 0) {
|
|
await UniTask.Delay(5);
|
|
|
|
// In cascades, there is no "creating slot" bomb protection.
|
|
await DestroyMatchesAsync(new List<Vector2Int>());
|
|
} else {
|
|
await UniTask.Delay(5);
|
|
this.currentState = GameState.Move;
|
|
}
|
|
}
|
|
|
|
private void RefillBoard() {
|
|
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) {
|
|
int gemToUse = RandomUtils.RandomGemTypeAsInt();
|
|
SpawnGem(new Vector2Int(x, y), (GemType)gemToUse);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void DestroyMatchedGems(Vector2Int position) {
|
|
List<GemView> gemsViews = GemsViews();
|
|
Gem currentGem = this.gameBoard.GetGemAt(position);
|
|
if (currentGem != null)
|
|
{
|
|
GemView gemView = gemsViews.FirstOrDefault(gv => gv.Gem == currentGem);
|
|
if (gemView is null) {
|
|
return;
|
|
}
|
|
|
|
this.objectPool.Release(gemView);
|
|
RemovePresenterFor(gemView);
|
|
SetGem(position, null);
|
|
}
|
|
}
|
|
|
|
#region Utils
|
|
private void RemovePresenterFor(GemView gemView) {
|
|
if (gemView is null) {
|
|
return;
|
|
}
|
|
|
|
GemPresenter presenter = this.gemPresenters.FirstOrDefault(p => p.GemView == gemView);
|
|
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) {
|
|
Vector2Int d = b - a;
|
|
return (Mathf.Abs(d.x) == 1 && d.y == 0) || (Mathf.Abs(d.y) == 1 && d.x == 0);
|
|
}
|
|
#endregion
|
|
|
|
public void Dispose() {
|
|
this.objectPool.Clear();
|
|
}
|
|
}
|
|
} |