From e3243e58b6c2c1af99f689221281d74610270319 Mon Sep 17 00:00:00 2001 From: MpakobecEx Date: Thu, 22 Oct 2026 20:07:57 +0500 Subject: [PATCH] Refine board camera, hand, piles and card backs --- .../Vanguard/Runtime/App/SceneEnvironment.cs | 6 +- .../Board3D/Config/PlaymatLayoutConfig.asset | 14 ++-- .../Board3D/Config/PlaymatLayoutConfig.cs | 14 ++-- .../Runtime/Board3D/Logic/PlaymatLayout.cs | 4 +- .../Runtime/Board3D/View/BoardMeshLibrary.cs | 22 ++---- .../Runtime/Board3D/View/GameBoard3D.cs | 23 +++++- .../Vanguard/Runtime/Board3D/View/Pile3D.cs | 2 +- .../Assets/Vanguard/Runtime/UI/BattleView.cs | 77 ++++++++++--------- .../Tests/Editor/PlaymatLayoutTests.cs | 15 ++++ docs/HANDOFF.md | 19 +++-- docs/SESSION-STATE.md | 17 ++-- 11 files changed, 125 insertions(+), 88 deletions(-) diff --git a/client/Assets/Vanguard/Runtime/App/SceneEnvironment.cs b/client/Assets/Vanguard/Runtime/App/SceneEnvironment.cs index 9ff1c57..41e06a3 100644 --- a/client/Assets/Vanguard/Runtime/App/SceneEnvironment.cs +++ b/client/Assets/Vanguard/Runtime/App/SceneEnvironment.cs @@ -51,15 +51,15 @@ namespace Vanguard.App camera.clearFlags = CameraClearFlags.SolidColor; camera.backgroundColor = palette.Background; - camera.fieldOfView = theme == SceneTheme.Battle ? 50f : 52f; + camera.fieldOfView = theme == SceneTheme.Battle ? 46f : 52f; camera.transform.position = theme switch { - SceneTheme.Battle => new Vector3(1.4f, 9f, -5.2f), + SceneTheme.Battle => new Vector3(1.4f, 5.8f, -8f), SceneTheme.Login => new Vector3(0f, 2.4f, -9.5f), _ => new Vector3(0f, 3.2f, -10f), }; camera.transform.rotation = theme == SceneTheme.Battle - ? Quaternion.Euler(60.5f, 0f, 0f) + ? Quaternion.Euler(35.9f, 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 256ec59..c49ff7c 100644 --- a/client/Assets/Vanguard/Runtime/Board3D/Config/PlaymatLayoutConfig.asset +++ b/client/Assets/Vanguard/Runtime/Board3D/Config/PlaymatLayoutConfig.asset @@ -37,10 +37,10 @@ MonoBehaviour: handMaxWidth: 7.4 handArc: 0.035 handViewportX: 0.44 - handViewportY: 0.18 - handViewDistance: 3.8 - handViewMaxWidth: 1.8 - handViewScale: 0.62 + handViewportY: 0.1 + handViewDistance: 4 + handViewMaxWidth: 1.4 + handViewScale: 0.5 handViewRoll: 3.2 handViewArc: 0.02 guardianRowGap: 0.55 @@ -48,9 +48,9 @@ MonoBehaviour: guardianMaxWidth: 3.6 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 + cameraPosition: {x: 1.4, y: 5.8, z: -8} + cameraTarget: {x: 1.4, y: 0, z: 0} + cameraFov: 46 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 1ee6b13..7a0a5ee 100644 --- a/client/Assets/Vanguard/Runtime/Board3D/Config/PlaymatLayoutConfig.cs +++ b/client/Assets/Vanguard/Runtime/Board3D/Config/PlaymatLayoutConfig.cs @@ -53,11 +53,11 @@ namespace Vanguard.Board3D [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.18f; + [Range(0.05f, 0.45f)] public float handViewportY = 0.10f; [Tooltip("Расстояние от камеры до веера руки, метры.")] - public float handViewDistance = 3.8f; - public float handViewMaxWidth = 1.8f; - public float handViewScale = 0.62f; + public float handViewDistance = 4.0f; + public float handViewMaxWidth = 1.4f; + public float handViewScale = 0.5f; public float handViewRoll = 3.2f; public float handViewArc = 0.02f; @@ -74,9 +74,9 @@ namespace Vanguard.Board3D [Header("Камера")] [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 Vector3 cameraPosition = new Vector3(1.4f, 5.8f, -8.0f); + public Vector3 cameraTarget = new Vector3(1.4f, 0f, 0f); + public float cameraFov = 46f; [Header("Палитра")] public Color tableColor = new Color(0.045f, 0.04f, 0.055f); diff --git a/client/Assets/Vanguard/Runtime/Board3D/Logic/PlaymatLayout.cs b/client/Assets/Vanguard/Runtime/Board3D/Logic/PlaymatLayout.cs index 117a5db..67293e0 100644 --- a/client/Assets/Vanguard/Runtime/Board3D/Logic/PlaymatLayout.cs +++ b/client/Assets/Vanguard/Runtime/Board3D/Logic/PlaymatLayout.cs @@ -78,14 +78,14 @@ namespace Vanguard.Board3D public Vector3 DeckPosition(int localSeat) { var x = (localSeat == 0 ? 1f : -1f) * _config.pileX; - var z = ZSign(localSeat) * (_config.backRowZ + 0.15f); + 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.frontRowZ + 0.05f); + var z = ZSign(localSeat) * (_config.backRowZ + 0.15f); return new Vector3(x, _config.pileLift, z); } diff --git a/client/Assets/Vanguard/Runtime/Board3D/View/BoardMeshLibrary.cs b/client/Assets/Vanguard/Runtime/Board3D/View/BoardMeshLibrary.cs index dd8e516..7c44c4c 100644 --- a/client/Assets/Vanguard/Runtime/Board3D/View/BoardMeshLibrary.cs +++ b/client/Assets/Vanguard/Runtime/Board3D/View/BoardMeshLibrary.cs @@ -45,21 +45,13 @@ namespace Vanguard.Board3D new Vector3(halfW, 0f, halfH), new Vector3(-halfW, 0f, halfH), }, - uv = front - ? new[] - { - new Vector2(0f, 0f), - new Vector2(1f, 0f), - new Vector2(1f, 1f), - new Vector2(0f, 1f), - } - : new[] - { - new Vector2(0f, 1f), - new Vector2(1f, 1f), - new Vector2(1f, 0f), - new Vector2(0f, 0f), - }, + uv = new[] + { + new Vector2(0f, 0f), + new Vector2(1f, 0f), + new Vector2(1f, 1f), + new Vector2(0f, 1f), + }, normals = new[] { normal, normal, normal, normal }, triangles = new[] { diff --git a/client/Assets/Vanguard/Runtime/Board3D/View/GameBoard3D.cs b/client/Assets/Vanguard/Runtime/Board3D/View/GameBoard3D.cs index 2e9d1b0..09e190c 100644 --- a/client/Assets/Vanguard/Runtime/Board3D/View/GameBoard3D.cs +++ b/client/Assets/Vanguard/Runtime/Board3D/View/GameBoard3D.cs @@ -79,6 +79,7 @@ namespace Vanguard.Board3D public event Action SlotClicked; public event Action CardDropped; public event Action PileClicked; + public event Action PileRightClicked; public static GameBoard3D Create(Transform parent, PlaymatLayoutConfig config = null) { @@ -276,7 +277,9 @@ namespace Vanguard.Board3D new Vector3(width - 0.3f, 0.012f, 0.055f), divider); - var railMaterial = BoardMaterials.CreateLit(_config.lineColor, 0.9f); + var railMaterial = BoardMaterials.CreateUnlit( + null, + new Color(0.40f, 0.29f, 0.11f)); CreateBox( "Rail Left", new Vector3(-(_config.boardHalfWidth + 1.02f), 0.04f, 0f), @@ -809,9 +812,27 @@ namespace Vanguard.Board3D card.Tick(delta); } + HandleRightClick(); HandlePointer(); } + private void HandleRightClick() + { + var mouse = Mouse.current; + if (mouse == null + || !mouse.rightButton.wasPressedThisFrame + || IsPointerOverUi()) + { + return; + } + + var ray = GetRay(mouse.position.ReadValue()); + if (!TryRaycast(ray, out _, out _, out var pile) || pile == null) + return; + + PileRightClicked?.Invoke(pile.Ref); + } + private void HandlePointer() { if (!_hasRendered && _view == null) diff --git a/client/Assets/Vanguard/Runtime/Board3D/View/Pile3D.cs b/client/Assets/Vanguard/Runtime/Board3D/View/Pile3D.cs index 2085870..8d4f63f 100644 --- a/client/Assets/Vanguard/Runtime/Board3D/View/Pile3D.cs +++ b/client/Assets/Vanguard/Runtime/Board3D/View/Pile3D.cs @@ -102,7 +102,7 @@ namespace Vanguard.Board3D _top.transform.localPosition = new Vector3( 0f, - (layers - 1) * thickness + 0.0015f, + ((layers - 0.5f) * thickness) + 0.0015f, 0f); if (topTexture != null) diff --git a/client/Assets/Vanguard/Runtime/UI/BattleView.cs b/client/Assets/Vanguard/Runtime/UI/BattleView.cs index 10d0b6e..d50605a 100644 --- a/client/Assets/Vanguard/Runtime/UI/BattleView.cs +++ b/client/Assets/Vanguard/Runtime/UI/BattleView.cs @@ -40,8 +40,6 @@ namespace Vanguard.UI private readonly Button _concede; private readonly Text _concedeLabel; private readonly Button _reconnect; - private Button _soulCounter; - private Text _soulCounterLabel; private readonly RectTransform _result; private RectTransform _logContent; private Text _resultText; @@ -113,12 +111,6 @@ namespace Vanguard.UI UiKit.MakeLiteral(_topInfo); UiKit.SetPreferredHeight(_topInfo, 38f); - _soulCounter = UiKit.CreateButton( - top, "SoulCounter", "Душа: 0", OnSoulCounterClicked, 16); - _soulCounterLabel = _soulCounter.GetComponentInChildren(); - var soulElement = UiKit.SetPreferredHeight(_soulCounter, 44f, 0f); - soulElement.preferredWidth = 132f; - _clock = UiKit.CreateText( top, "Clock", "—", 22, Color.white, TextAnchor.MiddleRight); UiKit.SetPreferredHeight(_clock, 44f, 0f); @@ -198,6 +190,7 @@ namespace Vanguard.UI _board3D.SlotClicked += OnBoard3DSlotClicked; _board3D.CardDropped += OnBoard3DCardDropped; _board3D.PileClicked += OnBoard3DPileClicked; + _board3D.PileRightClicked += OnBoard3DPileRightClicked; _board3D.SetVisible(false); _root.gameObject.SetActive(false); @@ -235,8 +228,6 @@ namespace Vanguard.UI if (_soulOverlay != null) _soulOverlay.gameObject.SetActive(false); _topInfo.text = (info.DeckName ?? string.Empty) + " · " + info.MatchId; - _soulCounterLabel.text = "Душа: 0"; - _soulCounter.interactable = false; if (_selfStatsText != null) _selfStatsText.text = string.Empty; if (_opponentStatsText != null) @@ -779,7 +770,6 @@ namespace Vanguard.UI _board3D?.SetSelfPlayer(_view.You); _board3D?.Render(_view, BuildBoardRenderState()); - UpdateSoulCounter(); UpdateStatsOverlay(); RebuildIntents(_view); @@ -917,7 +907,7 @@ namespace Vanguard.UI break; case BoardZone.Soul: - ShowSoul(card.Player); + SetStatus("Душа открывается правой кнопкой мыши.", Muted); break; case BoardZone.Guardian: @@ -998,29 +988,29 @@ namespace Vanguard.UI OnIntentClicked(intent); } - private void OnSoulCounterClicked() + private void OnBoard3DPileRightClicked(BoardCardRef pile) { - if (_view != null) - ShowSoul(_view.You); - } - - private void UpdateSoulCounter() - { - if (_soulCounterLabel == null) + if (_view == null) return; - var self = _view?.Players.FirstOrDefault(player => player.Index == _view.You); - _soulCounterLabel.text = "Душа: " + (self?.Soul?.Count ?? 0); - _soulCounter.interactable = _view != null && !_view.IsFinished; + if (pile.Zone == BoardZone.Soul) + ShowSoul(pile.Player); } private void OnBoard3DPileClicked(BoardCardRef pile) - { if (_view == null) + { + if (_view == null) return; if (pile.Zone == BoardZone.Soul) { - ShowSoul(pile.Player); + SetStatus("Душа открывается правой кнопкой мыши.", Muted); + return; + } + + if (pile.Zone == BoardZone.Drop) + { + ShowDrop(pile.Player); return; } @@ -1029,13 +1019,7 @@ namespace Vanguard.UI return; if (pile.Zone == BoardZone.Deck) - { SetStatus("В колоде " + player.DeckCount + " карт.", Muted); - } - else if (pile.Zone == BoardZone.Drop) - { - SetStatus("В дропе " + (player.Drop?.Count ?? 0) + " карт.", Muted); - } } private void BuildStatsOverlays() @@ -1157,26 +1141,43 @@ namespace Vanguard.UI private void ShowSoul(int playerIndex) { var player = _view?.Players.FirstOrDefault(item => item.Index == playerIndex); - if (player == null || _soulOverlay == null) + if (player == null) return; - _soulTitle.text = "Душа · " + player.Name + " · " + (player.Soul?.Count ?? 0); + ShowCardList("Душа", player.Name, player.Soul); + } + + private void ShowDrop(int playerIndex) + { + var player = _view?.Players.FirstOrDefault(item => item.Index == playerIndex); + if (player == null) + return; + + ShowCardList("Дроп", player.Name, player.Drop); + } + + private void ShowCardList(string title, string playerName, List cards) + { + if (_soulOverlay == null) + return; + + _soulTitle.text = title + " · " + playerName + " · " + (cards?.Count ?? 0); ClearChildren(_soulContent); - if (player.Soul == null || player.Soul.Count == 0) + if (cards == null || cards.Count == 0) { var empty = UiKit.CreateText( _soulContent, - "SoulEmpty", - "В душе нет карт.", + "CardListEmpty", + "Нет карт.", 17, Muted); UiKit.SetPreferredHeight(empty, 80f); } else { - for (var i = player.Soul.Count - 1; i >= 0; i--) - AddSoulRow(player.Soul[i]); + for (var i = cards.Count - 1; i >= 0; i--) + AddSoulRow(cards[i]); } _soulOverlay.SetAsLastSibling(); diff --git a/client/Assets/Vanguard/Tests/Editor/PlaymatLayoutTests.cs b/client/Assets/Vanguard/Tests/Editor/PlaymatLayoutTests.cs index fa2cc5c..c5fe03e 100644 --- a/client/Assets/Vanguard/Tests/Editor/PlaymatLayoutTests.cs +++ b/client/Assets/Vanguard/Tests/Editor/PlaymatLayoutTests.cs @@ -80,6 +80,21 @@ namespace Vanguard.Tests Assert.IsTrue(hand.All(position => position.z < -4f)); } + [Test] + public void Deck_Is_Closer_Than_Drop_After_Swap() + { + var config = PlaymatLayoutConfig.CreateDefault(); + var layout = new PlaymatLayout(config); + + var deck = layout.DeckPosition(0); + var drop = layout.DropPosition(0); + Assert.Greater(deck.z, drop.z); + + var opponentDeck = layout.DeckPosition(1); + var opponentDrop = layout.DropPosition(1); + Assert.Less(opponentDeck.z, opponentDrop.z); + } + [Test] public void Drop_Target_Finds_Own_And_Opponent_Circles() { diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 5765b08..fa4b588 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -858,21 +858,26 @@ Battle; Boot и общие сервисы остаются персистент - `GameBoard3D` строит стол, круги, зоны, стопки и веер руки; raycast-клики, drag&drop на плоскость стола с подъёмом, подсветкой допустимых кругов и индикатором точки броска. Ошибка интента возвращает карту на место. -- Камера фиксирована над полем: позиция `(1.4, 9, -5.2)`, цель - `(1.4, 0, -0.1)`, FOV 50; игрок её не двигает. Рука не лежит на столе, +- Камера фиксирована над полем: позиция `(1.4, 5.8, -8)`, цель + `(1.4, 0, 0)`, FOV 46; игрок её не двигает. Рука не лежит на столе, а закреплена в экране снизу по центру (`handViewportX/Y`, - `handViewDistance`) и всегда развёрнута лицом к игроку; драг из руки - уходит на стол. + `handViewDistance`) и всегда развёрнута лицом к игроку; уменьшена, чтобы + не перекрывать задний ряд RC. Драг из руки уходит на стол. - Статистика карт вынесена в отдельные UI-панели: своя — снизу слева, - противника — сверху справа (`HAND/DECK/DROP/DMG/SOUL`); 3D-подписи + противника — сверху справа (`Рука/Колода/Дроп/Урон/Душа`); 3D-подписи стопок и зон больше не дублируют числа. +- Колода и дроп поменяны местами: колода ближе к игроку. Верх дропа + показывает последнюю положенную карту, ЛКМ открывает список всего дропа. + Душа открывается только ПКМ по стопке под V; кнопка души из топ-бара + убрана. - Душа: клик по стопке или кнопка «Душа: n» в верхней панели открывает оверлей со списком карт. - Песочница: `Assets/Vanguard/Scenes/GameBoard3D.unity` + `PlaymatSandbox`; собирается `Vanguard → Rebuild Playmat Sandbox`, проверяется `SceneSmoke.RunPlaymat`. -- Проверки: EditMode **18/18** (геометрия, зеркальность, правила, рендер - 3D-карт, пул, рука в поле зрения), `SCENE_SMOKE success=True`, +- Проверки: EditMode **19/19** (геометрия, зеркальность, правила, рендер + 3D-карт, пул, рука в поле зрения, обмен колоды/дропа), + `SCENE_SMOKE success=True`, `PLAYMAT_SMOKE success=True cards=20 hand=5/5 fieldVisible=True`. - Ограничения: retire/ghost-анимации старого 2D-поля пока не перенесены; модели карт/стола — процедурные примитивы. diff --git a/docs/SESSION-STATE.md b/docs/SESSION-STATE.md index 3af10d2..fd4c178 100644 --- a/docs/SESSION-STATE.md +++ b/docs/SESSION-STATE.md @@ -67,17 +67,19 @@ урон лежит на боку против часовой; закрытость — переворот вокруг X. Рубашка — стандартная VF Vanguard (`Resources/CardArt/vanguard_back.png`), общая для 2D и 3D. Рука закреплена в поле зрения камеры снизу по центру - и всегда смотрит на игрока, а не лежит на столе. Камера фиксирована над - полем под небольшим углом и не двигается игроком. Драг-н-дроп на - плоскость стола с подсветкой целей и плавным снапом. + и всегда смотрит на игрока, а не лежит на столе; уменьшена, чтобы не + закрывать задний ряд RC. Камера фиксирована над полем под небольшим + углом и не двигается игроком. Драг-н-дроп на плоскость стола с + подсветкой целей и плавным снапом. - Поле: у каждого игрока два ряда `R V R` / `R R R` (V + 5 RC), у противника зеркально; между полями общий круг защиты; душа — стопка под V; колода, дроп и урон — 3D-стопки/карты. Локальный игрок всегда снизу, даже если он за вторым местом. Карты в кругах V/R принудительно показываются лицом вверх. - Статистика карт — отдельное UI-наложение: своя панель снизу слева, - противника — сверху справа. Кнопка «Душа: n» в верхней панели и клик по - стопке души открывают список карт души. + противника — сверху справа. Душа открывается только ПКМ по стопке под V; + дроп показывает верхнюю карту, по ЛКМ — список всех карт дропа; колода и + дроп поменяны местами (колода ближе к игроку). - Зона урона: карты лежат открытыми и на боку; после Counter Blast выбранная карта переворачивается рубашкой. Heal выбирает карту урона; Counter Blast — открытую карту до активации способности. @@ -137,10 +139,11 @@ curl http://127.0.0.1:18080/health -executeMethod Vanguard.Editor.SceneSmoke.RunPlaymat ``` -Клиентские проверки: **18/18** EditMode (сборка всех экранов, импорт арта, +Клиентские проверки: **19/19** EditMode (сборка всех экранов, импорт арта, перевод статических/динамических строк, live-переключение языка, рендер 3D-доски с уроном и отдыхающей картой, геометрия поля, правила -перемещения, рука в поле зрения камеры), `SCENE_SMOKE success=True`, +перемещения, рука в поле зрения камеры, обмен колоды/дропа), +`SCENE_SMOKE success=True`, `PLAYMAT_SMOKE success=True cards=20 hand=5/5 fieldVisible=True`. `SceneSmoke` заменяет PlayMode-тест загрузки сцен: Single-загрузка сцены