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 _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 bool _immediate; 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 bool IsAnimating => !_dragging && (Vector3.SqrMagnitude(transform.position - _targetPosition) > 0.0000004f || Quaternion.Angle(transform.rotation, _targetRotation) > 0.1f || Vector3.SqrMagnitude(transform.localScale - _targetScale) > 0.0000004f); 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 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.0015f, 0f); var filter = highlightGo.AddComponent(); filter.sharedMesh = BoardMeshLibrary.Frame( width * 1.16f, height * 1.16f, Mathf.Max(0.025f, width * 0.035f)); _highlight = highlightGo.AddComponent(); _highlight.sharedMaterial = BoardMaterials.Highlight; BoardMaterials.SetShadows(_highlight, false); _highlight.gameObject.SetActive(false); _stats = BoardText.Create( transform, string.Empty, 0.028f, 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); } 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.62f)); _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 = Quaternion.Euler(faceUp ? 0f : 180f, 0f, 0f); _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); _immediate = immediate; if (_dragging) return; if (immediate) 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; _dragOriginPosition = transform.position; _dragOriginRotation = transform.rotation; _targetScale = Vector3.one * 1.05f; if (_collider != null) _collider.enabled = false; SetHighlight(null); } public void DragTo(Vector3 worldPosition) { if (!_dragging) return; var yaw = _targetRotation.eulerAngles.y; transform.position = new Vector3( worldPosition.x, _config.dragLift + (_config.cardThickness * 0.5f), worldPosition.z); transform.rotation = Quaternion.Euler(0f, yaw, 0f); transform.localScale = Vector3.one * 1.05f; } public void DropAt(Vector3 position, Quaternion rotation) { _dragging = false; if (_collider != null) _collider.enabled = true; _targetPosition = position; _targetRotation = rotation; _targetScale = Vector3.one; _immediate = false; } public void ReturnToOrigin() { _dragging = false; if (_collider != null) _collider.enabled = true; _targetPosition = _dragOriginPosition; _targetRotation = _dragOriginRotation; _targetScale = Vector3.one; _immediate = false; } public void Snap() { transform.position = _targetPosition; transform.rotation = _targetRotation; transform.localScale = _targetScale; _immediate = false; } public void Tick(float deltaTime) { if (_dragging || deltaTime <= 0f) 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); } } }