VanguardReMaster/client/Assets/Vanguard/Runtime/Board3D/Logic/PlaymatLayout.cs

259 lines
10 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;
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;
// Карты лежат на боку: вертикальный шаг задаётся шириной карты.
// 0.3 — новая карта перекрывает 30% предыдущей.
var step = _config.cardWidth
* _config.cardScale
* _config.damageStackOffset;
var z = zSign * (_config.frontRowZ + 0.4f)
+ zSign * index * step;
// Небольшой подъём по Y: новая карта рисуется поверх старой.
var y = _config.DamageCenterY + (index * 0.0015f);
return new Vector3(x, y, 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;
// Как и своя рука: веер по X/Y/roll, а по глубине каждая
// следующая карта ближе к камере и рисуется поверх предыдущей.
var z = _config.opponentHandZ - (index * 0.05f);
var y = _config.opponentHandHeight
+ (1f - Mathf.Abs(offset) / safeCount) * _config.opponentHandArc;
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; }
}
}
}