using UnityEngine; namespace Vanguard.Board3D { /// /// Геометрия стола. Локальное место 0 — ближняя половина (локальный /// игрок), 1 — дальняя (противник). Позиции всегда зеркальны, поэтому /// игрок за второе место видит своё поле внизу. /// public sealed class PlaymatLayout { public const int VanguardSlot = -1; public const int RearGuardCount = 5; public const int DamageSlots = 6; private readonly PlaymatLayoutConfig _config; public PlaymatLayout(PlaymatLayoutConfig config) { _config = config != null ? config : PlaymatLayoutConfig.Default; } public PlaymatLayoutConfig Config => _config; public int ToLocalSeat(int player, int selfPlayer) => player == selfPlayer ? 0 : 1; public int ToPlayer(int localSeat, int selfPlayer) => localSeat == 0 ? selfPlayer : 1 - selfPlayer; /// Z-направление от центра к половине локального места. public float ZSign(int localSeat) => localSeat == 0 ? -1f : 1f; /// Поворот лица карты: своя половина читается с -Z, чужая — с +Z. public float BaseYaw(int localSeat) => localSeat == 0 ? 0f : 180f; public Vector3 CirclePosition(int localSeat, int slot) { var zSign = ZSign(localSeat); var columnSign = localSeat == 0 ? 1f : -1f; var x = 0f; var z = slot == VanguardSlot || slot is 3 or 4 ? zSign * _config.frontRowZ : zSign * _config.backRowZ; switch (slot) { case 0: case 3: x = -_config.circleSpacingX * columnSign; break; case 2: case 4: x = _config.circleSpacingX * columnSign; break; } return new Vector3(x, _config.CardCenterY, z); } public Vector3 CirclePositionForPlayer(int player, int selfPlayer, int slot) => CirclePosition(ToLocalSeat(player, selfPlayer), slot); /// Круги урона: две колонки по три карты на боку. public Vector3 DamagePosition(int localSeat, int index) { var zSign = ZSign(localSeat); var sideSign = localSeat == 0 ? -1f : 1f; var innerSign = localSeat == 0 ? -1f : 1f; var xCenter = sideSign * _config.damageX; var x = xCenter + (index % 2 == 0 ? innerSign : -innerSign) * 0.42f; var z = zSign * (_config.frontRowZ + 0.45f) + zSign * (index / 2) * 0.55f; return new Vector3(x, _config.DamageCenterY, z); } /// Колода и дроп сбоку от поля, у своего края. public Vector3 DeckPosition(int localSeat) { var x = (localSeat == 0 ? 1f : -1f) * _config.pileX; var z = ZSign(localSeat) * (_config.backRowZ + 0.15f); return new Vector3(x, _config.pileLift, z); } public Vector3 DropPosition(int localSeat) { var x = (localSeat == 0 ? 1f : -1f) * _config.pileX; var z = ZSign(localSeat) * (_config.frontRowZ + 0.05f); return new Vector3(x, _config.pileLift, z); } /// Душа — визуальная стопка под кругом V, чуть ближе к игроку. public Vector3 SoulPosition(int localSeat) { var z = ZSign(localSeat) * (_config.frontRowZ + 0.55f); return new Vector3(0f, 0.002f, z); } /// Общий круг защиты: своя половина ближе к своему полю. public Vector3 GuardianPosition(int localSeat, int index, int count) { var spacing = _config.guardianSpacing; if (count > 1) spacing = Mathf.Min(spacing, _config.guardianMaxWidth / (count - 1)); var offset = index - (count - 1) * 0.5f; var z = ZSign(localSeat) * _config.guardianRowGap; return new Vector3(offset * spacing, _config.GuardianCenterY, z); } /// Веер руки перед локальным игроком. public HandPose GetHandPose(int index, int count) { var safeCount = Mathf.Max(1, count); var spacing = _config.handSpacing; if (safeCount > 1) spacing = Mathf.Min(spacing, _config.handMaxWidth / (safeCount - 1)); var offset = index - (safeCount - 1) * 0.5f; var normalized = offset / safeCount; var x = offset * spacing; var z = _config.handCenterZ + Mathf.Abs(offset) * _config.handArc; var y = _config.handHeight + (1f - Mathf.Abs(normalized)) * 0.02f; var yaw = -offset * 3.2f; return new HandPose(new Vector3(x, y, z), yaw); } /// /// Ближайшая цель для броска карты. Возвращает false, если карта /// отпущена мимо кругов и общего круга защиты. /// public bool TryFindDropTarget( Vector3 worldPoint, int selfPlayer, out BoardDropTarget target) { var bestDistance = float.MaxValue; target = default; for (var localSeat = 0; localSeat < 2; localSeat++) { for (var slot = VanguardSlot; slot < RearGuardCount; slot++) { var position = CirclePosition(localSeat, slot); var distance = XzDistance(worldPoint, position); if (distance > _config.snapRadius || distance >= bestDistance) continue; bestDistance = distance; target = new BoardDropTarget( slot == VanguardSlot ? BoardZone.Vanguard : BoardZone.RearGuard, ToPlayer(localSeat, selfPlayer), slot, 0); } } if (bestDistance < float.MaxValue) return true; // Общий круг защиты принимает карты с любой стороны. if (Mathf.Abs(worldPoint.x) <= _config.guardianMaxWidth * 0.5f + 0.35f && Mathf.Abs(worldPoint.z) <= _config.guardianRowGap + 0.45f) { var localSeat = worldPoint.z < 0f ? 0 : 1; target = new BoardDropTarget( BoardZone.Guardian, ToPlayer(localSeat, selfPlayer), -3, 0); return true; } return false; } public static float XzDistance(Vector3 a, Vector3 b) { var dx = a.x - b.x; var dz = a.z - b.z; return Mathf.Sqrt((dx * dx) + (dz * dz)); } public static bool IsFrontRow(int slot) => slot is 3 or 4; public static bool IsBackRow(int slot) => slot is 0 or 1 or 2; public static string SlotLabel(int slot) { if (slot == VanguardSlot) return "V"; return slot switch { 0 => "RC1", 1 => "RC2", 2 => "RC3", 3 => "RC4", 4 => "RC5", _ => "?", }; } public readonly struct HandPose { public HandPose(Vector3 position, float yaw) { Position = position; Yaw = yaw; } public Vector3 Position { get; } public float Yaw { get; } } } }