Files
MagnetismHeroes/Assets/MagnetismHeroes/Core/GameSession.cs
T

137 lines
7.2 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
namespace MagnetismHeroes.Core
{
public enum SessionScreen { Home, StageSelection, Puzzle, FreePlay }
// Owns committed game state. A gesture preview never reaches this class.
public sealed class GameSession
{
private readonly StageDefinition[] _definitions;
private readonly IProgressStore _store;
private ProgressSave _progress;
public SessionScreen Screen { get; private set; } = SessionScreen.Home;
public BoardRules Board { get; private set; }
public CameraState Camera { get; private set; } = DefaultCamera();
public string LastSaveError { get; private set; }
public event Action<string> SaveFinished;
public bool HasFreePlay => _progress.latestFreePlay != null;
public int[] CompletedStageIds => (int[])_progress.completedStageIds.Clone();
public IReadOnlyList<StageDefinition> Definitions => Array.AsReadOnly(_definitions);
public bool HasUnfinishedStage(int stageId) => _progress.puzzleStates.Any(state => state.stageId == stageId);
public bool HasSeenTutorial(string id) => _progress.seenTutorialIds.Contains(id);
public BoardHint Hint => BoardHint.Find(Board, _definitions.FirstOrDefault(stage => stage.Id == Board?.StageId));
public bool HasNextStage => Screen == SessionScreen.FreePlay && _definitions.Any(stage => stage.Id == Board.StageId + 1) && IsUnlocked(Board.StageId + 1);
public bool StartNextStage() => HasNextStage && StartStage(Board.StageId + 1);
public bool MarkTutorialSeen(string id)
{
if ((id != "merge" && id != "split") || HasSeenTutorial(id)) return false;
_progress.seenTutorialIds = _progress.seenTutorialIds.Concat(new[] { id }).ToArray();
SaveCurrent(); return true;
}
public GameSession(IEnumerable<StageDefinition> definitions, IProgressStore store)
{
_definitions = definitions.ToArray();
_store = store ?? throw new ArgumentNullException(nameof(store));
if (!_store.TryLoad(out var loaded, out var error)) loaded = new ProgressSave();
LastSaveError = error;
if (!SaveValidator.TryValidate(loaded, _definitions, out error))
{ loaded = new ProgressSave(); LastSaveError = error; }
_progress = loaded;
}
public bool IsUnlocked(int stageId) => stageId == 1 || _progress.completedStageIds.Contains(stageId)
|| (stageId >= 2 && stageId <= 30 && _progress.completedStageIds.Contains(stageId - 1));
public void ShowSelection() { SaveCurrent(); Screen = SessionScreen.StageSelection; Board = null; }
public void ShowHome() { SaveCurrent(); Screen = SessionScreen.Home; Board = null; }
public bool StartStage(int stageId)
{
var definition = _definitions.FirstOrDefault(value => value.Id == stageId);
if (definition == null || !IsUnlocked(stageId)) return false;
SaveCurrent();
var restored = _progress.puzzleStates.FirstOrDefault(state => state.stageId == stageId);
Board = new BoardRules(definition, restored);
Camera = restored == null ? DefaultCamera() : CopyCamera(restored.camera);
Screen = SessionScreen.Puzzle;
SaveCurrent();
return true;
}
public bool ContinueFreePlay()
{
if (_progress.latestFreePlay == null) return false;
SaveCurrent();
Board = BoardRules.CreateFreePlay(_progress.latestFreePlay);
Camera = CopyCamera(_progress.latestFreePlay.camera);
Screen = SessionScreen.FreePlay;
return true;
}
public RuleResult MoveGroup(string groupId, Position3 delta) => Commit(() => Board.TryMoveGroup(groupId, delta));
public RuleResult Merge(string first, string second) => Commit(() => Board.TryMerge(first, second));
public RuleResult MoveAndMerge(string groupId, Position3 delta, string targetGroupId) => Commit(() => Board.TryMoveAndMerge(groupId, delta, targetGroupId));
public RuleResult SplitAndMove(string groupId, IReadOnlyList<string> selectedIds, string newGroupId, Position3 delta) =>
Commit(() => Board.TrySplitAndMove(groupId, selectedIds, newGroupId, delta));
public RuleResult Count(string ballId) => Commit(() => Board.TryCount(ballId));
public RuleResult ReturnGroup(string groupId) => Commit(() => Board.TryReturn(groupId));
public RuleResult Spawn(Position3 position)
{
if (Board == null) return RuleResult.Reject(RejectionReason.InvalidGroup);
int index = 1;
while (Board.Balls.Any(ball => ball.id == $"free-b{index:D2}") || Board.Groups.Any(group => group.id == $"free-g{index:D2}")) index++;
return Commit(() => Board.TrySpawn($"free-b{index:D2}", $"free-g{index:D2}", position));
}
public void CommitCamera(CameraState camera)
{
// Board snapshot methods validate all camera coordinates and rotation.
Camera = Board == null ? CopyCamera(camera) : Board.ToFreePlayState(camera).camera;
SaveCurrent();
}
private RuleResult Commit(Func<RuleResult> mutation)
{
if (Board == null) return RuleResult.Reject(RejectionReason.InvalidGroup);
var result = mutation();
if (!result.Accepted) return result;
if (Screen == SessionScreen.Puzzle && Board.IsSolved)
{
// Completion, removing that unfinished puzzle, and creating free play are one saved update.
_progress.completedStageIds = _progress.completedStageIds.Concat(new[] { Board.StageId }).Distinct().OrderBy(id => id).ToArray();
_progress.puzzleStates = _progress.puzzleStates.Where(state => state.stageId != Board.StageId).ToArray();
_progress.latestFreePlay = Board.ToFreePlayState(Camera);
Board = BoardRules.CreateFreePlay(_progress.latestFreePlay);
Screen = SessionScreen.FreePlay;
}
SaveCurrent();
return result;
}
public bool SaveCurrent()
{
if (Board != null)
{
if (Screen == SessionScreen.Puzzle)
_progress.puzzleStates = _progress.puzzleStates.Where(state => state.stageId != Board.StageId)
.Concat(new[] { Board.ToPuzzleState(Camera) }).OrderBy(state => state.stageId).ToArray();
else if (Screen == SessionScreen.FreePlay) _progress.latestFreePlay = Board.ToFreePlayState(Camera);
}
var success = _store.TrySave(_progress, out var error);
LastSaveError = error;
SaveFinished?.Invoke(error);
return success;
}
public static CameraState DefaultCamera() => new CameraState {
target = new Position3(0, 0.6f, 0), rotation = new Rotation4(0, 0, 0, 1), distance = 8 };
private static CameraState CopyCamera(CameraState camera) => new CameraState {
target = camera.target, rotation = camera.rotation, distance = camera.distance };
}
}