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);
}
}
}