620 lines
22 KiB
C#
620 lines
22 KiB
C#
using UnityEngine;
|
||
using UnityEngine.Rendering;
|
||
|
||
namespace Vanguard.Board3D
|
||
{
|
||
/// <summary>
|
||
/// Карта на столе: тонкая коробка с отдельными материалами лица, рубашки
|
||
/// и торца. Rest — поворот вокруг Y, закрытость — переворот вокруг X.
|
||
/// </summary>
|
||
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;
|
||
|
||
/// <summary>
|
||
/// Переворот рубашкой вверх с сохранением вертикальной ориентации
|
||
/// рисунка рубашки.
|
||
/// </summary>
|
||
public static Quaternion FaceDownFlip =>
|
||
Quaternion.Euler(0f, 180f, 0f) * Quaternion.Euler(180f, 0f, 0f);
|
||
|
||
/// <summary>Поза рубашкой вверх для текущей базовой ориентации.</summary>
|
||
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<Card3D>();
|
||
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<MeshRenderer>();
|
||
_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<MeshFilter>();
|
||
frontFilter.sharedMesh = BoardMeshLibrary.CardFace(width, height, front: true);
|
||
_front = frontGo.AddComponent<MeshRenderer>();
|
||
_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<MeshFilter>();
|
||
frameFilter.sharedMesh = BoardMeshLibrary.Frame(
|
||
width,
|
||
height,
|
||
Mathf.Max(0.006f, width * 0.009f));
|
||
_frame = frameGo.AddComponent<MeshRenderer>();
|
||
_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<MeshFilter>();
|
||
backFilter.sharedMesh = BoardMeshLibrary.CardFace(width, height, front: false);
|
||
_back = backGo.AddComponent<MeshRenderer>();
|
||
_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<MeshFilter>();
|
||
filter.sharedMesh = BoardMeshLibrary.Frame(
|
||
width,
|
||
height,
|
||
Mathf.Max(0.006f, width * 0.009f));
|
||
_highlight = highlightGo.AddComponent<MeshRenderer>();
|
||
_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<BoxCollider>();
|
||
_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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Отпустить карту в текущей точке drag-а, не возвращая её в руку.
|
||
/// Держим её поднятой до ответа сервера или явного возврата.
|
||
/// </summary>
|
||
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<Collider>();
|
||
if (collider == null)
|
||
return;
|
||
|
||
if (Application.isPlaying)
|
||
Destroy(collider);
|
||
else
|
||
DestroyImmediate(collider);
|
||
}
|
||
}
|
||
}
|