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

216 lines
8.1 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 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);
}
/// <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>Душа — визуальная стопка под кругом 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;
return new Vector3(offset * spacing, _config.GuardianCenterY, 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>
/// Ближайшая цель для броска карты. Возвращает 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; }
}
}
}