468 lines
14 KiB
C#
468 lines
14 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using Minesweeper.Config;
|
|
using Minesweeper.Core;
|
|
using Minesweeper.Presentation.Factories;
|
|
using TMPro;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
|
|
namespace Minesweeper.Presentation.Views
|
|
{
|
|
public sealed class GameView : MonoBehaviour, IGameView
|
|
{
|
|
private const float ResizeRefreshDelaySeconds = 0.5f;
|
|
private const float ResizeSizeEpsilon = 0.5f;
|
|
|
|
[SerializeField] private GameObject gameRoot;
|
|
[SerializeField] private GameObject pauseRoot;
|
|
[SerializeField] private GameObject resultRoot;
|
|
[SerializeField] private RectTransform boardPanel;
|
|
[SerializeField] private GridLayoutGroup gridLayoutGroup;
|
|
[SerializeField] private Button pauseButton;
|
|
[SerializeField] private Button restartButton;
|
|
[SerializeField] private Button resumeButton;
|
|
[SerializeField] private Button mainMenuButton;
|
|
[SerializeField] private Button resultRestartButton;
|
|
[SerializeField] private Button resultMainMenuButton;
|
|
[SerializeField] private TMP_Text resultText;
|
|
[SerializeField] private TMP_Text timerText;
|
|
[SerializeField] private TMP_Text mineText;
|
|
[SerializeField] private MinesweeperUiConfig uiConfig;
|
|
|
|
private readonly Dictionary<int, CellView> cellsByCoordinate = new Dictionary<int, CellView>();
|
|
private IReadOnlyList<BoardCellData> currentCells;
|
|
private bool boardInputEnabled = true;
|
|
private bool currentRevealUnflaggedMines;
|
|
private bool resizeRefreshPending;
|
|
private int currentBoardWidth;
|
|
private int currentBoardHeight;
|
|
private float currentPixelsPerUnitMultiplier = 1f;
|
|
private float resizeStableAt;
|
|
private Vector2 lastObservedLayoutSize;
|
|
|
|
public event Action RestartRequested;
|
|
public event Action GoToMenuRequested;
|
|
public event Action PauseRequested;
|
|
public event Action ResumeRequested;
|
|
public event Action CellPressStarted;
|
|
public event Action CellPressEnded;
|
|
public event Action<int, int> CellOpenRequested;
|
|
public event Action<int, int> CellFlagRequested;
|
|
|
|
private void Awake()
|
|
{
|
|
if (gameRoot == null)
|
|
{
|
|
gameRoot = gameObject;
|
|
}
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
AddButtonListeners();
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
RemoveButtonListeners();
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
TrackBoardResize();
|
|
}
|
|
|
|
public void ShowGame()
|
|
{
|
|
gameRoot.SetActive(true);
|
|
SetBoardRootActive(true);
|
|
}
|
|
|
|
public void HideGame()
|
|
{
|
|
gameRoot.SetActive(true);
|
|
SetBoardRootActive(false);
|
|
}
|
|
|
|
public void ShowPause()
|
|
{
|
|
if (pauseRoot != null)
|
|
{
|
|
pauseRoot.SetActive(true);
|
|
}
|
|
}
|
|
|
|
public void HidePause()
|
|
{
|
|
if (pauseRoot != null)
|
|
{
|
|
pauseRoot.SetActive(false);
|
|
}
|
|
}
|
|
|
|
public void ShowResult(GameState state)
|
|
{
|
|
if (resultRoot != null)
|
|
{
|
|
resultRoot.SetActive(true);
|
|
}
|
|
|
|
if (resultText != null)
|
|
{
|
|
resultText.text = state == GameState.Won ? "YOU WIN" : "GAME OVER";
|
|
}
|
|
}
|
|
|
|
public void HideResult()
|
|
{
|
|
if (resultRoot != null)
|
|
{
|
|
resultRoot.SetActive(false);
|
|
}
|
|
}
|
|
|
|
public void SetTimer(float seconds)
|
|
{
|
|
if (timerText != null)
|
|
{
|
|
timerText.text = Mathf.FloorToInt(seconds).ToString("00000");
|
|
}
|
|
}
|
|
|
|
public void SetMineCount(int minesCount)
|
|
{
|
|
if (mineText != null)
|
|
{
|
|
mineText.text = minesCount.ToString("00000");
|
|
}
|
|
}
|
|
|
|
public void BindConfig(MinesweeperUiConfig config)
|
|
{
|
|
uiConfig = config;
|
|
}
|
|
|
|
public void BindScreens(MinesweeperScreenRefs refs)
|
|
{
|
|
if (isActiveAndEnabled)
|
|
{
|
|
RemoveButtonListeners();
|
|
}
|
|
|
|
boardPanel = refs.BoardPanel;
|
|
gridLayoutGroup = refs.BoardGrid;
|
|
pauseRoot = refs.PauseRoot;
|
|
restartButton = refs.PauseRestartButton;
|
|
resumeButton = refs.PauseResumeButton;
|
|
mainMenuButton = refs.PauseMainMenuButton;
|
|
resultRoot = refs.ResultRoot;
|
|
resultRestartButton = refs.ResultRestartButton;
|
|
resultMainMenuButton = refs.ResultMainMenuButton;
|
|
resultText = refs.ResultText;
|
|
ResetResizeTracking();
|
|
|
|
if (isActiveAndEnabled)
|
|
{
|
|
AddButtonListeners();
|
|
}
|
|
}
|
|
|
|
public void RebuildBoard(IReadOnlyList<BoardCellData> cells, int width, int height, ICellViewFactory cellViewFactory, bool revealUnflaggedMines)
|
|
{
|
|
ClearBoard();
|
|
currentBoardWidth = width;
|
|
currentBoardHeight = height;
|
|
currentCells = cells;
|
|
currentRevealUnflaggedMines = revealUnflaggedMines;
|
|
ConfigureGrid(width, height);
|
|
|
|
for (var i = 0; i < cells.Count; i++)
|
|
{
|
|
CreateCell(cells[i], cellViewFactory);
|
|
}
|
|
|
|
RefreshBoard(cells, revealUnflaggedMines);
|
|
}
|
|
|
|
public void RefreshBoard(IReadOnlyList<BoardCellData> cells, bool revealUnflaggedMines)
|
|
{
|
|
currentCells = cells;
|
|
currentRevealUnflaggedMines = revealUnflaggedMines;
|
|
|
|
for (var i = 0; i < cells.Count; i++)
|
|
{
|
|
var cell = cells[i];
|
|
if (cellsByCoordinate.TryGetValue(ToKey(cell.X, cell.Y), out var view))
|
|
{
|
|
view.Render(cell, uiConfig, currentPixelsPerUnitMultiplier, revealUnflaggedMines);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void SetBoardInputEnabled(bool enabled)
|
|
{
|
|
boardInputEnabled = enabled;
|
|
foreach (var cell in cellsByCoordinate.Values)
|
|
{
|
|
cell.SetInputEnabled(enabled);
|
|
}
|
|
}
|
|
|
|
private void AddButtonListeners()
|
|
{
|
|
if (pauseButton != null)
|
|
{
|
|
pauseButton.onClick.AddListener(OnPauseClicked);
|
|
}
|
|
|
|
if (restartButton != null)
|
|
{
|
|
restartButton.onClick.AddListener(OnRestartClicked);
|
|
}
|
|
|
|
if (resumeButton != null)
|
|
{
|
|
resumeButton.onClick.AddListener(OnResumeClicked);
|
|
}
|
|
|
|
if (mainMenuButton != null)
|
|
{
|
|
mainMenuButton.onClick.AddListener(OnMainMenuClicked);
|
|
}
|
|
|
|
if (resultRestartButton != null)
|
|
{
|
|
resultRestartButton.onClick.AddListener(OnRestartClicked);
|
|
}
|
|
|
|
if (resultMainMenuButton != null)
|
|
{
|
|
resultMainMenuButton.onClick.AddListener(OnMainMenuClicked);
|
|
}
|
|
}
|
|
|
|
private void RemoveButtonListeners()
|
|
{
|
|
if (pauseButton != null)
|
|
{
|
|
pauseButton.onClick.RemoveListener(OnPauseClicked);
|
|
}
|
|
|
|
if (restartButton != null)
|
|
{
|
|
restartButton.onClick.RemoveListener(OnRestartClicked);
|
|
}
|
|
|
|
if (resumeButton != null)
|
|
{
|
|
resumeButton.onClick.RemoveListener(OnResumeClicked);
|
|
}
|
|
|
|
if (mainMenuButton != null)
|
|
{
|
|
mainMenuButton.onClick.RemoveListener(OnMainMenuClicked);
|
|
}
|
|
|
|
if (resultRestartButton != null)
|
|
{
|
|
resultRestartButton.onClick.RemoveListener(OnRestartClicked);
|
|
}
|
|
|
|
if (resultMainMenuButton != null)
|
|
{
|
|
resultMainMenuButton.onClick.RemoveListener(OnMainMenuClicked);
|
|
}
|
|
}
|
|
|
|
private void SetBoardRootActive(bool active)
|
|
{
|
|
if (boardPanel != null)
|
|
{
|
|
boardPanel.gameObject.SetActive(active);
|
|
}
|
|
|
|
if (active)
|
|
{
|
|
ResetResizeTracking();
|
|
}
|
|
}
|
|
|
|
private void TrackBoardResize()
|
|
{
|
|
if (boardPanel == null || !boardPanel.gameObject.activeInHierarchy || currentCells == null || currentBoardWidth <= 0 || currentBoardHeight <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var layoutSize = GetLayoutSourceSize();
|
|
if (layoutSize.x <= 0f || layoutSize.y <= 0f)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (HasSizeChanged(layoutSize, lastObservedLayoutSize))
|
|
{
|
|
lastObservedLayoutSize = layoutSize;
|
|
resizeStableAt = Time.unscaledTime + ResizeRefreshDelaySeconds;
|
|
resizeRefreshPending = true;
|
|
return;
|
|
}
|
|
|
|
if (resizeRefreshPending && Time.unscaledTime >= resizeStableAt)
|
|
{
|
|
resizeRefreshPending = false;
|
|
RefreshBoardLayout();
|
|
}
|
|
}
|
|
|
|
private void RefreshBoardLayout()
|
|
{
|
|
ConfigureGrid(currentBoardWidth, currentBoardHeight);
|
|
RefreshBoard(currentCells, currentRevealUnflaggedMines);
|
|
}
|
|
|
|
private void ConfigureGrid(int width, int height)
|
|
{
|
|
if (gridLayoutGroup == null || boardPanel == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Canvas.ForceUpdateCanvases();
|
|
|
|
var layoutSize = GetLayoutSourceSize();
|
|
var panelWidth = layoutSize.x > 0f ? layoutSize.x : uiConfig.ReferenceCellSize * width;
|
|
var panelHeight = layoutSize.y > 0f ? layoutSize.y : uiConfig.ReferenceCellSize * height;
|
|
var cellSize = CalculateCellSize(panelWidth, panelHeight, width, height);
|
|
var padding = cellSize * uiConfig.BoardPaddingRatio;
|
|
var spacing = cellSize * uiConfig.GridSpacingRatio;
|
|
|
|
boardPanel.anchorMin = Vector2.zero;
|
|
boardPanel.anchorMax = Vector2.one;
|
|
boardPanel.offsetMin = new Vector2(padding, padding);
|
|
boardPanel.offsetMax = new Vector2(-padding, -padding);
|
|
|
|
gridLayoutGroup.constraint = GridLayoutGroup.Constraint.FixedColumnCount;
|
|
gridLayoutGroup.constraintCount = width;
|
|
gridLayoutGroup.padding = new RectOffset();
|
|
gridLayoutGroup.spacing = new Vector2(spacing, spacing);
|
|
gridLayoutGroup.cellSize = new Vector2(cellSize, cellSize);
|
|
currentPixelsPerUnitMultiplier = uiConfig.ReferenceCellSize / cellSize;
|
|
lastObservedLayoutSize = layoutSize;
|
|
}
|
|
|
|
private Vector2 GetLayoutSourceSize()
|
|
{
|
|
var parentRect = boardPanel.parent as RectTransform;
|
|
var rect = parentRect != null ? parentRect.rect : boardPanel.rect;
|
|
return rect.size;
|
|
}
|
|
|
|
private void ResetResizeTracking()
|
|
{
|
|
resizeRefreshPending = false;
|
|
if (boardPanel != null)
|
|
{
|
|
lastObservedLayoutSize = GetLayoutSourceSize();
|
|
}
|
|
}
|
|
|
|
private static bool HasSizeChanged(Vector2 current, Vector2 previous)
|
|
{
|
|
return Mathf.Abs(current.x - previous.x) > ResizeSizeEpsilon || Mathf.Abs(current.y - previous.y) > ResizeSizeEpsilon;
|
|
}
|
|
|
|
private float CalculateCellSize(float panelWidth, float panelHeight, int width, int height)
|
|
{
|
|
var widthUnits = width + 2f * uiConfig.BoardPaddingRatio + Mathf.Max(0, width - 1) * uiConfig.GridSpacingRatio;
|
|
var heightUnits = height + 2f * uiConfig.BoardPaddingRatio + Mathf.Max(0, height - 1) * uiConfig.GridSpacingRatio;
|
|
var cellByWidth = panelWidth / widthUnits;
|
|
var cellByHeight = panelHeight / heightUnits;
|
|
return Mathf.Max(uiConfig.MinimumCellSize, Mathf.Floor(Mathf.Min(cellByWidth, cellByHeight)));
|
|
}
|
|
|
|
private void CreateCell(BoardCellData cell, ICellViewFactory cellViewFactory)
|
|
{
|
|
var view = cellViewFactory.CreateCell(cell, gridLayoutGroup.transform);
|
|
view.SetInputEnabled(boardInputEnabled);
|
|
view.OpenRequested += OnCellOpenRequested;
|
|
view.FlagRequested += OnCellFlagRequested;
|
|
view.PressStarted += OnCellPressStarted;
|
|
view.PressEnded += OnCellPressEnded;
|
|
cellsByCoordinate[ToKey(cell.X, cell.Y)] = view;
|
|
}
|
|
|
|
private void ClearBoard()
|
|
{
|
|
foreach (var cell in cellsByCoordinate.Values)
|
|
{
|
|
if (cell != null)
|
|
{
|
|
cell.OpenRequested -= OnCellOpenRequested;
|
|
cell.FlagRequested -= OnCellFlagRequested;
|
|
cell.PressStarted -= OnCellPressStarted;
|
|
cell.PressEnded -= OnCellPressEnded;
|
|
}
|
|
}
|
|
|
|
cellsByCoordinate.Clear();
|
|
|
|
if (gridLayoutGroup == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (var i = gridLayoutGroup.transform.childCount - 1; i >= 0; i--)
|
|
{
|
|
Destroy(gridLayoutGroup.transform.GetChild(i).gameObject);
|
|
}
|
|
}
|
|
|
|
private void OnCellOpenRequested(int x, int y)
|
|
{
|
|
CellOpenRequested?.Invoke(x, y);
|
|
}
|
|
|
|
private void OnCellFlagRequested(int x, int y)
|
|
{
|
|
CellFlagRequested?.Invoke(x, y);
|
|
}
|
|
|
|
private void OnCellPressStarted()
|
|
{
|
|
CellPressStarted?.Invoke();
|
|
}
|
|
|
|
private void OnCellPressEnded()
|
|
{
|
|
CellPressEnded?.Invoke();
|
|
}
|
|
|
|
private void OnPauseClicked()
|
|
{
|
|
PauseRequested?.Invoke();
|
|
}
|
|
|
|
private void OnRestartClicked()
|
|
{
|
|
RestartRequested?.Invoke();
|
|
}
|
|
|
|
private void OnResumeClicked()
|
|
{
|
|
ResumeRequested?.Invoke();
|
|
}
|
|
|
|
private void OnMainMenuClicked()
|
|
{
|
|
GoToMenuRequested?.Invoke();
|
|
}
|
|
|
|
private static int ToKey(int x, int y)
|
|
{
|
|
return (y << 16) ^ x;
|
|
}
|
|
}
|
|
}
|