강화 시스템

아이템 타입과 강화 단계에 따라 필요한 재화와 재료를 계산하고, 룰렛을 이용한 확률 기반 강화 결과를 제공한다.

성공과 실패는 명확히 분기되며, 각 결과에 따라 아이템 상태와 시각·청각적 피드백이 즉시 반영된다.

전체 플레이 영상

전체 코드

using System.Collections;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class UpgradeSlot : MonoBehaviour
{
    private const string CrystalItemId = "200_02_01";   // 강화 크리스탈 아이템 ID

    ItemManager itemManager;
    Item selectedItem;  // 선택한 아이템의 대한 정보

    [Header("선택된 아이템 정보")]
    [SerializeField] Image icon = null;                    // 아이템 이미지
    [SerializeField] Image gradeImage = null;              // 아이템의 강화단계 이미지
    [SerializeField] TextMeshProUGUI itemName = null;      // 아이템 이름
    [SerializeField] TextMeshProUGUI upgradeInfo = null;   // 강화 정보 메세지 (현재 레벨 > 다음 레벨)  

    [Header("필요한 아이템 정보")]
    [SerializeField] TextMeshProUGUI crystalCount = null;  // '소지하고 있는 수량 / 필요한 크리스탈 수량'
    [SerializeField] TextMeshProUGUI reqCoin = null;       // 필요한 재화

    int requiredCoin;           // 필요한 재화
    int requiredCrystal;        // 필요한 크리스탈 수량
    int currentCrystalCount;    // 소유하고 있는 크리스탈 수량

    [SerializeField] GameObject upgradeBtn;     // 강화 버튼
    [SerializeField] GameObject warningText;    // 재료가 부족할 때 띄울 경고 메세지

    [Header("룰렛")]
    public GameObject roulettePanel;                          // 룰렛 패널 
    [HideInInspector] public bool isRotating = false;         // 룰렛 회전 여부
    [HideInInspector] public bool upgradeAttempted = false;   // 강화의 추가 시도를 제한

    [Header("결과창")]
    public GameObject resultPanel;      // 결과 패널
    public TextMeshProUGUI percentTxt;  // 강화 확률
    public TextMeshProUGUI resultTxt;   // 강화 결과

    [Header("이펙트 & 사운드")]
    [SerializeField] private ParticleSystem successEffect;   // 성공 이펙트
    [SerializeField] private ParticleSystem failEffect;      // 실패 이펙트

    [SerializeField] private AudioSource audioSource;        // 공용 오디오 소스
    [SerializeField] private AudioClip successClip;          // 성공 사운드
    [SerializeField] private AudioClip failClip;             // 실패 사운드

    void Start()
    {
        itemManager = GameManager_LDW.instance.itemManager;
        CancelSelection();
    }

    void Update()
    {
        currentCrystalCount = itemManager.GetItem(CrystalItemId).count;   // 현재 소지하고 있는 크리스탈 수량

        // [ 선택한 아이템에 따라 재료 설정 ]
        // 아이템 선택 시 
        if (selectedItem != null)
        {
            UpdateSelectedItemUI();
            UpdateRequirementsByType();
            UpdateUpgradeState();
        }

        UpdateGradeImageVisibility();
    }

    public void UpdateItem()
    {
        currentCrystalCount = itemManager.GetItem(CrystalItemId).count;   // 현재 소지하고 있는 크리스탈 수량
    }

    // 아이템 선택
    public void SelectItem(Item _item)  // SelectItem함수에서 Item을 받아옴
    {
        selectedItem = _item;   // SelectedItem에 받아온 아이템의 정보를 담음
    }

    // 선택된 아이템의 기본 UI 정보 업데이트
    private void UpdateSelectedItemUI()
    {
        icon.enabled = true;                            // 선택된 아이템이 있다면, 아이템 아이콘 이미지를 활성화
        icon.sprite = selectedItem.itemImage;           // icon에 선택한 아이템의 이미지를 가져옴
        gradeImage.sprite = selectedItem.gradeSprite;   // 강화 정보에 선택한 아이템의 강화 이미지를 가져옴
        itemName.text = selectedItem.itemName;          // 선택한 아이템 이름을 화면에 띄움
    }

    // 아이템 선택 취소 및 초기화 (UI 리셋)
    public void CancelSelection()
    {
        // 선택된 아이템 정보 초기화
        selectedItem = null;

        // 아이콘 / 강화 이미지 / 텍스트들 리셋
        icon.enabled = false;
        icon.sprite = null;

        gradeImage.enabled = false;
        gradeImage.sprite = null;

        itemName.text = "";
        upgradeInfo.text = "";
        crystalCount.text = "";
        reqCoin.text = "";

        // 강화 버튼 / 경고 문구 비활성화
        BtnOrErr(false, false);

        // 룰렛 / 결과창도 혹시 열려 있으면 닫기
        if (roulettePanel != null) roulettePanel.SetActive(false);
        if (resultPanel != null) resultPanel.SetActive(false);

        // 룰렛 상태 플래그 초기화
        isRotating = false;
        upgradeAttempted = false;
    }

    // 강화 단계 이미지 표시 여부
    private void UpdateGradeImageVisibility()
    {
        //이미 강화된 아이템이면, 강화 단계 이미지를 활성화, 강회되지 않았으면 강화 단계 이미지 비활성화
        gradeImage.enabled = (gradeImage.sprite != null);
    }


    // --- 비용 계산 / 조건 설정 --- //
    // 아이템 타입에 따라 강화 설정 분기
    private void UpdateRequirementsByType()
    {
        if (selectedItem.equipType == Item.EquipType.Weapon)
        {
            SetAccordingToType(1, 10);  // 무기: x = 1, 최대강화 10
        }
        else
        {
            SetAccordingToType(2, 3);   // 예: 반지: x = 2, 최대강화 3
        }
    }

    // 아이템 타입에 따른 필요 재료 및 UI 텍스트 설정
    private void SetAccordingToType(int x, int maxGrade)
    {
        requiredCoin = (selectedItem.grade + 1) * 400 * x;   // 필요한 재화 = (현재 강화 단계 + 1) * 400 * x
        requiredCrystal = (selectedItem.grade + 1) * x;      // 필요한 크리스탈 = 현재 강화 단계 + 1 * x

        if (selectedItem.grade < maxGrade)    // 최대 강화가 아닐 때 (최대강화 단계 = 10)
        {
            // 강화 정보 UI 업데이트
            upgradeInfo.text = $"{selectedItem.grade}  >  {selectedItem.grade + 1}";   
            crystalCount.text = $"{currentCrystalCount} / {requiredCrystal}";          
            reqCoin.text = $"필요 재화: {requiredCoin}";    
        }
        else   // 최대 강화
        {
            upgradeInfo.text = "최대 강화"; 
            crystalCount.text = "-";       
            reqCoin.text = "필요 재화: -"; 
        }
    }

    // 강화가 가능 여부에 따라 버튼/경고 상태 제어
    private void UpdateUpgradeState()
    {
        // 해당 아이템이 이미 최대 강화라면
        if (upgradeInfo.text == "최대 강화")
        {
            BtnOrErr(false, false);
            return;
        }

        bool hasEnoughCrystal = currentCrystalCount >= requiredCrystal;
        bool hasEnoughCoin = itemManager.coin >= requiredCoin;

        // 강화 가능
        if (hasEnoughCrystal && hasEnoughCoin) BtnOrErr(true, false);

        // 재료 부족
        else BtnOrErr(false, true);
    }

    // 강화버튼 / 경고 활성화 상태 관리
    public void BtnOrErr(bool btn, bool err)
    {
        upgradeBtn.SetActive(btn);
        warningText.SetActive(err);
    }


    /// --- 강화 시작 / 룰렛 --- //
    // 강화 클릭 시 호출
    public void UpgradeBtn()
    {
        // 재화, 크리스탈 차감
        itemManager.coin -= requiredCoin;
        itemManager.GetItem(CrystalItemId).count -= requiredCrystal;

        // 룰렛 창 활성화
        roulettePanel.SetActive(true);  

        // 룰렛 회전 시작
        upgradeAttempted = false;   // 강화의 추가 시도를 가능 (룰렛 창에서 멈춤 버튼을 안눌렀음)
        isRotating = true;          // 룰렛 변수를 true로 하여, 룰렛이 돌아가도록 설정
    }

    // 멈추기 버튼 클릭 시 호출
    public void StopBtn()
    {
        isRotating = false; // 룰렛을 멈춤
    }


    /// --- 강화 결과 처리 --- ///
    // 룰렛이 멈추었을 때, 해당 확률로 강화 시도
    public void AttemptUpgradeWithProbability(string probabilityText)
    {
        // 룰렛/결과 패널 잠깐 보여주기
        StartCoroutine(ShowTemp(roulettePanel, 1f));    // 1초 뒤에 upgradeSystemPanel을 비활성화 함
        StartCoroutine(ShowTemp(resultPanel, 1f));        // 결과 창인 resultPanel을 활성화하고, 0.5초 뒤에 비활성화 함

        // 이미 한 번 처리된 강화라면 중복 실행 방지
        if (upgradeAttempted) return;

        // 확률 문자열을 실수로 변환
        if (!float.TryParse(probabilityText, out float successProbability))
            return;

        // 0-100 사이의 확률 값을 0-1 사이로 변환함
        successProbability /= 100f;

        percentTxt.text = $"강화 확률 : {probabilityText}%";    // 강화 확률 텍스트에 '강화 확률 : 현재 강화 확률 %'로 초기화 

        bool isSuccess = Random.value <= successProbability;
        ApplyUpgradeResult(isSuccess);

        upgradeAttempted = true;    // 성공 또는 실패 후에 upgradeAttempted를 true로 설정하여 추가 시도 제한
    }

    // 성공/실패에 따른 결과 텍스트 및 강화 단계 반영
    private void ApplyUpgradeResult(bool success)
    {
        if (success)
        {
            int currentGrade = selectedItem.grade;
            int nextGrade = currentGrade + 1;

            selectedItem.grade = nextGrade;

            resultTxt.color = Color.yellow;
            resultTxt.text = $"강화 성공: {currentGrade} -> {nextGrade}";

            UpdateSelectedItemUI();
            UpdateRequirementsByType();
            UpdateUpgradeState();

            PlayResultEffect(true);
        }
        else
        {
            resultTxt.color = Color.red;
            resultTxt.text = "강화 실패";

            PlayResultEffect(false);
        }
    }

    // obj 오브젝트를 duration만큼만 활성화
    private IEnumerator ShowTemp(GameObject obj, float duration) 
    {
        obj.SetActive(true);                       
        yield return new WaitForSeconds(duration);  
        obj.SetActive(false);                    
    }

    // 성공/실패 공통 이펙트 재생
    private void PlayResultEffect(bool success)
    {
        // 파티클
        if (success && successEffect != null)
            successEffect.Play();
        else if (!success && failEffect != null)
            failEffect.Play();

        // 사운드
        if (audioSource != null)
        {
            AudioClip clip = success ? successClip : failClip;
            if (clip != null)
                audioSource.PlayOneShot(clip);
        }
    }
}
using TMPro;
using UnityEngine;

