Files

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);
}
}
}