1016 lines
38 KiB
C#
1016 lines
38 KiB
C#
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
|
|
{
|
|
/// <summary>Presentation for the current-run mission list and future achievements.</summary>
|
|
[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<MissionCompletionNotice> 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 = "<size=36><color=#FFD675><b>TAB</b></color></size>\n"
|
|
+ "<size=24>(미션 / 업적)</size>";
|
|
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<RectTransform>();
|
|
overlayRect.anchorMin = Vector2.zero;
|
|
overlayRect.anchorMax = Vector2.one;
|
|
overlayRect.offsetMin = Vector2.zero;
|
|
overlayRect.offsetMax = Vector2.zero;
|
|
overlay.GetComponent<Image>().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<Image>());
|
|
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<Button>();
|
|
CreateButton(
|
|
"Achievement Tab",
|
|
"업적 [2]",
|
|
new Vector2(136f, 316f),
|
|
new Vector2(240f, 52f),
|
|
ShowAchievements);
|
|
achievementTabButton = panel.transform.Find("Achievement Tab")
|
|
.GetComponent<Button>();
|
|
previousPageButton = CreateButton(
|
|
"Previous Mission Page",
|
|
"이전",
|
|
new Vector2(-140f, 258f),
|
|
new Vector2(104f, 40f),
|
|
() => SetPage(pageIndex - 1));
|
|
nextPageButton = CreateButton(
|
|
"Next Mission Page",
|
|
"다음",
|
|
new Vector2(140f, 258f),
|
|
new Vector2(104f, 40f),
|
|
() => SetPage(pageIndex + 1));
|
|
pageText = PresentationUiStyle.CreateText(
|
|
panel.transform,
|
|
"Mission Page Number",
|
|
font,
|
|
22,
|
|
TextAnchor.MiddleCenter,
|
|
new Vector2(0f, 258f),
|
|
new Vector2(120f, 40f));
|
|
progressHeader = PresentationUiStyle.CreateText(
|
|
panel.transform,
|
|
"Mission Progress Header",
|
|
font,
|
|
22,
|
|
TextAnchor.MiddleCenter,
|
|
new Vector2(285f, 205f),
|
|
new Vector2(180f, 30f));
|
|
progressHeader.text = "진행도";
|
|
progressHeader.color = new Color32(255, 218, 135, 255);
|
|
rewardHeader = PresentationUiStyle.CreateText(
|
|
panel.transform,
|
|
"Mission Reward Header",
|
|
font,
|
|
22,
|
|
TextAnchor.MiddleCenter,
|
|
new Vector2(520f, 205f),
|
|
new Vector2(180f, 30f));
|
|
rewardHeader.text = "보상";
|
|
rewardHeader.color = new Color32(255, 218, 135, 255);
|
|
CreateButton(
|
|
"Close Mission List",
|
|
"닫기",
|
|
new Vector2(620f, 386f),
|
|
new Vector2(90f, 44f),
|
|
() => ClosePanel(suppressPause: false, playSound: true));
|
|
|
|
missionListRoot = new GameObject("Mission List Page");
|
|
missionListRoot.transform.SetParent(panel.transform, false);
|
|
missionRows = new MissionRowView[MissionsPerPage];
|
|
for (int i = 0; i < missionRows.Length; i++)
|
|
{
|
|
Image row = PresentationUiStyle.CreateImage(
|
|
missionListRoot.transform,
|
|
$"Mission Row {i + 1}",
|
|
new Vector2(0f, 150f - i * MissionRowSpacing),
|
|
new Vector2(MissionRowWidth, MissionRowHeight));
|
|
row.color = i % 2 == 0
|
|
? new Color(0.08f, 0.10f, 0.15f, 0.98f)
|
|
: new Color(0.065f, 0.085f, 0.13f, 0.98f);
|
|
|
|
Image statusBadge = PresentationUiStyle.CreateImage(
|
|
row.transform,
|
|
"Mission Status Badge",
|
|
new Vector2(45f, 21f),
|
|
new Vector2(170f, 34f));
|
|
statusBadge.color = LockedColor;
|
|
Text status = PresentationUiStyle.CreateText(
|
|
statusBadge.transform,
|
|
"Label",
|
|
font,
|
|
18,
|
|
TextAnchor.MiddleCenter,
|
|
Vector2.zero,
|
|
new Vector2(162f, 32f));
|
|
|
|
Text missionTitle = PresentationUiStyle.CreateText(
|
|
row.transform,
|
|
"Mission Title",
|
|
font,
|
|
32,
|
|
TextAnchor.MiddleLeft,
|
|
new Vector2(-335f, 21f),
|
|
new Vector2(540f, 42f));
|
|
missionTitle.resizeTextForBestFit = false;
|
|
missionTitle.horizontalOverflow = HorizontalWrapMode.Overflow;
|
|
missionTitle.color = Color.white;
|
|
|
|
Text condition = PresentationUiStyle.CreateText(
|
|
row.transform,
|
|
"Mission Condition",
|
|
font,
|
|
26,
|
|
TextAnchor.MiddleLeft,
|
|
new Vector2(-210f, -21f),
|
|
new Vector2(810f, 38f));
|
|
condition.resizeTextForBestFit = false;
|
|
condition.horizontalOverflow = HorizontalWrapMode.Overflow;
|
|
condition.color = MissionConditionColor;
|
|
|
|
Text progress = PresentationUiStyle.CreateText(
|
|
row.transform,
|
|
"Mission Progress",
|
|
font,
|
|
32,
|
|
TextAnchor.MiddleCenter,
|
|
new Vector2(285f, 14f),
|
|
new Vector2(190f, 40f));
|
|
progress.resizeTextForBestFit = false;
|
|
|
|
Image progressBar = PresentationUiStyle.CreateImage(
|
|
row.transform,
|
|
"Mission Progress Bar",
|
|
new Vector2(285f, -25f),
|
|
new Vector2(ProgressBarWidth, 8f));
|
|
progressBar.color = new Color32(44, 55, 72, 255);
|
|
Image progressFill = PresentationUiStyle.CreateImage(
|
|
progressBar.transform,
|
|
"Fill",
|
|
Vector2.zero,
|
|
Vector2.zero);
|
|
RectTransform progressFillRect = progressFill.rectTransform;
|
|
progressFillRect.anchorMin = new Vector2(0f, 0f);
|
|
progressFillRect.anchorMax = new Vector2(0f, 1f);
|
|
progressFillRect.pivot = new Vector2(0f, 0.5f);
|
|
progressFillRect.anchoredPosition = Vector2.zero;
|
|
progressFill.color = ProgressFillColor;
|
|
|
|
Text reward = PresentationUiStyle.CreateText(
|
|
row.transform,
|
|
"Mission Reward",
|
|
font,
|
|
26,
|
|
TextAnchor.MiddleCenter,
|
|
new Vector2(520f, 2f),
|
|
new Vector2(180f, 42f));
|
|
reward.resizeTextForBestFit = false;
|
|
reward.color = new Color32(255, 224, 157, 255);
|
|
|
|
missionRows[i] = new MissionRowView
|
|
{
|
|
Root = row.gameObject,
|
|
Title = missionTitle,
|
|
Condition = condition,
|
|
StatusBadge = statusBadge.gameObject,
|
|
StatusBadgeImage = statusBadge,
|
|
Status = status,
|
|
Progress = progress,
|
|
ProgressFill = progressFill,
|
|
Reward = reward,
|
|
};
|
|
}
|
|
|
|
achievementRoot = new GameObject("Achievement List Page");
|
|
achievementRoot.transform.SetParent(panel.transform, false);
|
|
Text achievementText = PresentationUiStyle.CreateText(
|
|
achievementRoot.transform,
|
|
"Achievement Preparation Message",
|
|
font,
|
|
30,
|
|
TextAnchor.MiddleCenter,
|
|
new Vector2(0f, 76f),
|
|
new Vector2(1120f, 180f));
|
|
achievementText.text =
|
|
"업적 준비 중\n업적 조건과 외형 보상은 아직 정해지지 않았습니다.";
|
|
achievementRecordsText = PresentationUiStyle.CreateText(
|
|
achievementRoot.transform,
|
|
"Lifetime Mission Records",
|
|
font,
|
|
25,
|
|
TextAnchor.MiddleCenter,
|
|
new Vector2(0f, -95f),
|
|
new Vector2(1120f, 130f));
|
|
|
|
panelHelpText = PresentationUiStyle.CreateText(
|
|
panel.transform,
|
|
"Mission List Help",
|
|
font,
|
|
22,
|
|
TextAnchor.MiddleCenter,
|
|
new Vector2(0f, -408f),
|
|
new Vector2(1200f, 36f));
|
|
panelHelpText.text = "Tab / Esc 닫기 · ← / → 탭 · ↑ / ↓ 페이지";
|
|
panel.SetActive(false);
|
|
overlay.SetActive(false);
|
|
}
|
|
|
|
private void CreateCompletionToast()
|
|
{
|
|
completionToast = PresentationUiStyle.CreatePanel(
|
|
transform,
|
|
"Mission Completion Toast",
|
|
new Vector2(1f, 0f),
|
|
new Vector2(-28f, 28f),
|
|
new Vector2(440f, 152f),
|
|
new Color(0.035f, 0.045f, 0.075f, 0.96f));
|
|
RectTransform toastRect = completionToast.GetComponent<RectTransform>();
|
|
toastRect.anchorMin = new Vector2(1f, 0f);
|
|
toastRect.anchorMax = new Vector2(1f, 0f);
|
|
toastRect.pivot = new Vector2(1f, 0f);
|
|
toastRect.anchoredPosition = new Vector2(-28f, 28f);
|
|
PresentationUiStyle.ApplyPanel(completionToast.GetComponent<Image>());
|
|
completionToast.GetComponent<Image>().raycastTarget = false;
|
|
CanvasGroup canvasGroup = completionToast.AddComponent<CanvasGroup>();
|
|
canvasGroup.interactable = false;
|
|
canvasGroup.blocksRaycasts = false;
|
|
|
|
Font font = PresentationUiStyle.GetGalmuriFont();
|
|
Text title = PresentationUiStyle.CreateText(
|
|
completionToast.transform,
|
|
"Mission Completion Toast Title",
|
|
font,
|
|
24,
|
|
TextAnchor.MiddleCenter,
|
|
new Vector2(0f, 48f),
|
|
new Vector2(400f, 32f));
|
|
title.text = "미션 성공";
|
|
title.color = new Color32(255, 218, 135, 255);
|
|
title.raycastTarget = false;
|
|
completionToastMissionText = PresentationUiStyle.CreateText(
|
|
completionToast.transform,
|
|
"Mission Completion Toast Mission",
|
|
font,
|
|
28,
|
|
TextAnchor.MiddleCenter,
|
|
new Vector2(0f, 10f),
|
|
new Vector2(400f, 38f));
|
|
completionToastMissionText.color = Color.white;
|
|
completionToastMissionText.raycastTarget = false;
|
|
completionToastRewardText = PresentationUiStyle.CreateText(
|
|
completionToast.transform,
|
|
"Mission Completion Toast Reward",
|
|
font,
|
|
24,
|
|
TextAnchor.MiddleCenter,
|
|
new Vector2(0f, -32f),
|
|
new Vector2(400f, 32f));
|
|
completionToastRewardText.color = new Color32(166, 230, 191, 255);
|
|
completionToastRewardText.raycastTarget = false;
|
|
completionToast.SetActive(false);
|
|
}
|
|
|
|
private Button CreateButton(
|
|
string name,
|
|
string label,
|
|
Vector2 position,
|
|
Vector2 size,
|
|
UnityEngine.Events.UnityAction onClick)
|
|
{
|
|
GameObject buttonObject = new(
|
|
name,
|
|
typeof(RectTransform),
|
|
typeof(CanvasRenderer),
|
|
typeof(Image),
|
|
typeof(Button));
|
|
buttonObject.transform.SetParent(panel.transform, false);
|
|
RectTransform rect = buttonObject.GetComponent<RectTransform>();
|
|
rect.anchorMin = new Vector2(0.5f, 0.5f);
|
|
rect.anchorMax = new Vector2(0.5f, 0.5f);
|
|
rect.pivot = new Vector2(0.5f, 0.5f);
|
|
rect.anchoredPosition = position;
|
|
rect.sizeDelta = size;
|
|
Image image = buttonObject.GetComponent<Image>();
|
|
image.color = TabIdleColor;
|
|
image.raycastTarget = true;
|
|
Button button = buttonObject.GetComponent<Button>();
|
|
button.targetGraphic = image;
|
|
button.navigation = new Navigation { mode = Navigation.Mode.None };
|
|
PresentationUiStyle.ApplyButton(button);
|
|
Text labelText = PresentationUiStyle.CreateText(
|
|
buttonObject.transform,
|
|
"Label",
|
|
PresentationUiStyle.GetGalmuriFont(),
|
|
name == "Mission Tab" || name == "Achievement Tab" ? 24 : 22,
|
|
TextAnchor.MiddleCenter,
|
|
Vector2.zero,
|
|
size);
|
|
labelText.text = label;
|
|
if (name == "Mission Tab" || name == "Achievement Tab")
|
|
{
|
|
image.sprite = null;
|
|
image.type = Image.Type.Simple;
|
|
image.color = TabIdleColor;
|
|
button.transition = Selectable.Transition.None;
|
|
Outline outline = buttonObject.AddComponent<Outline>();
|
|
outline.effectColor = TabIdleLabelColor;
|
|
outline.effectDistance = new Vector2(2f, 2f);
|
|
outline.useGraphicAlpha = true;
|
|
}
|
|
button.onClick.AddListener(onClick);
|
|
return button;
|
|
}
|
|
|
|
private void OpenPanel(bool achievementsOnly = false)
|
|
{
|
|
titleAchievementsOnly = achievementsOnly;
|
|
panel.SetActive(true);
|
|
overlay.SetActive(true);
|
|
showingAchievements = achievementsOnly;
|
|
pageIndex = 0;
|
|
panelTitleText.text = achievementsOnly ? "업적" : "미션 / 업적";
|
|
panelHelpText.text = achievementsOnly
|
|
? "Esc 닫기"
|
|
: "Tab / Esc 닫기 · ← / → 탭 · ↑ / ↓ 페이지";
|
|
if (!achievementsOnly)
|
|
{
|
|
titleAchievementsClosed = null;
|
|
}
|
|
RefreshView();
|
|
}
|
|
|
|
private void RefreshView()
|
|
{
|
|
if (panel == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
missionListRoot.SetActive(!showingAchievements);
|
|
achievementRoot.SetActive(showingAchievements);
|
|
missionTabButton.gameObject.SetActive(!titleAchievementsOnly);
|
|
achievementTabButton.gameObject.SetActive(!titleAchievementsOnly);
|
|
progressHeader.gameObject.SetActive(!showingAchievements);
|
|
rewardHeader.gameObject.SetActive(!showingAchievements);
|
|
previousPageButton.gameObject.SetActive(!showingAchievements);
|
|
nextPageButton.gameObject.SetActive(!showingAchievements);
|
|
pageText.gameObject.SetActive(!showingAchievements);
|
|
pageText.text = $"{pageIndex + 1}/{PageCount}";
|
|
UpdateTabVisual(missionTabButton, !showingAchievements);
|
|
UpdateTabVisual(achievementTabButton, showingAchievements);
|
|
|
|
VisibleMissionRowCount = 0;
|
|
for (int row = 0; row < missionRows.Length; row++)
|
|
{
|
|
int missionIndex = pageIndex * MissionsPerPage + row;
|
|
bool visible = !showingAchievements
|
|
&& missionIndex < RunMissionTracker.MissionCount;
|
|
MissionRowView view = missionRows[row];
|
|
view.Root.SetActive(visible);
|
|
if (!visible)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
RunMissionProgress entry = missionTracker != null
|
|
? missionTracker.GetProgress(missionIndex)
|
|
: new RunMissionProgress(
|
|
RunMissionTracker.GetDefinition(missionIndex),
|
|
0,
|
|
false,
|
|
false);
|
|
int progress = Mathf.Clamp(
|
|
entry.Progress,
|
|
0,
|
|
entry.Definition.Target);
|
|
view.Title.text = entry.Definition.Name;
|
|
view.Condition.text = GetDisplayCondition(missionIndex);
|
|
view.Progress.text = $"{progress}/{entry.Definition.Target}";
|
|
view.Reward.text = $"+{entry.Definition.ExperienceReward} XP";
|
|
view.StatusBadge.SetActive(entry.Locked || entry.Completed);
|
|
view.Status.text = entry.Locked ? "해금 대기" : "완료";
|
|
view.Status.color = entry.Locked
|
|
? new Color32(25, 30, 40, 255)
|
|
: new Color32(40, 32, 17, 255);
|
|
view.StatusBadgeImage.color = entry.Locked
|
|
? LockedColor
|
|
: CompletedColor;
|
|
view.Title.color = entry.Locked
|
|
? LockedColor
|
|
: entry.Completed
|
|
? CompletedColor
|
|
: Color.white;
|
|
view.Condition.color = entry.Locked
|
|
? new Color32(163, 169, 182, 255)
|
|
: entry.Completed
|
|
? new Color32(226, 216, 187, 255)
|
|
: MissionConditionColor;
|
|
view.Progress.color = entry.Locked
|
|
? LockedColor
|
|
: entry.Completed
|
|
? CompletedColor
|
|
: Color.white;
|
|
view.ProgressFill.color = entry.Locked
|
|
? new Color32(116, 125, 140, 255)
|
|
: entry.Completed
|
|
? CompletedColor
|
|
: ProgressFillColor;
|
|
view.ProgressFill.rectTransform.sizeDelta = new Vector2(
|
|
ProgressBarWidth * progress / entry.Definition.Target,
|
|
0f);
|
|
VisibleMissionRowCount++;
|
|
}
|
|
|
|
achievementRecordsText.text = GetLifetimeRecordText();
|
|
}
|
|
|
|
private static string GetDisplayCondition(int missionIndex)
|
|
{
|
|
return missionIndex switch
|
|
{
|
|
0 => "일반 적 100마리 처치 · 소환수 제외",
|
|
1 => "경험치 구슬 50개 획득",
|
|
2 => "몸통박치기로 피해 60회 주기",
|
|
3 => "아티팩트 10회 사용해 적중시키기",
|
|
4 => "후방 몸통박치기로 다른 적 15마리 공격",
|
|
5 => "아티팩트 한 번으로 다른 적 5마리 맞히기",
|
|
6 => "후방 몸통박치기로 일반 적 공격 끊기 8회",
|
|
7 => "가드 3회로 실제 공격 막기",
|
|
8 => "맞는 색 아티팩트로 실드 6회 줄이기",
|
|
9 => "충전 강화기 3회 사용해 적중시키기",
|
|
10 => "3초 안에 적 10마리 처치 · 소환수 제외",
|
|
_ => RunMissionTracker.GetDefinition(missionIndex).Condition,
|
|
};
|
|
}
|
|
|
|
private string GetLifetimeRecordText()
|
|
{
|
|
UserProfileStore store = missionTracker?.ProfileStore;
|
|
if (store == null)
|
|
{
|
|
return "영구 기록을 사용할 수 없습니다.";
|
|
}
|
|
|
|
string issueText = GetProfileIssueText(store.LoadIssue);
|
|
if (!store.CanSave && store.LoadIssue != "save-failed")
|
|
{
|
|
return issueText ?? "영구 기록을 사용할 수 없습니다.";
|
|
}
|
|
|
|
string totals = $"누적 미션 완료 {missionTracker.GetLifetimeMissionCompletions()}회\n"
|
|
+ $"파죽지세 완료 {missionTracker.GetLifetimeRampageCompletions()}회";
|
|
return string.IsNullOrEmpty(issueText)
|
|
? totals
|
|
: totals + "\n" + issueText;
|
|
}
|
|
|
|
private static string GetProfileIssueText(string issue)
|
|
{
|
|
return issue switch
|
|
{
|
|
"save-failed" =>
|
|
"영구 저장 실패 · 표시 수치가 다음 실행에 남지 않을 수 있습니다.",
|
|
"malformed" => "프로필을 읽을 수 없어 영구 기록을 표시하지 않습니다.",
|
|
"newer-schema" => "프로필 버전이 높아 영구 기록을 표시하지 않습니다.",
|
|
"unsupported-schema" => "지원하지 않는 프로필 버전입니다.",
|
|
_ => null,
|
|
};
|
|
}
|
|
|
|
private void HandleProgressChanged()
|
|
{
|
|
CaptureCompletionState(showNotice: true);
|
|
RefreshView();
|
|
}
|
|
|
|
private void CaptureCompletionState(bool showNotice)
|
|
{
|
|
if (missionTracker == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (int i = 0; i < RunMissionTracker.MissionCount; i++)
|
|
{
|
|
RunMissionProgress progress = missionTracker.GetProgress(i);
|
|
if (showNotice && progress.Completed && !lastMissionCompletionState[i])
|
|
{
|
|
pendingCompletionNotices.Enqueue(new MissionCompletionNotice
|
|
{
|
|
MissionName = progress.Definition.Name,
|
|
ExperienceReward = progress.Definition.ExperienceReward,
|
|
});
|
|
}
|
|
lastMissionCompletionState[i] = progress.Completed;
|
|
}
|
|
}
|
|
|
|
private void UpdateCompletionToast()
|
|
{
|
|
if (!CanDisplayCompletionToast())
|
|
{
|
|
completionToast?.SetActive(false);
|
|
return;
|
|
}
|
|
|
|
if (activeCompletionNotice == null && pendingCompletionNotices.Count > 0)
|
|
{
|
|
activeCompletionNotice = pendingCompletionNotices.Dequeue();
|
|
activeCompletionNoticeRemaining = CompletionNoticeDuration;
|
|
completionToastMissionText.text = activeCompletionNotice.MissionName;
|
|
completionToastRewardText.text =
|
|
$"+{activeCompletionNotice.ExperienceReward} XP";
|
|
}
|
|
|
|
if (activeCompletionNotice == null)
|
|
{
|
|
completionToast?.SetActive(false);
|
|
return;
|
|
}
|
|
|
|
completionToast.SetActive(true);
|
|
activeCompletionNoticeRemaining -= Time.deltaTime;
|
|
if (activeCompletionNoticeRemaining <= 0f)
|
|
{
|
|
activeCompletionNotice = null;
|
|
activeCompletionNoticeRemaining = 0f;
|
|
completionToast.SetActive(false);
|
|
}
|
|
}
|
|
|
|
private void UpdateCompletionToastVisibility()
|
|
{
|
|
if (completionToast != null)
|
|
{
|
|
completionToast.SetActive(
|
|
activeCompletionNotice != null && CanDisplayCompletionToast());
|
|
}
|
|
}
|
|
|
|
private bool CanDisplayCompletionToast()
|
|
{
|
|
return isActiveAndEnabled
|
|
&& runManager != null
|
|
&& !runManager.IsTitleScreen
|
|
&& !runManager.IsGameOver
|
|
&& !runManager.IsPaused
|
|
&& !runManager.IsSelectionOpen;
|
|
}
|
|
|
|
private void ClearCompletionNotices()
|
|
{
|
|
pendingCompletionNotices.Clear();
|
|
activeCompletionNotice = null;
|
|
activeCompletionNoticeRemaining = 0f;
|
|
completionToast?.SetActive(false);
|
|
}
|
|
|
|
private static void UpdateTabVisual(Button button, bool selected)
|
|
{
|
|
if (button != null && button.targetGraphic is Image image)
|
|
{
|
|
image.color = selected ? TabSelectedColor : TabIdleColor;
|
|
Text label = button.GetComponentInChildren<Text>();
|
|
if (label != null)
|
|
{
|
|
label.color = selected
|
|
? TabSelectedLabelColor
|
|
: TabIdleLabelColor;
|
|
}
|
|
|
|
Outline outline = button.GetComponent<Outline>();
|
|
if (outline != null)
|
|
{
|
|
outline.effectColor = selected
|
|
? TabSelectedColor
|
|
: TabIdleLabelColor;
|
|
outline.effectDistance = selected
|
|
? new Vector2(3f, 3f)
|
|
: new Vector2(2f, 2f);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ClosePanel(bool suppressPause, bool playSound)
|
|
{
|
|
if (!IsOpen)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Action closedCallback = titleAchievementsOnly
|
|
? titleAchievementsClosed
|
|
: null;
|
|
titleAchievementsOnly = false;
|
|
titleAchievementsClosed = null;
|
|
panel.SetActive(false);
|
|
overlay.SetActive(false);
|
|
if (suppressPause)
|
|
{
|
|
runManager?.SuppressPauseOpeningForCurrentFrame();
|
|
}
|
|
runManager?.CloseSelection(this);
|
|
if (playSound)
|
|
{
|
|
BumpCombatAudioService.Instance?.PlayUi("ui_cancel");
|
|
}
|
|
closedCallback?.Invoke();
|
|
}
|
|
|
|
private void HandleGameOver()
|
|
{
|
|
ClosePanel(suppressPause: false, playSound: false);
|
|
ClearCompletionNotices();
|
|
UpdateHintVisibility();
|
|
}
|
|
|
|
private void UpdateHintVisibility()
|
|
{
|
|
if (hintText == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
bool visible = runManager != null
|
|
&& isActiveAndEnabled
|
|
&& !IsOpen
|
|
&& !runManager.IsTitleScreen
|
|
&& !runManager.IsGameOver
|
|
&& !runManager.IsPaused
|
|
&& !runManager.IsSelectionOpen;
|
|
hintText.gameObject.SetActive(visible);
|
|
}
|
|
}
|
|
}
|