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(); [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(); [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().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(); } } } }