using System.Collections.Generic; using BumpCombat.Audio; using BumpCombat.Constants; using BumpCombat.Core; using BumpCombat.Player; using BumpCombat.UI; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.UI; namespace BumpCombat.Progression { public sealed class LevelUpController : MonoBehaviour { private const int MaximumSupportedOptions = 3; [SerializeField] private GameObject panel; [SerializeField] private Text[] optionTexts; [System.NonSerialized] private ModifierDefinition[] modifierCatalog; private readonly ModifierDefinition[] shownModifiers = new ModifierDefinition[MaximumSupportedOptions]; private Image[] optionCardImages = System.Array.Empty(); private ExperienceSystem experienceSystem; private PlayerStats playerStats; private PlayerHealth playerHealth; private int selectedIndex; private int shownCount; private int pendingSelections; private int ignoreInputThroughFrame; private void Start() { experienceSystem = FindAnyObjectByType(); if (experienceSystem == null || panel == null) { enabled = false; return; } playerStats = experienceSystem.GetComponent(); playerHealth = experienceSystem.GetComponent(); experienceSystem.OnLevelGained += QueueSelection; if (RunManager.Instance != null) { RunManager.Instance.OnSelectionChanged += HandleSelectionChanged; } PresentationUiStyle.ApplyPanel(panel.GetComponent()); ApplySelectionTextStyle(); CreateCardBackdrops(); panel.SetActive(false); } private void OnDestroy() { if (experienceSystem != null) { experienceSystem.OnLevelGained -= QueueSelection; } if (RunManager.Instance != null) { RunManager.Instance.OnSelectionChanged -= HandleSelectionChanged; } } private void Update() { if (!panel.activeSelf || shownCount == 0 || Time.frameCount <= ignoreInputThroughFrame || Keyboard.current == null) { return; } if (Keyboard.current.leftArrowKey.wasPressedThisFrame || Keyboard.current.upArrowKey.wasPressedThisFrame) { selectedIndex = (selectedIndex + shownCount - 1) % shownCount; BumpCombatAudioService.Instance?.PlayUi("ui_move"); UpdateSelectionVisual(); } else if (Keyboard.current.rightArrowKey.wasPressedThisFrame || Keyboard.current.downArrowKey.wasPressedThisFrame) { selectedIndex = (selectedIndex + 1) % shownCount; BumpCombatAudioService.Instance?.PlayUi("ui_move"); UpdateSelectionVisual(); } else if (Keyboard.current.spaceKey.wasPressedThisFrame) { bool applied = shownModifiers[selectedIndex].TryApply( playerStats, playerHealth); if (applied) { BumpCombatAudioService.Instance?.PlayUi("ui_confirm"); } panel.SetActive(false); RunManager.Instance?.CloseSelection(this); } } private void QueueSelection(int level) { pendingSelections++; TryOpenPendingSelection(); } private void TryOpenPendingSelection() { if (pendingSelections == 0 || RunManager.Instance == null || !RunManager.Instance.TryOpenSelection(this)) { return; } if (!BuildChoices()) { pendingSelections--; RunManager.Instance.CloseSelection(this); return; } pendingSelections--; selectedIndex = 0; panel.SetActive(true); ignoreInputThroughFrame = Time.frameCount; UpdateSelectionVisual(); } private bool BuildChoices() { List pool = new(); ModifierDefinition[] definitions = modifierCatalog ?? GameplayConstants.Current.LevelUp.ModifierDefinitions; if (definitions != null) { foreach (ModifierDefinition definition in definitions) { if (definition != null && definition.CanApply(playerStats)) { pool.Add(definition); } } } shownCount = Mathf.Min( Mathf.Clamp( GameplayConstants.Current.LevelUp.OptionCount, 1, MaximumSupportedOptions), optionTexts?.Length ?? 0, pool.Count); for (int i = 0; i < shownCount; i++) { int choiceIndex = Random.Range(0, pool.Count); shownModifiers[i] = pool[choiceIndex]; pool.RemoveAt(choiceIndex); optionTexts[i].text = shownModifiers[i].DisplayName; optionTexts[i].gameObject.SetActive(true); } for (int i = shownCount; i < (optionTexts?.Length ?? 0); i++) { optionTexts[i].gameObject.SetActive(false); } return shownCount > 0; } private void HandleSelectionChanged(bool isOpen) { if (!isOpen && !panel.activeSelf) { TryOpenPendingSelection(); } } private void UpdateSelectionVisual() { for (int i = 0; i < (optionTexts?.Length ?? 0); i++) { bool selected = i == selectedIndex && i < shownCount; optionTexts[i].color = selected ? new Color(1f, 0.85f, 0.2f) : Color.white; if (i < optionCardImages.Length) { PresentationUiStyle.ApplyCard( optionCardImages[i], selected); optionCardImages[i].gameObject.SetActive( optionTexts[i].gameObject.activeSelf); } } } private void ApplySelectionTextStyle() { Text[] texts = panel.GetComponentsInChildren(true); foreach (Text text in texts) { PresentationUiStyle.ApplyText(text, Mathf.Max(1, text.fontSize)); if (text.name == "Title") { text.horizontalOverflow = HorizontalWrapMode.Overflow; text.verticalOverflow = VerticalWrapMode.Truncate; text.resizeTextForBestFit = true; text.resizeTextMinSize = 24; text.resizeTextMaxSize = 38; } } } private void CreateCardBackdrops() { optionCardImages = new Image[optionTexts?.Length ?? 0]; for (int i = 0; i < optionCardImages.Length; i++) { Text optionText = optionTexts[i]; if (optionText == null) { continue; } optionCardImages[i] = PresentationUiStyle.CreateCardBackdrop( optionText, $"Option Card {i + 1}", optionText.rectTransform.sizeDelta); } } #if UNITY_EDITOR public bool DebugIsSelectionVisible => panel != null && panel.activeSelf; public void Configure( GameObject selectionPanel, Text[] options, ModifierDefinition[] definitions) { panel = selectionPanel; optionTexts = options; modifierCatalog = definitions; } public ModifierDefinition GetShownModifier(int index) { return index >= 0 && index < shownCount ? shownModifiers[index] : null; } #endif } }