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 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;
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 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;
_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);
_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 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)
{
SetState(baseRotation, faceUp, rested, damageLayout);
_targetPosition = position;
_targetScale = Vector3.one;
_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(-_config.smoothMoveSpeed * deltaTime);
var rotate = 1f - Mathf.Exp(-_config.smoothRotateSpeed * 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);
}
}
}