using System.Collections.Generic; using BumpCombat.Audio; using BumpCombat.Combat; using BumpCombat.Core; using BumpCombat.Player; using BumpCombat.Spawning; using BumpCombat.UI; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.UI; namespace BumpCombat.Progression { public sealed class ArtifactRewardController : MonoBehaviour { private enum RewardSource { Elite, EliteRed, EliteBlue, MidBoss, } private const int RewardOptionCount = 2; private const int DebugOptionCount = 6; private const int StartupColorCount = 3; private const float OptionCardWidth = 380f; private const float OptionCardHeight = 128f; private const float OptionLabelHeight = 64f; private readonly Queue pendingRewards = new(); private readonly HashSet queuedSources = new(); private readonly ActiveArtifactDefinition[] shownArtifacts = new ActiveArtifactDefinition[DebugOptionCount]; private ActiveArtifactController artifacts; private SpawnDirector spawnDirector; private GameObject panel; private Text titleText; private Text[] optionTexts; private Image[] optionCardImages = System.Array.Empty(); private Image[] optionIconImages = System.Array.Empty(); private int shownCount; private int selectedIndex; private bool debugStartupSelection; private int startupSelectionCount; private bool debugStartupSelectionPending; private int debugStartupSelectionEarliestFrame; private int debugSelectionsRequired; private int debugSelectionsRemaining; private int ignoreInputThroughFrame; #if UNITY_EDITOR public static bool GrantCatalogForTests { get; set; } #endif private void Start() { artifacts = FindAnyObjectByType(); spawnDirector = FindAnyObjectByType(); if (artifacts == null || spawnDirector == null) { enabled = false; return; } CreatePanel(); spawnDirector.OnEventEnemyDefeated += HandleEventEnemyDefeated; if (RunManager.Instance != null) { RunManager.Instance.OnSelectionChanged += HandleSelectionChanged; RunManager.Instance.OnRunStarted += HandleRunStarted; } #if UNITY_EDITOR if (GrantCatalogForTests) { artifacts.DebugGrantCatalogArtifacts(); return; } #endif QueueStartupSelection( RunManager.Instance?.StartupArtifactSelectionCount ?? 3); } private void OnDestroy() { if (spawnDirector != null) { spawnDirector.OnEventEnemyDefeated -= HandleEventEnemyDefeated; } if (RunManager.Instance != null) { RunManager.Instance.OnSelectionChanged -= HandleSelectionChanged; RunManager.Instance.OnRunStarted -= HandleRunStarted; } } private void Update() { if (debugStartupSelectionPending && Time.frameCount >= debugStartupSelectionEarliestFrame && RunManager.Instance != null && !RunManager.Instance.IsTitleScreen && !RunManager.Instance.IsSelectionOpen && !RunManager.Instance.IsPaused && !RunManager.Instance.IsGameOver) { if (OpenStartupSelection()) { debugStartupSelectionPending = false; return; } } if (panel == null || !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) { ChooseOption(selectedIndex); } } private void HandleRunStarted() { QueueStartupSelection( RunManager.Instance?.StartupArtifactSelectionCount ?? 3); } private void QueueStartupSelection(int selectionCount) { startupSelectionCount = Mathf.Max(0, selectionCount); debugStartupSelectionPending = true; debugStartupSelectionEarliestFrame = Time.frameCount + 1; } private bool ChooseOption(int optionIndex) { if (panel == null || !panel.activeSelf || optionIndex < 0 || optionIndex >= shownCount || !artifacts.TryAddArtifact(shownArtifacts[optionIndex])) { return false; } BumpCombatAudioService.Instance?.PlayUi("ui_confirm"); if (artifacts.OwnedArtifactCount == 1) { GetComponent()?.NotifyArtifactAcquired(); } if (debugStartupSelection) { debugSelectionsRemaining--; if (debugSelectionsRemaining > 0) { // Startup selection is deliberately color ordered. Each // step exposes only the two definitions in the next color // group, so a failed duplicate-color click cannot consume // a slot or leave an invalid loadout. if (!PopulateStartupOptions()) { debugStartupSelection = false; CloseSelection(); return true; } selectedIndex = 0; UpdateDebugTitle(); ShowOptions(); return true; } debugStartupSelection = false; } CloseSelection(); return true; } private bool OpenStartupSelection() { if (RunManager.Instance == null || !RunManager.Instance.TryOpenSelection(this)) { return false; } debugSelectionsRequired = Mathf.Min( startupSelectionCount, artifacts.MaxOwnedArtifacts - artifacts.OwnedArtifactCount); if (debugSelectionsRequired <= 0) { RunManager.Instance.CloseSelection(this); return true; } debugStartupSelection = true; debugSelectionsRemaining = debugSelectionsRequired; if (!PopulateStartupOptions()) { debugStartupSelection = false; RunManager.Instance.CloseSelection(this); return true; } selectedIndex = 0; ShowOptions(); UpdateDebugTitle(); return true; } private bool PopulateStartupOptions() { shownCount = 0; int selectedCount = debugSelectionsRequired - debugSelectionsRemaining; if (selectedCount < 0 || selectedCount >= StartupColorCount) { return false; } ArtifactColor requiredColor = GetStartupColor(selectedCount); for (int i = 0; i < artifacts.CatalogCount && shownCount < DebugOptionCount; i++) { ActiveArtifactDefinition definition = artifacts.GetCatalogArtifactAt(i); if (definition == null || definition.ArtifactColor != requiredColor || artifacts.OwnsArtifact(definition)) { continue; } shownArtifacts[shownCount++] = definition; } return shownCount > 0; } private static ArtifactColor GetStartupColor(int selectionIndex) { return selectionIndex switch { 0 => ArtifactColor.Green, 1 => ArtifactColor.Red, _ => ArtifactColor.Blue, }; } private static string GetArtifactColorName(ArtifactColor color) { return color switch { ArtifactColor.Green => "GREEN", ArtifactColor.Red => "RED", _ => "BLUE", }; } private void HandleEventEnemyDefeated(RunTimedEvent timedEvent) { if (timedEvent == RunTimedEvent.Elite) { if (RunManager.Instance?.IsProductionRun == true) { int spawnedEliteCount = spawnDirector?.TimedEliteSpawnCount ?? 0; if (spawnedEliteCount == 2) { QueueReward(RewardSource.EliteRed); } else if (spawnedEliteCount == 3) { QueueReward(RewardSource.EliteBlue); } } else { QueueReward(RewardSource.Elite); } } else if (timedEvent == RunTimedEvent.MidBoss && RunManager.Instance?.IsProductionRun != true) { QueueReward(RewardSource.MidBoss); } } private void QueueReward(RewardSource source) { if (artifacts == null || artifacts.OwnedArtifactCount >= artifacts.MaxOwnedArtifacts) { return; } if (!queuedSources.Add(source)) { return; } pendingRewards.Enqueue(source); TryOpenPendingReward(); } private void TryOpenPendingReward() { if (pendingRewards.Count == 0 || artifacts == null) { return; } if (artifacts.OwnedArtifactCount >= artifacts.MaxOwnedArtifacts) { pendingRewards.Clear(); return; } if (RunManager.Instance == null || !RunManager.Instance.TryOpenSelection(this)) { return; } RewardSource source = pendingRewards.Peek(); bool hasRequiredColor = TryGetRewardColor( source, out ArtifactColor requiredColor); List candidates = new(); for (int i = 0; i < artifacts.CatalogCount; i++) { ActiveArtifactDefinition definition = artifacts.GetCatalogArtifactAt(i); if (definition != null && !artifacts.OwnsArtifact(definition) && (!hasRequiredColor || definition.ArtifactColor == requiredColor)) { candidates.Add(definition); } } shownCount = Mathf.Min(RewardOptionCount, candidates.Count); if (shownCount == 0) { pendingRewards.Clear(); RunManager.Instance.CloseSelection(this); return; } pendingRewards.Dequeue(); debugStartupSelection = false; for (int i = 0; i < shownCount; i++) { int choiceIndex = Random.Range(0, candidates.Count); shownArtifacts[i] = candidates[choiceIndex]; candidates.RemoveAt(choiceIndex); } titleText.text = source switch { RewardSource.Elite => "ELITE REWARD - CHOOSE ARTIFACT", RewardSource.EliteRed => "ELITE REWARD - CHOOSE RED ARTIFACT", RewardSource.EliteBlue => "ELITE REWARD - CHOOSE BLUE ARTIFACT", RewardSource.MidBoss => "MID BOSS REWARD - CHOOSE ARTIFACT", _ => "CHOOSE ARTIFACT", }; selectedIndex = 0; ShowOptions(); } private void HandleSelectionChanged(bool isOpen) { if (!isOpen && (panel == null || !panel.activeSelf)) { TryOpenPendingReward(); } } private void UpdateSelectionVisual() { for (int i = 0; i < optionTexts.Length; i++) { bool ownedDebugOption = debugStartupSelection && i < shownCount && artifacts.OwnsArtifact(shownArtifacts[i]); bool selected = i == selectedIndex && i < shownCount; optionTexts[i].color = ownedDebugOption ? new Color(0.35f, 1f, 0.55f) : selected ? new Color(1f, 0.85f, 0.2f) : Color.white; if (i < optionCardImages.Length) { PresentationUiStyle.ApplyCard( optionCardImages[i], selected || ownedDebugOption); optionCardImages[i].gameObject.SetActive( optionTexts[i].gameObject.activeSelf); } } } private void ShowOptions() { RefreshOptionTexts(); LayoutOptions(); panel.SetActive(true); ignoreInputThroughFrame = Time.frameCount; UpdateSelectionVisual(); } private void RefreshOptionTexts() { for (int i = 0; i < optionTexts.Length; i++) { bool visible = i < shownCount; optionTexts[i].gameObject.SetActive(visible); if (!visible) { if (i < optionIconImages.Length) { optionIconImages[i].gameObject.SetActive(false); } continue; } ActiveArtifactDefinition definition = shownArtifacts[i]; Sprite icon = PresentationUiStyle.GetArtifactIcon(definition); optionIconImages[i].sprite = icon; optionIconImages[i].enabled = icon != null; optionIconImages[i].gameObject.SetActive(icon != null); optionTexts[i].text = icon != null ? definition.DisplayName : $"{definition.PlaceholderSymbol}\n{definition.DisplayName}"; if (debugStartupSelection && artifacts.OwnsArtifact(definition)) { optionTexts[i].text += "\nSELECTED"; } } } private void LayoutOptions() { bool twoRows = shownCount > RewardOptionCount; for (int i = 0; i < shownCount; i++) { int row = twoRows ? i / RewardOptionCount : 0; int column = twoRows ? i % RewardOptionCount : i; int rowStart = row * RewardOptionCount; int rowCount = Mathf.Min( RewardOptionCount, shownCount - rowStart); Vector2 cardPosition = new Vector2( (column - (rowCount - 1) * 0.5f) * 430f, twoRows ? 90f - row * 190f : 0f); if (i < optionCardImages.Length) { optionCardImages[i].rectTransform.anchoredPosition = cardPosition; } optionTexts[i].rectTransform.anchoredPosition = cardPosition + Vector2.down * 25f; if (i < optionIconImages.Length) { optionIconImages[i].rectTransform.anchoredPosition = cardPosition + Vector2.up * 20f; } } } private void UpdateDebugTitle() { int selectedCount = debugSelectionsRequired - debugSelectionsRemaining; if (startupSelectionCount <= 1) { titleText.text = "CHOOSE GREEN ARTIFACT " + $"({selectedCount}/{debugSelectionsRequired})"; return; } ArtifactColor requiredColor = GetStartupColor( Mathf.Clamp(selectedCount, 0, StartupColorCount - 1)); titleText.text = $"CHOOSE {GetArtifactColorName(requiredColor)} ARTIFACT " + $"({selectedCount}/{debugSelectionsRequired})"; } private static bool TryGetRewardColor( RewardSource source, out ArtifactColor color) { switch (source) { case RewardSource.EliteRed: color = ArtifactColor.Red; return true; case RewardSource.EliteBlue: color = ArtifactColor.Blue; return true; default: color = ArtifactColor.Green; return false; } } private void CloseSelection() { panel.SetActive(false); RunManager.Instance?.CloseSelection(this); } private void CreatePanel() { Font font = PresentationUiStyle.GetGalmuriFont(); panel = PresentationUiStyle.CreatePanel( transform, "Artifact Reward Selection", new Vector2(0.5f, 0.5f), Vector2.zero, new Vector2(1500f, 650f), new Color(0.03f, 0.04f, 0.08f, 0.96f)); PresentationUiStyle.ApplyPanel(panel.GetComponent()); titleText = PresentationUiStyle.CreateText( panel.transform, "Title", font, 38, TextAnchor.MiddleCenter, new Vector2(0f, 255f), new Vector2(1200f, 60f)); optionTexts = new Text[DebugOptionCount]; optionCardImages = new Image[DebugOptionCount]; optionIconImages = new Image[DebugOptionCount]; for (int i = 0; i < optionTexts.Length; i++) { optionTexts[i] = PresentationUiStyle.CreateText( panel.transform, $"Option {i + 1}", font, 26, TextAnchor.MiddleCenter, Vector2.zero, new Vector2(OptionCardWidth, OptionLabelHeight)); optionCardImages[i] = PresentationUiStyle.CreateCardBackdrop( optionTexts[i], $"{optionTexts[i].name} Card", new Vector2(OptionCardWidth, OptionCardHeight)); RectTransform optionRect = optionTexts[i].rectTransform; optionIconImages[i] = PresentationUiStyle.CreateImage( optionTexts[i].transform.parent, $"{optionTexts[i].name} Icon", Vector2.zero, new Vector2(48f, 48f)); optionIconImages[i].rectTransform.anchorMin = optionRect.anchorMin; optionIconImages[i].rectTransform.anchorMax = optionRect.anchorMax; optionIconImages[i].rectTransform.pivot = optionRect.pivot; optionIconImages[i].rectTransform.anchoredPosition = optionRect.anchoredPosition + Vector2.up * 20f; optionIconImages[i].transform.SetSiblingIndex( optionTexts[i].transform.GetSiblingIndex()); optionIconImages[i].enabled = false; } Text help = PresentationUiStyle.CreateText( panel.transform, "Help", font, 22, TextAnchor.MiddleCenter, new Vector2(0f, -275f), new Vector2(900f, 40f)); help.text = "Arrow keys: select Space: confirm"; PresentationUiStyle.ApplyText(help, 22); panel.SetActive(false); } #if UNITY_EDITOR public int DebugShownCount => shownCount; public bool DebugIsSelectionVisible => panel != null && panel.activeSelf; public ActiveArtifactDefinition DebugGetShownArtifact(int index) { return index >= 0 && index < shownCount ? shownArtifacts[index] : null; } public bool DebugChooseOption(int optionIndex) { return ChooseOption(optionIndex); } public void DebugQueueRedArtifactReward() { QueueReward(RewardSource.EliteRed); } #endif } }