251 lines
7.0 KiB
C#
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;
|
|
}
|