189 lines
7.0 KiB
C#
189 lines
7.0 KiB
C#
using UnityEngine;
|
||
|
||
/// <summary>
|
||
/// Компонент на игроке.
|
||
/// ЛКМ — удержание → разрушение блока с прогрессом.
|
||
/// ПКМ — мгновенная постановка блока из хотбара.
|
||
/// </summary>
|
||
public class BlockPlacer : MonoBehaviour
|
||
{
|
||
[Header("Settings")]
|
||
public float reach = 5f; // дальность в Unity-единицах
|
||
public float breakTime = 1f; // секунд до разрушения
|
||
public LayerMask blockLayer; // слой чанков (назначить в Inspector)
|
||
|
||
[Header("Break Overlay")]
|
||
public Material breakMaterial; // Blocks/BlockBreak шейдер
|
||
public Texture2D crackTexture; // текстура трещин (grayscale)
|
||
|
||
// ── Приватное ─────────────────────────────────────────────────────────────
|
||
Camera _cam;
|
||
float _breakProgress;
|
||
Vector3Int _breakingBlock = Vector3Int.one * int.MinValue; // "никакой"
|
||
GameObject _breakOverlay;
|
||
MeshRenderer _overlayRenderer;
|
||
|
||
static readonly int PropProgress = Shader.PropertyToID("_Progress");
|
||
|
||
void Start()
|
||
{
|
||
_cam = Camera.main;
|
||
|
||
if (breakMaterial == null)
|
||
{
|
||
var shader = Shader.Find("Blocks/BlockBreak");
|
||
if (shader != null) breakMaterial = new Material(shader);
|
||
}
|
||
|
||
if (breakMaterial != null && crackTexture != null)
|
||
breakMaterial.SetTexture("_CrackTex", crackTexture);
|
||
}
|
||
|
||
void Update()
|
||
{
|
||
HandleBreak();
|
||
HandlePlace();
|
||
}
|
||
|
||
// ── Разрушение (ЛКМ удержание) ───────────────────────────────────────────
|
||
|
||
void HandleBreak()
|
||
{
|
||
if (!Input.GetMouseButton(0))
|
||
{
|
||
CancelBreak();
|
||
return;
|
||
}
|
||
|
||
if (!Raycast(out RaycastHit hit, out Vector3Int blockPos))
|
||
{
|
||
CancelBreak();
|
||
return;
|
||
}
|
||
|
||
// Сменился ли целевой блок?
|
||
if (blockPos != _breakingBlock)
|
||
{
|
||
CancelBreak();
|
||
_breakingBlock = blockPos;
|
||
SpawnBreakOverlay(blockPos);
|
||
}
|
||
|
||
_breakProgress += Time.deltaTime / breakTime;
|
||
UpdateOverlay(_breakProgress);
|
||
|
||
if (_breakProgress >= 1f)
|
||
{
|
||
FinishBreak(blockPos);
|
||
}
|
||
}
|
||
|
||
void FinishBreak(Vector3Int blockPos)
|
||
{
|
||
string blockId = WorldManager.Instance.BreakBlock(blockPos);
|
||
CancelBreak();
|
||
|
||
if (blockId == null) return;
|
||
|
||
// Дроп предмета
|
||
var def = BlockRegistry.Instance?.GetById(blockId);
|
||
if (def?.linkedItem != null && InventoryItemFactory.Instance != null)
|
||
{
|
||
Vector3 dropPos = new Vector3(blockPos.x + 0.5f, blockPos.y + 0.5f, blockPos.z + 0.5f);
|
||
InventoryItemFactory.Instance.CreateWorldItem(def.linkedItem, 1, dropPos);
|
||
}
|
||
}
|
||
|
||
void CancelBreak()
|
||
{
|
||
_breakProgress = 0f;
|
||
_breakingBlock = Vector3Int.one * int.MinValue;
|
||
if (_breakOverlay != null) Destroy(_breakOverlay);
|
||
_breakOverlay = null;
|
||
}
|
||
|
||
// ── Постановка (ПКМ) ─────────────────────────────────────────────────────
|
||
|
||
void HandlePlace()
|
||
{
|
||
if (!Input.GetMouseButtonDown(1)) return;
|
||
if (!Raycast(out RaycastHit hit, out Vector3Int blockPos)) return;
|
||
|
||
// Ячейка, куда ставим = сосед грани которую мы смотрим
|
||
Vector3Int placePos = blockPos + Vector3Int.RoundToInt(hit.normal);
|
||
|
||
// Не ставим блок внутри игрока
|
||
if (IsInsidePlayer(placePos)) return;
|
||
|
||
// Получаем блок из хотбара
|
||
var selectedItem = InventoryManager.Instance?.GetSelectedHotbarItem();
|
||
if (selectedItem == null) return;
|
||
|
||
var def = BlockRegistry.Instance?.GetByItemId(selectedItem.id);
|
||
if (def == null)
|
||
{
|
||
Debug.Log($"[BlockPlacer] {selectedItem.itemName} не является блоком.");
|
||
return;
|
||
}
|
||
|
||
WorldManager.Instance.PlaceBlock(placePos, def.blockId);
|
||
|
||
// Потребляем 1 из хотбара
|
||
InventoryManager.Instance.RemoveItem(selectedItem.id, 1);
|
||
}
|
||
|
||
// ── Overlay разрушения ────────────────────────────────────────────────────
|
||
|
||
void SpawnBreakOverlay(Vector3Int blockPos)
|
||
{
|
||
if (breakMaterial == null) return;
|
||
|
||
_breakOverlay = GameObject.CreatePrimitive(PrimitiveType.Cube);
|
||
_breakOverlay.name = "BreakOverlay";
|
||
_breakOverlay.transform.position = blockPos + new Vector3(0.5f, 0.5f, 0.5f);
|
||
_breakOverlay.transform.localScale = Vector3.one * 1.001f; // чуть больше, без z-fight
|
||
|
||
// Убираем коллайдер — он не нужен
|
||
Destroy(_breakOverlay.GetComponent<Collider>());
|
||
|
||
_overlayRenderer = _breakOverlay.GetComponent<MeshRenderer>();
|
||
_overlayRenderer.sharedMaterial = breakMaterial;
|
||
_overlayRenderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
|
||
}
|
||
|
||
void UpdateOverlay(float progress)
|
||
{
|
||
if (_overlayRenderer == null) return;
|
||
// MaterialPropertyBlock — не создаём новый экземпляр материала
|
||
var mpb = new MaterialPropertyBlock();
|
||
_overlayRenderer.GetPropertyBlock(mpb);
|
||
mpb.SetFloat(PropProgress, progress);
|
||
_overlayRenderer.SetPropertyBlock(mpb);
|
||
}
|
||
|
||
// ── Утилиты ───────────────────────────────────────────────────────────────
|
||
|
||
bool Raycast(out RaycastHit hit, out Vector3Int blockPos)
|
||
{
|
||
blockPos = default;
|
||
Ray ray = new Ray(_cam.transform.position, _cam.transform.forward);
|
||
|
||
if (Physics.Raycast(ray, out hit, reach, blockLayer))
|
||
{
|
||
// hit.point лежит на грани — смещаемся чуть внутрь блока
|
||
Vector3 inner = hit.point - hit.normal * 0.01f;
|
||
blockPos = WorldManager.WorldToBlock(inner);
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
bool IsInsidePlayer(Vector3Int blockPos)
|
||
{
|
||
// Проверяем две ячейки (ноги и голова)
|
||
Vector3Int feet = WorldManager.WorldToBlock(transform.position);
|
||
Vector3Int head = WorldManager.WorldToBlock(transform.position + Vector3.up * 1.8f);
|
||
return blockPos == feet || blockPos == head;
|
||
}
|
||
}
|