public class RouletteCollisionDetector : MonoBehaviour
{
    private UpgradeSlot upgrade;

    private void Start()
    {
        // UpgradeSlot 컴포넌트가 현재 GameObject에 없으면 부모에서 찾음
        upgrade = GetComponentInParent<UpgradeSlot>();
    }

    void OnTriggerStay(Collider other)
    {
        // upgrade가 null이 아니고, 부딪힌 객체의 태그가 Percent이고, isRotating이 false일 때만 충돌을 체크.
        if (!upgrade.isRotating && other.CompareTag("Percent") && !upgrade.upgradeAttempted)
        {
            Debug.Log("충돌!");
            TextMeshProUGUI probabilityText = other.GetComponentInChildren<TextMeshProUGUI>();   // 부딪힌 객체의 TextMeshProUGUI 컴포넌트를 찾아서 참조

            if (probabilityText != null)
            {
                Debug.Log("확률" + probabilityText.text);
                upgrade.AttemptUpgradeWithProbability(probabilityText.text);
            }
        }
    }
}
using UnityEngine;

public class Roulette : MonoBehaviour
{
    private UpgradeSlot upgrade;

    RectTransform rtf;
    private float rotationSpeed = 1000.0f;  // 룰렛이 돌아가는 속도

    private void Start()
    {
        upgrade = GetComponentInParent<UpgradeSlot>();
        rtf = GetComponent<RectTransform>();
    }

    private void Update()
    {
        if (upgrade.isRotating)
        {
            rtf.Rotate(0f, 0f, rotationSpeed);  // 룰렛의 rectTransform에서 rotation에서 z축만 돌림
        }   
    }
}

전체 흐름도