250 lines
9.6 KiB
C#
250 lines
9.6 KiB
C#
using UnityEngine;
|
||
|
||
namespace Vanguard.Board3D
|
||
{
|
||
/// <summary>
|
||
/// Геометрия стола. Локальное место 0 — ближняя половина (локальный
|
||
/// игрок), 1 — дальняя (противник). Позиции всегда зеркальны, поэтому
|
||
/// игрок за второе место видит своё поле внизу.
|
||
/// </summary>
|
||
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;
|
||
|
||
/// <summary>Z-направление от центра к половине локального места.</summary>
|
||
public float ZSign(int localSeat) => localSeat == 0 ? -1f : 1f;
|
||
|
||
/// <summary>Поворот лица карты: своя половина читается с -Z, чужая — с +Z.</summary>
|
||
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);
|
||
|
||
/// <summary>Урон — один вертикальный ряд, карты на боку.</summary>
|
||
public Vector3 DamagePosition(int localSeat, int index)
|
||
{
|
||
var zSign = ZSign(localSeat);
|
||
var sideSign = localSeat == 0 ? -1f : 1f;
|
||
var x = sideSign * _config.damageX;
|
||
var z = zSign * (_config.frontRowZ + 0.4f)
|
||
+ zSign * index * 0.4f;
|
||
return new Vector3(x, _config.DamageCenterY, z);
|
||
}
|
||
|
||
/// <summary>Колода и дроп сбоку от поля, у своего края.</summary>
|
||
public Vector3 DeckPosition(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);
|
||
}
|
||
|
||
public Vector3 DropPosition(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);
|
||
}
|
||
|
||
/// <summary>Зона проверки триггера — над колодой, ближе к центру стола.</summary>
|
||
public Vector3 TriggerPosition(int localSeat, int index, int count)
|
||
{
|
||
var x = (localSeat == 0 ? 1f : -1f) * _config.pileX;
|
||
var offset = index - (Mathf.Max(1, count) - 1) * 0.5f;
|
||
var sideSign = localSeat == 0 ? 1f : -1f;
|
||
// «Над колодой» — к центру стола на половине владельца.
|
||
var z = ZSign(localSeat) * Mathf.Max(0.25f, _config.frontRowZ - 1.0f);
|
||
return new Vector3(x + offset * 0.42f * sideSign, _config.cardLift, z);
|
||
}
|
||
|
||
/// <summary>Душа — визуальная стопка под кругом V, чуть ближе к игроку.</summary>
|
||
public Vector3 SoulPosition(int localSeat)
|
||
{
|
||
var z = ZSign(localSeat) * (_config.frontRowZ + 0.55f);
|
||
return new Vector3(0f, 0.002f, z);
|
||
}
|
||
|
||
/// <summary>Общий круг защиты: своя половина ближе к своему полю.</summary>
|
||
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;
|
||
var stackLift = index * 0.0015f;
|
||
return new Vector3(
|
||
offset * spacing,
|
||
_config.GuardianCenterY + stackLift,
|
||
z);
|
||
}
|
||
|
||
/// <summary>Веер руки перед локальным игроком.</summary>
|
||
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);
|
||
}
|
||
|
||
/// <summary>Веер рубашек противника у дальнего края стола.</summary>
|
||
public HandPose GetOpponentHandPose(int index, int count)
|
||
{
|
||
var safeCount = Mathf.Max(1, count);
|
||
var spacing = _config.opponentHandSpacing;
|
||
if (safeCount > 1)
|
||
{
|
||
spacing = Mathf.Min(
|
||
spacing,
|
||
_config.opponentHandMaxWidth / (safeCount - 1));
|
||
}
|
||
|
||
var offset = index - (safeCount - 1) * 0.5f;
|
||
var x = offset * spacing;
|
||
var z = _config.opponentHandZ + Mathf.Abs(offset) * 0.015f;
|
||
var y = _config.opponentHandHeight
|
||
- (1f - Mathf.Abs(offset) / safeCount) * 0.02f;
|
||
var roll = -offset * _config.opponentHandRoll;
|
||
return new HandPose(new Vector3(x, y, z), roll);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Ближайшая цель для броска карты. Возвращает false, если карта
|
||
/// отпущена мимо кругов и общего круга защиты.
|
||
/// </summary>
|
||
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; }
|
||
}
|
||
}
|
||
}
|