using System.Collections; using TMPro; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.SceneManagement; using UnityEngine.UI; #if UNITY_EDITOR using UnityEditor; #endif public partial 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 DutyFreeBackgroundResourcePath = "UI_DutyFreeShopInteriorPanel"; private const string DutyFreeBackgroundAssetPath = "Assets/Resources/UI_DutyFreeShopInteriorPanel.png"; private const string DutyFreeWindowPanelResourcePath = "UI_DutyFreeShopWindowPanel"; private const string DutyFreeWindowPanelAssetPath = "Assets/Resources/UI_DutyFreeShopWindowPanel.png"; private const string DutyFreeLunchboxAssetPath = "Assets/Sprites/DutyFree/DutyFree_Normal_Lunchbox.png"; private const string DutyFreeMileageCardAssetPath = "Assets/Sprites/DutyFree/DutyFree_Normal_MileageCard.png"; private const string DutyFreeSupplementAssetPath = "Assets/Sprites/DutyFree/DutyFree_Normal_Supplement.png"; private const string DutyFreeBackpackAssetPath = "Assets/Sprites/DutyFree/DutyFree_Normal_Backpack.png"; private const string DutyFreeProductCardBrownResourcePath = "UI_DutyFreeProductCard_Brown_Clean"; private const string DutyFreeProductCardGreenResourcePath = "UI_DutyFreeProductCard_Green_Clean"; private const string DutyFreeProductCardBlueResourcePath = "UI_DutyFreeProductCard_Blue_Clean"; private const string DutyFreeProductCardPurpleResourcePath = "UI_DutyFreeProductCard_Purple_Clean"; private const string DutyFreeEquipSlotPanelResourcePath = "UI_DutyFreeBagSlotPanel"; private const string DutyFreeActiveSlotResourcePath = "UI_DutyFreeBagSlot_Active"; private const string DutyFreeInactiveSlotResourcePath = "UI_DutyFreeBagSlot_Inactive"; private const string DutyFreeOccupiedSlotResourcePath = "UI_DutyFreeBagSlot_Occupied"; private const string DutyFreeButtonPrimaryResourcePath = "UI_DutyFreeButton_PrimaryYellow"; private const string DutyFreeButtonDisabledResourcePath = "UI_DutyFreeButton_DisabledGray"; private const string DutyFreeDirectionSignResourcePath = "UI_DutyFreeAirportDirectionSign"; private const string DutyFreePriceTagResourcePath = "UI_DutyFreePriceTag_LightBlue"; private const string DutyFreeProductPriceTagResourcePath = "UI_DutyFreeProductPriceTag"; 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 LunchboxCost = 80; private const int MileageCardCost = 160; private const int SupplementCost = 220; private const int BaseMaxLife = 3; private const int MaxSupplementPurchases = 4; private const int MaxLifeWithSupplements = 5; private const int BagSlotUpgradeCost = 260; 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(); } else if(Input.GetKeyDown(KeyCode.Alpha7) || Input.GetKeyDown(KeyCode.Keypad7)) { ShowDebugMapGameStartPreview(MapId.Japan); } else if(Input.GetKeyDown(KeyCode.Alpha8) || Input.GetKeyDown(KeyCode.Keypad8)) { ShowDebugMapResultPreview(MapId.Japan, true); } else if(Input.GetKeyDown(KeyCode.Alpha9) || Input.GetKeyDown(KeyCode.Keypad9)) { ShowDebugMapGameStartPreview(MapId.China); } else if(Input.GetKeyDown(KeyCode.Alpha0) || Input.GetKeyDown(KeyCode.Keypad0)) { ShowDebugMapResultPreview(MapId.China, true); } else if(Input.GetKeyDown(KeyCode.F1)) { ShowDebugMapGameStartPreview(MapId.America); } else if(Input.GetKeyDown(KeyCode.F2)) { ShowDebugMapResultPreview(MapId.America, true); } } #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(); } 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 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); BuildMapPreviewScreen(); } private void ConfirmMapAndStart() { saveData.currentMapIndex = MapDatabase.GetIndex(currentMap.mapId); saveData.unlockedMapIndex = Mathf.Max(saveData.unlockedMapIndex, saveData.currentMapIndex); GameSaveManager.Save(saveData); LoadGameSceneForCurrentMap(); } private void BuildMapPreviewScreen() { if(overlayBackground != null) { overlayBackground.sprite = null; overlayBackground.preserveAspect = false; overlayBackground.color = Color.clear; } if(flowCamera != null) { flowCamera.backgroundColor = new Color(0.006f, 0.022f, 0.035f, 1f); } MapId destinationMapId = currentMap != null ? currentMap.mapId : MapId.TutorialIncheon; MapId originMapId = GetMapPreviewOriginMapId(destinationMapId); RectTransform mapRect = AddIntroWorldMap(); AddMapPreviewRoute(mapRect, originMapId, destinationMapId); AddMapPreviewTravelPlane(mapRect, originMapId, destinationMapId); AddMapPreviewActionDock(); } private MapId GetMapPreviewOriginMapId(MapId destinationMapId) { if(completedMap != null && lastResultCleared && completedMap.hasNextMap) { return completedMap.mapId; } int destinationIndex = MapDatabase.GetIndex(destinationMapId); if(destinationIndex > 0) { return MapDatabase.GetMapByIndex(destinationIndex - 1).mapId; } return destinationMapId; } private void AddMapPreviewRoute(RectTransform mapRect, MapId originMapId, MapId destinationMapId) { Vector2[] routePoints = GetIntroRoutePoints(); Color inactiveRouteColor = new Color(0.72f, 0.92f, 1f, 0.22f); Color activeRouteColor = new Color(1f, 0.86f, 0.28f, 0.88f); for(int i = 0; i < routePoints.Length - 1; i++) { AddIntroRouteSegment(mapRect, routePoints[i], routePoints[i + 1], inactiveRouteColor, 3f); } int originIndex = GetIntroRouteIndex(originMapId); int destinationIndex = GetIntroRouteIndex(destinationMapId); if(originIndex != destinationIndex) { int step = originIndex < destinationIndex ? 1 : -1; for(int i = originIndex; i != destinationIndex; i += step) { AddIntroRouteSegment(mapRect, routePoints[i], routePoints[i + step], activeRouteColor, 6f); } } AddMapPreviewRouteMarker(mapRect, MapId.TutorialIncheon, originMapId, destinationMapId, 0f); AddMapPreviewRouteMarker(mapRect, MapId.Japan, originMapId, destinationMapId, 0.4f); AddMapPreviewRouteMarker(mapRect, MapId.China, originMapId, destinationMapId, 0.8f); AddMapPreviewRouteMarker(mapRect, MapId.America, originMapId, destinationMapId, 1.2f); } private void AddMapPreviewRouteMarker(RectTransform mapRect, MapId markerMapId, MapId originMapId, MapId destinationMapId, float phase) { bool isOrigin = markerMapId == originMapId; bool isDestination = markerMapId == destinationMapId; Vector2 position = GetIntroRoutePoint(markerMapId); GameObject markerObject = CreateUiObject("Map Preview Route Marker " + markerMapId, mapRect, typeof(Image)); RectTransform markerRect = markerObject.GetComponent(); markerRect.anchorMin = new Vector2(0.5f, 0.5f); markerRect.anchorMax = new Vector2(0.5f, 0.5f); markerRect.pivot = new Vector2(0.5f, 0.5f); markerRect.anchoredPosition = position; markerRect.sizeDelta = isDestination ? new Vector2(28f, 28f) : isOrigin ? new Vector2(21f, 21f) : new Vector2(15f, 15f); Image markerImage = markerObject.GetComponent(); markerImage.color = isDestination ? new Color(1f, 0.86f, 0.26f, 0.96f) : isOrigin ? new Color(0.82f, 0.96f, 1f, 0.90f) : new Color(0.56f, 0.78f, 0.86f, 0.54f); markerImage.raycastTarget = false; FlowIntroFloatMotion markerMotion = markerObject.AddComponent(); markerMotion.scalePulse = isDestination ? 0.18f : isOrigin ? 0.10f : 0.05f; markerMotion.frequency = new Vector2(0f, 0.72f); markerMotion.phase = phase; TextMeshProUGUI labelText = AddIntroMapLabel( mapRect, "Map Preview Marker Label " + markerMapId, GetIntroRouteLabel(markerMapId), position + GetIntroRouteLabelOffset(markerMapId), new Vector2(94f, 20f), isDestination ? 10.5f : 9.5f); labelText.color = isDestination ? new Color(1f, 0.92f, 0.34f, 0.94f) : isOrigin ? new Color(0.80f, 0.95f, 1f, 0.84f) : new Color(0.68f, 0.86f, 0.92f, 0.54f); } private void AddMapPreviewTravelPlane(RectTransform mapRect, MapId originMapId, MapId destinationMapId) { Sprite planeSprite = LoadUiSprite("", AirplaneTravelIconAssetPath); GameObject planeObject = CreateUiObject("Map Preview Airplane", overlayRoot, typeof(Image)); RectTransform planeRect = planeObject.GetComponent(); planeRect.anchorMin = new Vector2(0.5f, 0.5f); planeRect.anchorMax = new Vector2(0.5f, 0.5f); planeRect.pivot = new Vector2(0.5f, 0.5f); planeRect.anchoredPosition = GetIntroRoutePoint(originMapId) + mapRect.anchoredPosition; planeRect.sizeDelta = new Vector2(66f, 66f); Image planeImage = planeObject.GetComponent(); planeImage.sprite = planeSprite; planeImage.color = planeSprite != null ? new Color(1f, 1f, 1f, 0.98f) : new Color(0.9f, 0.97f, 1f, 0.92f); planeImage.preserveAspect = true; planeImage.raycastTarget = false; FlowIntroRouteMotion motion = planeObject.AddComponent(); motion.points = GetMapPreviewTravelPoints(originMapId, destinationMapId); motion.offset = mapRect.anchoredPosition; motion.duration = 3.8f; } private Vector2[] GetMapPreviewTravelPoints(MapId originMapId, MapId destinationMapId) { if(originMapId == destinationMapId) { Vector2 destinationPoint = GetIntroRoutePoint(destinationMapId); return new Vector2[] { destinationPoint + new Vector2(-70f, -12f), destinationPoint }; } Vector2[] routePoints = GetIntroRoutePoints(); int originIndex = GetIntroRouteIndex(originMapId); int destinationIndex = GetIntroRouteIndex(destinationMapId); int step = originIndex < destinationIndex ? 1 : -1; int pointCount = Mathf.Abs(destinationIndex - originIndex) + 1; Vector2[] travelPoints = new Vector2[pointCount]; int writeIndex = 0; for(int i = originIndex; i != destinationIndex + step; i += step) { travelPoints[writeIndex] = routePoints[i]; writeIndex++; } return travelPoints; } private void AddMapPreviewActionDock() { GameObject dockObject = CreateUiObject("Map Preview Button Dock", overlayRoot); RectTransform dockRect = dockObject.GetComponent(); dockRect.anchorMin = new Vector2(0.5f, 0.5f); dockRect.anchorMax = new Vector2(0.5f, 0.5f); dockRect.pivot = new Vector2(0.5f, 0.5f); dockRect.anchoredPosition = new Vector2(328f, -236f); dockRect.sizeDelta = new Vector2(424f, 136f); AddIntroButton(dockRect, "출발", new Vector2(45f, 0f), ConfirmMapAndStart, true, 0); AddIntroButton(dockRect, "인트로", new Vector2(110f, -76f), ShowIntro, false, 1); } 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; #if UNITY_EDITOR || DEVELOPMENT_BUILD case FlowScreen.DebugMenu: ShowDebugMenu(); break; #endif case FlowScreen.Intro: default: ShowIntro(); break; } } private void ResetAndStayIntro() { saveData = GameSaveManager.ResetSave(); currentMap = MapDatabase.GetMapByIndex(saveData.currentMapIndex); ShowIntro(); } private void BuyLunchbox() { EnsureDutyFreeStagePurchaseLimits(); PrepareOrBuyDutyFreeItem(DutyFreeItemType.Lunchbox, LunchboxCost); ShowDutyFreeShop(); } private void BuyMileageCard() { EnsureDutyFreeStagePurchaseLimits(); PrepareOrBuyDutyFreeItem(DutyFreeItemType.MileageCard, MileageCardCost); ShowDutyFreeShop(); } private void BuySupplement() { EnsureDutyFreeStagePurchaseLimits(); if(CanBuySupplement() && TrySpendMileage(SupplementCost)) { saveData.supplementPurchases++; saveData.purchasedSupplementThisStage = true; GameSaveManager.Save(saveData); } ShowDutyFreeShop(); } private void BuyBagSlotUpgrade() { if(saveData.bagSlots < 3 && TrySpendMileage(BagSlotUpgradeCost)) { saveData.bagSlots++; GameSaveManager.Save(saveData); } ShowDutyFreeShop(); } private void PrepareOrBuyDutyFreeItem(DutyFreeItemType itemType, int cost) { if(TryEquipOwnedDutyFreeItem(itemType)) { GameSaveManager.Save(saveData); return; } if(!CanBuyDutyFreeItem(itemType, cost) || !TrySpendMileage(cost)) { return; } AddDutyFreeStock(itemType, 1); if(itemType == DutyFreeItemType.MileageCard) { saveData.purchasedMileageCardThisStage = true; } TryEquipOwnedDutyFreeItem(itemType); GameSaveManager.Save(saveData); } private bool TryEquipOwnedDutyFreeItem(DutyFreeItemType itemType) { if(itemType == DutyFreeItemType.None || !HasFreeDutyFreeSlot()) { return false; } if(GetEquippedDutyFreeItemCount(itemType) >= GetDutyFreeStock(itemType)) { return false; } for(int i = 0; i < saveData.bagSlots; i++) { if(GetEquippedDutyFreeItem(i) != DutyFreeItemType.None) { continue; } SetEquippedDutyFreeItem(i, itemType); return true; } return false; } private void UnequipDutyFreeSlot(int slotIndex) { SetEquippedDutyFreeItem(slotIndex, DutyFreeItemType.None); GameSaveManager.Save(saveData); ShowDutyFreeShop(); } public void ConsumeEquippedDutyFreeItem(int slotIndex, DutyFreeItemType itemType) { if(saveData == null || itemType == DutyFreeItemType.None || GetEquippedDutyFreeItem(slotIndex) != itemType) { return; } SetEquippedDutyFreeItem(slotIndex, DutyFreeItemType.None); AddDutyFreeStock(itemType, -1); GameSaveManager.Save(saveData); } private bool TrySpendMileage(int cost) { if(saveData.totalMileage < cost) { return false; } saveData.totalMileage -= cost; return true; } private bool HasFreeDutyFreeSlot() { for(int i = 0; i < saveData.bagSlots; i++) { if(GetEquippedDutyFreeItem(i) == DutyFreeItemType.None) { return true; } } return false; } private DutyFreeItemType GetEquippedDutyFreeItem(int slotIndex) { switch(slotIndex) { case 0: return saveData.equippedSlot1; case 1: return saveData.equippedSlot2; case 2: return saveData.equippedSlot3; default: return DutyFreeItemType.None; } } private void SetEquippedDutyFreeItem(int slotIndex, DutyFreeItemType itemType) { switch(slotIndex) { case 0: saveData.equippedSlot1 = itemType; break; case 1: saveData.equippedSlot2 = itemType; break; case 2: saveData.equippedSlot3 = itemType; break; } } private int GetEquippedDutyFreeItemCount(DutyFreeItemType itemType) { int count = 0; for(int i = 0; i < saveData.bagSlots; i++) { if(GetEquippedDutyFreeItem(i) == itemType) { count++; } } return count; } private int GetDutyFreeStock(DutyFreeItemType itemType) { switch(itemType) { case DutyFreeItemType.Lunchbox: return saveData.lunchboxStock; case DutyFreeItemType.MileageCard: return saveData.mileageCardStock; default: return 0; } } private void AddDutyFreeStock(DutyFreeItemType itemType, int amount) { switch(itemType) { case DutyFreeItemType.Lunchbox: saveData.lunchboxStock = Mathf.Max(0, saveData.lunchboxStock + amount); break; case DutyFreeItemType.MileageCard: saveData.mileageCardStock = Mathf.Max(0, saveData.mileageCardStock + amount); break; } } private int GetDutyFreeLimitStageIndex() { if(completedMap != null && lastResultCleared && completedMap.hasNextMap) { return MapDatabase.GetIndex(completedMap.nextMapId); } if(currentMap != null) { return MapDatabase.GetIndex(currentMap.mapId); } return saveData != null ? Mathf.Clamp(saveData.currentMapIndex, 0, MapDatabase.LastMapIndex) : 0; } private void EnsureDutyFreeStagePurchaseLimits() { if(saveData == null) { return; } int stageIndex = GetDutyFreeLimitStageIndex(); if(saveData.dutyFreeLimitStageIndex == stageIndex) { return; } saveData.dutyFreeLimitStageIndex = stageIndex; saveData.purchasedMileageCardThisStage = false; saveData.purchasedSupplementThisStage = false; GameSaveManager.Save(saveData); } private bool HasPurchasedMileageCardInDutyFreeStage() { return saveData != null && saveData.dutyFreeLimitStageIndex == GetDutyFreeLimitStageIndex() && saveData.purchasedMileageCardThisStage; } private bool HasPurchasedSupplementInDutyFreeStage() { return saveData != null && saveData.dutyFreeLimitStageIndex == GetDutyFreeLimitStageIndex() && saveData.purchasedSupplementThisStage; } private bool CanBuyDutyFreeItem(DutyFreeItemType itemType, int cost) { if(saveData == null || itemType == DutyFreeItemType.None || saveData.totalMileage < cost) { return false; } if(!HasFreeDutyFreeSlot() || GetOwnedDutyFreeItemCount() >= Mathf.Clamp(saveData.bagSlots, 1, 3)) { return false; } if(itemType == DutyFreeItemType.Lunchbox && GetDutyFreeStock(DutyFreeItemType.Lunchbox) >= GetDutyFreeMaxLife()) { return false; } if(itemType == DutyFreeItemType.MileageCard && HasPurchasedMileageCardInDutyFreeStage()) { return false; } return true; } private bool CanBuySupplement() { return saveData != null && saveData.supplementPurchases < MaxSupplementPurchases && !HasPurchasedSupplementInDutyFreeStage() && saveData.totalMileage >= SupplementCost; } private int GetDutyFreeMaxLife() { int supplementPurchases = saveData != null ? Mathf.Clamp(saveData.supplementPurchases, 0, MaxSupplementPurchases) : 0; return Mathf.Min(MaxLifeWithSupplements, BaseMaxLife + (supplementPurchases / 2)); } private int GetOwnedDutyFreeItemCount() { return saveData != null ? Mathf.Max(0, saveData.lunchboxStock) + Mathf.Max(0, saveData.mileageCardStock) : 0; } private string GetDutyFreePurchaseLabel(DutyFreeItemType itemType, bool canEquip, bool canBuy, bool hasFreeSlot, int stock, int equippedCount) { if(canEquip) { return "장착"; } if(canBuy) { return "구매"; } if(itemType == DutyFreeItemType.Lunchbox && stock >= GetDutyFreeMaxLife()) { return "MAX"; } if(itemType == DutyFreeItemType.MileageCard && HasPurchasedMileageCardInDutyFreeStage()) { return "1회 완료"; } if(stock > equippedCount && !hasFreeSlot) { return "슬롯 가득"; } if(!hasFreeSlot || GetOwnedDutyFreeItemCount() >= Mathf.Clamp(saveData.bagSlots, 1, 3)) { return "슬롯 가득"; } return "부족"; } private string GetDutyFreeSupplementPurchaseLabel(bool canBuy, bool isMax, bool alreadyPurchasedThisStage) { if(isMax) { return "MAX"; } if(alreadyPurchasedThisStage) { return "1회 완료"; } return canBuy ? "구매" : "부족"; } private Sprite GetDutyFreeItemSprite(DutyFreeItemType itemType) { switch(itemType) { case DutyFreeItemType.Lunchbox: return LoadUiSprite("", DutyFreeLunchboxAssetPath); case DutyFreeItemType.MileageCard: return LoadUiSprite("", DutyFreeMileageCardAssetPath); case DutyFreeItemType.Supplement: return LoadUiSprite("", DutyFreeSupplementAssetPath); default: return null; } } private Sprite LoadDutyFreeResourceSprite(string resourcePath) { return LoadUiSprite(resourcePath, "Assets/Resources/" + resourcePath + ".png"); } private Sprite GetDutyFreeProductCardSprite(DutyFreeItemType itemType) { switch(itemType) { case DutyFreeItemType.Lunchbox: return LoadUiSprite(DutyFreeProductCardBrownResourcePath, "Assets/Resources/UI_DutyFreeProductCard_Brown_Clean.png"); case DutyFreeItemType.Supplement: return LoadUiSprite(DutyFreeProductCardGreenResourcePath, "Assets/Resources/UI_DutyFreeProductCard_Green_Clean.png"); case DutyFreeItemType.MileageCard: return LoadUiSprite(DutyFreeProductCardPurpleResourcePath, "Assets/Resources/UI_DutyFreeProductCard_Purple_Clean.png"); default: return null; } } private string GetDutyFreeItemEffectText(DutyFreeItemType itemType) { switch(itemType) { case DutyFreeItemType.Lunchbox: return "HP 회복"; case DutyFreeItemType.MileageCard: return "획득 증가"; case DutyFreeItemType.Supplement: return "최대 HP +0.5"; default: return ""; } } private string GetDutyFreeSupplementEffectText(int purchaseCount) { int clampedCount = Mathf.Clamp(purchaseCount, 0, MaxSupplementPurchases); return "최대 HP +0.5\n" + clampedCount + "/" + MaxSupplementPurchases; } private string GetDutyFreeItemShortName(DutyFreeItemType itemType) { switch(itemType) { case DutyFreeItemType.Lunchbox: return "도시락"; case DutyFreeItemType.MileageCard: return "마일"; case DutyFreeItemType.Supplement: return "영양"; default: return ""; } } 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(); EnsureFlowCamera(); 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; overlayCanvas.targetDisplay = 0; 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.sprite = null; overlayBackground.preserveAspect = false; 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.sprite = null; overlayBackground.preserveAspect = false; 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.sprite = null; overlayBackground.preserveAspect = false; 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