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