681 lines
21 KiB
C#
681 lines
21 KiB
C#
using System.Collections;
|
|
using Unity.Netcode;
|
|
using UnityEngine;
|
|
|
|
/// <summary>
|
|
/// NetworkBehaviour for a mineable chunk containing 4x4x4 blocks.
|
|
/// Manages block state, mesh generation, damage, and item drops.
|
|
/// </summary>
|
|
[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<ChunkState> _networkState = new NetworkVariable<ChunkState>(
|
|
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<MeshFilter>();
|
|
_meshRenderer = GetComponent<MeshRenderer>();
|
|
_meshCollider = GetComponent<MeshCollider>();
|
|
_outline = GetComponent<Outline>();
|
|
|
|
// 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<Collider>();
|
|
if (collider != null) Destroy(collider);
|
|
|
|
// Setup renderer
|
|
_highlightRenderer = _highlightCube.GetComponent<MeshRenderer>();
|
|
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<ChunkedUndergroundGenerator>();
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initialize chunk with generated block data (called by generator on server)
|
|
/// </summary>
|
|
public void InitializeBlocks(ChunkState initialState)
|
|
{
|
|
if (!IsServer) return;
|
|
|
|
_networkState.Value = initialState;
|
|
SyncLocalState();
|
|
RebuildMeshImmediate();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get block data at local coordinates
|
|
/// </summary>
|
|
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];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get block data at local index
|
|
/// </summary>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Request damage to a specific block (called by clients)
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Server-side damage application (for direct server calls)
|
|
/// </summary>
|
|
public void DamageBlock(int localIndex, float damageAmount)
|
|
{
|
|
if (!IsServer) return;
|
|
DamageBlockServerRpc(localIndex, (byte)Mathf.Min(255, damageAmount));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update a single block on all clients
|
|
/// </summary>
|
|
[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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Play hit visual effect
|
|
/// </summary>
|
|
[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
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handle block destruction (server-side)
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Spawn item drop at position
|
|
/// </summary>
|
|
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<NetworkObject>();
|
|
netObj.Spawn();
|
|
|
|
if (dropObj.TryGetComponent<DroppedItem>(out var droppedItem))
|
|
{
|
|
droppedItem.Initialize(dropItem.itemID);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Fog of War
|
|
|
|
/// <summary>
|
|
/// Check if block is currently visible (in player's sight right now)
|
|
/// </summary>
|
|
public bool IsBlockCurrentlyVisible(int localIndex)
|
|
{
|
|
if (_blockVisibilityTime == null || localIndex < 0 || localIndex >= _blockVisibilityTime.Length)
|
|
return false;
|
|
return (Time.time - _blockVisibilityTime[localIndex]) < visibilityTimeout;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update local visibility for blocks in range (called by local player)
|
|
/// </summary>
|
|
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();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Request reveal of a specific block (called by clients)
|
|
/// </summary>
|
|
[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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reveal all blocks within radius of a world position
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check if block at local index is discovered
|
|
/// </summary>
|
|
public bool IsBlockDiscovered(int localIndex)
|
|
{
|
|
if (localIndex < 0 || localIndex >= ChunkState.BLOCKS_PER_CHUNK) return false;
|
|
return _localState.blocks[localIndex].IsDiscovered;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Mesh Building
|
|
|
|
/// <summary>
|
|
/// Full state sync from server
|
|
/// </summary>
|
|
[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
|
|
|
|
/// <summary>
|
|
/// Check if block at world grid position is solid (for cross-chunk face culling)
|
|
/// </summary>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Convert world hit point to local block index
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get world position of block at local index
|
|
/// </summary>
|
|
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));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set highlight state for visual feedback
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the currently highlighted block index
|
|
/// </summary>
|
|
public int HighlightedBlockIndex => _highlightedBlockIndex;
|
|
|
|
#endregion
|
|
|
|
#region Static Registration
|
|
|
|
public static void SetChunkManager(ChunkedUndergroundGenerator manager)
|
|
{
|
|
_chunkManager = manager;
|
|
}
|
|
|
|
#endregion
|
|
}
|