Files
ProjectMD/Assets/Scripts/MineableBlock.cs
2026-01-22 18:13:20 +09:00

251 lines
7.0 KiB
C#

using System.Collections;
using Unity.Netcode;
using UnityEngine;
/// <summary>
/// A block that can be mined by players.
/// Uses HealthComponent for health management and implements IDamageable.
/// </summary>
[RequireComponent(typeof(HealthComponent))]
public class MineableBlock : NetworkBehaviour, IDamageable
{
[Header("Drop Settings")]
[SerializeField] private ItemData dropItemData;
[SerializeField] private GameObject genericDropPrefab;
[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;
private MaterialPropertyBlock _propBlock;
private NetworkVariable<bool> isDiscovered = new NetworkVariable<bool>(false);
private MeshRenderer _renderer;
private HealthComponent _health;
#region IDamageable Implementation
public float CurrentHealth => _health != null ? _health.CurrentHealth : 0f;
public float MaxHealth => _health != null ? _health.MaxHealth : 0f;
public bool IsAlive => _health != null && _health.IsAlive;
public void TakeDamage(float amount)
{
TakeDamage(new DamageInfo(amount, DamageType.Mining));
}
public void TakeDamage(DamageInfo damageInfo)
{
if (_health != null)
{
_health.TakeDamage(damageInfo);
}
}
#endregion
void Awake()
{
_health = GetComponent<HealthComponent>();
_renderer = GetComponentInChildren<MeshRenderer>();
_propBlock = new MaterialPropertyBlock();
// Start hidden
if (_renderer != null) _renderer.enabled = false;
if (_renderer != null && _renderer.sharedMaterial != null)
{
_originalColor = _renderer.sharedMaterial.HasProperty("_BaseColor")
? _renderer.sharedMaterial.GetColor("_BaseColor")
: _renderer.sharedMaterial.GetColor("_Color");
}
// Find outline component
_outline = GetComponentInChildren<Outline>();
_originalPos = transform.localPosition;
if (_outline != null)
{
_outline.enabled = false;
}
// Subscribe to health events
if (_health != null)
{
_health.OnDamaged += HandleDamaged;
_health.OnDeath += HandleDeath;
}
}
public override void OnDestroy() // 1. override 키워드 추가
{
if (_health != null)
{
_health.OnDamaged -= HandleDamaged;
_health.OnDeath -= HandleDeath;
}
// 2. 부모 클래스 실행
base.OnDestroy();
}
public override void OnNetworkSpawn()
{
// Update visuals when discovered state syncs
UpdateVisuals(isDiscovered.Value);
isDiscovered.OnValueChanged += (oldVal, newVal) =>
{
if (newVal) UpdateState();
};
}
void Update()
{
// Check if block should be discovered based on player distance
if (!isDiscovered.Value && NetworkManager.Singleton != null &&
NetworkManager.Singleton.LocalClient != null &&
NetworkManager.Singleton.LocalClient.PlayerObject != null)
{
float dist = Vector3.Distance(transform.position,
NetworkManager.Singleton.LocalClient.PlayerObject.transform.position);
if (FogOfWarManager.Instance != null && dist < FogOfWarManager.Instance.revealRadius)
{
RequestRevealServerRpc();
}
}
// Update renderer visibility
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 || !_renderer.enabled) return;
bool isCurrentlyVisible = (Time.time - _lastVisibleTime) < VisibilityThreshold;
_renderer.GetPropertyBlock(_propBlock);
Color targetColor = isCurrentlyVisible ? _originalColor : _originalColor * darkIntensity;
_propBlock.SetColor("_BaseColor", targetColor);
_renderer.SetPropertyBlock(_propBlock);
}
/// <summary>
/// Reveal this block (called by server).
/// </summary>
public void RevealBlock()
{
if (IsServer && !isDiscovered.Value)
{
isDiscovered.Value = true;
}
}
/// <summary>
/// Update local visibility for fog of war.
/// </summary>
public void UpdateLocalVisibility()
{
_lastVisibleTime = Time.time;
}
private void HandleDamaged(DamageInfo info)
{
// Play hit effect on all clients
PlayHitEffectClientRpc();
}
private void HandleDeath()
{
if (IsServer)
{
DropItem();
GetComponent<NetworkObject>().Despawn();
}
}
/// <summary>
/// Set highlight state for targeting feedback.
/// </summary>
public void SetHighlight(bool isOn)
{
if (_outline != null)
{
_outline.enabled = isOn;
}
}
/// <summary>
/// Play hit visual effect on all clients.
/// </summary>
[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;
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<NetworkObject>();
netObj.Spawn();
if (dropObj.TryGetComponent<DroppedItem>(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;
}
/// <summary>
/// Get the health component for direct access if needed.
/// </summary>
public HealthComponent Health => _health;
}