222 lines
14 KiB
C#
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(); } }
|
|
}
|
|
}
|