using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
namespace BulletHellTemplate
{
///
/// Manages the profile menu UI, including displaying player information (name, icon, frame), favorite character data,
/// audio sliders, localized status messages, and *toggleable* icon / frame selection panels.
/// Buttons that previously called or directly can remain wired the same way: each method now behaves as a toggle. If the requested content is already visible, the call will close the container; otherwise it will open the container and (re)build the corresponding entries.
/// The container now starts closed every time the menu is enabled.
///
public class UIProfileMenu : MonoBehaviour
{
/*────────────────────────────────────────────────────────────────────────────*/
#region Inspector Fields
[Header("UI Elements")]
[Tooltip("Text component to display the player's name.")]
public TextMeshProUGUI playerName;
[Tooltip("Image component to display the player's icon.")]
public Image playerIcon;
[Tooltip("Image component to display the player's frame.")]
public Image playerFrame;
[Header("UI Prefabs")]
[Tooltip("Prefab for creating icon entries.")]
public IconsEntry iconsEntryPrefab;
[Tooltip("Prefab for creating frame entries.")]
public FramesEntry framesEntryPrefab;
[Tooltip("Container to hold dynamically created icon and frame entries.")]
public Transform container;
[Tooltip("Container GameObject for icons/frames.")]
public GameObject containerPref;
[Tooltip("UI passEntryPrefab for the name change input screen.")]
public GameObject changeNamePref;
[Tooltip("Input field for entering a new name.")]
public TMP_InputField changeNameInput;
[Tooltip("UI component to display status messages (success/error).")]
public TextMeshProUGUI statusText;
[Header("Favorite character")]
[Tooltip("Image to display mastery icon of the favorite character.")]
public Image masteryIcon;
[Tooltip("Image fill component to display mastery progress.")]
public Image masteryProgressBar;
[Tooltip("UI text to display current mastery experience.")]
public TextMeshProUGUI currentMasteryExp;
[Tooltip("UI text to display the mastery level name.")]
public TextMeshProUGUI masteryName;
[Tooltip("Container to hold the temporary favorite character model.")]
public Transform tempCharacterContainer;
[Header("UI Translations")]
[Tooltip("Fallback message when there are not enough tickets to change the name.")]
public string notEnoughTickets = "Not enough tickets to change the name.";
[Tooltip("Translations for the message when there are not enough tickets to change the name.")]
public NameTranslatedByLanguage[] notEnoughTicketsTranslated;
[Tooltip("Fallback message when name length is invalid.")]
public string nameLengthError = "Name must be between 3 and 14 characters.";
[Tooltip("Translations for the message when name length is invalid.")]
public NameTranslatedByLanguage[] nameLengthErrorTranslated;
[Tooltip("Fallback message when the name is already taken.")]
public string nameAlreadyTaken = "Name already taken.";
[Tooltip("Translations for the message when the name is already taken.")]
public NameTranslatedByLanguage[] nameAlreadyTakenTranslated;
[Tooltip("Fallback message when the name is changed successfully.")]
public string nameChangeSuccess = "Name changed successfully.";
[Tooltip("Translations for the message when the name is changed successfully.")]
public NameTranslatedByLanguage[] nameChangeSuccessTranslated;
[Tooltip("Fallback message when there's an error changing the name.")]
public string nameChangeFail = "Error changing name.";
[Tooltip("Translations for the message when there's an error changing the name.")]
public NameTranslatedByLanguage[] nameChangeFailTranslated;
[Header("Events")]
[Tooltip("Event invoked when the menu is opened.")]
public UnityEvent OnOpenMenu;
[Tooltip("Event invoked when the menu is closed.")]
public UnityEvent OnCloseMenu;
[Tooltip("Event invoked when the nickname is successfully changed.")]
public UnityEvent OnChangeNickname;
[Tooltip("Event invoked when the icon is successfully changed.")]
public UnityEvent OnChangeIcon;
[Tooltip("Event invoked when the frame is successfully changed.")]
public UnityEvent OnChangeFrame;
[Tooltip("Event invoked when a coupon is redeemed successfully.")]
public UnityEvent OnRedeemCoupon;
[Header("Audio Settings")]
[Tooltip("Slider for adjusting master volume.")]
public Slider masterVolumeSlider;
[Tooltip("Slider for adjusting VFX volume.")]
public Slider vfxVolumeSlider;
[Tooltip("Slider for adjusting ambience volume.")]
public Slider ambienceVolumeSlider;
#endregion
/*────────────────────────────────────────────────────────────────────────────*/
/*────────────────────────────────────────────────────────────────────────────*/
#region Private Runtime State
private string currentLang;
private readonly List iconsEntries = new();
private readonly List framesEntries = new();
public static UIProfileMenu Singleton; // maintained for legacy usages
///
/// Runtime state for the dynamic selection container.
///
private enum ContentState { Closed, Icons, Frames }
private ContentState _contentState = ContentState.Closed;
#endregion
/*────────────────────────────────────────────────────────────────────────────*/
#region Unity Lifecycle
private void Awake()
{
LanguageManager.LanguageManager.onLanguageChanged += UpdateText;
Singleton = this;
}
private void OnDestroy()
{
LanguageManager.LanguageManager.onLanguageChanged -= UpdateText;
}
private void OnEnable()
{
currentLang = LanguageManager.LanguageManager.Instance.GetCurrentLanguage();
OnOpenMenu?.Invoke();
DestroyTemporaryModel();
LoadProfile();
LoadFavoriteCharacter();
UpdateText();
SetupSliders();
// Ensure the selection container starts closed when menu opens.
HideContainer();
}
private void OnDisable()
{
DestroyTemporaryModel();
HideContainer();
OnCloseMenu?.Invoke();
if (UIMainMenu.Singleton != null)
UIMainMenu.Singleton.LoadPlayerInfo();
}
#endregion
#region Language & Localization
private void UpdateText()
{
currentLang = LanguageManager.LanguageManager.Instance.GetCurrentLanguage();
LoadFavoriteCharacter();
}
#endregion
#region Profile Loading
///
/// Loads and displays the player's profile information (name, icon, frame).
/// NOTE: No longer toggles the icon/frame container; that behavior is handled
/// by / toggle methods.
///
public void LoadProfile()
{
string name = PlayerSave.GetPlayerName();
if (playerName != null)
playerName.text = !string.IsNullOrEmpty(name) ? name : "Unknown Player";
string iconId = PlayerSave.GetPlayerIcon();
string frameId = PlayerSave.GetPlayerFrame();
SetPlayerIcon(iconId);
SetPlayerFrame(frameId);
}
#endregion
#region Audio Sliders
///
/// Initialize sliders from persisted audio values and hook change events.
/// Uses setters (which also persist changes).
///
private void SetupSliders()
{
var am = AudioManager.Singleton;
if (am == null)
return;
if (masterVolumeSlider != null)
{
masterVolumeSlider.onValueChanged.RemoveAllListeners();
masterVolumeSlider.SetValueWithoutNotify(am.masterVolume); // start from saved
masterVolumeSlider.onValueChanged.AddListener(OnMasterVolumeChanged);
}
if (vfxVolumeSlider != null)
{
vfxVolumeSlider.onValueChanged.RemoveAllListeners();
vfxVolumeSlider.SetValueWithoutNotify(am.vfxVolume); // start from saved
vfxVolumeSlider.onValueChanged.AddListener(OnVFXVolumeChanged);
}
if (ambienceVolumeSlider != null)
{
ambienceVolumeSlider.onValueChanged.RemoveAllListeners();
ambienceVolumeSlider.SetValueWithoutNotify(am.ambienceVolume); // start from saved
ambienceVolumeSlider.onValueChanged.AddListener(OnAmbienceVolumeChanged);
}
}
/// Called by Master slider – forwards to .
public void OnMasterVolumeChanged(float value)
{
var am = AudioManager.Singleton;
if (am != null)
am.SetMasterVolume(value);
}
/// Called by VFX slider – forwards to .
public void OnVFXVolumeChanged(float value)
{
var am = AudioManager.Singleton;
if (am != null)
am.SetVFXVolume(value);
}
/// Called by Ambience slider – forwards to .
public void OnAmbienceVolumeChanged(float value)
{
var am = AudioManager.Singleton;
if (am != null)
am.SetAmbienceVolume(value);
}
#endregion
/*────────────────────────────────────────────────────────────────────────────*/
#region Toggle Container API (Call These From Buttons)
///
/// Toggle the icon selection panel. If icons are visible, closes the panel.
/// If closed or currently showing frames, shows icons.
///
public void LoadIcons()
{
if (_contentState == ContentState.Icons && containerPref != null && containerPref.activeSelf)
{
HideContainer();
return;
}
ShowIcons();
}
///
/// Toggle the frame selection panel. If frames are visible, closes the panel.
/// If closed or currently showing icons, shows frames.
///
public void LoadFrames()
{
if (_contentState == ContentState.Frames && containerPref != null && containerPref.activeSelf)
{
HideContainer();
return;
}
ShowFrames();
}
#endregion
#region Internal Build Helpers
private void ShowIcons()
{
ClearContainer();
if (containerPref != null)
containerPref.SetActive(true);
BuildIcons();
_contentState = ContentState.Icons;
}
private void ShowFrames()
{
ClearContainer();
if (containerPref != null)
containerPref.SetActive(true);
BuildFrames();
_contentState = ContentState.Frames;
}
private void HideContainer()
{
ClearContainer();
if (containerPref != null && containerPref.activeSelf)
containerPref.SetActive(false);
_contentState = ContentState.Closed;
}
private void ClearContainer()
{
iconsEntries.Clear();
framesEntries.Clear();
if (container == null)
return;
for (int i = container.childCount - 1; i >= 0; --i)
{
Destroy(container.GetChild(i).gameObject);
}
}
///
/// Build icon entry UI elements into the container (assumes container active/cleared).
///
private void BuildIcons()
{
if (container == null || iconsEntryPrefab == null || GameInstance.Singleton == null)
return;
foreach (IconItem item in GameInstance.Singleton.iconItems)
{
if (PlayerSave.IsIconPurchased(item.iconId) || item.isUnlocked)
{
IconsEntry iconEntry = Instantiate(iconsEntryPrefab, container);
iconEntry.SetIconInfo(item.iconId);
iconEntry.icon.sprite = item.icon;
string iconNameTranslatedName = GetTranslatedString(item.iconNameTranslated, item.iconName, currentLang);
iconEntry.iconName.text = iconNameTranslatedName;
iconsEntries.Add(iconEntry);
}
}
}
///
/// Build frame entry UI elements into the container (assumes container active/cleared).
///
private void BuildFrames()
{
if (container == null || framesEntryPrefab == null || GameInstance.Singleton == null)
return;
foreach (FrameItem item in GameInstance.Singleton.frameItems)
{
if (PlayerSave.IsFramePurchased(item.frameId) || item.isUnlocked)
{
FramesEntry frameEntry = Instantiate(framesEntryPrefab, container);
frameEntry.SetFrameInfo(item.frameId);
string frameNameTranslatedName = GetTranslatedString(item.frameNameTranslated, item.frameName, currentLang);
frameEntry.icon.sprite = item.icon;
frameEntry.frameName.text = frameNameTranslatedName;
framesEntries.Add(frameEntry);
}
}
}
#endregion
/*────────────────────────────────────────────────────────────────────────────*/
#region Favorite Character
public void LoadFavoriteCharacter()
{
int favouriteCharacterId = PlayerSave.GetFavouriteCharacter();
int currentMasteryLevel = PlayerSave.GetCharacterMasteryLevel(favouriteCharacterId);
int currMasteryExp = PlayerSave.GetCharacterCurrentMasteryExp(favouriteCharacterId);
CharacterMasteryLevel masteryInfo = GameInstance.Singleton.GetMasteryLevel(currentMasteryLevel);
string masteryTranslatedName = GetTranslatedString(masteryInfo.masteryNameTranslated, masteryInfo.masteryName, currentLang);
if (masteryName != null) masteryName.text = masteryTranslatedName;
if (masteryIcon != null) masteryIcon.sprite = masteryInfo.masteryIcon;
if (currentMasteryLevel < GameInstance.Singleton.characterMastery.maxMasteryLevel)
{
int requiredMasteryExp = GameInstance.Singleton.GetMasteryExpForLevel(currentMasteryLevel);
if (currentMasteryExp != null) currentMasteryExp.text = $"{currMasteryExp}/{requiredMasteryExp}";
if (masteryProgressBar != null) masteryProgressBar.fillAmount = (float)currMasteryExp / requiredMasteryExp;
}
else
{
if (currentMasteryExp != null) currentMasteryExp.text = $"{currMasteryExp}/MAX";
if (masteryProgressBar != null) masteryProgressBar.fillAmount = 1f;
}
// Rebuild the temporary model visual
DestroyTemporaryModel();
CharacterData cd = GetCharacterDataById(favouriteCharacterId);
if (cd != null && tempCharacterContainer != null)
{
int skinIndex = PlayerSave.GetCharacterSkin(cd.characterId);
if (cd.characterSkins != null && cd.characterSkins.Length > 0 && skinIndex >= 0 && skinIndex < cd.characterSkins.Length)
{
CharacterSkin skin = cd.characterSkins[skinIndex];
if (skin.skinCharacterModel != null)
Instantiate(skin.skinCharacterModel, tempCharacterContainer);
}
else if (cd.characterModel != null)
{
Instantiate(cd.characterModel, tempCharacterContainer);
}
}
}
#endregion
/*────────────────────────────────────────────────────────────────────────────*/
#region Name Change Flow
public void OnClickChangeName()
{
if (GameInstance.Singleton.needTicket && MonetizationManager.GetCurrency(GameInstance.Singleton.changeNameTick) < GameInstance.Singleton.ticketsToChange)
{
DisplayStatusMessage(GetTranslatedString(notEnoughTicketsTranslated, notEnoughTickets, currentLang), Color.red);
return;
}
if (changeNamePref != null)
changeNamePref.SetActive(true);
}
public async void ProcessChangeName()
{
string newName = changeNameInput.text;
RequestResult res = await BackendManager.Service.ChangePlayerNameAsync(newName);
string msg;
Color color;
if (res.Success)
{
msg = GetTranslatedString(nameChangeSuccessTranslated, nameChangeSuccess, currentLang);
color = Color.white;
if (playerName != null)
playerName.text = newName;
if (UIMainMenu.Singleton != null)
UIMainMenu.Singleton.LoadPlayerInfo();
OnChangeNickname?.Invoke();
if (changeNamePref != null)
changeNamePref.SetActive(false);
}
else
{
(msg, color) = res.Reason switch
{
"0" => (GetTranslatedString(nameLengthErrorTranslated, nameLengthError, currentLang), Color.red),
"1" => (GetTranslatedString(notEnoughTicketsTranslated, notEnoughTickets, currentLang), Color.red),
"2" => (GetTranslatedString(nameAlreadyTakenTranslated, nameAlreadyTaken, currentLang), Color.red),
_ => (GetTranslatedString(nameChangeFailTranslated, nameChangeFail, currentLang), Color.red)
};
}
DisplayStatusMessage(msg, color);
}
#endregion
/*────────────────────────────────────────────────────────────────────────────*/
#region Utility Helpers
private CharacterData GetCharacterDataById(int characterId)
{
if (GameInstance.Singleton == null || GameInstance.Singleton.characterData == null)
return null;
foreach (CharacterData item in GameInstance.Singleton.characterData)
{
if (item.characterId == characterId)
return item;
}
return null;
}
private void DestroyTemporaryModel()
{
if (tempCharacterContainer == null) return;
for (int i = tempCharacterContainer.childCount - 1; i >= 0; i--)
Destroy(tempCharacterContainer.GetChild(i).gameObject);
}
private string GetTranslatedString(NameTranslatedByLanguage[] translations, string fallback, string currentLang)
{
if (translations != null)
{
foreach (var translation in translations)
{
if (!string.IsNullOrEmpty(translation.LanguageId) &&
translation.LanguageId.Equals(currentLang) &&
!string.IsNullOrEmpty(translation.Translate))
{
return translation.Translate;
}
}
}
return fallback;
}
public void DisplayStatusMessage(string message, Color color)
{
if (statusText == null)
return;
statusText.text = message;
statusText.color = color;
StartCoroutine(HideStatusMessageAfterDelay(statusText, 2f));
}
private IEnumerator HideStatusMessageAfterDelay(TextMeshProUGUI txt, float delay)
{
yield return new WaitForSeconds(delay);
if (txt != null)
txt.text = string.Empty;
}
private void SetPlayerIcon(string iconId)
{
if (playerIcon == null || GameInstance.Singleton == null)
return;
foreach (IconItem item in GameInstance.Singleton.iconItems)
{
if (item.iconId == iconId)
{
playerIcon.sprite = item.icon;
break;
}
}
}
private void SetPlayerFrame(string frameId)
{
if (playerFrame == null || GameInstance.Singleton == null)
return;
foreach (FrameItem item in GameInstance.Singleton.frameItems)
{
if (item.frameId == frameId)
{
playerFrame.sprite = item.icon;
break;
}
}
}
#endregion
}
}