using NUnit.Framework.Interfaces; using System.Collections; using Unity.Netcode; using UnityEngine; public class MineableBlock : NetworkBehaviour { [Header("Block Stats")] [SerializeField] private int maxHp = 100; // [동기화] 모든 플레이어가 동일한 블록 체력을 보게 함 private NetworkVariable _currentHp = new NetworkVariable(); [SerializeField] private ItemData dropItemData; [SerializeField] private GameObject genericDropPrefab; // 여기에 위에서 만든 'GenericDroppedItem' 프리팹을 넣으세요. [Header("Visuals")] private Outline _outline; private Vector3 _originalPos; [Header("Shake Settings")] [SerializeField] private float shakeDuration = 0.15f; // 흔들리는 시간 [SerializeField] private float shakeMagnitude = 0.1f; // 흔들리는 강도 private Coroutine _shakeCoroutine; private Color _originalColor; // 본래의 색상을 저장할 변수 private float _lastVisibleTime; private const float VisibilityThreshold = 0.25f; [Header("Fog Settings")] [Range(0f, 1f)] [SerializeField] private float darkIntensity = 0.2f; // 안개 속에서 얼마나 어두워질지 (0: 완전 검정, 1: 원본) private MaterialPropertyBlock _propBlock; private NetworkVariable isDiscovered = new NetworkVariable(false); private MeshRenderer _renderer; void Awake() { _renderer = GetComponentInChildren(); _propBlock = new MaterialPropertyBlock(); // 시작 시에는 보이지 않게 설정 if (_renderer != null) _renderer.enabled = false; _originalColor = _renderer.sharedMaterial.HasProperty("_BaseColor") ? _renderer.sharedMaterial.GetColor("_BaseColor") : _renderer.sharedMaterial.GetColor("_Color"); _renderer.enabled = false; // 해당 오브젝트 혹은 자식에게서 Outline 컴포넌트를 찾습니다. _outline = GetComponentInChildren(); _originalPos = transform.localPosition; // 로컬 위치 저장 if (_outline != null) { // 게임 시작 시 하이라이트는 꺼둡니다. _outline.enabled = false; } else { Debug.LogWarning($"{gameObject.name}: QuickOutline 에셋의 Outline 컴포넌트를 찾을 수 없습니다."); } } public override void OnNetworkSpawn() { if (IsServer) { _currentHp.Value = maxHp; } // 데이터가 동기화될 때 비주얼 업데이트 UpdateVisuals(isDiscovered.Value); isDiscovered.OnValueChanged += (oldVal, newVal) => { if (newVal) UpdateState(); }; } void Update() { // 1. 이미 발견된 블록인지는 서버 변수(isDiscovered)로 확인 // 2. 현재 내 위치가 안개에서 벗어났는지 확인 (매우 단순화된 로직) if (!isDiscovered.Value) { float dist = Vector3.Distance(transform.position, NetworkManager.Singleton.LocalClient.PlayerObject.transform.position); if (dist < FogOfWarManager.Instance.revealRadius) { // 서버에 "나 발견됐어!"라고 보고 RequestRevealServerRpc(); } } // 3. 비주얼 업데이트: 발견된 적이 있을 때만 렌더러를 켬 if (_renderer != null) { _renderer.enabled = isDiscovered.Value; } UpdateState(); } [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)] private void RequestRevealServerRpc() { isDiscovered.Value = true; } private void UpdateState() { if (_renderer == null) return; // 2. 현재 시야 안에 있는지 판단합니다. bool isCurrentlyVisible = (Time.time - _lastVisibleTime) < VisibilityThreshold; // 3. 상태에 따라 색상과 렌더러 상태를 결정합니다. if (_renderer.enabled == false) return; _renderer.GetPropertyBlock(_propBlock); // 2. 시야 내에 있으면 원본 색상(_originalColor), 멀어지면 어둡게 만든 색상을 적용합니다. Color targetColor = isCurrentlyVisible ? _originalColor : _originalColor * darkIntensity; _propBlock.SetColor("_BaseColor", targetColor); _renderer.SetPropertyBlock(_propBlock); } public void RevealBlock() // 서버에서 호출 { if (IsServer && !isDiscovered.Value) isDiscovered.Value = true; } // 플레이어가 주변을 훑을 때 호출해줄 함수 public void UpdateLocalVisibility() { _lastVisibleTime = Time.time; } // 서버에서만 대미지를 처리하도록 제한 [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)] public void TakeDamageRpc(int damageAmount) { if (_currentHp.Value <= 0) return; _currentHp.Value -= damageAmount; if (_currentHp.Value <= 0) { DestroyBlock(); } } private void DestroyBlock() { DropItem(); // 2. 서버에서 네트워크 오브젝트 제거 (모든 클라이언트에서 사라짐) GetComponent().Despawn(); } // 하이라이트 상태를 설정하는 공개 메서드 public void SetHighlight(bool isOn) { if (_outline == null) return; // 외곽선 컴포넌트 활성화/비활성화 _outline.enabled = isOn; } // 서버에서 호출하여 모든 클라이언트에게 흔들림 지시 [ClientRpc] public void PlayHitEffectClientRpc() { if (_shakeCoroutine != null) StopCoroutine(_shakeCoroutine); _shakeCoroutine = StartCoroutine(ShakeRoutine()); } private IEnumerator ShakeRoutine() { float elapsed = 0.0f; while (elapsed < shakeDuration) { Vector3 randomOffset = Random.insideUnitSphere * shakeMagnitude; transform.localPosition = _originalPos + randomOffset; // 좌표가 실제로 바뀌고 있는지 로그 출력 // Debug.Log($"현재 좌표: {transform.localPosition}"); elapsed += Time.deltaTime; yield return null; } transform.localPosition = _originalPos; } private void DropItem() { if (!IsServer || dropItemData == null || genericDropPrefab == null) return; // 원본 블록이 아니라 '범용 컨테이너'를 소환합니다. GameObject dropObj = Instantiate(genericDropPrefab, transform.position + Vector3.up * 0.5f, Quaternion.identity); NetworkObject netObj = dropObj.GetComponent(); netObj.Spawn(); // 소환된 컨테이너에 "너는 어떤 아이템의 모양을 따라해야 해"라고 알려줍니다. if (dropObj.TryGetComponent(out var droppedItem)) { droppedItem.Initialize(dropItemData.itemID); } } private void UpdateVisuals(bool discovered) { if (_renderer != null) _renderer.enabled = discovered; // 발견되지 않은 블록은 아웃라인도 표시되지 않아야 함 if (!discovered && _outline != null) _outline.enabled = false; } }