Files
MagnetismHeroes/Assets/MagnetismHeroes/UnityRuntime/Bootstrap/GameApp.cs
T

222 lines
14 KiB
C#

using System;
using System.Linq;
using System.IO;
using MagnetismHeroes.Core;
using MagnetismHeroes.UnityRuntime.Interaction;
using MagnetismHeroes.UnityRuntime.Persistence;
using MagnetismHeroes.UnityRuntime.Presentation;
using MagnetismHeroes.UnityRuntime.StageAssets;
using TMPro;
using UnityEngine;
namespace MagnetismHeroes.UnityRuntime.Bootstrap
{
public sealed class GameApp : MonoBehaviour
{
[SerializeField] private StageAsset[] _stages;
[SerializeField] private GameObject _homePanel;
[SerializeField] private GameObject _selectionPanel;
[SerializeField] private GameObject _playPanel;
[SerializeField] private GameObject _world;
[SerializeField] private TMP_Text _instruction;
[SerializeField] private TMP_Text _counter;
[SerializeField] private TMP_Text _stageTitle;
[SerializeField] private TMP_Text _saveNotice;
[SerializeField] private UnityEngine.UI.Button _playButton;
[SerializeField] private UnityEngine.UI.Button _continueButton;
[SerializeField] private UnityEngine.UI.Button _stageOneButton;
[SerializeField] private UnityEngine.UI.Button _stageTwoButton;
[SerializeField] private UnityEngine.UI.Button _spawnButton;
[SerializeField] private UnityEngine.UI.Button _returnButton;
[SerializeField] private UnityEngine.UI.Button _homeButton;
[SerializeField] private UnityEngine.UI.Button _selectionHomeButton;
[SerializeField] private UnityEngine.UI.Button[] _compositionButtons = Array.Empty<UnityEngine.UI.Button>();
[SerializeField] private UnityEngine.UI.Button _splitButton;
[SerializeField] private UnityEngine.UI.Button _cancelSplitButton;
[SerializeField] private BoardPresenter _presenter;
[SerializeField] private OrbitCamera _orbit;
[SerializeField] private BoardInput _input;
[SerializeField] private VoicePlayer _voice;
[SerializeField] private UnityEngine.UI.Button[] _stageButtons = Array.Empty<UnityEngine.UI.Button>();
[SerializeField] private UnityEngine.UI.Button _helpButton;
[SerializeField] private UnityEngine.UI.Button _nextButton;
[SerializeField] private BoardEnvironment _environment;
[SerializeField] private GhostDemonstration _demonstration;
public GameSession Session { get; private set; }
public BoardPresenter Presenter => _presenter;
public BoardInput Input => _input;
public OrbitCamera Orbit => _orbit;
public VoicePlayer Voice => _voice;
public BoardEnvironment Environment => _environment;
public GhostDemonstration Demonstration => _demonstration;
public string Instruction => _instruction.text;
public string Counter => _counter.text;
private void Awake()
{
_playButton.onClick.AddListener(ShowSelection);
_continueButton.onClick.AddListener(ContinueFreePlay);
if (_stageButtons.Length == 0)
{
_stageOneButton.onClick.AddListener(() => StartStage(1));
if (_stageTwoButton != null) _stageTwoButton.onClick.AddListener(() => StartStage(2));
}
for (int index = 0; index < _stageButtons.Length; index++)
{ int stageId = _stages[index].StageId; _stageButtons[index].onClick.AddListener(() => StartStage(stageId)); }
if (_helpButton != null) _helpButton.onClick.AddListener(ShowHelp);
if (_nextButton != null) _nextButton.onClick.AddListener(StartNextStage);
if (_spawnButton != null) _spawnButton.onClick.AddListener(SpawnFriend);
if (_returnButton != null) _returnButton.onClick.AddListener(ReturnSelectedFriend);
_homeButton.onClick.AddListener(ShowHome);
_selectionHomeButton.onClick.AddListener(ShowHome);
for (int index = 0; _stageButtons.Length == 0 && index < _compositionButtons.Length; index++)
{ int stageId = _stages.Where(stage => stage.ToDefinition().Kind == StageKind.Composition).ElementAt(index).StageId; _compositionButtons[index].onClick.AddListener(() => StartStage(stageId)); }
if (_splitButton != null) _splitButton.onClick.AddListener(() => { _input.BeginSplit(); if (_input.IsSplitting) _voice.Guide("guide_split"); });
if (_cancelSplitButton != null) _cancelSplitButton.onClick.AddListener(() => { _input.CancelSplit(); _voice.CancelGuide(); });
}
private void Start()
{
if (Session == null) Initialize(new LocalProgressStore(Path.Combine(Application.persistentDataPath, "progress.json"),
_stages.Select(stage => stage.ToDefinition()).ToArray()));
}
public void Initialize(IProgressStore store)
{
Session = new GameSession(_stages.Select(stage => stage.ToDefinition()), store);
Session.SaveFinished += ShowSaveResult;
_saveNotice.text = Session.LastSaveError == null ? "" : Session.LastSaveError.StartsWith("Recovered", StringComparison.Ordinal)
? "이전 기록으로 이어서 시작할게요." : "기록을 읽지 못해 처음부터 시작할게요.";
if (Session.LastSaveError != null) Debug.LogWarning("[ProgressLoad] " + Session.LastSaveError);
Refresh(true);
}
private void ShowSaveResult(string error)
{
_saveNotice.text = error == null ? "" : "지금은 저장하지 못했어요. 다시 해볼게요.";
if (error != null) Debug.LogWarning("[ProgressSave] " + error);
}
public void SpawnFriend()
{
if (Session.Screen != SessionScreen.FreePlay || Session.Board.BallCount >= 10) return;
_input.CancelSplit();
if (!_presenter.TryFindSpawnPosition(out var position)) { _instruction.text = "빈 곳이 보이도록 화면을 움직여 볼까?"; return; }
if (Session.Spawn(position).Accepted) Refresh(false);
}
public void ReturnSelectedFriend()
{
if (Session.Screen != SessionScreen.FreePlay || _input.SelectedGroup == null) return;
string groupId = _input.SelectedGroup; _input.CancelSplit();
if (Session.ReturnGroup(groupId).Accepted) Refresh(false);
}
public void ShowSelection()
{ _input.CancelSplit(); _voice.Clear(); _demonstration?.Hide(); Session.ShowSelection(); Refresh(true); }
public void ShowHome()
{ _input.CancelSplit(); _voice.Clear(); _demonstration?.Hide(); Session.ShowHome(); Refresh(true); }
public void StartStage(int id)
{
_input.CancelSplit(); _voice.Clear(); _demonstration?.Hide(); bool restored = Session.HasUnfinishedStage(id);
if (!Session.StartStage(id)) return;
Refresh(true);
string tutorial = id == 6 ? "merge" : id == 10 ? "split" : null;
if (tutorial != null && !Session.HasSeenTutorial(tutorial) && _demonstration != null)
{ Session.MarkTutorialSeen(tutorial); _demonstration.Show(id == 10); _voice.Guide(id == 6 ? "demo_merge6" : "demo_split10"); return; }
// Restoring never replays historical count/celebration clips.
if (Session.Board.Mode == BoardMode.Counting && Session.Board.CollectedBallIds.Length == 0) _voice.Guide("guide_collect");
else if (!restored) _voice.Guide(id == 10 ? "guide_split" : "guide_merge");
}
public void ContinueFreePlay()
{ _input.CancelSplit(); _voice.Clear(); _demonstration?.Hide(); if (Session.ContinueFreePlay()) Refresh(true); }
public void StartNextStage()
{ if (Session.HasNextStage) StartStage(Session.Board.StageId + 1); }
public void ShowHelp()
{
var hint = Session.Hint; if (hint == null) return;
_voice.CancelGuide();
_instruction.text = hint.Kind == HintKind.Collect ? "이 친구를 찾아 끌어와 볼까?"
: hint.Kind == HintKind.Count ? "아직 세지 않은 이 구를 눌러 볼까?"
: hint.Kind == HintKind.Merge ? "빛나는 두 친구를 가까이 끌어와 붙여 볼까?"
: $"빛나는 {hint.GroupSize} 친구에서 나누기를 누르고 {hint.SelectionCount}개를 골라 끌어내 보자.";
_presenter.ShowHint(hint.BallIds); _voice.Guide(hint.VoiceId);
}
public void CommitMove(string groupId, Position3 delta, string mergeTarget = null)
{
var result = mergeTarget == null ? Session.MoveGroup(groupId, delta) : Session.MoveAndMerge(groupId, delta, mergeTarget);
if (result.Accepted) _voice.CancelGuide();
Refresh(false);
if (result.Accepted && Session.Screen == SessionScreen.Puzzle && Session.Board.CanCount) _voice.Guide("guide_count");
}
public bool CommitSplit(string groupId, string[] selected, Position3 delta)
{
string newId = groupId + "-split";
while (Session.Board.Groups.Any(group => group.id == newId)) newId += "-split";
var result = Session.SplitAndMove(groupId, selected, newId, delta);
if (result.Accepted) { _voice.CancelGuide(); Refresh(false); }
return result.Accepted;
}
public void CountBall(string ballId)
{
if (Session.Board.Mode != BoardMode.Counting) return;
var result = Session.Count(ballId);
if (result.Accepted) _voice.EnqueueCount(result.Count);
if (result.Accepted) Refresh(false);
if (!result.Accepted && result.Reason == RejectionReason.NotCollected)
{
_instruction.text = "친구를 먼저 끌어와 볼까?";
if (_presenter.Balls.TryGetValue(ballId, out var friend)) friend.Encourage();
}
}
private void Refresh(bool applyCamera)
{
var screen = Session.Screen;
_homePanel.SetActive(screen == SessionScreen.Home);
_selectionPanel.SetActive(screen == SessionScreen.StageSelection);
bool playing = screen == SessionScreen.Puzzle || screen == SessionScreen.FreePlay;
_playPanel.SetActive(playing); _world.SetActive(playing);
_continueButton.gameObject.SetActive(Session.HasFreePlay);
if (_stageTwoButton != null) _stageTwoButton.interactable = Session.IsUnlocked(2);
for (int index = 0; index < _stageButtons.Length; index++)
{
int id = _stages[index].StageId; bool unlocked = Session.IsUnlocked(id);
_stageButtons[index].interactable = unlocked;
string state = !unlocked ? "잠금" : Session.HasUnfinishedStage(id) ? "이어하기" : Session.CompletedStageIds.Contains(id) ? "완료" : "시작";
_stageButtons[index].GetComponentInChildren<TMP_Text>().text = $"{id} · {state}";
}
for (int index = 0; _stageButtons.Length == 0 && index < _compositionButtons.Length; index++) _compositionButtons[index].interactable = Session.IsUnlocked(_stages.Where(stage => stage.ToDefinition().Kind == StageKind.Composition).ElementAt(index).StageId);
if (_spawnButton != null) { _spawnButton.gameObject.SetActive(screen == SessionScreen.FreePlay); _spawnButton.interactable = Session.Board != null && Session.Board.BallCount < 10; }
if (_returnButton != null) _returnButton.gameObject.SetActive(screen == SessionScreen.FreePlay);
if (_helpButton != null) _helpButton.gameObject.SetActive(screen == SessionScreen.Puzzle);
if (_nextButton != null) _nextButton.gameObject.SetActive(Session.HasNextStage);
if (applyCamera) _orbit.Apply(Session.Camera);
_presenter.Render(Session.Board);
if (!playing) return;
_environment?.Apply(_stages.First(stage => stage.StageId == Session.Board.StageId));
_stageTitle.text = screen == SessionScreen.FreePlay ? "자유 놀이터" : $"숫자 {Session.Board.BallCount} 친구 찾기";
_counter.text = screen == SessionScreen.FreePlay ? $"친구 {Session.Board.BallCount}" : $"센 친구 {Session.Board.CountedCount} / {Session.Board.BallCount}";
_instruction.text = screen == SessionScreen.FreePlay ? "해냈어! 친구와 자유롭게 놀아 보자."
: Session.Board.CanCount ? "모두 모았네! 하나씩 눌러 세어 보자." : "친구를 찾아 끌어와 볼까?";
if (Session.Board.Mode == BoardMode.Composition)
{
var definition = Session.Definitions.First(stage => stage.Id == Session.Board.StageId);
_stageTitle.text = $"{Session.Board.StageId} 친구 만들기";
_counter.text = "목표 " + string.Join(" · ", definition.TargetGroups);
_instruction.text = "붙이거나 나누어 목표 친구를 만들어 보자.";
}
RefreshSelection();
}
public void RefreshSelection()
{
if (Session == null || _splitButton == null) return;
var group = Session.Board?.Groups.FirstOrDefault(value => value.id == _input.SelectedGroup);
if (_returnButton != null) _returnButton.interactable = Session.Screen == SessionScreen.FreePlay && group != null && !_input.IsSplitting;
_splitButton.gameObject.SetActive(!_input.IsSplitting);
_splitButton.interactable = group != null && group.ballIds.Length > 1;
_cancelSplitButton.gameObject.SetActive(_input.IsSplitting);
if (_input.IsSplitting) _instruction.text = $"선택 {_input.SelectedCount} · 구를 골라 끌어내 보자.";
else if (Session.Board?.Mode == BoardMode.Composition) _instruction.text = "붙이거나 나누어 목표 친구를 만들어 보자.";
else if (Session.Board?.Mode == BoardMode.FreePlay) _instruction.text = "해냈어! 친구와 자유롭게 놀아 보자.";
_presenter.ShowSelection(_input.SelectedIds);
}
private void OnApplicationPause(bool paused)
{ if (paused && Session != null) { _input.CancelSplit(); _voice.Clear(); _demonstration?.Hide(); Session.SaveCurrent(); } }
private void OnApplicationQuit() { if (Session != null) { _input.Cancel(); Session.SaveCurrent(); } }
}
}