VanguardReMaster/client/Assets/Vanguard/Runtime/Board3D/View/Card3D.cs

620 lines
22 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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