using System; using System.Collections; using System.Collections.Generic; using System.Linq; using BlueWater.Items; using BlueWater.Players.Tycoons; using BlueWater.Tycoons; using DG.Tweening; using Sirenix.OdinInspector; using TMPro; using UnityEngine; using UnityEngine.Pool; using UnityEngine.Serialization; using UnityEngine.UI; namespace BlueWater { public class LiquidController : MonoBehaviour { #region Variables [Title("컴포넌트")] [SerializeField] private GameObject _liquidPanel; [SerializeField] private GameObject _shaker; [SerializeField] private Renderer _renderTexture; [SerializeField] private Renderer _liquidRenderer; [SerializeField] private Collider2D _reachedCollider; [SerializeField] private TMP_Text _amountText; [SerializeField] private Image _completeCocktailImage; [SerializeField] private TMP_Text _completeText; [Title("스폰 데이터")] [SerializeField, Required] private Transform _spawnTransform; [SerializeField] private Transform _spawnLocation; [SerializeField] private Vector3 _pushDirection; [SerializeField] private float _pushPower; [Title("Liquid / Garnish")] [SerializeField, Required, Tooltip("원액 프리팹")] private Liquid _liquidObject; [SerializeField, Required, Tooltip("가니쉬 프리팹")] private Garnish _garnishObject; [SerializeField, Tooltip("초당 생성되는 액체 수(ml)")] private int _liquidsPerSecond = 80; [SerializeField] private int _maxLiquidCount = 400; [SerializeField, Range(0f, 1f), Tooltip("목표 색상으로 변경되는데 걸리는 시간")] private float _colorLerpSpeed = 0.5f; [SerializeField, Range(1f, 5f), Tooltip("목표 색상 * 밝기")] private float _colorIntensity = 2f; [Title("오브젝트 풀링")] [SerializeField, Tooltip("오브젝트 풀링 최대 개수")] private int _objectPoolCount = 1000; [Title("패널")] [SerializeField] private float _moveDuration = 0.5f; [SerializeField] private Vector3 _endPosition = new(-300f, 0f, 0f); [SerializeField] private Vector3 _endPositionOffset = new(0f, 20f, 0f); [SerializeField] private Vector3 _endScale = new(0.3f, 0.3f, 0.3f); private Barrel _currentBarrel; private IObjectPool _liquidObjectPool; private IObjectPool _garnishObjectPool; private List _activeLiquidDatas = new(); private List _activeGarnishDatas = new(); private Dictionary _liquidDataCounts = new(7); private Material _instanceMaterial; private Tween _showTween; private Tween _hideTween; private bool _isShowingPanel; private bool _isPouring; private float _startTime = float.PositiveInfinity; private int _instanceLiquidCount; private float _currentLiquidAmount; private float _liquidReachedTime; private float _timeInterval; private Color _currentMixedColor = Color.black; private Color _targetColor; private Camera _overlayCamera; private TycoonPlayer _tycoonPlayer; private Vector3 _lastPlayerPosition; // Hashes private static readonly int LiquidAmountHash = Shader.PropertyToID("_LiquidAmount"); private static readonly int LiquidColorHash = Shader.PropertyToID("_LiquidColor"); #endregion // Unity events #region Unity events private void Awake() { _liquidObjectPool = new ObjectPool(CreateLiquidObject, OnGetLiquidObject, OnReleaseLiquidObject, OnDestroyLiquidObject, maxSize: _objectPoolCount); _garnishObjectPool = new ObjectPool(CreateGarnishObject, OnGetGarnishObject, OnReleaseGarnishObject, OnDestroyGarnishObject, maxSize: _objectPoolCount); _hideTween = _liquidPanel.transform.DOMoveX(_endPosition.x + 100f, _moveDuration).Pause() .SetAutoKill(false); _showTween = _liquidPanel.transform.DOMoveX(_endPosition.x, _moveDuration).Pause() .SetAutoKill(false); } private void Start() { EventManager.OnLiquidRegionEntered += ShowPanel; EventManager.OnLiquidRegionExited += HidePanel; EventManager.OnCocktailDiscarded += ReleaseAllObject; EventManager.OnPlaceOnServingTable += ReleaseAllObject; LiquidIngredient.OnReachedTarget += OnTargetReached; Barrel.OnBarrelInteracted += HandleBarrelInteraction; Barrel.OnBarrelCancelInteracted += HandleBarrelCancelInteraction; _overlayCamera = TycoonCameraManager.Instance.LiquidOverlayCamera; _tycoonPlayer = GameManager.Instance.CurrentTycoonPlayer; _instanceMaterial = Instantiate(_liquidRenderer.material); _liquidRenderer.material = _instanceMaterial; _instanceMaterial.SetFloat(LiquidAmountHash, 0f); _timeInterval = 1f / _liquidsPerSecond; _shaker.SetActive(true); _amountText.enabled = true; _completeCocktailImage.enabled = false; _completeText.enabled = false; _instanceLiquidCount = 0; SetCurrentAmount(0f); } private void Update() { if (_isPouring) { var currentBarrel = _currentBarrel; // 술이 완성되었을 때 if (_instanceLiquidCount >= _maxLiquidCount) { StartCoroutine(nameof(CompleteCocktail)); return; } if (Time.time - _startTime >= _timeInterval) { switch (currentBarrel.GetLiquidData().Type) { case LiquidType.None: Debug.LogError("원액 종류 None 오류"); break; case LiquidType.Liquid: _liquidObjectPool.Get(); break; case LiquidType.Garnish: _garnishObjectPool.Get(); break; default: throw new ArgumentOutOfRangeException(); } if (!_liquidDataCounts.TryAdd(currentBarrel.GetLiquidData(), 1)) { _liquidDataCounts[currentBarrel.GetLiquidData()] += 1; } currentBarrel.Consume(1); _startTime = Time.time; } } else { if (_instanceLiquidCount <= 0) return; var playerPosition = _tycoonPlayer.transform.position; var playerScreenPosition = TycoonCameraManager.Instance.MainCamera.WorldToScreenPoint(playerPosition); var panelWorldPosition = _overlayCamera.ScreenToWorldPoint(new Vector3(playerScreenPosition.x, playerScreenPosition.y, _overlayCamera.nearClipPlane)); panelWorldPosition += _endPositionOffset; if (_activeLiquidDatas.Count == 0 && _activeGarnishDatas.Count == 0) { _liquidPanel.transform.DOScale(_endScale, _moveDuration).SetEase(Ease.Linear); _liquidPanel.transform.DOMove(panelWorldPosition, _moveDuration).SetEase(Ease.Linear); } } if (_liquidReachedTime + _colorLerpSpeed >= Time.time) { _currentMixedColor = Color.Lerp(_currentMixedColor, _targetColor, _colorLerpSpeed * Time.deltaTime); _instanceMaterial.SetColor(LiquidColorHash, _currentMixedColor * _colorIntensity); } } private void OnDestroy() { EventManager.OnLiquidRegionEntered -= ShowPanel; EventManager.OnLiquidRegionExited -= HidePanel; EventManager.OnCocktailDiscarded -= ReleaseAllObject; EventManager.OnPlaceOnServingTable -= ReleaseAllObject; LiquidIngredient.OnReachedTarget -= OnTargetReached; Barrel.OnBarrelInteracted -= HandleBarrelInteraction; Barrel.OnBarrelCancelInteracted -= HandleBarrelCancelInteraction; } #endregion // Object pooling system #region Object pooling system // 원액 오브젝트 풀 private Liquid CreateLiquidObject() { var instance = Instantiate(_liquidObject, _spawnTransform.position, Quaternion.identity, _spawnLocation); instance.SetManagedPool(_liquidObjectPool); return instance; } private void OnGetLiquidObject(Liquid liquid) { _instanceLiquidCount++; var liquidColor = _currentBarrel.GetLiquidData().Color; liquid.Initialize(_spawnTransform.position, Quaternion.identity, _reachedCollider, _pushDirection.normalized * _pushPower, liquidColor); _activeLiquidDatas.Add(liquid); } private void OnReleaseLiquidObject(Liquid liquid) { liquid.gameObject.SetActive(false); _activeLiquidDatas.Remove(liquid); } private void OnDestroyLiquidObject(Liquid liquid) { Destroy(liquid.gameObject); _activeLiquidDatas.Remove(liquid); } // 가니쉬 오브젝트 풀 private Garnish CreateGarnishObject() { var instance = Instantiate(_garnishObject, _spawnTransform.position, Quaternion.identity, _spawnLocation); instance.SetManagedPool(_garnishObjectPool); return instance; } private void OnGetGarnishObject(Garnish garnish) { _instanceLiquidCount++; var liquidSprite = _currentBarrel.GetLiquidData().Sprite; garnish.Initialize(_spawnTransform.position, Quaternion.identity, _reachedCollider, _pushDirection.normalized * _pushPower, liquidSprite); _activeGarnishDatas.Add(garnish); } private void OnReleaseGarnishObject(Garnish garnish) { garnish.gameObject.SetActive(false); _activeGarnishDatas.Remove(garnish); } private void OnDestroyGarnishObject(Garnish garnish) { Destroy(garnish.gameObject); _activeGarnishDatas.Remove(garnish); } #endregion // Custom methods #region Custom methods /// /// 술 제조 과정 초기화 함수 /// public void ReleaseAllObject() { // 리스트 삭제는 뒤에서부터 해야 오류가 없음 for (var i = _activeLiquidDatas.Count - 1; i >= 0; i--) { _activeLiquidDatas[i].Destroy(); } _liquidDataCounts.Clear(); _instanceLiquidCount = 0; _instanceMaterial.SetFloat(LiquidAmountHash, 0f); SetCurrentAmount(0f); HidePanel(); } public void HandleBarrelInteraction(Barrel barrel) { _currentBarrel = barrel; if (_instanceLiquidCount == 0) { ShowPanelFast(); _shaker.SetActive(true); _amountText.enabled = true; _completeCocktailImage.enabled = false; _completeText.enabled = false; _currentMixedColor = barrel.GetLiquidData().Color; _instanceMaterial.SetColor(LiquidColorHash, _currentMixedColor * _colorIntensity); EventManager.OnCocktailStarted?.Invoke(); } _startTime = Time.time; _isPouring = true; } public void HandleBarrelCancelInteraction() { _isPouring = false; } // public void ActiveIsPouring(Barrel barrel) // { // _currentBarrel = barrel; // if (_instanceLiquidCount == 0) // { // ShowPanelFast(); // _shaker.SetActive(true); // _amountText.enabled = true; // _completeCocktailImage.enabled = false; // _completeText.enabled = false; // _currentMixedColor = _currentBarrel.GetLiquidData().Color; // _instanceMaterial.SetColor(LiquidColorHash, _currentMixedColor * _colorIntensity); // EventManager.OnCocktailStarted?.Invoke(); // } // // _startTime = Time.time; // _isPouring = true; // } // public void InActiveIsPouring() // { // _isPouring = false; // } private void SetCurrentAmount(float value) { _currentLiquidAmount = value; if (_amountText) { var percent = (int)(_currentLiquidAmount / _maxLiquidCount * 100); _amountText.text = $"{percent}%"; } else { if (_amountText.enabled) { _amountText.enabled = false; } } } /// /// 술을 완성한 경우 /// private IEnumerator CompleteCocktail() { HandleBarrelCancelInteraction(); yield return new WaitUntil(() => _currentLiquidAmount >= _maxLiquidCount); var currentCocktailIngredients = new List(7); foreach (var element in _liquidDataCounts) { var idx = element.Key.Idx; var amount = element.Value; currentCocktailIngredients.Add(new CocktailIngredient(idx, amount)); } // ItemManager를 통해 모든 CocktailData 가져오기 var cocktailDatas = ItemManager.Instance.CocktailDataSo.GetData(); CocktailData matchingCocktail = null; // 모든 칵테일 데이터를 순회하면서 조건에 맞는 칵테일 찾기 foreach (var cocktailData in cocktailDatas.Values) { var validIngredients = cocktailData.GetValidIngredients(); // 조건 1: 재료 개수 동일 체크 if (validIngredients.Count != currentCocktailIngredients.Count) continue; var allIngredientsMatch = true; // 현재 음료 재료를 하나씩 validIngredients와 비교 foreach (var currentIngredient in currentCocktailIngredients) { // 동일한 Idx를 가진 재료가 있는지 찾음 var matchingValidIngredient = validIngredients.FirstOrDefault(ingredient => ingredient.Idx == currentIngredient.Idx); // 만약 Idx가 일치하는 재료가 없으면 조건 불충족 if (matchingValidIngredient == null) { allIngredientsMatch = false; break; } // 조건 2: Amount 값이 RatioRange에 따른 오차 범위 내에 있는지 체크 var validAmount = matchingValidIngredient.Amount; //var maxLiquidCount = cocktailData.GetCocktailAmount(validIngredients); var range = _maxLiquidCount / 100 * cocktailData.RatioRange; var minAmount = validAmount - range; var maxAmount = validAmount + range; if (currentIngredient.Amount < minAmount || currentIngredient.Amount > maxAmount) { allIngredientsMatch = false; break; } } // 조건이 모두 만족하면 매칭되는 칵테일을 찾음 if (allIngredientsMatch) { matchingCocktail = cocktailData; break; } } // 조건에 만족하는 칵테일이 없음 if (matchingCocktail == null) { matchingCocktail = ItemManager.Instance.CocktailDataSo.GetDataByIdx("Cocktail000"); _completeText.text = "실패"; } else { _completeText.text = $"성공!\n{matchingCocktail.Name}"; } // TODO : 음료 제조 성공, 실패 연출 추가 _shaker.SetActive(false); _amountText.enabled = false; _completeCocktailImage.sprite = matchingCocktail.Sprite; _completeCocktailImage.enabled = true; _completeText.enabled = true; // 1. 플레이어 음료 들기 EventManager.OnCocktailCompleted?.Invoke(matchingCocktail); yield return new WaitForSeconds(1f); HidePanel(); } /// /// 사용된 색상의 비율에 맞게 색을 혼합시키는 함수 /// private Color MixColorsByTime() { var totalCounts = _liquidDataCounts.Values.Sum(); var mixedColor = Color.black; foreach (var element in _liquidDataCounts) { var color = element.Key.Color; var count = element.Value; var ratio = count / (float)totalCounts; mixedColor += color * ratio; } mixedColor.a = 1f; return mixedColor; } /// /// 액체가 특정 오브젝트에 충돌했을 때, 실행해야하는 과정 /// public void OnTargetReached() { _liquidReachedTime = Time.time; SetCurrentAmount(++_currentLiquidAmount); var liquidAmount = Mathf.Clamp(_currentLiquidAmount / _maxLiquidCount, 0f, 1f); _instanceMaterial.SetFloat(LiquidAmountHash, liquidAmount); _targetColor = MixColorsByTime(); if (liquidAmount >= 1f) { HandleBarrelCancelInteraction(); } } public void ShowPanelFast() { if (_isShowingPanel) return; _liquidPanel.transform.localScale = Vector3.one; _liquidPanel.transform.position = _endPosition; _liquidPanel.SetActive(true); _isShowingPanel = true; _hideTween.Pause(); _showTween.Pause(); } public void ShowPanel() { if (_isShowingPanel || _instanceLiquidCount <= 0) return; _isShowingPanel = true; _hideTween.Pause(); _showTween.Restart(); } public void HidePanel() { if (!_isShowingPanel) return; _isShowingPanel = false; _showTween.Pause(); _hideTween.Restart(); } #endregion } }