259 lines
10 KiB
C#
259 lines
10 KiB
C#
using UnityEngine;
|
||
|
||
[RequireComponent(typeof(SphereCollider))]
|
||
public class WheelBolt : MonoBehaviour, IInteractable
|
||
{
|
||
[Header("Настройки вылета и скорости")]
|
||
[SerializeField] private float pullOutDistance = 0.05f;
|
||
[SerializeField] private float workDuration = 0.5f;
|
||
|
||
private CarPart parentWheel;
|
||
private MeshRenderer meshRenderer;
|
||
private SphereCollider boltCollider;
|
||
private Material originalMaterial;
|
||
private Material ghostMaterial;
|
||
private PlayerToolManager toolManager;
|
||
|
||
private Vector3 tightenedLocalPos;
|
||
private Quaternion tightenedLocalRot;
|
||
private Vector3 baseLocalScale;
|
||
|
||
private Vector3 localMeshCenter;
|
||
private Vector3 boltCenterInParent;
|
||
private Vector3 parentOutwardDir = Vector3.right;
|
||
|
||
private float tightness = 1f;
|
||
private bool isTightened = true;
|
||
private bool isHoveredThisFrame = false;
|
||
|
||
// Глобальная блокировка до отпускания кнопки, чтобы болт НИКОГДА не крутился туда-сюда по кругу
|
||
private static bool waitForKeyRelease = false;
|
||
|
||
public bool IsTightened => isTightened;
|
||
public bool IsAvailableForAim => parentWheel == null || parentWheel.State == PartState.Installed;
|
||
|
||
// ВАЖНО: Координата для прицела берется от неподвижного гнезда на ступице,
|
||
// чтобы во время выдвижения болта он не выскакивал из-под прицела!
|
||
public Vector3 WorldBoltCenter => transform.parent != null
|
||
? transform.parent.TransformPoint(boltCenterInParent)
|
||
: transform.TransformPoint(localMeshCenter);
|
||
|
||
private void Awake()
|
||
{
|
||
parentWheel = GetComponentInParent<CarPart>();
|
||
meshRenderer = GetComponent<MeshRenderer>();
|
||
boltCollider = GetComponent<SphereCollider>();
|
||
toolManager = FindAnyObjectByType<PlayerToolManager>();
|
||
|
||
tightenedLocalPos = transform.localPosition;
|
||
tightenedLocalRot = transform.localRotation;
|
||
baseLocalScale = transform.localScale;
|
||
|
||
MeshFilter mf = GetComponent<MeshFilter>();
|
||
if (mf != null && mf.sharedMesh != null)
|
||
localMeshCenter = mf.sharedMesh.bounds.center;
|
||
|
||
boltCollider.isTrigger = true;
|
||
boltCollider.center = localMeshCenter;
|
||
boltCollider.radius = 0.055f;
|
||
|
||
boltCenterInParent = tightenedLocalPos + tightenedLocalRot * Vector3.Scale(localMeshCenter, baseLocalScale);
|
||
|
||
if (meshRenderer != null)
|
||
originalMaterial = meshRenderer.sharedMaterial;
|
||
|
||
if (parentWheel != null && parentWheel.Data != null)
|
||
ghostMaterial = parentWheel.Data.ghostMaterial;
|
||
|
||
// Если в CarPartData забыли назначить Ghost Material — создаем запасной полупрозрачный материал
|
||
if (ghostMaterial == null)
|
||
{
|
||
Shader sh = Shader.Find("Universal Render Pipeline/Unlit") ?? Shader.Find("Sprites/Default");
|
||
if (sh != null)
|
||
{
|
||
ghostMaterial = new Material(sh);
|
||
ghostMaterial.color = new Color(0.2f, 0.95f, 0.35f, 0.5f);
|
||
}
|
||
}
|
||
|
||
RecalculateOutwardDirection();
|
||
}
|
||
|
||
public void RecalculateOutwardDirection()
|
||
{
|
||
Vector3 worldBoltPos = WorldBoltCenter;
|
||
VehicleInteraction car = GetComponentInParent<VehicleInteraction>();
|
||
Transform carRoot = car != null ? car.transform : transform.root;
|
||
|
||
Vector3 fromCarToBolt = worldBoltPos - carRoot.position;
|
||
float sideSign = Mathf.Sign(Vector3.Dot(fromCarToBolt, carRoot.right));
|
||
if (sideSign == 0f) sideSign = 1f;
|
||
|
||
Vector3 worldOutDir = carRoot.right * sideSign;
|
||
parentOutwardDir = transform.parent != null
|
||
? transform.parent.InverseTransformDirection(worldOutDir).normalized
|
||
: Vector3.right * sideSign;
|
||
|
||
UpdateBoltTransform(1f);
|
||
}
|
||
|
||
public void SetTightenedImmediate(bool state)
|
||
{
|
||
isTightened = state;
|
||
tightness = state ? 1f : 0f;
|
||
UpdateBoltTransform(1f);
|
||
}
|
||
|
||
private void Update()
|
||
{
|
||
// Снимаем блокировку сразу, как только игрок отпустил ЛКМ и клавишу E
|
||
if (waitForKeyRelease && !Input.GetMouseButton(0) && !Input.GetKey(KeyCode.E))
|
||
{
|
||
waitForKeyRelease = false;
|
||
}
|
||
}
|
||
|
||
private void LateUpdate()
|
||
{
|
||
if (parentWheel == null)
|
||
parentWheel = GetComponentInParent<CarPart>();
|
||
|
||
// Пока колесо в руках или лежит на земле — болты полностью скрыты
|
||
if (parentWheel != null && parentWheel.State != PartState.Installed)
|
||
{
|
||
if (meshRenderer != null && meshRenderer.enabled) meshRenderer.enabled = false;
|
||
if (boltCollider != null && boltCollider.enabled) boltCollider.enabled = false;
|
||
isHoveredThisFrame = false;
|
||
return;
|
||
}
|
||
|
||
if (boltCollider != null && !boltCollider.enabled)
|
||
boltCollider.enabled = true;
|
||
|
||
if (toolManager == null)
|
||
toolManager = FindAnyObjectByType<PlayerToolManager>();
|
||
|
||
bool hasWrench = toolManager != null && toolManager.currentTool == ToolType.Wrench;
|
||
bool isAnimating = tightness > 0.01f && tightness < 0.99f;
|
||
|
||
// Если игрок отпустил кнопку на полпути или увел взгляд — возвращаем болт в крайнее положение
|
||
if (!isHoveredThisFrame || (!Input.GetMouseButton(0) && !Input.GetKey(KeyCode.E)))
|
||
{
|
||
tightness = isTightened ? 1f : 0f;
|
||
isAnimating = false;
|
||
}
|
||
|
||
// Болт под прицелом заметно увеличивается (1.35x), чтобы игрок четко видел выбранный болт
|
||
float scaleMult = (isHoveredThisFrame && hasWrench) ? 1.35f : 1.0f;
|
||
UpdateBoltTransform(scaleMult);
|
||
|
||
if (meshRenderer != null)
|
||
{
|
||
if (isTightened || isAnimating)
|
||
{
|
||
// Закрученный болт всегда виден; под прицелом с ключом — подсвечивается
|
||
meshRenderer.enabled = true;
|
||
if (isHoveredThisFrame && hasWrench && ghostMaterial != null)
|
||
meshRenderer.sharedMaterial = ghostMaterial;
|
||
else if (originalMaterial != null)
|
||
meshRenderer.sharedMaterial = originalMaterial;
|
||
}
|
||
else
|
||
{
|
||
// КЛЮЧЕВОЕ ИСПРАВЛЕНИЕ: когда болты выкручены и игрок взял КЛЮЧ (hasWrench),
|
||
// ВСЕ 4 места под болты сразу подсвечиваются силуэтом, чтобы игрок видел, куда наводиться!
|
||
meshRenderer.enabled = hasWrench;
|
||
if (hasWrench && ghostMaterial != null)
|
||
meshRenderer.sharedMaterial = ghostMaterial;
|
||
}
|
||
}
|
||
|
||
isHoveredThisFrame = false;
|
||
}
|
||
|
||
public string GetPrompt()
|
||
{
|
||
isHoveredThisFrame = true;
|
||
|
||
if (isTightened && parentWheel != null && !CarJack.IsWheelLifted(parentWheel.transform))
|
||
{
|
||
return "Колесо под нагрузкой! Сначала поднимите эту сторону домкратом";
|
||
}
|
||
|
||
bool hasWrench = toolManager != null && toolManager.currentTool == ToolType.Wrench;
|
||
if (!hasWrench)
|
||
{
|
||
return isTightened
|
||
? "Колесный болт (Возьмите баллонный ключ [Tab])"
|
||
: "Место под болт (Возьмите баллонный ключ [Tab])";
|
||
}
|
||
|
||
bool isHoldingKey = Input.GetMouseButton(0) || Input.GetKey(KeyCode.E);
|
||
|
||
if (isHoldingKey && !waitForKeyRelease)
|
||
{
|
||
ProcessWrench(Time.deltaTime);
|
||
}
|
||
|
||
int percent = Mathf.RoundToInt((isTightened ? (1f - tightness) : tightness) * 100f);
|
||
|
||
if (isTightened)
|
||
{
|
||
if (waitForKeyRelease) return "Болт закручен! Отпустите кнопку";
|
||
return percent > 0 ? $"Откручиваем болт... {percent}%" : "[Удерж. ЛКМ / E] Открутить болт";
|
||
}
|
||
else
|
||
{
|
||
if (waitForKeyRelease) return "Болт выкручен! Отпустите кнопку";
|
||
return percent > 0 ? $"Закручиваем болт... {percent}%" : "[Удерж. ЛКМ / E] Закрутить болт";
|
||
}
|
||
}
|
||
|
||
private void ProcessWrench(float dt)
|
||
{
|
||
float step = dt / workDuration;
|
||
|
||
if (isTightened)
|
||
{
|
||
tightness -= step;
|
||
if (tightness <= 0f)
|
||
{
|
||
tightness = 0f;
|
||
isTightened = false;
|
||
waitForKeyRelease = true; // Блокируем до отпускания кнопки!
|
||
}
|
||
}
|
||
else
|
||
{
|
||
tightness += step;
|
||
if (tightness >= 1f)
|
||
{
|
||
tightness = 1f;
|
||
isTightened = true;
|
||
waitForKeyRelease = true; // Блокируем до отпускания кнопки!
|
||
}
|
||
}
|
||
}
|
||
|
||
private void UpdateBoltTransform(float scaleMultiplier = 1f)
|
||
{
|
||
// Выкрученный неанимируемый силуэт стоит ровно в гнезде ступицы (outFactor = 0),
|
||
// чтобы силуэты отверстий не висели в воздухе в 5 см от диска
|
||
bool isAnimating = tightness > 0.001f && tightness < 0.999f;
|
||
float outFactor = isAnimating ? (1f - Mathf.Clamp01(tightness)) : 0f;
|
||
|
||
Vector3 currentScale = baseLocalScale * scaleMultiplier;
|
||
|
||
Quaternion spinRotation = Quaternion.AngleAxis(outFactor * 720f, parentOutwardDir);
|
||
Quaternion newLocalRot = spinRotation * tightenedLocalRot;
|
||
|
||
Vector3 rotatedCenterOffset = newLocalRot * Vector3.Scale(localMeshCenter, currentScale);
|
||
Vector3 pullOutOffset = parentOutwardDir * (pullOutDistance * outFactor);
|
||
|
||
transform.localScale = currentScale;
|
||
transform.localRotation = newLocalRot;
|
||
transform.localPosition = boltCenterInParent - rotatedCenterOffset + pullOutOffset;
|
||
}
|
||
|
||
public void Interact(GameObject interactor) { }
|
||
} |