using UnityEngine; using UnityEngine.Rendering; namespace Vanguard.Board3D { /// /// Карта на столе: тонкая коробка с отдельными материалами лица, рубашки /// и торца. Rest — поворот вокруг Y, закрытость — переворот вокруг X. /// public sealed class Card3D : MonoBehaviour { private PlaymatLayoutConfig _config; private MeshRenderer _front; private MeshRenderer _back; private MeshRenderer _edge; private MeshRenderer _frame; private MeshRenderer _highlight; private BoxCollider _collider; private MaterialPropertyBlock _block; private TextMesh _stats; private bool _faceUp = true; private bool _rested; private Quaternion _baseRotation = Quaternion.identity; private Quaternion _targetRotation = Quaternion.identity; private Vector3 _targetPosition; private Vector3 _targetScale = Vector3.one; private Vector3 _baseScale = Vector3.one; private bool _immediate; private bool _arcMoving; private Vector3 _arcFromPosition; private Quaternion _arcFromRotation; private Vector3 _arcFromScale; private float _arcDuration; private float _arcElapsed; private float _arcHeight; private bool _revealActive; private float _revealElapsed; private float _revealDuration; private int _revealDirection; private Vector3 _revealFromPosition; private Vector3 _revealFromScale; // Пока запрос ждёт в очереди CardMoveAnimator, карта не должна // плавно двигаться к target — иначе она окажется у цели до дуги. private bool _hold; private Vector3 _holdPosition; private Quaternion _holdRotation; private Vector3 _holdScale; private bool _dragging; private Vector3 _dragOriginPosition; private Quaternion _dragOriginRotation; private float _moveSpeed = 16f; private float _rotateSpeed = 14f; public BoardCardRef Ref { get; private set; } public bool IsFaceUp => _faceUp; public bool IsRested => _rested; public bool IsDragging => _dragging; public Vector3 TargetPosition => _targetPosition; public float TargetYaw => _targetRotation.eulerAngles.y; public Quaternion TargetRotation => _targetRotation; /// /// Переворот рубашкой вверх с сохранением вертикальной ориентации /// рисунка рубашки. /// public static Quaternion FaceDownFlip => Quaternion.Euler(0f, 180f, 0f) * Quaternion.Euler(180f, 0f, 0f); /// Поза рубашкой вверх для текущей базовой ориентации. public Quaternion FaceDownRotation => _baseRotation * FaceDownFlip; public bool IsAnimating => !_dragging && (_arcMoving || _revealActive || Vector3.SqrMagnitude(transform.position - _targetPosition) > 0.0000004f || Quaternion.Angle(transform.rotation, _targetRotation) > 0.1f || Vector3.SqrMagnitude(transform.localScale - _targetScale) > 0.0000004f); public bool IsMoving => _arcMoving; public string StatsText => _stats != null && _stats.gameObject.activeSelf ? _stats.text : null; public static Card3D Create(Transform parent, PlaymatLayoutConfig config) { var go = new GameObject("Card3D"); go.transform.SetParent(parent, false); var card = go.AddComponent(); card.Build(config != null ? config : PlaymatLayoutConfig.Default); return card; } private void Build(PlaymatLayoutConfig config) { _config = config; _moveSpeed = config.smoothMoveSpeed; _rotateSpeed = config.smoothRotateSpeed; _block = new MaterialPropertyBlock(); var width = _config.cardWidth; var height = _config.cardHeight; var thickness = _config.cardThickness; var edgeGo = GameObject.CreatePrimitive(PrimitiveType.Cube); edgeGo.name = "Edge"; RemoveCollider(edgeGo); edgeGo.transform.SetParent(transform, false); edgeGo.transform.localScale = new Vector3(width, thickness, height); _edge = edgeGo.GetComponent(); _edge.sharedMaterial = BoardMaterials.Edge; BoardMaterials.SetShadows(_edge, true); var frontGo = new GameObject("Front"); frontGo.transform.SetParent(transform, false); frontGo.transform.localPosition = new Vector3(0f, (thickness * 0.5f) + 0.0008f, 0f); var frontFilter = frontGo.AddComponent(); frontFilter.sharedMesh = BoardMeshLibrary.CardFace(width, height, front: true); _front = frontGo.AddComponent(); _front.sharedMaterial = BoardMaterials.White; BoardMaterials.SetShadows(_front, false); var frameGo = new GameObject("Frame"); frameGo.transform.SetParent(transform, false); frameGo.transform.localPosition = new Vector3( 0f, (thickness * 0.5f) + 0.0012f, 0f); var frameFilter = frameGo.AddComponent(); frameFilter.sharedMesh = BoardMeshLibrary.Frame( width, height, Mathf.Max(0.006f, width * 0.009f)); _frame = frameGo.AddComponent(); _frame.sharedMaterial = BoardMaterials.Highlight; BoardMaterials.SetShadows(_frame, false); var backGo = new GameObject("Back"); backGo.transform.SetParent(transform, false); backGo.transform.localPosition = new Vector3(0f, -(thickness * 0.5f) - 0.0008f, 0f); var backFilter = backGo.AddComponent(); backFilter.sharedMesh = BoardMeshLibrary.CardFace(width, height, front: false); _back = backGo.AddComponent(); _back.sharedMaterial = BoardMaterials.Back; BoardMaterials.SetShadows(_back, false); var highlightGo = new GameObject("Highlight"); highlightGo.transform.SetParent(transform, false); highlightGo.transform.localPosition = new Vector3( 0f, (thickness * 0.5f) + 0.0018f, 0f); var filter = highlightGo.AddComponent(); filter.sharedMesh = BoardMeshLibrary.Frame( width, height, Mathf.Max(0.006f, width * 0.009f)); _highlight = highlightGo.AddComponent(); _highlight.sharedMaterial = BoardMaterials.Highlight; BoardMaterials.SetShadows(_highlight, false); _highlight.gameObject.SetActive(false); _stats = BoardText.Create( transform, string.Empty, 0.034f, Color.white, Vector3.zero); _stats.gameObject.SetActive(false); _collider = gameObject.AddComponent(); _collider.size = new Vector3( width * 1.02f, Mathf.Max(thickness + 0.02f, 0.03f), height * 1.02f); } public void Bind(BoardCardRef reference) { Ref = reference; name = "Card3D " + reference; } public void SetArt( Texture2D texture, Color accent) { if (_front == null) return; _front.GetPropertyBlock(_block); _block.Clear(); BoardMaterials.SetTexture( _block, _front.sharedMaterial, texture != null ? texture : BoardTextures.White); BoardMaterials.SetColor( _block, _front.sharedMaterial, texture != null ? Color.white : accent); _front.SetPropertyBlock(_block); if (_frame != null) { _frame.GetPropertyBlock(_block); _block.Clear(); BoardMaterials.SetColor( _block, _frame.sharedMaterial, new Color(0.015f, 0.015f, 0.018f, 1f)); _frame.SetPropertyBlock(_block); _frame.gameObject.SetActive(Ref.Zone != BoardZone.Hand); } } public void SetAnimationSpeeds(float moveSpeed, float rotateSpeed) { _moveSpeed = Mathf.Max(1f, moveSpeed); _rotateSpeed = Mathf.Max(1f, rotateSpeed); } public void SetStats(string text, bool modified) { if (_stats == null) return; if (string.IsNullOrEmpty(text)) { _stats.gameObject.SetActive(false); return; } _stats.text = text; _stats.color = modified ? new Color(1f, 0.82f, 0.28f) : new Color(0.92f, 0.95f, 1f, 0.92f); _stats.gameObject.SetActive(true); } private void LateUpdate() { if (_stats == null || !_stats.gameObject.activeSelf) return; var position = transform.position; _stats.transform.position = new Vector3( position.x, position.y + 0.015f, position.z - (_config.cardHeight * 0.68f)); _stats.transform.rotation = Quaternion.Euler(90f, 0f, 0f); } public void SetState( Quaternion baseRotation, bool faceUp, bool rested, bool damageLayout = false) { _baseRotation = baseRotation; _faceUp = faceUp; _rested = rested; // Атака/Rest — по часовой стрелке (+90°), урон лежит против (-90°). var restYaw = rested ? (damageLayout ? -90f : 90f) : 0f; var rest = Quaternion.Euler(0f, restYaw, 0f); // Урон переворачивается вокруг длинной стороны карты, а не // поперёк неё. Обычная рубашка сохраняет вертикальную ориентацию. var flip = faceUp ? Quaternion.identity : damageLayout ? Quaternion.Euler(0f, 0f, 180f) : FaceDownFlip; _targetRotation = rest * baseRotation * flip; } public void SetPose( Vector3 position, Quaternion baseRotation, bool faceUp, bool rested, bool immediate, bool damageLayout = false, float scale = 1f) { SetState(baseRotation, faceUp, rested, damageLayout); _targetPosition = position; _targetScale = Vector3.one * Mathf.Max(0.05f, scale); _baseScale = _targetScale; _immediate = immediate; if (_dragging) return; if (immediate) Snap(); } public void PlaceImmediate( Vector3 position, Quaternion rotation, Vector3 scale) { _arcMoving = false; _immediate = false; _hold = false; _revealActive = false; transform.position = position; transform.rotation = rotation; transform.localScale = scale; } public void HoldAtSource( Vector3 position, Quaternion rotation, Vector3 scale) { _hold = true; _holdPosition = position; _holdRotation = rotation; _holdScale = scale; _immediate = false; _revealActive = false; transform.position = position; transform.rotation = rotation; transform.localScale = scale; } public void CancelHold() { _hold = false; } public void Reveal(float duration, int direction = 1) { if (duration <= 0.001f) { Snap(); return; } _dragging = false; _arcMoving = false; _hold = false; _immediate = false; _revealActive = true; _revealElapsed = 0f; _revealDuration = Mathf.Max(0.01f, duration); _revealDirection = direction < 0 ? -1 : 1; _revealFromPosition = _targetPosition + (Vector3.up * 0.12f); _revealFromScale = _targetScale * 0.85f; // Начинаем с текущей позы рубашкой вверх и переворачиваем // вокруг длинной оси карты: слева направо, у противника зеркально. transform.position = _revealFromPosition; transform.rotation = _baseRotation * FaceDownFlip; transform.localScale = _revealFromScale; } public void StartArcMove( Vector3 fromPosition, Quaternion fromRotation, Vector3 fromScale, float duration, float arcHeight) { if (_dragging) return; _arcMoving = false; _immediate = false; _hold = false; _revealActive = false; var distance = Vector3.Distance(fromPosition, _targetPosition); var angle = Quaternion.Angle(fromRotation, _targetRotation); var scaleDistance = Vector3.Distance(fromScale, _targetScale); if (duration <= 0.001f || (distance < 0.002f && angle < 0.2f && scaleDistance < 0.002f)) { Snap(); return; } _arcFromPosition = fromPosition; _arcFromRotation = fromRotation; _arcFromScale = fromScale; _arcDuration = Mathf.Max(0.01f, duration); _arcElapsed = 0f; _arcHeight = Mathf.Max(0f, arcHeight); _arcMoving = true; transform.position = fromPosition; transform.rotation = fromRotation; transform.localScale = fromScale; } public void StopMove(bool snap) { if (!_arcMoving && !_hold && !_revealActive) return; _arcMoving = false; _hold = false; _revealActive = false; if (snap) Snap(); } public void SetVisible(bool visible) { gameObject.SetActive(visible); } public void SetHighlight(Color? color) { if (_highlight == null) return; var active = color.HasValue; _highlight.gameObject.SetActive(active); if (!active) return; _highlight.GetPropertyBlock(_block); _block.Clear(); BoardMaterials.SetColor(_block, _highlight.sharedMaterial, color.Value); _highlight.SetPropertyBlock(_block); } public void BeginDrag() { if (_dragging) return; _dragging = true; _hold = false; _revealActive = false; _dragOriginPosition = transform.position; _dragOriginRotation = transform.rotation; _targetScale = _baseScale * 1.05f; if (_collider != null) _collider.enabled = false; SetHighlight(null); } public float DragLift => Ref.Zone == BoardZone.Hand ? _config.cardHeight * _baseScale.x * _config.handHoverLift : _config.dragLift; public void DragTo(Vector3 worldPosition) { if (!_dragging) return; // Сохраняем исходную ориентацию карты: карта из руки остаётся // лицом к игроку, а не «ложится» на стол при перетаскивании. // Высота та же, что у hover: 10% высоты карты. transform.position = new Vector3( worldPosition.x, DragLift + (_config.cardThickness * 0.5f), worldPosition.z); transform.rotation = _dragOriginRotation; transform.localScale = _baseScale * 1.05f; } /// /// Отпустить карту в текущей точке drag-а, не возвращая её в руку. /// Держим её поднятой до ответа сервера или явного возврата. /// public void EndDragAtCurrent() { _dragging = false; if (_collider != null) _collider.enabled = true; _hold = true; _holdPosition = transform.position; _holdRotation = transform.rotation; _holdScale = transform.localScale; _targetPosition = _holdPosition; _targetRotation = _holdRotation; _targetScale = _holdScale; _arcMoving = false; _revealActive = false; _immediate = false; } public void DropAt(Vector3 position, Quaternion rotation) { _dragging = false; _hold = false; _revealActive = false; if (_collider != null) _collider.enabled = true; _targetPosition = position; _targetRotation = rotation; _targetScale = _baseScale; _immediate = false; } public void ReturnToOrigin() { _dragging = false; _hold = false; _revealActive = false; if (_collider != null) _collider.enabled = true; _targetPosition = _dragOriginPosition; _targetRotation = _dragOriginRotation; _targetScale = _baseScale; _immediate = false; } public void Snap() { _arcMoving = false; _hold = false; _revealActive = false; transform.position = _targetPosition; transform.rotation = _targetRotation; transform.localScale = _targetScale; _immediate = false; } public void Tick(float deltaTime) { if (_dragging || deltaTime <= 0f) return; if (_revealActive) { _revealElapsed += deltaTime; var t = Mathf.Clamp01(_revealElapsed / _revealDuration); var eased = t * t * (3f - (2f * t)); var angle = 180f * eased * _revealDirection; transform.position = Vector3.Lerp( _revealFromPosition, _targetPosition, eased); transform.rotation = _baseRotation * Quaternion.Euler(0f, 0f, angle) * FaceDownFlip; transform.localScale = Vector3.Lerp( _revealFromScale, _targetScale, eased); if (t >= 1f) { _revealActive = false; Snap(); } return; } if (_hold && !_arcMoving) return; if (_arcMoving) { _arcElapsed += deltaTime; var t = Mathf.Clamp01(_arcElapsed / _arcDuration); var eased = t * t * (3f - (2f * t)); var position = Vector3.Lerp( _arcFromPosition, _targetPosition, eased); position.y += Mathf.Sin(t * Mathf.PI) * _arcHeight; transform.position = position; transform.rotation = Quaternion.Slerp( _arcFromRotation, _targetRotation, eased); transform.localScale = Vector3.Lerp( _arcFromScale, _targetScale, eased); if (t >= 1f) Snap(); return; } if (_immediate) { Snap(); return; } var move = 1f - Mathf.Exp(-_moveSpeed * deltaTime); var rotate = 1f - Mathf.Exp(-_rotateSpeed * deltaTime); transform.position = Vector3.Lerp(transform.position, _targetPosition, move); transform.rotation = Quaternion.Slerp(transform.rotation, _targetRotation, rotate); transform.localScale = Vector3.Lerp(transform.localScale, _targetScale, move); if (Vector3.SqrMagnitude(transform.position - _targetPosition) < 0.0000004f && Quaternion.Angle(transform.rotation, _targetRotation) < 0.1f) { Snap(); } } private static void RemoveCollider(GameObject target) { var collider = target.GetComponent(); if (collider == null) return; if (Application.isPlaying) Destroy(collider); else DestroyImmediate(collider); } } }