using System.Collections; using TMPro; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.SceneManagement; using UnityEngine.UI; #if UNITY_EDITOR using UnityEditor; #endif public class GameFlowManager : MonoBehaviour { public enum FlowScreen { Intro, OpeningStory, Game, Result, DutyFreeShop, MapPreview } public static GameFlowManager Instance { get; private set; } public static bool IsGameplayActive { get { return Instance == null || Instance.gameplayActive; } } public GameSaveData SaveData { get { return saveData; } } public MapDefinition CurrentMap { get { return currentMap; } } private const string IntroSceneName = "Intro"; private const string GameSceneName = "Main"; private const string ResultSceneName = "Result"; private const string DutyFreeShopSceneName = "DutyFreeShop"; private const string MapPreviewSceneName = "MapPreview"; private const int ShieldCost = 120; private const int LunchboxCost = 80; private const int SpeedShoesCost = 140; private const int MileageCardCost = 160; private static readonly string[] OpeningStoryPageAssetPaths = { "Assets/ConceptArt/Opening/opening_cutscene_comic_jj_03_no_bubbles.png", "Assets/ConceptArt/Opening/opening_cutscene_comic_jj_04_prepare_no_bubbles.png", "Assets/ConceptArt/Opening/opening_cutscene_final_departure_jj_06_street_strict_stylematch.png" }; private GameSaveData saveData; private MapDefinition currentMap; private MapDefinition completedMap; private FlowScreen currentScreen = FlowScreen.Intro; private bool gameplayActive = false; private bool pendingStartGameplay = false; private bool lastResultCleared = false; private int lastResultScore = 0; private int lastResultMileage = 0; private float lastResultTime = 0f; private float lastResultDistance = 0f; private int openingStoryPageIndex = 0; private Canvas overlayCanvas; private RectTransform overlayRoot; private Image overlayBackground; private Camera flowCamera; private TMP_FontAsset runtimeFont; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] private static void Bootstrap() { if(Instance != null) { return; } GameObject flowObject = new GameObject("Game Flow Manager"); flowObject.AddComponent(); DontDestroyOnLoad(flowObject); } private void Awake() { if(Instance != null && Instance != this) { Destroy(gameObject); return; } Instance = this; DontDestroyOnLoad(gameObject); saveData = GameSaveManager.Load(); currentMap = MapDatabase.GetMapByIndex(saveData.currentMapIndex); SceneManager.sceneLoaded += OnSceneLoaded; } private void Start() { ShowIntro(); } private void OnDestroy() { if(Instance == this) { SceneManager.sceneLoaded -= OnSceneLoaded; Instance = null; } } private void OnSceneLoaded(Scene scene, LoadSceneMode mode) { EnsureOverlay(); if(pendingStartGameplay) { StartCoroutine(StartGameplayAfterSceneLoad()); return; } if(currentScreen != FlowScreen.Game) { RefreshCurrentScreen(); } } public void SetTotalMileage(int totalMileage) { if(saveData == null) { return; } saveData.totalMileage = Mathf.Max(0, totalMileage); GameSaveManager.Save(saveData); } public void AddMileage(int amount) { if(saveData == null) { return; } GameSaveManager.AddMileage(saveData, amount); } public void CompleteMap(bool cleared, int score, int stageMileage, float elapsedTime, float distance) { if(!gameplayActive && currentScreen == FlowScreen.Result) { return; } gameplayActive = false; currentScreen = FlowScreen.Result; completedMap = currentMap; lastResultCleared = cleared; lastResultScore = score; lastResultMileage = stageMileage; lastResultTime = elapsedTime; lastResultDistance = distance; if(cleared) { saveData.bestScore = Mathf.Max(saveData.bestScore, score); GameSaveManager.AddMileage(saveData, completedMap.clearMileageReward); if(completedMap.hasNextMap) { GameSaveManager.UnlockMap(saveData, completedMap.nextMapId); } else { GameSaveManager.Save(saveData); } } ShowResult(); } private void StartNewJourney() { saveData = GameSaveManager.ResetSave(); currentMap = MapDatabase.GetMapByIndex(saveData.currentMapIndex); completedMap = null; openingStoryPageIndex = 0; ShowOpeningStory(); } private void ContinueJourney() { saveData = GameSaveManager.Load(); currentMap = MapDatabase.GetMapByIndex(saveData.currentMapIndex); completedMap = null; ShowMapPreview(); } private void RetryCurrentMap() { currentMap = MapDatabase.GetMapByIndex(saveData.currentMapIndex); LoadGameSceneForCurrentMap(); } private void LoadGameSceneForCurrentMap() { currentScreen = FlowScreen.Game; gameplayActive = false; pendingStartGameplay = true; ShowLoading("탑승 준비 중", currentMap.displayName + " 코스를 불러오는 중입니다."); if(Application.CanStreamedLevelBeLoaded(GameSceneName)) { SceneManager.LoadScene(GameSceneName); return; } Scene activeScene = SceneManager.GetActiveScene(); if(!string.IsNullOrEmpty(activeScene.name)) { SceneManager.LoadScene(activeScene.name); } } private IEnumerator StartGameplayAfterSceneLoad() { yield return null; pendingStartGameplay = false; currentScreen = FlowScreen.Game; gameplayActive = true; SetOverlayVisible(false); if(GameManager.instance != null) { GameManager.instance.BeginMap(currentMap); } } private void ShowOpeningStory() { currentScreen = FlowScreen.OpeningStory; gameplayActive = false; pendingStartGameplay = false; if(TryLoadFlowScene(IntroSceneName)) { return; } EnsureOverlay(); ClearOverlay(); SetOverlayVisible(true); BuildOpeningStoryScreen(); } private void ShowIntro() { currentScreen = FlowScreen.Intro; gameplayActive = false; pendingStartGameplay = false; completedMap = null; if(TryLoadFlowScene(IntroSceneName)) { return; } EnsureOverlay(); ClearOverlay(); SetOverlayVisible(true); BuildIntroScreen(); } private void ShowResult() { if(TryLoadFlowScene(ResultSceneName)) { return; } EnsureOverlay(); ClearOverlay(); SetOverlayVisible(true); SetDefaultOverlayBackground(); string title = lastResultCleared ? "맵 클리어" : "일정 실패"; string body = completedMap.displayName + "\n점수: " + lastResultScore + "\n획득 마일리지: " + lastResultMileage + "\n거리: " + Mathf.FloorToInt(lastResultDistance) + "m / " + Mathf.FloorToInt(completedMap.targetDistance) + "m" + "\n시간: " + lastResultTime.ToString("0.0") + "s"; if(lastResultCleared) { body += "\n클리어 보상: " + completedMap.clearMileageReward + " 마일리지"; } AddTitle(title); AddBody(body); if(lastResultCleared) { AddButton("면세점으로", ShowDutyFreeShop); } else { AddButton("다시 도전", RetryCurrentMap); } AddButton("인트로", ShowIntro); } private void ShowDutyFreeShop() { currentScreen = FlowScreen.DutyFreeShop; gameplayActive = false; pendingStartGameplay = false; if(TryLoadFlowScene(DutyFreeShopSceneName)) { return; } EnsureOverlay(); ClearOverlay(); SetOverlayVisible(true); SetDefaultOverlayBackground(); AddTitle("면세점"); AddBody("보유 마일리지: " + saveData.totalMileage + "\n도시락: " + saveData.lunchboxStock + " / 방어막: " + saveData.shieldStock + "\n운동화: " + saveData.speedShoesStock + " / 마일리지 카드: " + saveData.mileageCardStock); AddButton("도시락 구매 " + LunchboxCost, BuyLunchbox); AddButton("방어막 구매 " + ShieldCost, BuyShield); AddButton("운동화 구매 " + SpeedShoesCost, BuySpeedShoes); AddButton("마일리지 카드 구매 " + MileageCardCost, BuyMileageCard); AddButton("다음 맵 보기", ShowMapPreview); } private void ShowMapPreview() { currentScreen = FlowScreen.MapPreview; gameplayActive = false; pendingStartGameplay = false; if(completedMap != null && lastResultCleared && completedMap.hasNextMap) { currentMap = MapDatabase.GetMap(completedMap.nextMapId); } else { currentMap = MapDatabase.GetMapByIndex(saveData.currentMapIndex); } if(TryLoadFlowScene(MapPreviewSceneName)) { return; } EnsureOverlay(); ClearOverlay(); SetOverlayVisible(true); SetDefaultOverlayBackground(); AddTitle("다음 경로"); AddBody(currentMap.displayName + "\n" + currentMap.routeName + "\n목표 거리: " + Mathf.FloorToInt(currentMap.targetDistance) + "m" + "\n제한 시간: " + currentMap.timeLimit.ToString("0") + "s"); AddButton("출발", ConfirmMapAndStart); AddButton("인트로", ShowIntro); } private void ConfirmMapAndStart() { saveData.currentMapIndex = MapDatabase.GetIndex(currentMap.mapId); saveData.unlockedMapIndex = Mathf.Max(saveData.unlockedMapIndex, saveData.currentMapIndex); GameSaveManager.Save(saveData); LoadGameSceneForCurrentMap(); } private void RefreshCurrentScreen() { switch(currentScreen) { case FlowScreen.Result: ShowResult(); break; case FlowScreen.DutyFreeShop: ShowDutyFreeShop(); break; case FlowScreen.MapPreview: ShowMapPreview(); break; case FlowScreen.OpeningStory: ShowOpeningStory(); break; case FlowScreen.Intro: default: ShowIntro(); break; } } private void ResetAndStayIntro() { saveData = GameSaveManager.ResetSave(); currentMap = MapDatabase.GetMapByIndex(saveData.currentMapIndex); ShowIntro(); } private void BuyLunchbox() { if(TrySpendMileage(LunchboxCost)) { saveData.lunchboxStock++; GameSaveManager.Save(saveData); } ShowDutyFreeShop(); } private void BuyShield() { if(TrySpendMileage(ShieldCost)) { saveData.shieldStock++; GameSaveManager.Save(saveData); } ShowDutyFreeShop(); } private void BuySpeedShoes() { if(TrySpendMileage(SpeedShoesCost)) { saveData.speedShoesStock++; GameSaveManager.Save(saveData); } ShowDutyFreeShop(); } private void BuyMileageCard() { if(TrySpendMileage(MileageCardCost)) { saveData.mileageCardStock++; GameSaveManager.Save(saveData); } ShowDutyFreeShop(); } private bool TrySpendMileage(int cost) { if(saveData.totalMileage < cost) { return false; } saveData.totalMileage -= cost; return true; } private void ShowLoading(string title, string body) { EnsureOverlay(); ClearOverlay(); SetOverlayVisible(true); SetDefaultOverlayBackground(); AddTitle(title); AddBody(body); } private bool TryLoadFlowScene(string sceneName) { Scene activeScene = SceneManager.GetActiveScene(); if(activeScene.name == sceneName) { return false; } if(!Application.CanStreamedLevelBeLoaded(sceneName)) { return false; } SceneManager.LoadScene(sceneName); return true; } private void EnsureOverlay() { EnsureEventSystem(); if(overlayCanvas != null) { return; } GameObject canvasObject = new GameObject("Flow Overlay", typeof(RectTransform), typeof(Canvas), typeof(CanvasScaler), typeof(GraphicRaycaster)); DontDestroyOnLoad(canvasObject); overlayCanvas = canvasObject.GetComponent(); overlayCanvas.renderMode = RenderMode.ScreenSpaceOverlay; overlayCanvas.worldCamera = null; overlayCanvas.sortingOrder = 100; SetUiLayer(canvasObject); CanvasScaler scaler = canvasObject.GetComponent(); scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize; scaler.referenceResolution = new Vector2(1280f, 720f); scaler.matchWidthOrHeight = 0.5f; GameObject rootObject = new GameObject("Flow Overlay Root", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image)); rootObject.transform.SetParent(canvasObject.transform, false); SetUiLayer(rootObject); overlayRoot = rootObject.GetComponent(); overlayRoot.anchorMin = Vector2.zero; overlayRoot.anchorMax = Vector2.one; overlayRoot.offsetMin = Vector2.zero; overlayRoot.offsetMax = Vector2.zero; Image background = rootObject.GetComponent(); overlayBackground = background; background.color = new Color(0.02f, 0.035f, 0.045f, 0.92f); background.raycastTarget = true; runtimeFont = CreateKoreanFontAsset(); } private void SetDefaultOverlayBackground() { if(overlayBackground != null) { overlayBackground.color = new Color(0.02f, 0.035f, 0.045f, 0.92f); } if(flowCamera != null) { flowCamera.backgroundColor = new Color(0.02f, 0.035f, 0.045f, 1f); } } private void BuildIntroScreen() { if(overlayBackground != null) { overlayBackground.color = Color.clear; } if(flowCamera != null) { flowCamera.backgroundColor = new Color(0.006f, 0.022f, 0.035f, 1f); } RectTransform mapRect = AddIntroWorldMap(); Vector2[] routePoints = GetIntroRoutePoints(); AddIntroRoute(mapRect, routePoints); AddIntroPlane(mapRect, routePoints); AddIntroPassportAndPhone(); AddIntroTitlePanel(); AddIntroButtonDock(); } private void BuildOpeningStoryScreen() { if(overlayBackground != null) { overlayBackground.color = new Color(0.012f, 0.018f, 0.024f, 0.96f); } if(flowCamera != null) { flowCamera.backgroundColor = new Color(0.012f, 0.018f, 0.024f, 1f); } openingStoryPageIndex = Mathf.Clamp(openingStoryPageIndex, 0, OpeningStoryPageAssetPaths.Length - 1); AddOpeningStoryPage(); AddOpeningStoryProgressDots(); AddOpeningStoryControls(); } private void AddOpeningStoryPage() { GameObject shadowObject = CreateUiObject("Opening Story Shadow", overlayRoot, typeof(Image)); RectTransform shadowRect = shadowObject.GetComponent(); shadowRect.anchorMin = new Vector2(0.5f, 0.5f); shadowRect.anchorMax = new Vector2(0.5f, 0.5f); shadowRect.pivot = new Vector2(0.5f, 0.5f); shadowRect.anchoredPosition = new Vector2(7f, 18f); shadowRect.sizeDelta = new Vector2(1132f, 637f); Image shadowImage = shadowObject.GetComponent(); shadowImage.color = new Color(0f, 0f, 0f, 0.34f); shadowImage.raycastTarget = false; GameObject pageObject = CreateUiObject("Opening Story Page", overlayRoot, typeof(Image), typeof(Button)); RectTransform pageRect = pageObject.GetComponent(); pageRect.anchorMin = new Vector2(0.5f, 0.5f); pageRect.anchorMax = new Vector2(0.5f, 0.5f); pageRect.pivot = new Vector2(0.5f, 0.5f); pageRect.anchoredPosition = new Vector2(0f, 24f); pageRect.sizeDelta = new Vector2(1120f, 630f); Image pageImage = pageObject.GetComponent(); pageImage.sprite = LoadUiTextureSprite(OpeningStoryPageAssetPaths[openingStoryPageIndex]); pageImage.color = pageImage.sprite != null ? Color.white : new Color(0.91f, 0.87f, 0.78f, 1f); pageImage.preserveAspect = true; Button pageButton = pageObject.GetComponent