using System.Collections.Generic; using System; using BumpCombat.Audio; using BumpCombat.Core; using BumpCombat.Progression; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.UI; namespace BumpCombat.UI { /// Presentation for the current-run mission list and future achievements. [DefaultExecutionOrder(-100)] [DisallowMultipleComponent] public sealed class MissionAchievementPanel : MonoBehaviour { private const int MissionsPerPage = 6; private const float PanelWidth = 1360f; private const float PanelHeight = 880f; private const float MissionRowWidth = 1260f; private const float MissionRowHeight = 88f; private const float MissionRowSpacing = 96f; private const float ProgressBarWidth = 170f; private const float CompletionNoticeDuration = 3f; private static readonly Color TabIdleColor = new(0.08f, 0.12f, 0.19f, 1f); private static readonly Color TabSelectedColor = new(0.96f, 0.76f, 0.31f, 1f); private static readonly Color TabIdleLabelColor = new(0.93f, 0.95f, 0.99f, 1f); private static readonly Color TabSelectedLabelColor = new(0.11f, 0.10f, 0.08f, 1f); private static readonly Color LockedColor = new(0.67f, 0.71f, 0.78f, 1f); private static readonly Color CompletedColor = new(1f, 0.86f, 0.53f, 1f); private static readonly Color MissionConditionColor = new(0.81f, 0.85f, 0.91f, 1f); private static readonly Color ProgressFillColor = new(0.31f, 0.78f, 0.65f, 1f); private RunManager runManager; private RunMissionTracker missionTracker; private GameObject overlay; private GameObject panel; private Text panelTitleText; private Text panelHelpText; private GameObject missionListRoot; private GameObject achievementRoot; private MissionRowView[] missionRows; private Text pageText; private Text progressHeader; private Text rewardHeader; private Text achievementRecordsText; private Button missionTabButton; private Button achievementTabButton; private Button previousPageButton; private Button nextPageButton; private Text hintText; private GameObject completionToast; private Text completionToastMissionText; private Text completionToastRewardText; private bool showingAchievements; private bool titleAchievementsOnly; private Action titleAchievementsClosed; private bool started; private bool subscribed; private int pageIndex; private readonly Queue pendingCompletionNotices = new(); private MissionCompletionNotice activeCompletionNotice; private float activeCompletionNoticeRemaining; private readonly bool[] lastMissionCompletionState = new bool[RunMissionTracker.MissionCount]; private sealed class MissionCompletionNotice { public string MissionName; public int ExperienceReward; } private sealed class MissionRowView { public GameObject Root; public Text Title; public Text Condition; public GameObject StatusBadge; public Image StatusBadgeImage; public Text Status; public Text Progress; public Image ProgressFill; public Text Reward; } public bool IsOpen => panel != null && panel.activeSelf; public bool ShowingAchievements => showingAchievements; public int CurrentPageIndex => pageIndex; public int VisibleMissionRowCount { get; private set; } public int PageCount => Mathf.CeilToInt( RunMissionTracker.MissionCount / (float)MissionsPerPage); public bool OpenTitleAchievements(Action onClosed) { if (!started || runManager == null || !runManager.IsTitleScreen || IsOpen) { return false; } titleAchievementsClosed = onClosed; OpenPanel(achievementsOnly: true); return true; } private void Start() { PresentationUiStyle.EnsureEventSystem(clearMenuNavigationActions: false); runManager = RunManager.Instance; missionTracker = RunMissionTracker.Instance; CreateHint(); CreatePanel(); CreateCompletionToast(); started = true; Subscribe(); CaptureCompletionState(showNotice: false); RefreshView(); } private void OnEnable() { if (started) { Subscribe(); CaptureCompletionState(showNotice: false); RefreshView(); } } private void OnDisable() { if (titleAchievementsOnly && IsOpen) { ClosePanel(suppressPause: false, playSound: false); } // Releasing by owner is safe even if another modal currently owns // the selection lock, and closes a stale lock if the panel was // disabled during scene teardown. panel?.SetActive(false); overlay?.SetActive(false); hintText?.gameObject.SetActive(false); ClearCompletionNotices(); runManager?.CloseSelection(this); titleAchievementsOnly = false; titleAchievementsClosed = null; Unsubscribe(); } private void OnDestroy() { panel?.SetActive(false); overlay?.SetActive(false); ClearCompletionNotices(); runManager?.CloseSelection(this); titleAchievementsOnly = false; titleAchievementsClosed = null; Unsubscribe(); } private void Subscribe() { if (subscribed) { return; } if (missionTracker != null) { missionTracker.OnProgressChanged += HandleProgressChanged; } if (runManager != null) { runManager.OnGameOver += HandleGameOver; } subscribed = true; } private void Unsubscribe() { if (!subscribed) { return; } if (missionTracker != null) { missionTracker.OnProgressChanged -= HandleProgressChanged; } if (runManager != null) { runManager.OnGameOver -= HandleGameOver; } subscribed = false; } private void Update() { UpdateHintVisibility(); UpdateCompletionToast(); if (runManager == null || Keyboard.current == null) { return; } Keyboard keyboard = Keyboard.current; bool tabPressed = keyboard.tabKey.wasPressedThisFrame; bool escapePressed = keyboard.escapeKey.wasPressedThisFrame; if (IsOpen) { if (tabPressed || escapePressed) { ClosePanel( suppressPause: escapePressed && !titleAchievementsOnly, playSound: true); return; } if (titleAchievementsOnly) { return; } if (keyboard.digit1Key.wasPressedThisFrame) { ShowMissions(); } else if (keyboard.digit2Key.wasPressedThisFrame) { ShowAchievements(); } else if (keyboard.leftArrowKey.wasPressedThisFrame) { ShowMissions(); } else if (keyboard.rightArrowKey.wasPressedThisFrame) { ShowAchievements(); } else if (!showingAchievements && (keyboard.upArrowKey.wasPressedThisFrame || keyboard.pageUpKey.wasPressedThisFrame)) { SetPage(pageIndex - 1); } else if (!showingAchievements && (keyboard.downArrowKey.wasPressedThisFrame || keyboard.pageDownKey.wasPressedThisFrame)) { SetPage(pageIndex + 1); } return; } if (tabPressed && CanOpen() && runManager.TryOpenSelection(this)) { OpenPanel(); BumpCombatAudioService.Instance?.PlayUi("ui_confirm"); } } private void LateUpdate() { UpdateCompletionToastVisibility(); if (completionToast != null && completionToast.activeSelf) { completionToast.transform.SetAsLastSibling(); } if (!IsOpen || overlay == null || panel == null) { return; } int lastIndex = transform.childCount - 1; int overlayIndex = overlay.transform.GetSiblingIndex(); int panelIndex = panel.transform.GetSiblingIndex(); if (panelIndex == lastIndex && overlayIndex == lastIndex - 1) { return; } overlay.transform.SetAsLastSibling(); panel.transform.SetAsLastSibling(); } private void ShowMissions() { if (titleAchievementsOnly) { return; } showingAchievements = false; pageIndex = Mathf.Clamp(pageIndex, 0, PageCount - 1); RefreshView(); } private void ShowAchievements() { showingAchievements = true; RefreshView(); } private void SetPage(int requestedPage) { if (titleAchievementsOnly) { return; } pageIndex = Mathf.Clamp(requestedPage, 0, PageCount - 1); showingAchievements = false; RefreshView(); } private bool CanOpen() { return runManager != null && !runManager.IsTitleScreen && !runManager.IsGameOver && !runManager.IsPaused && !runManager.IsSelectionOpen; } private void CreateHint() { hintText = PresentationUiStyle.CreateText( transform, "Mission List Hint", PresentationUiStyle.GetGalmuriFont(), 24, TextAnchor.MiddleLeft, Vector2.zero, new Vector2(220f, 110f)); RectTransform hintRect = hintText.rectTransform; hintRect.anchorMin = new Vector2(0f, 1f); hintRect.anchorMax = new Vector2(0f, 1f); hintRect.pivot = new Vector2(0f, 1f); hintRect.anchoredPosition = new Vector2(24f, -250f); hintText.supportRichText = true; hintText.resizeTextForBestFit = false; hintText.horizontalOverflow = HorizontalWrapMode.Wrap; hintText.verticalOverflow = VerticalWrapMode.Overflow; hintText.text = "TAB\n" + "(미션 / 업적)"; hintText.color = Color.white; } private void CreatePanel() { overlay = PresentationUiStyle.CreatePanel( transform, "Mission List Dim", Vector2.zero, Vector2.zero, Vector2.zero, new Color(0f, 0f, 0f, 0.78f)); RectTransform overlayRect = overlay.GetComponent(); overlayRect.anchorMin = Vector2.zero; overlayRect.anchorMax = Vector2.one; overlayRect.offsetMin = Vector2.zero; overlayRect.offsetMax = Vector2.zero; overlay.GetComponent().raycastTarget = true; panel = PresentationUiStyle.CreatePanel( transform, "Mission and Achievement List", new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(PanelWidth, PanelHeight), new Color(0.035f, 0.045f, 0.075f, 0.98f)); PresentationUiStyle.ApplyPanel(panel.GetComponent()); Font font = PresentationUiStyle.GetGalmuriFont(); panelTitleText = PresentationUiStyle.CreateText( panel.transform, "Mission List Title", font, 34, TextAnchor.MiddleCenter, new Vector2(0f, 386f), new Vector2(900f, 56f)); panelTitleText.text = "미션 / 업적"; panelTitleText.color = new Color32(255, 230, 173, 255); CreateButton( "Mission Tab", "미션 [1]", new Vector2(-136f, 316f), new Vector2(240f, 52f), ShowMissions); missionTabButton = panel.transform.Find("Mission Tab") .GetComponent