diff --git a/client/Assets/Vanguard/Resources/CardArt/vanguard_back.png b/client/Assets/Vanguard/Resources/CardArt/vanguard_back.png new file mode 100644 index 0000000..9d0442c Binary files /dev/null and b/client/Assets/Vanguard/Resources/CardArt/vanguard_back.png differ diff --git a/client/Assets/Vanguard/Resources/CardArt/vanguard_back.png.meta b/client/Assets/Vanguard/Resources/CardArt/vanguard_back.png.meta new file mode 100644 index 0000000..cb6d930 --- /dev/null +++ b/client/Assets/Vanguard/Resources/CardArt/vanguard_back.png.meta @@ -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: diff --git a/client/Assets/Vanguard/Runtime/App/SceneEnvironment.cs b/client/Assets/Vanguard/Runtime/App/SceneEnvironment.cs index 8459a8a..9ff1c57 100644 --- a/client/Assets/Vanguard/Runtime/App/SceneEnvironment.cs +++ b/client/Assets/Vanguard/Runtime/App/SceneEnvironment.cs @@ -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); } diff --git a/client/Assets/Vanguard/Runtime/Board3D/Config/PlaymatLayoutConfig.asset b/client/Assets/Vanguard/Runtime/Board3D/Config/PlaymatLayoutConfig.asset index f19a043..256ec59 100644 --- a/client/Assets/Vanguard/Runtime/Board3D/Config/PlaymatLayoutConfig.asset +++ b/client/Assets/Vanguard/Runtime/Board3D/Config/PlaymatLayoutConfig.asset @@ -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} diff --git a/client/Assets/Vanguard/Runtime/Board3D/Config/PlaymatLayoutConfig.cs b/client/Assets/Vanguard/Runtime/Board3D/Config/PlaymatLayoutConfig.cs index 7af84eb..1ee6b13 100644 --- a/client/Assets/Vanguard/Runtime/Board3D/Config/PlaymatLayoutConfig.cs +++ b/client/Assets/Vanguard/Runtime/Board3D/Config/PlaymatLayoutConfig.cs @@ -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); } } } diff --git a/client/Assets/Vanguard/Runtime/Board3D/PlaymatSandbox.cs b/client/Assets/Vanguard/Runtime/Board3D/PlaymatSandbox.cs index 668fee1..fe4b616 100644 --- a/client/Assets/Vanguard/Runtime/Board3D/PlaymatSandbox.cs +++ b/client/Assets/Vanguard/Runtime/Board3D/PlaymatSandbox.cs @@ -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("CardArt/" + set + "_" + number); + } + private static BoardRenderState BuildState() { var state = new BoardRenderState(); diff --git a/client/Assets/Vanguard/Runtime/Board3D/View/BoardCameraRig.cs b/client/Assets/Vanguard/Runtime/Board3D/View/BoardCameraRig.cs deleted file mode 100644 index 36b8899..0000000 --- a/client/Assets/Vanguard/Runtime/Board3D/View/BoardCameraRig.cs +++ /dev/null @@ -1,132 +0,0 @@ -using UnityEngine; -using UnityEngine.EventSystems; -using UnityEngine.InputSystem; - -namespace Vanguard.Board3D -{ - /// - /// Ограниченная орбита камеры: ПКМ — поворот, СКМ — панорама, - /// колесо — зум. Левая кнопка остаётся картам и UI. - /// - 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(); - if (camera != null) - camera.fieldOfView = _config.cameraFov; - } - - private static bool IsPointerOverUi() - { - var eventSystem = EventSystem.current; - return eventSystem != null && eventSystem.IsPointerOverGameObject(); - } - } -} diff --git a/client/Assets/Vanguard/Runtime/Board3D/View/BoardCameraRig.cs.meta b/client/Assets/Vanguard/Runtime/Board3D/View/BoardCameraRig.cs.meta deleted file mode 100644 index 7bfc876..0000000 --- a/client/Assets/Vanguard/Runtime/Board3D/View/BoardCameraRig.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 30bd2e29295c0b241bb1f29e9dc73ca8 \ No newline at end of file diff --git a/client/Assets/Vanguard/Runtime/Board3D/View/BoardSlotView.cs b/client/Assets/Vanguard/Runtime/Board3D/View/BoardSlotView.cs index c7f1504..2e2cdd0 100644 --- a/client/Assets/Vanguard/Runtime/Board3D/View/BoardSlotView.cs +++ b/client/Assets/Vanguard/Runtime/Board3D/View/BoardSlotView.cs @@ -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"; diff --git a/client/Assets/Vanguard/Runtime/Board3D/View/BoardTextures.cs b/client/Assets/Vanguard/Runtime/Board3D/View/BoardTextures.cs index f4f11bd..775b126 100644 --- a/client/Assets/Vanguard/Runtime/Board3D/View/BoardTextures.cs +++ b/client/Assets/Vanguard/Runtime/Board3D/View/BoardTextures.cs @@ -36,6 +36,10 @@ namespace Vanguard.Board3D { get { + if (_back != null) + return _back; + + _back = Resources.Load("CardArt/vanguard_back"); if (_back == null) _back = BuildBack(); return _back; diff --git a/client/Assets/Vanguard/Runtime/Board3D/View/GameBoard3D.cs b/client/Assets/Vanguard/Runtime/Board3D/View/GameBoard3D.cs index 79ee3ff..2e9d1b0 100644 --- a/client/Assets/Vanguard/Runtime/Board3D/View/GameBoard3D.cs +++ b/client/Assets/Vanguard/Runtime/Board3D/View/GameBoard3D.cs @@ -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(); - if (_cameraRig == null) - { - _cameraRig = _camera.gameObject.AddComponent(); - _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 diff --git a/client/Assets/Vanguard/Runtime/Board3D/View/Pile3D.cs b/client/Assets/Vanguard/Runtime/Board3D/View/Pile3D.cs index 8be69f6..2085870 100644 --- a/client/Assets/Vanguard/Runtime/Board3D/View/Pile3D.cs +++ b/client/Assets/Vanguard/Runtime/Board3D/View/Pile3D.cs @@ -73,12 +73,15 @@ namespace Vanguard.Board3D _top.sharedMaterial = BoardMaterials.White; BoardMaterials.SetShadows(_top, false); - _label = BoardText.Create( - transform, - title, - 0.115f, - new Color(0.86f, 0.90f, 1f), - new Vector3(0f, 0.02f, -(height * 0.78f))); + if (Zone != BoardZone.Soul) + { + _label = BoardText.Create( + transform, + title, + 0.05f, + new Color(0.86f, 0.90f, 1f), + new Vector3(0f, 0.02f, -(height * 0.78f))); + } var collider = gameObject.AddComponent(); 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) diff --git a/client/Assets/Vanguard/Runtime/UI/BattleView.cs b/client/Assets/Vanguard/Runtime/UI/BattleView.cs index 2502b9f..10d0b6e 100644 --- a/client/Assets/Vanguard/Runtime/UI/BattleView.cs +++ b/client/Assets/Vanguard/Runtime/UI/BattleView.cs @@ -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(); + 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)); diff --git a/client/Assets/Vanguard/Runtime/UI/CardArt.cs b/client/Assets/Vanguard/Runtime/UI/CardArt.cs index 2efac2e..1bf14b4 100644 --- a/client/Assets/Vanguard/Runtime/UI/CardArt.cs +++ b/client/Assets/Vanguard/Runtime/UI/CardArt.cs @@ -85,6 +85,17 @@ namespace Vanguard.UI if (_back != null) return _back; + var texture = Resources.Load("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; } diff --git a/client/Assets/Vanguard/Runtime/UI/CardImageService.cs b/client/Assets/Vanguard/Runtime/UI/CardImageService.cs index cfaba86..5881c4b 100644 --- a/client/Assets/Vanguard/Runtime/UI/CardImageService.cs +++ b/client/Assets/Vanguard/Runtime/UI/CardImageService.cs @@ -51,5 +51,36 @@ namespace Vanguard.UI Cache[imageKey] = sprite; return sprite; } + + /// + /// "BT01/042EN" → "bt01_042". Нужно, когда каталог ещё не загрузился, + /// а арт уже лежит в Resources/CardArt. + /// + 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; + } } } diff --git a/client/Assets/Vanguard/Runtime/UI/Localization.cs b/client/Assets/Vanguard/Runtime/UI/Localization.cs index 9ba8c7e..45dc4b5 100644 --- a/client/Assets/Vanguard/Runtime/UI/Localization.cs +++ b/client/Assets/Vanguard/Runtime/UI/Localization.cs @@ -38,6 +38,8 @@ namespace Vanguard.UI ["нет"] = "no", ["Вы: "] = "You: ", ["ДРОП"] = "DROP", + ["Душа"] = "Soul", + ["ДУША"] = "SOUL", ["Кадр"] = "Frame", ["Клан"] = "Clan", ["Пыль"] = "Dust", diff --git a/client/Assets/Vanguard/Tests/Editor/ClientSmokeTests.cs b/client/Assets/Vanguard/Tests/Editor/ClientSmokeTests.cs index 3b72935..59cfaa4 100644 --- a/client/Assets/Vanguard/Tests/Editor/ClientSmokeTests.cs +++ b/client/Assets/Vanguard/Tests/Editor/ClientSmokeTests.cs @@ -66,6 +66,9 @@ namespace Vanguard.Tests Assert.IsNotNull( Resources.Load("CardArt/td01_005"), "TD01 card art is not imported"); + var standardBack = Resources.Load("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(); @@ -234,6 +237,20 @@ namespace Vanguard.Tests card.Ref.Zone == BoardZone.Guardian && card.Ref.InstanceId == 7)); + var selfStats = Resources.FindObjectsOfTypeAll() + .FirstOrDefault(text => text.name == "Stats" + && text.transform.parent != null + && text.transform.parent.name == "SelfStats"); + var opponentStats = Resources.FindObjectsOfTypeAll() + .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(); } diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 21422db..5765b08 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -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` + diff --git a/docs/SESSION-STATE.md b/docs/SESSION-STATE.md index 410ff51..3af10d2 100644 --- a/docs/SESSION-STATE.md +++ b/docs/SESSION-STATE.md @@ -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