using System.Collections;
using Unity.Netcode;
using UnityEngine;
///
/// NetworkBehaviour for a mineable chunk containing 4x4x4 blocks.
/// Manages block state, mesh generation, damage, and item drops.
///
[RequireComponent(typeof(MeshFilter))]
[RequireComponent(typeof(MeshRenderer))]
[RequireComponent(typeof(MeshCollider))]
public class MineableChunk : NetworkBehaviour, ChunkMeshBuilder.INeighborProvider
{
[Header("Block Settings")]
[SerializeField] private byte normalBlockHealth = 100;
[SerializeField] private byte resourceBlockHealth = 150;
[Header("Drop Settings")]
[SerializeField] private ItemData normalDropItem;
[SerializeField] private ItemData resourceDropItem;
[SerializeField] private GameObject genericDropPrefab;
[Header("Materials")]
[SerializeField] private Material normalBlockMaterial;
[SerializeField] private Material resourceBlockMaterial;
[Header("Visual Settings")]
[SerializeField] private float meshRebuildDelay = 0.1f;
[Header("Highlight Settings")]
[SerializeField] private Material highlightMaterial;
private GameObject _highlightCube;
private MeshRenderer _highlightRenderer;
[Header("Fog of War")]
[SerializeField] private float visibilityTimeout = 0.5f; // Time before block goes from "visible" to "discovered"
[SerializeField] private Color discoveredTint = new Color(0.3f, 0.3f, 0.3f, 1f); // Darkened color for discovered-but-not-visible
private float[] _blockVisibilityTime; // Last time each block was visible (local only)
private MaterialPropertyBlock _propBlock;
// Chunk state synced to all clients
private NetworkVariable _networkState = new NetworkVariable(
default,
NetworkVariableReadPermission.Everyone,
NetworkVariableWritePermission.Server
);
// Local cache for quick access
private ChunkState _localState;
private ChunkCoord _chunkCoord;
private bool _isInitialized = false;
// Components
private MeshFilter _meshFilter;
private MeshRenderer _meshRenderer;
private MeshCollider _meshCollider;
private Outline _outline;
// Mesh rebuild state
private bool _meshDirty = false;
private Coroutine _rebuildCoroutine;
// Highlight state
private int _highlightedBlockIndex = -1;
// Reference to chunk manager for neighbor queries
private static ChunkedUndergroundGenerator _chunkManager;
public ChunkCoord Coord => _chunkCoord;
public ChunkState State => _localState;
// Visibility check interval
private float _lastVisibilityCheck;
private const float VISIBILITY_CHECK_INTERVAL = 0.3f;
private bool _hasVisibleBlocks = false;
private void Awake()
{
_meshFilter = GetComponent();
_meshRenderer = GetComponent();
_meshCollider = GetComponent();
_outline = GetComponent();
// Initialize local state
_localState = ChunkState.CreateEmpty();
// Initialize visibility tracking (local only, not networked)
_blockVisibilityTime = new float[ChunkState.BLOCKS_PER_CHUNK];
_propBlock = new MaterialPropertyBlock();
// Set materials: 0=normal-visible, 1=resource-visible, 2=normal-dark, 3=resource-dark
SetupMaterials();
// Create highlight cube for per-block highlighting
CreateHighlightCube();
}
private void SetupMaterials()
{
if (normalBlockMaterial != null && resourceBlockMaterial != null)
{
// Create darkened versions of materials for discovered-but-not-visible state
Material normalDark = new Material(normalBlockMaterial);
ApplyDarkTint(normalDark, normalBlockMaterial);
Material resourceDark = new Material(resourceBlockMaterial);
ApplyDarkTint(resourceDark, resourceBlockMaterial);
_meshRenderer.materials = new Material[]
{
normalBlockMaterial, // Submesh 0: Normal blocks (visible)
resourceBlockMaterial, // Submesh 1: Resource blocks (visible)
normalDark, // Submesh 2: Normal blocks (discovered/dark)
resourceDark // Submesh 3: Resource blocks (discovered/dark)
};
}
}
private void ApplyDarkTint(Material darkMat, Material sourceMat)
{
// Try URP _BaseColor first, then fallback to _Color
if (sourceMat.HasProperty("_BaseColor"))
{
Color baseColor = sourceMat.GetColor("_BaseColor");
darkMat.SetColor("_BaseColor", baseColor * discoveredTint);
}
else if (sourceMat.HasProperty("_Color"))
{
Color baseColor = sourceMat.GetColor("_Color");
darkMat.SetColor("_Color", baseColor * discoveredTint);
}
}
private void CreateHighlightCube()
{
_highlightCube = GameObject.CreatePrimitive(PrimitiveType.Cube);
_highlightCube.name = "BlockHighlight";
_highlightCube.transform.SetParent(transform);
_highlightCube.transform.localScale = Vector3.one * 1.02f; // Slightly larger than block
// Remove collider - this is visual only
var collider = _highlightCube.GetComponent();
if (collider != null) Destroy(collider);
// Setup renderer
_highlightRenderer = _highlightCube.GetComponent();
if (highlightMaterial != null)
{
_highlightRenderer.material = highlightMaterial;
}
else
{
// Create default highlight material if none assigned
var mat = new Material(Shader.Find("Standard"));
mat.color = new Color(1f, 1f, 0f, 0.3f); // Yellow transparent
mat.SetFloat("_Mode", 3); // Transparent mode
mat.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
mat.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
mat.SetInt("_ZWrite", 0);
mat.DisableKeyword("_ALPHATEST_ON");
mat.EnableKeyword("_ALPHABLEND_ON");
mat.DisableKeyword("_ALPHAPREMULTIPLY_ON");
mat.renderQueue = 3000;
_highlightRenderer.material = mat;
}
_highlightCube.SetActive(false);
}
public override void OnNetworkSpawn()
{
// Calculate chunk coordinates from world position
_chunkCoord = ChunkCoord.FromWorldPos(transform.position);
// Subscribe to state changes
_networkState.OnValueChanged += OnChunkStateChanged;
// Initial sync
SyncLocalState();
RebuildMeshImmediate();
// Find chunk manager if not set
if (_chunkManager == null)
{
_chunkManager = FindFirstObjectByType();
}
}
private void Update()
{
// Only check visibility timeout periodically and if we had visible blocks
if (!_hasVisibleBlocks) return;
if (Time.time - _lastVisibilityCheck < VISIBILITY_CHECK_INTERVAL) return;
_lastVisibilityCheck = Time.time;
CheckVisibilityTimeout();
}
private void CheckVisibilityTimeout()
{
bool anyStillVisible = false;
bool needsRebuild = false;
for (int i = 0; i < ChunkState.BLOCKS_PER_CHUNK; i++)
{
if (_blockVisibilityTime[i] <= 0) continue;
bool wasVisible = (Time.time - _blockVisibilityTime[i]) < visibilityTimeout;
if (wasVisible)
{
anyStillVisible = true;
}
else if (_blockVisibilityTime[i] > 0)
{
// Block just transitioned from visible to discovered
// Check if it was visible last frame (within timeout + check interval)
float timeSinceVisible = Time.time - _blockVisibilityTime[i];
if (timeSinceVisible < visibilityTimeout + VISIBILITY_CHECK_INTERVAL * 2)
{
needsRebuild = true;
}
}
}
_hasVisibleBlocks = anyStillVisible;
if (needsRebuild)
{
ScheduleMeshRebuild();
}
}
public override void OnNetworkDespawn()
{
_networkState.OnValueChanged -= OnChunkStateChanged;
}
///
/// Initialize chunk with generated block data (called by generator on server)
///
public void InitializeBlocks(ChunkState initialState)
{
if (!IsServer) return;
_networkState.Value = initialState;
SyncLocalState();
RebuildMeshImmediate();
}
///
/// Get block data at local coordinates
///
public BlockData GetBlock(int x, int y, int z)
{
if (!ChunkCoord.IsValidLocal(x, y, z)) return BlockData.Empty;
if (_localState.blocks == null) return BlockData.Empty;
int index = ChunkCoord.LocalToIndex(x, y, z);
return _localState.blocks[index];
}
///
/// Get block data at local index
///
public BlockData GetBlock(int index)
{
if (index < 0 || index >= ChunkState.BLOCKS_PER_CHUNK) return BlockData.Empty;
if (_localState.blocks == null) return BlockData.Empty;
return _localState.blocks[index];
}
#region Damage System
///
/// Request damage to a specific block (called by clients)
///
[Rpc(SendTo.Server)]
public void DamageBlockServerRpc(int localIndex, byte damage)
{
if (localIndex < 0 || localIndex >= ChunkState.BLOCKS_PER_CHUNK) return;
var state = _networkState.Value;
BlockData block = state.blocks[localIndex];
if (block.IsEmpty) return;
// Apply damage
int newHealth = Mathf.Max(0, block.health - damage);
block.health = (byte)newHealth;
// Update state
state.blocks[localIndex] = block;
_networkState.Value = state;
// Sync to clients
UpdateBlockClientRpc(localIndex, block);
// Play hit effect
PlayHitEffectClientRpc(localIndex);
// Check for destruction
if (newHealth <= 0)
{
DestroyBlock(localIndex, state.blocks[localIndex].blockType);
}
}
///
/// Server-side damage application (for direct server calls)
///
public void DamageBlock(int localIndex, float damageAmount)
{
if (!IsServer) return;
DamageBlockServerRpc(localIndex, (byte)Mathf.Min(255, damageAmount));
}
///
/// Update a single block on all clients
///
[ClientRpc]
private void UpdateBlockClientRpc(int localIndex, BlockData block)
{
if (localIndex < 0 || localIndex >= ChunkState.BLOCKS_PER_CHUNK) return;
_localState.blocks[localIndex] = block;
// Schedule mesh rebuild
ScheduleMeshRebuild();
}
///
/// Play hit visual effect
///
[ClientRpc]
private void PlayHitEffectClientRpc(int localIndex)
{
// Get block world position for effect
Vector3Int localPos = ChunkCoord.IndexToLocal(localIndex);
Vector3 worldPos = _chunkCoord.LocalToWorld(localPos);
// TODO: Spawn particle effect at worldPos
// For now, just a debug visualization
}
///
/// Handle block destruction (server-side)
///
private void DestroyBlock(int localIndex, byte blockType)
{
if (!IsServer) return;
// Spawn dropped item
Vector3Int localPos = ChunkCoord.IndexToLocal(localIndex);
Vector3 worldPos = _chunkCoord.LocalToWorld(localPos);
SpawnDrop(worldPos, blockType);
// Clear block
var state = _networkState.Value;
state.blocks[localIndex] = BlockData.Empty;
_networkState.Value = state;
// Full sync and rebuild
SyncStateClientRpc(state);
}
///
/// Spawn item drop at position
///
private void SpawnDrop(Vector3 position, byte blockType)
{
if (genericDropPrefab == null) return;
ItemData dropItem = blockType == BlockData.TYPE_RESOURCE ? resourceDropItem : normalDropItem;
if (dropItem == null) return;
GameObject dropObj = Instantiate(genericDropPrefab, position + Vector3.up * 0.5f, Quaternion.identity);
NetworkObject netObj = dropObj.GetComponent();
netObj.Spawn();
if (dropObj.TryGetComponent(out var droppedItem))
{
droppedItem.Initialize(dropItem.itemID);
}
}
#endregion
#region Fog of War
///
/// Check if block is currently visible (in player's sight right now)
///
public bool IsBlockCurrentlyVisible(int localIndex)
{
if (_blockVisibilityTime == null || localIndex < 0 || localIndex >= _blockVisibilityTime.Length)
return false;
return (Time.time - _blockVisibilityTime[localIndex]) < visibilityTimeout;
}
///
/// Update local visibility for blocks in range (called by local player)
///
public void UpdateLocalVisibility(Vector3 playerPos, float radius)
{
bool needsRebuild = false;
bool anyVisible = false;
for (int i = 0; i < ChunkState.BLOCKS_PER_CHUNK; i++)
{
BlockData block = _localState.blocks[i];
if (block.IsEmpty || !block.IsDiscovered) continue;
Vector3 blockWorldPos = GetBlockWorldPosition(i);
bool wasVisible = IsBlockCurrentlyVisible(i);
if (Vector3.Distance(playerPos, blockWorldPos) <= radius)
{
_blockVisibilityTime[i] = Time.time;
anyVisible = true;
// If block just became visible, need to rebuild mesh
if (!wasVisible)
{
needsRebuild = true;
}
}
}
// Track if we have any visible blocks (for timeout checking in Update)
_hasVisibleBlocks = anyVisible || _hasVisibleBlocks;
if (needsRebuild)
{
ScheduleMeshRebuild();
}
}
///
/// Request reveal of a specific block (called by clients)
///
[Rpc(SendTo.Server)]
public void RevealBlockServerRpc(int localIndex)
{
if (localIndex < 0 || localIndex >= ChunkState.BLOCKS_PER_CHUNK) return;
var state = _networkState.Value;
BlockData block = state.blocks[localIndex];
if (block.IsEmpty || block.IsDiscovered) return;
// Mark as discovered
block.IsDiscovered = true;
state.blocks[localIndex] = block;
_networkState.Value = state;
// Sync to clients
UpdateBlockClientRpc(localIndex, block);
}
///
/// Reveal all blocks within radius of a world position
///
public void RevealBlocksInRadius(Vector3 worldPos, float radius)
{
if (!IsServer) return;
bool anyRevealed = false;
var state = _networkState.Value;
for (int i = 0; i < ChunkState.BLOCKS_PER_CHUNK; i++)
{
BlockData block = state.blocks[i];
if (block.IsEmpty || block.IsDiscovered) continue;
Vector3Int localPos = ChunkCoord.IndexToLocal(i);
Vector3 blockWorldPos = _chunkCoord.LocalToWorld(localPos);
if (Vector3.Distance(worldPos, blockWorldPos) <= radius)
{
block.IsDiscovered = true;
state.blocks[i] = block;
anyRevealed = true;
}
}
if (anyRevealed)
{
_networkState.Value = state;
SyncStateClientRpc(state);
}
}
///
/// Check if block at local index is discovered
///
public bool IsBlockDiscovered(int localIndex)
{
if (localIndex < 0 || localIndex >= ChunkState.BLOCKS_PER_CHUNK) return false;
return _localState.blocks[localIndex].IsDiscovered;
}
#endregion
#region Mesh Building
///
/// Full state sync from server
///
[ClientRpc]
private void SyncStateClientRpc(ChunkState state)
{
_localState = state;
RebuildMeshImmediate();
}
private void OnChunkStateChanged(ChunkState oldState, ChunkState newState)
{
SyncLocalState();
ScheduleMeshRebuild();
}
private void SyncLocalState()
{
_localState = _networkState.Value;
// Ensure blocks array is initialized
if (_localState.blocks == null)
{
_localState = ChunkState.CreateEmpty();
}
}
private void ScheduleMeshRebuild()
{
if (_meshDirty) return;
_meshDirty = true;
if (_rebuildCoroutine != null)
{
StopCoroutine(_rebuildCoroutine);
}
_rebuildCoroutine = StartCoroutine(DelayedMeshRebuild());
}
private IEnumerator DelayedMeshRebuild()
{
yield return new WaitForSeconds(meshRebuildDelay);
RebuildMeshImmediate();
_meshDirty = false;
_rebuildCoroutine = null;
}
private void RebuildMeshImmediate()
{
if (_meshFilter == null) return;
// Build visual mesh with fog of war (4 submeshes: normal-visible, resource-visible, normal-dark, resource-dark)
Mesh visualMesh = ChunkMeshBuilder.BuildMeshWithVisibility(
_localState,
_chunkCoord,
this,
true, // only discovered
IsBlockCurrentlyVisible // visibility checker
);
_meshFilter.mesh = visualMesh;
// Build collision mesh (all blocks, including undiscovered, no visibility distinction)
if (_meshCollider != null)
{
Mesh collisionMesh = ChunkMeshBuilder.BuildMesh(_localState, _chunkCoord, this, false);
_meshCollider.sharedMesh = null; // Force refresh
_meshCollider.sharedMesh = collisionMesh;
}
}
#endregion
#region INeighborProvider Implementation
///
/// Check if block at world grid position is solid (for cross-chunk face culling)
///
public bool IsBlockSolid(Vector3Int gridPos)
{
// Check if position is in this chunk
ChunkCoord coord = ChunkCoord.FromGridPos(gridPos);
if (coord == _chunkCoord)
{
Vector3Int local = ChunkCoord.GridToLocal(gridPos);
int index = ChunkCoord.LocalToIndex(local);
var block = _localState.blocks[index];
return !block.IsEmpty && block.IsDiscovered;
}
// Query chunk manager for other chunks
if (_chunkManager != null)
{
return _chunkManager.IsBlockSolid(gridPos);
}
// Default: assume not solid at chunk boundaries
return false;
}
#endregion
#region Interaction Support
///
/// Convert world hit point to local block index
///
public int WorldPointToBlockIndex(Vector3 worldPoint)
{
// Convert world point to local space relative to chunk transform
Vector3 localPoint = transform.InverseTransformPoint(worldPoint);
// Each block is 1 unit, centered at integer coordinates
// So block at (0,0,0) spans from -0.5 to 0.5
int lx = Mathf.Clamp(Mathf.RoundToInt(localPoint.x), 0, ChunkCoord.CHUNK_SIZE - 1);
int ly = Mathf.Clamp(Mathf.RoundToInt(localPoint.y), 0, ChunkCoord.CHUNK_SIZE - 1);
int lz = Mathf.Clamp(Mathf.RoundToInt(localPoint.z), 0, ChunkCoord.CHUNK_SIZE - 1);
return ChunkCoord.LocalToIndex(lx, ly, lz);
}
///
/// Get world position of block at local index
///
public Vector3 GetBlockWorldPosition(int localIndex)
{
Vector3Int localPos = ChunkCoord.IndexToLocal(localIndex);
// Convert local position to world using chunk transform
return transform.TransformPoint(new Vector3(localPos.x, localPos.y, localPos.z));
}
///
/// Set highlight state for visual feedback
///
public void SetHighlight(bool isOn, int blockIndex = -1)
{
_highlightedBlockIndex = isOn ? blockIndex : -1;
// Use per-block highlight cube instead of outline
if (_highlightCube != null)
{
if (isOn && blockIndex >= 0)
{
// Position highlight at specific block
Vector3Int localPos = ChunkCoord.IndexToLocal(blockIndex);
_highlightCube.transform.localPosition = new Vector3(localPos.x, localPos.y, localPos.z);
_highlightCube.SetActive(true);
}
else
{
_highlightCube.SetActive(false);
}
}
// Disable outline component if present (we use highlight cube instead)
if (_outline != null)
{
_outline.enabled = false;
}
}
///
/// Get the currently highlighted block index
///
public int HighlightedBlockIndex => _highlightedBlockIndex;
#endregion
#region Static Registration
public static void SetChunkManager(ChunkedUndergroundGenerator manager)
{
_chunkManager = manager;
}
#endregion
}