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