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