Fix card faces, lock camera, add stats overlay and standard sleeve back
CI / test (push) Successful in 2m43s
Details
CI / test (push) Successful in 2m43s
Details
This commit is contained in:
parent
f2a01bae16
commit
b4fa5d132f
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
|
|
@ -0,0 +1,132 @@
|
|||
fileFormatVersion: 2
|
||||
guid: fcf11b652a4a6f744917c23591d4b479
|
||||
TextureImporter:
|
||||
internalIDToNameTable: []
|
||||
externalObjects: {}
|
||||
serializedVersion: 13
|
||||
mipmaps:
|
||||
mipMapMode: 0
|
||||
enableMipMap: 1
|
||||
sRGBTexture: 1
|
||||
linearTexture: 0
|
||||
fadeOut: 0
|
||||
borderMipMap: 0
|
||||
mipMapsPreserveCoverage: 0
|
||||
alphaTestReferenceValue: 0.5
|
||||
mipMapFadeDistanceStart: 1
|
||||
mipMapFadeDistanceEnd: 3
|
||||
bumpmap:
|
||||
convertToNormalMap: 0
|
||||
externalNormalMap: 0
|
||||
heightScale: 0.25
|
||||
normalMapFilter: 0
|
||||
flipGreenChannel: 0
|
||||
isReadable: 0
|
||||
streamingMipmaps: 0
|
||||
streamingMipmapsPriority: 0
|
||||
vTOnly: 0
|
||||
ignoreMipmapLimit: 0
|
||||
grayScaleToAlpha: 0
|
||||
generateCubemap: 6
|
||||
cubemapConvolution: 0
|
||||
seamlessCubemap: 0
|
||||
textureFormat: 1
|
||||
maxTextureSize: 2048
|
||||
textureSettings:
|
||||
serializedVersion: 2
|
||||
filterMode: 1
|
||||
aniso: 1
|
||||
mipBias: 0
|
||||
wrapU: 0
|
||||
wrapV: 0
|
||||
wrapW: 0
|
||||
nPOTScale: 1
|
||||
lightmap: 0
|
||||
compressionQuality: 50
|
||||
spriteMode: 0
|
||||
spriteExtrude: 1
|
||||
spriteMeshType: 1
|
||||
alignment: 0
|
||||
spritePivot: {x: 0.5, y: 0.5}
|
||||
spritePixelsToUnits: 100
|
||||
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
|
||||
spriteGenerateFallbackPhysicsShape: 1
|
||||
alphaUsage: 1
|
||||
alphaIsTransparency: 0
|
||||
spriteTessellationMethod: 0
|
||||
spriteTessellationDetail: -1
|
||||
spriteGeometrySubdivision: -1
|
||||
textureType: 0
|
||||
textureShape: 1
|
||||
singleChannelComponent: 0
|
||||
flipbookRows: 1
|
||||
flipbookColumns: 1
|
||||
maxTextureSizeSet: 0
|
||||
compressionQualitySet: 0
|
||||
textureFormatSet: 0
|
||||
ignorePngGamma: 0
|
||||
applyGammaDecoding: 0
|
||||
swizzle: 50462976
|
||||
cookieLightType: 0
|
||||
platformSettings:
|
||||
- serializedVersion: 4
|
||||
buildTarget: DefaultTexturePlatform
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: Standalone
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
- serializedVersion: 4
|
||||
buildTarget: WebGL
|
||||
maxTextureSize: 2048
|
||||
resizeAlgorithm: 0
|
||||
textureFormat: -1
|
||||
textureCompression: 1
|
||||
compressionQuality: 50
|
||||
crunchedCompression: 0
|
||||
allowsAlphaSplitting: 0
|
||||
overridden: 0
|
||||
ignorePlatformSupport: 0
|
||||
androidETC2FallbackOverride: 0
|
||||
forceMaximumCompressionQuality_BC6H_BC7: 0
|
||||
spriteSheet:
|
||||
serializedVersion: 2
|
||||
sprites: []
|
||||
outline: []
|
||||
customData:
|
||||
physicsShape: []
|
||||
bones: []
|
||||
spriteID:
|
||||
internalID: 0
|
||||
vertices: []
|
||||
indices:
|
||||
edges: []
|
||||
weights: []
|
||||
secondaryTextures: []
|
||||
spriteCustomMetadata:
|
||||
entries: []
|
||||
nameFileIdTable: {}
|
||||
mipmapLimitGroupName:
|
||||
pSDRemoveMatte: 0
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -54,12 +54,12 @@ namespace Vanguard.App
|
|||
camera.fieldOfView = theme == SceneTheme.Battle ? 50f : 52f;
|
||||
camera.transform.position = theme switch
|
||||
{
|
||||
SceneTheme.Battle => new Vector3(0f, 6.9f, -9.8f),
|
||||
SceneTheme.Battle => new Vector3(1.4f, 9f, -5.2f),
|
||||
SceneTheme.Login => new Vector3(0f, 2.4f, -9.5f),
|
||||
_ => new Vector3(0f, 3.2f, -10f),
|
||||
};
|
||||
camera.transform.rotation = theme == SceneTheme.Battle
|
||||
? Quaternion.Euler(35.2f, 0f, 0f)
|
||||
? Quaternion.Euler(60.5f, 0f, 0f)
|
||||
: Quaternion.Euler(8f, 0f, 0f);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,27 +36,21 @@ MonoBehaviour:
|
|||
handSpacing: 0.42
|
||||
handMaxWidth: 7.4
|
||||
handArc: 0.035
|
||||
handViewportY: 0.16
|
||||
handViewDistance: 3.6
|
||||
handViewMaxWidth: 2
|
||||
handViewScale: 0.9
|
||||
handViewportX: 0.44
|
||||
handViewportY: 0.18
|
||||
handViewDistance: 3.8
|
||||
handViewMaxWidth: 1.8
|
||||
handViewScale: 0.62
|
||||
handViewRoll: 3.2
|
||||
handViewArc: 0.02
|
||||
guardianRowGap: 0.55
|
||||
guardianSpacing: 0.44
|
||||
guardianMaxWidth: 3.6
|
||||
damageX: 3.5
|
||||
pileX: 3.55
|
||||
cameraPosition: {x: 0, y: 6.9, z: -9.8}
|
||||
cameraTarget: {x: 0, y: 0, z: -0.15}
|
||||
damageX: 3
|
||||
pileX: 2.95
|
||||
cameraPosition: {x: 1.4, y: 9, z: -5.2}
|
||||
cameraTarget: {x: 1.4, y: 0, z: -0.1}
|
||||
cameraFov: 50
|
||||
orbitYawLimit: 32
|
||||
orbitPitchMin: 20
|
||||
orbitPitchMax: 62
|
||||
zoomMin: 9.5
|
||||
zoomMax: 16
|
||||
zoomSpeed: 0.006
|
||||
panSpeed: 0.0035
|
||||
tableColor: {r: 0.045, g: 0.04, b: 0.055, a: 1}
|
||||
matSelfColor: {r: 0.055, g: 0.095, b: 0.16, a: 1}
|
||||
matOpponentColor: {r: 0.13, g: 0.065, b: 0.075, a: 1}
|
||||
|
|
|
|||
|
|
@ -50,12 +50,14 @@ namespace Vanguard.Board3D
|
|||
public float handArc = 0.035f;
|
||||
|
||||
[Header("Рука в поле зрения")]
|
||||
[Tooltip("Доля ширины экрана для центра карт: 0 — левый край, 0.5 — середина.")]
|
||||
[Range(0.05f, 0.95f)] public float handViewportX = 0.44f;
|
||||
[Tooltip("Доля высоты экрана для центра карт: 0 — низ, 0.5 — середина.")]
|
||||
[Range(0.05f, 0.45f)] public float handViewportY = 0.16f;
|
||||
[Range(0.05f, 0.45f)] public float handViewportY = 0.18f;
|
||||
[Tooltip("Расстояние от камеры до веера руки, метры.")]
|
||||
public float handViewDistance = 3.6f;
|
||||
public float handViewMaxWidth = 2.0f;
|
||||
public float handViewScale = 0.9f;
|
||||
public float handViewDistance = 3.8f;
|
||||
public float handViewMaxWidth = 1.8f;
|
||||
public float handViewScale = 0.62f;
|
||||
public float handViewRoll = 3.2f;
|
||||
public float handViewArc = 0.02f;
|
||||
|
||||
|
|
@ -66,21 +68,15 @@ namespace Vanguard.Board3D
|
|||
|
||||
[Header("Стопки")]
|
||||
[Tooltip("Расстояние от центра стола до зоны урона.")]
|
||||
public float damageX = 3.5f;
|
||||
public float damageX = 3.0f;
|
||||
[Tooltip("Расстояние от центра стола до колоды и дропа.")]
|
||||
public float pileX = 3.55f;
|
||||
public float pileX = 2.95f;
|
||||
|
||||
[Header("Камера")]
|
||||
public Vector3 cameraPosition = new Vector3(0f, 6.9f, -9.8f);
|
||||
public Vector3 cameraTarget = new Vector3(0f, 0f, -0.15f);
|
||||
[Tooltip("Фиксированная камера над полем. Не двигается игроком.")]
|
||||
public Vector3 cameraPosition = new Vector3(1.4f, 9.0f, -5.2f);
|
||||
public Vector3 cameraTarget = new Vector3(1.4f, 0f, -0.1f);
|
||||
public float cameraFov = 50f;
|
||||
public float orbitYawLimit = 32f;
|
||||
public float orbitPitchMin = 20f;
|
||||
public float orbitPitchMax = 62f;
|
||||
public float zoomMin = 9.5f;
|
||||
public float zoomMax = 16f;
|
||||
public float zoomSpeed = 0.006f;
|
||||
public float panSpeed = 0.0035f;
|
||||
|
||||
[Header("Палитра")]
|
||||
public Color tableColor = new Color(0.045f, 0.04f, 0.055f);
|
||||
|
|
@ -142,13 +138,12 @@ namespace Vanguard.Board3D
|
|||
handMaxWidth = Mathf.Max(1f, handMaxWidth);
|
||||
guardianMaxWidth = Mathf.Max(1f, guardianMaxWidth);
|
||||
handViewportY = Mathf.Clamp(handViewportY, 0.05f, 0.45f);
|
||||
handViewportX = Mathf.Clamp(handViewportX, 0.05f, 0.95f);
|
||||
handViewDistance = Mathf.Max(1f, handViewDistance);
|
||||
handViewMaxWidth = Mathf.Max(0.5f, handViewMaxWidth);
|
||||
handViewScale = Mathf.Clamp(handViewScale, 0.3f, 1.5f);
|
||||
handViewRoll = Mathf.Clamp(handViewRoll, 0f, 20f);
|
||||
handViewArc = Mathf.Max(0f, handViewArc);
|
||||
orbitPitchMax = Mathf.Clamp(orbitPitchMax, orbitPitchMin + 1f, 85f);
|
||||
zoomMax = Mathf.Max(zoomMin + 0.5f, zoomMax);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,10 @@ namespace Vanguard.Board3D
|
|||
|
||||
private static BoardArt SandboxArt(string printingId)
|
||||
{
|
||||
var texture = LoadSandboxTexture(printingId);
|
||||
if (texture != null)
|
||||
return new BoardArt(texture, Color.white, printingId);
|
||||
|
||||
var clan = printingId != null && printingId.Contains("002")
|
||||
? "Kagero"
|
||||
: "RoyalPaladin";
|
||||
|
|
@ -67,6 +71,36 @@ namespace Vanguard.Board3D
|
|||
printingId);
|
||||
}
|
||||
|
||||
private static Texture2D LoadSandboxTexture(string printingId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(printingId))
|
||||
return null;
|
||||
|
||||
var slash = printingId.IndexOf('/');
|
||||
if (slash <= 0 || slash >= printingId.Length - 1)
|
||||
return null;
|
||||
|
||||
var set = printingId.Substring(0, slash).ToLowerInvariant();
|
||||
var tail = printingId.Substring(slash + 1);
|
||||
var number = string.Empty;
|
||||
for (var i = 0; i < tail.Length; i++)
|
||||
{
|
||||
var c = tail[i];
|
||||
if (char.IsDigit(c) || (i == 0 && (c == 'S' || c == 's')))
|
||||
{
|
||||
number += char.ToLowerInvariant(c);
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (number.Length == 0)
|
||||
return null;
|
||||
|
||||
return Resources.Load<Texture2D>("CardArt/" + set + "_" + number);
|
||||
}
|
||||
|
||||
private static BoardRenderState BuildState()
|
||||
{
|
||||
var state = new BoardRenderState();
|
||||
|
|
|
|||
|
|
@ -1,132 +0,0 @@
|
|||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
namespace Vanguard.Board3D
|
||||
{
|
||||
/// <summary>
|
||||
/// Ограниченная орбита камеры: ПКМ — поворот, СКМ — панорама,
|
||||
/// колесо — зум. Левая кнопка остаётся картам и UI.
|
||||
/// </summary>
|
||||
public sealed class BoardCameraRig : MonoBehaviour
|
||||
{
|
||||
private PlaymatLayoutConfig _config;
|
||||
private Vector3 _target;
|
||||
private float _distance = 15f;
|
||||
private float _yaw;
|
||||
private float _pitch = 35f;
|
||||
private bool _orbiting;
|
||||
private bool _panning;
|
||||
private Vector2 _lastPointer;
|
||||
|
||||
public void Configure(PlaymatLayoutConfig config)
|
||||
{
|
||||
_config = config != null ? config : PlaymatLayoutConfig.Default;
|
||||
_target = _config.cameraTarget;
|
||||
var offset = _config.cameraPosition - _target;
|
||||
_distance = Mathf.Clamp(
|
||||
offset.magnitude,
|
||||
_config.zoomMin,
|
||||
_config.zoomMax);
|
||||
_pitch = Mathf.Clamp(
|
||||
Mathf.Asin(Mathf.Clamp(offset.y / Mathf.Max(0.001f, _distance), -1f, 1f))
|
||||
* Mathf.Rad2Deg,
|
||||
_config.orbitPitchMin,
|
||||
_config.orbitPitchMax);
|
||||
_yaw = Mathf.Atan2(-offset.x, -offset.z) * Mathf.Rad2Deg;
|
||||
_yaw = Mathf.Clamp(_yaw, -_config.orbitYawLimit, _config.orbitYawLimit);
|
||||
Apply();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (_config == null)
|
||||
return;
|
||||
|
||||
var mouse = Mouse.current;
|
||||
if (mouse == null)
|
||||
return;
|
||||
|
||||
var pointer = mouse.position.ReadValue();
|
||||
var overUi = IsPointerOverUi();
|
||||
|
||||
if (mouse.rightButton.wasPressedThisFrame && !overUi)
|
||||
{
|
||||
_orbiting = true;
|
||||
_lastPointer = pointer;
|
||||
}
|
||||
|
||||
if (mouse.rightButton.wasReleasedThisFrame)
|
||||
_orbiting = false;
|
||||
|
||||
if (_orbiting && mouse.rightButton.isPressed)
|
||||
{
|
||||
var delta = pointer - _lastPointer;
|
||||
_lastPointer = pointer;
|
||||
_yaw = Mathf.Clamp(
|
||||
_yaw + (delta.x * 0.18f),
|
||||
-_config.orbitYawLimit,
|
||||
_config.orbitYawLimit);
|
||||
_pitch = Mathf.Clamp(
|
||||
_pitch - (delta.y * 0.16f),
|
||||
_config.orbitPitchMin,
|
||||
_config.orbitPitchMax);
|
||||
Apply();
|
||||
}
|
||||
|
||||
if (mouse.middleButton.wasPressedThisFrame && !overUi)
|
||||
{
|
||||
_panning = true;
|
||||
_lastPointer = pointer;
|
||||
}
|
||||
|
||||
if (mouse.middleButton.wasReleasedThisFrame)
|
||||
_panning = false;
|
||||
|
||||
if (_panning && mouse.middleButton.isPressed)
|
||||
{
|
||||
var delta = pointer - _lastPointer;
|
||||
_lastPointer = pointer;
|
||||
var rotation = Quaternion.Euler(_pitch, _yaw, 0f);
|
||||
var right = rotation * Vector3.right;
|
||||
var forward = rotation * Vector3.forward;
|
||||
right.y = 0f;
|
||||
forward.y = 0f;
|
||||
right.Normalize();
|
||||
forward.Normalize();
|
||||
|
||||
var scale = _config.panSpeed * _distance;
|
||||
_target -= (right * (delta.x * scale)) + (forward * (delta.y * scale));
|
||||
_target.x = Mathf.Clamp(_target.x, -2.5f, 2.5f);
|
||||
_target.z = Mathf.Clamp(_target.z, -2f, 2f);
|
||||
Apply();
|
||||
}
|
||||
|
||||
var scroll = mouse.scroll.ReadValue().y;
|
||||
if (!overUi && Mathf.Abs(scroll) > 0.01f)
|
||||
{
|
||||
_distance = Mathf.Clamp(
|
||||
_distance - (scroll * _config.zoomSpeed),
|
||||
_config.zoomMin,
|
||||
_config.zoomMax);
|
||||
Apply();
|
||||
}
|
||||
}
|
||||
|
||||
private void Apply()
|
||||
{
|
||||
var rotation = Quaternion.Euler(_pitch, _yaw, 0f);
|
||||
transform.rotation = rotation;
|
||||
transform.position = _target - (rotation * Vector3.forward * _distance);
|
||||
var camera = GetComponent<Camera>();
|
||||
if (camera != null)
|
||||
camera.fieldOfView = _config.cameraFov;
|
||||
}
|
||||
|
||||
private static bool IsPointerOverUi()
|
||||
{
|
||||
var eventSystem = EventSystem.current;
|
||||
return eventSystem != null && eventSystem.IsPointerOverGameObject();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 30bd2e29295c0b241bb1f29e9dc73ca8
|
||||
|
|
@ -70,7 +70,7 @@ namespace Vanguard.Board3D
|
|||
var text = BoardText.Create(
|
||||
go.transform,
|
||||
label,
|
||||
0.14f,
|
||||
0.055f,
|
||||
new Color(1f, 1f, 1f, 0.34f),
|
||||
new Vector3(0f, 0.001f, 0f));
|
||||
text.name = "Slot Label";
|
||||
|
|
|
|||
|
|
@ -36,6 +36,10 @@ namespace Vanguard.Board3D
|
|||
{
|
||||
get
|
||||
{
|
||||
if (_back != null)
|
||||
return _back;
|
||||
|
||||
_back = Resources.Load<Texture2D>("CardArt/vanguard_back");
|
||||
if (_back == null)
|
||||
_back = BuildBack();
|
||||
return _back;
|
||||
|
|
|
|||
|
|
@ -43,8 +43,6 @@ namespace Vanguard.Board3D
|
|||
private Transform _pilesRoot;
|
||||
private Transform _zonesRoot;
|
||||
private Camera _camera;
|
||||
private BoardCameraRig _cameraRig;
|
||||
private bool _ownsRig;
|
||||
|
||||
private BoardZoneView _guardianZone;
|
||||
private BoardZoneView _dropIndicator;
|
||||
|
|
@ -159,8 +157,6 @@ namespace Vanguard.Board3D
|
|||
}
|
||||
|
||||
gameObject.SetActive(visible);
|
||||
if (_cameraRig != null)
|
||||
_cameraRig.enabled = visible;
|
||||
}
|
||||
|
||||
public void Render(MatchView view, BoardRenderState state)
|
||||
|
|
@ -271,7 +267,9 @@ namespace Vanguard.Board3D
|
|||
new Vector3(matWidth, 0.008f, matDepth),
|
||||
_config.matOpponentColor);
|
||||
|
||||
var divider = BoardMaterials.CreateLit(_config.lineColor, 1.2f);
|
||||
var divider = BoardMaterials.CreateUnlit(
|
||||
null,
|
||||
new Color(0.34f, 0.24f, 0.10f));
|
||||
CreateBox(
|
||||
"Center Line",
|
||||
new Vector3(0f, 0.011f, 0f),
|
||||
|
|
@ -339,8 +337,8 @@ namespace Vanguard.Board3D
|
|||
guardianDepth,
|
||||
Mathf.Max(0.022f, _config.cardWidth * 0.04f),
|
||||
_config.guardianColor,
|
||||
"GUARDIAN",
|
||||
0.11f,
|
||||
null,
|
||||
0f,
|
||||
-(guardianDepth * 0.5f) - 0.2f,
|
||||
new Color(0.72f, 0.92f, 1f, 0.75f),
|
||||
_config.highlightColor);
|
||||
|
|
@ -360,8 +358,8 @@ namespace Vanguard.Board3D
|
|||
2.25f,
|
||||
Mathf.Max(0.018f, _config.cardWidth * 0.03f),
|
||||
_config.damageColor,
|
||||
"DAMAGE",
|
||||
0.1f,
|
||||
null,
|
||||
0f,
|
||||
-zSign * 1.32f,
|
||||
new Color(1f, 0.78f, 0.76f, 0.75f),
|
||||
_config.highlightColor);
|
||||
|
|
@ -433,17 +431,11 @@ namespace Vanguard.Board3D
|
|||
if (_camera == null)
|
||||
return;
|
||||
|
||||
if (_cameraRig == null)
|
||||
{
|
||||
_cameraRig = _camera.GetComponent<BoardCameraRig>();
|
||||
if (_cameraRig == null)
|
||||
{
|
||||
_cameraRig = _camera.gameObject.AddComponent<BoardCameraRig>();
|
||||
_ownsRig = true;
|
||||
}
|
||||
}
|
||||
|
||||
_cameraRig.Configure(_config);
|
||||
_camera.transform.position = _config.cameraPosition;
|
||||
var direction = _config.cameraTarget - _config.cameraPosition;
|
||||
if (direction.sqrMagnitude > 0.0001f)
|
||||
_camera.transform.rotation = Quaternion.LookRotation(direction, Vector3.up);
|
||||
_camera.fieldOfView = _config.cameraFov;
|
||||
}
|
||||
|
||||
private void BuildSpecs(
|
||||
|
|
@ -496,7 +488,8 @@ namespace Vanguard.Board3D
|
|||
Art = ResolveArt(card.PrintingId),
|
||||
Position = _layout.CirclePosition(localSeat, slot),
|
||||
BaseRotation = Quaternion.Euler(0f, _layout.BaseYaw(localSeat), 0f),
|
||||
FaceUp = card.FaceUp,
|
||||
// Карты в кругах V/R всегда открыты, независимо от снимка.
|
||||
FaceUp = true,
|
||||
Rested = card.Rested,
|
||||
Highlight = state.Highlights.Contains(key)
|
||||
? _config.highlightColor
|
||||
|
|
@ -669,7 +662,9 @@ namespace Vanguard.Board3D
|
|||
var offset = index - (safeCount - 1) * 0.5f;
|
||||
var halfHeight = Mathf.Tan(
|
||||
_camera.fieldOfView * 0.5f * Mathf.Deg2Rad) * _config.handViewDistance;
|
||||
var x = offset * step;
|
||||
var halfWidth = halfHeight * Mathf.Max(0.1f, _camera.aspect);
|
||||
var x = ((_config.handViewportX - 0.5f) * 2f * halfWidth)
|
||||
+ (offset * step);
|
||||
var y = (_config.handViewportY - 0.5f) * 2f * halfHeight
|
||||
+ (1f - Mathf.Abs(offset) / safeCount) * _config.handViewArc;
|
||||
var z = _config.handViewDistance
|
||||
|
|
@ -725,22 +720,11 @@ namespace Vanguard.Board3D
|
|||
UpdatePileSide(self, _deckSelf, _dropSelf, _soulSelf);
|
||||
UpdatePileSide(opponent, _deckOpponent, _dropOpponent, _soulOpponent);
|
||||
|
||||
var damageSelf = self != null
|
||||
? self.FaceDownDamage + (self.FaceUpDamage?.Count ?? 0)
|
||||
: 0;
|
||||
var damageOpponent = opponent != null
|
||||
? opponent.FaceDownDamage + (opponent.FaceUpDamage?.Count ?? 0)
|
||||
: 0;
|
||||
_damageZones[0]?.SetLabel("DAMAGE " + damageSelf);
|
||||
_damageZones[1]?.SetLabel("DAMAGE " + damageOpponent);
|
||||
_damageZones[0]?.SetLabel(string.Empty);
|
||||
_damageZones[1]?.SetLabel(string.Empty);
|
||||
|
||||
if (_guardianZone != null)
|
||||
{
|
||||
var shield = view.Battle != null ? view.Battle.GuardShield : 0;
|
||||
var checks = view.Battle != null ? view.Battle.ChecksRemaining : 0;
|
||||
_guardianZone.SetLabel(
|
||||
"GUARDIAN SHIELD " + shield + " CHECK " + checks);
|
||||
}
|
||||
_guardianZone.SetLabel(string.Empty);
|
||||
}
|
||||
|
||||
private void UpdatePileSide(
|
||||
|
|
@ -1214,14 +1198,6 @@ namespace Vanguard.Board3D
|
|||
{
|
||||
if (_instance == this)
|
||||
_instance = null;
|
||||
|
||||
if (_cameraRig != null && _ownsRig)
|
||||
{
|
||||
if (Application.isPlaying)
|
||||
Destroy(_cameraRig);
|
||||
else
|
||||
DestroyImmediate(_cameraRig);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class HandCardBinding
|
||||
|
|
|
|||
|
|
@ -73,12 +73,15 @@ namespace Vanguard.Board3D
|
|||
_top.sharedMaterial = BoardMaterials.White;
|
||||
BoardMaterials.SetShadows(_top, false);
|
||||
|
||||
if (Zone != BoardZone.Soul)
|
||||
{
|
||||
_label = BoardText.Create(
|
||||
transform,
|
||||
title,
|
||||
0.115f,
|
||||
0.05f,
|
||||
new Color(0.86f, 0.90f, 1f),
|
||||
new Vector3(0f, 0.02f, -(height * 0.78f)));
|
||||
}
|
||||
|
||||
var collider = gameObject.AddComponent<BoxCollider>();
|
||||
collider.size = new Vector3(width * 1.06f, 0.16f, height * 1.06f);
|
||||
|
|
@ -128,7 +131,7 @@ namespace Vanguard.Board3D
|
|||
_top.SetPropertyBlock(_block);
|
||||
}
|
||||
|
||||
BoardText.SetText(_label, title + " " + safe);
|
||||
BoardText.SetText(_label, title);
|
||||
}
|
||||
|
||||
private static void RemoveCollider(GameObject target)
|
||||
|
|
|
|||
|
|
@ -80,6 +80,8 @@ namespace Vanguard.UI
|
|||
private RectTransform _soulOverlay;
|
||||
private RectTransform _soulContent;
|
||||
private Text _soulTitle;
|
||||
private Text _selfStatsText;
|
||||
private Text _opponentStatsText;
|
||||
|
||||
public BattleView(Transform parent, MetaService meta, Action onExit)
|
||||
{
|
||||
|
|
@ -129,7 +131,7 @@ namespace Vanguard.UI
|
|||
right.anchorMin = new Vector2(1f, 0f);
|
||||
right.anchorMax = new Vector2(1f, 1f);
|
||||
right.pivot = new Vector2(1f, 0.5f);
|
||||
right.offsetMin = new Vector2(-528f, 28f);
|
||||
right.offsetMin = new Vector2(-456f, 28f);
|
||||
right.offsetMax = new Vector2(-36f, -108f);
|
||||
UiKit.AddVerticalLayout(right, 14, 10);
|
||||
|
||||
|
|
@ -188,6 +190,7 @@ namespace Vanguard.UI
|
|||
_feedback.gameObject.SetActive(false);
|
||||
|
||||
BuildSoulOverlay();
|
||||
BuildStatsOverlays();
|
||||
|
||||
_board3D = GameBoard3D.Create(null, PlaymatLayoutConfig.Default);
|
||||
_board3D.ArtProvider = ResolveBoardArt;
|
||||
|
|
@ -234,6 +237,10 @@ namespace Vanguard.UI
|
|||
_topInfo.text = (info.DeckName ?? string.Empty) + " · " + info.MatchId;
|
||||
_soulCounterLabel.text = "Душа: 0";
|
||||
_soulCounter.interactable = false;
|
||||
if (_selfStatsText != null)
|
||||
_selfStatsText.text = string.Empty;
|
||||
if (_opponentStatsText != null)
|
||||
_opponentStatsText.text = string.Empty;
|
||||
SetStatus("Подключаемся к матчу…", Muted);
|
||||
_root.gameObject.SetActive(true);
|
||||
_ = LoadCatalogAsync();
|
||||
|
|
@ -773,6 +780,7 @@ namespace Vanguard.UI
|
|||
_board3D?.SetSelfPlayer(_view.You);
|
||||
_board3D?.Render(_view, BuildBoardRenderState());
|
||||
UpdateSoulCounter();
|
||||
UpdateStatsOverlay();
|
||||
|
||||
RebuildIntents(_view);
|
||||
UpdateClock();
|
||||
|
|
@ -848,9 +856,16 @@ namespace Vanguard.UI
|
|||
_catalog.TryGetValue(printingId, out var meta);
|
||||
var clan = meta != null ? meta.Clan : "None";
|
||||
var nameId = meta != null ? meta.NameId : printingId;
|
||||
var sprite = CardImageService.Resolve(
|
||||
meta != null ? meta.ImageKey : null,
|
||||
CardArt.ForCard(nameId ?? printingId ?? "card", clan));
|
||||
var sprite = CardImageService.Resolve(meta != null ? meta.ImageKey : null, null);
|
||||
if (sprite == null)
|
||||
{
|
||||
sprite = CardImageService.Resolve(
|
||||
CardImageService.ImageKeyForPrinting(printingId),
|
||||
null);
|
||||
}
|
||||
|
||||
if (sprite == null)
|
||||
sprite = CardArt.ForCard(nameId ?? printingId ?? "card", clan);
|
||||
return new BoardArt(
|
||||
sprite != null ? sprite.texture : null,
|
||||
CardArt.ClanColor(clan),
|
||||
|
|
@ -1023,6 +1038,86 @@ namespace Vanguard.UI
|
|||
}
|
||||
}
|
||||
|
||||
private void BuildStatsOverlays()
|
||||
{
|
||||
_selfStatsText = CreateStatsPanel(
|
||||
"SelfStats",
|
||||
new Vector2(0f, 0f),
|
||||
new Vector2(0f, 0f),
|
||||
new Vector2(30f, 24f),
|
||||
TextAnchor.MiddleLeft);
|
||||
_opponentStatsText = CreateStatsPanel(
|
||||
"OpponentStats",
|
||||
new Vector2(1f, 1f),
|
||||
new Vector2(1f, 1f),
|
||||
new Vector2(-488f, -108f),
|
||||
TextAnchor.MiddleRight);
|
||||
}
|
||||
|
||||
private Text CreateStatsPanel(
|
||||
string name,
|
||||
Vector2 anchor,
|
||||
Vector2 pivot,
|
||||
Vector2 position,
|
||||
TextAnchor alignment)
|
||||
{
|
||||
var panel = UiKit.CreatePanel(
|
||||
_root,
|
||||
name,
|
||||
new Color(0.02f, 0.035f, 0.055f, 0.72f));
|
||||
panel.anchorMin = anchor;
|
||||
panel.anchorMax = anchor;
|
||||
panel.pivot = pivot;
|
||||
panel.anchoredPosition = position;
|
||||
panel.sizeDelta = new Vector2(360f, 58f);
|
||||
|
||||
var image = panel.GetComponent<Image>();
|
||||
if (image != null)
|
||||
image.raycastTarget = false;
|
||||
|
||||
var text = UiKit.CreateText(
|
||||
panel,
|
||||
"Stats",
|
||||
string.Empty,
|
||||
14,
|
||||
new Color(0.86f, 0.91f, 1f),
|
||||
alignment);
|
||||
UiKit.Stretch(text.rectTransform);
|
||||
text.rectTransform.offsetMin = new Vector2(12f, 5f);
|
||||
text.rectTransform.offsetMax = new Vector2(-12f, -5f);
|
||||
text.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||
text.verticalOverflow = VerticalWrapMode.Overflow;
|
||||
UiKit.MakeLiteral(text);
|
||||
return text;
|
||||
}
|
||||
|
||||
private void UpdateStatsOverlay()
|
||||
{
|
||||
if (_view == null || _selfStatsText == null || _opponentStatsText == null)
|
||||
return;
|
||||
|
||||
var self = _view.Players.FirstOrDefault(player => player.Index == _view.You);
|
||||
var opponent = _view.Players.FirstOrDefault(player => player.Index != _view.You);
|
||||
_selfStatsText.text = FormatStats(self, selfSide: true);
|
||||
_opponentStatsText.text = FormatStats(opponent, selfSide: false);
|
||||
}
|
||||
|
||||
private static string FormatStats(PlayerView player, bool selfSide)
|
||||
{
|
||||
var title = Loc.Translate(selfSide ? "ВЫ" : "ПРОТИВНИК");
|
||||
if (player == null)
|
||||
return title;
|
||||
|
||||
var damage = player.FaceDownDamage + (player.FaceUpDamage?.Count ?? 0);
|
||||
var hand = Mathf.Max(player.HandCount, player.Hand?.Count ?? 0);
|
||||
return title + " · " + player.Name + "\n"
|
||||
+ Loc.Translate("Рука") + " " + hand
|
||||
+ " " + Loc.Translate("Колода") + " " + player.DeckCount
|
||||
+ " " + Loc.Translate("ДРОП") + " " + (player.Drop?.Count ?? 0)
|
||||
+ " " + Loc.Translate("УРОН") + " " + damage
|
||||
+ " " + Loc.Translate("Душа") + " " + (player.Soul?.Count ?? 0);
|
||||
}
|
||||
|
||||
private void BuildSoulOverlay()
|
||||
{
|
||||
_soulOverlay = UiKit.CreatePanel(_root, "SoulOverlay", new Color(0f, 0f, 0f, 0.74f));
|
||||
|
|
|
|||
|
|
@ -85,6 +85,17 @@ namespace Vanguard.UI
|
|||
if (_back != null)
|
||||
return _back;
|
||||
|
||||
var texture = Resources.Load<Texture2D>("CardArt/vanguard_back");
|
||||
if (texture != null)
|
||||
{
|
||||
_back = Sprite.Create(
|
||||
texture,
|
||||
new Rect(0f, 0f, texture.width, texture.height),
|
||||
new Vector2(0.5f, 0.5f),
|
||||
100f);
|
||||
return _back;
|
||||
}
|
||||
|
||||
_back = BackTexture();
|
||||
return _back;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,5 +51,36 @@ namespace Vanguard.UI
|
|||
Cache[imageKey] = sprite;
|
||||
return sprite;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// "BT01/042EN" → "bt01_042". Нужно, когда каталог ещё не загрузился,
|
||||
/// а арт уже лежит в Resources/CardArt.
|
||||
/// </summary>
|
||||
public static string ImageKeyForPrinting(string printingId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(printingId))
|
||||
return null;
|
||||
|
||||
var slash = printingId.IndexOf('/');
|
||||
if (slash <= 0 || slash >= printingId.Length - 1)
|
||||
return null;
|
||||
|
||||
var set = printingId.Substring(0, slash).ToLowerInvariant();
|
||||
var tail = printingId.Substring(slash + 1);
|
||||
var number = string.Empty;
|
||||
for (var i = 0; i < tail.Length; i++)
|
||||
{
|
||||
var c = tail[i];
|
||||
if (char.IsDigit(c) || (i == 0 && (c == 'S' || c == 's')))
|
||||
{
|
||||
number += char.ToLowerInvariant(c);
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return number.Length > 0 ? set + "_" + number : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ namespace Vanguard.UI
|
|||
["нет"] = "no",
|
||||
["Вы: "] = "You: ",
|
||||
["ДРОП"] = "DROP",
|
||||
["Душа"] = "Soul",
|
||||
["ДУША"] = "SOUL",
|
||||
["Кадр"] = "Frame",
|
||||
["Клан"] = "Clan",
|
||||
["Пыль"] = "Dust",
|
||||
|
|
|
|||
|
|
@ -66,6 +66,9 @@ namespace Vanguard.Tests
|
|||
Assert.IsNotNull(
|
||||
Resources.Load<Texture2D>("CardArt/td01_005"),
|
||||
"TD01 card art is not imported");
|
||||
var standardBack = Resources.Load<Texture2D>("CardArt/vanguard_back");
|
||||
Assert.IsNotNull(standardBack, "Standard Vanguard card back is not imported");
|
||||
Assert.AreEqual(standardBack, CardArt.Back.texture);
|
||||
|
||||
var artGo = new GameObject("ArtProbe", typeof(RectTransform), typeof(Image));
|
||||
var artImage = artGo.GetComponent<Image>();
|
||||
|
|
@ -234,6 +237,20 @@ namespace Vanguard.Tests
|
|||
card.Ref.Zone == BoardZone.Guardian
|
||||
&& card.Ref.InstanceId == 7));
|
||||
|
||||
var selfStats = Resources.FindObjectsOfTypeAll<Text>()
|
||||
.FirstOrDefault(text => text.name == "Stats"
|
||||
&& text.transform.parent != null
|
||||
&& text.transform.parent.name == "SelfStats");
|
||||
var opponentStats = Resources.FindObjectsOfTypeAll<Text>()
|
||||
.FirstOrDefault(text => text.name == "Stats"
|
||||
&& text.transform.parent != null
|
||||
&& text.transform.parent.name == "OpponentStats");
|
||||
Assert.IsNotNull(selfStats);
|
||||
Assert.IsNotNull(opponentStats);
|
||||
StringAssert.Contains("Рука 1", selfStats.text);
|
||||
StringAssert.Contains("Душа 1", selfStats.text);
|
||||
StringAssert.Contains("Рука 0", opponentStats.text);
|
||||
|
||||
Cleanup();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -840,7 +840,8 @@ Battle; Boot и общие сервисы остаются персистент
|
|||
- Раскладка: локальный игрок всегда внизу; передний ряд `RC–V–RC`, задний
|
||||
`RC–RC–RC`; у противника зеркально. Между половинами — общий круг защиты
|
||||
(карты обоих игроков и текущий Guard). Душа — стопка под V, колода и дроп
|
||||
сбоку, урон — две колонки по три карты на боку.
|
||||
сбоку, урон — две колонки по три карты на боку. Карты в V/R всегда
|
||||
рисуются лицом вверх, даже если снимок пришёл с `FaceUp=false`.
|
||||
- `PlaymatLayoutConfig` (ScriptableObject): размеры карт, позиции рядов,
|
||||
высоты, рука, камера, палитра. Ассет:
|
||||
`Runtime/Board3D/Config/PlaymatLayoutConfig.asset`.
|
||||
|
|
@ -850,16 +851,21 @@ Battle; Boot и общие сервисы остаются персистент
|
|||
- `Card3D` — тонкая коробка: отдельные лицо/рубашка/торец, коллайдер, арт из
|
||||
`CardImageService`, переворот вокруг X, Rest вокруг Y по часовой стрелке,
|
||||
урон — против часовой, плавный снап. Карты пулятся (`GameBoard3D`),
|
||||
материалы общие, instancing включён.
|
||||
материалы общие, instancing включён. Рубашка — стандартная VF Vanguard
|
||||
`Resources/CardArt/vanguard_back.png` (источник: Card Sleeve Gallery,
|
||||
`Sleeve_06.png`), общая с 2D-UI; при отсутствии файла остаётся
|
||||
процедурный fallback.
|
||||
- `GameBoard3D` строит стол, круги, зоны, стопки и веер руки; raycast-клики,
|
||||
drag&drop на плоскость стола с подъёмом, подсветкой допустимых кругов и
|
||||
индикатором точки броска. Ошибка интента возвращает карту на место.
|
||||
- Камера приближена: позиция `(0, 6.9, -9.8)`, цель `(0, 0, -0.15)`, FOV 50.
|
||||
Рука не лежит на столе, а закреплена в экране снизу по центру
|
||||
(`handViewportY`, `handViewDistance`) и всегда развёрнута лицом к игроку;
|
||||
при орбите камеры веер следует за полем зрения, драг из руки уходит на стол.
|
||||
- `BoardCameraRig` — ПКМ орбита (±32°), СКМ панорама, колесо зум
|
||||
(9.5–16); ограничения в конфиге.
|
||||
- Камера фиксирована над полем: позиция `(1.4, 9, -5.2)`, цель
|
||||
`(1.4, 0, -0.1)`, FOV 50; игрок её не двигает. Рука не лежит на столе,
|
||||
а закреплена в экране снизу по центру (`handViewportX/Y`,
|
||||
`handViewDistance`) и всегда развёрнута лицом к игроку; драг из руки
|
||||
уходит на стол.
|
||||
- Статистика карт вынесена в отдельные UI-панели: своя — снизу слева,
|
||||
противника — сверху справа (`HAND/DECK/DROP/DMG/SOUL`); 3D-подписи
|
||||
стопок и зон больше не дублируют числа.
|
||||
- Душа: клик по стопке или кнопка «Душа: n» в верхней панели открывает
|
||||
оверлей со списком карт.
|
||||
- Песочница: `Assets/Vanguard/Scenes/GameBoard3D.unity` +
|
||||
|
|
|
|||
|
|
@ -65,15 +65,19 @@
|
|||
остаются uGUI. Карты — тонкие 3D-объекты с артом, рубашкой, торцом и
|
||||
коллайдером. Rest (атака/поддержка) — поворот по часовой стрелке вокруг Y,
|
||||
урон лежит на боку против часовой; закрытость — переворот вокруг X.
|
||||
Рука закреплена в поле зрения камеры снизу по центру и всегда смотрит
|
||||
на игрока, а не лежит на столе. Драг-н-дроп на плоскость стола с
|
||||
подсветкой целей и плавным снапом; камера приближена.
|
||||
Рубашка — стандартная VF Vanguard (`Resources/CardArt/vanguard_back.png`),
|
||||
общая для 2D и 3D. Рука закреплена в поле зрения камеры снизу по центру
|
||||
и всегда смотрит на игрока, а не лежит на столе. Камера фиксирована над
|
||||
полем под небольшим углом и не двигается игроком. Драг-н-дроп на
|
||||
плоскость стола с подсветкой целей и плавным снапом.
|
||||
- Поле: у каждого игрока два ряда `R V R` / `R R R` (V + 5 RC), у
|
||||
противника зеркально; между полями общий круг защиты; душа — стопка под
|
||||
V; колода, дроп и урон — 3D-стопки/карты. Локальный игрок всегда снизу,
|
||||
даже если он за вторым местом. ПКМ — орбита, СКМ — панорама, колесо —
|
||||
зум. Кнопка «Душа: n» в верхней панели и клик по стопке души открывают
|
||||
список карт души.
|
||||
даже если он за вторым местом. Карты в кругах V/R принудительно
|
||||
показываются лицом вверх.
|
||||
- Статистика карт — отдельное UI-наложение: своя панель снизу слева,
|
||||
противника — сверху справа. Кнопка «Душа: n» в верхней панели и клик по
|
||||
стопке души открывают список карт души.
|
||||
- Зона урона: карты лежат открытыми и на боку; после Counter Blast
|
||||
выбранная карта переворачивается рубашкой. Heal выбирает карту урона;
|
||||
Counter Blast — открытую карту до активации способности.
|
||||
|
|
@ -95,7 +99,7 @@
|
|||
открывается из Lobby кнопкой «Коллекция» (сцена `DeckBuilder`).
|
||||
- Не готово: 3D-модели карт/стола (пока процедурные примитивы и арты),
|
||||
звук, настройки (язык уже есть), DOTween, drag&drop и live-валидация
|
||||
декбилдера. Камера боя — ограниченная орбита; анимации retire/ghost для
|
||||
декбилдера. Камера боя фиксирована; анимации retire/ghost для
|
||||
3D-карт ещё не перенесены.
|
||||
|
||||
## Запуск
|
||||
|
|
@ -170,8 +174,8 @@ client/Assets/Vanguard/Runtime/Board3D/
|
|||
Logic/BoardPlacementRules.cs Ride/Call/Move/Guard
|
||||
View/GameBoard3D.cs стол, круги, пул карт, drag&drop
|
||||
View/Card3D.cs карта: лицо/рубашка/торец, Rest, флип
|
||||
View/BoardCameraRig.cs орбита/панорама/зум
|
||||
PlaymatSandbox.cs демо без сервера
|
||||
client/Assets/Vanguard/Resources/CardArt/vanguard_back.png рубашка VF
|
||||
client/Assets/Vanguard/Scenes/GameBoard3D.unity песочница поля
|
||||
client/Assets/Vanguard/Runtime/Battle/GameConnection.cs
|
||||
client/Assets/Vanguard/Runtime/Api/ApiClient.cs + ApiContracts.cs
|
||||
|
|
|
|||
Loading…
Reference in New Issue