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, DebugMenu } 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 string DepartureStampResourcePath = "UI_DepartureStamp_Red"; private const string ResultPassportPanelResourcePath = "UI_ResultBoardingPassPanel_New"; private const string ResultPassportPanelAssetPath = "Assets/Resources/UI_ResultBoardingPassPanel_New.png"; private const string ResultAirportBackgroundResourcePath = "Map_Tutorial_Incheon_Airport"; private const string ResultButtonResourcePath = "UI_TutorialNextButton"; private const string ResultButtonAssetPath = "Assets/Resources/UI_TutorialNextButton.png"; private const string MileageIconAssetPath = "Assets/Sprites/UIIcons/UI_Mileage.png"; private const string TimerIconAssetPath = "Assets/Sprites/UIIcons/UI_Timer.png"; private const string CheckpointIconAssetPath = "Assets/Sprites/UIIcons/UI_Checkpoint.png"; private const string ClearRankIconAssetPath = "Assets/Sprites/UIIcons/UI_ClearRank.png"; private const string DutyFreeIconAssetPath = "Assets/Sprites/UIIcons/UI_DutyFreeShop.png"; private const string AirplaneTravelIconAssetPath = "Assets/Sprites/UIIcons/UI_AirplaneTravel.png"; private const string DutyFreeBackgroundAssetPath = "Assets/ConceptArt/duty_free_shop_screen_concept_01.png"; private const string DutyFreeLunchboxAssetPath = "Assets/Sprites/DutyFree/DutyFree_Normal_Lunchbox.png"; private const string DutyFreeShieldAssetPath = "Assets/Sprites/DutyFree/DutyFree_Normal_ShieldCharm.png"; private const string DutyFreeShoesAssetPath = "Assets/Sprites/DutyFree/DutyFree_Normal_Shoes.png"; private const string DutyFreeMileageCardAssetPath = "Assets/Sprites/DutyFree/DutyFree_Normal_MileageCard.png"; private const string SouvenirCollectionIconAssetPath = "Assets/Sprites/UIIcons/UI_SouvenirCollection.png"; private const string AirportSouvenirAssetPath = "Assets/Sprites/Souvenirs/Souvenir_AirportMagnet.png"; private const string JapanSouvenirAssetPath = "Assets/Sprites/Souvenirs/Souvenir_Japan_Magnet.png"; private const string ChinaSouvenirAssetPath = "Assets/Sprites/Souvenirs/Souvenir_TempleCharm.png"; private const string AmericaSouvenirAssetPath = "Assets/Sprites/Souvenirs/Souvenir_SkylineMagnet.png"; 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(); } #if UNITY_EDITOR || DEVELOPMENT_BUILD private void Update() { if(Input.GetKeyDown(KeyCode.Escape)) { ShowDebugMenu(); return; } HandleDebugMenuShortcutInput(); } private void HandleDebugMenuShortcutInput() { if(currentScreen != FlowScreen.DebugMenu) { return; } if(Input.GetKeyDown(KeyCode.Alpha1) || Input.GetKeyDown(KeyCode.Keypad1)) { ShowDebugIntroPreview(); } else if(Input.GetKeyDown(KeyCode.Alpha2) || Input.GetKeyDown(KeyCode.Keypad2)) { ShowDebugGameStartPreview(); } else if(Input.GetKeyDown(KeyCode.Alpha3) || Input.GetKeyDown(KeyCode.Keypad3)) { ShowDebugResultPreview(false); } else if(Input.GetKeyDown(KeyCode.Alpha4) || Input.GetKeyDown(KeyCode.Keypad4)) { ShowDebugResultPreview(true); } else if(Input.GetKeyDown(KeyCode.Alpha5) || Input.GetKeyDown(KeyCode.Keypad5)) { ShowDebugDutyFreeShopPreview(); } else if(Input.GetKeyDown(KeyCode.Alpha6) || Input.GetKeyDown(KeyCode.Keypad6)) { ShowDebugMapPreview(); } } #endif 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(); } #if UNITY_EDITOR || DEVELOPMENT_BUILD private void ShowDebugMenu() { EnsureDebugPreviewData(); gameplayActive = false; pendingStartGameplay = false; currentScreen = FlowScreen.DebugMenu; EnsureOverlay(); ClearOverlay(); SetOverlayVisible(true); SetDefaultOverlayBackground(); AddTitle("디버그 메뉴"); AddBody("화면 조정용 바로 이동\n숫자키 1~6도 사용할 수 있습니다."); AddButton("1. 인트로", ShowDebugIntroPreview); AddButton("2. 게임 시작", ShowDebugGameStartPreview); AddButton("3. 출국 실패", delegate { ShowDebugResultPreview(false); }); AddButton("4. 출국 성공", delegate { ShowDebugResultPreview(true); }); AddButton("5. 면세점", ShowDebugDutyFreeShopPreview); AddButton("6. 지도", ShowDebugMapPreview); } private void ShowDebugIntroPreview() { ShowIntro(); } private void ShowDebugGameStartPreview() { EnsureDebugPreviewData(); LoadGameSceneForCurrentMap(); } private void ShowDebugResultPreview(bool cleared) { EnsureDebugPreviewData(); gameplayActive = false; pendingStartGameplay = false; currentScreen = FlowScreen.Result; completedMap = currentMap; lastResultCleared = cleared; lastResultScore = cleared ? 12800 : 7200; lastResultMileage = cleared ? 263 : 84; lastResultTime = cleared ? 23.9f : 38.4f; lastResultDistance = completedMap != null ? (cleared ? completedMap.targetDistance : completedMap.targetDistance * 0.62f) : 380f; if(saveData != null) { saveData.totalMileage = Mathf.Max(saveData.totalMileage, 4120); } ShowResult(); } private void ShowDebugDutyFreeShopPreview() { EnsureDebugPreviewData(); if(saveData != null) { saveData.totalMileage = Mathf.Max(saveData.totalMileage, 4120); } ShowDutyFreeShop(); } private void ShowDebugMapPreview() { EnsureDebugPreviewData(); ShowMapPreview(); } private void EnsureDebugPreviewData() { if(saveData == null) { saveData = GameSaveManager.Load(); } if(currentMap == null) { currentMap = MapDatabase.GetMapByIndex(saveData != null ? saveData.currentMapIndex : 0); } if(currentMap == null) { currentMap = MapDatabase.GetMapByIndex(0); } } #endif 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); BuildResultScreen(); } private void BuildResultScreen() { ApplyResultAirportBackground(); MapDefinition resultMap = completedMap != null ? completedMap : currentMap; int rewardMileage = lastResultCleared && resultMap != null ? resultMap.clearMileageReward : 0; int gainedMileage = Mathf.Max(0, lastResultMileage) + rewardMileage; int targetDistance = resultMap != null ? Mathf.FloorToInt(resultMap.targetDistance) : 0; int shownDistance = targetDistance > 0 ? Mathf.Clamp(Mathf.FloorToInt(lastResultDistance), 0, targetDistance) : Mathf.Max(0, Mathf.FloorToInt(lastResultDistance)); RectTransform passportRect = AddResultPassportPanel(); AddResultHeader(passportRect, resultMap); AddResultStats(passportRect, gainedMileage, shownDistance, targetDistance); AddResultTickets(passportRect, resultMap, rewardMileage); if(lastResultCleared) { AddResultActionButton("면세점으로", ShowDutyFreeShop, new Vector2(-166f, -282f), new Vector2(250f, 48f), true, LoadUiSprite("", DutyFreeIconAssetPath)); AddResultActionButton("다음 여행지 가기", ShowMapPreview, new Vector2(160f, -282f), new Vector2(292f, 48f), false, LoadUiSprite("", AirplaneTravelIconAssetPath)); } else { AddResultActionButton("다시 도전", RetryCurrentMap, new Vector2(-166f, -282f), new Vector2(250f, 48f), true, LoadUiSprite("", CheckpointIconAssetPath)); AddResultActionButton("다음 여행지 가기", ShowMapPreview, new Vector2(160f, -282f), new Vector2(292f, 48f), false, LoadUiSprite("", AirplaneTravelIconAssetPath)); } } private void ApplyResultAirportBackground() { if(overlayBackground != null) { overlayBackground.sprite = LoadUiSprite(ResultAirportBackgroundResourcePath, "Assets/Resources/Map_Tutorial_Incheon_Airport.png"); overlayBackground.preserveAspect = false; overlayBackground.color = overlayBackground.sprite != null ? Color.white : new Color(0.008f, 0.024f, 0.034f, 0.96f); } if(flowCamera != null) { flowCamera.backgroundColor = new Color(0.006f, 0.022f, 0.035f, 1f); } GameObject scrimObject = CreateUiObject("Result Airport Background Scrim", overlayRoot, typeof(Image)); RectTransform scrimRect = scrimObject.GetComponent(); scrimRect.anchorMin = Vector2.zero; scrimRect.anchorMax = Vector2.one; scrimRect.offsetMin = Vector2.zero; scrimRect.offsetMax = Vector2.zero; Image scrimImage = scrimObject.GetComponent(); scrimImage.color = new Color(0.005f, 0.015f, 0.020f, 0.30f); scrimImage.raycastTarget = false; } private RectTransform AddResultPassportPanel() { GameObject panelObject = CreateUiObject("Result Passport Page", overlayRoot, typeof(Image)); RectTransform panelRect = panelObject.GetComponent(); panelRect.anchorMin = new Vector2(0.5f, 0.5f); panelRect.anchorMax = new Vector2(0.5f, 0.5f); panelRect.pivot = new Vector2(0.5f, 0.5f); panelRect.anchoredPosition = new Vector2(0f, 38f); panelRect.sizeDelta = new Vector2(1092f, 330f); Image panelImage = panelObject.GetComponent(); panelImage.sprite = LoadUiSprite(ResultPassportPanelResourcePath, ResultPassportPanelAssetPath); panelImage.color = panelImage.sprite != null ? Color.white : new Color(0.95f, 0.91f, 0.80f, 0.98f); panelImage.preserveAspect = true; panelImage.raycastTarget = false; return panelRect; } private void AddResultHeader(RectTransform parent, MapDefinition resultMap) { Sprite rankSprite = LoadUiSprite("", ClearRankIconAssetPath); AddResultImage(parent, "Result Rank Badge", rankSprite, new Vector2(150f, -140f), new Vector2(48f, 48f), true, Color.white); string title = lastResultCleared ? "출국 성공!" : "일정 실패"; Color titleColor = lastResultCleared ? new Color(0.05f, 0.17f, 0.27f, 1f) : new Color(0.52f, 0.08f, 0.07f, 1f); AddResultCenteredText(parent, "Result Title", title, new Vector2(244f, -140f), new Vector2(220f, 50f), 27f, FontStyles.Bold, titleColor, TextAlignmentOptions.Center); string routeText = GetResultOriginAirportCode(resultMap) + " -> " + GetResultDestinationAirportCode(resultMap); AddResultCenteredText(parent, "Result Route Summary", routeText, new Vector2(244f, -181f), new Vector2(220f, 28f), 18f, FontStyles.Bold, new Color(0.17f, 0.34f, 0.43f, 0.98f), TextAlignmentOptions.Center); AddResultCenteredText(parent, "Result Route Name", GetResultDestinationCityName(resultMap), new Vector2(244f, -207f), new Vector2(220f, 22f), 13f, FontStyles.Bold, new Color(0.43f, 0.58f, 0.64f, 0.96f), TextAlignmentOptions.Center); if(lastResultCleared) { TextMeshProUGUI stampText = AddResultCenteredText(parent, "Result Stamp Text", "출국\n성공", new Vector2(238f, -260f), new Vector2(86f, 50f), 19f, FontStyles.Bold, new Color(0.70f, 0.04f, 0.03f, 0.96f), TextAlignmentOptions.Center); stampText.lineSpacing = -16f; } else { AddResultCenteredText(parent, "Result Retry Note", "다시 준비해서\n출국 게이트까지", new Vector2(244f, -260f), new Vector2(210f, 58f), 17f, FontStyles.Bold, new Color(0.32f, 0.46f, 0.54f, 0.96f), TextAlignmentOptions.Center); } AddResultRouteBox(parent, "From", GetResultOriginAirportCode(resultMap), GetResultOriginCityName(resultMap), new Vector2(440f, -122f)); AddResultRouteBox(parent, "To", GetResultDestinationAirportCode(resultMap), GetResultDestinationCityName(resultMap), new Vector2(657f, -122f)); AddResultBoardingField(parent, "Flight", "항공편", GetResultFlightName(resultMap), new Vector2(402f, -253f)); AddResultBoardingField(parent, "Seat", "좌석", GetResultSeatName(resultMap), new Vector2(512f, -253f)); AddResultBoardingField(parent, "Boarding", "탑승", System.DateTime.Now.ToString("HH:mm"), new Vector2(622f, -253f)); AddResultBoardingField(parent, "Class", "클래스", lastResultCleared ? "일반석" : "대기", new Vector2(732f, -253f)); } private void AddResultStats(RectTransform parent, int gainedMileage, int shownDistance, int targetDistance) { AddResultStat(parent, "Mileage", LoadUiSprite("", MileageIconAssetPath), "마일리지", "+" + gainedMileage, new Vector2(812f, -140f)); AddResultStat(parent, "Time", LoadUiSprite("", TimerIconAssetPath), "도착 시간", lastResultTime.ToString("0.0") + "s", new Vector2(900f, -140f)); string distance = targetDistance > 0 ? shownDistance + "/" + targetDistance + "m" : shownDistance + "m"; AddResultStat(parent, "Distance", LoadUiSprite("", CheckpointIconAssetPath), "이동 거리", distance, new Vector2(986f, -140f)); } private void AddResultTickets(RectTransform parent, MapDefinition resultMap, int rewardMileage) { AddResultTicket( parent, "Destination", LoadUiSprite("", CheckpointIconAssetPath), "목적지", GetResultDestinationCityName(resultMap), new Vector2(812f, -251f)); AddResultTicket( parent, "Souvenir", lastResultCleared ? GetResultSouvenirSprite(resultMap) : LoadUiSprite("", SouvenirCollectionIconAssetPath), "보상", lastResultCleared ? GetResultSouvenirName(resultMap) : "다음 도전", new Vector2(900f, -251f)); AddResultTicket( parent, "Total Mileage", LoadUiSprite("", MileageIconAssetPath), "보유", saveData.totalMileage + " M", new Vector2(984f, -251f)); } private void AddResultStat(RectTransform parent, string objectName, Sprite iconSprite, string label, string value, Vector2 centerPosition) { AddResultImage(parent, "Result " + objectName + " Icon", iconSprite, centerPosition + new Vector2(0f, 23f), new Vector2(28f, 28f), true, Color.white); AddResultCenteredText(parent, "Result " + objectName + " Value", value, centerPosition + new Vector2(0f, -2f), new Vector2(90f, 22f), 12f, FontStyles.Bold, new Color(0.05f, 0.17f, 0.25f, 1f), TextAlignmentOptions.Center); AddResultCenteredText(parent, "Result " + objectName + " Label", label, centerPosition + new Vector2(0f, -25f), new Vector2(90f, 16f), 8f, FontStyles.Bold, new Color(0.42f, 0.58f, 0.64f, 0.92f), TextAlignmentOptions.Center); } private void AddResultTicket(RectTransform parent, string objectName, Sprite iconSprite, string label, string value, Vector2 centerPosition) { AddResultImage(parent, "Result " + objectName + " Icon", iconSprite, centerPosition + new Vector2(0f, 27f), new Vector2(34f, 34f), true, Color.white); AddResultCenteredText(parent, "Result " + objectName + " Label", label, centerPosition + new Vector2(0f, -9f), new Vector2(110f, 18f), 11f, FontStyles.Bold, new Color(0.43f, 0.58f, 0.64f, 0.96f), TextAlignmentOptions.Center); AddResultCenteredText(parent, "Result " + objectName + " Value", value, centerPosition + new Vector2(0f, -31f), new Vector2(118f, 26f), 12f, FontStyles.Bold, new Color(0.05f, 0.18f, 0.27f, 1f), TextAlignmentOptions.Center); } private void AddResultRouteBox(RectTransform parent, string objectName, string code, string cityName, Vector2 centerPosition) { AddResultCenteredText(parent, "Result " + objectName + " Airport Code", code, centerPosition + new Vector2(0f, 7f), new Vector2(150f, 48f), 34f, FontStyles.Bold, new Color(0.05f, 0.17f, 0.27f, 1f), TextAlignmentOptions.Center); AddResultCenteredText(parent, "Result " + objectName + " City", cityName, centerPosition + new Vector2(0f, -30f), new Vector2(160f, 22f), 12f, FontStyles.Bold, new Color(0.43f, 0.58f, 0.64f, 0.96f), TextAlignmentOptions.Center); } private void AddResultBoardingField(RectTransform parent, string objectName, string label, string value, Vector2 centerPosition) { AddResultCenteredText(parent, "Result " + objectName + " Label", label, centerPosition + new Vector2(0f, 13f), new Vector2(104f, 17f), 9f, FontStyles.Bold, new Color(0.43f, 0.58f, 0.64f, 0.96f), TextAlignmentOptions.Center); AddResultCenteredText(parent, "Result " + objectName + " Value", value, centerPosition + new Vector2(0f, -12f), new Vector2(108f, 28f), 14f, FontStyles.Bold, new Color(0.05f, 0.18f, 0.27f, 1f), TextAlignmentOptions.Center); } private void AddResultActionButton(string label, UnityEngine.Events.UnityAction action, Vector2 position, Vector2 size, bool primary, Sprite iconSprite) { GameObject buttonObject = CreateUiObject("Result Button " + label, overlayRoot, typeof(Image), typeof(Button)); RectTransform buttonRect = buttonObject.GetComponent(); buttonRect.anchorMin = new Vector2(0.5f, 0.5f); buttonRect.anchorMax = new Vector2(0.5f, 0.5f); buttonRect.pivot = new Vector2(0.5f, 0.5f); buttonRect.anchoredPosition = position; buttonRect.sizeDelta = size; Image buttonImage = buttonObject.GetComponent(); buttonImage.sprite = LoadUiSprite(ResultButtonResourcePath, ResultButtonAssetPath); buttonImage.color = buttonImage.sprite != null ? (primary ? Color.white : new Color(0.86f, 0.94f, 1f, 0.94f)) : primary ? new Color(1f, 0.82f, 0.28f, 0.98f) : new Color(0.82f, 0.91f, 0.96f, 0.90f); buttonImage.preserveAspect = false; buttonImage.raycastTarget = true; Button button = buttonObject.GetComponent